vagrant-share 0.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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