├── .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 | --------------------------------------------------------------------------------