| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- import { randomUUID } from 'node:crypto';
- import { TLSSocket } from 'node:tls';
- import { JSONRPCMessageSchema } from '../types.js';
- import getRawBody from 'raw-body';
- import contentType from 'content-type';
- import { URL } from 'node:url';
- const MAXIMUM_MESSAGE_SIZE = '4mb';
- /**
- * Server transport for SSE: this will send messages over an SSE connection and receive messages from HTTP POST requests.
- *
- * This transport is only available in Node.js environments.
- * @deprecated SSEServerTransport is deprecated. Use StreamableHTTPServerTransport instead.
- */
- export class SSEServerTransport {
- /**
- * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`.
- */
- constructor(_endpoint, res, options) {
- this._endpoint = _endpoint;
- this.res = res;
- this._sessionId = randomUUID();
- this._options = options || { enableDnsRebindingProtection: false };
- }
- /**
- * Validates request headers for DNS rebinding protection.
- * @returns Error message if validation fails, undefined if validation passes.
- */
- validateRequestHeaders(req) {
- // Skip validation if protection is not enabled
- if (!this._options.enableDnsRebindingProtection) {
- return undefined;
- }
- // Validate Host header if allowedHosts is configured
- if (this._options.allowedHosts && this._options.allowedHosts.length > 0) {
- const hostHeader = req.headers.host;
- if (!hostHeader || !this._options.allowedHosts.includes(hostHeader)) {
- return `Invalid Host header: ${hostHeader}`;
- }
- }
- // Validate Origin header if allowedOrigins is configured
- if (this._options.allowedOrigins && this._options.allowedOrigins.length > 0) {
- const originHeader = req.headers.origin;
- if (originHeader && !this._options.allowedOrigins.includes(originHeader)) {
- return `Invalid Origin header: ${originHeader}`;
- }
- }
- return undefined;
- }
- /**
- * Handles the initial SSE connection request.
- *
- * This should be called when a GET request is made to establish the SSE stream.
- */
- async start() {
- if (this._sseResponse) {
- throw new Error('SSEServerTransport already started! If using Server class, note that connect() calls start() automatically.');
- }
- this.res.writeHead(200, {
- 'Content-Type': 'text/event-stream',
- 'Cache-Control': 'no-cache, no-transform',
- Connection: 'keep-alive'
- });
- // Send the endpoint event
- // Use a dummy base URL because this._endpoint is relative.
- // This allows using URL/URLSearchParams for robust parameter handling.
- const dummyBase = 'http://localhost'; // Any valid base works
- const endpointUrl = new URL(this._endpoint, dummyBase);
- endpointUrl.searchParams.set('sessionId', this._sessionId);
- // Reconstruct the relative URL string (pathname + search + hash)
- const relativeUrlWithSession = endpointUrl.pathname + endpointUrl.search + endpointUrl.hash;
- this.res.write(`event: endpoint\ndata: ${relativeUrlWithSession}\n\n`);
- this._sseResponse = this.res;
- this.res.on('close', () => {
- this._sseResponse = undefined;
- this.onclose?.();
- });
- }
- /**
- * Handles incoming POST messages.
- *
- * This should be called when a POST request is made to send a message to the server.
- */
- async handlePostMessage(req, res, parsedBody) {
- if (!this._sseResponse) {
- const message = 'SSE connection not established';
- res.writeHead(500).end(message);
- throw new Error(message);
- }
- // Validate request headers for DNS rebinding protection
- const validationError = this.validateRequestHeaders(req);
- if (validationError) {
- res.writeHead(403).end(validationError);
- this.onerror?.(new Error(validationError));
- return;
- }
- const authInfo = req.auth;
- const host = req.headers.host;
- const protocol = req.socket instanceof TLSSocket ? 'https' : 'http';
- const fullUrl = host && req.url ? new URL(req.url, `${protocol}://${host}`) : undefined;
- const requestInfo = {
- headers: req.headers,
- url: fullUrl
- };
- let body;
- try {
- const ct = contentType.parse(req.headers['content-type'] ?? '');
- if (ct.type !== 'application/json') {
- throw new Error(`Unsupported content-type: ${ct.type}`);
- }
- body =
- parsedBody ??
- (await getRawBody(req, {
- limit: MAXIMUM_MESSAGE_SIZE,
- encoding: ct.parameters.charset ?? 'utf-8'
- }));
- }
- catch (error) {
- res.writeHead(400).end(String(error));
- this.onerror?.(error);
- return;
- }
- try {
- await this.handleMessage(typeof body === 'string' ? JSON.parse(body) : body, { requestInfo, authInfo });
- }
- catch {
- res.writeHead(400).end(`Invalid message: ${body}`);
- return;
- }
- res.writeHead(202).end('Accepted');
- }
- /**
- * 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.
- */
- async handleMessage(message, extra) {
- let parsedMessage;
- try {
- parsedMessage = JSONRPCMessageSchema.parse(message);
- }
- catch (error) {
- this.onerror?.(error);
- throw error;
- }
- this.onmessage?.(parsedMessage, extra);
- }
- async close() {
- this._sseResponse?.end();
- this._sseResponse = undefined;
- this.onclose?.();
- }
- async send(message) {
- if (!this._sseResponse) {
- throw new Error('Not connected');
- }
- this._sseResponse.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
- }
- /**
- * Returns the session ID for this transport.
- *
- * This can be used to route incoming POST requests.
- */
- get sessionId() {
- return this._sessionId;
- }
- }
- //# sourceMappingURL=sse.js.map
|