durable_streams-rails 0.5.2 → 0.6.0
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 +4 -4
- data/README.md +82 -0
- data/lib/durable_streams/rails/server_config.rb +54 -4
- data/lib/durable_streams/rails/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7536abdf74ca0e1333cc080be34b9007965a698c79c8f6275d5062a20e1cba9a
|
|
4
|
+
data.tar.gz: 4b6a41ec2819f411f2f8b4fe7da3682fa93aaf31553d056d688c8456ce7005ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f63473d9d5239b786233367f1d47e404646f57c89baf78854ae1bf84cafef0ea7e0e678e7bfa423b1259d8642dd7523096eee893245c2a286489a9484cffed9
|
|
7
|
+
data.tar.gz: 5740d145eba781c339e451fa6d15767064d5140e852ee61be435d618c9f6ff5dc71dbc6876f3f15ee44288243962893158659627baacbbf3f1173491fe40748a
|
data/README.md
CHANGED
|
@@ -83,6 +83,88 @@ production:
|
|
|
83
83
|
data_dir: /var/data/durable-streams
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
### Multi-server with internal listener
|
|
87
|
+
|
|
88
|
+
In production, you typically want browsers to reach the stream server through a CDN
|
|
89
|
+
(`client_url`), but Rails broadcasts should go directly over the private network
|
|
90
|
+
(`server_url`). The `internal` option adds a plain HTTP listener for server-to-server
|
|
91
|
+
traffic:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
production:
|
|
95
|
+
domain: streams.example.com
|
|
96
|
+
tls:
|
|
97
|
+
cert: /etc/caddy/certs/cert.pem
|
|
98
|
+
key: /etc/caddy/certs/key.pem
|
|
99
|
+
internal:
|
|
100
|
+
port: 4437
|
|
101
|
+
bind: 10.0.0.5 # Only listen on the private interface.
|
|
102
|
+
allowed_ips: # Caddy remote_ip matcher — 403 for everyone else.
|
|
103
|
+
- 10.0.0.4
|
|
104
|
+
auth:
|
|
105
|
+
url: https://10.0.0.4 # Connect to Rails over private VNET.
|
|
106
|
+
host: app.example.com # Host header + TLS SNI for the Kamal proxy.
|
|
107
|
+
path: /durable_streams/auth/verify
|
|
108
|
+
copy_headers:
|
|
109
|
+
- Cookie
|
|
110
|
+
- Authorization
|
|
111
|
+
storage:
|
|
112
|
+
data_dir: /data/streams
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
When `auth.host` is set and the URL is HTTPS, the generated Caddyfile adds
|
|
116
|
+
`header_up Host`, `tls_server_name`, and `tls_insecure_skip_verify` to the auth
|
|
117
|
+
reverse proxy — same pattern as the internal listener's loopback connection. Use this
|
|
118
|
+
when the auth URL is an internal IP and the Kamal proxy routes by Host header.
|
|
119
|
+
|
|
120
|
+
#### Request flow
|
|
121
|
+
|
|
122
|
+
A server-to-server write (e.g., Rails broadcasting a message) follows this path:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Rails (10.0.0.4)
|
|
126
|
+
│
|
|
127
|
+
│ PUT http://10.0.0.5:4437/v1/streams/{name}
|
|
128
|
+
│ (plain HTTP over private VNET — no TLS, no CDN)
|
|
129
|
+
│
|
|
130
|
+
▼
|
|
131
|
+
:4437 listener (Caddy on 10.0.0.5)
|
|
132
|
+
│
|
|
133
|
+
│ 1. remote_ip check: is it 10.0.0.4? → proceed (otherwise 403)
|
|
134
|
+
│
|
|
135
|
+
│ 2. reverse_proxy to https://127.0.0.1:443
|
|
136
|
+
│ Caddy connects to itself over loopback TLS:
|
|
137
|
+
│
|
|
138
|
+
│ header_up Host streams.example.com
|
|
139
|
+
│ → Host header so :443 matches the right server block
|
|
140
|
+
│
|
|
141
|
+
│ tls_server_name streams.example.com
|
|
142
|
+
│ → SNI in the TLS ClientHello so Caddy selects the right cert
|
|
143
|
+
│
|
|
144
|
+
│ tls_insecure_skip_verify
|
|
145
|
+
│ → Skip cert verification (origin certs aren't in the system
|
|
146
|
+
│ trust store; safe on loopback — nothing to MITM)
|
|
147
|
+
│
|
|
148
|
+
▼
|
|
149
|
+
streams.example.com :443 listener (same Caddy process)
|
|
150
|
+
│
|
|
151
|
+
│ reverse_proxy → app.example.com (validates Bearer token) - this is "forward_auth" expanded
|
|
152
|
+
│ durable_streams → writes to bbolt store
|
|
153
|
+
│
|
|
154
|
+
▼
|
|
155
|
+
Response flows back up the chain to Rails
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Client reads (browsers) go directly to `streams.example.com:443` through the CDN,
|
|
159
|
+
bypassing the internal listener entirely.
|
|
160
|
+
|
|
161
|
+
#### Why reverse_proxy instead of a second `durable_streams`?
|
|
162
|
+
|
|
163
|
+
The stream storage engine (bbolt) is single-writer — two handler instances can't open
|
|
164
|
+
the same database file. The second one deadlocks waiting for the write lock. The
|
|
165
|
+
internal listener avoids this by proxying to the main listener over loopback, so only
|
|
166
|
+
one `durable_streams` handler exists process-wide.
|
|
167
|
+
|
|
86
168
|
Power users can bypass the YAML entirely:
|
|
87
169
|
|
|
88
170
|
```bash
|
|
@@ -36,7 +36,15 @@ module DurableStreams
|
|
|
36
36
|
<%- end -%>
|
|
37
37
|
@allowed remote_ip <%= internal_allowed_ips.join(" ") %>
|
|
38
38
|
handle @allowed {
|
|
39
|
-
|
|
39
|
+
reverse_proxy <%= upstream_address %> {
|
|
40
|
+
<%- if upstream_tls? -%>
|
|
41
|
+
header_up Host <%= domain %>
|
|
42
|
+
transport http {
|
|
43
|
+
tls_server_name <%= domain %>
|
|
44
|
+
tls_insecure_skip_verify
|
|
45
|
+
}
|
|
46
|
+
<%- end -%>
|
|
47
|
+
}
|
|
40
48
|
}
|
|
41
49
|
respond 403
|
|
42
50
|
}
|
|
@@ -110,9 +118,27 @@ module DurableStreams
|
|
|
110
118
|
t = "\t" * indent
|
|
111
119
|
lines = []
|
|
112
120
|
lines << "#{t}route #{route_path} {"
|
|
113
|
-
lines << "#{t}\
|
|
114
|
-
lines << "#{t}\
|
|
115
|
-
|
|
121
|
+
lines << "#{t}\t# Forward auth — verify request with Rails before reaching durable_streams"
|
|
122
|
+
lines << "#{t}\treverse_proxy #{auth["url"]} {"
|
|
123
|
+
lines << "#{t}\t\tmethod GET"
|
|
124
|
+
lines << "#{t}\t\trewrite #{auth["path"]}"
|
|
125
|
+
lines << "#{t}\t\theader_up Host #{auth_host}" if auth_host
|
|
126
|
+
lines << "#{t}\t\theader_up X-Forwarded-Method {method}"
|
|
127
|
+
lines << "#{t}\t\theader_up X-Forwarded-Uri {uri}"
|
|
128
|
+
if auth_tls_transport?
|
|
129
|
+
lines << "#{t}\t\ttransport http {"
|
|
130
|
+
lines << "#{t}\t\t\ttls_server_name #{auth_host}"
|
|
131
|
+
lines << "#{t}\t\t\ttls_insecure_skip_verify"
|
|
132
|
+
lines << "#{t}\t\t}"
|
|
133
|
+
end
|
|
134
|
+
lines << ""
|
|
135
|
+
lines << "#{t}\t\t@ok status 200"
|
|
136
|
+
lines << "#{t}\t\thandle_response @ok {"
|
|
137
|
+
copy_headers.each { |h| lines << "#{t}\t\t\trequest_header #{h} {rp.header.#{h}}" }
|
|
138
|
+
lines << "#{t}\t\t}"
|
|
139
|
+
lines << "#{t}\t\thandle_response {"
|
|
140
|
+
lines << "#{t}\t\t\trespond 401"
|
|
141
|
+
lines << "#{t}\t\t}"
|
|
116
142
|
lines << "#{t}\t}"
|
|
117
143
|
|
|
118
144
|
directives = storage_directives
|
|
@@ -140,6 +166,14 @@ module DurableStreams
|
|
|
140
166
|
auth.fetch("copy_headers", [ "Cookie" ])
|
|
141
167
|
end
|
|
142
168
|
|
|
169
|
+
def auth_host
|
|
170
|
+
auth["host"]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def auth_tls_transport?
|
|
174
|
+
auth_host && auth["url"].start_with?("https://")
|
|
175
|
+
end
|
|
176
|
+
|
|
143
177
|
def storage_directives
|
|
144
178
|
storage = @config.fetch("storage", {})
|
|
145
179
|
STORAGE_DIRECTIVES.filter_map { |key| "#{key} #{storage[key]}" if storage[key] }
|
|
@@ -152,6 +186,22 @@ module DurableStreams
|
|
|
152
186
|
def internal_allowed_ips
|
|
153
187
|
internal&.dig("allowed_ips")
|
|
154
188
|
end
|
|
189
|
+
|
|
190
|
+
def upstream_address
|
|
191
|
+
if upstream_tls?
|
|
192
|
+
"https://127.0.0.1:443"
|
|
193
|
+
else
|
|
194
|
+
"http://127.0.0.1:#{@config.fetch("port", 4437)}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def upstream_tls?
|
|
199
|
+
tls_config || auto_https?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def domain
|
|
203
|
+
@config["domain"]
|
|
204
|
+
end
|
|
155
205
|
end
|
|
156
206
|
end
|
|
157
207
|
end
|