aboutsummaryrefslogtreecommitdiff
path: root/pkgs/yossh/yoshi.lua
blob: ce2086a6edd21b3740b1e2e50be4c9607d3bcb3f (plain)
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