@rip-lang/server 1.4.2 → 1.4.4
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 +12 -0
- package/acme/manager.rip +1 -1
- package/api.rip +3 -3
- package/browse.rip +8 -6
- package/control/cli.rip +6 -6
- package/control/lifecycle.rip +3 -3
- package/control/manager.rip +2 -2
- package/control/mdns.rip +1 -1
- package/control/worker.rip +5 -5
- package/middleware.rip +3 -3
- package/package.json +2 -2
- package/server.rip +9 -7
- package/serving/config.rip +1 -1
- package/serving/forwarding.rip +1 -1
- package/serving/proxy.rip +4 -4
- package/serving/realtime.rip +3 -3
- package/serving/runtime.rip +3 -3
- package/serving/static.rip +5 -4
- package/serving/tls.rip +4 -4
- package/serving/upstream.rip +1 -1
- package/streams/index.rip +26 -6
package/README.md
CHANGED
|
@@ -359,6 +359,18 @@ or:
|
|
|
359
359
|
curl --unix-socket /tmp/rip_myapp.ctl.sock -X POST http://localhost/reload
|
|
360
360
|
```
|
|
361
361
|
|
|
362
|
+
### Binding to ports 80 and 443
|
|
363
|
+
|
|
364
|
+
Ports below 1024 require elevated privileges. If you see a permission
|
|
365
|
+
error on startup, grant Bun the capability once:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
sudo setcap cap_net_bind_service=+ep $(which bun)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
This survives reboots but **not Bun upgrades** — re-run it after
|
|
372
|
+
`bun upgrade`.
|
|
373
|
+
|
|
362
374
|
### Diagnostics
|
|
363
375
|
|
|
364
376
|
```bash
|
package/acme/manager.rip
CHANGED
|
@@ -95,7 +95,7 @@ export createAcmeManager = (options = {}) ->
|
|
|
95
95
|
for domain in domains
|
|
96
96
|
try manager.renewIfNeeded!(domain)
|
|
97
97
|
catch e
|
|
98
|
-
|
|
98
|
+
warn "rip-acme: renewal failed for #{domain}:", e.message
|
|
99
99
|
renewalTimer = setInterval(check, intervalMs)
|
|
100
100
|
# Run first check after a short delay
|
|
101
101
|
setTimeout(check, 5000)
|
package/api.rip
CHANGED
|
@@ -272,7 +272,7 @@ smart = (fn) ->
|
|
|
272
272
|
result
|
|
273
273
|
catch err
|
|
274
274
|
status = err?.status or 500
|
|
275
|
-
|
|
275
|
+
warn 'Handler error:', err if status >= 500
|
|
276
276
|
if err?.notice
|
|
277
277
|
body = JSON.stringify { error: { notice: err.notice } }
|
|
278
278
|
new Response body, { status, headers: { 'Content-Type': 'application/json' } }
|
|
@@ -389,7 +389,7 @@ runHandler = (c, handler) ->
|
|
|
389
389
|
compose!(_middlewares, _beforeFilters, _afterFilters, handler)(c)
|
|
390
390
|
c._response or new Response('', { status: 204 })
|
|
391
391
|
catch err
|
|
392
|
-
|
|
392
|
+
warn 'Request error:', err if not err?.status or err.status >= 500
|
|
393
393
|
if _errorHandler?
|
|
394
394
|
_errorHandler.call!(c, err, c)
|
|
395
395
|
else
|
|
@@ -413,7 +413,7 @@ export start = (opts = {}) ->
|
|
|
413
413
|
host = opts.host or 'localhost'
|
|
414
414
|
port = opts.port or 3000
|
|
415
415
|
server = Bun.serve { hostname: host, port: port, fetch: handler }
|
|
416
|
-
|
|
416
|
+
p "rip-api listening on http://#{host}:#{port}" unless opts.silent
|
|
417
417
|
server
|
|
418
418
|
|
|
419
419
|
export App = (fn) ->
|
package/browse.rip
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
# Built-in file browser when no index.rip or index.ts exists in the target
|
|
6
6
|
# directory. Activated automatically by `rip server` when no app entry is found.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# Single-click serves files natively (HTML renders, images display, etc.).
|
|
9
|
+
# Double-click (appends ?) shows syntax-highlighted source for text files.
|
|
10
|
+
# Markdown always renders. Directories show a listing with index file detection.
|
|
10
11
|
# ==============================================================================
|
|
11
12
|
|
|
12
13
|
import { use, start, notFound } from '@rip-lang/server'
|
|
@@ -23,8 +24,10 @@ notFound ->
|
|
|
23
24
|
res = serveRipHighlightGrammar()
|
|
24
25
|
return res if res
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
url = new URL(@req.url)
|
|
28
|
+
reqPath = try decodeURIComponent(url.pathname) catch then url.pathname
|
|
27
29
|
path = resolve(root, reqPath.slice(1))
|
|
30
|
+
viewSource = url.search is '?'
|
|
28
31
|
|
|
29
32
|
unless path is root or path.startsWith(rootSlash)
|
|
30
33
|
return new Response 'Not Found', { status: 404 }
|
|
@@ -37,12 +40,11 @@ notFound ->
|
|
|
37
40
|
if stat.isFile()
|
|
38
41
|
accept = @req.header('accept') or ''
|
|
39
42
|
if accept.includes('text/html')
|
|
40
|
-
if path.endsWith('.md')
|
|
43
|
+
if path.endsWith('.md') and not viewSource
|
|
41
44
|
try
|
|
42
45
|
html = renderMarkdown(path)
|
|
43
46
|
return new Response html, { headers: { 'Content-Type': 'text/html; charset=UTF-8' } }
|
|
44
|
-
|
|
45
|
-
if isTextFile(path) and not path.endsWith('.html') and not path.endsWith('.htm') and not path.endsWith('.rip')
|
|
47
|
+
if viewSource and isTextFile(path)
|
|
46
48
|
try
|
|
47
49
|
html = renderTextFile(path)
|
|
48
50
|
return new Response html, { headers: { 'Content-Type': 'text/html; charset=UTF-8' } }
|
package/control/cli.rip
CHANGED
|
@@ -51,7 +51,7 @@ export resolveAppEntry = (appPathInput, defaultEntryPath = null) ->
|
|
|
51
51
|
entryPath = defaultEntryPath or abs
|
|
52
52
|
else
|
|
53
53
|
unless existsSync(abs)
|
|
54
|
-
|
|
54
|
+
warn "App path not found: #{abs}"
|
|
55
55
|
exit 2
|
|
56
56
|
baseDir = dirname(abs)
|
|
57
57
|
entryPath = abs
|
|
@@ -383,7 +383,7 @@ export stopServer = (prefix, getPidFilePathFn, existsSyncFn, readFileSyncFn, kil
|
|
|
383
383
|
p "rip-server: no PID file found at #{pidFile}, trying pkill..."
|
|
384
384
|
spawnSyncFn(['pkill', '-f', importMetaPath])
|
|
385
385
|
catch e
|
|
386
|
-
|
|
386
|
+
warn "rip-server: stop failed: #{e.message}"
|
|
387
387
|
|
|
388
388
|
export reloadConfig = (prefix, getControlSocketPathFn, fetchFn, exitFn) ->
|
|
389
389
|
controlUnix = getControlSocketPathFn(prefix)
|
|
@@ -394,10 +394,10 @@ export reloadConfig = (prefix, getControlSocketPathFn, fetchFn, exitFn) ->
|
|
|
394
394
|
p "rip-server: config reload applied"
|
|
395
395
|
else
|
|
396
396
|
reason = j?.reason or 'unknown'
|
|
397
|
-
|
|
397
|
+
warn "rip-server: config reload rejected: #{reason}"
|
|
398
398
|
exitFn(1)
|
|
399
399
|
catch e
|
|
400
|
-
|
|
400
|
+
warn "rip-server: reload failed: #{e?.message or e}"
|
|
401
401
|
exitFn(1)
|
|
402
402
|
|
|
403
403
|
export listApps = (prefix, getControlSocketPathFn, fetchFn, exitFn) ->
|
|
@@ -409,7 +409,7 @@ export listApps = (prefix, getControlSocketPathFn, fetchFn, exitFn) ->
|
|
|
409
409
|
hosts = if Array.isArray(j?.hosts) then j.hosts else []
|
|
410
410
|
p if hosts.length then hosts.join('\n') else '(no hosts)'
|
|
411
411
|
catch e
|
|
412
|
-
|
|
412
|
+
warn "rip-server: list failed: #{e?.message or e}"
|
|
413
413
|
exitFn(1)
|
|
414
414
|
|
|
415
415
|
formatUptime = (seconds) ->
|
|
@@ -437,5 +437,5 @@ export showInfo = (fetchFn, exitFn) ->
|
|
|
437
437
|
p "rip server v#{version} | #{status} | #{workers} workers | #{hosts} hosts#{upstreamPart} | uptime #{uptime}"
|
|
438
438
|
exitFn(if status is 'healthy' then 0 else 1)
|
|
439
439
|
catch e
|
|
440
|
-
|
|
440
|
+
warn "rip-server: info failed: #{e?.message or e}"
|
|
441
441
|
exitFn(1)
|
package/control/lifecycle.rip
CHANGED
|
@@ -133,10 +133,10 @@ export installShutdownHandlers = (cleanup, processObj, onReload = null) ->
|
|
|
133
133
|
processObj.on 'SIGHUP', ->
|
|
134
134
|
try onReload!()
|
|
135
135
|
catch err
|
|
136
|
-
|
|
136
|
+
warn 'rip-server: config reload failed:', err?.message or err
|
|
137
137
|
processObj.on 'uncaughtException', (err) ->
|
|
138
|
-
|
|
138
|
+
warn 'rip-server: uncaught exception:', err
|
|
139
139
|
cleanup()
|
|
140
140
|
processObj.on 'unhandledRejection', (err) ->
|
|
141
|
-
|
|
141
|
+
warn 'rip-server: unhandled rejection:', err
|
|
142
142
|
cleanup()
|
package/control/manager.rip
CHANGED
|
@@ -73,7 +73,7 @@ export class Manager
|
|
|
73
73
|
|
|
74
74
|
code = await proc.exited
|
|
75
75
|
if code isnt 0
|
|
76
|
-
|
|
76
|
+
warn "rip-server: setup exited with code #{code} for app #{app.appId}"
|
|
77
77
|
exit 1
|
|
78
78
|
|
|
79
79
|
start: ->
|
|
@@ -208,7 +208,7 @@ export class Manager
|
|
|
208
208
|
readyResults = Promise.all!(pairs.map((p) => @waitWorkerReady(p.replacement.socketPath, 3000)))
|
|
209
209
|
allReady = readyResults.every((ready) -> ready)
|
|
210
210
|
unless allReady
|
|
211
|
-
|
|
211
|
+
warn "[manager] Rolling restart aborted: not all new workers ready for app #{app.appId}"
|
|
212
212
|
for pair, i in pairs
|
|
213
213
|
unless readyResults[i]
|
|
214
214
|
try pair.replacement.process.kill()
|
package/control/mdns.rip
CHANGED
|
@@ -53,4 +53,4 @@ export startMdnsAdvertisement = (host, mdnsProcesses, getLanIP, flags, formatPor
|
|
|
53
53
|
url = "#{protocol}://#{host}#{formatPort(protocol, port)}"
|
|
54
54
|
publishUrl(url)
|
|
55
55
|
catch e
|
|
56
|
-
|
|
56
|
+
warn "rip-server: failed to advertise #{host} via mDNS:", e.message
|
package/control/worker.rip
CHANGED
|
@@ -15,7 +15,7 @@ export runSetupMode = ->
|
|
|
15
15
|
if typeof fn is 'function'
|
|
16
16
|
await fn()
|
|
17
17
|
catch e
|
|
18
|
-
|
|
18
|
+
warn "rip-server: setup failed:", e
|
|
19
19
|
exit 1
|
|
20
20
|
|
|
21
21
|
export runWorkerMode = ->
|
|
@@ -62,7 +62,7 @@ export runWorkerMode = ->
|
|
|
62
62
|
workerState.handler = h if h
|
|
63
63
|
workerState.handler or (-> new Response('not ready', { status: 503 }))
|
|
64
64
|
catch e
|
|
65
|
-
|
|
65
|
+
warn "[worker #{workerId}] import failed:", e
|
|
66
66
|
workerState.handler or (-> new Response('not ready', { status: 503 }))
|
|
67
67
|
|
|
68
68
|
selfJoin = ->
|
|
@@ -106,7 +106,7 @@ export runWorkerMode = ->
|
|
|
106
106
|
res = res!(req) if typeof res is 'function'
|
|
107
107
|
if res instanceof Response then res else new Response(String(res))
|
|
108
108
|
catch err
|
|
109
|
-
|
|
109
|
+
warn "[worker #{workerId}] ERROR:", err
|
|
110
110
|
new Response('error', { status: 500 })
|
|
111
111
|
finally
|
|
112
112
|
workerState.inflight = false
|
|
@@ -129,8 +129,8 @@ export runWorkerMode = ->
|
|
|
129
129
|
process.on 'SIGTERM', shutdown
|
|
130
130
|
process.on 'SIGINT', shutdown
|
|
131
131
|
process.on 'uncaughtException', (err) ->
|
|
132
|
-
|
|
132
|
+
warn "[worker #{workerId}] uncaught exception:", err
|
|
133
133
|
shutdown()
|
|
134
134
|
process.on 'unhandledRejection', (err) ->
|
|
135
|
-
|
|
135
|
+
warn "[worker #{workerId}] unhandled rejection:", err
|
|
136
136
|
shutdown()
|
package/middleware.rip
CHANGED
|
@@ -100,7 +100,7 @@ export cors = (opts = {}) ->
|
|
|
100
100
|
export logger = (opts = {}) ->
|
|
101
101
|
format = opts.format or 'dev'
|
|
102
102
|
skip = opts.skip or null
|
|
103
|
-
stream = opts.stream or { write: (msg) ->
|
|
103
|
+
stream = opts.stream or { write: (msg) -> p msg.trim() }
|
|
104
104
|
|
|
105
105
|
formatters =
|
|
106
106
|
tiny: (info) -> "#{info.method} #{info.path} #{info.status} - #{info.ms}ms"
|
|
@@ -274,7 +274,7 @@ export sessions = (opts = {}) ->
|
|
|
274
274
|
|
|
275
275
|
# Warn if no secret in production
|
|
276
276
|
if not secret and process.env.NODE_ENV is 'production'
|
|
277
|
-
|
|
277
|
+
warn 'WARNING: sessions() without secret is insecure. Set secret option for production.'
|
|
278
278
|
|
|
279
279
|
(c, next) ->
|
|
280
280
|
# Parse cookie header
|
|
@@ -587,6 +587,6 @@ export serve = (opts = {}) ->
|
|
|
587
587
|
body = JSON.stringify({ op: 'watch', prefix, dirs: watchDirs })
|
|
588
588
|
|
|
589
589
|
fetch('http://localhost/watch', { method: 'POST', body, headers: { 'content-type': 'application/json' }, unix: ctl }).catch (e) ->
|
|
590
|
-
|
|
590
|
+
warn "[Rip] Watch registration failed: #{e.message}"
|
|
591
591
|
|
|
592
592
|
(c, next) -> next!()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rip-lang/server",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "Bun-native content server: static sites, apps, HTTP proxy, and TCP/TLS passthrough",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "api.rip",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"author": "Steve Shreeve <steve.shreeve@gmail.com>",
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"rip-lang": ">=3.13.
|
|
50
|
+
"rip-lang": ">=3.13.134"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"api.rip",
|
package/server.rip
CHANGED
|
@@ -223,7 +223,9 @@ class Server
|
|
|
223
223
|
return Bun.serve(Object.assign({ port, idleTimeout: 0, fetch: fetchFn }, wsOpts, opts))
|
|
224
224
|
catch e
|
|
225
225
|
if e?.code is 'EACCES' and port < 1024
|
|
226
|
-
|
|
226
|
+
fallback = if opts.tls then 3443 else 3000
|
|
227
|
+
warn "rip-server: port #{port} requires elevated privileges, falling back to #{fallback}"
|
|
228
|
+
port = fallback
|
|
227
229
|
p = port
|
|
228
230
|
continue
|
|
229
231
|
throw e unless e?.code is 'EADDRINUSE'
|
|
@@ -422,9 +424,9 @@ class Server
|
|
|
422
424
|
@appendReloadHistory(entry)
|
|
423
425
|
if e.validationErrors
|
|
424
426
|
label = 'serve.rip'
|
|
425
|
-
|
|
427
|
+
warn formatConfigErrors(label, e.validationErrors)
|
|
426
428
|
else
|
|
427
|
-
|
|
429
|
+
warn "rip-server: failed to load active config: #{e.message or e}"
|
|
428
430
|
logEvent('config_loaded',
|
|
429
431
|
id: attemptId
|
|
430
432
|
source: source
|
|
@@ -819,7 +821,7 @@ class Server
|
|
|
819
821
|
@releaseWorker(retry, app)
|
|
820
822
|
return @buildResponse(res, req, start, workerSeconds, requestId)
|
|
821
823
|
catch err
|
|
822
|
-
|
|
824
|
+
warn "[server] forwardToWorker error:", err.message or err if isDebug()
|
|
823
825
|
app.sockets = app.sockets.filter((x) -> x.socket isnt socket.socket)
|
|
824
826
|
app.availableWorkers = app.availableWorkers.filter((x) -> x.socket isnt socket.socket)
|
|
825
827
|
released = true
|
|
@@ -1030,13 +1032,13 @@ main = ->
|
|
|
1030
1032
|
svr.internalHttpsServer?.reload({ tls: svr.tlsMaterial }) if svr.internalHttpsServer
|
|
1031
1033
|
p "rip-acme: TLS reloaded with renewed cert for #{domain}"
|
|
1032
1034
|
catch e
|
|
1033
|
-
|
|
1035
|
+
warn "rip-acme: TLS reload failed after renewal: #{e.message}"
|
|
1034
1036
|
svr.acmeManager = acmeMgr
|
|
1035
1037
|
for domain in acmeDomains
|
|
1036
1038
|
try acmeMgr.obtainCert!(domain)
|
|
1037
1039
|
catch e
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
+
warn "rip-acme: cert obtain failed for #{domain}: #{e.message}"
|
|
1041
|
+
warn "rip-acme: will retry during renewal loop"
|
|
1040
1042
|
acmeMgr.startRenewalLoop(acmeDomains)
|
|
1041
1043
|
|
|
1042
1044
|
process.env.RIP_URLS = svr.urls.join(',') # exact URLs the Server just built
|
package/serving/config.rip
CHANGED
|
@@ -616,7 +616,7 @@ normalizeServeConfig = (config, baseDir) ->
|
|
|
616
616
|
pushError(errors, 'E_UNKNOWN_KEY', key, "unknown top-level key #{key}", 'Allowed keys: version, server, proxies, apps, certs, rules, groups, hosts, streams.') unless TOP_LEVEL_KEYS.has(key)
|
|
617
617
|
|
|
618
618
|
if config.edge? and not config.server?
|
|
619
|
-
|
|
619
|
+
warn "rip-server: 'edge' is deprecated in serve.rip; use 'server' instead"
|
|
620
620
|
serverSettings = normalizeServerSettings(config.server or config.edge or {}, errors)
|
|
621
621
|
|
|
622
622
|
proxies = {}
|
package/serving/forwarding.rip
CHANGED
|
@@ -123,7 +123,7 @@ export createWsPassthrough = (clientWs, upstreamUrl, protocols = []) ->
|
|
|
123
123
|
try clientWs.close()
|
|
124
124
|
|
|
125
125
|
upstream.addEventListener 'error', (err) ->
|
|
126
|
-
|
|
126
|
+
warn "ws-passthrough: upstream error:", err.message or err
|
|
127
127
|
try clientWs.close()
|
|
128
128
|
|
|
129
129
|
# Return object for client-side event wiring
|
package/serving/proxy.rip
CHANGED
|
@@ -10,7 +10,7 @@ import { toUpstreamWsUrl } from './runtime.rip'
|
|
|
10
10
|
export proxyRouteToUpstream = (req, route, requestId, clientIp, runtime, metrics, flags, addSecurityHeaders, logAccess) ->
|
|
11
11
|
upstream = getUpstream(runtime.upstreamPool, route.proxy)
|
|
12
12
|
unless upstream
|
|
13
|
-
|
|
13
|
+
warn "[proxy] no proxy '#{route.proxy}'" if isDebug()
|
|
14
14
|
return serviceUnavailableResponse()
|
|
15
15
|
|
|
16
16
|
attempt = 1
|
|
@@ -19,7 +19,7 @@ export proxyRouteToUpstream = (req, route, requestId, clientIp, runtime, metrics
|
|
|
19
19
|
while attempt <= upstream.retry.attempts
|
|
20
20
|
target = selectTarget(upstream, runtime.upstreamPool.nowFn)
|
|
21
21
|
unless target
|
|
22
|
-
|
|
22
|
+
warn "[proxy] no healthy target for '#{route.proxy}'" if isDebug()
|
|
23
23
|
return serviceUnavailableResponse()
|
|
24
24
|
|
|
25
25
|
markTargetBusy(target)
|
|
@@ -36,9 +36,9 @@ export proxyRouteToUpstream = (req, route, requestId, clientIp, runtime, metrics
|
|
|
36
36
|
)
|
|
37
37
|
workerSeconds = (performance.now() - t0) / 1000
|
|
38
38
|
success = res.status < 500
|
|
39
|
-
|
|
39
|
+
warn "[proxy] #{route.proxy} -> #{target.url} status=#{res.status} attempt=#{attempt}" if isDebug()
|
|
40
40
|
catch err
|
|
41
|
-
|
|
41
|
+
warn "[proxy] #{route.proxy} error: #{err.message or err}" if isDebug()
|
|
42
42
|
res = serviceUnavailableResponse()
|
|
43
43
|
finally
|
|
44
44
|
releaseTarget(target, workerSeconds * 1000, success, runtime.upstreamPool)
|
package/serving/realtime.rip
CHANGED
|
@@ -139,7 +139,7 @@ export processResponse = (hub, body, clientId) ->
|
|
|
139
139
|
clients = resolveTargets(hub, groups)
|
|
140
140
|
deliverToClients(hub, clients, senders, JSON.stringify(bundle))
|
|
141
141
|
catch e
|
|
142
|
-
|
|
142
|
+
warn "realtime: invalid response:", e.message
|
|
143
143
|
|
|
144
144
|
# --- Proxy to backend ---
|
|
145
145
|
|
|
@@ -160,7 +160,7 @@ export proxyToBackend = (hub, headers, frameType, body) ->
|
|
|
160
160
|
body: body or ''
|
|
161
161
|
res.text!
|
|
162
162
|
catch e
|
|
163
|
-
|
|
163
|
+
warn "realtime: proxy failed:", e.message
|
|
164
164
|
null
|
|
165
165
|
|
|
166
166
|
# --- Binary delivery (for CRDT and raw data) ---
|
|
@@ -219,7 +219,7 @@ export proxyRealtimeToWorker = (headers, frameType, body, getSocket, releaseSock
|
|
|
219
219
|
decompress: false
|
|
220
220
|
res.text!
|
|
221
221
|
catch e
|
|
222
|
-
|
|
222
|
+
warn "realtime: worker proxy failed:", e.message
|
|
223
223
|
null
|
|
224
224
|
finally
|
|
225
225
|
releaseSocket(sock)
|
package/serving/runtime.rip
CHANGED
|
@@ -86,7 +86,7 @@ export printCheckConfigResult = (loaded) ->
|
|
|
86
86
|
export runCheckConfig = (flags) ->
|
|
87
87
|
source = resolveConfigSource(flags.appEntry, flags.configPath)
|
|
88
88
|
unless source?.path
|
|
89
|
-
|
|
89
|
+
warn 'rip-server: no serve.rip found to validate'
|
|
90
90
|
exit 1
|
|
91
91
|
try
|
|
92
92
|
checked = checkConfigFile!(source.path)
|
|
@@ -94,7 +94,7 @@ export runCheckConfig = (flags) ->
|
|
|
94
94
|
catch e
|
|
95
95
|
if e.validationErrors
|
|
96
96
|
label = 'serve.rip'
|
|
97
|
-
|
|
97
|
+
warn formatConfigErrors(label, e.validationErrors)
|
|
98
98
|
else
|
|
99
|
-
|
|
99
|
+
warn "rip-server: config check failed: #{e.message or e}"
|
|
100
100
|
exit 1
|
package/serving/static.rip
CHANGED
|
@@ -189,7 +189,7 @@ export renderDirectoryListing = (reqPath, fsPath, rootName) ->
|
|
|
189
189
|
</table>
|
|
190
190
|
</div></div>
|
|
191
191
|
<script>
|
|
192
|
-
!function(){var p,c,ox=14,oy=14;function mk(){if(p)return p;p=document.createElement('div');p.className='pop';p.innerHTML='<div class="pop-b"></div>';document.body.appendChild(p);return p}function show(a,e){var s=a.dataset.src;if(!s)return;var el=mk(),b=el.firstChild;b.innerHTML='';var img=new Image();img.src=s;b.appendChild(img);c=a;el.classList.add('on');place(e.clientX,e.clientY)}function hide(){c=null;if(p)p.classList.remove('on')}function place(x,y){if(!p)return;var r=p.getBoundingClientRect(),m=12,l=x+ox,t=y+oy;if(l+r.width>innerWidth-m)l=x-r.width-ox;if(t+r.height>innerHeight-m)t=innerHeight-r.height-m;if(l<m)l=m;if(t<m)t=m;p.style.left=l+'px';p.style.top=t+'px'}document.addEventListener('mouseover',function(e){var a=e.target.closest('[data-pv]');if(a)show(a,e)});document.addEventListener('mousemove',function(e){if(c)place(e.clientX,e.clientY)});document.addEventListener('mouseout',function(e){var a=e.target.closest('[data-pv]');if(a&&a===c)hide()});document.addEventListener('scroll',hide,{passive:true});document.addEventListener('keydown',function(e){if(e.key==='Escape')hide()});window.addEventListener('blur',hide)}();
|
|
192
|
+
!function(){var p,c,ox=14,oy=14;function mk(){if(p)return p;p=document.createElement('div');p.className='pop';p.innerHTML='<div class="pop-b"></div>';document.body.appendChild(p);return p}function show(a,e){var s=a.dataset.src;if(!s)return;var el=mk(),b=el.firstChild;b.innerHTML='';var img=new Image();img.src=s;b.appendChild(img);c=a;el.classList.add('on');place(e.clientX,e.clientY)}function hide(){c=null;if(p)p.classList.remove('on')}function place(x,y){if(!p)return;var r=p.getBoundingClientRect(),m=12,l=x+ox,t=y+oy;if(l+r.width>innerWidth-m)l=x-r.width-ox;if(t+r.height>innerHeight-m)t=innerHeight-r.height-m;if(l<m)l=m;if(t<m)t=m;p.style.left=l+'px';p.style.top=t+'px'}document.addEventListener('mouseover',function(e){var a=e.target.closest('[data-pv]');if(a)show(a,e)});document.addEventListener('mousemove',function(e){if(c)place(e.clientX,e.clientY)});document.addEventListener('mouseout',function(e){var a=e.target.closest('[data-pv]');if(a&&a===c)hide()});document.addEventListener('scroll',hide,{passive:true});document.addEventListener('keydown',function(e){if(e.key==='Escape')hide()});window.addEventListener('blur',hide);var dt=0;document.addEventListener('click',function(e){var a=e.target.closest('td.nm a');if(!a)return;var h=a.getAttribute('href');if(!h||h.endsWith('/'))return;e.preventDefault();clearTimeout(dt);dt=setTimeout(function(){location.href=h},250)});document.addEventListener('dblclick',function(e){var a=e.target.closest('td.nm a');if(!a)return;var h=a.getAttribute('href');if(!h||h.endsWith('/'))return;e.preventDefault();clearTimeout(dt);location.href=h+'?'})}();
|
|
193
193
|
</script>
|
|
194
194
|
</body>
|
|
195
195
|
</html>
|
|
@@ -321,7 +321,7 @@ export renderTextFile = (filePath) ->
|
|
|
321
321
|
|
|
322
322
|
# --- Static file serving ---
|
|
323
323
|
|
|
324
|
-
INDEX_FILES = ['index.html', 'index.rip', 'index.ts', 'index.tsx', 'index.jsx', 'index.js']
|
|
324
|
+
export INDEX_FILES = ['index.html', 'index.rip', 'index.ts', 'index.tsx', 'index.jsx', 'index.js']
|
|
325
325
|
|
|
326
326
|
export findIndexFile = (dir) ->
|
|
327
327
|
for name in INDEX_FILES
|
|
@@ -376,10 +376,11 @@ export serveStaticRoute = (req, url, route) ->
|
|
|
376
376
|
return new Response(html, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
377
377
|
if stat.isFile()
|
|
378
378
|
if route.browse and acceptsHtml(req)
|
|
379
|
-
|
|
379
|
+
viewSource = url.search is '?'
|
|
380
|
+
if filePath.endsWith('.md') and not viewSource
|
|
380
381
|
html = renderMarkdown(filePath)
|
|
381
382
|
return new Response(html, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
382
|
-
if isTextFile(filePath)
|
|
383
|
+
if viewSource and isTextFile(filePath)
|
|
383
384
|
html = renderTextFile(filePath)
|
|
384
385
|
return new Response(html, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
385
386
|
file = Bun.file(filePath)
|
package/serving/tls.rip
CHANGED
|
@@ -21,7 +21,7 @@ export buildTlsArray = (defaultMaterial, certMap) ->
|
|
|
21
21
|
key = readFileSync(certInfo.keyPath, 'utf8')
|
|
22
22
|
entries.push({ serverName, cert, key, specificity: hostSpecificity(serverName) })
|
|
23
23
|
catch e
|
|
24
|
-
|
|
24
|
+
warn "rip-server: failed to load cert for #{serverName}: #{e.message}"
|
|
25
25
|
entries.sort((a, b) -> b.specificity - a.specificity)
|
|
26
26
|
result = entries.map((e) -> { serverName: e.serverName, cert: e.cert, key: e.key })
|
|
27
27
|
if defaultMaterial?.cert and defaultMaterial?.key
|
|
@@ -45,7 +45,7 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
|
|
|
45
45
|
key = readFileSync(flags.keyPath, 'utf8')
|
|
46
46
|
return { cert, key }
|
|
47
47
|
catch
|
|
48
|
-
|
|
48
|
+
warn 'Failed to read TLS cert/key from provided paths. Use http or fix paths.'
|
|
49
49
|
exit 2
|
|
50
50
|
|
|
51
51
|
# ACME-managed cert (if domain is configured)
|
|
@@ -63,6 +63,6 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
|
|
|
63
63
|
key = readFileSync(keyPath, 'utf8')
|
|
64
64
|
return { cert, key }
|
|
65
65
|
catch e
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
warn "rip-server: failed to load TLS certs from #{certsDir}: #{e.message}"
|
|
67
|
+
warn 'Use --cert/--key to provide your own, or use http to disable TLS.'
|
|
68
68
|
exit 2
|
package/serving/upstream.rip
CHANGED
|
@@ -233,7 +233,7 @@ export checkTargetHealth = (pool, target, fetchFn = null) ->
|
|
|
233
233
|
updateTargetHealth(target, res.ok, pool)
|
|
234
234
|
res.ok
|
|
235
235
|
catch e
|
|
236
|
-
|
|
236
|
+
warn "[health] #{url} failed: #{e?.message or e}" if process.env.RIP_DEBUG?
|
|
237
237
|
target.lastStatus = null
|
|
238
238
|
updateTargetHealth(target, false, pool)
|
|
239
239
|
false
|
package/streams/index.rip
CHANGED
|
@@ -273,12 +273,32 @@ export startStreamListeners = (runtime, options = {}) ->
|
|
|
273
273
|
handshakeMs: Math.max(...portRoutes.map((route) -> route.timeouts?.handshakeMs or 5000), 5000)
|
|
274
274
|
idleMs: Math.max(...portRoutes.map((route) -> route.timeouts?.idleMs or 300000), 300000)
|
|
275
275
|
connectMs: Math.max(...portRoutes.map((route) -> route.timeouts?.connectMs or 5000), 5000)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
276
|
+
try
|
|
277
|
+
listener = Bun.listen
|
|
278
|
+
hostname: options.hostname or '0.0.0.0'
|
|
279
|
+
port: port
|
|
280
|
+
allowHalfOpen: true
|
|
281
|
+
socket: createClientHandlers(runtime, port, listenerTimeouts, options).socket
|
|
282
|
+
runtime.listeners.set(port, listener)
|
|
283
|
+
catch e
|
|
284
|
+
if e?.code is 'EACCES' and port < 1024
|
|
285
|
+
warn ""
|
|
286
|
+
warn "rip-server: cannot bind to port #{port} — permission denied"
|
|
287
|
+
warn ""
|
|
288
|
+
warn " Ports below 1024 require elevated privileges."
|
|
289
|
+
warn " This commonly happens after upgrading Bun, which resets capabilities."
|
|
290
|
+
warn ""
|
|
291
|
+
warn " Options:"
|
|
292
|
+
warn ""
|
|
293
|
+
warn " 1. Grant Bun the capability (recommended, one-time):"
|
|
294
|
+
warn " sudo setcap cap_net_bind_service=+ep $(which bun)"
|
|
295
|
+
warn ""
|
|
296
|
+
warn " 2. Run with sudo:"
|
|
297
|
+
warn " sudo rip server --file serve.rip"
|
|
298
|
+
warn ""
|
|
299
|
+
warn " Note: upgrading Bun resets the capability — re-run setcap after upgrades."
|
|
300
|
+
warn ""
|
|
301
|
+
throw e
|
|
282
302
|
runtime
|
|
283
303
|
|
|
284
304
|
export stopStreamListeners = (runtime, closeActiveConnections = false) ->
|