-
Notifications
You must be signed in to change notification settings - Fork 9
/
main.go
210 lines (174 loc) · 6.51 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package main
import (
"errors"
"log"
"net"
"os"
"path"
"strings"
"time"
"golang.org/x/crypto/ssh"
"github.com/dtylman/scp"
)
const (
// DirectionUpload specifies an upload of local files to a remote target.
DirectionUpload = "upload"
// DirectionDownload specifies the download of remote files to a local target.
DirectionDownload = "download"
)
type copyFunc func(client *ssh.Client, source string, target string) (int64, error)
func main() {
// Parse timeout.
actionTimeout, err := time.ParseDuration(os.Getenv("ACTION_TIMEOUT"))
if err != nil {
log.Fatalf("❌ Failed to parse action timeout: %v", err)
}
// Stop the action if it takes longer that the specified timeout.
actionTimeoutTimer := time.NewTimer(actionTimeout)
go func() {
<-actionTimeoutTimer.C
log.Fatalf("❌ Failed to run action: %v", errors.New("action timed out"))
os.Exit(1)
}()
// Parse direction.
direction := os.Getenv("DIRECTION")
if direction != DirectionDownload && direction != DirectionUpload {
log.Fatalf("❌ Failed to parse direction: %v", errors.New("direction must be either upload or download"))
}
// Parse timeout.
timeout, err := time.ParseDuration(os.Getenv("TIMEOUT"))
if err != nil {
log.Fatalf("❌ Failed to parse timeout: %v", err)
}
// Parse target host.
targetHost := os.Getenv("HOST")
if targetHost == "" {
log.Fatalf("❌ Failed to parse target host: %v", errors.New("target host must not be empty"))
}
// Create configuration for SSH target.
targetConfig := &ssh.ClientConfig{
Timeout: timeout,
User: os.Getenv("USERNAME"),
Auth: ConfigureAuthentication(os.Getenv("KEY"), os.Getenv("PASSPHRASE"), os.Getenv("INSECURE_PASSWORD")),
HostKeyCallback: ConfigureHostKeyCallback(os.Getenv("FINGERPRINT"), os.Getenv("INSECURE_IGNORE_FINGERPRINT")),
}
// Configure target address.
targetAddress := os.Getenv("HOST") + ":" + os.Getenv("PORT")
// Initialize target SSH client.
var targetClient *ssh.Client
// Check if a proxy should be used.
if proxyHost := os.Getenv("PROXY_HOST"); proxyHost != "" {
// Create SSH config for SSH proxy.
proxyConfig := &ssh.ClientConfig{
Timeout: timeout,
User: os.Getenv("PROXY_USERNAME"),
Auth: ConfigureAuthentication(os.Getenv("PROXY_KEY"), os.Getenv("PROXY_PASSPHRASE"), os.Getenv("INSECURE_PROXY_PASSWORD")),
HostKeyCallback: ConfigureHostKeyCallback(os.Getenv("PROXY_FINGERPRINT"), os.Getenv("INSECURE_PROXY_IGNORE_FINGERPRINT")),
}
// Establish SSH session to proxy host.
proxyAddress := proxyHost + ":" + os.Getenv("PROXY_PORT")
proxyClient, err := ssh.Dial("tcp", proxyAddress, proxyConfig)
if err != nil {
log.Fatalf("❌ Failed to connect to proxy: %v", err)
}
defer proxyClient.Close()
// Create a TCP connection to from the proxy host to the target.
netConn, err := proxyClient.Dial("tcp", targetAddress)
if err != nil {
log.Fatalf("❌ Failed to dial to target: %v", err)
}
targetConn, channel, req, err := ssh.NewClientConn(netConn, targetAddress, targetConfig)
if err != nil {
log.Fatalf("❌ Failed to connect to target: %v", err)
}
targetClient = ssh.NewClient(targetConn, channel, req)
} else {
if targetClient, err = ssh.Dial("tcp", targetAddress, targetConfig); err != nil {
log.Fatalf("❌ Failed to connect to target: %v", err)
}
}
defer targetClient.Close()
Copy(targetClient)
}
// Copy transfers files between remote host and local machine.
func Copy(client *ssh.Client) {
sourceFiles := strings.Split(os.Getenv("SOURCE"), "\n")
targetFileOrFolder := strings.TrimSpace(os.Getenv("TARGET"))
direction := os.Getenv("DIRECTION")
var copy copyFunc
var emoji string
if direction == DirectionDownload {
copy = scp.CopyFrom
emoji = "🔽"
}
if direction == DirectionUpload {
copy = scp.CopyTo
emoji = "🔼"
}
log.Printf("%s %sing ...\n", emoji, strings.Title(direction))
if len(sourceFiles) == 1 {
// Rename file if there is only one source file.
if _, err := copy(client, sourceFiles[0], targetFileOrFolder); err != nil {
log.Fatalf("❌ Failed to %s file from remote: %v", os.Getenv("DIRECTION"), err)
}
log.Println("📑 " + sourceFiles[0] + " >> " + targetFileOrFolder)
log.Println("📡 Transferred 1 file")
} else {
transferredFiles := int64(0)
for _, sourceFile := range sourceFiles {
_, file := path.Split(sourceFile)
targetFile := path.Join(targetFileOrFolder, file)
if _, err := copy(client, sourceFile, targetFile); err != nil {
log.Fatalf("❌ Failed to %s file from remote: %v", os.Getenv("DIRECTION"), err)
}
log.Println("📑 " + sourceFile + " >> " + targetFile)
transferredFiles += 1
}
log.Printf("📡 Transferred %d files\n", transferredFiles)
}
}
// ConfigureAuthentication configures the authentication method.
func ConfigureAuthentication(key string, passphrase string, password string) []ssh.AuthMethod {
// Create signer for public key authentication method.
auth := make([]ssh.AuthMethod, 1)
if key != "" {
var err error
var targetSigner ssh.Signer
if passphrase != "" {
targetSigner, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(passphrase))
} else {
targetSigner, err = ssh.ParsePrivateKey([]byte(key))
}
if err != nil {
log.Fatalf("❌ Failed to parse private key: %v", err)
}
// Configure public key authentication.
auth[0] = ssh.PublicKeys(targetSigner)
} else if password != "" {
// Fall back to password authentication.
auth[0] = ssh.Password(password)
log.Println("⚠️ Using a password for authentication is insecure!")
log.Println("⚠️ Please consider using public key authentication!")
} else {
log.Fatal("❌ Failed to configure authentication method: missing credentials")
}
return auth
}
// ConfigureHostKeyCallback configures the SSH host key verification callback.
// Unless the `skip` option is set to `string("true")` it will return a function,
// which verifies the host key against the specified ssh key fingerprint.
func ConfigureHostKeyCallback(expected string, skip string) ssh.HostKeyCallback {
if skip == "true" {
log.Println("⚠️ Skipping host key verification is insecure!")
log.Println("⚠️ This allows for person-in-the-middle attacks!")
log.Println("⚠️ Please consider using host key verification!")
return ssh.InsecureIgnoreHostKey()
}
return func(hostname string, remote net.Addr, pubKey ssh.PublicKey) error {
fingerprint := ssh.FingerprintSHA256(pubKey)
if fingerprint != expected {
return errors.New("fingerprint mismatch: server fingerprint: " + fingerprint)
}
return nil
}
}