Skip to content

Commit a9facd2

Browse files
committed
fix: uws writeStatus must be called before anything else!
1 parent b847834 commit a9facd2

1 file changed

Lines changed: 81 additions & 52 deletions

File tree

src/services/http/uws/uws-http.ts

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,23 @@ export class UWSHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {
8181
this.listenSocket = token
8282
// handle options requests
8383
this.server.options('/*', (response: uws.HttpResponse, request: uws.HttpRequest) => {
84+
const baseHeaders: Dictionary<string> = {};
8485
if (!this.pluginOptions.allowAllOrigins) {
85-
if (!this.verifyOrigin(response, request)) {
86-
return
86+
const corsValidationHeaders = this.getVerifiedOriginHeaders(response, request);
87+
if (!corsValidationHeaders) { // Verification failed and response terminated
88+
return;
8789
}
88-
this.handleOptions(response, request)
90+
Object.assign(baseHeaders, corsValidationHeaders);
91+
this.handleOptions(response, request, baseHeaders);
8992
} else {
90-
response.cork(() => {
91-
response.writeHeader('Access-Control-Allow-Origin', '*')
92-
this.handleOptions(response, request)
93-
})
93+
baseHeaders['Access-Control-Allow-Origin'] = '*';
94+
this.handleOptions(response, request, baseHeaders);
9495
}
9596
})
9697

98+
// Health check path uses GET, so CORS headers will be applied by registerGetPathPrefix
99+
// The handler calls response(null), which will use sendResponse.
100+
// sendResponse will correctly order writeStatus and add Content-Type: application/json.
97101
this.registerGetPathPrefix(this.pluginOptions.healthCheckPath, (meta: DeepstreamHTTPMeta, response: DeepstreamHTTPResponse) => {
98102
response(null)
99103
})
@@ -134,27 +138,30 @@ export class UWSHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {
134138

135139
const meta = { headers: this.getHeaders(request), url: request.getUrl() }
136140

141+
const accumulatedHeaders: Dictionary<string> = {};
142+
137143
if (!this.pluginOptions.allowAllOrigins) {
138-
if (!this.verifyOrigin(response, request)) {
139-
return
144+
const corsHeaders = this.getVerifiedOriginHeaders(response, request);
145+
if (!corsHeaders) {
146+
return; // Response already terminated
140147
}
148+
Object.assign(accumulatedHeaders, corsHeaders);
141149
} else {
142-
response.cork(() => {
143-
response.writeHeader('Access-Control-Allow-Origin', '*')
144-
})
150+
accumulatedHeaders['Access-Control-Allow-Origin'] = '*';
145151
}
146152

147153
readJson(response, (body: any) => {
148154
handler(
149155
body,
150156
meta,
151-
this.sendResponse.bind(this, response)
157+
(err, data) => this.sendResponse(response, err, data, accumulatedHeaders)
152158
)
153159
}, (code: number) => {
154160
this.terminateResponse(
155161
response,
156162
code,
157-
HTTPStatus[`${code}_MESSAGE` as keyof typeof HTTPStatus] as string
163+
HTTPStatus[`${code}_MESSAGE` as keyof typeof HTTPStatus] as string,
164+
accumulatedHeaders
158165
)
159166
}, this.pluginOptions.maxMessageSize)
160167
})
@@ -167,20 +174,23 @@ export class UWSHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {
167174
this.services.logger.warn(EVENT.ERROR, 'get request aborted')
168175
})
169176

177+
const accumulatedHeaders: Dictionary<string> = {};
178+
170179
if (!this.pluginOptions.allowAllOrigins) {
171-
if (!this.verifyOrigin(response, request)) {
172-
return
180+
const corsHeaders = this.getVerifiedOriginHeaders(response, request);
181+
if (!corsHeaders) {
182+
return; // Response already terminated
173183
}
184+
Object.assign(accumulatedHeaders, corsHeaders);
174185
} else {
175-
response.cork(() => {
176-
response.writeHeader('Access-Control-Allow-Origin', '*')
177-
})
186+
accumulatedHeaders['Access-Control-Allow-Origin'] = '*';
178187
}
179188

180189
handler(
181190
{ headers: this.getHeaders(request), url: request.getUrl() },
182-
this.sendResponse.bind(this, response)
183-
)
191+
// Ensure the bound sendResponse uses these accumulatedHeaders
192+
(err, data) => this.sendResponse(response, err, data, accumulatedHeaders)
193+
);
184194
})
185195
}
186196

@@ -247,13 +257,21 @@ export class UWSHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {
247257
} as any)
248258
}
249259

250-
private terminateResponse (response: uws.HttpResponse, code: number, message?: string) {
260+
private terminateResponse (response: uws.HttpResponse, code: number, message?: string, additionalHeaders: Dictionary<string> = {}) {
251261
response.cork(() => {
252-
response.writeHeader('Content-Type', 'text/plain; charset=utf-8')
253262
response.writeStatus(code.toString())
254-
if (message) {
263+
for (const key in additionalHeaders) {
264+
if (additionalHeaders.hasOwnProperty(key)) {
265+
response.writeHeader(key, additionalHeaders[key])
266+
}
267+
}
268+
// Only set Content-Type if there's a message body, and not for 204/304
269+
if (message && code !== HTTPStatus.NO_CONTENT) {
270+
response.writeHeader('Content-Type', 'text/plain; charset=utf-8')
255271
response.end(`${message}\r\n\r\n`)
256272
} else {
273+
// For 204 NO_CONTENT or other cases without a message, just end.
274+
// uWS requires end() to be called.
257275
response.end()
258276
}
259277
})
@@ -262,32 +280,37 @@ export class UWSHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {
262280
private sendResponse (
263281
response: uws.HttpResponse,
264282
err: { statusCode: number, message: string } | null,
265-
data: { result: string, body: object }
283+
data: { result: string, body: object },
284+
additionalHeaders: Dictionary<string> = {}
266285
): void {
267286
if (err) {
268287
const statusCode = err.statusCode || HTTPStatus.BAD_REQUEST
269-
this.terminateResponse(response, statusCode, err.message)
288+
this.terminateResponse(response, statusCode, err.message, additionalHeaders)
270289
return
271290
}
291+
272292
response.cork(() => {
273-
response.writeHeader('Content-Type', 'application/json; charset=utf-8')
274293
response.writeStatus(HTTPStatus.OK.toString())
294+
for (const key in additionalHeaders) {
295+
if (additionalHeaders.hasOwnProperty(key)) {
296+
response.writeHeader(key, additionalHeaders[key])
297+
}
298+
}
299+
response.writeHeader('Content-Type', 'application/json; charset=utf-8')
275300
if (data) {
276301
response.end(`${JSON.stringify(data)}\r\n\r\n`)
277302
} else {
278303
response.end()
279304
}
280305
})
281306
}
282-
283307
public getHeaders (req: uws.HttpRequest) {
284308
const headers: Dictionary<string> = {}
285309
for (const wantedHeader of this.pluginOptions.headers) {
286310
headers[wantedHeader] = req.getHeader(wantedHeader).toString()
287311
}
288312
return headers
289313
}
290-
291314
private getSLLParams (options: any) {
292315
if (!options.ssl) {
293316
return null
@@ -301,7 +324,6 @@ export class UWSHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {
301324
if (!cert_file_name) {
302325
this.services.logger.fatal(EVENT.PLUGIN_INITIALIZATION_ERROR, 'Must also include ssl cert in order to use SSL')
303326
}
304-
305327
return {
306328
key_file_name,
307329
cert_file_name,
@@ -312,63 +334,71 @@ export class UWSHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {
312334
return null
313335
}
314336

315-
private verifyOrigin (response: uws.HttpResponse, request: uws.HttpRequest): boolean {
337+
// This method now either terminates the response or returns headers for the caller to use.
338+
private getVerifiedOriginHeaders (response: uws.HttpResponse, request: uws.HttpRequest): Dictionary<string> | null {
316339
const requestOriginUrl = request.getHeader('origin') as string || request.getHeader('referer') as string
317340
const requestHostUrl = request.getHeader('host')
318341

319342
if (this.pluginOptions.hostUrl && requestHostUrl !== this.pluginOptions.hostUrl) {
320343
this.terminateResponse(response, HTTPStatus.FORBIDDEN, 'Forbidden Host.')
321-
return false
344+
return null
322345
}
323346

324347
if (this.pluginOptions.origins!.indexOf(requestOriginUrl) === -1) {
325348
if (!requestOriginUrl) {
326349
this.terminateResponse(
327350
response,
328351
HTTPStatus.FORBIDDEN,
329-
'CORS is configured for this. All requests must set a valid "Origin" header.'
352+
'CORS is configured for this. All requests must set a valid "Origin" header.',
353+
// No additional headers known at this point for the error response itself
330354
)
331355
} else {
332356
this.terminateResponse(
333357
response,
334358
HTTPStatus.FORBIDDEN,
335-
`Origin "${requestOriginUrl}" is forbidden.`
359+
`Origin "${requestOriginUrl}" is forbidden.`,
360+
// No additional headers known at this point for the error response itself
336361
)
337362
}
338-
return false
363+
return null
339364
}
340365

341-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
342-
response.cork(() => {
343-
response.writeHeader('Access-Control-Allow-Origin', requestOriginUrl)
344-
response.writeHeader('Access-Control-Allow-Credentials', 'true')
345-
response.writeHeader('Vary', 'Origin')
346-
})
347-
348-
return true
366+
// If verification is successful, return the headers to be set by the caller.
367+
return {
368+
'Access-Control-Allow-Origin': requestOriginUrl,
369+
'Access-Control-Allow-Credentials': 'true', // Typically needed with specific origins
370+
'Vary': 'Origin' // Good practice when Access-Control-Allow-Origin is dynamic
371+
};
349372
}
350373

351-
private handleOptions (response: uws.HttpResponse, request: uws.HttpRequest): void {
374+
private handleOptions (response: uws.HttpResponse, request: uws.HttpRequest, baseCorsHeaders: Dictionary<string>): void {
375+
const allHeadersForResponse = { ...baseCorsHeaders };
376+
352377
const requestMethod = request.getHeader('access-control-request-method') as string | undefined
353378
if (!requestMethod) {
354379
this.terminateResponse(
355380
response,
356381
HTTPStatus.BAD_REQUEST,
357-
'Missing header "Access-Control-Request-Method".'
382+
'Missing header "Access-Control-Request-Method".',
383+
allHeadersForResponse // Pass along already determined CORS headers
358384
)
359385
return
360386
}
361387
if (this.methods.indexOf(requestMethod) === -1) {
362388
this.terminateResponse(
363389
response,
364390
HTTPStatus.FORBIDDEN,
365-
`Method ${requestMethod} is forbidden. Supported methods: ${this.methodsStr}`
391+
`Method ${requestMethod} is forbidden. Supported methods: ${this.methodsStr}`,
392+
allHeadersForResponse
366393
)
367394
return
368395
}
369396

370397
const requestHeadersRaw = request.getHeader('access-control-request-headers') as string | undefined
371398
if (!requestHeadersRaw) {
399+
// Some browsers might not send this for simple requests, but for preflight it's expected.
400+
// Depending on strictness, this could be an error or allowed.
401+
// For now, let's assume it's required for a preflight OPTIONS.
372402
this.terminateResponse(
373403
response,
374404
HTTPStatus.BAD_REQUEST,
@@ -382,17 +412,16 @@ export class UWSHTTP extends DeepstreamPlugin implements DeepstreamHTTPService {
382412
this.terminateResponse(
383413
response,
384414
HTTPStatus.FORBIDDEN,
385-
`Header ${requestHeaders[i]} is forbidden. Supported headers: ${this.headersStr}`
415+
`Header ${requestHeaders[i]} is forbidden. Supported headers: ${this.headersStr}`,
416+
allHeadersForResponse
386417
)
387418
return
388419
}
389420
}
390421

391-
response.cork(() => {
392-
response.writeHeader('Access-Control-Allow-Methods', this.methodsStr)
393-
response.writeHeader('Access-Control-Allow-Headers', this.headersStr)
394-
this.terminateResponse(response, HTTPStatus.NO_CONTENT)
395-
})
422+
allHeadersForResponse['Access-Control-Allow-Methods'] = this.methodsStr;
423+
allHeadersForResponse['Access-Control-Allow-Headers'] = this.headersStr;
424+
this.terminateResponse(response, HTTPStatus.NO_CONTENT, undefined, allHeadersForResponse);
396425
}
397426
}
398427

0 commit comments

Comments
 (0)