wiki-plugin-hitchhiker 0.3.0 → 0.3.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/package.json +1 -1
- package/server/server.js +38 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-plugin-hitchhiker",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "House UX + default-pages plugin for the hitchhiker farms: ships default pages (Tools, Changes to this Site) and hosts the wiki-ux.js client override (friendlier factory chooser + page-flag tool page), loaded globally by the wiki-security-hitchhiker bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
package/server/server.js
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
// the client: on any failure it returns a minimal card so a drop always works.
|
|
18
18
|
//
|
|
19
19
|
// v1 scope: private farm (owner-only). Before public use, harden SSRF against
|
|
20
|
-
// redirect-following
|
|
21
|
-
//
|
|
20
|
+
// redirect-following: since 0.3.1 the OpenGraph and image fetches walk redirects
|
|
21
|
+
// by hand and re-check every hop (safeFetch); the oEmbed endpoint hop remains
|
|
22
|
+
// the library's own — see fromOembed.
|
|
22
23
|
|
|
23
24
|
import dns from 'node:dns/promises'
|
|
24
25
|
import net from 'node:net'
|
|
@@ -116,12 +117,34 @@ const assertPublicUrl = async raw => {
|
|
|
116
117
|
return u
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// ── a fetch that re-checks every hop ─────────────────────────────────────────
|
|
121
|
+
// redirect:'follow' would let a public host 302 us onto 127.0.0.1 after the
|
|
122
|
+
// guard has already passed the starting URL. So follow by hand: manual
|
|
123
|
+
// redirects, assertPublicUrl on each Location, and a small hop cap.
|
|
124
|
+
const MAX_HOPS = 4
|
|
125
|
+
const safeFetch = async (raw, init = {}) => {
|
|
126
|
+
let url = raw
|
|
127
|
+
for (let hop = 0; hop <= MAX_HOPS; hop++) {
|
|
128
|
+
await assertPublicUrl(url)
|
|
129
|
+
const r = await fetch(url, { ...init, redirect: 'manual' })
|
|
130
|
+
if (r.status >= 300 && r.status < 400) {
|
|
131
|
+
const loc = r.headers.get('location')
|
|
132
|
+
if (!loc) return r
|
|
133
|
+
url = new URL(loc, url).href
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
return r
|
|
137
|
+
}
|
|
138
|
+
throw new Error('too many redirects')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const __safeFetchForTest = (...a) => safeFetch(...a)
|
|
142
|
+
|
|
119
143
|
// ── thumbnail → data URI (no client→provider request when the card renders) ───
|
|
120
144
|
const inlineImage = async imgUrl => {
|
|
121
145
|
if (!imgUrl) return ''
|
|
122
146
|
try {
|
|
123
|
-
await
|
|
124
|
-
const r = await fetch(imgUrl, { signal: AbortSignal.timeout(FETCH_MS), redirect: 'follow' })
|
|
147
|
+
const r = await safeFetch(imgUrl, { signal: AbortSignal.timeout(FETCH_MS) })
|
|
125
148
|
if (!r.ok) return ''
|
|
126
149
|
const type = r.headers.get('content-type') || ''
|
|
127
150
|
if (!/^image\//i.test(type)) return ''
|
|
@@ -138,6 +161,12 @@ const inlineImage = async imgUrl => {
|
|
|
138
161
|
// ── extractors ────────────────────────────────────────────────────────────────
|
|
139
162
|
const fromOembed = async url => {
|
|
140
163
|
try {
|
|
164
|
+
// Residual: the extractor discovers an oEmbed endpoint from the page and
|
|
165
|
+
// fetches it with its own client. The starting URL is checked by the route,
|
|
166
|
+
// the thumbnail it returns is re-checked by inlineImage, but the endpoint
|
|
167
|
+
// hop itself is the library's. Acceptable on a host that only unfurls
|
|
168
|
+
// public URLs; not acceptable as the only guard. Hence the redirect-checked
|
|
169
|
+
// path for OpenGraph and the image, which are the two fetches we control.
|
|
141
170
|
const d = await oembedExtract(url, {}, { signal: AbortSignal.timeout(FETCH_MS) })
|
|
142
171
|
if (!d) return null
|
|
143
172
|
return {
|
|
@@ -156,7 +185,11 @@ const fromOembed = async url => {
|
|
|
156
185
|
|
|
157
186
|
const fromOpenGraph = async url => {
|
|
158
187
|
try {
|
|
159
|
-
|
|
188
|
+
// Walk redirects ourselves so every hop is checked, then hand the library
|
|
189
|
+
// the final URL with redirects off. It cannot be led anywhere we did not look.
|
|
190
|
+
const probe = await safeFetch(url, { method: 'GET', signal: AbortSignal.timeout(FETCH_MS) })
|
|
191
|
+
const finalUrl = probe.url || url
|
|
192
|
+
const r = await ogs({ url: finalUrl, fetchOptions: { signal: AbortSignal.timeout(FETCH_MS), redirect: 'manual' } })
|
|
160
193
|
if (!r || r.error || !r.result) return null
|
|
161
194
|
const x = r.result
|
|
162
195
|
const img =
|