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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
|
local SSHTarget = {}
function SSHTarget:clone()
return {
user = self.user,
host = self.host,
port = self.port
}
end
local function parse_user_host_port(input)
local user, rest = input:match("^(.-)@(.*)$")
if not rest then rest = input end
local host, port_str = rest:match("^(.-):(%d+)$")
local port
if not host then
host = rest
else
port = tonumber(port_str)
end
return {
user = user,
host = host,
port = port,
}
end
local function split_path(path)
local t = {}
for seg in path:gmatch("[^/]+") do
table.insert(t, seg)
end
return t
end
local yoshi = {
hosts = {}
}
local SSHConfig = {}
local SSHCommand = {}
function SSHCommand:clone()
local new_proxy_jumps
if self.proxy_jumps then
new_proxy_jumps = {}
for _, j in ipairs(self.proxy_jumps) do
table.insert(new_proxy_jumps, {
user = j.user,
host = j.host,
port = j.port,
})
end
end
return setmetatable({
target = {
user = self.target.user,
host = self.target.host,
port = self.target.port,
},
session_type = self.session_type,
dynamic_forward = self.dynamic_forward,
local_forward = self.local_forward and { self.local_forward[1], self.local_forward[2] } or nil,
remote_command = self.remote_command,
proxy_jumps = new_proxy_jumps,
}, { __index = SSHCommand })
end
function SSHCommand:overlayTarget(target)
local new = self:clone()
if target.user then
new.target.user = target.user
end
if target.port then
new.target.port = target.port
end
return new
end
function yoshi.getHost(hostname)
local base = yoshi.hosts[hostname]
if not base then
error("Unknown host: " .. hostname)
end
local cmd = base()
if cmd then
return cmd
else
error("Calling host returned nil")
end
end
function yoshi.ssh(o)
local local_forward
local l = o.LocalForward
if type(l) == "number" then
local_forward = { l, l }
else
local_forward = l
end
local proxy_jumps = {}
if o.ProxyJump then
local u = parse_user_host_port(o.ProxyJump)
local jmp = yoshi.getHost(u.host):overlayTarget(u)
table.insert(proxy_jumps, jmp.target)
if #jmp.proxy_jumps ~= 0 then
for _, p in ipairs(jmp.proxy_jumps) do
table.insert(proxy_jumps, p)
end
end
end
local self = setmetatable({
target = {
user = o.User,
host = o.HostName,
port = o.Port or 22,
},
session_type = o.SessionType or "default",
dynamic_forward = o.DynamicForward,
local_forward = local_forward,
remote_command = o.RemoteCommand,
proxy_jumps = proxy_jumps,
}, { __index = SSHCommand })
return self
end
function SSHCommand:toCommand()
local ret = { "ssh" }
if self.dynamic_forward then
table.insert(ret, "-D")
table.insert(ret, tostring(self.dynamic_forward))
end
if self.local_forward then
table.insert(ret, "-L")
local lf = self.local_forward
table.insert(ret,
lf[1] .. ":127.0.0.1:" .. lf[2])
end
if self.session_type == "none" then
table.insert(ret, "-N")
end
if self.proxy_jumps and #self.proxy_jumps > 0 then
local js = {}
for _, j in ipairs(self.proxy_jumps) do
local s = (j.user and j.user .. "@" or "") .. j.host
if j.port and j.port ~= 22 then s = s .. ":" .. j.port end
table.insert(js, s)
end
table.insert(ret, "-J")
table.insert(ret, table.concat(js, ","))
end
if self.target.port and self.target.port ~= 22 then
table.insert(ret, "-p")
table.insert(ret, tostring(self.target.port))
end
table.insert(ret, (self.target.user and self.target.user .. "@" or "") .. self.target.host)
if self.remote_command then
table.insert(ret, "-t")
table.insert(ret, "'" .. self.remote_command .. "'")
end
return table.concat(ret, " ")
end
function SSHCommand:overlayCliOpts(cli_args)
local new = self:clone()
local i = 2
while i <= #cli_args do
local a = cli_args[i]
if a == "-D" then
new.dynamic_forward = tonumber(cli_args[i + 1])
i = i + 2
elseif a == "-L" then
local spec = cli_args[i + 1]
local port = tonumber(spec)
if port then
new.local_forward = { port, port }
i = i + 2
else
local l, r = spec:match("^(%d+):.*:(%d+)$")
if not l or not r then
error("Invalid -L spec: " .. spec)
end
new.local_forward = { tonumber(l), tonumber(r) }
i = i + 2
end
elseif a == "-N" then
new.session_type = "none"
i = i + 1
elseif a == "--" then
local parts = {}
for j = i + 1, #cli_args do
table.insert(parts, cli_args[j])
end
new.remote_command = table.concat(parts, " ")
break
else
error("Unknown or unsupported ssh option: " .. a)
end
end
return new
end
local function show_help()
print("Usage: ssh <[jumphost/...]host> [options] [-- command]")
print("\nAvailable hosts:")
for h, _ in pairs(yoshi.hosts) do
print(" - " .. h)
end
end
function yoshi.run(cli_opts, opts)
if not cli_opts then
error("Please pass `arg` to yoshi.run (ex. `yoshi.run(arg)`)")
end
if cli_opts[1] == nil then
show_help()
os.exit(0)
end
local path = split_path(cli_opts[1])
local destName = path[#path]
local target = yoshi.getHost(destName)
local acc = {}
if #path > 1 then
for i = 1, #path - 1 do
local hop = parse_user_host_port(path[i])
table.insert(acc, {
user = hop.user,
host = hop.host,
port = hop.port,
})
end
target.proxy_jumps = acc
end
local cmd = target:overlayCliOpts(cli_opts)
print("+ " .. cmd:toCommand())
if not opts or not opts.dry_run then
os.execute(cmd)
end
end
return yoshi
|