├── .gitignore
├── README.md
├── Unlicense
├── bin
└── index.js
├── package-lock.json
├── package.json
└── src
├── files.js
├── index.js
├── mimes.js
├── proxy.js
├── request.js
├── shared.js
└── ws.js
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log*
2 | node_modules
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ey
2 |
3 | Ey is an ergonomic and fast server+router, built on [uWebSockets](https://github.com/uNetworking/uWebSockets.js) (for now). It has a simple interface embracing todays JS features.
4 |
5 | ## Getting Started
6 |
7 | ```javascript
8 | import ey from 'ey'
9 |
10 | const app = ey()
11 |
12 | app.get('/', r => r.end('Hello World'))
13 |
14 | const { port } = await app.listen(process.env.PORT)
15 | ```
16 |
17 | ## The request
18 |
19 | The request object contains all relavant data and methods to handle incoming requests and methods to respond as desired.
20 |
21 | ```javascript
22 | app.get(r => {
23 | r // is the request object
24 | })
25 | ```
26 |
27 | ### Incoming
28 |
29 | #### `.method`
30 | The HTTP verb of the request.
31 |
32 | #### `.url`
33 | Contains the actual url sent by the client
34 |
35 | #### `.pathname`
36 | Contains the relative url for the specific handler
37 |
38 | #### `.headers`
39 | An object containing headers. If multiple headers are found the value will be a comma separated list of values.
40 |
41 | #### `.query`
42 | A URLSearchParams object for the query string. This is a getter so the URLSearchParams object will be created lazily.
43 |
44 | #### `.params`
45 | An object of the matched routing params like.
46 | `/authors/:author/books/:book` = `{ author: 'Murray', book: 'Ethics of Liberty' }`
47 |
48 | #### `.body() -> Promise`
49 | A function which reads the incoming body and transforms it to an optional type `text` or `json`. If no type is specificed a `Buffer` will be returned.
50 |
51 | #### `r.cookie(name) -> Object`
52 | Returns an object representing the cookie
53 |
54 |
55 | ### Outgoing
56 |
57 | #### `.status(status)`
58 |
59 | #### `.header(name, value) | r.header({ name: value })`
60 |
61 | #### `.end(body)`
62 |
63 | #### `.tryEnd(body)`
64 |
65 | #### `.write()`
66 |
67 | #### `.cork()`
68 |
69 | #### `.offset()`
70 |
71 | #### `.status()`
72 |
73 | #### `.writable()`
74 |
75 | #### `.close()`
76 |
77 | #### `.cookie(name, { ... })`
78 |
79 | ### Middleware
80 |
81 | Implement middleware with `all`. Ey forwards the request to the next handler, unless a response has begun.
82 |
83 | ```javascript
84 | app.all((r, next) => {
85 | r.headers.authorization
86 | ? r.token = r.headers.authorization.split(' ')[1]
87 | : r.statusEnd(401) // request ends here
88 | })
89 |
90 | app.all(r => {
91 | r.token
92 | })
93 | ```
94 |
95 | ### Optimizations
96 |
97 | There are various ways to optimize your routes by being more specific in their definition.
98 |
99 | #### Pre-specified request headers
100 | You can specify which headers a route will use, up front, to prevent reading and loading unnecessary headers. Be aware that this is not possible for route handlers that come after other async handlers.
101 |
102 | ```javascript
103 | const app = ey({ headers: ['content-type'] })
104 |
105 | app.get('/login', { headers: ['authorization'] }, r => {
106 | r.headers['content-type'] // string
107 | r.headers['authorization'] // string
108 | r.headers['accept'] // undefined
109 | })
110 | ```
111 |
112 | ### Error handling
113 |
114 | Any middleware that accepts 2 arguments will be registrered as an error handler and receive the error as the first argument, and the request object as the second. This will allow you to log errors and reply accordingly. If no error handler makes a response the default error handler will reply with `500 Internal Server Error` and call `console.error`.
115 |
116 | ```javascript
117 | app.all((error, r) => {
118 | connected = true
119 | res.end(await ...)
120 | })
121 | ```
122 |
123 | ### Route matching
124 | Routes are matched according to the order in which they are defined. This is important to support middleware with less specific route matching.
125 |
126 | #### Exact match
127 | | hej | med | yo |
128 | | -- | -- | -- |
129 | | ```/user``` | will only match requests to the url `/user` | |
130 |
131 | #### Wildcard (not implemented yet)
132 | ```/user*``` All requests that begins with `/user`
133 | ```/user/*``` All requests that begins with `/user/`
134 | ```*/user``` All requests that ends with `/user`
135 |
136 |
137 | #### Parameters
138 | ##### ```/user```
139 | > All requests with one path segment, setting `r.params = { user }`
140 |
141 |
142 | ##### ```/:user/posts/:post```
143 | > All requests with 3 segments that has `posts` in the middle, setting `r.params = { user, post }`
144 |
145 |
146 | #### Regex
147 |
148 |
149 | ## Examples
150 |
151 | ### Get
152 |
153 |
154 | ### Get with Params
155 |
156 | ```javascript
157 | // GET /u25
158 | app.get('/:id', r =>
159 | r.params.id // The actual id value
160 | )
161 | ```
162 |
163 | ### Get query params
164 | ```javascript
165 | // GET /?sort=-price
166 | app.get(r => {
167 | req.query.sort // -price
168 | })
169 | ```
170 |
171 |
172 | ### Getting posted JSON
173 |
174 | ```javascript
175 | // POST /user { name: 'Murray' }
176 | app.post('/user', async r => {
177 | const body = await r.body('json')
178 | , user = await sql`insert into users ${ sql(body) }`
179 |
180 | r.json(user, 201) // r.json sets Content-Type: application/json header
181 | })
182 | ```
183 |
184 | ### Send file
185 | ```javascript
186 | app.get('/favicon.ico', r => r.file('/public/favicon.ico'))
187 | ```
188 |
189 | ### Serve files
190 | ```javascript
191 | app.all('/node_modules', r.files('/node_modules'))
192 | ```
193 |
194 |
195 | ### File upload
196 | ```javascript
197 | // POST /file data
198 | app.post('/file', async r => {
199 | await new Promise((resolve, reject) =>
200 | r.readable.pipe(fs.createWriteStream('...'))
201 | .on('finish', resolve)
202 | .on('error', reject)
203 | )
204 | r.end('Done')
205 | })
206 | ```
207 |
208 | ### Redirect
209 | ```javascript
210 | // GET /old-link
211 | app.get('/old-link', async r => {
212 | r.statusEnd(302, { location: '/new-link' })
213 | })
214 | ```
215 |
216 | ### Auth
217 |
218 | ```javascript
219 | app.all(r => {
220 | const session = await sql`select * from sessions where key = ${
221 | r.headers.authorization.split(' ')[0]
222 | }`
223 | })
224 | ```
225 |
226 |
227 | ## The request object
228 |
229 |
230 | ## Routing
231 |
232 | Ey simplifies the express routing model, by removing `request` and `next`.
233 |
234 |
235 |
236 | ```js
237 |
238 | ```
239 |
240 | ## WebSockets
241 |
242 |
--------------------------------------------------------------------------------
/Unlicense:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/bin/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /* eslint-disable no-console */
4 |
5 | import { Worker, isMainThread, threadId } from 'worker_threads'
6 | import os from 'os'
7 | import path from 'path'
8 | import ey from '../src/index.js'
9 |
10 | const argv = process.argv.slice(2)
11 | , cwd = process.cwd()
12 | , cpus = parseInt(argv.find((x, i, xs) => xs[i - 1] === '--threads') || os.cpus().length)
13 | , folder = argv.find(x => x[0] !== '-') || '.'
14 | , abs = folder[0] === '/' ? folder : path.join(cwd, folder)
15 | , port = process.env.PORT || (process.env.SSL_CERT ? 443 : 80)
16 | , supportsThreads = process.platform === 'linux'
17 |
18 | const options = {
19 | cert: process.env.SSL_CERT,
20 | key: process.env.SSL_KEY,
21 | cache: false || !!argv.find(x => x === '--cache')
22 | }
23 |
24 | argv.find(x => x === '--no-compress') && (options.compressions = [])
25 |
26 | if (supportsThreads && isMainThread) {
27 | for (let i = 0; i < cpus; i++)
28 | new Worker(new URL(import.meta.url), { argv }) // eslint-disable-line
29 | } else {
30 | const app = ey(options)
31 | app.get(app.files(abs, options))
32 | try {
33 | const x = await app.listen(port)
34 | if (isMainThread || threadId === cpus)
35 | console.log('Serving', abs === cwd ? './' : abs.replace(cwd + '/', ''), 'on', x.port, ...(supportsThreads ? ['with', threadId, 'workers'] : []))
36 | } catch (error) {
37 | console.log('Could not open port', port, '@', threadId)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ey",
3 | "version": "2.3.3",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "ey",
9 | "version": "2.3.3",
10 | "license": "Unlicense",
11 | "dependencies": {
12 | "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.42.0"
13 | },
14 | "bin": {
15 | "ey": "bin/index.js"
16 | },
17 | "engines": {
18 | "node": ">=16"
19 | }
20 | },
21 | "node_modules/uWebSockets.js": {
22 | "version": "20.42.0",
23 | "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#f40213ec0a97d0d8721d9d32d92d6eb6ddcd22e7"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ey",
3 | "version": "2.3.3",
4 | "description": "Ey is an ergonomic and fast server+router, built on uWebSockets",
5 | "main": "src/index.js",
6 | "type": "module",
7 | "author": "Rasmus Porsager ",
8 | "repository": "porsager/ey",
9 | "license": "Unlicense",
10 | "engines": {
11 | "node": ">=16"
12 | },
13 | "scripts": {
14 | "test": "eslint ."
15 | },
16 | "bin": {
17 | "ey": "./bin/index.js"
18 | },
19 | "files": [
20 | "/src",
21 | "/bin"
22 | ],
23 | "keywords": [
24 | "uws",
25 | "uWebSockets",
26 | "uWebSockets.js",
27 | "express",
28 | "router",
29 | "fast",
30 | "small",
31 | "simple",
32 | "zerodeps"
33 | ],
34 | "dependencies": {
35 | "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.42.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/files.js:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 |
3 | const rewrites = new Map()
4 | const trimSlash = x => x.charCodeAt(x.length - 1) === 47 ? x.slice(0, -1) : x
5 | const notFound = x => x.code === 'ENOENT' || x.code === 'EISDIR'
6 |
7 | export default function(Ey) {
8 | return function files(folder, options) {
9 | if (!options && typeof folder !== 'string') {
10 | options = folder || {}
11 | folder = ''
12 | }
13 |
14 | options = {
15 | rewrite: true,
16 | fallthrough: true,
17 | ...options
18 | }
19 |
20 | folder = path.isAbsolute(folder)
21 | ? folder
22 | : path.join(process.cwd(), folder)
23 |
24 | return Ey().get(cache, file, index)
25 |
26 | function cache(r) {
27 | const url = trimSlash(r.pathname)
28 |
29 | if (options.rewrite && r.headers.accept && r.headers.accept.indexOf('text/html') === 0 && rewrites.has(url))
30 | return r.url = rewrites.get(url)
31 | }
32 |
33 | async function file(r) {
34 | return r.file(resolve(r.url), options)
35 | }
36 |
37 | function index(r) {
38 | if (r.headers.accept && r.headers.accept.indexOf('text/html') === 0)
39 | return tryHtml(r)
40 | }
41 |
42 | async function tryHtml(r) {
43 | let url = resolve(path.join(r.url, 'index.html'))
44 | try {
45 | await r.file(url)
46 | rewrites.set(trimSlash(r.pathname), url)
47 | } catch (error) {
48 | if (!options.fallthrough || !notFound(error))
49 | throw error
50 |
51 | if (r.ended || !trimSlash(r.url))
52 | return
53 |
54 | try {
55 | await r.file(url = resolve(r.url + '.html'))
56 | rewrites.set(trimSlash(r.pathname), url)
57 | } catch (error) {
58 | if (!options.fallthrough || !notFound(error))
59 | throw error
60 | }
61 | }
62 | }
63 |
64 | function resolve(x) {
65 | x = path.join(folder, x)
66 | if (x.indexOf(folder) !== 0)
67 | throw new Error('Resolved path is outside root folder')
68 |
69 | return x
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { symbols as $, hasOwn, isPromise } from './shared.js'
2 | import fs from 'node:fs'
3 | import Request from './request.js'
4 | import files from './files.js'
5 | import mimes from './mimes.js'
6 | import { Websocket, Message } from './ws.js'
7 | import uWS from 'uWebSockets.js'
8 |
9 | export default function ey({
10 | methods = ['head', 'get', 'put', 'post', 'delete', 'patch', 'options', 'trace', 'all'],
11 | ...o
12 | } = {}) {
13 | let uws
14 | , handle
15 |
16 | const handlers = new Map()
17 | , connects = new Set()
18 | , wss = new Set()
19 | , asn = new Set()
20 | , msn = new Set()
21 | , rsn = new Set()
22 |
23 | hasOwn.call(o, 'compressions') || (o.compressions = o.cert ? ['br', 'gzip', 'deflate'] : ['gzip', 'deflate'])
24 | methods.forEach(register)
25 |
26 | router.route = route
27 | router.mimes = mimes
28 | router.files = files(ey)
29 | router.handlers = handlers
30 | router.ws = ws
31 | router.connect = (...xs) => connects.add(xs)
32 | router.listen = listen(o)
33 | router.publish = (...xs) => uws ? uws.publish(...xs) : false
34 | router.subscribers = (...xs) => uws ? uws.numSubscribers(...xs) : 0
35 | router.addServerName = (...xs) => (uws ? uws.addServerName(...xs) : asn.add(xs), router)
36 | router.missingServerName = (...xs) => (uws ? uws.missingServerName(...xs) : msn.add(xs), router)
37 | router.removeServerName = (...xs) => (uws ? uws.removeServerName(...xs) : rsn.add(xs), router)
38 | router.close = () => uws && uws.close()
39 |
40 | return router
41 |
42 | async function router(r) {
43 | if (r.handled)
44 | return
45 |
46 | const method = handlers.has(r.method)
47 | ? handlers.get(r.method)
48 | : handlers.get('all')
49 |
50 | for (const x of method) {
51 | if (r.handled)
52 | break
53 |
54 | if (hasOwn.call(r, $.error) !== x.error)
55 | continue
56 |
57 | const match = x.match(r)
58 | if (!match)
59 | continue
60 |
61 | try {
62 | let response = x.handler({ error: r[$.error], r, match })
63 | if (isPromise(response)) {
64 | r.onAborted()
65 | response = await response
66 | }
67 | if (response instanceof Response)
68 | return await r.end(response)
69 | else
70 | r.response = response
71 | } catch (error) {
72 | r[$.error] = error
73 | }
74 | }
75 |
76 | if (!handle) // Ensure we only use fallback responses in root listen router
77 | return
78 |
79 | if (r.handled)
80 | return
81 |
82 | hasOwn.call(r, $.error)
83 | ? r[$.error] instanceof URIError
84 | ? (r.statusEnd(400), console.error(400, r.url, '->', r[$.error])) // eslint-disable-line
85 | : (r.statusEnd(500), console.error(500, 'Uncaught route error', r[$.error])) // eslint-disable-line
86 | : r.response instanceof Response
87 | ? r.end(r.response.body, r.response.status || 200 + (r.response.statusText ? ' ' + r.response.statusText : ''), r.response.headers)
88 | : r.statusEnd(404)
89 | }
90 |
91 | function route(...xs) {
92 | const x = xs.pop()
93 | if (typeof x !== 'function')
94 | return ey(x)
95 |
96 | const app = ey()
97 | x(app)
98 | router.all(...xs, app)
99 | return router
100 | }
101 |
102 | function listen(defaultOptions) {
103 | return (port, address, options) => {
104 | return new Promise((resolve, reject) => {
105 | typeof address === 'object' && (options = address, address = null)
106 | const o = {
107 | ...defaultOptions,
108 | ...(options || {})
109 | }
110 |
111 | // Prettier errors than uws if missing
112 | o.cert && fs.accessSync(o.cert, fs.constants.R_OK) && fs.accessSync(o.key || 'private.key', fs.constants.R_OK)
113 |
114 | port = parseInt(port)
115 | uws = o.cert
116 | ? uWS.SSLApp({ cert_file_name: o.cert, key_file_name: o.key, ...o })
117 | : uWS.App(o)
118 | asn.forEach(xs => uws.addServerName(...xs))
119 | msn.forEach(xs => uws.missingServerName(...xs))
120 | rsn.forEach(xs => uws.removeServerName(...xs))
121 | connects.forEach((xs) => uws.connect(...xs))
122 | wss.forEach(([pattern, handlers]) =>
123 | uws.ws(
124 | pattern,
125 | {
126 | maxPayloadLength: 128 * 1024,
127 | ...handlers,
128 | ...(handlers.upgrade ? { upgrade: upgrader(o, pattern, handlers) } : {})
129 | }
130 | )
131 | )
132 | uws.any('/*', wrap)
133 |
134 | address
135 | ? uws.listen(address, port, callback)
136 | : uws.listen(port, o, callback)
137 |
138 | function callback(x) {
139 | if (!x)
140 | return reject(new Error('Could not listen on', port))
141 |
142 | handle = x
143 | resolve({ port: uWS.us_socket_local_port(handle), handle, unlisten })
144 | }
145 |
146 | function unlisten() {
147 | handle && uWS.us_listen_socket_close(handle)
148 | }
149 |
150 | function wrap(res, req) {
151 | router(new Request(res, req, o))
152 | }
153 | })
154 | }
155 | }
156 |
157 | function ws(pattern, handlers) {
158 | typeof pattern !== 'string' && (handlers = pattern, pattern = '/*')
159 | wss.add([
160 | pattern,
161 | {
162 | ...handlers,
163 | open: catcher('open', handlers, open),
164 | message: catcher('message', handlers, message),
165 | subscription: catcher('subscription', handlers),
166 | drain: catcher('drain', handlers),
167 | ping: catcher('ping', handlers),
168 | pong: catcher('pong', handlers),
169 | close: catcher('close', handlers, close)
170 | }
171 | ])
172 | }
173 |
174 | function close(fn, ws, code, data) {
175 | ws[$.ws].open = false
176 | fn(ws[$.ws], code, new Message(data, true))
177 | }
178 |
179 | function open(fn, ws) {
180 | fn(ws[$.ws] = new Websocket(ws))
181 | }
182 |
183 | function message(fn, ws, data, binary) {
184 | fn(ws[$.ws], new Message(data, binary))
185 | }
186 |
187 | function catcher(name, handlers, fn) {
188 | if (!(name in handlers) && !fn)
189 | return
190 |
191 | const method = handlers[name]
192 | return function(ws, ...xs) {
193 | try {
194 | fn
195 | ? fn(method, ws, ...xs)
196 | : method(ws[$.ws], ...xs)
197 | } catch (error) {
198 | name === 'close' || ws.end(1011, 'Internal Server Error')
199 | console.error(500, 'Uncaught ' + name + ' error', error) // eslint-disable-line
200 | }
201 | }
202 | }
203 |
204 | function register(name) {
205 | handlers.set(name, new Set())
206 | router[name] = function(match, options, ...fns) {
207 | if (typeof options === 'function') {
208 | fns.unshift(options)
209 | options = undefined
210 | }
211 |
212 | if (typeof match === 'function') {
213 | fns.unshift(match)
214 | match = true
215 | }
216 |
217 | if (typeof match === 'object' && match instanceof RegExp === false) {
218 | options = match
219 | match = true
220 | }
221 |
222 | options = {
223 | ...o,
224 | ...options,
225 | headers: o.headers
226 | ? o.headers.concat(options.headers || [])
227 | : options && options.headers
228 | }
229 |
230 | fns.forEach(fn => {
231 | if (typeof fn !== 'function')
232 | throw new Error(fn + ' is not a function')
233 |
234 | const isRouter = hasOwn.call(fn, 'handlers')
235 | const route = {
236 | options,
237 | handler: handler(fn),
238 | error: !isRouter && fn.length === 2,
239 | match: prepare(match, isRouter)
240 | }
241 |
242 | if (name === 'all') {
243 | for (const key of handlers.keys())
244 | handlers.get(key).add(route)
245 | } else {
246 | handlers.get(name).add(route)
247 | name === 'get' && handlers.get('head').add(route)
248 | }
249 | })
250 |
251 | return router
252 | }
253 | }
254 | }
255 |
256 | function handler(fn) {
257 | return hasOwn.call(fn, 'handlers')
258 | ? sub
259 | : direct
260 |
261 | function sub(x) {
262 | const url = x.r.url
263 | x.r.url = x.r.url.slice(x.match.length)
264 | const result = direct(x)
265 | result && typeof result.then === 'function'
266 | ? result.finally(() => x.r.url = url)
267 | : x.r.url = url
268 | return result
269 | }
270 |
271 | function direct({ error, r, match }) {
272 | return error
273 | ? fn(error, r)
274 | : fn(r)
275 | }
276 | }
277 |
278 | function prepare(match, sub) {
279 | const fn = typeof match === 'string'
280 | ? prepareString(match, sub)
281 | : match instanceof RegExp
282 | ? prepareRegex(match, sub)
283 | : Array.isArray(match)
284 | ? prepareArray(match, sub)
285 | : match === true && (() => true)
286 |
287 | if (!fn)
288 | throw new Error('Unknown match type')
289 |
290 | return fn
291 | }
292 |
293 | function prepareString(match, sub) {
294 | const named = match.match(/\/:([a-z][a-z0-9_]*)?/g)
295 | , wildcard = match.indexOf('*') !== -1
296 |
297 | if (!named && !wildcard) {
298 | return sub
299 | ? (r) => r.url.indexOf(match) === 0 && match
300 | : (r) => (r.url === match || r.url + '/' === match) && match
301 | }
302 |
303 | const names = named && named.map(n => n.slice(2))
304 | const regex = new RegExp(
305 | '^('
306 | + match.replace(/:.+?(\/|$)/g, '([^/]+?)$1').replace(/\*/, '.*?')
307 | + ')'
308 | + (sub ? '(/|$)' : '$')
309 | )
310 |
311 | return function(r) {
312 | const result = r.url.match(regex)
313 | result && names && names.forEach((n, i) => r.params[n] = decodeURIComponent(result[i + 2]))
314 | return result && result[1]
315 | }
316 | }
317 |
318 | function prepareRegex(match) {
319 | return function(r) {
320 | const result = r.url.match(match)
321 | result && result.forEach((m, i) => r.params[i] = m)
322 | return result && result[0]
323 | }
324 | }
325 |
326 | function prepareArray(match, sub) {
327 | match = match.map(m => prepare(m, sub))
328 | return function(r) {
329 | return match.some(fn => fn(r))
330 | }
331 | }
332 |
333 | function upgrader(options, pattern, handlers) {
334 | handlers.headers
335 | ? handlers.headers.push('sec-websocket-key', 'sec-websocket-protocol', 'sec-websocket-extensions')
336 | : handlers.headers === false && (handlers.headers = ['sec-websocket-key', 'sec-websocket-protocol', 'sec-websocket-extensions'])
337 |
338 | return async function(res, req, context) {
339 | res.options = options
340 | const r = new Request(res, req, { ...options, headers: handlers.headers })
341 | ;(pattern.match(/\/:([^/]+|$)/g) || []).map((x, i) => r.params[x.slice(2)] = res.getParameter(i))
342 | let error
343 | , data
344 | try {
345 | data = handlers.upgrade(r)
346 | data && typeof data.then === 'function' && (r.onAborted(), data = await data)
347 | } catch (err) {
348 | error = err
349 | console.error(500, 'Uncaught upgrade error', error) // eslint-disable-line
350 | }
351 |
352 | if (r.ended)
353 | return
354 |
355 | if (error)
356 | return r.statusEnd(500)
357 |
358 | r.status(101)
359 | r.cork(() =>
360 | res.upgrade(
361 | { data },
362 | r.headers['sec-websocket-key'],
363 | r.headers['sec-websocket-protocol'],
364 | r.headers['sec-websocket-extensions'],
365 | context
366 | )
367 | )
368 | }
369 | }
370 |
--------------------------------------------------------------------------------
/src/mimes.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | export const compressable = new Set([
4 | 'application/atom+xml',
5 | 'application/eot',
6 | 'application/font',
7 | 'application/font-sfnt',
8 | 'application/font-woff',
9 | 'application/geo+json',
10 | 'application/graphql+json',
11 | 'application/javascript',
12 | 'application/javascript-binast ',
13 | 'application/json',
14 | 'application/manifest+json',
15 | 'application/rss+xml',
16 | 'application/vnd.api+json',
17 | 'application/vnd.apple.mpegurl',
18 | 'application/ld+json',
19 | 'application/manifest+json ',
20 | 'application/opentype',
21 | 'application/otf',
22 | 'application/truetype',
23 | 'application/ttf',
24 | 'application/vnd.api+json ',
25 | 'application/vnd.apple.mpegurl',
26 | 'application/vnd.ms-fontobject',
27 | 'application/x-font-opentype',
28 | 'application/x-font-truetype',
29 | 'application/x-font-ttf',
30 | 'application/wasm',
31 | 'application/x-httpd-cgi',
32 | 'application/x-javascript',
33 | 'application/x-opentype',
34 | 'application/x-otf',
35 | 'application/x-perl',
36 | 'application/x-protobuf ',
37 | 'application/x-ttf',
38 | 'application/xhtml+xml',
39 | 'application/xml',
40 | 'application/xml+rss',
41 | 'font/eot',
42 | 'font/opentype',
43 | 'font/otf',
44 | 'font/ttf',
45 | 'font/x-woff',
46 | 'image/svg+xml',
47 | 'image/vnd.microsoft.icon',
48 | 'image/x-icon',
49 | 'multipart/bag',
50 | 'multipart/mixed',
51 | 'text/css',
52 | 'text/html',
53 | 'text/javascript',
54 | 'text/js',
55 | 'text/plain',
56 | 'text/richtext',
57 | 'text/x-component',
58 | 'text/x-java-source',
59 | 'text/x-markdown',
60 | 'text/x-script',
61 | 'text/xml'
62 | ])
63 |
64 | export default new Map([
65 | ['3g2', 'video/3gpp2'],
66 | ['3gp', 'video/3gpp'],
67 | ['3gpp', 'video/3gpp'],
68 | ['3mf', 'model/3mf'],
69 | ['ac', 'application/pkix-attr-cert'],
70 | ['adp', 'audio/adpcm'],
71 | ['ai', 'application/postscript'],
72 | ['amr', 'audio/amr'],
73 | ['apng', 'image/apng'],
74 | ['appcache', 'text/cache-manifest'],
75 | ['asc', 'application/pgp-signature'],
76 | ['atom', 'application/atom+xml'],
77 | ['atomcat', 'application/atomcat+xml'],
78 | ['atomdeleted', 'application/atomdeleted+xml'],
79 | ['atomsvc', 'application/atomsvc+xml'],
80 | ['au', 'audio/basic'],
81 | ['avif', 'image/avif'],
82 | ['aw', 'application/applixware'],
83 | ['bdoc', 'application/bdoc'],
84 | ['bin', 'application/octet-stream'],
85 | ['bmp', 'image/bmp'],
86 | ['bpk', 'application/octet-stream'],
87 | ['btif', 'image/prs.btif'],
88 | ['buffer', 'application/octet-stream'],
89 | ['ccxml', 'application/ccxml+xml'],
90 | ['cdfx', 'application/cdfx+xml'],
91 | ['cdmia', 'application/cdmi-capability'],
92 | ['cdmic', 'application/cdmi-container'],
93 | ['cdmid', 'application/cdmi-domain'],
94 | ['cdmio', 'application/cdmi-object'],
95 | ['cdmiq', 'application/cdmi-queue'],
96 | ['cer', 'application/pkix-cert'],
97 | ['cgm', 'image/cgm'],
98 | ['cjs', 'application/node'],
99 | ['class', 'application/java-vm'],
100 | ['coffee', 'text/coffeescript'],
101 | ['conf', 'text/plain'],
102 | ['cpt', 'application/mac-compactpro'],
103 | ['crl', 'application/pkix-crl'],
104 | ['css', 'text/css'],
105 | ['csv', 'text/csv'],
106 | ['cu', 'application/cu-seeme'],
107 | ['cww', 'application/prs.cww'],
108 | ['davmount', 'application/davmount+xml'],
109 | ['dbk', 'application/docbook+xml'],
110 | ['deb', 'application/octet-stream'],
111 | ['def', 'text/plain'],
112 | ['deploy', 'application/octet-stream'],
113 | ['disposition-notification', 'message/disposition-notification'],
114 | ['dist', 'application/octet-stream'],
115 | ['distz', 'application/octet-stream'],
116 | ['dll', 'application/octet-stream'],
117 | ['dmg', 'application/octet-stream'],
118 | ['dms', 'application/octet-stream'],
119 | ['doc', 'application/msword'],
120 | ['dot', 'application/msword'],
121 | ['drle', 'image/dicom-rle'],
122 | ['dsc', 'text/prs.lines.tag'],
123 | ['dssc', 'application/dssc+der'],
124 | ['dtd', 'application/xml-dtd'],
125 | ['dump', 'application/octet-stream'],
126 | ['dwd', 'application/atsc-dwd+xml'],
127 | ['ear', 'application/java-archive'],
128 | ['ecma', 'application/ecmascript'],
129 | ['elc', 'application/octet-stream'],
130 | ['emf', 'image/emf'],
131 | ['eml', 'message/rfc822'],
132 | ['emma', 'application/emma+xml'],
133 | ['emotionml', 'application/emotionml+xml'],
134 | ['eps', 'application/postscript'],
135 | ['epub', 'application/epub+zip'],
136 | ['es', 'application/ecmascript'],
137 | ['exe', 'application/octet-stream'],
138 | ['exi', 'application/exi'],
139 | ['exr', 'image/aces'],
140 | ['ez', 'application/andrew-inset'],
141 | ['fdt', 'application/fdt+xml'],
142 | ['fits', 'image/fits'],
143 | ['flac', 'audio/flac'],
144 | ['g3', 'image/g3fax'],
145 | ['gbr', 'application/rpki-ghostbusters'],
146 | ['geojson', 'application/geo+json'],
147 | ['gif', 'image/gif'],
148 | ['glb', 'model/gltf-binary'],
149 | ['gltf', 'model/gltf+json'],
150 | ['gml', 'application/gml+xml'],
151 | ['gpx', 'application/gpx+xml'],
152 | ['gram', 'application/srgs'],
153 | ['grxml', 'application/srgs+xml'],
154 | ['gxf', 'application/gxf'],
155 | ['gz', 'application/gzip'],
156 | ['h261', 'video/h261'],
157 | ['h263', 'video/h263'],
158 | ['h264', 'video/h264'],
159 | ['heic', 'image/heic'],
160 | ['heics', 'image/heic-sequence'],
161 | ['heif', 'image/heif'],
162 | ['heifs', 'image/heif-sequence'],
163 | ['hej2', 'image/hej2k'],
164 | ['held', 'application/atsc-held+xml'],
165 | ['hjson', 'application/hjson'],
166 | ['hlp', 'application/winhlp'],
167 | ['hqx', 'application/mac-binhex40'],
168 | ['hsj2', 'image/hsj2'],
169 | ['htm', 'text/html'],
170 | ['html', 'text/html'],
171 | ['ics', 'text/calendar'],
172 | ['ief', 'image/ief'],
173 | ['ifb', 'text/calendar'],
174 | ['iges', 'model/iges'],
175 | ['igs', 'model/iges'],
176 | ['img', 'application/octet-stream'],
177 | ['in', 'text/plain'],
178 | ['ini', 'text/plain'],
179 | ['ink', 'application/inkml+xml'],
180 | ['inkml', 'application/inkml+xml'],
181 | ['ipfix', 'application/ipfix'],
182 | ['iso', 'application/octet-stream'],
183 | ['its', 'application/its+xml'],
184 | ['jade', 'text/jade'],
185 | ['jar', 'application/java-archive'],
186 | ['jhc', 'image/jphc'],
187 | ['jls', 'image/jls'],
188 | ['jp2', 'image/jp2'],
189 | ['jpe', 'image/jpeg'],
190 | ['jpeg', 'image/jpeg'],
191 | ['jpf', 'image/jpx'],
192 | ['jpg', 'image/jpeg'],
193 | ['jpg2', 'image/jp2'],
194 | ['jpgm', 'image/jpm'],
195 | ['jpgv', 'video/jpeg'],
196 | ['jph', 'image/jph'],
197 | ['jpm', 'image/jpm'],
198 | ['jpx', 'image/jpx'],
199 | ['js', 'text/javascript'],
200 | ['json', 'application/json'],
201 | ['json5', 'application/json5'],
202 | ['jsonld', 'application/ld+json'],
203 | ['jsonml', 'application/jsonml+json'],
204 | ['jsx', 'text/jsx'],
205 | ['jxr', 'image/jxr'],
206 | ['jxra', 'image/jxra'],
207 | ['jxrs', 'image/jxrs'],
208 | ['jxs', 'image/jxs'],
209 | ['jxsc', 'image/jxsc'],
210 | ['jxsi', 'image/jxsi'],
211 | ['jxss', 'image/jxss'],
212 | ['kar', 'audio/midi'],
213 | ['ktx', 'image/ktx'],
214 | ['ktx2', 'image/ktx2'],
215 | ['less', 'text/less'],
216 | ['lgr', 'application/lgr+xml'],
217 | ['list', 'text/plain'],
218 | ['litcoffee', 'text/coffeescript'],
219 | ['log', 'text/plain'],
220 | ['lostxml', 'application/lost+xml'],
221 | ['lrf', 'application/octet-stream'],
222 | ['m1v', 'video/mpeg'],
223 | ['m21', 'application/mp21'],
224 | ['m2a', 'audio/mpeg'],
225 | ['m2v', 'video/mpeg'],
226 | ['m3a', 'audio/mpeg'],
227 | ['m3u8', 'application/vnd.apple.mpegurl'],
228 | ['m4a', 'audio/mp4'],
229 | ['m4p', 'application/mp4'],
230 | ['m4s', 'video/iso.segment'],
231 | ['ma', 'application/mathematica'],
232 | ['mads', 'application/mads+xml'],
233 | ['maei', 'application/mmt-aei+xml'],
234 | ['man', 'text/troff'],
235 | ['manifest', 'text/cache-manifest'],
236 | ['map', 'application/json'],
237 | ['mar', 'application/octet-stream'],
238 | ['markdown', 'text/markdown'],
239 | ['mathml', 'application/mathml+xml'],
240 | ['mb', 'application/mathematica'],
241 | ['mbox', 'application/mbox'],
242 | ['md', 'text/markdown'],
243 | ['mdx', 'text/mdx'],
244 | ['me', 'text/troff'],
245 | ['mesh', 'model/mesh'],
246 | ['meta4', 'application/metalink4+xml'],
247 | ['metalink', 'application/metalink+xml'],
248 | ['mets', 'application/mets+xml'],
249 | ['mft', 'application/rpki-manifest'],
250 | ['mid', 'audio/midi'],
251 | ['midi', 'audio/midi'],
252 | ['mime', 'message/rfc822'],
253 | ['mj2', 'video/mj2'],
254 | ['mjp2', 'video/mj2'],
255 | ['mjs', 'text/javascript'],
256 | ['mml', 'text/mathml'],
257 | ['mods', 'application/mods+xml'],
258 | ['mov', 'video/quicktime'],
259 | ['mp2', 'audio/mpeg'],
260 | ['mp21', 'application/mp21'],
261 | ['mp2a', 'audio/mpeg'],
262 | ['mp3', 'audio/mpeg'],
263 | ['mp4', 'video/mp4'],
264 | ['mp4a', 'audio/mp4'],
265 | ['mp4s', 'application/mp4'],
266 | ['mp4v', 'video/mp4'],
267 | ['mpd', 'application/dash+xml'],
268 | ['mpe', 'video/mpeg'],
269 | ['mpeg', 'video/mpeg'],
270 | ['mpg', 'video/mpeg'],
271 | ['mpg4', 'video/mp4'],
272 | ['mpga', 'audio/mpeg'],
273 | ['mrc', 'application/marc'],
274 | ['mrcx', 'application/marcxml+xml'],
275 | ['ms', 'text/troff'],
276 | ['mscml', 'application/mediaservercontrol+xml'],
277 | ['msh', 'model/mesh'],
278 | ['msi', 'application/octet-stream'],
279 | ['msm', 'application/octet-stream'],
280 | ['msp', 'application/octet-stream'],
281 | ['mtl', 'model/mtl'],
282 | ['musd', 'application/mmt-usd+xml'],
283 | ['mxf', 'application/mxf'],
284 | ['mxmf', 'audio/mobile-xmf'],
285 | ['mxml', 'application/xv+xml'],
286 | ['msg', 'application/vnd.ms-outlook'],
287 | ['n3', 'text/n3'],
288 | ['nb', 'application/mathematica'],
289 | ['nq', 'application/n-quads'],
290 | ['nt', 'application/n-triples'],
291 | ['obj', 'model/obj'],
292 | ['oda', 'application/oda'],
293 | ['oga', 'audio/ogg'],
294 | ['ogg', 'audio/ogg'],
295 | ['ogv', 'video/ogg'],
296 | ['ogx', 'application/ogg'],
297 | ['omdoc', 'application/omdoc+xml'],
298 | ['onepkg', 'application/onenote'],
299 | ['onetmp', 'application/onenote'],
300 | ['onetoc', 'application/onenote'],
301 | ['onetoc2', 'application/onenote'],
302 | ['opf', 'application/oebps-package+xml'],
303 | ['opus', 'audio/ogg'],
304 | ['otf', 'font/otf'],
305 | ['owl', 'application/rdf+xml'],
306 | ['oxps', 'application/oxps'],
307 | ['p10', 'application/pkcs10'],
308 | ['p7c', 'application/pkcs7-mime'],
309 | ['p7m', 'application/pkcs7-mime'],
310 | ['p7s', 'application/pkcs7-signature'],
311 | ['p8', 'application/pkcs8'],
312 | ['pdf', 'application/pdf'],
313 | ['pfr', 'application/font-tdpfr'],
314 | ['pgp', 'application/pgp-encrypted'],
315 | ['pkg', 'application/octet-stream'],
316 | ['pki', 'application/pkixcmp'],
317 | ['pkipath', 'application/pkix-pkipath'],
318 | ['pls', 'application/pls+xml'],
319 | ['png', 'image/png'],
320 | ['prf', 'application/pics-rules'],
321 | ['provx', 'application/provenance+xml'],
322 | ['ps', 'application/postscript'],
323 | ['pskcxml', 'application/pskc+xml'],
324 | ['pti', 'image/prs.pti'],
325 | ['qt', 'video/quicktime'],
326 | ['raml', 'application/raml+yaml'],
327 | ['rapd', 'application/route-apd+xml'],
328 | ['rdf', 'application/rdf+xml'],
329 | ['relo', 'application/p2p-overlay+xml'],
330 | ['rif', 'application/reginfo+xml'],
331 | ['rl', 'application/resource-lists+xml'],
332 | ['rld', 'application/resource-lists-diff+xml'],
333 | ['rmi', 'audio/midi'],
334 | ['rnc', 'application/relax-ng-compact-syntax'],
335 | ['rng', 'application/xml'],
336 | ['roa', 'application/rpki-roa'],
337 | ['roff', 'text/troff'],
338 | ['rq', 'application/sparql-query'],
339 | ['rs', 'application/rls-services+xml'],
340 | ['rsat', 'application/atsc-rsat+xml'],
341 | ['rsd', 'application/rsd+xml'],
342 | ['rsheet', 'application/urc-ressheet+xml'],
343 | ['rss', 'application/rss+xml'],
344 | ['rtf', 'application/rtf'],
345 | ['rtx', 'text/richtext'],
346 | ['rusd', 'application/route-usd+xml'],
347 | ['s3m', 'audio/s3m'],
348 | ['sbml', 'application/sbml+xml'],
349 | ['scq', 'application/scvp-cv-request'],
350 | ['scs', 'application/scvp-cv-response'],
351 | ['sdp', 'application/sdp'],
352 | ['senmlx', 'application/senml+xml'],
353 | ['sensmlx', 'application/sensml+xml'],
354 | ['ser', 'application/java-serialized-object'],
355 | ['setpay', 'application/set-payment-initiation'],
356 | ['setreg', 'application/set-registration-initiation'],
357 | ['sgi', 'image/sgi'],
358 | ['sgm', 'text/sgml'],
359 | ['sgml', 'text/sgml'],
360 | ['shex', 'text/shex'],
361 | ['shf', 'application/shf+xml'],
362 | ['shtml', 'text/html'],
363 | ['sieve', 'application/sieve'],
364 | ['sig', 'application/pgp-signature'],
365 | ['sil', 'audio/silk'],
366 | ['silo', 'model/mesh'],
367 | ['siv', 'application/sieve'],
368 | ['slim', 'text/slim'],
369 | ['slm', 'text/slim'],
370 | ['sls', 'application/route-s-tsid+xml'],
371 | ['smi', 'application/smil+xml'],
372 | ['smil', 'application/smil+xml'],
373 | ['snd', 'audio/basic'],
374 | ['so', 'application/octet-stream'],
375 | ['spdx', 'text/spdx'],
376 | ['spp', 'application/scvp-vp-response'],
377 | ['spq', 'application/scvp-vp-request'],
378 | ['spx', 'audio/ogg'],
379 | ['sru', 'application/sru+xml'],
380 | ['srx', 'application/sparql-results+xml'],
381 | ['ssdl', 'application/ssdl+xml'],
382 | ['ssml', 'application/ssml+xml'],
383 | ['stk', 'application/hyperstudio'],
384 | ['stl', 'model/stl'],
385 | ['stpxz', 'model/step-xml+zip'],
386 | ['stpz', 'model/step+zip'],
387 | ['styl', 'text/stylus'],
388 | ['stylus', 'text/stylus'],
389 | ['svg', 'image/svg+xml'],
390 | ['svgz', 'image/svg+xml'],
391 | ['swidtag', 'application/swid+xml'],
392 | ['t', 'text/troff'],
393 | ['t38', 'image/t38'],
394 | ['td', 'application/urc-targetdesc+xml'],
395 | ['tei', 'application/tei+xml'],
396 | ['teicorpus', 'application/tei+xml'],
397 | ['text', 'text/plain'],
398 | ['tfi', 'application/thraud+xml'],
399 | ['tfx', 'image/tiff-fx'],
400 | ['tif', 'image/tiff'],
401 | ['tiff', 'image/tiff'],
402 | ['toml', 'application/toml'],
403 | ['tr', 'text/troff'],
404 | ['trig', 'application/trig'],
405 | ['ts', 'video/mp2t'],
406 | ['tsd', 'application/timestamped-data'],
407 | ['tsv', 'text/tab-separated-values'],
408 | ['ttc', 'font/collection'],
409 | ['ttf', 'font/ttf'],
410 | ['ttl', 'text/turtle'],
411 | ['ttml', 'application/ttml+xml'],
412 | ['txt', 'text/plain'],
413 | ['u8dsn', 'message/global-delivery-status'],
414 | ['u8hdr', 'message/global-headers'],
415 | ['u8mdn', 'message/global-disposition-notification'],
416 | ['u8msg', 'message/global'],
417 | ['ubj', 'application/ubjson'],
418 | ['uri', 'text/uri-list'],
419 | ['uris', 'text/uri-list'],
420 | ['urls', 'text/uri-list'],
421 | ['vcard', 'text/vcard'],
422 | ['vrml', 'model/vrml'],
423 | ['vtt', 'text/vtt'],
424 | ['vxml', 'application/voicexml+xml'],
425 | ['war', 'application/java-archive'],
426 | ['wasm', 'application/wasm'],
427 | ['wav', 'audio/wav'],
428 | ['weba', 'audio/webm'],
429 | ['webm', 'video/webm'],
430 | ['webmanifest', 'application/manifest+json'],
431 | ['webp', 'image/webp'],
432 | ['wgt', 'application/widget'],
433 | ['wmf', 'image/wmf'],
434 | ['woff', 'font/woff'],
435 | ['woff2', 'font/woff2'],
436 | ['wrl', 'model/vrml'],
437 | ['wsdl', 'application/wsdl+xml'],
438 | ['wspolicy', 'application/wspolicy+xml'],
439 | ['x3d', 'model/x3d+xml'],
440 | ['x3db', 'model/x3d+fastinfoset'],
441 | ['x3dbz', 'model/x3d+binary'],
442 | ['x3dv', 'model/x3d-vrml'],
443 | ['x3dvz', 'model/x3d+vrml'],
444 | ['x3dz', 'model/x3d+xml'],
445 | ['xaml', 'application/xaml+xml'],
446 | ['xav', 'application/xcap-att+xml'],
447 | ['xca', 'application/xcap-caps+xml'],
448 | ['xcs', 'application/calendar+xml'],
449 | ['xdf', 'application/xcap-diff+xml'],
450 | ['xdssc', 'application/dssc+xml'],
451 | ['xel', 'application/xcap-el+xml'],
452 | ['xenc', 'application/xenc+xml'],
453 | ['xer', 'application/patch-ops-error+xml'],
454 | ['xht', 'application/xhtml+xml'],
455 | ['xhtml', 'application/xhtml+xml'],
456 | ['xhvml', 'application/xv+xml'],
457 | ['xlf', 'application/xliff+xml'],
458 | ['xm', 'audio/xm'],
459 | ['xml', 'application/xml'],
460 | ['xns', 'application/xcap-ns+xml'],
461 | ['xop', 'application/xop+xml'],
462 | ['xpl', 'application/xproc+xml'],
463 | ['xsd', 'application/xml'],
464 | ['xsl', 'application/xml'],
465 | ['xslt', 'application/xml'],
466 | ['xspf', 'application/xspf+xml'],
467 | ['xvm', 'application/xv+xml'],
468 | ['xvml', 'application/xv+xml'],
469 | ['yaml', 'text/yaml'],
470 | ['yang', 'application/yang'],
471 | ['yin', 'application/yin+xml'],
472 | ['yml', 'text/yaml'],
473 | ['zip', 'application/zip']
474 | ])
475 |
--------------------------------------------------------------------------------
/src/proxy.js:
--------------------------------------------------------------------------------
1 | import { symbols as $ } from './shared.js'
2 | import net from 'node:net'
3 | import tls from 'node:tls'
4 |
5 | const nets = new Map()
6 | const tlss = new Map()
7 | const keepAlive = parseInt(process.env.EY_PROXY_KEEP_ALIVE || (2 * 60 * 1000))
8 |
9 | export default function(r, url, options = {}) {
10 | url = new URL(url)
11 | url.secure = url.protocol === 'https:'
12 | const xs = url.secure ? tlss : nets
13 | const headers = options.headers ? { ...r.headers, ...options.headers } : r.headers
14 | const head = r.method.toUpperCase() + ' '
15 | + url.pathname + url.search + ' HTTP/1.1\r\n'
16 | + Object.entries(headers).map(([h, v]) => h + ': ' + v).join('\r\n')
17 | + '\r\n\r\n'
18 |
19 | return xs.has(url.host)
20 | ? reuse(r, url, xs, head)
21 | : open(r, url, url.secure ? tls : net, xs, head, options)
22 | }
23 |
24 | function reuse(r, url, xs, head) {
25 | const sockets = xs.get(url.host)
26 | const socket = sockets.pop()
27 | sockets.length || xs.delete(url.host)
28 | socket(r, url, head)
29 | }
30 |
31 | function remove(xs, host, x) {
32 | if (!xs.has(host))
33 | return
34 |
35 | const sockets = xs.get(host)
36 | const i = sockets.indexOf(x)
37 | i === -1 || sockets.splice(i, 1)
38 | sockets.length || xs.delete(host)
39 | }
40 |
41 | function open(r, url, x, xs, head, options) {
42 | let i = -1
43 | let header = -1
44 | let body = -1
45 | let colon = -1
46 | let char = -1
47 | let space = -1
48 | let name = ''
49 | let value = ''
50 | let offset = -1
51 | let aborted = null
52 | let timer = null
53 |
54 | const s = x.connect({
55 | host: url.hostname,
56 | port: url.port || (url.secure ? 443 : 80),
57 | ...options,
58 | servername: options.servername || options.headers?.host || (
59 | url.secure && !net.isIP(url.host) ? url.host : undefined
60 | ),
61 | onread: {
62 | buffer: Buffer.alloc(128 * 1024),
63 | callback: (length, buffer) => {
64 | if (body !== -1)
65 | return write(r, buffer.subarray(0, length))
66 |
67 | i = 0
68 | if (header === -1) {
69 | while (header === -1 && i++ < length) {
70 | if (buffer[i] === 10)
71 | header = i = i + 1
72 | else if (buffer[i] === 13)
73 | header = i = i + 2
74 | }
75 | }
76 | r.status(buffer.toString('utf8', 9, header).trim())
77 | if (body === -1) {
78 | while (body === -1 && i++ < length) {
79 | char = buffer[i]
80 | if (char === 10) {
81 | name = buffer.toString('utf8', header, colon)
82 | value = buffer.toString('utf8', colon > space ? colon : space, i - 1)
83 | name.toLowerCase() === 'host'
84 | ? r.set('Host', url.hostname)
85 | : r.set(name, value)
86 | header = i + 1
87 | buffer[i + 1] === 10
88 | ? body = i + 2
89 | : buffer[i + 2] === 10
90 | ? body = i + 3
91 | : null
92 | } else if (colon < header && char === 58) {
93 | colon = i
94 | } else if (space < header && char === 32) {
95 | space = i + 1
96 | }
97 | }
98 | }
99 | if (body !== -1)
100 | write(r, buffer.subarray(body, length))
101 | }
102 | }
103 | })
104 |
105 | r.onAborted(() => (aborted && aborted(), s.destroy()))
106 | s.setKeepAlive(true, keepAlive)
107 | s.once('connect', connect)
108 | s.once('error', error)
109 | s.once('close', close)
110 |
111 | function connect() {
112 | s.write(head)
113 | r.readable.pipe(s, { end: false })
114 | }
115 |
116 | function error(error) {
117 | r.end(error, 500)
118 | }
119 |
120 | function close() {
121 | clearTimeout(timer)
122 | remove(xs, url.host, start)
123 | r.end()
124 | }
125 |
126 | function finished() {
127 | timer = setTimeout(() => s.destroy(), keepAlive)
128 | xs.has(url.host)
129 | ? xs.get(url.host).push(start)
130 | : xs.set(url.host, [start])
131 | }
132 |
133 | function start(...xs) {
134 | [r, url, head] = xs
135 | clearTimeout(timer)
136 | i = header = body = colon = char = space = offset = -1
137 | name = value = ''
138 | aborted = null
139 | r.onAborted(() => (aborted && aborted(), s.destroy()))
140 | connect()
141 | }
142 |
143 | async function write(r, buffer) {
144 | if (r[$.length]) {
145 | offset = r.getWriteOffset()
146 | const [ok, done] = r.tryEnd(buffer, r[$.length])
147 | if (done)
148 | return finished()
149 |
150 | ok || await new Promise(resolve => {
151 | s.pause()
152 | aborted = resolve
153 | r.onWritable(i => {
154 | const [ok] = r.tryEnd(buffer.subarray(i - offset), r[$.length])
155 | ok && resolve()
156 | return ok
157 | })
158 | })
159 | s.resume()
160 | } else {
161 | r.write(buffer)
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/request.js:
--------------------------------------------------------------------------------
1 | import uWS from 'uWebSockets.js'
2 |
3 | import fsp from 'node:fs/promises'
4 | import zlib from 'node:zlib'
5 | import { promisify } from 'node:util'
6 | import path from 'node:path'
7 | import { STATUS_CODES } from 'node:http'
8 | import { Readable, Writable } from 'node:stream'
9 | import { pipeline } from 'node:stream/promises'
10 |
11 | import proxy from './proxy.js'
12 | import mimes, { compressable } from './mimes.js'
13 | import { symbols as $, isPromise } from './shared.js'
14 |
15 | const cwd = process.cwd()
16 | const ipv4 = Buffer.from('00000000000000000000ffff', 'hex')
17 |
18 | const compressors = {
19 | identity: null,
20 | gzip: promisify(zlib.gzip),
21 | deflate: promisify(zlib.deflate),
22 | br: promisify(zlib.brotliCompress)
23 | }
24 |
25 | const streamingCompressors = {
26 | identity: null,
27 | gzip: zlib.createGzip,
28 | deflate: zlib.createDeflate,
29 | br: zlib.createBrotliCompress
30 | }
31 |
32 | const caches = {
33 | deflate: new Map(),
34 | gzip: new Map(),
35 | br: new Map(),
36 | identity: new Map()
37 | }
38 |
39 | export default class Request {
40 | constructor(res, req, options) {
41 | this.options = options
42 | this.method = req.getMethod()
43 | try {
44 | this.url = decodeURIComponent(req.getUrl())
45 | } catch (error) {
46 | this.url = req.getUrl()
47 | this[$.error] = error
48 | }
49 | this.pathname = this.url
50 | this.last = null
51 | this.ended = false
52 | this.paused = false
53 | this.handled = false
54 | this.aborted = false
55 | this.sentStatus = false
56 | this.sentHeaders = false
57 | this.rawQuery = req.getQuery() || ''
58 | this.params = {}
59 | this[$.ip] = null
60 | this[$.res] = res
61 | this[$.req] = req
62 | this[$.body] = null
63 | this[$.data] = null
64 | this[$.head] = null
65 | this[$.ended] = null
66 | this[$.query] = null
67 | this[$.length] = null
68 | this[$.status] = null
69 | this[$.corked] = false
70 | this[$.onData] = null
71 | this[$.handled] = null
72 | this[$.aborted] = null
73 | this[$.headers] = null
74 | this[$.reading] = null
75 | this[$.readable] = null
76 | this[$.writable] = null
77 | }
78 |
79 | onData(fn) {
80 | this[$.onData] = fn
81 | if (this[$.data] !== null) {
82 | this[$.data].forEach(({ buffer, last }) => fn(buffer, last))
83 | this[$.data] = null
84 | }
85 | return read(this)
86 | }
87 |
88 | body(type) {
89 | if (this[$.body] !== null)
90 | return this[$.body]
91 |
92 | const length = parseInt(header(this, 'content-length'))
93 | , contentType = header(this, 'content-type')
94 | , known = Number.isNaN(length) === false
95 |
96 | let full = known
97 | ? Buffer.allocUnsafe(parseInt(length))
98 | : []
99 |
100 | let offset = 0
101 | return this[$.body] = this.onData(buffer => {
102 | known
103 | ? Buffer.from(buffer).copy(full, offset)
104 | : full.push(buffer)
105 | offset += buffer.byteLength
106 | }).then(() => {
107 | known || (full = Buffer.concat(full))
108 | if (known && offset !== full.byteLength)
109 | throw new Error('Expected data of length', full.byteLength, 'but only got', offset)
110 |
111 | return this[$.body] = type === 'json'
112 | ? JSON.parse(full)
113 | : type === 'text'
114 | ? full.toString()
115 | : type === 'multipart'
116 | ? uWS.getParts(full, contentType)
117 | : full
118 | })
119 | }
120 |
121 | onAborted(fn) {
122 | if (this[$.aborted])
123 | return fn && this[$.aborted].push(fn)
124 |
125 | this[$.aborted] = fn ? [fn] : []
126 | this.method.charCodeAt(0) === 112 && read(this) // (p) cache reading on post, put, patch
127 | this.ip // ensure IP is read on first tick
128 | this.headers // ensure headers are read on first tick
129 | this[$.req] = null
130 | return this[$.res].onAborted(() => aborted(this))
131 | }
132 |
133 | get headers() {
134 | if (this[$.head] !== null)
135 | return this[$.head]
136 |
137 | this.options.headers != null
138 | ? (this.options.headers && (this[$.head] = {}), this.options.headers.forEach(k => this[$.head][k] = this[$.req].getHeader(k)))
139 | : (this[$.head] = {}, this[$.req].forEach((k, v) => this[$.head][k] = v))
140 |
141 | return this[$.head]
142 | }
143 |
144 | get secure() {
145 | return this.protocol === 'https'
146 | }
147 |
148 | get protocol() {
149 | return this.options.cert
150 | ? 'https'
151 | : header(this, 'x-forwarded-proto')
152 | }
153 |
154 | get query() {
155 | return this[$.query]
156 | ? this[$.query]
157 | : this[$.query] = new URLSearchParams(this.rawQuery)
158 | }
159 |
160 | get ip() {
161 | if (this[$.ip] !== null)
162 | return this[$.ip]
163 |
164 | const proxyIP = header(this, 'x-forwarded-for')
165 | , remoteIP = Buffer.from(this[$.res].getRemoteAddress())
166 |
167 | return this[$.ip] = (proxyIP
168 | ? proxyIP.replace(/::ffff:/g, '').split(',')[0].trim()
169 | : Buffer.compare(ipv4, remoteIP.subarray(0, 12)) === 0
170 | ? [...remoteIP.subarray(12)].join('.')
171 | : Buffer.from(this[$.res].getRemoteAddressAsText()).toString()
172 | ).replace(/(^|:)0+/g, '$1').replace(/::+/g, '::')
173 | }
174 |
175 | get readable() {
176 | const r = this // eslint-disable-line
177 | if (r[$.readable] !== null)
178 | return r[$.readable]
179 |
180 | const stream = r[$.readable] = new Readable({
181 | read() {
182 | r.resume()
183 | }
184 | })
185 |
186 | start()
187 |
188 | return stream
189 |
190 | async function start() {
191 | try {
192 | await r.onData(x => stream.push(Buffer.from(Buffer.from(x))) || r.pause())
193 | r.resume()
194 | stream.push(null)
195 | } catch (error) {
196 | stream.destroy(error)
197 | }
198 | }
199 | }
200 |
201 | get writable() {
202 | const r = this // eslint-disable-line
203 | if (r[$.writable] !== null)
204 | return r[$.writable]
205 |
206 | const writable = r[$.writable] = new Writable({
207 | autoDestroy: true,
208 | write(chunk, encoding, callback) {
209 | r.write(chunk)
210 | ? callback()
211 | : r.onWritable(() => (callback(), true))
212 | },
213 | destroy(error, callback) {
214 | callback(error)
215 | r.end()
216 | },
217 | final(callback) {
218 | r.end()
219 | callback()
220 | }
221 | })
222 |
223 | r.onAborted(() => writable.destroy())
224 |
225 | return writable
226 | }
227 |
228 | resume() {
229 | if (!this.paused || this.ended)
230 | return
231 | this.paused = false
232 | this[$.res].resume()
233 | }
234 |
235 | pause() {
236 | if (this.paused || this.ended)
237 | return
238 | this.paused = true
239 | this[$.res].pause()
240 | }
241 |
242 | cookie(name, value, options = {}) {
243 | if (arguments.length === 1)
244 | return getCookie(name, this.headers.cookie)
245 |
246 | if (options.Expires && options.Expires instanceof Date)
247 | options.Expires = options.Expires.toUTCString()
248 |
249 | return this.header(
250 | 'Set-Cookie',
251 | encodeURIComponent(name) + '=' + encodeURIComponent(value) + '; '
252 | + Object.entries({
253 | HttpOnly: true,
254 | Path: '/',
255 | ...options
256 | }).map(([k, v]) => k + (v === true ? '' : '=' + v)).join('; ')
257 | )
258 | }
259 |
260 | onEnded(fn) {
261 | this[$.ended] === null
262 | ? this[$.ended] = [fn]
263 | : this[$.ended].push(fn)
264 | }
265 |
266 | onHandled(fn) {
267 | this[$.handled] === null
268 | ? this[$.handled] = [fn]
269 | : this[$.handled].push(fn)
270 | }
271 |
272 | close() {
273 | this[$.res].close()
274 | ended(this)
275 | return this
276 | }
277 |
278 | end(x, status, headers) {
279 | typeof status === 'object' && (headers = status, status = null),
280 | status && this.status(status),
281 | headers && this.header(headers)
282 |
283 | return this.cork(() => {
284 | handled(this)
285 | if (this.method === 'head') {
286 | if (x && this[$.length] === null)
287 | this[$.res].writeHeader('Content-Length', '' + Buffer.byteLength(x))
288 | else if (this[$.length] !== null)
289 | this[$.res].writeHeader('Content-Length', '' + this[$.length])
290 | this[$.res].endWithoutBody()
291 | } else {
292 | this[$.res].end(
293 | x == null
294 | ? ''
295 | : typeof x === 'string' || x instanceof ArrayBuffer || (x && x.buffer instanceof ArrayBuffer)
296 | ? x
297 | : '' + x
298 | )
299 | }
300 | ended(this)
301 | })
302 | }
303 |
304 | statusEnd(status, headers) {
305 | if (this.headers.accept === 'application/json' && !this.type)
306 | this.type = 'application/json'
307 |
308 | return this.end(
309 | this.type === 'application/json'
310 | ? JSON.stringify(STATUS_CODES[status])
311 | : STATUS_CODES[status],
312 | status,
313 | headers
314 | )
315 | }
316 |
317 | status(x) {
318 | this[$.status] = x
319 | return this
320 | }
321 |
322 | header(h, v, x) {
323 | if (typeof h === 'number') {
324 | this.status(h)
325 | h = v
326 | v = x
327 | }
328 |
329 | if (typeof h === 'object') {
330 | Object.entries(h).forEach(xs => this.header(...xs))
331 | } else if (v || v === 0 || v === '') {
332 | const lower = h.toLowerCase()
333 | lower === 'content-length'
334 | ? this[$.length] = parseInt(v)
335 | : lower === 'date' || lower === 'uwebsockets'
336 | ? null // ignore
337 | : lower === 'content-type'
338 | ? this.type = v
339 | : this[$.headers]
340 | ? this[$.headers].push(['' + h, '' + v])
341 | : this[$.headers] = [['' + h, '' + v]]
342 | }
343 |
344 | return this
345 | }
346 |
347 | set(...xs) {
348 | return this.header(...xs)
349 | }
350 |
351 | cork(fn) {
352 | if (this.ended)
353 | return
354 |
355 | if (this[$.corked])
356 | return fn()
357 |
358 | let result
359 | this[$.res].cork(() => {
360 | if (!this.sentHeaders) {
361 | if (!this.sentStatus) {
362 | this.sentStatus = true
363 | const status = this[$.status]
364 | status && this[$.res].writeStatus(typeof status === 'number'
365 | ? status + (status in STATUS_CODES ? ' ' + STATUS_CODES[status] : '')
366 | : status
367 | )
368 | }
369 | this.sentHeaders = true
370 | this.type && this[$.res].writeHeader('Content-Type', this.type)
371 | this[$.headers] && this[$.headers].forEach(([header, value]) => {
372 | value && this[$.res].writeHeader(
373 | header,
374 | value instanceof Date
375 | ? value.toUTCString()
376 | : value
377 | )
378 | })
379 | }
380 | result = fn()
381 | })
382 | return result
383 | }
384 |
385 | getWriteOffset() {
386 | // getWriteOffset has thrown aborted even without onAborted being called.
387 | // Might need try catch if reproducable
388 | return this.ended
389 | ? -1
390 | : this[$.res].getWriteOffset()
391 | }
392 |
393 | onWritable(fn) {
394 | return this[$.res].onWritable(x => {
395 | this[$.corked] = true
396 | const result = fn(x)
397 | this[$.corked] = false
398 | return result
399 | })
400 | }
401 |
402 | proxy(url, options) {
403 | proxy(this, url, options)
404 | handled(this)
405 | }
406 |
407 | tryEnd(x, total) {
408 | if (this.ended)
409 | return [true, true]
410 |
411 | try {
412 | return this.cork(() => {
413 | if (this.method === 'head') {
414 | ended(this)
415 | this[$.res].endWithoutBody(total)
416 | return [true, true]
417 | }
418 |
419 | const xs = this[$.res].tryEnd(x, total)
420 | xs[1] && ended(this)
421 | return xs
422 | })
423 | } catch (err) {
424 | ended(this)
425 | return [true, true]
426 | }
427 | }
428 |
429 | write(x) {
430 | if (this.ended)
431 | return true
432 |
433 | handled(this)
434 | return this.cork(() =>
435 | this.method === 'head'
436 | ? this.end()
437 | : this[$.res].write(x)
438 | )
439 | }
440 |
441 | json(body, ...xs) {
442 | this.type = 'application/json'
443 | return this.end(JSON.stringify(body), ...xs)
444 | }
445 |
446 | html(body) {
447 | this.type = 'text/html'
448 | return this.end(body)
449 | }
450 |
451 | file(file, options) {
452 | options = Object.assign({
453 | lastModified: true,
454 | etag: true,
455 | minStreamSize: process.env.EY_MIN_STREAM_SIZE || (512 * 1024),
456 | maxCacheSize: process.env.EY_MIN_CACHE_SIZE || (128 * 1024),
457 | minCompressSize: process.env.EY_MIN_COMPRESS_SIZE || 1280,
458 | cache: true
459 | }, options)
460 |
461 | file = path.isAbsolute(file) ? file : path.join(cwd, file)
462 | const compressions = options.compressions || this.options.compressions
463 | , cache = options.cache || this.options.cache
464 | , ext = path.extname(file).slice(1)
465 | , type = mimes.get(ext)
466 |
467 | const compressor = compressions && compressions.length
468 | ? getEncoding(this.headers['accept-encoding'], compressions, type)
469 | : null
470 |
471 | return cache && caches[compressor || 'identity'].has(file)
472 | ? this.end(...caches[compressor || 'identity'].get(file))
473 | : readFile(this, file, type, compressor, options)
474 | }
475 | }
476 |
477 | async function readFile(r, file, type, compressor, o) {
478 | r.onAborted()
479 | let handle
480 | , stat
481 |
482 | try {
483 | handle = await fsp.open(file)
484 | stat = await handle.stat()
485 | if (!stat.isFile() && o.fallthrough)
486 | return handle.close()
487 | } catch (error) {
488 | handle && handle.close()
489 | if (o.fallthrough && error.code === 'ENOENT')
490 | return
491 | throw error
492 | }
493 |
494 | if (stat.size < o.minCompressSize)
495 | compressor = null
496 |
497 | if (r.headers.range || (stat.size >= o.minStreamSize && stat.size > o.maxCacheSize))
498 | return stream(r, type, { handle, stat, compressor }, o).finally(() => handle.close())
499 |
500 | let bytes = await handle.readFile()
501 |
502 | handle.close()
503 | handle = null
504 |
505 | if (o.transform) {
506 | bytes = o.transform(bytes, file, type, r)
507 | if (isPromise(bytes))
508 | bytes = await bytes
509 | }
510 |
511 | if (compressor)
512 | bytes = await compressors[compressor](bytes)
513 |
514 | const headers = {
515 | ETag: createEtag(stat.mtime, stat.size, compressor),
516 | 'Last-Modified': stat.mtime.toUTCString(),
517 | 'Content-Encoding': compressor,
518 | 'Content-Type': r.type || type
519 | }
520 |
521 | const response = [bytes, 200, headers]
522 | o.cache && stat.size < o.maxCacheSize && caches[compressor || 'identity'].set(file, response)
523 | r.end(...response)
524 | }
525 |
526 | function stream(r, type, { handle, stat, compressor }, options) {
527 | const { size, mtime } = stat
528 | , range = r.headers.range || ''
529 | , highWaterMark = options.highWaterMark || options.minStreamSize
530 | , end = parseInt(range.slice(range.indexOf('-') + 1)) || size - 1
531 | , start = parseInt(range.slice(6, range.indexOf('-')) || size - end - 1)
532 | , total = end - start + 1
533 |
534 | if (end >= size)
535 | return r.header(416, { 'Content-Range': 'bytes */' + (size - 1) }).end('Range Not Satisfiable')
536 |
537 | r.header(range ? 206 : 200, {
538 | 'Accept-Ranges': range ? 'bytes' : null,
539 | 'Last-Modified': mtime.toUTCString(),
540 | 'Content-Encoding': compressor,
541 | 'Content-Range': range ? 'bytes ' + start + '-' + end + '/' + size : null,
542 | 'Content-Type': r.type || type,
543 | ETag: createEtag(mtime, size, compressor)
544 | })
545 |
546 | if (r.method === 'head') {
547 | compressor
548 | ? r.header('Transfer-Encoding', 'chunked')
549 | : r.header('Content-Length', size)
550 | return Promise.resolve(r.end())
551 | }
552 |
553 | return compressor
554 | ? streamCompressed(r, handle, compressor, highWaterMark, total, start)
555 | : streamRaw(r, handle, highWaterMark, total, start)
556 | }
557 |
558 | async function streamRaw(r, handle, highWaterMark, total, start) {
559 | let lastOffset = 0
560 | , read = 0
561 | , buffer = Buffer.allocUnsafe(highWaterMark)
562 | , aborted
563 |
564 | r.onAborted(() => aborted && aborted())
565 |
566 | while (read < total) {
567 | const { bytesRead } = await handle.read(buffer, 0, Math.min(highWaterMark, total - read), start + read)
568 | read += bytesRead
569 | lastOffset = r.getWriteOffset()
570 | const [ok] = r.tryEnd(buffer.subarray(0, bytesRead), total)
571 | ok || await new Promise(resolve => {
572 | aborted = resolve
573 | r.onWritable(offset => {
574 | const [ok] = r.tryEnd(buffer.subarray(offset - lastOffset, bytesRead), total)
575 | ok && resolve()
576 | return ok
577 | })
578 | })
579 | }
580 | }
581 |
582 | async function streamCompressed(r, handle, compressor, highWaterMark, total, start) {
583 | const compressStream = streamingCompressors[compressor]({ chunkSize: highWaterMark })
584 | await pipeline(handle.createReadStream({ highWaterMark }), compressStream, r.writable)
585 | }
586 |
587 | function getEncoding(x, supported, type) {
588 | if (!x)
589 | return
590 |
591 | const accepted = parseAcceptEncoding(x, supported)
592 | let compressor
593 | for (const x of accepted) {
594 | if (x.type in compressors) {
595 | compressor = x.type === 'identity' ? null : x.type
596 | break
597 | }
598 | }
599 | return compressable.has(type) && compressor
600 | }
601 |
602 | function parseAcceptEncoding(x, compressions = []) {
603 | return (x || '').split(',')
604 | .map(x => (x = x.split(';q='), { type: x[0].trim(), q: parseFloat(x[1] || 1) }))
605 | .filter(x => x.q !== 0 && compressions.indexOf(x.type) !== -1)
606 | .sort((a, b) => a.q === b.q
607 | ? compressions.indexOf(a.type) - compressions.indexOf(b.type)
608 | : b.q - a.q)
609 | }
610 |
611 | function createEtag(mtime, size, weak) {
612 | return (weak ? 'W/' : '') + '"' + Math.floor(mtime.getTime() / 1000).toString(16) + '-' + size.toString(16) + '"'
613 | }
614 |
615 | function getCookie(name, x) {
616 | if (!x)
617 | return null
618 |
619 | const xs = x.match('(?:^|; )' + name + '=([^;]+)(;|$)')
620 | return xs ? decodeURIComponent(xs[1]) : null
621 | }
622 |
623 | function aborted(r) {
624 | r.aborted = true
625 | r[$.aborted] === null || r[$.aborted].forEach(x => x())
626 | ended(r)
627 | }
628 |
629 | function handled(r) {
630 | if (r.handled)
631 | return
632 |
633 | r.handled = true
634 | r[$.handled] === null || r[$.handled].forEach(x => x())
635 | }
636 |
637 | function ended(r) {
638 | r.ended = r.handled = true
639 | r[$.ended] === null || r[$.ended].forEach(x => x())
640 | }
641 |
642 | function header(r, header) {
643 | return r[$.req] && r[$.req].getHeader(header) || r.headers[header] || ''
644 | }
645 |
646 | function read(r) {
647 | if (r[$.reading] !== null)
648 | return r[$.reading]
649 |
650 | return r.handled || (r[$.reading] = new Promise((resolve, reject) =>
651 | r[$.res].onData((x, last) => {
652 | try {
653 | r[$.onData]
654 | ? r[$.onData](x, last)
655 | : (r[$.data] === null && (r[$.data] = []), r[$.data].push({ buffer: Buffer.from(Buffer.from(x)), last })) // must copy - uws clears memory in next tick
656 | last && resolve()
657 | } catch (error) {
658 | reject(error)
659 | }
660 | })
661 | ))
662 | }
663 |
--------------------------------------------------------------------------------
/src/shared.js:
--------------------------------------------------------------------------------
1 | export const hasOwn = {}.hasOwnProperty
2 |
3 | export function isPromise(x) {
4 | return x && typeof x.then === 'function'
5 | }
6 |
7 | export const symbols = {
8 | ip: Symbol('ip'),
9 | ws : Symbol('ws'),
10 | req: Symbol('req'),
11 | res: Symbol('res'),
12 | body: Symbol('body'),
13 | data: Symbol('data'),
14 | head: Symbol('head'),
15 | ended: Symbol('ended'),
16 | error: Symbol('error'),
17 | query: Symbol('query'),
18 | corked: Symbol('corked'),
19 | length: Symbol('length'),
20 | onData: Symbol('onData'),
21 | status: Symbol('status'),
22 | aborted: Symbol('aborted'),
23 | headers: Symbol('headers'),
24 | reading: Symbol('reading'),
25 | readable: Symbol('readable'),
26 | writable: Symbol('writable'),
27 | handling: Symbol('handling')
28 | }
29 |
--------------------------------------------------------------------------------
/src/ws.js:
--------------------------------------------------------------------------------
1 | import { symbols as $ } from './shared.js'
2 |
3 | export class Message {
4 | constructor(data, binary) {
5 | this.data = data
6 | this.binary = binary
7 | }
8 | get buffer() { return Buffer.from(this.data) }
9 | get json() { return tryJSON(this.data) }
10 | get text() { return Buffer.from(this.data).toString() }
11 | }
12 |
13 | export class Websocket {
14 | constructor(ws) {
15 | this[$.ws] = ws
16 | this.open = true
17 | ws.data && Object.assign(this, ws.data)
18 | }
19 | close() { return this.open && this[$.ws].close.apply(this[$.ws], arguments) }
20 | cork() { return this.open && this[$.ws].cork.apply(this[$.ws], arguments) }
21 | end() { return this.open && this[$.ws].end.apply(this[$.ws], arguments) }
22 | getBufferedAmount() { return this.open && this[$.ws].getBufferedAmount.apply(this[$.ws], arguments) }
23 | getRemoteAddress() { return this.open && this[$.ws].getRemoteAddress.apply(this[$.ws], arguments) }
24 | getRemoteAddressAsText() { return this.open && this[$.ws].getRemoteAddressAsText.apply(this[$.ws], arguments) }
25 | getTopics() { return this.open && this[$.ws].getTopics.apply(this[$.ws], arguments) }
26 | getUserData() { return this.open && this[$.ws].getUserData.apply(this[$.ws], arguments) }
27 | isSubscribed() { return this.open && this[$.ws].isSubscribed.apply(this[$.ws], arguments) }
28 | ping() { return this.open && this[$.ws].ping.apply(this[$.ws], arguments) }
29 | publish() { return this.open && this[$.ws].publish.apply(this[$.ws], arguments) }
30 | send() { return this.open && this[$.ws].send.apply(this[$.ws], arguments) }
31 | subscribe() { return this.open && this[$.ws].subscribe.apply(this[$.ws], arguments) }
32 | unsubscribe() { return this.open && this[$.ws].unsubscribe.apply(this[$.ws], arguments) }
33 | }
34 |
35 | function tryJSON(data) {
36 | try {
37 | return JSON.parse(Buffer.from(data))
38 | } catch (x) {
39 | return undefined
40 | }
41 | }
42 |
--------------------------------------------------------------------------------