rb-portless 0.4.0 → 0.4.1.dev.20260707.17213cc

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: 33ab8aad78892a8c3a4889fa0b5c7b27b26871e6db726b7e59213786890c0eb0
4
- data.tar.gz: b6e68f4074485b5d8cb37814164bffba5f4f3a3e120a33bc5d46e66fe1d9502c
3
+ metadata.gz: 9c5231d955992c5d9c1dc235bce13b8ee6c5d50852147e50c8da632f1918c8a4
4
+ data.tar.gz: 60856b5d93cc1016c5a1fdde83ec692a681f1c060518c0437ffd9d642df190cf
5
5
  SHA512:
6
- metadata.gz: 95fb0005242dee8b657e2cf129905214ba5fbc1785b84ba358dbb31e4cee8c578320b3e3086624b1faf22122ac1a6654ee8281ead950d81f26fd54d8ab70197c
7
- data.tar.gz: de6f31af583cbe7307fbbd876361e681db21382d89362c51f23cf58d52f69b3334dd0788e11aefa04863e8b3d7375f41071d2c9f69828b0d873e456517f55ebd
6
+ metadata.gz: 3b391f24ff703e076ad3fb1df430024773cfbf1fb6acc9309ed8eb68d9395041db9e48521e5afd3a029fad80075ed6754c1f86a3ad7ab7fa033c5665a1be2443
7
+ data.tar.gz: 327c09c0c578019c00e0fc9f92513ad335f7faaf8408075d34003d2db2b37ba93571097149a009397ec89cf04de4d3b86a5ec03a6bb0ac43606010fe091f742a
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/), and the project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.4.1]
8
+
9
+ ### Fixed
10
+
11
+ - **Proxied apps could no longer verify public TLS certificates.** `run` set the
12
+ child's `SSL_CERT_FILE` to our local CA so the app would trust sibling
13
+ `.localhost` hosts — but that env var *replaces* OpenSSL's trust store rather
14
+ than extending it, so the app lost every public root and all outbound HTTPS
15
+ (payment APIs, S3, webhooks, exchange-rate feeds…) failed with `certificate
16
+ verify failed (unable to get local issuer certificate)`. `run` now hands apps
17
+ a combined bundle (the system's default roots **plus** our CA), assembled once
18
+ into `~/.rb-portless/ca-bundle.pem` and rebuilt when either input changes, so
19
+ apps trust the public web *and* our local hosts. Falls back to leaving
20
+ `SSL_CERT_FILE` unset when the CA or system roots can't be located.
21
+
7
22
  ## [0.4.0]
8
23
 
9
24
  ### Added
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "fileutils"
5
+
6
+ module Portless
7
+ # The CA bundle handed to proxied apps via SSL_CERT_FILE.
8
+ #
9
+ # SSL_CERT_FILE *replaces* OpenSSL's trust file rather than adding to it, so
10
+ # pointing an app at our local CA alone makes it distrust every public
11
+ # certificate — Frankfurter, Stripe, S3, GitHub — and all outbound HTTPS from
12
+ # the dev server fails with "unable to get local issuer certificate". We
13
+ # instead assemble a combined bundle (the system's default roots + our CA) and
14
+ # point apps at that, so they trust the public web *and* sibling .localhost
15
+ # hosts served under our CA.
16
+ module CaBundle
17
+ # Well-known system trust stores, tried when OpenSSL's compiled-in default
18
+ # isn't readable (varies by distro / OpenSSL build).
19
+ FALLBACK_ROOTS = [
20
+ "/etc/ssl/cert.pem", # macOS, OpenBSD
21
+ "/etc/ssl/certs/ca-certificates.crt", # Debian, Ubuntu, Alpine
22
+ "/etc/pki/tls/certs/ca-bundle.crt", # Fedora, RHEL
23
+ "/etc/ssl/ca-bundle.pem" # openSUSE
24
+ ].freeze
25
+
26
+ module_function
27
+
28
+ # Absolute path to the combined bundle, (re)building it when missing or older
29
+ # than either input. Returns nil when our CA doesn't exist yet or assembly
30
+ # fails, so the caller simply omits the SSL_CERT_FILE override (apps keep
31
+ # their own defaults) rather than shipping a broken trust store.
32
+ def path
33
+ return nil unless File.exist?(State.ca_cert)
34
+
35
+ bundle = bundle_path
36
+ rebuild(bundle) if stale?(bundle)
37
+ File.exist?(bundle) ? bundle : nil
38
+ rescue StandardError
39
+ nil
40
+ end
41
+
42
+ def bundle_path = State.path("ca-bundle.pem")
43
+
44
+ def stale?(bundle)
45
+ return true unless File.exist?(bundle)
46
+
47
+ inputs = [ State.ca_cert, system_roots ].compact.select { |f| File.exist?(f) }
48
+ inputs.empty? || inputs.any? { |f| File.mtime(f) > File.mtime(bundle) }
49
+ end
50
+
51
+ def rebuild(bundle)
52
+ State.ensure_dir!
53
+ pems = []
54
+ roots = system_roots
55
+ pems << File.read(roots) if roots && File.exist?(roots)
56
+ pems << File.read(State.ca_cert)
57
+
58
+ # Atomic write: concurrent app starts may both rebuild; a temp file + rename
59
+ # keeps any reader from seeing a half-written bundle.
60
+ tmp = "#{bundle}.#{Process.pid}.tmp"
61
+ File.write(tmp, "#{pems.join("\n").strip}\n")
62
+ File.rename(tmp, bundle)
63
+ State.fix_ownership(bundle)
64
+ end
65
+
66
+ # The public root store OpenSSL would use by default, so our bundle is a
67
+ # superset of it. An operator-set SSL_CERT_FILE wins (but never our own
68
+ # bundle, which would read itself in circles).
69
+ def system_roots
70
+ env = ENV["SSL_CERT_FILE"].to_s
71
+ return env if !env.empty? && env != bundle_path && File.exist?(env)
72
+
73
+ default = OpenSSL::X509::DEFAULT_CERT_FILE
74
+ return default if default && File.exist?(default)
75
+
76
+ FALLBACK_ROOTS.find { |f| File.exist?(f) }
77
+ end
78
+ end
79
+ end
@@ -12,8 +12,10 @@ module Portless
12
12
  "PORT" => port.to_s,
13
13
  "HOST" => "127.0.0.1",
14
14
  "PORTLESS_URL" => url,
15
- # Let the app's own server-side TLS verification trust our CA.
16
- "SSL_CERT_FILE" => (File.exist?(State.ca_cert) ? State.ca_cert : nil)
15
+ # Let the app's own server-side TLS verification trust our CA — via a
16
+ # bundle that *also* carries the public roots, so SSL_CERT_FILE replacing
17
+ # the trust store doesn't break the app's outbound HTTPS. See CaBundle.
18
+ "SSL_CERT_FILE" => CaBundle.path
17
19
  }.compact
18
20
  # Our own bundle (rb-portless is loaded via the app's Bundler binstub) must
19
21
  # not leak into the dev command — a foreman-style `bin/dev` isn't in the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Portless
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1.dev.20260707.17213cc"
5
5
  end
data/lib/rb-portless.rb CHANGED
@@ -15,6 +15,7 @@ require_relative "portless/health"
15
15
  require_relative "portless/privilege"
16
16
  require_relative "portless/hosts"
17
17
  require_relative "portless/certs"
18
+ require_relative "portless/ca_bundle"
18
19
  require_relative "portless/trust"
19
20
  require_relative "portless/proxy"
20
21
  require_relative "portless/daemon"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rb-portless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1.dev.20260707.17213cc
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Afonso
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-04 00:00:00.000000000 Z
11
+ date: 2026-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -57,6 +57,7 @@ files:
57
57
  - README.md
58
58
  - exe/rb-portless
59
59
  - lib/portless/banner.rb
60
+ - lib/portless/ca_bundle.rb
60
61
  - lib/portless/certs.rb
61
62
  - lib/portless/cli.rb
62
63
  - lib/portless/colors.rb