Module: Portless::Share::Ngrok

Defined in:
lib/portless/share/ngrok.rb

Overview

Expose the local app publicly via ngrok. We point ngrok at the backend port directly (with the app's Host header) rather than tunnelling through our self-signed TLS proxy — simpler and avoids cert-trust issues. Returns { pid:, url: } or nil. EXPERIMENTAL. Mirrors portless's ngrok.ts.

Constant Summary collapse

API =
"http://127.0.0.1:4040/api/tunnels"

Class Method Summary collapse

Class Method Details

.fetch(url) ⇒ Object



62
63
64
65
66
# File 'lib/portless/share/ngrok.rb', line 62

def fetch(url)
  Net::HTTP.get(URI(url))
rescue StandardError
  nil
end

.poll_public_url(tries: 25) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/portless/share/ngrok.rb', line 47

def poll_public_url(tries: 25)
  tries.times do
    sleep 0.3
    body = fetch(API)
    next unless body

    tunnels = JSON.parse(body)["tunnels"] || []
    url = tunnels.map { |t| t["public_url"] }.compact.find { |u| u.start_with?("https") }
    return url if url
  end
  nil
rescue StandardError
  nil
end

.start(hostname:, backend_port:) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/portless/share/ngrok.rb', line 18

def start(hostname:, backend_port:)
  unless Portless.which("ngrok")
    warn "rb-portless: ngrok not found — install it (https://ngrok.com/download) to use --ngrok"
    return nil
  end

  pid = Process.spawn("ngrok", "http", backend_port.to_s, "--host-header=#{hostname}",
                      out: File::NULL, err: File::NULL)
  Process.detach(pid)

  if (url = poll_public_url)
    { pid: pid, url: url }
  else
    stop(pid: pid)
    warn "rb-portless: ngrok didn't produce a public URL — is your authtoken set? (`ngrok config add-authtoken <token>`)"
    nil
  end
rescue StandardError => e
  warn "rb-portless: ngrok failed (#{e.message})"
  nil
end

.stop(handle) ⇒ Object



40
41
42
43
44
45
# File 'lib/portless/share/ngrok.rb', line 40

def stop(handle)
  pid = handle.is_a?(Hash) ? handle[:pid] : handle
  Process.kill("TERM", pid) if pid
rescue StandardError
  nil
end