Skip to content

Commit

Permalink
Allow charset in content-type header of incoming requests (#2728)
Browse files Browse the repository at this point in the history
* Allow charset in content-type header of incoming requests
  • Loading branch information
matthieusieben authored Aug 20, 2024
1 parent 3ebcd4e commit 5131b02
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 15 deletions.
6 changes: 6 additions & 0 deletions .changeset/seven-shrimps-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@atproto/oauth-provider": patch
---

Allow charset in content-type header of incoming requests

1 change: 1 addition & 0 deletions packages/oauth/oauth-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@atproto/oauth-types": "workspace:*",
"@hapi/accept": "^6.0.3",
"@hapi/bourne": "^3.0.0",
"@hapi/content": "^6.0.0",
"cookie": "^0.6.0",
"http-errors": "^2.0.0",
"ioredis": "^5.3.2",
Expand Down
50 changes: 37 additions & 13 deletions packages/oauth/oauth-provider/src/lib/http/parser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import { parse as parseJson } from '@hapi/bourne'
import { type as hapiContentType } from '@hapi/content'
import createHttpError from 'http-errors'

export type JsonScalar = string | number | boolean | null
export type Json = JsonScalar | Json[] | { [_ in string]?: Json }

export const parseContentType = (type: string): ContentType => {
try {
return hapiContentType(type)
} catch (err) {
// De-boomify the error
if (err?.['isBoom']) {
throw createHttpError(err['output']['statusCode'], err['message'])
}

throw err
}
}

export type ContentType = {
mime: string
charset?: string
boundary?: string
}

export type Parser<T extends string = string, R = unknown> = {
readonly name: string
readonly test: (type: string) => type is T
readonly parse: (buffer: Buffer) => R
readonly test: (mime: string) => mime is T
readonly parse: (buffer: Buffer, type: ContentType) => R
}

export type ParserName<P extends Parser> = P extends { readonly name: infer N }
Expand All @@ -22,12 +42,13 @@ export type ParserForType<P extends Parser, T> =
export const parsers = [
{
name: 'json',
test: (
type: string,
): type is `application/json` | `application/${string}+json` => {
return /^application\/(?:.+\+)?json$/.test(type)
test: (mime): mime is `application/json` | `application/${string}+json` => {
return /^application\/(?:.+\+)?json$/.test(mime)
},
parse: (buffer: Buffer): Json => {
parse: (buffer, { charset }): Json => {
if (charset != null && !/^utf-?8$/i.test(charset)) {
throw createHttpError(415, 'Unsupported charset')
}
try {
return parseJson(buffer.toString())
} catch (err) {
Expand All @@ -37,10 +58,13 @@ export const parsers = [
},
{
name: 'urlencoded',
test: (type: string): type is 'application/x-www-form-urlencoded' => {
return type === 'application/x-www-form-urlencoded'
test: (mime): mime is 'application/x-www-form-urlencoded' => {
return mime === 'application/x-www-form-urlencoded'
},
parse: (buffer: Buffer): Partial<Record<string, string>> => {
parse: (buffer, { charset }): Partial<Record<string, string>> => {
if (charset != null && !/^utf-?8$/i.test(charset)) {
throw createHttpError(415, 'Unsupported charset')
}
try {
if (!buffer.length) return {}
return Object.fromEntries(new URLSearchParams(buffer.toString()))
Expand All @@ -51,10 +75,10 @@ export const parsers = [
},
{
name: 'bytes',
test: (type: string): type is 'application/octet-stream' => {
return type === 'application/octet-stream'
test: (mime): mime is 'application/octet-stream' => {
return mime === 'application/octet-stream'
},
parse: (buffer: Buffer): Buffer => buffer,
parse: (buffer): Buffer => buffer,
},
] as const satisfies Parser[]

Expand Down
7 changes: 5 additions & 2 deletions packages/oauth/oauth-provider/src/lib/http/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
KnownNames,
KnownParser,
KnownTypes,
parseContentType,
ParserForType,
ParserResult,
parsers,
Expand Down Expand Up @@ -64,15 +65,17 @@ export async function parseStream(
throw createHttpError(400, 'Invalid content-type')
}

const type = parseContentType(contentType)

const parser = parsers.find(
(parser) =>
allow?.includes(parser.name) !== false && parser.test(contentType),
allow?.includes(parser.name) !== false && parser.test(type.mime),
)

if (!parser) {
throw createHttpError(400, 'Unsupported content-type')
}

const buffer = await readStream(req)
return parser.parse(buffer)
return parser.parse(buffer, type)
}
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 5131b02

Please sign in to comment.