@rip-lang/server 1.3.115 → 1.3.117
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/README.md +435 -622
- package/api.rip +4 -4
- package/control/cli.rip +221 -1
- package/control/control.rip +9 -0
- package/control/lifecycle.rip +6 -1
- package/control/watchers.rip +10 -0
- package/control/workers.rip +9 -5
- package/default.rip +3 -1
- package/docs/READ_VALIDATORS.md +656 -0
- package/docs/edge/CONFIG_LIFECYCLE.md +70 -32
- package/docs/edge/CONTRACTS.md +60 -69
- package/docs/edge/EDGEFILE_CONTRACT.md +258 -29
- package/docs/edge/M0B_REVIEW_NOTES.md +5 -5
- package/edge/config.rip +584 -52
- package/edge/forwarding.rip +6 -2
- package/edge/metrics.rip +19 -1
- package/edge/registry.rip +29 -3
- package/edge/router.rip +138 -0
- package/edge/runtime.rip +98 -0
- package/edge/static.rip +69 -0
- package/edge/tls.rip +23 -0
- package/edge/upstream.rip +272 -0
- package/edge/verify.rip +73 -0
- package/middleware.rip +3 -3
- package/package.json +2 -2
- package/server.rip +775 -393
- package/tests/control.rip +18 -0
- package/tests/edgefile.rip +165 -0
- package/tests/metrics.rip +16 -0
- package/tests/proxy.rip +22 -1
- package/tests/registry.rip +27 -0
- package/tests/router.rip +101 -0
- package/tests/runtime_entrypoints.rip +16 -0
- package/tests/servers.rip +262 -0
- package/tests/static.rip +64 -0
- package/tests/streams_clienthello.rip +108 -0
- package/tests/streams_index.rip +53 -0
- package/tests/streams_pipe.rip +70 -0
- package/tests/streams_router.rip +39 -0
- package/tests/streams_runtime.rip +38 -0
- package/tests/streams_upstream.rip +34 -0
- package/tests/upstream.rip +191 -0
- package/tests/verify.rip +148 -0
- package/tests/watchers.rip +15 -0
package/edge/forwarding.rip
CHANGED
|
@@ -80,6 +80,8 @@ export proxyToUpstream = (req, upstreamBase, options = {}) ->
|
|
|
80
80
|
headers.set('X-Forwarded-For', clientIp)
|
|
81
81
|
headers.set('X-Forwarded-Proto', if inUrl.protocol is 'https:' then 'https' else 'http')
|
|
82
82
|
headers.set('X-Forwarded-Host', inUrl.host)
|
|
83
|
+
via = req.headers.get('via')
|
|
84
|
+
headers.set('Via', if via then "#{via}, 1.1 rip" else '1.1 rip')
|
|
83
85
|
|
|
84
86
|
timer = null
|
|
85
87
|
try
|
|
@@ -97,6 +99,8 @@ export proxyToUpstream = (req, upstreamBase, options = {}) ->
|
|
|
97
99
|
resHeaders = new Headers()
|
|
98
100
|
for [key, val] as res.headers.entries()
|
|
99
101
|
resHeaders.set(key, val) unless HOP_HEADERS.has(key.toLowerCase())
|
|
102
|
+
upstreamVia = res.headers.get('via')
|
|
103
|
+
resHeaders.set('Via', if upstreamVia then "#{upstreamVia}, 1.1 rip" else '1.1 rip')
|
|
100
104
|
new Response(res.body, { status: res.status, statusText: res.statusText, headers: resHeaders })
|
|
101
105
|
catch err
|
|
102
106
|
clearTimeout(timer)
|
|
@@ -106,8 +110,8 @@ export proxyToUpstream = (req, upstreamBase, options = {}) ->
|
|
|
106
110
|
|
|
107
111
|
# --- WebSocket passthrough to external upstream ---
|
|
108
112
|
|
|
109
|
-
export createWsPassthrough = (clientWs, upstreamUrl) ->
|
|
110
|
-
upstream = new WebSocket(upstreamUrl)
|
|
113
|
+
export createWsPassthrough = (clientWs, upstreamUrl, protocols = []) ->
|
|
114
|
+
upstream = new WebSocket(upstreamUrl, protocols)
|
|
111
115
|
|
|
112
116
|
upstream.addEventListener 'open', ->
|
|
113
117
|
null # upstream connected
|
package/edge/metrics.rip
CHANGED
|
@@ -51,10 +51,13 @@ export createMetrics = ->
|
|
|
51
51
|
{ p50: pick(0.50), p95: pick(0.95), p99: pick(0.99) }
|
|
52
52
|
|
|
53
53
|
# Full snapshot of all metrics
|
|
54
|
-
snapshot: (startedAt, appRegistry) ->
|
|
54
|
+
snapshot: (startedAt, appRegistry, upstreamPool = null) ->
|
|
55
55
|
totalInflight = 0
|
|
56
56
|
totalQueueDepth = 0
|
|
57
57
|
apps = []
|
|
58
|
+
upstreams = []
|
|
59
|
+
upstreamHealthy = 0
|
|
60
|
+
upstreamUnhealthy = 0
|
|
58
61
|
if appRegistry
|
|
59
62
|
for [appId, app] as appRegistry.apps
|
|
60
63
|
totalInflight += app.inflightTotal
|
|
@@ -64,10 +67,23 @@ export createMetrics = ->
|
|
|
64
67
|
workers: app.sockets.length
|
|
65
68
|
inflight: app.inflightTotal
|
|
66
69
|
queueDepth: app.queue.length
|
|
70
|
+
if upstreamPool
|
|
71
|
+
for [upstreamId, upstream] as upstreamPool.upstreams
|
|
72
|
+
targets = upstream.targets or []
|
|
73
|
+
healthyTargets = targets.filter((t) -> t.healthy and t.circuitState isnt 'open').length
|
|
74
|
+
unhealthyTargets = targets.length - healthyTargets
|
|
75
|
+
upstreamHealthy += healthyTargets
|
|
76
|
+
upstreamUnhealthy += unhealthyTargets
|
|
77
|
+
upstreams.push
|
|
78
|
+
id: upstreamId
|
|
79
|
+
targets: targets.length
|
|
80
|
+
healthyTargets: healthyTargets
|
|
81
|
+
unhealthyTargets: unhealthyTargets
|
|
67
82
|
uptimeSeconds = if startedAt then Math.floor((Date.now() - startedAt) / 1000) else 0
|
|
68
83
|
|
|
69
84
|
uptime: uptimeSeconds
|
|
70
85
|
apps: apps
|
|
86
|
+
upstreams: upstreams
|
|
71
87
|
counters:
|
|
72
88
|
requests: m.requests
|
|
73
89
|
responses: { '2xx': m.responses2xx, '4xx': m.responses4xx, '5xx': m.responses5xx }
|
|
@@ -80,6 +96,8 @@ export createMetrics = ->
|
|
|
80
96
|
workersActive: apps.reduce(((sum, a) -> sum + a.workers), 0)
|
|
81
97
|
inflight: totalInflight
|
|
82
98
|
queueDepth: totalQueueDepth
|
|
99
|
+
upstreamTargetsHealthy: upstreamHealthy
|
|
100
|
+
upstreamTargetsUnhealthy: upstreamUnhealthy
|
|
83
101
|
latency: m.percentiles()
|
|
84
102
|
|
|
85
103
|
m
|
package/edge/registry.rip
CHANGED
|
@@ -18,12 +18,25 @@ createAppState = (appId, config) ->
|
|
|
18
18
|
export createAppRegistry = ->
|
|
19
19
|
apps: new Map()
|
|
20
20
|
hostIndex: new Map()
|
|
21
|
+
wildcardIndex: new Map()
|
|
22
|
+
|
|
23
|
+
wildcardKey = (host) ->
|
|
24
|
+
return null unless typeof host is 'string' and host.startsWith('*.')
|
|
25
|
+
base = host.slice(2)
|
|
26
|
+
labels = base.split('.').filter(Boolean)
|
|
27
|
+
return null if labels.length < 2
|
|
28
|
+
".#{base}"
|
|
21
29
|
|
|
22
30
|
export registerApp = (registry, appId, config) ->
|
|
23
31
|
state = createAppState(appId, config)
|
|
24
32
|
registry.apps.set(appId, state)
|
|
25
33
|
for host in (config.hosts or [])
|
|
26
|
-
|
|
34
|
+
normalized = host.toLowerCase()
|
|
35
|
+
wildcard = wildcardKey(normalized)
|
|
36
|
+
if wildcard
|
|
37
|
+
registry.wildcardIndex.set(wildcard, appId)
|
|
38
|
+
else
|
|
39
|
+
registry.hostIndex.set(normalized, appId)
|
|
27
40
|
state
|
|
28
41
|
|
|
29
42
|
export removeApp = (registry, appId) ->
|
|
@@ -31,11 +44,24 @@ export removeApp = (registry, appId) ->
|
|
|
31
44
|
return unless state
|
|
32
45
|
for host in state.hosts
|
|
33
46
|
h = host.toLowerCase()
|
|
34
|
-
|
|
47
|
+
wildcard = wildcardKey(h)
|
|
48
|
+
if wildcard
|
|
49
|
+
registry.wildcardIndex.delete(wildcard) if registry.wildcardIndex.get(wildcard) is appId
|
|
50
|
+
else
|
|
51
|
+
registry.hostIndex.delete(h) if registry.hostIndex.get(h) is appId
|
|
35
52
|
registry.apps.delete(appId)
|
|
36
53
|
|
|
37
54
|
export resolveHost = (registry, hostname) ->
|
|
38
|
-
|
|
55
|
+
host = hostname.toLowerCase()
|
|
56
|
+
exact = registry.hostIndex.get(host)
|
|
57
|
+
return exact if exact?
|
|
58
|
+
|
|
59
|
+
dot = host.indexOf('.')
|
|
60
|
+
if dot > 0
|
|
61
|
+
wildcard = registry.wildcardIndex.get(host.slice(dot))
|
|
62
|
+
return wildcard if wildcard?
|
|
63
|
+
|
|
64
|
+
registry.hostIndex.get('*') ?? null
|
|
39
65
|
|
|
40
66
|
export getAppState = (registry, appId) ->
|
|
41
67
|
registry.apps.get(appId) ?? null
|
package/edge/router.rip
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# edge/router.rip — edge route compilation and matching
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
|
|
5
|
+
normalizeMethods = (methods) ->
|
|
6
|
+
return '*' unless methods?
|
|
7
|
+
return '*' if methods is '*'
|
|
8
|
+
if Array.isArray(methods)
|
|
9
|
+
list = methods.map((m) -> String(m).toUpperCase()).filter(Boolean)
|
|
10
|
+
return '*' unless list.length
|
|
11
|
+
return Array.from(new Set(list))
|
|
12
|
+
[String(methods).toUpperCase()]
|
|
13
|
+
|
|
14
|
+
hostKind = (host) ->
|
|
15
|
+
return 'catchall' if host is '*'
|
|
16
|
+
return 'wildcard' if typeof host is 'string' and host.startsWith('*.')
|
|
17
|
+
'exact'
|
|
18
|
+
|
|
19
|
+
pathKind = (path) ->
|
|
20
|
+
if typeof path is 'string' and path.endsWith('*') then 'prefix' else 'exact'
|
|
21
|
+
|
|
22
|
+
pathBase = (path) ->
|
|
23
|
+
if pathKind(path) is 'prefix' then path.slice(0, -1) else path
|
|
24
|
+
|
|
25
|
+
wildcardMatch = (pattern, hostname) ->
|
|
26
|
+
return false unless typeof pattern is 'string' and pattern.startsWith('*.')
|
|
27
|
+
suffix = pattern.slice(1) # keep the leading dot
|
|
28
|
+
return false unless hostname.endsWith(suffix)
|
|
29
|
+
prefix = hostname.slice(0, hostname.length - suffix.length)
|
|
30
|
+
prefix.length > 0 and not prefix.includes('.')
|
|
31
|
+
|
|
32
|
+
routeMatchesHost = (route, hostname) ->
|
|
33
|
+
switch route.hostKind
|
|
34
|
+
when 'catchall' then true
|
|
35
|
+
when 'exact' then hostname is route.host
|
|
36
|
+
when 'wildcard' then wildcardMatch(route.host, hostname)
|
|
37
|
+
else false
|
|
38
|
+
|
|
39
|
+
routeMatchesPath = (route, pathname) ->
|
|
40
|
+
if route.pathKind is 'exact'
|
|
41
|
+
pathname is route.path
|
|
42
|
+
else
|
|
43
|
+
pathname.startsWith(route.pathBase)
|
|
44
|
+
|
|
45
|
+
routeMatchesMethod = (route, method) ->
|
|
46
|
+
route.methods is '*' or route.methods.includes(String(method).toUpperCase())
|
|
47
|
+
|
|
48
|
+
pathScore = (route) ->
|
|
49
|
+
route.pathBase.length * 10 + (if route.pathKind is 'exact' then 5 else 0)
|
|
50
|
+
|
|
51
|
+
hostScore = (route) ->
|
|
52
|
+
switch route.hostKind
|
|
53
|
+
when 'exact' then 30
|
|
54
|
+
when 'wildcard' then 20
|
|
55
|
+
else 10
|
|
56
|
+
|
|
57
|
+
matchTuple = (route) ->
|
|
58
|
+
[
|
|
59
|
+
hostScore(route)
|
|
60
|
+
pathScore(route)
|
|
61
|
+
-route.priority
|
|
62
|
+
-route.order
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
betterMatch = (candidate, current) ->
|
|
66
|
+
return true unless current
|
|
67
|
+
a = matchTuple(candidate)
|
|
68
|
+
b = matchTuple(current)
|
|
69
|
+
for idx in [0...a.length]
|
|
70
|
+
return true if a[idx] > b[idx]
|
|
71
|
+
return false if a[idx] < b[idx]
|
|
72
|
+
false
|
|
73
|
+
|
|
74
|
+
compileRoute = (route, order, inheritedHost = null) ->
|
|
75
|
+
host = (route.host or inheritedHost or '*').toLowerCase()
|
|
76
|
+
path = route.path or '/'
|
|
77
|
+
methods = normalizeMethods(route.methods)
|
|
78
|
+
{
|
|
79
|
+
id: route.id or "route-#{order + 1}"
|
|
80
|
+
order
|
|
81
|
+
host
|
|
82
|
+
hostKind: hostKind(host)
|
|
83
|
+
path
|
|
84
|
+
pathKind: pathKind(path)
|
|
85
|
+
pathBase: pathBase(path)
|
|
86
|
+
methods
|
|
87
|
+
priority: route.priority or order
|
|
88
|
+
upstream: route.upstream or null
|
|
89
|
+
app: route.app or null
|
|
90
|
+
static: route.static or null
|
|
91
|
+
root: route.root or null
|
|
92
|
+
spa: route.spa is true
|
|
93
|
+
browse: route.browse is true
|
|
94
|
+
redirect: route.redirect or null
|
|
95
|
+
headers: route.headers or null
|
|
96
|
+
websocket: route.websocket is true
|
|
97
|
+
timeouts: route.timeouts or {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export compileRouteTable = (routes = [], sites = {}) ->
|
|
101
|
+
compiled = []
|
|
102
|
+
order = 0
|
|
103
|
+
|
|
104
|
+
for route in routes
|
|
105
|
+
compiled.push(compileRoute(route, order))
|
|
106
|
+
order++
|
|
107
|
+
|
|
108
|
+
for siteHost, siteConfig of sites
|
|
109
|
+
for route in (siteConfig.routes or [])
|
|
110
|
+
compiled.push(compileRoute(route, order, siteHost))
|
|
111
|
+
order++
|
|
112
|
+
|
|
113
|
+
{ routes: compiled }
|
|
114
|
+
|
|
115
|
+
export matchRoute = (table, hostname, pathname, method = 'GET') ->
|
|
116
|
+
host = String(hostname or '').toLowerCase()
|
|
117
|
+
path = String(pathname or '/')
|
|
118
|
+
verb = String(method or 'GET').toUpperCase()
|
|
119
|
+
match = null
|
|
120
|
+
|
|
121
|
+
for route in (table?.routes or [])
|
|
122
|
+
continue unless routeMatchesHost(route, host)
|
|
123
|
+
continue unless routeMatchesPath(route, path)
|
|
124
|
+
continue unless routeMatchesMethod(route, verb)
|
|
125
|
+
match = route if betterMatch(route, match)
|
|
126
|
+
|
|
127
|
+
match
|
|
128
|
+
|
|
129
|
+
export describeRoute = (route) ->
|
|
130
|
+
return null unless route
|
|
131
|
+
action = if route.upstream? then "proxy:#{route.upstream}"
|
|
132
|
+
else if route.app? then "app:#{route.app}"
|
|
133
|
+
else if route.static? then "static"
|
|
134
|
+
else if route.redirect? then "redirect"
|
|
135
|
+
else if route.headers? then "headers"
|
|
136
|
+
else 'unknown'
|
|
137
|
+
suffix = if route.websocket then ' [ws]' else ''
|
|
138
|
+
"#{route.host} #{route.path} #{action}#{suffix}"
|
package/edge/runtime.rip
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# edge/runtime.rip — edge runtime lifecycle helpers
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
|
|
5
|
+
import { createUpstreamPool } from './upstream.rip'
|
|
6
|
+
import { compileRouteTable, describeRoute } from './router.rip'
|
|
7
|
+
import { resolveConfigSource, loadConfig, loadEdgeConfig, summarizeConfig, formatConfigErrors, checkConfigFile } from './config.rip'
|
|
8
|
+
|
|
9
|
+
export createEdgeRuntime = (configInfo = null, upstreamPool = null, routeTable = null) ->
|
|
10
|
+
upstreamPool = upstreamPool or createUpstreamPool()
|
|
11
|
+
routeTable = routeTable or compileRouteTable()
|
|
12
|
+
configInfo = configInfo or {
|
|
13
|
+
kind: 'none'
|
|
14
|
+
path: null
|
|
15
|
+
version: null
|
|
16
|
+
counts: { apps: 0, upstreams: 0, routes: 0, sites: 0 }
|
|
17
|
+
lastResult: 'none'
|
|
18
|
+
loadedAt: null
|
|
19
|
+
note: null
|
|
20
|
+
lastError: null
|
|
21
|
+
lastErrorCode: null
|
|
22
|
+
lastErrorDetails: null
|
|
23
|
+
activeRouteDescriptions: []
|
|
24
|
+
}
|
|
25
|
+
{
|
|
26
|
+
id: "edge-#{Date.now()}-#{Math.random().toString(16).slice(2, 8)}"
|
|
27
|
+
upstreamPool
|
|
28
|
+
routeTable
|
|
29
|
+
configInfo
|
|
30
|
+
verifyPolicy: configInfo?.verifyPolicy or null
|
|
31
|
+
inflight: 0
|
|
32
|
+
wsConnections: 0
|
|
33
|
+
retiredAt: null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export createReloadHistoryEntry = (id, source, oldVersion, newVersion, result, reason = null, code = null, details = null) ->
|
|
37
|
+
id: id
|
|
38
|
+
source: source
|
|
39
|
+
oldVersion: oldVersion
|
|
40
|
+
newVersion: newVersion
|
|
41
|
+
result: result
|
|
42
|
+
reason: reason
|
|
43
|
+
code: code
|
|
44
|
+
details: details
|
|
45
|
+
at: new Date().toISOString()
|
|
46
|
+
|
|
47
|
+
export restoreRegistrySnapshot = (registry, snapshot) ->
|
|
48
|
+
registry.apps = new Map(snapshot.apps)
|
|
49
|
+
registry.hostIndex = new Map(snapshot.hostIndex)
|
|
50
|
+
registry.wildcardIndex = new Map(snapshot.wildcardIndex)
|
|
51
|
+
|
|
52
|
+
export configNote = (loaded) ->
|
|
53
|
+
return null unless loaded?.source?.kind is 'edge'
|
|
54
|
+
if loaded.summary.counts.upstreams > 0 or loaded.summary.counts.routes > 0 or loaded.summary.counts.sites > 0
|
|
55
|
+
'Edgefile upstream routes, websocket routes, wildcard hosts, managed app-route expansion, and per-app server-side reload parity are live.'
|
|
56
|
+
else
|
|
57
|
+
null
|
|
58
|
+
|
|
59
|
+
export toUpstreamWsUrl = (baseUrl, pathName, search) ->
|
|
60
|
+
origin = new URL(baseUrl)
|
|
61
|
+
protocol = if origin.protocol is 'https:' then 'wss:' else 'ws:'
|
|
62
|
+
"#{protocol}//#{origin.host}#{pathName}#{search}"
|
|
63
|
+
|
|
64
|
+
export loadRuntimeConfig = (flags) ->
|
|
65
|
+
source = resolveConfigSource(flags.appEntry, flags.edgefilePath)
|
|
66
|
+
return null unless source?.path
|
|
67
|
+
normalized = if source.kind is 'edge' then loadEdgeConfig!(source.path) else loadConfig!(source.path)
|
|
68
|
+
return null unless normalized
|
|
69
|
+
{
|
|
70
|
+
source
|
|
71
|
+
normalized
|
|
72
|
+
summary: summarizeConfig(source.path, normalized)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export printCheckConfigResult = (loaded) ->
|
|
76
|
+
label = if loaded.source.kind is 'edge' then 'Edgefile.rip' else 'config.rip'
|
|
77
|
+
p "rip-server: #{label} OK"
|
|
78
|
+
p " path: #{loaded.summary.path}"
|
|
79
|
+
p " apps: #{loaded.summary.counts.apps} upstreams: #{loaded.summary.counts.upstreams} routes: #{loaded.summary.counts.routes} sites: #{loaded.summary.counts.sites}"
|
|
80
|
+
p " version: #{loaded.summary.version}" if loaded.summary.version?
|
|
81
|
+
note = configNote(loaded)
|
|
82
|
+
p " note: #{note}" if note
|
|
83
|
+
|
|
84
|
+
export runCheckConfig = (flags) ->
|
|
85
|
+
source = resolveConfigSource(flags.appEntry, flags.edgefilePath)
|
|
86
|
+
unless source?.path
|
|
87
|
+
console.error 'rip-server: no Edgefile.rip or config.rip found to validate'
|
|
88
|
+
exit 1
|
|
89
|
+
try
|
|
90
|
+
checked = checkConfigFile!(source.path)
|
|
91
|
+
printCheckConfigResult({ source, normalized: checked.normalized, summary: checked.summary })
|
|
92
|
+
catch e
|
|
93
|
+
if e.validationErrors
|
|
94
|
+
label = if source.kind is 'edge' then 'Edgefile.rip' else 'config.rip'
|
|
95
|
+
console.error formatConfigErrors(label, e.validationErrors)
|
|
96
|
+
else
|
|
97
|
+
console.error "rip-server: config check failed: #{e.message or e}"
|
|
98
|
+
exit 1
|
package/edge/static.rip
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# edge/static.rip — static file serving and redirect route handlers
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
|
|
5
|
+
import { resolve, join } from 'node:path'
|
|
6
|
+
import { statSync } from 'node:fs'
|
|
7
|
+
import { mimeType } from '../api.rip'
|
|
8
|
+
|
|
9
|
+
stripRoutePrefix = (pathname, routePath) ->
|
|
10
|
+
return pathname if routePath is '/' or routePath is '/*'
|
|
11
|
+
prefix = if routePath.endsWith('/*') then routePath.slice(0, -2) else routePath
|
|
12
|
+
if pathname.startsWith(prefix)
|
|
13
|
+
rest = pathname.slice(prefix.length)
|
|
14
|
+
return if rest is '' then '/' else rest
|
|
15
|
+
pathname
|
|
16
|
+
|
|
17
|
+
isSafeWithinRoot = (root, resolved) ->
|
|
18
|
+
rootSlash = if root.endsWith('/') then root else root + '/'
|
|
19
|
+
resolved is root or resolved.startsWith(rootSlash)
|
|
20
|
+
|
|
21
|
+
acceptsHtml = (req) ->
|
|
22
|
+
accept = req.headers?.get?('accept') or ''
|
|
23
|
+
accept.includes('text/html')
|
|
24
|
+
|
|
25
|
+
export serveStaticRoute = (req, url, route) ->
|
|
26
|
+
method = req.method or 'GET'
|
|
27
|
+
return new Response(null, { status: 405 }) unless method is 'GET' or method is 'HEAD'
|
|
28
|
+
|
|
29
|
+
base = route.root or route.static
|
|
30
|
+
return new Response('Not Found', { status: 404 }) unless base
|
|
31
|
+
|
|
32
|
+
staticDir = if route.static and route.static isnt '.'
|
|
33
|
+
if route.static.startsWith('/') then route.static else resolve(base, route.static)
|
|
34
|
+
else
|
|
35
|
+
base
|
|
36
|
+
|
|
37
|
+
pathname = try decodeURIComponent(url.pathname) catch then url.pathname
|
|
38
|
+
relative = stripRoutePrefix(pathname, route.path or '/*')
|
|
39
|
+
filePath = resolve(staticDir, '.' + relative)
|
|
40
|
+
|
|
41
|
+
return new Response('Forbidden', { status: 403 }) unless isSafeWithinRoot(staticDir, filePath)
|
|
42
|
+
|
|
43
|
+
try
|
|
44
|
+
stat = statSync(filePath)
|
|
45
|
+
if stat.isDirectory()
|
|
46
|
+
indexPath = join(filePath, 'index.html')
|
|
47
|
+
try
|
|
48
|
+
indexStat = statSync(indexPath)
|
|
49
|
+
if indexStat.isFile()
|
|
50
|
+
file = Bun.file(indexPath)
|
|
51
|
+
return new Response(file, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
52
|
+
if stat.isFile()
|
|
53
|
+
file = Bun.file(filePath)
|
|
54
|
+
return new Response(file, { headers: { 'content-type': mimeType(filePath) } })
|
|
55
|
+
|
|
56
|
+
if route.spa and acceptsHtml(req)
|
|
57
|
+
spaPath = join(staticDir, 'index.html')
|
|
58
|
+
try
|
|
59
|
+
spaStat = statSync(spaPath)
|
|
60
|
+
if spaStat.isFile()
|
|
61
|
+
file = Bun.file(spaPath)
|
|
62
|
+
return new Response(file, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
63
|
+
|
|
64
|
+
new Response('Not Found', { status: 404 })
|
|
65
|
+
|
|
66
|
+
export buildRedirectResponse = (req, url, route) ->
|
|
67
|
+
target = route.redirect?.to or '/'
|
|
68
|
+
status = route.redirect?.status or 302
|
|
69
|
+
Response.redirect(target, status)
|
package/edge/tls.rip
CHANGED
|
@@ -6,6 +6,29 @@ import { readFileSync } from 'node:fs'
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { X509Certificate } from 'node:crypto'
|
|
8
8
|
|
|
9
|
+
hostSpecificity = (serverName) ->
|
|
10
|
+
return 0 unless typeof serverName is 'string'
|
|
11
|
+
if serverName.startsWith('*.')
|
|
12
|
+
labels = serverName.slice(2).split('.').length
|
|
13
|
+
return 100 + labels
|
|
14
|
+
serverName.split('.').length + 200
|
|
15
|
+
|
|
16
|
+
export buildTlsArray = (defaultMaterial, certMap) ->
|
|
17
|
+
entries = []
|
|
18
|
+
for serverName, certInfo of (certMap or {})
|
|
19
|
+
try
|
|
20
|
+
cert = readFileSync(certInfo.certPath, 'utf8')
|
|
21
|
+
key = readFileSync(certInfo.keyPath, 'utf8')
|
|
22
|
+
entries.push({ serverName, cert, key, specificity: hostSpecificity(serverName) })
|
|
23
|
+
catch e
|
|
24
|
+
console.error "rip-server: failed to load cert for #{serverName}: #{e.message}"
|
|
25
|
+
entries.sort((a, b) -> b.specificity - a.specificity)
|
|
26
|
+
result = entries.map((e) -> { serverName: e.serverName, cert: e.cert, key: e.key })
|
|
27
|
+
if defaultMaterial?.cert and defaultMaterial?.key
|
|
28
|
+
fallbackName = '*.localhost'
|
|
29
|
+
result.push({ serverName: fallbackName, cert: defaultMaterial.cert, key: defaultMaterial.key })
|
|
30
|
+
result
|
|
31
|
+
|
|
9
32
|
export printCertSummary = (certPem) ->
|
|
10
33
|
try
|
|
11
34
|
x = new X509Certificate(certPem)
|