rb-portless 0.3.0.dev.20260630.fb5fc36 → 0.3.0.dev.20260630.7305b58

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad245d30c0c47ba8ac0be15c5483524cedc6d2a6fffd30b4df35ad0904287af6
4
- data.tar.gz: 64e421e608545dc03fa7ad75506cd473ae86cece15b90bc38395c03fa8db0bba
3
+ metadata.gz: 77efce157b70a56f16f260b04bfad49a83b55d1aa1c5367b8b24f4d596ce1541
4
+ data.tar.gz: 17f9a98ff52119a61fc2d770e7c3df451b3bdc39547f897f0e432b7b9fb2b57d
5
5
  SHA512:
6
- metadata.gz: cf6f98facf2bc05067f832f1f865b18977a00b9cbd608189289d5c89284a2b887f1d056cbcf3c664ef5a5a4762808e30dea9c1b8f582e430750562ce8b3c3b4b
7
- data.tar.gz: '02658fcdf8c2ad85e58a3d5cf71148dcbec8819adb2d0bc439ec06cb44aad599d5e84a0055804ce910374f72012720e758ed418a7ded1e25033f57760f433d13'
6
+ metadata.gz: f4e976fc65b4887516767d64b8d273066e8b03cfccebca66e762e473d8f77b6a441dd78b24ff4fe3a5485628e845ce904e8898a75e0e4db80e5d3852b9767869
7
+ data.tar.gz: e320411a2f3104a3ab3ab4f5c61cd7f41c423d4978a0e4b001fbf67554c008c7ea396da8950a0cd09928facaa192cb518d16cda6f97391b87a8f5387f7483b41
data/CHANGELOG.md CHANGED
@@ -6,20 +6,8 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [0.3.0]
8
8
 
9
- ### Fixed / hardened
10
-
11
- - **Health probes can't hang.** Added a read timeout to the TLS and plain probes
12
- so a port that accepts but never answers no longer blocks `discover_port`.
13
-
14
9
  ### Added
15
10
 
16
- - **Risky-TLD warning.** Warn when the configured `tld` ends in a real/reserved
17
- TLD (`dev`, `app`, `local`, …) that could intercept live traffic.
18
- - **More tests** — health probes and privilege logic. (Verified manually, since
19
- the Async proxy can't be driven in-process without deadlock: HTTP/HTTPS/HTTP-2
20
- forwarding, wildcard routing, and the **WebSocket upgrade relay** / ActionCable.)
21
-
22
-
23
11
  - **Startup banner.** Running a dev server through rb-portless now prints a clear
24
12
  banner with the named URL(s) it's reachable at — not just `127.0.0.1:port`.
25
13
  - **Monorepo / multi-app.** A `portless.json` `apps` map runs several apps under
@@ -28,13 +16,7 @@ All notable changes to this project are documented here. The format follows
28
16
  detects the LAN IP, registers `<name>.local`, and publishes it over mDNS
29
17
  (`dns-sd`/`avahi-publish`). `--ip` overrides the detected address.
30
18
  - **Public sharing (experimental).** `--ngrok`, `--tailscale`, `--funnel` expose
31
- the app via ngrok / your tailnet (their CLIs, installed separately). When a
32
- tool is missing or unconfigured, print an **actionable** message (install link,
33
- `ngrok config add-authtoken`, "enable HTTPS in your tailnet DNS settings",
34
- "run `tailscale up`") rather than failing silently — mirroring portless. Tailscale is **non-destructive**: it reads
35
- `tailscale serve status`, picks a free HTTPS port (never clobbering your
36
- existing serve/funnel config), registers with `--yes`, and removes only the
37
- port it created on exit — mirroring portless's port-conflict handling.
19
+ the app via ngrok / your tailnet. Degrade gracefully when the tool is absent.
38
20
 
39
21
  ## [0.2.0]
40
22
 
data/README.md CHANGED
@@ -95,17 +95,7 @@ rb-portless run --ngrok bin/dev # https://xxxx.ngrok.app
95
95
  rb-portless run --tailscale bin/dev # your-machine.tailnet.ts.net
96
96
  rb-portless run --funnel bin/dev # tailscale Funnel (public)
97
97
  ```
98
-
99
- These use the tools' own CLIs — **install them separately** (we don't bundle
100
- anything, same as portless), and each degrades gracefully if the tool is absent:
101
- - **ngrok** — install the `ngrok` CLI and configure an authtoken (free account).
102
- - **tailscale** — install `tailscale`, be logged into a tailnet, and enable HTTPS
103
- certificates (and Funnel, for `--funnel`) in your tailnet settings.
104
-
105
- > **Your tailnet config is safe.** rb-portless reads `tailscale serve status`,
106
- > picks a **free** HTTPS port (never one you're already serving on), registers
107
- > with `--yes`, and on exit turns off **only the port it created** — it never
108
- > touches your existing `serve`/`funnel` setup.
98
+ Each degrades gracefully if the tool isn't installed.
109
99
 
110
100
  ## Commands
111
101
 
@@ -32,17 +32,6 @@ module Portless
32
32
  tld.split(".").include?(name) ? tld : "#{name}.#{tld}"
33
33
  end
34
34
 
35
- # Real/reserved TLDs that can intercept live traffic or clash with mDNS.
36
- RISKY_TLDS = %w[dev app page zip mov local].freeze
37
-
38
- # A warning string if the tld looks risky, else nil. (.localhost / .test are safe.)
39
- def tld_warning
40
- last = tld.split(".").last
41
- return unless RISKY_TLDS.include?(last)
42
-
43
- "tld \".#{last}\" is a real/reserved TLD — prefer \".localhost\" so you don't intercept real traffic"
44
- end
45
-
46
35
  def self.read_file(dir)
47
36
  json = File.join(dir, "portless.json")
48
37
  return JSON.parse(File.read(json)) if File.exist?(json)
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "socket"
4
4
  require "openssl"
5
- require "timeout"
6
5
 
7
6
  module Portless
8
7
  # "Is *our* proxy on this port?" — every proxied response carries the
@@ -27,8 +26,7 @@ module Portless
27
26
  ssl.sync_close = true
28
27
  ssl.connect
29
28
  ssl.write(REQUEST)
30
- # Read timeout too — a port that accepts but never answers must not hang us.
31
- marker?(Timeout.timeout(timeout) { ssl.read(4096) })
29
+ marker?(ssl.read(4096))
32
30
  rescue StandardError
33
31
  false
34
32
  ensure
@@ -40,7 +38,7 @@ module Portless
40
38
  Socket.tcp("127.0.0.1", port, connect_timeout: timeout) do |sock|
41
39
  sock.write(REQUEST)
42
40
  sock.close_write
43
- marker?(Timeout.timeout(timeout) { sock.read(4096) })
41
+ marker?(sock.read(4096))
44
42
  end
45
43
  rescue StandardError
46
44
  false
@@ -21,7 +21,6 @@ module Portless
21
21
  command = Frameworks.inject(command, port) # --port/--host for vite/astro/etc.
22
22
  hostname = @config.hostname
23
23
 
24
- warn "rb-portless: #{@config.tld_warning}" if @config.tld_warning
25
24
  ensure_trusted if @config.tls
26
25
  proxy_port = Daemon.ensure_running(tls: @config.tls)
27
26
  @route_store.add(hostname: hostname, port: port, pid: Process.pid, force: true)
@@ -16,10 +16,7 @@ module Portless
16
16
  API = "http://127.0.0.1:4040/api/tunnels"
17
17
 
18
18
  def start(hostname:, backend_port:)
19
- unless Portless.which("ngrok")
20
- warn "rb-portless: ngrok not found — install it (https://ngrok.com/download) to use --ngrok"
21
- return nil
22
- end
19
+ return nil unless Portless.which("ngrok")
23
20
 
24
21
  pid = Process.spawn("ngrok", "http", backend_port.to_s, "--host-header=#{hostname}",
25
22
  out: File::NULL, err: File::NULL)
@@ -29,11 +26,9 @@ module Portless
29
26
  { pid: pid, url: url }
30
27
  else
31
28
  stop(pid: pid)
32
- warn "rb-portless: ngrok didn't produce a public URL — is your authtoken set? (`ngrok config add-authtoken <token>`)"
33
29
  nil
34
30
  end
35
- rescue StandardError => e
36
- warn "rb-portless: ngrok failed (#{e.message})"
31
+ rescue StandardError
37
32
  nil
38
33
  end
39
34
 
@@ -5,121 +5,41 @@ require "json"
5
5
  module Portless
6
6
  module Share
7
7
  # Expose the local app on your tailnet (`serve`) or publicly (`funnel`) via
8
- # the tailscale CLI. Returns { mode:, port:, url: } or nil. EXPERIMENTAL.
9
- #
10
- # SAFETY (mirrors portless's tailscale.ts): we never clobber your existing
11
- # serve config — we read `tailscale serve status` for ports already in use
12
- # and pick the first FREE one from the preferred list, register with `--yes`
13
- # (no prompt), and on teardown turn off ONLY the port we registered.
8
+ # the tailscale CLI. Returns { mode:, url: } or nil. EXPERIMENTAL. Mirrors
9
+ # portless's tailscale.ts.
14
10
  module Tailscale
15
11
  module_function
16
12
 
17
- PREFERRED_SERVE_PORTS = [ 443, 8443, 8444, 8445, 8446, 8447, 8448, 8449, 8450 ].freeze
18
- FUNNEL_PORTS = [ 443, 8443, 10_000 ].freeze # Funnel supports only these
19
-
20
13
  def start(backend_port:, funnel: false)
21
- unless Portless.which("tailscale")
22
- warn "rb-portless: tailscale not found — install it (https://tailscale.com/download) to use --tailscale"
23
- return nil
24
- end
25
-
26
- status = status_json
27
- unless status
28
- warn "rb-portless: tailscale isn't connected — run `tailscale up`, then retry"
29
- return nil
30
- end
31
- unless capability?(status, "https")
32
- warn "rb-portless: tailscale HTTPS certs aren't enabled — turn on HTTPS in your tailnet DNS settings"
33
- return nil
34
- end
35
- if funnel && !capability?(status, "funnel")
36
- warn "rb-portless: tailscale Funnel isn't enabled for this node — enable it, then retry --funnel"
37
- return nil
38
- end
14
+ return nil unless Portless.which("tailscale")
39
15
 
40
16
  mode = funnel ? "funnel" : "serve"
41
- port = available_port(funnel: funnel)
42
- unless port
43
- warn "rb-portless: all tailscale Funnel ports are in use (443/8443/10000)"
44
- return nil
45
- end
46
-
47
- unless system("tailscale", mode, "--bg", "--yes", "--https=#{port}",
48
- "http://127.0.0.1:#{backend_port}", out: File::NULL, err: File::NULL)
49
- warn "rb-portless: `tailscale #{mode}` failed to register"
50
- return nil
51
- end
17
+ ok = system("tailscale", mode, "--bg", "--https=443", "http://127.0.0.1:#{backend_port}",
18
+ out: File::NULL, err: File::NULL)
19
+ return nil unless ok
52
20
 
53
- base = dns_name(status)
54
- return (off(mode, port) and nil) unless base
55
-
56
- { mode: mode, port: port, url: format_url(base, port) }
57
- rescue StandardError => e
58
- warn "rb-portless: tailscale sharing failed (#{e.message})"
21
+ url = magic_dns_url
22
+ url ? { mode: mode, url: url } : nil
23
+ rescue StandardError
59
24
  nil
60
25
  end
61
26
 
62
27
  def stop(handle)
63
28
  return unless handle && Portless.which("tailscale")
64
29
 
65
- off(handle[:mode], handle[:port])
66
- end
67
-
68
- # Turn off ONLY the registration we created (scoped to our port).
69
- def off(mode, port)
70
- system("tailscale", mode, "--yes", "--https=#{port}", "off", out: File::NULL, err: File::NULL)
30
+ system("tailscale", handle[:mode], "--https=443", "off", out: File::NULL, err: File::NULL)
71
31
  rescue StandardError
72
32
  nil
73
33
  end
74
34
 
75
- # First free HTTPS port from the preferred pool, never one already in use by
76
- # the user's existing serve/funnel config. nil if the funnel pool is full.
77
- def available_port(funnel:)
78
- used = used_serve_ports
79
- pool = funnel ? FUNNEL_PORTS : PREFERRED_SERVE_PORTS
80
- free = pool.find { |port| !used.include?(port) }
81
- return free if free
82
- return nil if funnel
83
-
84
- port = PREFERRED_SERVE_PORTS.last + 1
85
- port += 1 while used.include?(port)
86
- port
87
- end
88
-
89
- # HTTPS ports the user's current serve config already occupies.
90
- def used_serve_ports
91
- config = JSON.parse(`tailscale serve status --json 2>/dev/null`)
92
- ports = []
93
- (config["Web"] || {}).each_key { |host_port| ports << Regexp.last_match(1).to_i if host_port =~ /:(\d+)\z/ }
94
- (config["TCP"] || {}).each_key { |port| ports << port.to_i }
95
- ports
96
- rescue StandardError
97
- []
98
- end
99
-
100
- def status_json
35
+ # The machine's MagicDNS name → its tailnet HTTPS URL.
36
+ def magic_dns_url
101
37
  json = JSON.parse(`tailscale status --json 2>/dev/null`)
102
- json.is_a?(Hash) ? json : nil
38
+ dns = json.dig("Self", "DNSName").to_s.chomp(".")
39
+ dns.empty? ? nil : "https://#{dns}"
103
40
  rescue StandardError
104
41
  nil
105
42
  end
106
-
107
- def dns_name(status)
108
- dns = status.dig("Self", "DNSName").to_s.chomp(".")
109
- dns.empty? ? nil : "https://#{dns}"
110
- end
111
-
112
- # Does this node have the HTTPS / Funnel capability? (mirrors portless)
113
- def capability?(status, name)
114
- node = status["Self"] || {}
115
- names = Array(node["Capabilities"]) + (node["CapMap"] || {}).keys
116
- names.any? { |cap| down = cap.to_s.downcase; down == name || down.end_with?("/#{name}") }
117
- end
118
-
119
- def format_url(base, port)
120
- base = base.chomp("/")
121
- port == 443 ? base : "#{base}:#{port}"
122
- end
123
43
  end
124
44
  end
125
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Portless
4
- VERSION = "0.3.0.dev.20260630.fb5fc36"
4
+ VERSION = "0.3.0.dev.20260630.7305b58"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rb-portless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0.dev.20260630.fb5fc36
4
+ version: 0.3.0.dev.20260630.7305b58
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Afonso