Class: Portless::Proxy

Inherits:
Object
  • Object
show all
Defined in:
lib/portless/proxy.rb

Overview

The reverse proxy daemon (async-http: HTTP/1.1 + TLS + WebSockets; HTTP/2 in phase 2). Routes by Host to a backend 127.0.0.1: from the route store — exact match, then wildcard .name — adds X-Forwarded-, stamps the X-Portless-Rb health header, guards against proxy loops, and re-reads routes.json per request so new apps appear without a restart. A sibling :80 listener 302-redirects to HTTPS. Mirrors portless's proxy.ts.

Constant Summary collapse

HOP_HEADER =
"x-rb-portless-hops"
HOP_BY_HOP =
%w[connection keep-alive proxy-authenticate proxy-authorization
te trailers transfer-encoding upgrade host].freeze

Instance Method Summary collapse

Constructor Details

#initialize(port:, tls: true, route_store: RouteStore.new, certs: Certs.new) ⇒ Proxy

Returns a new instance of Proxy.



22
23
24
25
26
27
28
29
# File 'lib/portless/proxy.rb', line 22

def initialize(port:, tls: true, route_store: RouteStore.new, certs: Certs.new)
  @port = port
  @tls = tls
  @route_store = route_store
  @certs = certs
  @clients = {}
  @host_contexts = {}
end

Instance Method Details

#call(request) ⇒ Object

The reverse-proxy app: resolve the request's host to a backend, forward it, stamp the health header. Public so it can be mounted in a test reactor (Async::HTTP::Server.for(endpoint, &proxy.method(:call))).



57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/portless/proxy.rb', line 57

def call(request)
  host = request_host(request)
  route = route_for(host)
  return error(404, "No app is registered for #{host}.") unless route

  hops = request.headers[HOP_HEADER].to_a.first.to_i
  return error(508, "Proxy loop detected for #{host}.") if hops >= Constants::MAX_PROXY_HOPS

  response = client_for(route.port).call(build_forward(request, host, hops))
  response.headers.add(Constants::HEALTH_HEADER, "1")
  response
rescue StandardError => e
  error(502, "Backend for #{host} is not responding (#{e.class}).")
end

#route_for(host) ⇒ Object

Exact host match, then wildcard fallback so *.name.localhost all reach the single app registered as name.localhost.



47
48
49
50
51
52
# File 'lib/portless/proxy.rb', line 47

def route_for(host)
  host = host.to_s.split(":").first.to_s.downcase
  routes = @route_store.routes
  routes.find { |r| r.hostname == host } ||
    routes.find { |r| host.end_with?(".#{r.hostname}") }
end

#runObject



31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/portless/proxy.rb', line 31

def run
  State.ensure_dir!
  @certs.ensure_ca! if @tls
  write_markers
  install_signal_handlers

  Async do
    make_server(listen_endpoint).run
    start_redirect_listener if @tls && @port != Constants::HTTP_PORT
  end
ensure
  cleanup
end