sse.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.SSEServerTransport = void 0;
  7. const node_crypto_1 = require("node:crypto");
  8. const node_tls_1 = require("node:tls");
  9. const types_js_1 = require("../types.js");
  10. const raw_body_1 = __importDefault(require("raw-body"));
  11. const content_type_1 = __importDefault(require("content-type"));
  12. const node_url_1 = require("node:url");
  13. const MAXIMUM_MESSAGE_SIZE = '4mb';
  14. /**
  15. * Server transport for SSE: this will send messages over an SSE connection and receive messages from HTTP POST requests.
  16. *
  17. * This transport is only available in Node.js environments.
  18. * @deprecated SSEServerTransport is deprecated. Use StreamableHTTPServerTransport instead.
  19. */
  20. class SSEServerTransport {
  21. /**
  22. * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`.
  23. */
  24. constructor(_endpoint, res, options) {
  25. this._endpoint = _endpoint;
  26. this.res = res;
  27. this._sessionId = (0, node_crypto_1.randomUUID)();
  28. this._options = options || { enableDnsRebindingProtection: false };
  29. }
  30. /**
  31. * Validates request headers for DNS rebinding protection.
  32. * @returns Error message if validation fails, undefined if validation passes.
  33. */
  34. validateRequestHeaders(req) {
  35. // Skip validation if protection is not enabled
  36. if (!this._options.enableDnsRebindingProtection) {
  37. return undefined;
  38. }
  39. // Validate Host header if allowedHosts is configured
  40. if (this._options.allowedHosts && this._options.allowedHosts.length > 0) {
  41. const hostHeader = req.headers.host;
  42. if (!hostHeader || !this._options.allowedHosts.includes(hostHeader)) {
  43. return `Invalid Host header: ${hostHeader}`;
  44. }
  45. }
  46. // Validate Origin header if allowedOrigins is configured
  47. if (this._options.allowedOrigins && this._options.allowedOrigins.length > 0) {
  48. const originHeader = req.headers.origin;
  49. if (originHeader && !this._options.allowedOrigins.includes(originHeader)) {
  50. return `Invalid Origin header: ${originHeader}`;
  51. }
  52. }
  53. return undefined;
  54. }
  55. /**
  56. * Handles the initial SSE connection request.
  57. *
  58. * This should be called when a GET request is made to establish the SSE stream.
  59. */
  60. async start() {
  61. if (this._sseResponse) {
  62. throw new Error('SSEServerTransport already started! If using Server class, note that connect() calls start() automatically.');
  63. }
  64. this.res.writeHead(200, {
  65. 'Content-Type': 'text/event-stream',
  66. 'Cache-Control': 'no-cache, no-transform',
  67. Connection: 'keep-alive'
  68. });
  69. // Send the endpoint event
  70. // Use a dummy base URL because this._endpoint is relative.
  71. // This allows using URL/URLSearchParams for robust parameter handling.
  72. const dummyBase = 'http://localhost'; // Any valid base works
  73. const endpointUrl = new node_url_1.URL(this._endpoint, dummyBase);
  74. endpointUrl.searchParams.set('sessionId', this._sessionId);
  75. // Reconstruct the relative URL string (pathname + search + hash)
  76. const relativeUrlWithSession = endpointUrl.pathname + endpointUrl.search + endpointUrl.hash;
  77. this.res.write(`event: endpoint\ndata: ${relativeUrlWithSession}\n\n`);
  78. this._sseResponse = this.res;
  79. this.res.on('close', () => {
  80. this._sseResponse = undefined;
  81. this.onclose?.();
  82. });
  83. }
  84. /**
  85. * Handles incoming POST messages.
  86. *
  87. * This should be called when a POST request is made to send a message to the server.
  88. */
  89. async handlePostMessage(req, res, parsedBody) {
  90. if (!this._sseResponse) {
  91. const message = 'SSE connection not established';
  92. res.writeHead(500).end(message);
  93. throw new Error(message);
  94. }
  95. // Validate request headers for DNS rebinding protection
  96. const validationError = this.validateRequestHeaders(req);
  97. if (validationError) {
  98. res.writeHead(403).end(validationError);
  99. this.onerror?.(new Error(validationError));
  100. return;
  101. }
  102. const authInfo = req.auth;
  103. const host = req.headers.host;
  104. const protocol = req.socket instanceof node_tls_1.TLSSocket ? 'https' : 'http';
  105. const fullUrl = host && req.url ? new node_url_1.URL(req.url, `${protocol}://${host}`) : undefined;
  106. const requestInfo = {
  107. headers: req.headers,
  108. url: fullUrl
  109. };
  110. let body;
  111. try {
  112. const ct = content_type_1.default.parse(req.headers['content-type'] ?? '');
  113. if (ct.type !== 'application/json') {
  114. throw new Error(`Unsupported content-type: ${ct.type}`);
  115. }
  116. body =
  117. parsedBody ??
  118. (await (0, raw_body_1.default)(req, {
  119. limit: MAXIMUM_MESSAGE_SIZE,
  120. encoding: ct.parameters.charset ?? 'utf-8'
  121. }));
  122. }
  123. catch (error) {
  124. res.writeHead(400).end(String(error));
  125. this.onerror?.(error);
  126. return;
  127. }
  128. try {
  129. await this.handleMessage(typeof body === 'string' ? JSON.parse(body) : body, { requestInfo, authInfo });
  130. }
  131. catch {
  132. res.writeHead(400).end(`Invalid message: ${body}`);
  133. return;
  134. }
  135. res.writeHead(202).end('Accepted');
  136. }
  137. /**
  138. * Handle a client message, regardless of how it arrived. This can be used to inform the server of messages that arrive via a means different than HTTP POST.
  139. */
  140. async handleMessage(message, extra) {
  141. let parsedMessage;
  142. try {
  143. parsedMessage = types_js_1.JSONRPCMessageSchema.parse(message);
  144. }
  145. catch (error) {
  146. this.onerror?.(error);
  147. throw error;
  148. }
  149. this.onmessage?.(parsedMessage, extra);
  150. }
  151. async close() {
  152. this._sseResponse?.end();
  153. this._sseResponse = undefined;
  154. this.onclose?.();
  155. }
  156. async send(message) {
  157. if (!this._sseResponse) {
  158. throw new Error('Not connected');
  159. }
  160. this._sseResponse.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
  161. }
  162. /**
  163. * Returns the session ID for this transport.
  164. *
  165. * This can be used to route incoming POST requests.
  166. */
  167. get sessionId() {
  168. return this._sessionId;
  169. }
  170. }
  171. exports.SSEServerTransport = SSEServerTransport;
  172. //# sourceMappingURL=sse.js.map