Module: Portless::RailsHosts

Defined in:
lib/portless/rails_hosts.rb

Overview

The host matchers to whitelist in Rails development, derived from the URL rb-portless run injects (PORTLESS_URL). Plain Ruby so it's testable without booting Rails; the Railtie is just glue around it.

Class Method Summary collapse

Class Method Details

.allowed(portless_url = ENV["PORTLESS_URL"], lan_host = ENV["PORTLESS_LAN_HOST"]) ⇒ Object

Empty unless we're actually running under rb-portless.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/portless/rails_hosts.rb', line 13

def allowed(portless_url = ENV["PORTLESS_URL"], lan_host = ENV["PORTLESS_LAN_HOST"])
  return [] if portless_url.to_s.empty?

  # Rails wraps a Regexp as /\A<re>(:port)?\z/, so match the whole host.
  patterns = [ /.+\.localhost/ ]

  host = begin
    URI(portless_url).host
  rescue StandardError
    nil
  end
  # A custom, non-.localhost tld too. A regexp, not a ".host" string —
  # Rails' leading-dot shorthand only matches a single subdomain label,
  # which would 403 a.b.myapp.test while a.b.myapp.localhost passes.
  patterns.push(host, /.+\.#{Regexp.escape(host)}/) if host && !host.end_with?(".localhost")
  # The `--lan` mDNS host (<name>.local) — not covered by any default.
  patterns.push(lan_host) unless lan_host.to_s.empty?
  # Public tunnels forward with their own Host (*.ts.net / *.ngrok.app).
  patterns.concat(share_hosts)
  patterns
end

.cable_origins(portless_url = , lan_host = ) ⇒ Object

Action Cable rejects a WebSocket whose Origin isn't in allowed_request_origins; under rb-portless that's the portless host and any of its subdomains (tenant hosts), over the portless scheme — not the localhost default. Empty unless we're running under rb-portless.



82
83
84
85
86
87
88
89
90
91
# File 'lib/portless/rails_hosts.rb', line 82

def cable_origins(portless_url = ENV["PORTLESS_URL"], lan_host = ENV["PORTLESS_LAN_HOST"])
  options = url_options(portless_url) or return []

  hosts = [ options[:host] ]
  hosts << lan_host unless lan_host.to_s.empty?
  hosts.concat(share_hosts)
  hosts.map do |host|
    %r{\A#{options[:protocol]}://([\w-]+\.)*#{Regexp.escape(host)}(:\d+)?\z}
  end
end

.merge_url_options(existing, options) ⇒ Object

default_url_options merge that can't leave a stale :port behind: when the portless URL has no explicit port (443/80), an app-configured port: 3000 must not survive into https://.localhost:3000 links.



52
53
54
55
56
# File 'lib/portless/rails_hosts.rb', line 52

def merge_url_options(existing, options)
  merged = existing.to_h.merge(options)
  merged.delete(:port) unless options.key?(:port)
  merged
end

.share_hosts(env = ENV) ⇒ Object

Hostnames of any active public tunnels (--tailscale / --ngrok), from the env the runner injects.



37
38
39
40
41
42
43
44
45
46
47
# File 'lib/portless/rails_hosts.rb', line 37

def share_hosts(env = ENV)
  [ env["PORTLESS_TAILSCALE_URL"], env["PORTLESS_NGROK_URL"] ].filter_map do |url|
    next if url.to_s.empty?

    begin
      URI(url).host
    rescue StandardError
      nil
    end
  end
end

.url_options(portless_url = ) ⇒ Object

The default_url_options (host + scheme) that mailers, jobs, and other request-less URL generation need so their links point at the portless URL instead of a bare localhost:<random-port>. Nil unless we're running under rb-portless. The standard 80/443 ports are dropped (portless serves without a port number); a custom one is carried through.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/portless/rails_hosts.rb', line 63

def url_options(portless_url = ENV["PORTLESS_URL"])
  return if portless_url.to_s.empty?

  uri = begin
    URI(portless_url)
  rescue StandardError
    nil
  end
  return unless uri&.host

  options = { host: uri.host, protocol: uri.scheme }
  options[:port] = uri.port if uri.port && ![ 80, 443 ].include?(uri.port)
  options
end