@rip-lang/server 1.3.125 → 1.4.1
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.
- package/{docs/READ_VALIDATORS.md → API.md} +41 -119
- package/CONFIG.md +408 -0
- package/README.md +246 -1109
- package/acme/crypto.rip +0 -2
- package/browse.rip +62 -0
- package/control/cli.rip +95 -36
- package/control/lifecycle.rip +67 -1
- package/control/manager.rip +250 -0
- package/control/mdns.rip +3 -0
- package/middleware.rip +1 -1
- package/package.json +14 -11
- package/server.rip +189 -673
- package/serving/config.rip +766 -0
- package/{edge → serving}/forwarding.rip +2 -2
- package/serving/logging.rip +101 -0
- package/{edge → serving}/metrics.rip +29 -1
- package/serving/proxy.rip +99 -0
- package/{edge → serving}/queue.rip +1 -1
- package/{edge → serving}/ratelimit.rip +1 -1
- package/{edge → serving}/realtime.rip +71 -2
- package/{edge → serving}/registry.rip +1 -1
- package/{edge → serving}/router.rip +3 -3
- package/{edge → serving}/runtime.rip +18 -16
- package/{edge → serving}/security.rip +1 -1
- package/serving/static.rip +393 -0
- package/{edge → serving}/tls.rip +3 -7
- package/{edge → serving}/upstream.rip +4 -4
- package/{edge → serving}/verify.rip +16 -16
- package/streams/{tls_clienthello.rip → clienthello.rip} +1 -1
- package/streams/config.rip +8 -8
- package/streams/index.rip +5 -5
- package/streams/router.rip +2 -2
- package/tests/acme.rip +1 -1
- package/tests/config.rip +215 -0
- package/tests/control.rip +1 -1
- package/tests/{runtime_entrypoints.rip → entrypoints.rip} +11 -7
- package/tests/extracted.rip +118 -0
- package/tests/helpers.rip +4 -4
- package/tests/metrics.rip +3 -3
- package/tests/proxy.rip +9 -8
- package/tests/read.rip +1 -1
- package/tests/realtime.rip +3 -3
- package/tests/registry.rip +4 -4
- package/tests/router.rip +27 -27
- package/tests/runner.rip +70 -0
- package/tests/security.rip +4 -4
- package/tests/servers.rip +102 -136
- package/tests/static.rip +2 -2
- package/tests/streams_clienthello.rip +2 -2
- package/tests/streams_index.rip +4 -4
- package/tests/streams_pipe.rip +1 -1
- package/tests/streams_router.rip +10 -10
- package/tests/streams_runtime.rip +4 -4
- package/tests/streams_upstream.rip +1 -1
- package/tests/upstream.rip +2 -2
- package/tests/verify.rip +18 -18
- package/tests/watchers.rip +4 -4
- package/default.rip +0 -435
- package/docs/edge/CONFIG_LIFECYCLE.md +0 -111
- package/docs/edge/CONTRACTS.md +0 -137
- package/docs/edge/EDGEFILE_CONTRACT.md +0 -282
- package/docs/edge/M0B_REVIEW_NOTES.md +0 -102
- package/docs/edge/SCHEDULER.md +0 -46
- package/docs/logo.png +0 -0
- package/docs/logo.svg +0 -13
- package/docs/social.png +0 -0
- package/edge/config.rip +0 -607
- package/edge/static.rip +0 -69
- package/tests/edgefile.rip +0 -165
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/forwarding.rip — helpers, responses, forwarding, and proxy
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
import { randomBytes } from 'node:crypto'
|
|
6
6
|
|
|
7
|
-
# ---
|
|
7
|
+
# --- Serving utilities ---
|
|
8
8
|
|
|
9
9
|
export generateRequestId = ->
|
|
10
10
|
"req-#{randomBytes(6).toString('hex')}"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# serving/logging.rip — access logging, formatting, and server utilities
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
|
|
5
|
+
export nowMs = -> Date.now()
|
|
6
|
+
|
|
7
|
+
_envOverride = null
|
|
8
|
+
_debugMode = false
|
|
9
|
+
|
|
10
|
+
export isDev = ->
|
|
11
|
+
env = (_envOverride or process.env.NODE_ENV or '').toLowerCase()
|
|
12
|
+
env in ['development', 'dev', '']
|
|
13
|
+
|
|
14
|
+
export isDebug = -> _debugMode or process.env.RIP_DEBUG?
|
|
15
|
+
|
|
16
|
+
export applyFlagSideEffects = (flags) ->
|
|
17
|
+
_envOverride = flags.envOverride if flags.envOverride
|
|
18
|
+
_debugMode = flags.debug is true
|
|
19
|
+
process.env.RIP_DEBUG = '1' if _debugMode
|
|
20
|
+
|
|
21
|
+
export formatTimestamp = ->
|
|
22
|
+
now = new Date()
|
|
23
|
+
pad = (n, w = 2) -> String(n).padStart(w, '0')
|
|
24
|
+
timestamp = "#{now.getFullYear()}-#{pad(now.getMonth() + 1)}-#{pad(now.getDate())} #{pad(now.getHours())}:#{pad(now.getMinutes())}:#{pad(now.getSeconds())}.#{String(now.getMilliseconds()).padStart(3, '0')}"
|
|
25
|
+
tzMin = now.getTimezoneOffset()
|
|
26
|
+
tzSign = if tzMin <= 0 then '+' else '-'
|
|
27
|
+
tzAbs = Math.abs(tzMin)
|
|
28
|
+
timezone = "#{tzSign}#{String(Math.floor(tzAbs / 60)).padStart(2, '0')}#{String(tzAbs % 60).padStart(2, '0')}"
|
|
29
|
+
{ timestamp, timezone }
|
|
30
|
+
|
|
31
|
+
export scale = (value, unit, pad = true) ->
|
|
32
|
+
if value > 0 and Number.isFinite(value)
|
|
33
|
+
span = ['T', 'G', 'M', 'k', (if pad then ' ' else ''), 'm', 'µ', 'n', 'p']
|
|
34
|
+
base = 4
|
|
35
|
+
minSlot = 0
|
|
36
|
+
maxSlot = span.length - 1
|
|
37
|
+
slot = base
|
|
38
|
+
|
|
39
|
+
while value < 0.995 and slot <= maxSlot
|
|
40
|
+
value *= 1000
|
|
41
|
+
slot++
|
|
42
|
+
while value >= 999.5 and slot >= minSlot
|
|
43
|
+
value /= 1000
|
|
44
|
+
slot--
|
|
45
|
+
|
|
46
|
+
if slot >= minSlot and slot <= maxSlot
|
|
47
|
+
tens = Math.round(value * 10) / 10
|
|
48
|
+
if tens >= 99.5
|
|
49
|
+
nums = Math.round(value).toString()
|
|
50
|
+
else if tens >= 10
|
|
51
|
+
nums = Math.round(value).toString()
|
|
52
|
+
else
|
|
53
|
+
nums = tens.toFixed(1)
|
|
54
|
+
nums = nums.padStart(3, ' ') if pad
|
|
55
|
+
return "#{nums}#{span[slot]}#{unit}"
|
|
56
|
+
|
|
57
|
+
return (if pad then ' 0 ' else '0') + unit if value is 0
|
|
58
|
+
'???' + (if pad then ' ' else '') + unit
|
|
59
|
+
|
|
60
|
+
export logAccessJson = (app, req, res, totalSeconds, workerSeconds) ->
|
|
61
|
+
url = new URL(req.url)
|
|
62
|
+
len = res.headers.get('content-length')
|
|
63
|
+
type = (res.headers.get('content-type') or '').split(';')[0] or undefined
|
|
64
|
+
p JSON.stringify
|
|
65
|
+
t: new Date().toISOString()
|
|
66
|
+
app: app
|
|
67
|
+
method: req.method or 'GET'
|
|
68
|
+
path: url.pathname
|
|
69
|
+
status: res.status
|
|
70
|
+
totalSeconds: totalSeconds
|
|
71
|
+
workerSeconds: workerSeconds
|
|
72
|
+
type: type
|
|
73
|
+
length: if len then Number(len) else undefined
|
|
74
|
+
|
|
75
|
+
typeAbbrev =
|
|
76
|
+
html: 'html', css: 'css', javascript: 'js', json: 'json', plain: 'text'
|
|
77
|
+
png: 'png', jpeg: 'jpg', gif: 'gif', webp: 'webp', svg: 'svg'
|
|
78
|
+
'svg+xml': 'svg', 'x-icon': 'ico', 'octet-stream': 'bin', 'x-rip': 'rip'
|
|
79
|
+
|
|
80
|
+
export logAccessHuman = (app, req, res, totalSeconds, workerSeconds) ->
|
|
81
|
+
{ timestamp, timezone } = formatTimestamp()
|
|
82
|
+
dur = scale(totalSeconds, 's')
|
|
83
|
+
method = req.method or 'GET'
|
|
84
|
+
url = new URL(req.url)
|
|
85
|
+
path = url.pathname
|
|
86
|
+
status = res.status
|
|
87
|
+
bytes = Number(res.headers.get('content-length') or 0)
|
|
88
|
+
size = scale(bytes, 'B')
|
|
89
|
+
contentType = (res.headers.get('content-type') or '').split(';')[0] or ''
|
|
90
|
+
sub = if contentType.includes('/') then contentType.split('/')[1] else contentType
|
|
91
|
+
type = (typeAbbrev[sub] or sub or '').padEnd(4)
|
|
92
|
+
p "#{timestamp} #{timezone} #{dur} │ #{status} #{type} #{size} │ #{method} #{path}"
|
|
93
|
+
|
|
94
|
+
INTERNAL_HEADERS = new Set(['rip-worker-busy', 'rip-worker-id', 'rip-no-log'])
|
|
95
|
+
|
|
96
|
+
export stripInternalHeaders = (h) ->
|
|
97
|
+
out = new Headers()
|
|
98
|
+
for [k, v] as h.entries()
|
|
99
|
+
continue if INTERNAL_HEADERS.has(k.toLowerCase())
|
|
100
|
+
out.append(k, v)
|
|
101
|
+
out
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/metrics.rip — lightweight in-process metrics collector
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
LATENCY_WINDOW = 1000
|
|
@@ -101,3 +101,31 @@ export createMetrics = ->
|
|
|
101
101
|
latency: m.percentiles()
|
|
102
102
|
|
|
103
103
|
m
|
|
104
|
+
|
|
105
|
+
export buildDiagnosticsBody = (state) ->
|
|
106
|
+
snap = state.metrics.snapshot(state.startedAt, state.appRegistry, state.servingRuntime.upstreamPool)
|
|
107
|
+
JSON.stringify
|
|
108
|
+
status: if snap.gauges.workersActive > 0 then 'healthy' else 'degraded'
|
|
109
|
+
version: { server: state.serverVersion, rip: state.ripVersion }
|
|
110
|
+
uptime: snap.uptime
|
|
111
|
+
apps: snap.apps
|
|
112
|
+
proxies: snap.upstreams
|
|
113
|
+
metrics:
|
|
114
|
+
requests: snap.counters.requests
|
|
115
|
+
responses: snap.counters.responses
|
|
116
|
+
latency: snap.latency
|
|
117
|
+
queue: snap.counters.queue
|
|
118
|
+
workers: snap.counters.workers
|
|
119
|
+
acme: snap.counters.acme
|
|
120
|
+
websocket: snap.counters.websocket
|
|
121
|
+
gauges: snap.gauges
|
|
122
|
+
realtime: state.realtimeStats
|
|
123
|
+
hosts: state.hosts
|
|
124
|
+
streams: state.streamDiagnostics
|
|
125
|
+
config: Object.assign({}, state.configInfo,
|
|
126
|
+
multiplexer: state.multiplexer
|
|
127
|
+
activeRuntime: state.activeRuntime
|
|
128
|
+
retiredRuntimes: state.retiredRuntimes
|
|
129
|
+
activeStreamRuntime: state.activeStreamRuntime
|
|
130
|
+
retiredStreamRuntimes: state.retiredStreamRuntimes
|
|
131
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# serving/proxy.rip — HTTP and WebSocket proxy to upstream backends
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
|
|
5
|
+
import { getUpstream, selectTarget, markTargetBusy, releaseTarget, shouldRetry, computeRetryDelayMs } from './upstream.rip'
|
|
6
|
+
import { serviceUnavailableResponse, buildUpstreamResponse, proxyToUpstream } from './forwarding.rip'
|
|
7
|
+
import { isDebug, stripInternalHeaders } from './logging.rip'
|
|
8
|
+
import { toUpstreamWsUrl } from './runtime.rip'
|
|
9
|
+
|
|
10
|
+
export proxyRouteToUpstream = (req, route, requestId, clientIp, runtime, metrics, flags, addSecurityHeaders, logAccess) ->
|
|
11
|
+
upstream = getUpstream(runtime.upstreamPool, route.proxy)
|
|
12
|
+
unless upstream
|
|
13
|
+
console.error "[proxy] no proxy '#{route.proxy}'" if isDebug()
|
|
14
|
+
return serviceUnavailableResponse()
|
|
15
|
+
|
|
16
|
+
attempt = 1
|
|
17
|
+
start = performance.now()
|
|
18
|
+
|
|
19
|
+
while attempt <= upstream.retry.attempts
|
|
20
|
+
target = selectTarget(upstream, runtime.upstreamPool.nowFn)
|
|
21
|
+
unless target
|
|
22
|
+
console.error "[proxy] no healthy target for '#{route.proxy}'" if isDebug()
|
|
23
|
+
return serviceUnavailableResponse()
|
|
24
|
+
|
|
25
|
+
markTargetBusy(target)
|
|
26
|
+
workerSeconds = 0
|
|
27
|
+
res = null
|
|
28
|
+
success = false
|
|
29
|
+
|
|
30
|
+
try
|
|
31
|
+
t0 = performance.now()
|
|
32
|
+
timeoutMs = route.timeouts?.readMs or upstream.timeouts?.readMs or flags.readTimeoutMs
|
|
33
|
+
res = proxyToUpstream!(req, target.url,
|
|
34
|
+
timeoutMs: timeoutMs
|
|
35
|
+
clientIp: clientIp
|
|
36
|
+
)
|
|
37
|
+
workerSeconds = (performance.now() - t0) / 1000
|
|
38
|
+
success = res.status < 500
|
|
39
|
+
console.error "[proxy] #{route.proxy} -> #{target.url} status=#{res.status} attempt=#{attempt}" if isDebug()
|
|
40
|
+
catch err
|
|
41
|
+
console.error "[proxy] #{route.proxy} error: #{err.message or err}" if isDebug()
|
|
42
|
+
res = serviceUnavailableResponse()
|
|
43
|
+
finally
|
|
44
|
+
releaseTarget(target, workerSeconds * 1000, success, runtime.upstreamPool)
|
|
45
|
+
|
|
46
|
+
if res and shouldRetry(upstream.retry, req.method, res.status, false) and attempt < upstream.retry.attempts
|
|
47
|
+
delayMs = computeRetryDelayMs(upstream.retry, attempt, runtime.upstreamPool.randomFn)
|
|
48
|
+
await new Promise (r) -> setTimeout(r, delayMs)
|
|
49
|
+
attempt++
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
totalSeconds = (performance.now() - start) / 1000
|
|
53
|
+
metrics.forwarded++
|
|
54
|
+
metrics.recordLatency(totalSeconds)
|
|
55
|
+
metrics.recordStatus(res.status)
|
|
56
|
+
response = buildUpstreamResponse(
|
|
57
|
+
res,
|
|
58
|
+
req,
|
|
59
|
+
totalSeconds,
|
|
60
|
+
workerSeconds,
|
|
61
|
+
addSecurityHeaders,
|
|
62
|
+
logAccess,
|
|
63
|
+
stripInternalHeaders
|
|
64
|
+
)
|
|
65
|
+
response.headers.set('X-Request-Id', requestId) if requestId
|
|
66
|
+
response.headers.set('X-Rip-Route', route.id) if route.id
|
|
67
|
+
return response
|
|
68
|
+
|
|
69
|
+
serviceUnavailableResponse()
|
|
70
|
+
|
|
71
|
+
export upgradeProxyWebSocket = (req, bunServer, route, requestId, runtime) ->
|
|
72
|
+
upstream = getUpstream(runtime.upstreamPool, route.proxy)
|
|
73
|
+
return new Response('Service unavailable', { status: 503 }) unless upstream
|
|
74
|
+
target = selectTarget(upstream, runtime.upstreamPool.nowFn)
|
|
75
|
+
return new Response('Service unavailable', { status: 503 }) unless target
|
|
76
|
+
|
|
77
|
+
inUrl = new URL(req.url)
|
|
78
|
+
protocols = (req.headers.get('sec-websocket-protocol') or '')
|
|
79
|
+
.split(',')
|
|
80
|
+
.map((p) -> p.trim())
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
|
|
83
|
+
markTargetBusy(target)
|
|
84
|
+
data =
|
|
85
|
+
kind: 'ws-proxy'
|
|
86
|
+
runtime: runtime
|
|
87
|
+
requestId: requestId
|
|
88
|
+
routeId: route.id
|
|
89
|
+
upstreamTarget: target
|
|
90
|
+
upstreamUrl: toUpstreamWsUrl(target.url, inUrl.pathname, inUrl.search)
|
|
91
|
+
protocols: protocols
|
|
92
|
+
passthrough: null
|
|
93
|
+
released: false
|
|
94
|
+
|
|
95
|
+
if bunServer.upgrade(req, { data })
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
releaseTarget(target, 0, false, runtime.upstreamPool)
|
|
99
|
+
new Response('WebSocket upgrade failed', { status: 400 })
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/queue.rip — scheduling, queue drain, and job processing
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
# --- Scheduler ---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/ratelimit.rip — per-IP sliding window rate limiter
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
DEFAULT_WINDOW_MS = 60000 # 1 minute
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/realtime.rip — Bam-style WebSocket realtime hub
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
#
|
|
5
5
|
# Manages WebSocket connections, group membership, and message routing.
|
|
@@ -194,7 +194,76 @@ export handlePublish = (hub, body) ->
|
|
|
194
194
|
|
|
195
195
|
export getRealtimeStats = (hub) ->
|
|
196
196
|
clients: hub.sockets.size
|
|
197
|
-
groups: hub.members.size - hub.sockets.size
|
|
197
|
+
groups: hub.members.size - hub.sockets.size
|
|
198
198
|
deliveries: hub.deliveries
|
|
199
199
|
messages: hub.messages
|
|
200
200
|
connections: hub.connections
|
|
201
|
+
|
|
202
|
+
export proxyRealtimeToWorker = (headers, frameType, body, getSocket, releaseSocket) ->
|
|
203
|
+
sock = getSocket()
|
|
204
|
+
return null unless sock
|
|
205
|
+
proxyHeaders = new Headers(headers)
|
|
206
|
+
proxyHeaders.set('Sec-WebSocket-Frame', frameType)
|
|
207
|
+
proxyHeaders.set('Content-Type', 'text/plain')
|
|
208
|
+
proxyHeaders.delete('Upgrade')
|
|
209
|
+
proxyHeaders.delete('Connection')
|
|
210
|
+
proxyHeaders.delete('Sec-WebSocket-Key')
|
|
211
|
+
proxyHeaders.delete('Sec-WebSocket-Version')
|
|
212
|
+
proxyHeaders.delete('Sec-WebSocket-Extensions')
|
|
213
|
+
try
|
|
214
|
+
res = fetch! "http://localhost/v1/realtime",
|
|
215
|
+
method: 'POST'
|
|
216
|
+
headers: proxyHeaders
|
|
217
|
+
body: body or ''
|
|
218
|
+
unix: sock.socket
|
|
219
|
+
decompress: false
|
|
220
|
+
res.text!
|
|
221
|
+
catch e
|
|
222
|
+
console.error "realtime: worker proxy failed:", e.message
|
|
223
|
+
null
|
|
224
|
+
finally
|
|
225
|
+
releaseSocket(sock)
|
|
226
|
+
|
|
227
|
+
export buildWebSocketHandlers = (hub, server) ->
|
|
228
|
+
websocket:
|
|
229
|
+
idleTimeout: 120
|
|
230
|
+
sendPings: true
|
|
231
|
+
|
|
232
|
+
open: (ws) ->
|
|
233
|
+
if ws.data?.kind is 'ws-proxy'
|
|
234
|
+
server.retainRuntimeWs(ws.data.runtime)
|
|
235
|
+
ws.data.passthrough = server.createWsPassthrough(ws, ws.data.upstreamUrl, ws.data.protocols or [])
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
{ clientId, headers } = ws.data
|
|
239
|
+
addClient(hub, clientId, ws)
|
|
240
|
+
server.metrics.wsConnections++
|
|
241
|
+
server.logEvent 'ws_open', { clientId }
|
|
242
|
+
response = server.proxyRealtimeToWorker!(headers, 'open', '')
|
|
243
|
+
processResponse(hub, response, clientId) if response
|
|
244
|
+
|
|
245
|
+
message: (ws, message) ->
|
|
246
|
+
if ws.data?.kind is 'ws-proxy'
|
|
247
|
+
ws.data.passthrough?.sendToUpstream(message)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
{ clientId, headers } = ws.data
|
|
251
|
+
server.metrics.wsMessages++
|
|
252
|
+
isBinary = typeof message isnt 'string'
|
|
253
|
+
msg = if isBinary then Buffer.from(message) else message
|
|
254
|
+
frameType = if isBinary then 'binary' else 'text'
|
|
255
|
+
response = server.proxyRealtimeToWorker!(headers, frameType, msg)
|
|
256
|
+
processResponse(hub, response, clientId) if response
|
|
257
|
+
|
|
258
|
+
close: (ws) ->
|
|
259
|
+
if ws.data?.kind is 'ws-proxy'
|
|
260
|
+
ws.data.passthrough?.close()
|
|
261
|
+
unless ws.data.released
|
|
262
|
+
server.releaseProxyTarget(ws.data)
|
|
263
|
+
ws.data.released = true
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
{ clientId, headers } = ws.data
|
|
267
|
+
server.logEvent 'ws_close', { clientId }
|
|
268
|
+
removeClient(hub, clientId)
|
|
269
|
+
server.proxyRealtimeToWorker!(headers, 'close', '')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/registry.rip — per-app state and multi-app registry
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
createAppState = (appId, config) ->
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/router.rip — route compilation and matching
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
normalizeMethods = (methods) ->
|
|
@@ -85,7 +85,7 @@ compileRoute = (route, order, inheritedHost = null) ->
|
|
|
85
85
|
pathBase: pathBase(path)
|
|
86
86
|
methods
|
|
87
87
|
priority: route.priority or order
|
|
88
|
-
|
|
88
|
+
proxy: route.proxy or null
|
|
89
89
|
app: route.app or null
|
|
90
90
|
static: route.static or null
|
|
91
91
|
root: route.root or null
|
|
@@ -128,7 +128,7 @@ export matchRoute = (table, hostname, pathname, method = 'GET') ->
|
|
|
128
128
|
|
|
129
129
|
export describeRoute = (route) ->
|
|
130
130
|
return null unless route
|
|
131
|
-
action = if route.
|
|
131
|
+
action = if route.proxy? then "proxy:#{route.proxy}"
|
|
132
132
|
else if route.app? then "app:#{route.app}"
|
|
133
133
|
else if route.static? then "static"
|
|
134
134
|
else if route.redirect? then "redirect"
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/runtime.rip — serving runtime lifecycle helpers
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
import { createUpstreamPool } from './upstream.rip'
|
|
6
|
-
import { compileRouteTable
|
|
7
|
-
import { resolveConfigSource, loadConfig,
|
|
6
|
+
import { compileRouteTable } from './router.rip'
|
|
7
|
+
import { resolveConfigSource, loadConfig, summarizeConfig, formatConfigErrors, checkConfigFile } from './config.rip'
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export createServingRuntime = (configInfo = null, upstreamPool = null, routeTable = null) ->
|
|
10
10
|
upstreamPool = upstreamPool or createUpstreamPool()
|
|
11
11
|
routeTable = routeTable or compileRouteTable()
|
|
12
12
|
configInfo = configInfo or {
|
|
13
13
|
kind: 'none'
|
|
14
14
|
path: null
|
|
15
15
|
version: null
|
|
16
|
-
counts: { apps: 0,
|
|
16
|
+
counts: { apps: 0, proxies: 0, routes: 0, hosts: 0 }
|
|
17
17
|
lastResult: 'none'
|
|
18
18
|
loadedAt: null
|
|
19
19
|
note: null
|
|
@@ -23,7 +23,7 @@ export createEdgeRuntime = (configInfo = null, upstreamPool = null, routeTable =
|
|
|
23
23
|
activeRouteDescriptions: []
|
|
24
24
|
}
|
|
25
25
|
{
|
|
26
|
-
id: "
|
|
26
|
+
id: "srv-#{Date.now()}-#{Math.random().toString(16).slice(2, 8)}"
|
|
27
27
|
upstreamPool
|
|
28
28
|
routeTable
|
|
29
29
|
configInfo
|
|
@@ -33,6 +33,8 @@ export createEdgeRuntime = (configInfo = null, upstreamPool = null, routeTable =
|
|
|
33
33
|
retiredAt: null
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export createEdgeRuntime = (configInfo, upstreamPool, routeTable) -> createServingRuntime(configInfo, upstreamPool, routeTable)
|
|
37
|
+
|
|
36
38
|
export createReloadHistoryEntry = (id, source, oldVersion, newVersion, result, reason = null, code = null, details = null) ->
|
|
37
39
|
id: id
|
|
38
40
|
source: source
|
|
@@ -50,9 +52,9 @@ export restoreRegistrySnapshot = (registry, snapshot) ->
|
|
|
50
52
|
registry.wildcardIndex = new Map(snapshot.wildcardIndex)
|
|
51
53
|
|
|
52
54
|
export configNote = (loaded) ->
|
|
53
|
-
return null unless loaded?.source?.kind
|
|
54
|
-
if loaded.summary.counts.
|
|
55
|
-
'
|
|
55
|
+
return null unless loaded?.source?.kind in ['serve', 'edge']
|
|
56
|
+
if loaded.summary.counts.proxies > 0 or loaded.summary.counts.routes > 0 or loaded.summary.counts.hosts > 0
|
|
57
|
+
'Composable host blocks, proxy routes, websocket routes, wildcard hosts, and managed app routing are live.'
|
|
56
58
|
else
|
|
57
59
|
null
|
|
58
60
|
|
|
@@ -62,9 +64,9 @@ export toUpstreamWsUrl = (baseUrl, pathName, search) ->
|
|
|
62
64
|
"#{protocol}//#{origin.host}#{pathName}#{search}"
|
|
63
65
|
|
|
64
66
|
export loadRuntimeConfig = (flags) ->
|
|
65
|
-
source = resolveConfigSource(flags.appEntry, flags.
|
|
67
|
+
source = resolveConfigSource(flags.appEntry, flags.configPath)
|
|
66
68
|
return null unless source?.path
|
|
67
|
-
normalized =
|
|
69
|
+
normalized = loadConfig!(source.path)
|
|
68
70
|
return null unless normalized
|
|
69
71
|
{
|
|
70
72
|
source
|
|
@@ -73,25 +75,25 @@ export loadRuntimeConfig = (flags) ->
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
export printCheckConfigResult = (loaded) ->
|
|
76
|
-
label =
|
|
78
|
+
label = 'serve.rip'
|
|
77
79
|
p "rip-server: #{label} OK"
|
|
78
80
|
p " path: #{loaded.summary.path}"
|
|
79
|
-
p " apps: #{loaded.summary.counts.apps}
|
|
81
|
+
p " apps: #{loaded.summary.counts.apps} proxies: #{loaded.summary.counts.proxies} hosts: #{loaded.summary.counts.hosts} routes: #{loaded.summary.counts.routes}"
|
|
80
82
|
p " version: #{loaded.summary.version}" if loaded.summary.version?
|
|
81
83
|
note = configNote(loaded)
|
|
82
84
|
p " note: #{note}" if note
|
|
83
85
|
|
|
84
86
|
export runCheckConfig = (flags) ->
|
|
85
|
-
source = resolveConfigSource(flags.appEntry, flags.
|
|
87
|
+
source = resolveConfigSource(flags.appEntry, flags.configPath)
|
|
86
88
|
unless source?.path
|
|
87
|
-
console.error 'rip-server: no
|
|
89
|
+
console.error 'rip-server: no serve.rip found to validate'
|
|
88
90
|
exit 1
|
|
89
91
|
try
|
|
90
92
|
checked = checkConfigFile!(source.path)
|
|
91
93
|
printCheckConfigResult({ source, normalized: checked.normalized, summary: checked.summary })
|
|
92
94
|
catch e
|
|
93
95
|
if e.validationErrors
|
|
94
|
-
label =
|
|
96
|
+
label = 'serve.rip'
|
|
95
97
|
console.error formatConfigErrors(label, e.validationErrors)
|
|
96
98
|
else
|
|
97
99
|
console.error "rip-server: config check failed: #{e.message or e}"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/security.rip — request smuggling and validation defenses
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
MAX_URL_LENGTH = 8192
|