userver.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.uServer = void 0;
  4. const debug_1 = require("debug");
  5. const server_1 = require("./server");
  6. const transports_uws_1 = require("./transports-uws");
  7. const debug = (0, debug_1.default)("engine:uws");
  8. /**
  9. * An Engine.IO server based on the `uWebSockets.js` package.
  10. */
  11. // TODO export it into its own package
  12. class uServer extends server_1.BaseServer {
  13. init() { }
  14. cleanup() { }
  15. /**
  16. * Prepares a request by processing the query string.
  17. *
  18. * @private
  19. */
  20. prepare(req, res) {
  21. req.method = req.getMethod().toUpperCase();
  22. req.url = req.getUrl();
  23. const params = new URLSearchParams(req.getQuery());
  24. req._query = Object.fromEntries(params.entries());
  25. req.headers = {};
  26. req.forEach((key, value) => {
  27. req.headers[key] = value;
  28. });
  29. req.connection = {
  30. remoteAddress: Buffer.from(res.getRemoteAddressAsText()).toString(),
  31. };
  32. res.onAborted(() => {
  33. debug("response has been aborted");
  34. });
  35. }
  36. createTransport(transportName, req) {
  37. return new transports_uws_1.default[transportName](req);
  38. }
  39. /**
  40. * Attach the engine to a µWebSockets.js server
  41. * @param app
  42. * @param options
  43. */
  44. attach(app /* : TemplatedApp */, options = {}) {
  45. const path = this._computePath(options);
  46. app
  47. .any(path, this.handleRequest.bind(this))
  48. //
  49. .ws(path, {
  50. compression: options.compression,
  51. idleTimeout: options.idleTimeout,
  52. maxBackpressure: options.maxBackpressure,
  53. maxPayloadLength: this.opts.maxHttpBufferSize,
  54. upgrade: this.handleUpgrade.bind(this),
  55. open: (ws) => {
  56. const transport = ws.getUserData().transport;
  57. transport.socket = ws;
  58. transport.writable = true;
  59. transport.emit("ready");
  60. },
  61. message: (ws, message, isBinary) => {
  62. ws.getUserData().transport.onData(isBinary ? message : Buffer.from(message).toString());
  63. },
  64. close: (ws, code, message) => {
  65. ws.getUserData().transport.onClose(code, message);
  66. },
  67. });
  68. }
  69. _applyMiddlewares(req, res, callback) {
  70. if (this.middlewares.length === 0) {
  71. return callback();
  72. }
  73. // needed to buffer headers until the status is computed
  74. req.res = new ResponseWrapper(res);
  75. super._applyMiddlewares(req, req.res, (err) => {
  76. // some middlewares (like express-session) wait for the writeHead() call to flush their headers
  77. // see https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L220-L244
  78. req.res.writeHead();
  79. callback(err);
  80. });
  81. }
  82. handleRequest(res, req) {
  83. debug('handling "%s" http request "%s"', req.getMethod(), req.getUrl());
  84. this.prepare(req, res);
  85. req.res = res;
  86. const callback = (errorCode, errorContext) => {
  87. if (errorCode !== undefined) {
  88. this.emit("connection_error", {
  89. req,
  90. code: errorCode,
  91. message: server_1.Server.errorMessages[errorCode],
  92. context: errorContext,
  93. });
  94. this.abortRequest(req.res, errorCode, errorContext);
  95. return;
  96. }
  97. if (req._query.sid) {
  98. debug("setting new request for existing client");
  99. // @ts-ignore
  100. this.clients[req._query.sid].transport.onRequest(req);
  101. }
  102. else {
  103. const closeConnection = (errorCode, errorContext) => this.abortRequest(res, errorCode, errorContext);
  104. this.handshake(req._query.transport, req, closeConnection);
  105. }
  106. };
  107. this._applyMiddlewares(req, res, (err) => {
  108. if (err) {
  109. callback(server_1.Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" });
  110. }
  111. else {
  112. this.verify(req, false, callback);
  113. }
  114. });
  115. }
  116. handleUpgrade(res, req, context) {
  117. debug("on upgrade");
  118. this.prepare(req, res);
  119. req.res = res;
  120. const callback = async (errorCode, errorContext) => {
  121. if (errorCode !== undefined) {
  122. this.emit("connection_error", {
  123. req,
  124. code: errorCode,
  125. message: server_1.Server.errorMessages[errorCode],
  126. context: errorContext,
  127. });
  128. this.abortRequest(res, errorCode, errorContext);
  129. return;
  130. }
  131. const id = req._query.sid;
  132. let transport;
  133. if (id) {
  134. const client = this.clients[id];
  135. if (!client) {
  136. debug("upgrade attempt for closed client");
  137. return res.close();
  138. }
  139. else if (client.upgrading) {
  140. debug("transport has already been trying to upgrade");
  141. return res.close();
  142. }
  143. else if (client.upgraded) {
  144. debug("transport had already been upgraded");
  145. return res.close();
  146. }
  147. else {
  148. debug("upgrading existing transport");
  149. transport = this.createTransport(req._query.transport, req);
  150. client._maybeUpgrade(transport);
  151. }
  152. }
  153. else {
  154. transport = await this.handshake(req._query.transport, req, (errorCode, errorContext) => this.abortRequest(res, errorCode, errorContext));
  155. if (!transport) {
  156. return;
  157. }
  158. }
  159. // calling writeStatus() triggers the flushing of any header added in a middleware
  160. req.res.writeStatus("101 Switching Protocols");
  161. res.upgrade({
  162. transport,
  163. }, req.getHeader("sec-websocket-key"), req.getHeader("sec-websocket-protocol"), req.getHeader("sec-websocket-extensions"), context);
  164. };
  165. this._applyMiddlewares(req, res, (err) => {
  166. if (err) {
  167. callback(server_1.Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" });
  168. }
  169. else {
  170. this.verify(req, true, callback);
  171. }
  172. });
  173. }
  174. abortRequest(res, errorCode, errorContext) {
  175. const statusCode = errorCode === server_1.Server.errors.FORBIDDEN
  176. ? "403 Forbidden"
  177. : "400 Bad Request";
  178. const message = errorContext && errorContext.message
  179. ? errorContext.message
  180. : server_1.Server.errorMessages[errorCode];
  181. res.writeStatus(statusCode);
  182. res.writeHeader("Content-Type", "application/json");
  183. res.end(JSON.stringify({
  184. code: errorCode,
  185. message,
  186. }));
  187. }
  188. }
  189. exports.uServer = uServer;
  190. class ResponseWrapper {
  191. constructor(res) {
  192. this.res = res;
  193. this.statusWritten = false;
  194. this.headers = [];
  195. this.isAborted = false;
  196. }
  197. set statusCode(status) {
  198. if (!status) {
  199. return;
  200. }
  201. // FIXME: handle all status codes?
  202. this.writeStatus(status === 200 ? "200 OK" : "204 No Content");
  203. }
  204. writeHead(status) {
  205. this.statusCode = status;
  206. }
  207. setHeader(key, value) {
  208. if (Array.isArray(value)) {
  209. value.forEach((val) => {
  210. this.writeHeader(key, val);
  211. });
  212. }
  213. else {
  214. this.writeHeader(key, value);
  215. }
  216. }
  217. removeHeader() {
  218. // FIXME: not implemented
  219. }
  220. // needed by vary: https://github.com/jshttp/vary/blob/5d725d059b3871025cf753e9dfa08924d0bcfa8f/index.js#L134
  221. getHeader() { }
  222. writeStatus(status) {
  223. if (this.isAborted)
  224. return;
  225. this.res.writeStatus(status);
  226. this.statusWritten = true;
  227. this.writeBufferedHeaders();
  228. return this;
  229. }
  230. writeHeader(key, value) {
  231. if (this.isAborted)
  232. return;
  233. if (key === "Content-Length") {
  234. // the content length is automatically added by uWebSockets.js
  235. return;
  236. }
  237. if (this.statusWritten) {
  238. this.res.writeHeader(key, value);
  239. }
  240. else {
  241. this.headers.push([key, value]);
  242. }
  243. }
  244. writeBufferedHeaders() {
  245. this.headers.forEach(([key, value]) => {
  246. this.res.writeHeader(key, value);
  247. });
  248. }
  249. end(data) {
  250. if (this.isAborted)
  251. return;
  252. this.res.cork(() => {
  253. if (!this.statusWritten) {
  254. // status will be inferred as "200 OK"
  255. this.writeBufferedHeaders();
  256. }
  257. this.res.end(data);
  258. });
  259. }
  260. onData(fn) {
  261. if (this.isAborted)
  262. return;
  263. this.res.onData(fn);
  264. }
  265. onAborted(fn) {
  266. if (this.isAborted)
  267. return;
  268. this.res.onAborted(() => {
  269. // Any attempt to use the UWS response object after abort will throw!
  270. this.isAborted = true;
  271. fn();
  272. });
  273. }
  274. cork(fn) {
  275. if (this.isAborted)
  276. return;
  277. this.res.cork(fn);
  278. }
  279. }