@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
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# serving/static.rip — static file serving and redirect route handlers
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
|
|
5
|
+
import { resolve, join, basename, dirname } from 'node:path'
|
|
6
|
+
import { statSync, readdirSync, readFileSync } from 'node:fs'
|
|
7
|
+
import { mimeType } from '../api.rip'
|
|
8
|
+
|
|
9
|
+
findHljsRip = ->
|
|
10
|
+
bases = [
|
|
11
|
+
join(import.meta.dir, '..', '..', 'print', 'hljs-rip.js')
|
|
12
|
+
join(import.meta.dir, '..', '..', '..', 'print', 'hljs-rip.js')
|
|
13
|
+
]
|
|
14
|
+
try bases.push(import.meta.resolve('@rip-lang/print/hljs-rip.js').replace(/^file:\/\//, ''))
|
|
15
|
+
for p in bases
|
|
16
|
+
try
|
|
17
|
+
statSync(p)
|
|
18
|
+
return p
|
|
19
|
+
null
|
|
20
|
+
|
|
21
|
+
HLJS_RIP_PATH = findHljsRip()
|
|
22
|
+
|
|
23
|
+
stripRoutePrefix = (pathname, routePath) ->
|
|
24
|
+
return pathname if routePath is '/' or routePath is '/*'
|
|
25
|
+
prefix = if routePath.endsWith('/*') then routePath.slice(0, -2) else routePath
|
|
26
|
+
if pathname.startsWith(prefix)
|
|
27
|
+
rest = pathname.slice(prefix.length)
|
|
28
|
+
return if rest is '' then '/' else rest
|
|
29
|
+
pathname
|
|
30
|
+
|
|
31
|
+
isSafeWithinRoot = (root, resolved) ->
|
|
32
|
+
rootSlash = if root.endsWith('/') then root else root + '/'
|
|
33
|
+
resolved is root or resolved.startsWith(rootSlash)
|
|
34
|
+
|
|
35
|
+
acceptsHtml = (req) ->
|
|
36
|
+
accept = req.headers?.get?('accept') or ''
|
|
37
|
+
accept.includes('text/html')
|
|
38
|
+
|
|
39
|
+
# --- Directory listing ---
|
|
40
|
+
|
|
41
|
+
esc = (s) -> s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
42
|
+
|
|
43
|
+
fmtSize = (n) ->
|
|
44
|
+
for u in ['B', 'KB', 'MB', 'GB']
|
|
45
|
+
if n < 1024
|
|
46
|
+
return if u is 'B' then "#{n} B" else "#{n.toFixed(1)} #{u}"
|
|
47
|
+
n /= 1024
|
|
48
|
+
"#{n.toFixed(1)} TB"
|
|
49
|
+
|
|
50
|
+
fmtDate = (ms) ->
|
|
51
|
+
d = new Date(ms)
|
|
52
|
+
mo = d.toLocaleString('en', { month: 'short' })
|
|
53
|
+
day = String(d.getDate()).padStart(2, '0')
|
|
54
|
+
"#{mo} #{day}, #{d.getFullYear()}"
|
|
55
|
+
|
|
56
|
+
IMG_EXTS = new Set ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'avif']
|
|
57
|
+
CODE_EXTS = new Set ['js', 'mjs', 'ts', 'rip', 'html', 'htm', 'css', 'json', 'xml', 'yaml', 'yml', 'sh']
|
|
58
|
+
DOC_EXTS = new Set ['pdf', 'md', 'txt', 'doc', 'docx', 'rtf']
|
|
59
|
+
ARCHIVE_EXTS = new Set ['zip', 'gz', 'tar', 'bz2', 'xz', '7z', 'rar']
|
|
60
|
+
|
|
61
|
+
SVG_FOLDER = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" fill="currentColor" fill-opacity=".08"/></svg>'
|
|
62
|
+
SVG_FILE = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'
|
|
63
|
+
SVG_IMAGE = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>'
|
|
64
|
+
SVG_CODE = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>'
|
|
65
|
+
SVG_DOC = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'
|
|
66
|
+
SVG_ARCHIVE = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>'
|
|
67
|
+
SVG_UP = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l-6-6 6-6"/><path d="M3 12h18"/></svg>'
|
|
68
|
+
|
|
69
|
+
fileIcon = (name) ->
|
|
70
|
+
ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase()
|
|
71
|
+
if IMG_EXTS.has(ext) then SVG_IMAGE
|
|
72
|
+
else if CODE_EXTS.has(ext) then SVG_CODE
|
|
73
|
+
else if DOC_EXTS.has(ext) then SVG_DOC
|
|
74
|
+
else if ARCHIVE_EXTS.has(ext) then SVG_ARCHIVE
|
|
75
|
+
else SVG_FILE
|
|
76
|
+
|
|
77
|
+
previewAttr = (name, href) ->
|
|
78
|
+
ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase()
|
|
79
|
+
if IMG_EXTS.has(ext) then " data-pv=\"image\" data-src=\"#{esc href}\"" else ''
|
|
80
|
+
|
|
81
|
+
dirRow = (ic, href, label, size = '', date = '', extra = '') ->
|
|
82
|
+
"<tr><td class=\"ic\">#{ic}</td><td class=\"nm\"><a href=\"#{esc href}\"#{extra}>#{esc label}</a></td><td class=\"sz\">#{size}</td><td class=\"dt\">#{date}</td></tr>"
|
|
83
|
+
|
|
84
|
+
export renderDirectoryListing = (reqPath, fsPath, rootName) ->
|
|
85
|
+
entries = try readdirSync(fsPath, { withFileTypes: true }) catch then []
|
|
86
|
+
dirs = []
|
|
87
|
+
files = []
|
|
88
|
+
for e in entries
|
|
89
|
+
continue if e.name.startsWith('.')
|
|
90
|
+
if e.isDirectory() then dirs.push(e.name) else files.push(e.name)
|
|
91
|
+
dirs.sort()
|
|
92
|
+
files.sort()
|
|
93
|
+
|
|
94
|
+
rows = []
|
|
95
|
+
rows.push dirRow(SVG_UP, '../', '..') if reqPath isnt '/'
|
|
96
|
+
for d in dirs
|
|
97
|
+
rows.push dirRow(SVG_FOLDER, "#{d}/", d)
|
|
98
|
+
for f in files
|
|
99
|
+
try
|
|
100
|
+
s = statSync(join(fsPath, f))
|
|
101
|
+
rows.push dirRow(fileIcon(f), f, f, fmtSize(s.size), fmtDate(s.mtimeMs), previewAttr(f, f))
|
|
102
|
+
catch
|
|
103
|
+
rows.push dirRow(fileIcon(f), f, f)
|
|
104
|
+
|
|
105
|
+
numDirs = dirs.length
|
|
106
|
+
numFiles = files.length
|
|
107
|
+
counts = []
|
|
108
|
+
counts.push "#{numDirs} folder#{if numDirs isnt 1 then 's' else ''}" if numDirs > 0
|
|
109
|
+
counts.push "#{numFiles} file#{if numFiles isnt 1 then 's' else ''}" if numFiles > 0
|
|
110
|
+
|
|
111
|
+
crumbs = "<a href=\"/\">#{esc rootName}</a>"
|
|
112
|
+
href = ''
|
|
113
|
+
for part in reqPath.split('/').filter((p) -> p)
|
|
114
|
+
href += "/#{part}"
|
|
115
|
+
crumbs += "<span class=\"sep\">/</span><a href=\"#{href}/\">#{esc part}</a>"
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
<!DOCTYPE html>
|
|
119
|
+
<html lang="en">
|
|
120
|
+
<head>
|
|
121
|
+
<meta charset="UTF-8">
|
|
122
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
123
|
+
<title>#{esc(if reqPath is '/' then rootName else reqPath.split('/').filter((p) -> p).slice(-1)[0] or rootName)}</title>
|
|
124
|
+
<style>
|
|
125
|
+
:root {
|
|
126
|
+
--bg: #f8f9fa; --card: #fff; --border: #e2e5e9; --row-border: #f0f1f3;
|
|
127
|
+
--text: #1a1d21; --secondary: #6b7280; --icon: #9ca3af;
|
|
128
|
+
--link: #2563eb; --link-visited: #7c3aed; --link-hover: #1d4ed8;
|
|
129
|
+
--hover: #f0f5ff; --sep: #cbd5e1; --bar-bg: #f3f4f6; --head-bg: #f9fafb;
|
|
130
|
+
--radius: 10px; --shadow: 0 1px 3px rgba(0,0,0,.05), 0 0 0 1px rgba(0,0,0,.03);
|
|
131
|
+
}
|
|
132
|
+
@media (prefers-color-scheme: dark) { :root {
|
|
133
|
+
--bg: #0a0a0c; --card: #161618; --border: #2a2a2e; --row-border: #1e1e22;
|
|
134
|
+
--text: #e4e4e7; --secondary: #71717a; --icon: #52525b;
|
|
135
|
+
--link: #60a5fa; --link-visited: #a78bfa; --link-hover: #93c5fd;
|
|
136
|
+
--hover: #1a1d24; --sep: #3f3f46; --bar-bg: #1c1c1f; --head-bg: #19191c;
|
|
137
|
+
--shadow: 0 1px 3px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.04);
|
|
138
|
+
}}
|
|
139
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
140
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
|
|
141
|
+
font-size: 14px; line-height: 1.5; color: var(--text); background: var(--bg);
|
|
142
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
|
143
|
+
.wrap { max-width: 880px; margin: 3rem auto; padding: 0 1.25rem; }
|
|
144
|
+
.card { background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
|
|
145
|
+
.bar { display: flex; align-items: center; justify-content: space-between;
|
|
146
|
+
padding: .75rem 1rem; gap: .75rem; border-bottom: 1px solid var(--border); background: var(--bar-bg); }
|
|
147
|
+
.crumbs { font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 0; flex-wrap: wrap; }
|
|
148
|
+
.crumbs a { color: var(--link); text-decoration: none; padding: 2px 6px; border-radius: 4px; transition: all 120ms; }
|
|
149
|
+
.crumbs a:last-child { color: var(--text); font-weight: 600; }
|
|
150
|
+
.crumbs a:hover { color: var(--link-hover); background: var(--hover); }
|
|
151
|
+
.sep { color: var(--sep); font-size: 11px; margin: 0 1px; user-select: none; }
|
|
152
|
+
.meta { font-size: 12px; color: var(--secondary); white-space: nowrap; font-weight: 400; }
|
|
153
|
+
table { width: 100%; border-collapse: collapse; }
|
|
154
|
+
th { background: var(--head-bg); text-align: left; padding: 6px 1rem; font-size: 11px;
|
|
155
|
+
font-weight: 500; color: var(--secondary); letter-spacing: .03em; border-bottom: 1px solid var(--border); }
|
|
156
|
+
th.sz { text-align: right; }
|
|
157
|
+
td { padding: 7px 1rem; vertical-align: middle; font-size: 13px; border-bottom: 1px solid var(--row-border); }
|
|
158
|
+
tr:last-child td { border-bottom: none; }
|
|
159
|
+
tbody tr { position: relative; transition: background 80ms; cursor: pointer; }
|
|
160
|
+
tbody tr:hover { background: var(--hover); }
|
|
161
|
+
.ic { width: 2.25rem; text-align: center; color: var(--icon); line-height: 0; }
|
|
162
|
+
.ic svg { vertical-align: middle; }
|
|
163
|
+
.nm a { color: var(--link); text-decoration: none; font-weight: 450; letter-spacing: -.01em; }
|
|
164
|
+
.nm a::after { content: ''; position: absolute; inset: 0; }
|
|
165
|
+
.nm a:visited { color: var(--link-visited); }
|
|
166
|
+
.nm a:hover { color: var(--link-hover); }
|
|
167
|
+
.sz { width: 5.5rem; text-align: right; color: var(--secondary); white-space: nowrap;
|
|
168
|
+
font-variant-numeric: tabular-nums; font-size: 12px; letter-spacing: .01em; }
|
|
169
|
+
.dt { color: var(--secondary); white-space: nowrap; font-size: 12px; }
|
|
170
|
+
.pop { position: fixed; z-index: 9999; pointer-events: none; width: 300px;
|
|
171
|
+
background: var(--card); border-radius: 10px; box-shadow: 0 20px 40px rgba(0,0,0,.15), 0 0 0 1px rgba(0,0,0,.05);
|
|
172
|
+
overflow: hidden; opacity: 0; transform: translateY(4px) scale(.98);
|
|
173
|
+
transition: opacity 120ms ease, transform 120ms ease; }
|
|
174
|
+
.pop.on { opacity: 1; transform: none; }
|
|
175
|
+
.pop-b { display: flex; align-items: center; justify-content: center; min-height: 80px; padding: 8px; }
|
|
176
|
+
.pop-b img { display: block; max-width: 100%; max-height: 220px; border-radius: 4px; }
|
|
177
|
+
@media (max-width: 640px) {
|
|
178
|
+
.wrap { margin: 1rem auto; } td { padding: 8px .75rem; }
|
|
179
|
+
.dt { display: none; } .bar { padding: .625rem .75rem; } .pop { display: none; }
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
182
|
+
</head>
|
|
183
|
+
<body>
|
|
184
|
+
<div class="wrap"><div class="card">
|
|
185
|
+
<div class="bar"><div class="crumbs">#{crumbs}</div><div class="meta">#{counts.join(' · ')}</div></div>
|
|
186
|
+
<table>
|
|
187
|
+
<thead><tr><th></th><th>Name</th><th class="sz">Size</th><th class="dt">Modified</th></tr></thead>
|
|
188
|
+
<tbody>#{rows.join('\n')}</tbody>
|
|
189
|
+
</table>
|
|
190
|
+
</div></div>
|
|
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)}();
|
|
193
|
+
</script>
|
|
194
|
+
</body>
|
|
195
|
+
</html>
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
# --- Markdown rendering ---
|
|
199
|
+
|
|
200
|
+
export renderMarkdown = (filePath) ->
|
|
201
|
+
md = readFileSync(filePath, 'utf8')
|
|
202
|
+
body = Bun.markdown.html(md)
|
|
203
|
+
name = esc(basename(filePath))
|
|
204
|
+
"""
|
|
205
|
+
<!DOCTYPE html>
|
|
206
|
+
<html lang="en">
|
|
207
|
+
<head>
|
|
208
|
+
<meta charset="UTF-8">
|
|
209
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
210
|
+
<title>#{name}</title>
|
|
211
|
+
<style>
|
|
212
|
+
:root { --bg: #fff; --text: #1a1d21; --muted: #6b7280; --link: #2563eb; --code-bg: #f4f4f5; --border: #e2e5e9; --pre-bg: #fafafa; }
|
|
213
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #0a0a0c; --text: #e4e4e7; --muted: #71717a; --link: #60a5fa; --code-bg: #1e1e22; --border: #2a2a2e; --pre-bg: #161618; } }
|
|
214
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
215
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
|
|
216
|
+
font-size: 16px; line-height: 1.7; color: var(--text); background: var(--bg);
|
|
217
|
+
max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem;
|
|
218
|
+
-webkit-font-smoothing: antialiased; }
|
|
219
|
+
h1, h2, h3, h4 { margin: 1.75em 0 .5em; line-height: 1.3; }
|
|
220
|
+
h1 { font-size: 1.75rem; } h2 { font-size: 1.375rem; } h3 { font-size: 1.125rem; }
|
|
221
|
+
p, ul, ol, blockquote, pre, table { margin: 0 0 1em; }
|
|
222
|
+
a { color: var(--link); text-decoration: none; } a:hover { text-decoration: underline; }
|
|
223
|
+
code { font-family: 'SF Mono', 'Fira Code', monospace; font-size: .875em; background: var(--code-bg); padding: 2px 5px; border-radius: 4px; }
|
|
224
|
+
pre { background: var(--pre-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; overflow-x: auto; }
|
|
225
|
+
pre code { background: none; padding: 0; font-size: .8125rem; }
|
|
226
|
+
blockquote { border-left: 3px solid var(--border); padding-left: 1rem; color: var(--muted); }
|
|
227
|
+
img { max-width: 100%; border-radius: 6px; }
|
|
228
|
+
table { border-collapse: collapse; width: 100%; }
|
|
229
|
+
th, td { padding: .5rem .75rem; border: 1px solid var(--border); text-align: left; font-size: .875rem; }
|
|
230
|
+
th { font-weight: 600; background: var(--pre-bg); }
|
|
231
|
+
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
|
232
|
+
@page { margin: 0.5in; }
|
|
233
|
+
@media print {
|
|
234
|
+
body { background: #fff; color: #1a1a1a; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
235
|
+
pre { background: #f4f4f5; box-shadow: inset 0 0 0 1000px #f4f4f5; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
236
|
+
code { background: #f0f0f2; box-shadow: inset 0 0 0 1000px #f0f0f2; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
237
|
+
pre code { background: none; box-shadow: none; }
|
|
238
|
+
th { background: #f6f8fa; box-shadow: inset 0 0 0 1000px #f6f8fa; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
239
|
+
a { color: #2563eb; }
|
|
240
|
+
}
|
|
241
|
+
</style>
|
|
242
|
+
</head>
|
|
243
|
+
<body>#{body}</body>
|
|
244
|
+
</html>
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
# --- Text file rendering with syntax highlighting ---
|
|
248
|
+
|
|
249
|
+
TEXT_EXTS = new Set [
|
|
250
|
+
'js', 'mjs', 'cjs', 'ts', 'tsx', 'jsx', 'json', 'jsonc'
|
|
251
|
+
'css', 'scss', 'less', 'html', 'htm', 'xml', 'svg'
|
|
252
|
+
'rip', 'coffee', 'rb', 'py', 'go', 'rs', 'c', 'cpp', 'h', 'hpp', 'java', 'kt', 'swift', 'cs'
|
|
253
|
+
'sh', 'bash', 'zsh', 'fish', 'ps1', 'bat', 'cmd'
|
|
254
|
+
'yaml', 'yml', 'toml', 'ini', 'conf', 'cfg', 'env'
|
|
255
|
+
'md', 'txt', 'log', 'csv', 'tsv', 'diff', 'patch'
|
|
256
|
+
'sql', 'graphql', 'gql', 'prisma'
|
|
257
|
+
'dockerfile', 'makefile', 'gemfile', 'rakefile'
|
|
258
|
+
'gitignore', 'gitattributes', 'editorconfig', 'eslintrc', 'prettierrc'
|
|
259
|
+
'lock', 'license', 'licence', 'readme', 'changelog', 'contributing', 'authors'
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
HLJS_LANG_MAP =
|
|
263
|
+
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript'
|
|
264
|
+
ts: 'typescript', tsx: 'typescript'
|
|
265
|
+
rb: 'ruby', py: 'python', rs: 'rust', go: 'go', sh: 'bash', zsh: 'bash'
|
|
266
|
+
yml: 'yaml', toml: 'ini', conf: 'ini', cfg: 'ini'
|
|
267
|
+
rip: 'rip', coffee: 'coffeescript'
|
|
268
|
+
md: 'markdown', htm: 'xml', svg: 'xml'
|
|
269
|
+
dockerfile: 'dockerfile', makefile: 'makefile'
|
|
270
|
+
jsonc: 'json', gql: 'graphql'
|
|
271
|
+
|
|
272
|
+
export isTextFile = (filePath) ->
|
|
273
|
+
name = basename(filePath).toLowerCase()
|
|
274
|
+
ext = name.slice(name.lastIndexOf('.') + 1)
|
|
275
|
+
TEXT_EXTS.has(ext) or TEXT_EXTS.has(name)
|
|
276
|
+
|
|
277
|
+
hljsLang = (filePath) ->
|
|
278
|
+
name = basename(filePath).toLowerCase()
|
|
279
|
+
ext = name.slice(name.lastIndexOf('.') + 1)
|
|
280
|
+
HLJS_LANG_MAP[ext] or HLJS_LANG_MAP[name] or ext
|
|
281
|
+
|
|
282
|
+
export renderTextFile = (filePath) ->
|
|
283
|
+
content = readFileSync(filePath, 'utf8')
|
|
284
|
+
name = basename(filePath)
|
|
285
|
+
lang = hljsLang(filePath)
|
|
286
|
+
"""
|
|
287
|
+
<!DOCTYPE html>
|
|
288
|
+
<html lang="en">
|
|
289
|
+
<head>
|
|
290
|
+
<meta charset="UTF-8">
|
|
291
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
292
|
+
<title>#{esc name}</title>
|
|
293
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)">
|
|
294
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" media="(prefers-color-scheme: dark)">
|
|
295
|
+
<style>
|
|
296
|
+
:root { --bg: #fff; --text: #1a1d21; --border: #e2e5e9; --bar-bg: #f3f4f6; --secondary: #6b7280; }
|
|
297
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #0d1117; --text: #e4e4e7; --border: #2a2a2e; --bar-bg: #161618; --secondary: #71717a; } }
|
|
298
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
299
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif; background: var(--bg); color: var(--text); }
|
|
300
|
+
.wrap { max-width: 960px; margin: 3rem auto; padding: 0 1.25rem; }
|
|
301
|
+
.card { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.05); }
|
|
302
|
+
.bar { display: flex; align-items: center; justify-content: space-between; padding: .625rem 1.25rem; background: var(--bar-bg); border-bottom: 1px solid var(--border); }
|
|
303
|
+
.name { font-size: 13px; font-weight: 600; }
|
|
304
|
+
.meta { font-size: 12px; color: var(--secondary); }
|
|
305
|
+
pre { margin: 0; border: none; border-radius: 0; }
|
|
306
|
+
pre code, pre code.hljs { display: block; padding: 1rem 1.25rem 1.5rem; font-size: 13px; line-height: 1.6; font-family: ui-monospace, 'SF Mono', 'Fira Code', monospace; overflow-x: auto; }
|
|
307
|
+
@page { margin: 0.5in; }
|
|
308
|
+
@media print { .bar { background: #f3f4f6; box-shadow: inset 0 0 0 1000px #f3f4f6; -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
|
309
|
+
</style>
|
|
310
|
+
</head>
|
|
311
|
+
<body>
|
|
312
|
+
<div class="wrap"><div class="card">
|
|
313
|
+
<div class="bar"><span class="name">#{esc name}</span><span class="meta">#{n = content.split('\\n').length}#{n} line#{if n isnt 1 then 's' else ''}</span></div>
|
|
314
|
+
<pre><code class="language-#{lang}">#{esc content}</code></pre>
|
|
315
|
+
</div></div>
|
|
316
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
|
317
|
+
#{if lang is 'rip' and HLJS_RIP_PATH then '<script src="/_rip/hljs-rip.js"></script><script>hljs.registerLanguage("rip",hljsRip);hljs.highlightAll()</script>' else '<script>hljs.highlightAll()</script>'}
|
|
318
|
+
</body>
|
|
319
|
+
</html>
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
# --- Static file serving ---
|
|
323
|
+
|
|
324
|
+
export serveRipHighlightGrammar = ->
|
|
325
|
+
return null unless HLJS_RIP_PATH
|
|
326
|
+
content = readFileSync(HLJS_RIP_PATH, 'utf8')
|
|
327
|
+
wrapped = content.replace('export default function', 'var hljsRip = function')
|
|
328
|
+
file = Bun.file(HLJS_RIP_PATH)
|
|
329
|
+
etag = "W/\"#{file.lastModified}-#{file.size}\""
|
|
330
|
+
new Response(wrapped, { headers: { 'content-type': 'application/javascript; charset=UTF-8', 'etag': etag, 'cache-control': 'public, max-age=86400' } })
|
|
331
|
+
|
|
332
|
+
export serveStaticRoute = (req, url, route) ->
|
|
333
|
+
method = req.method or 'GET'
|
|
334
|
+
return new Response(null, { status: 405 }) unless method is 'GET' or method is 'HEAD'
|
|
335
|
+
|
|
336
|
+
if url.pathname is '/_rip/hljs-rip.js'
|
|
337
|
+
res = serveRipHighlightGrammar()
|
|
338
|
+
return res if res
|
|
339
|
+
|
|
340
|
+
base = route.root or route.static
|
|
341
|
+
return new Response('Not Found', { status: 404 }) unless base
|
|
342
|
+
|
|
343
|
+
staticDir = if route.static and route.static isnt '.'
|
|
344
|
+
if route.static.startsWith('/') then route.static else resolve(base, route.static)
|
|
345
|
+
else
|
|
346
|
+
base
|
|
347
|
+
|
|
348
|
+
pathname = try decodeURIComponent(url.pathname) catch then url.pathname
|
|
349
|
+
relative = stripRoutePrefix(pathname, route.path or '/*')
|
|
350
|
+
filePath = resolve(staticDir, '.' + relative)
|
|
351
|
+
|
|
352
|
+
return new Response('Forbidden', { status: 403 }) unless isSafeWithinRoot(staticDir, filePath)
|
|
353
|
+
|
|
354
|
+
try
|
|
355
|
+
stat = statSync(filePath)
|
|
356
|
+
if stat.isDirectory()
|
|
357
|
+
unless pathname.endsWith('/')
|
|
358
|
+
return Response.redirect("#{pathname}/", 301)
|
|
359
|
+
indexPath = join(filePath, 'index.html')
|
|
360
|
+
try
|
|
361
|
+
indexStat = statSync(indexPath)
|
|
362
|
+
if indexStat.isFile()
|
|
363
|
+
file = Bun.file(indexPath)
|
|
364
|
+
return new Response(file, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
365
|
+
if route.browse and acceptsHtml(req)
|
|
366
|
+
rootName = basename(staticDir)
|
|
367
|
+
html = renderDirectoryListing(pathname, filePath, rootName)
|
|
368
|
+
return new Response(html, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
369
|
+
if stat.isFile()
|
|
370
|
+
if route.browse and acceptsHtml(req)
|
|
371
|
+
if filePath.endsWith('.md')
|
|
372
|
+
html = renderMarkdown(filePath)
|
|
373
|
+
return new Response(html, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
374
|
+
if isTextFile(filePath)
|
|
375
|
+
html = renderTextFile(filePath)
|
|
376
|
+
return new Response(html, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
377
|
+
file = Bun.file(filePath)
|
|
378
|
+
return new Response(file, { headers: { 'content-type': mimeType(filePath) } })
|
|
379
|
+
|
|
380
|
+
if route.spa and acceptsHtml(req)
|
|
381
|
+
spaPath = join(staticDir, 'index.html')
|
|
382
|
+
try
|
|
383
|
+
spaStat = statSync(spaPath)
|
|
384
|
+
if spaStat.isFile()
|
|
385
|
+
file = Bun.file(spaPath)
|
|
386
|
+
return new Response(file, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
|
|
387
|
+
|
|
388
|
+
new Response('Not Found', { status: 404 })
|
|
389
|
+
|
|
390
|
+
export buildRedirectResponse = (req, url, route) ->
|
|
391
|
+
target = route.redirect?.to or '/'
|
|
392
|
+
status = route.redirect?.status or 302
|
|
393
|
+
Response.redirect(target, status)
|
package/{edge → serving}/tls.rip
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/tls.rip — TLS material loading helpers
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
import { readFileSync } from 'node:fs'
|
|
@@ -29,13 +29,13 @@ export buildTlsArray = (defaultMaterial, certMap) ->
|
|
|
29
29
|
result.push({ serverName: fallbackName, cert: defaultMaterial.cert, key: defaultMaterial.key })
|
|
30
30
|
result
|
|
31
31
|
|
|
32
|
-
export printCertSummary = (certPem) ->
|
|
32
|
+
export printCertSummary = (certPem, quiet = false) ->
|
|
33
33
|
try
|
|
34
34
|
x = new X509Certificate(certPem)
|
|
35
35
|
subject = x.subject.split(/,/)[0]?.trim() or x.subject
|
|
36
36
|
issuer = x.issuer.split(/,/)[0]?.trim() or x.issuer
|
|
37
37
|
exp = new Date(x.validTo)
|
|
38
|
-
p "rip-server: tls cert #{subject} issued by #{issuer} expires #{exp.toISOString()}"
|
|
38
|
+
p "rip-server: tls cert #{subject} issued by #{issuer} expires #{exp.toISOString()}" unless quiet
|
|
39
39
|
|
|
40
40
|
export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
|
|
41
41
|
# Explicit cert/key paths
|
|
@@ -43,7 +43,6 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
|
|
|
43
43
|
try
|
|
44
44
|
cert = readFileSync(flags.certPath, 'utf8')
|
|
45
45
|
key = readFileSync(flags.keyPath, 'utf8')
|
|
46
|
-
printCertSummary(cert)
|
|
47
46
|
return { cert, key }
|
|
48
47
|
catch
|
|
49
48
|
console.error 'Failed to read TLS cert/key from provided paths. Use http or fix paths.'
|
|
@@ -53,8 +52,6 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
|
|
|
53
52
|
if acmeLoadCertFn and (flags.acme or flags.acmeStaging) and flags.acmeDomain
|
|
54
53
|
acmeCert = acmeLoadCertFn(flags.acmeDomain)
|
|
55
54
|
if acmeCert
|
|
56
|
-
p "rip-server: using ACME cert for #{flags.acmeDomain}"
|
|
57
|
-
printCertSummary(acmeCert.cert)
|
|
58
55
|
return { cert: acmeCert.cert, key: acmeCert.key }
|
|
59
56
|
|
|
60
57
|
# Shipped wildcard cert for *.ripdev.io (GlobalSign, valid for all subdomains)
|
|
@@ -64,7 +61,6 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
|
|
|
64
61
|
try
|
|
65
62
|
cert = readFileSync(certPath, 'utf8')
|
|
66
63
|
key = readFileSync(keyPath, 'utf8')
|
|
67
|
-
printCertSummary(cert)
|
|
68
64
|
return { cert, key }
|
|
69
65
|
catch e
|
|
70
66
|
console.error "rip-server: failed to load TLS certs from #{certsDir}: #{e.message}"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/upstream.rip — upstream pools, health checks, and retry helpers
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
DEFAULT_HEALTH =
|
|
@@ -228,12 +228,12 @@ export checkTargetHealth = (pool, target, fetchFn = null) ->
|
|
|
228
228
|
runFetch = fetchFn or pool.fetchFn or fetch
|
|
229
229
|
url = target.url + target.health.path
|
|
230
230
|
try
|
|
231
|
-
res = runFetch(url)
|
|
232
|
-
res = res! if res?.then
|
|
231
|
+
res = await runFetch(url)
|
|
233
232
|
target.lastStatus = res.status
|
|
234
233
|
updateTargetHealth(target, res.ok, pool)
|
|
235
234
|
res.ok
|
|
236
|
-
catch
|
|
235
|
+
catch e
|
|
236
|
+
console.error "[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
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
#
|
|
2
|
+
# serving/verify.rip — post-activate runtime verification
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
export buildVerificationResult = (ok, code = null, message = null, details = null) ->
|
|
6
6
|
{ ok, code, message, details }
|
|
7
7
|
|
|
8
8
|
defaultPolicy =
|
|
9
|
-
|
|
9
|
+
requireHealthyProxies: true
|
|
10
10
|
requireReadyApps: true
|
|
11
11
|
includeUnroutedManagedApps: true
|
|
12
|
-
|
|
12
|
+
minHealthyTargetsPerProxy: 1
|
|
13
13
|
|
|
14
14
|
mergePolicy = (policy = {}) ->
|
|
15
|
-
|
|
15
|
+
requireHealthyProxies: if policy.requireHealthyProxies? then policy.requireHealthyProxies is true else defaultPolicy.requireHealthyProxies
|
|
16
16
|
requireReadyApps: if policy.requireReadyApps? then policy.requireReadyApps is true else defaultPolicy.requireReadyApps
|
|
17
17
|
includeUnroutedManagedApps: if policy.includeUnroutedManagedApps? then policy.includeUnroutedManagedApps is true else defaultPolicy.includeUnroutedManagedApps
|
|
18
|
-
|
|
18
|
+
minHealthyTargetsPerProxy: Math.max(1, parseInt(String(policy.minHealthyTargetsPerProxy or defaultPolicy.minHealthyTargetsPerProxy)) or 1)
|
|
19
19
|
|
|
20
20
|
export collectRouteRequirements = (routeTable, appRegistry = null, defaultAppId = null, policy = {}) ->
|
|
21
21
|
policy = mergePolicy(policy)
|
|
22
|
-
|
|
22
|
+
proxyIds = new Set()
|
|
23
23
|
appIds = new Set()
|
|
24
24
|
|
|
25
25
|
for route in (routeTable?.routes or [])
|
|
26
|
-
|
|
26
|
+
proxyIds.add(route.proxy) if route.proxy
|
|
27
27
|
appIds.add(route.app) if route.app
|
|
28
28
|
|
|
29
29
|
if appRegistry and policy.includeUnroutedManagedApps
|
|
@@ -33,7 +33,7 @@ export collectRouteRequirements = (routeTable, appRegistry = null, defaultAppId
|
|
|
33
33
|
appIds.add(appId)
|
|
34
34
|
|
|
35
35
|
{
|
|
36
|
-
|
|
36
|
+
proxyIds: Array.from(proxyIds)
|
|
37
37
|
appIds: Array.from(appIds)
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -41,21 +41,21 @@ export verifyRouteRuntime = (runtime, appRegistry, defaultAppId, getUpstreamFn,
|
|
|
41
41
|
policy = mergePolicy(policy)
|
|
42
42
|
requirements = collectRouteRequirements(runtime?.routeTable, appRegistry, defaultAppId, policy)
|
|
43
43
|
|
|
44
|
-
if policy.
|
|
45
|
-
for
|
|
46
|
-
upstream = getUpstreamFn(runtime.upstreamPool,
|
|
47
|
-
return buildVerificationResult(false, '
|
|
44
|
+
if policy.requireHealthyProxies
|
|
45
|
+
for proxyId in requirements.proxyIds
|
|
46
|
+
upstream = getUpstreamFn(runtime.upstreamPool, proxyId)
|
|
47
|
+
return buildVerificationResult(false, 'proxy_missing', "Referenced proxy #{proxyId} is missing", { proxyId }) unless upstream
|
|
48
48
|
|
|
49
49
|
healthyTargets = []
|
|
50
50
|
for target in upstream.targets
|
|
51
51
|
healthyTargets.push(target.targetId) if checkTargetHealthFn(runtime.upstreamPool, target)
|
|
52
52
|
|
|
53
|
-
unless healthyTargets.length >= policy.
|
|
53
|
+
unless healthyTargets.length >= policy.minHealthyTargetsPerProxy
|
|
54
54
|
return buildVerificationResult(
|
|
55
55
|
false
|
|
56
|
-
'
|
|
57
|
-
"
|
|
58
|
-
{
|
|
56
|
+
'proxy_no_healthy_targets'
|
|
57
|
+
"Proxy #{proxyId} has too few healthy targets after activation"
|
|
58
|
+
{ proxyId, targetCount: upstream.targets.length, healthyTargets: healthyTargets.length, requiredHealthyTargets: policy.minHealthyTargetsPerProxy }
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
if policy.requireReadyApps
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
# streams/
|
|
2
|
+
# streams/clienthello.rip — minimal ClientHello SNI extractor
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
5
|
MAX_CLIENT_HELLO_BYTES = 16384
|
package/streams/config.rip
CHANGED
|
@@ -93,28 +93,28 @@ normalizeStreamTimeouts = (value, path, errors) ->
|
|
|
93
93
|
connectMs: normalizePositiveInt(value.connectMs, 5000, "#{path}.connectMs", errors, 'connectMs')
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
export normalizeStreams = (rawStreams,
|
|
96
|
+
export normalizeStreams = (rawStreams, knownProxies, errors, path = 'streams') ->
|
|
97
97
|
streams = []
|
|
98
98
|
return streams unless rawStreams?
|
|
99
99
|
unless Array.isArray(rawStreams)
|
|
100
|
-
pushError(errors, 'E_STREAMS_TYPE', path, 'streams must be an array', "Use `streams: [{ listen: 8443, sni: ['incus.example.com'],
|
|
100
|
+
pushError(errors, 'E_STREAMS_TYPE', path, 'streams must be an array', "Use `streams: [{ listen: 8443, sni: ['incus.example.com'], proxy: 'incus' }]`.")
|
|
101
101
|
return streams
|
|
102
102
|
for route, idx in rawStreams
|
|
103
103
|
itemPath = "#{path}[#{idx}]"
|
|
104
104
|
unless isPlainObject(route)
|
|
105
|
-
pushError(errors, 'E_STREAM_ROUTE_TYPE', itemPath, 'stream route must be an object', "Use `{ listen: 8443, sni: ['incus.example.com'],
|
|
105
|
+
pushError(errors, 'E_STREAM_ROUTE_TYPE', itemPath, 'stream route must be an object', "Use `{ listen: 8443, sni: ['incus.example.com'], proxy: 'incus' }`.")
|
|
106
106
|
continue
|
|
107
107
|
listen = normalizePositiveInt(route.listen, 0, "#{itemPath}.listen", errors, 'listen')
|
|
108
108
|
pushError(errors, 'E_STREAM_LISTEN_RANGE', "#{itemPath}.listen", 'listen port must be between 1 and 65535', 'Choose a valid TCP port.') unless listen > 0 and listen <= 65535
|
|
109
|
-
unless typeof route.
|
|
110
|
-
pushError(errors, '
|
|
111
|
-
else unless
|
|
112
|
-
pushError(errors, '
|
|
109
|
+
unless typeof route.proxy is 'string' and route.proxy
|
|
110
|
+
pushError(errors, 'E_STREAM_PROXY_REF', "#{itemPath}.proxy", 'stream route must reference a TCP proxy', "Use `proxy: 'incus'`.")
|
|
111
|
+
else unless knownProxies.has(route.proxy)
|
|
112
|
+
pushError(errors, 'E_STREAM_PROXY_REF', "#{itemPath}.proxy", "unknown TCP proxy #{route.proxy}", 'Define the proxy under `proxies` with host:port entries first.')
|
|
113
113
|
streams.push
|
|
114
114
|
id: route.id or "stream-#{idx + 1}"
|
|
115
115
|
order: idx
|
|
116
116
|
listen: listen
|
|
117
117
|
sni: normalizeSniPatterns(route.sni, "#{itemPath}.sni", errors)
|
|
118
|
-
|
|
118
|
+
proxy: route.proxy or null
|
|
119
119
|
timeouts: normalizeStreamTimeouts(route.timeouts, "#{itemPath}.timeouts", errors)
|
|
120
120
|
streams
|
package/streams/index.rip
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# streams/index.rip — stream runtime facade and listener orchestration
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
|
-
import { parseSNI } from './
|
|
6
|
-
import { compileStreamTable, matchStreamRoute
|
|
5
|
+
import { parseSNI } from './clienthello.rip'
|
|
6
|
+
import { compileStreamTable, matchStreamRoute } from './router.rip'
|
|
7
7
|
import { createStreamRuntime, buildStreamConfigInfo, describeStreamRuntime } from './runtime.rip'
|
|
8
8
|
import { createStreamUpstreamPool, addStreamUpstream, getStreamUpstream, selectStreamTarget, openStreamConnection, releaseStreamConnection } from './upstream.rip'
|
|
9
9
|
import { createPipeState, writeChunk, flushPending, markPaused, markResumed } from './pipe.rip'
|
|
@@ -20,7 +20,7 @@ concatBytes = (a, b) ->
|
|
|
20
20
|
export resolveHandshakeTarget = (runtime, listenPort, sni, httpFallback = {}) ->
|
|
21
21
|
route = matchStreamRoute(runtime.routeTable, listenPort, sni)
|
|
22
22
|
if route
|
|
23
|
-
upstream = getStreamUpstream(runtime.upstreamPool, route.
|
|
23
|
+
upstream = getStreamUpstream(runtime.upstreamPool, route.proxy)
|
|
24
24
|
unless upstream
|
|
25
25
|
return {
|
|
26
26
|
kind: 'reject'
|
|
@@ -52,8 +52,8 @@ export resolveHandshakeTarget = (runtime, listenPort, sni, httpFallback = {}) ->
|
|
|
52
52
|
export buildStreamRuntime = (normalized) ->
|
|
53
53
|
routeTable = compileStreamTable(normalized.streams or [])
|
|
54
54
|
upstreamPool = createStreamUpstreamPool()
|
|
55
|
-
for
|
|
56
|
-
addStreamUpstream(upstreamPool,
|
|
55
|
+
for proxyId, upstreamConfig of (normalized.streamUpstreams or {})
|
|
56
|
+
addStreamUpstream(upstreamPool, proxyId, upstreamConfig)
|
|
57
57
|
configInfo = buildStreamConfigInfo(normalized)
|
|
58
58
|
createStreamRuntime(configInfo, upstreamPool, routeTable)
|
|
59
59
|
|
package/streams/router.rip
CHANGED
|
@@ -30,7 +30,7 @@ export compileStreamTable = (routes = []) ->
|
|
|
30
30
|
id: route.id
|
|
31
31
|
order: route.order or 0
|
|
32
32
|
listen: route.listen
|
|
33
|
-
|
|
33
|
+
proxy: route.proxy
|
|
34
34
|
sni: pattern
|
|
35
35
|
sniKind: sniKind(pattern)
|
|
36
36
|
timeouts: route.timeouts or {}
|
|
@@ -54,4 +54,4 @@ export matchStreamRoute = (table, listenPort, sni) ->
|
|
|
54
54
|
|
|
55
55
|
export describeStreamRoute = (route) ->
|
|
56
56
|
return null unless route
|
|
57
|
-
"#{route.listen} #{route.sni} -> #{route.
|
|
57
|
+
"#{route.listen} #{route.sni} -> #{route.proxy}"
|
package/tests/acme.rip
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# test-acme.rip — ACME challenges, store, and crypto tests
|
|
2
2
|
|
|
3
|
-
import { test, eq, ok } from '
|
|
3
|
+
import { test, eq, ok } from './runner.rip'
|
|
4
4
|
import { createChallengeStore, handleChallengeRequest } from '../acme/store.rip'
|
|
5
5
|
import { defaultCertDir, ensureDir, saveCert, loadCert, needsRenewal, listCerts } from '../acme/store.rip'
|
|
6
6
|
import { b64url, generateAccountKeyPair, exportPublicJwk, thumbprint, signJws } from '../acme/crypto.rip'
|