vagrant-share 0.0.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,500 @@
1
+ module VagrantPlugins
2
+ module Share
3
+ module Command
4
+ # Ngrok specific implementation
5
+ module Ngrok
6
+ # Ngrok share implementation
7
+ module Share
8
+ # Guest port within proxy
9
+ GUEST_PROXY_PORT = 31338
10
+
11
+ # Start the ngrok based share
12
+ #
13
+ # @param [Array<String>] argv CLI arguments
14
+ # @param [Hash] options CLI options
15
+ def start_share(argv, options)
16
+ validate_ngrok_installation!
17
+
18
+ # Define variables to ensure availability within ensure block
19
+ # Set this here so they're available in our ensure block
20
+ vagrant_port = nil
21
+ share_machine = nil
22
+ share_api = nil
23
+ share_api_runner = nil
24
+ proxy_runner = nil
25
+ output_runner = nil
26
+ port_file = nil
27
+ ui = nil
28
+ share_info_output = Queue.new
29
+ configuration = {
30
+ "tunnels" => {}
31
+ }
32
+
33
+ begin
34
+ with_target_vms(argv, single_target: true) do |machine|
35
+ ui = machine.ui
36
+ machine.ui.output(I18n.t("vagrant_share.detecting"))
37
+
38
+ target = validate_target_machine(machine, options) || "127.0.0.1"
39
+
40
+ restrict = false
41
+ ports = []
42
+
43
+ machine.ui.detail("Local machine address: #{target}")
44
+ if target == "127.0.0.1"
45
+ machine.ui.detail(" \n" + I18n.t(
46
+ "vagrant_share.local_address_only",
47
+ provider: machine.provider_name.to_s,
48
+ ) + "\n ")
49
+
50
+ # Restrict the ports that can be accessed since we're
51
+ # on localhost.
52
+ restrict = true
53
+ ports = Helper.forwarded_ports(machine).keys
54
+ end
55
+
56
+ if !options[:http_port] && !options[:disable_http]
57
+ begin
58
+ @logger.debug("No HTTP port set. Auto-detection will be attempted.")
59
+ # Always target localhost when using ngrok
60
+ detect_ports!(options, "127.0.0.1", machine)
61
+ options[:https_port] = nil if options[:disable_https]
62
+ rescue Errors::DetectHTTPForwardedPortFailed,
63
+ Errors::DetectHTTPCommonPortFailed
64
+ # If SSH isn't enabled, raise the errors. If SSH is enabled,
65
+ # then we can ignore that HTTP is unavailable.
66
+ raise if !options[:ssh]
67
+
68
+ machine.ui.detail(I18n.t("vagrant_share.ssh_no_http") + "\n ")
69
+ end
70
+ end
71
+
72
+ if options[:ssh]
73
+ options[:ssh_username], options[:ssh_port] = configure_ssh_share(machine, "127.0.0.1", options)
74
+ options[:ssh_password], options[:ssh_privkey] = configure_ssh_connect(machine, configuration, options)
75
+ end
76
+
77
+ machine.ui.detail(
78
+ "Local HTTP port: #{options[:http_port] || "disabled"}")
79
+ machine.ui.detail(
80
+ "Local HTTPS port: #{options[:https_port] || "disabled"}")
81
+ if options[:ssh]
82
+ machine.ui.detail("SSH Port: #{options[:ssh_port]}")
83
+ end
84
+ if restrict
85
+ ports.each do |port|
86
+ machine.ui.detail("Port: #{port}")
87
+ end
88
+ end
89
+
90
+ machine.ui.output(I18n.t("vagrant_share.creating"))
91
+
92
+ if options[:http_port]
93
+ configuration["tunnels"]["http"] = {
94
+ "proto" => "http",
95
+ "bind_tls" => false,
96
+ "addr" => options[:http_port]
97
+ }
98
+ end
99
+
100
+ if options[:https_port]
101
+ configuration["tunnels"]["https"] = {
102
+ "proto" => "tls",
103
+ "addr" => options[:https_port]
104
+ }
105
+ end
106
+
107
+ if options[:full_share] || (options[:ssh] && !options[:ssh_no_password])
108
+ configuration["tunnels"].delete("ssh")
109
+
110
+ if options[:full_share]
111
+ options[:shared_ports] = ports
112
+ else
113
+ options[:shared_ports] = [options[:ssh_port]]
114
+ end
115
+
116
+ @logger.debug("Starting local Vagrant API")
117
+
118
+ share_api = setup_share_api(machine)
119
+
120
+ options[:vagrant_api_port] = share_api.listeners.first.addr[1]
121
+ proxy_port, port_file = Helper.acquire_port(@env)
122
+
123
+ @logger.debug("Local Vagrant API is listening on port `#{vagrant_port}`")
124
+ @logger.debug("Local port for proxy forwarding: `#{proxy_port}`")
125
+ configuration["tunnels"]["proxy"] = {
126
+ "proto" => "tcp",
127
+ "addr" => proxy_port
128
+ }
129
+ share_machine = Helper.share_machine(@env, port: {guest: GUEST_PROXY_PORT, host: proxy_port}, name: "share")
130
+
131
+ ui = share_machine.ui
132
+ proxy_ui = share_machine.ui.dup
133
+ proxy_ui.opts[:bold] = false
134
+ proxy_ui.opts[:prefix_spaces] = true
135
+ port_forwards = target ? options[:shared_ports] : []
136
+ port_forwards << options[:vagrant_api_port]
137
+
138
+ @logger.debug("Starting share proxy VM")
139
+ share_machine.with_ui(proxy_ui) do
140
+ share_machine.action(:up)
141
+ share_machine.guest.capability(:share_proxy,
142
+ proxy: GUEST_PROXY_PORT,
143
+ forwards: port_forwards,
144
+ target: target
145
+ )
146
+ end
147
+ end
148
+ end
149
+
150
+ if share_api
151
+ @logger.debug("Starting internal Vagrant API")
152
+ share_api_runner = Thread.new{ share_api.start }
153
+ end
154
+ output_runner = start_connect_info_watcher(share_info_output, ui, options)
155
+ ngrok_process = start_ngrok_proxy(ui, configuration, share_info_output, options)
156
+
157
+ # Allow user to halt the share process and
158
+ # proxy VM via ctrl-c
159
+ Helper.signal_retrap("INT") do
160
+ ui.warn("Halting Vagrant share!")
161
+ ngrok_process.stop
162
+ share_api.stop if share_api
163
+ share_info_output.push(nil)
164
+ end
165
+ ensure
166
+ if port_file
167
+ port_file.close
168
+ File.delete(port_file) rescue nil
169
+ end
170
+ output_runner.join if output_runner
171
+ share_api_runner.join if share_api_runner
172
+ if share_machine
173
+ share_machine.action(:destroy, force_confirm_destroy: true)
174
+ end
175
+ end
176
+ end
177
+
178
+ # Start the ngrok proxy process
179
+ #
180
+ # @param [Vagrant::UI] ui UI instance for output
181
+ # @param [Hash] configurations ngrok process configuration
182
+ # @param [Queue] share_info_output location to push share information
183
+ # @param [Hash] options CLI options
184
+ def start_ngrok_proxy(ui, configuration, share_info_output, options)
185
+ ngrok_process = nil
186
+ base_config = File.expand_path("~/.ngrok2/ngrok.yml")
187
+ share_config = Tempfile.new("vagrant-share")
188
+ share_config.write(configuration.to_yaml)
189
+ share_config.close
190
+ if !File.exists?(base_config)
191
+ base_config = share_config.path
192
+ end
193
+ @logger.debug("Generated configuration for ngrok:\n#{configuration.to_yaml}")
194
+ @logger.debug("Starting ngrok proxy process.")
195
+
196
+ ngrok_process = Vagrant::Util::Subprocess.new(
197
+ *["ngrok", "start", "--config", base_config, "--config", share_config.path,
198
+ "--all", "--log", "stdout", "--log-format", "json", "--log-level", "debug"],
199
+ notify: [:stdout]
200
+ )
201
+
202
+ Thread.new do
203
+ begin
204
+ share_info = {}
205
+ share_info_keys = []
206
+ share_info_keys.push(:http) if options[:http_port]
207
+ share_info_keys.push(:https) if options[:https_port]
208
+ share_info_keys.push(:name) if options[:ssh] || options[:full_share]
209
+
210
+ ngrok_process.execute do |type, data|
211
+ if type == :stdout
212
+ data.split("\n").each do |line|
213
+ begin
214
+ info = JSON.parse(line)
215
+ if info["msg"].to_s == "decoded response"
216
+ begin
217
+ r_info = info["resp"]
218
+ if !r_info["Error"].to_s.empty?
219
+ @logger.error("Error encountered with ngrok connection: #{r_info["Error"]}")
220
+ share_info_output.push(r_info["Error"])
221
+ Process.kill("INT", Process.pid)
222
+ end
223
+
224
+ if r_info["URL"] && r_info["Proto"]
225
+ share_info[:uri] = URI.parse(r_info["URL"])
226
+ case share_info[:uri].scheme
227
+ when "http"
228
+ share_info[:http] = share_info[:uri].to_s
229
+ when "https"
230
+ share_info[:https] = share_info[:uri].to_s
231
+ when "tcp"
232
+ connect_name = [share_info[:uri].port, options[:vagrant_api_port]].map do |item|
233
+ Helper.wordify(item).join('_')
234
+ end
235
+ share_info[:tcp] = share_info[:uri].to_s
236
+ share_info[:name] = connect_name.join(":")
237
+ if share_info[:uri].host != DEFAULT_NGROK_TCP_ENDPOINT
238
+ host_num = share_info[:uri].host.split(".").first
239
+ host_num_word = Helper.wordify(host_num.to_i).join("_")
240
+ share_info[:name] += "@#{host_num_word}"
241
+ end
242
+ else
243
+ @logger.warn("Unhandled URI scheme detected: #{share_info[:uri].scheme} - `#{share_info[:uri]}`")
244
+ share_info.delete(:uri)
245
+ end
246
+ end
247
+ rescue => err
248
+ @logger.warn("Failed to parse line: #{err}")
249
+ end
250
+ end
251
+ if info["err"] && info["msg"] == "start tunnel listen" && info["err"] != "<nil>"
252
+ @logger.error("Error encountered with ngrok connection: #{info["err"]}")
253
+ share_info_output.push(info["err"])
254
+ # Force shutdown
255
+ Process.kill("INT", Process.pid)
256
+ end
257
+ if share_info_keys.all?{|key| share_info.keys.include?(key)}
258
+ share_info_output.push(share_info.dup)
259
+ share_info = {}
260
+ end
261
+ rescue => e
262
+ @logger.warn("Failure handling ngrok process output line: #{e.class} - #{e} (`#{line}`)")
263
+ end
264
+ end
265
+ end
266
+ end
267
+ ensure
268
+ share_config.unlink
269
+ end
270
+ end
271
+ ngrok_process
272
+ end
273
+
274
+ # Start the share information watcher to print connect instructions
275
+ #
276
+ # @param [Queue] share_info_output Queue to receive share information
277
+ # @param [Vagrant::UI] ui UI for output
278
+ # @param [Hash] options CLI options
279
+ def start_connect_info_watcher(share_info_output, ui, options)
280
+ Thread.new do
281
+ until((info = share_info_output.pop).nil?)
282
+ begin
283
+ case info
284
+ when String
285
+ ui.error(info)
286
+ when Hash
287
+ if info[:name]
288
+ i_uri = URI.parse(info[:tcp])
289
+ if i_uri.host != DEFAULT_NGROK_TCP_ENDPOINT
290
+ driver_name = i_uri.host
291
+ else
292
+ driver_name = "ngrok"
293
+ end
294
+ ui.success("")
295
+ ui.success(I18n.t("vagrant_share.started", name: info[:name]))
296
+ if options[:full_share]
297
+ ui.success("")
298
+ ui.success(I18n.t("vagrant_share.ngrok.started_full", name: info[:name], driver: driver_name))
299
+ end
300
+ if options[:ssh]
301
+ ui.success("")
302
+ ui.success(I18n.t("vagrant_share.ngrok.started_ssh", name: info[:name], driver: driver_name))
303
+ end
304
+ ui.success("")
305
+ end
306
+ if info[:http]
307
+ ui.success("HTTP URL: #{info[:http]}")
308
+ ui.success("")
309
+ end
310
+ if info[:https]
311
+ ui.success("HTTPS URL: #{info[:https]}")
312
+ ui.success("")
313
+ end
314
+ else
315
+ @logger.warn("Unknown data type receied for output: #{e.class} - #{e}")
316
+ end
317
+ rescue => e
318
+ @logger.error("Unexpected error processing connect information: #{e.class} - #{e}")
319
+ end
320
+ end
321
+ end
322
+ end
323
+
324
+ # Validate the target machine for the share
325
+ #
326
+ # @param [Vagrant::Machine] machine
327
+ # @param [Hash] options
328
+ # @return [String, NilClass] public address
329
+ def validate_target_machine(machine, options)
330
+ if !machine.ssh_info
331
+ # We use this as a flag of whether or not the machine is
332
+ # running. We can't share a machine that is not running.
333
+ raise Errors::MachineNotReady
334
+ end
335
+
336
+ if options[:ssh]
337
+ # Do some quick checks to make sure we can setup this
338
+ # machine for SSH access from other users.
339
+ begin
340
+ if !machine.guest.capability?(:insert_public_key)
341
+ raise Errors::SSHCantInsertKey,
342
+ guest: machine.guest.name.to_s
343
+ end
344
+ rescue Vagrant::Errors::MachineGuestNotReady
345
+ raise Errors::SSHNotReady
346
+ end
347
+ end
348
+
349
+ target = nil
350
+ if !machine.provider.capability?(:public_address)
351
+ machine.ui.warn(I18n.t(
352
+ "vagrant_share.provider_unsupported",
353
+ provider: machine.provider_name.to_s,
354
+ ))
355
+ else
356
+ target = machine.provider.capability(:public_address)
357
+ end
358
+ target
359
+ end
360
+
361
+ # Configure settings for SSH sharing
362
+ #
363
+ # @param [Vagrant::Machine] machine
364
+ # @param [Hash] options
365
+ # @return [Array<String>] ssh username, ssh port
366
+ def configure_ssh_share(machine, target, options)
367
+ ssh_username = nil
368
+ ssh_port = options[:ssh_port] if options[:ssh] && options[:ssh_port]
369
+ if options[:ssh]
370
+ ssh_info = machine.ssh_info
371
+ raise Errors::SSHNotReady if !ssh_info
372
+
373
+ ssh_username = ssh_info[:username]
374
+ if !ssh_port
375
+ ssh_port = ssh_info[:port]
376
+ if ssh_info[:host] == "127.0.0.1" && target != "127.0.0.1"
377
+ # Since we're targetting ourselves, the port probably
378
+ # points to a forwarded port. Look it up.
379
+ ssh_port = Helper.guest_forwarded_port(machine, ssh_port)
380
+
381
+ if !ssh_port
382
+ raise Errors::SSHPortNotDetected
383
+ end
384
+ end
385
+
386
+ if target == "127.0.0.1" && ssh_info[:host] != "127.0.0.1"
387
+ # The opposite case now. We're proxying to localhost, but
388
+ # the SSH port is NOT on localhost. We need to look for
389
+ # a host forwarded port.
390
+ guest_port = ssh_port
391
+ ssh_port = Helper.host_forwarded_port(machine, guest_port)
392
+
393
+ if !ssh_port
394
+ raise Errors::SSHHostPortNotDetected,
395
+ guest_port: guest_port.to_s
396
+ end
397
+ end
398
+ end
399
+ end
400
+ [ssh_username, ssh_port]
401
+ end
402
+
403
+ # Configure settings for SSH connect
404
+ #
405
+ # @param [Vagrant::Machine] machine Machine to share
406
+ # @param [Hash] configuration ngrok configuration hash
407
+ # @param [Hash] options CLI options
408
+ # @param [Array<String>] ssh password, ssh privkey
409
+ def configure_ssh_connect(machine, configuration, options)
410
+ ssh_password = nil
411
+ ssh_privkey = nil
412
+ machine.ui.output(I18n.t("vagrant_share.generating_ssh_key"))
413
+
414
+ if !options[:ssh_no_password]
415
+ while !ssh_password
416
+ ssh_password = machine.ui.ask(
417
+ "#{I18n.t("vagrant_share.ssh_password_prompt")} ",
418
+ echo: false)
419
+ end
420
+
421
+ while ssh_password.length < 4
422
+ machine.ui.warn(
423
+ "#{I18n.t("vagrant_share.password_not_long_enough")}")
424
+ ssh_password = machine.ui.ask(
425
+ "#{I18n.t("vagrant_share.ssh_password_prompt")} ",
426
+ echo: false)
427
+ end
428
+
429
+ confirm_password = nil
430
+ while confirm_password != ssh_password
431
+ confirm_password = machine.ui.ask(
432
+ "#{I18n.t("vagrant_share.ssh_password_confirm_prompt")} ",
433
+ echo: false)
434
+ end
435
+ else
436
+ configuration["tunnels"]["ssh"] = {
437
+ "proto" => "tcp",
438
+ "addr" => ssh_port
439
+ }
440
+ end
441
+
442
+ _, ssh_privkey, openssh_key = Helper.generate_keypair(ssh_password)
443
+
444
+ machine.ui.detail(I18n.t("vagrant_share.inserting_ssh_key"))
445
+ machine.guest.capability(:insert_public_key, openssh_key)
446
+ [ssh_password, ssh_privkey]
447
+ end
448
+
449
+ # Setup the local share API
450
+ #
451
+ # @param [Vagrant::Machine] machine
452
+ # @return [WEBrick::HTTPServer]
453
+ def setup_share_api(machine)
454
+ share_api = Helper::Api.start_api(machine) do |api|
455
+ api.mount_proc("/ping") do |req, res|
456
+ res.status = 200
457
+ res.body = {message: "pong"}.to_json
458
+ end
459
+ api.mount_proc("/share-info") do |req, res|
460
+ res.status = 200
461
+ res.body = {
462
+ ports: options[:shared_ports],
463
+ has_private_key: !!options[:ssh_privkey],
464
+ private_key_password: !options[:ssh_no_password],
465
+ ssh_username: options[:ssh_username],
466
+ ssh_port: options[:ssh_port]
467
+ }.to_json
468
+ end
469
+ api.mount_proc("/shared-ports") do |req, res|
470
+ res.body = {ports: options[:shared_ports]}.to_json
471
+ res.status = 200
472
+ end
473
+ api.mount_proc("/connect-ssh") do |req, res|
474
+ res.body = {
475
+ ssh_username: options[:ssh_username],
476
+ ssh_port: options[:ssh_port],
477
+ ssh_key: options[:ssh_privkey],
478
+ has_private_key: !!options[:ssh_privkey],
479
+ private_key_password: !options[:ssh_no_password]
480
+ }.to_json
481
+ res.status = 200
482
+ end
483
+ end
484
+ end
485
+
486
+ # Check that ngrok is available on user's PATH
487
+ def validate_ngrok_installation!
488
+ begin
489
+ Vagrant::Util::Subprocess.new("ngrok")
490
+ rescue Vagrant::Errors::CommandUnavailable
491
+ raise Errors::NgrokUnavailable
492
+ end
493
+ end
494
+ end
495
+ end
496
+ end
497
+ end
498
+ end
499
+
500
+ Thread.abort_on_exception = true