forked from hplush/slowreader
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
162 lines (142 loc) · 4.69 KB
/
index.ts
File metadata and controls
162 lines (142 loc) · 4.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import type { IncomingMessage, ServerResponse } from 'node:http'
import { isIP } from 'node:net'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { styleText } from 'node:util'
class BadRequestError extends Error {
code: number
constructor(message: string, code = 400) {
super(message)
this.name = 'BadRequestError'
this.code = code
}
}
export interface ProxyConfig {
allowLocalhost?: boolean
allowsFrom: string
bodyTimeout: number
maxSize: number
requestTimeout: number
}
export const DEFAULT_PROXY_CONFIG: Omit<ProxyConfig, 'allowsFrom'> = {
bodyTimeout: 10000,
maxSize: 10 * 1024 * 1024,
requestTimeout: 10000
}
function allowCors(res: ServerResponse, origin: string): void {
res.setHeader('Access-Control-Allow-Headers', '*')
res.setHeader(
'Access-Control-Allow-Methods',
'OPTIONS, POST, GET, PUT, DELETE'
)
res.setHeader('Access-Control-Allow-Origin', origin)
}
export function createProxy(
config: ProxyConfig
): (req: IncomingMessage, res: ServerResponse) => void {
let allowsFrom = new RegExp(config.allowsFrom)
return async (req, res) => {
let sent = false
/* node:coverage disable */
function sendError(statusCode: number, message: string): void {
if (!sent) {
res.writeHead(statusCode, { 'Content-Type': 'text/plain' })
res.end(message)
} else {
res.end()
}
}
/* node:coverage enable */
if (req.method === 'OPTIONS' && req.headers.origin) {
allowCors(res, req.headers.origin)
res.setHeader('Access-Control-Max-Age', '600')
res.writeHead(204)
return res.end()
}
try {
if (req.headers.origin) {
allowCors(res, req.headers.origin)
}
let url = decodeURIComponent(req.url!.slice(1).replace(/^proxy\//, ''))
let parsedUrl = new URL(url)
// Only HTTP or HTTPS protocols are allowed
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new BadRequestError('Only HTTP or HTTPS are supported')
}
// We do not typically need non-GET to load RSS
if (req.method !== 'GET') {
throw new BadRequestError('Only GET is allowed', 405)
}
// We only allow request from our app
let origin = req.headers.origin
if (!origin && req.headers.referer) {
origin = new URL(req.headers.referer).origin
}
if (!origin || !allowsFrom.test(origin)) {
throw new BadRequestError(
`Unauthorized Origin. Only ${allowsFrom} is allowed.`
)
}
if (
(!config.allowLocalhost && parsedUrl.hostname === 'localhost') ||
isIP(parsedUrl.hostname) !== 0
) {
throw new BadRequestError('IP addresses are not allowed')
}
// Remove all cookie headers so they will not be set on proxy domain
delete req.headers.cookie
delete req.headers['set-cookie']
delete req.headers.host
let targetResponse = await fetch(url, {
headers: {
...(req.headers as Record<string, string>),
'host': parsedUrl.host,
'X-Forwarded-For': req.socket.remoteAddress!
},
method: req.method,
signal: AbortSignal.timeout(config.requestTimeout)
})
let length: number | undefined
if (targetResponse.headers.has('content-length')) {
length = parseInt(targetResponse.headers.get('content-length')!)
}
if (length && length > config.maxSize) {
throw new BadRequestError('Response too large', 413)
}
res.writeHead(targetResponse.status, {
'Content-Type':
targetResponse.headers.get('content-type') ?? 'text/plain'
})
sent = true
if (targetResponse.body) {
// @ts-expect-error Until Node.js types are broken
let nodeStream = Readable.fromWeb(targetResponse.body)
await pipeline(nodeStream, res, {
signal: AbortSignal.timeout(config.bodyTimeout)
})
}
res.end()
} catch (e) {
/* node:coverage disable */
// Known errors
if (e instanceof Error && e.name === 'TimeoutError') {
sendError(400, 'Timeout')
return
} else if (e instanceof Error && e.message === 'Invalid URL') {
sendError(400, 'Invalid URL')
return
} else if (e instanceof BadRequestError) {
sendError(e.code, e.message)
return
}
// Unknown or Internal errors
if (e instanceof Error) {
process.stderr.write(styleText('red', e.stack ?? e.message) + '\n')
} else if (typeof e === 'string') {
process.stderr.write(styleText('red', e) + '\n')
}
sendError(500, 'Internal Server Error')
}
/* node:coverage enable */
}
}