wildebeest/backend/src/utils/httpsigjs/parser.ts

366 wiersze
10 KiB
TypeScript

// @ts-nocheck
// Copyright 2012 Joyent, Inc. All rights reserved.
import { HEADER, HttpSignatureError, InvalidAlgorithmError, validateAlgorithm } from './utils'
///--- Globals
let State = {
New: 0,
Params: 1,
}
let ParamsState = {
Name: 0,
Quote: 1,
Value: 2,
Comma: 3,
Number: 4,
}
///--- Specific Errors
class ExpiredRequestError extends HttpSignatureError {
constructor(message: string) {
super(message, ExpiredRequestError)
}
}
class InvalidHeaderError extends HttpSignatureError {
constructor(message: string) {
super(message, InvalidHeaderError)
}
}
class InvalidParamsError extends HttpSignatureError {
constructor(message: string) {
super(message, InvalidParamsError)
}
}
class MissingHeaderError extends HttpSignatureError {
constructor(message: string) {
super(message, MissingHeaderError)
}
}
class StrictParsingError extends HttpSignatureError {
constructor(message: string) {
super(message, StrictParsingError)
}
}
type Options = {
clockSkew: number
headers: string[]
strict: boolean
}
export type ParsedSignature = {
signature: string
keyId: string
signingString: string
algorithm: string
}
///--- Exported API
/**
* Parses the 'Authorization' header out of an http.ServerRequest object.
*
* Note that this API will fully validate the Authorization header, and throw
* on any error. It will not however check the signature, or the keyId format
* as those are specific to your environment. You can use the options object
* to pass in extra constraints.
*
* As a response object you can expect this:
*
* {
* "scheme": "Signature",
* "params": {
* "keyId": "foo",
* "algorithm": "rsa-sha256",
* "headers": [
* "date" or "x-date",
* "digest"
* ],
* "signature": "base64"
* },
* "signingString": "ready to be passed to crypto.verify()"
* }
*
* @param {Object} request an http.ServerRequest.
* @param {Object} options an optional options object with:
* - clockSkew: allowed clock skew in seconds (default 300).
* - headers: required header names (def: date or x-date)
* - strict: should enforce latest spec parsing
* (default: false).
* @return {Object} parsed out object (see above).
* @throws {TypeError} on invalid input.
* @throws {InvalidHeaderError} on an invalid Authorization header error.
* @throws {InvalidParamsError} if the params in the scheme are invalid.
* @throws {MissingHeaderError} if the params indicate a header not present,
* either in the request headers from the params,
* or not in the params from a required header
* in options.
* @throws {StrictParsingError} if old attributes are used in strict parsing
* mode.
* @throws {ExpiredRequestError} if the value of date or x-date exceeds skew.
*/
export function parseRequest(request: Request, options?: Options): ParsedSignature {
if (options === undefined) {
options = {
clockSkew: 300,
headers: ['host', '(request-target)'],
strict: false,
}
}
if (request.method == 'POST') {
options.headers.push('digest')
}
let headers = [request.headers.has('x-date') ? 'x-date' : 'date']
if (options.headers !== undefined) {
headers = options.headers
}
let authz = request.headers.get(HEADER.AUTH) || request.headers.get(HEADER.SIG)
if (!authz) {
let errHeader = HEADER.AUTH + ' or ' + HEADER.SIG
throw new MissingHeaderError('no ' + errHeader + ' header ' + 'present in the request')
}
options.clockSkew = options.clockSkew || 300
let i = 0
let state = authz === request.headers.get(HEADER.SIG) ? State.Params : State.New
let substate = ParamsState.Name
let tmpName = ''
let tmpValue = ''
let parsed = {
scheme: authz === request.headers.get(HEADER.SIG) ? 'Signature' : '',
params: {},
signingString: '',
}
for (i = 0; i < authz.length; i++) {
let c = authz.charAt(i)
let code = c.charCodeAt(0)
switch (Number(state)) {
case State.New:
if (c !== ' ') parsed.scheme += c
else state = State.Params
break
case State.Params:
switch (Number(substate)) {
case ParamsState.Name:
// restricted name of A-Z / a-z
if (
(code >= 0x41 && code <= 0x5a) || // A-Z
(code >= 0x61 && code <= 0x7a)
) {
// a-z
tmpName += c
} else if (c === '=') {
if (tmpName.length === 0) throw new InvalidHeaderError('bad param format')
substate = ParamsState.Quote
} else {
throw new InvalidHeaderError('bad param format')
}
break
case ParamsState.Quote:
if (c === '"') {
tmpValue = ''
substate = ParamsState.Value
} else {
//number
substate = ParamsState.Number
code = c.charCodeAt(0)
if (code < 0x30 || code > 0x39) {
//character not in 0-9
throw new InvalidHeaderError('bad param format')
}
tmpValue = c
}
break
case ParamsState.Value:
if (c === '"') {
parsed.params[tmpName] = tmpValue
substate = ParamsState.Comma
} else {
tmpValue += c
}
break
case ParamsState.Number:
if (c === ',') {
parsed.params[tmpName] = parseInt(tmpValue, 10)
tmpName = ''
substate = ParamsState.Name
} else {
code = c.charCodeAt(0)
if (code < 0x30 || code > 0x39) {
//character not in 0-9
throw new InvalidHeaderError('bad param format')
}
tmpValue += c
}
break
case ParamsState.Comma:
if (c === ',') {
tmpName = ''
substate = ParamsState.Name
} else {
throw new InvalidHeaderError('bad param format')
}
break
default:
throw new Error('Invalid substate')
}
break
default:
throw new Error('Invalid substate')
}
}
if (!parsed.params.headers || parsed.params.headers === '') {
if (request.headers.has('x-date')) {
parsed.params.headers = ['x-date']
} else {
parsed.params.headers = ['date']
}
} else {
parsed.params.headers = parsed.params.headers.split(' ')
}
// Minimally validate the parsed object
if (!parsed.scheme || parsed.scheme !== 'Signature') throw new InvalidHeaderError('scheme was not "Signature"')
if (!parsed.params.keyId) throw new InvalidHeaderError('keyId was not specified')
if (!parsed.params.algorithm) throw new InvalidHeaderError('algorithm was not specified')
if (!parsed.params.signature) throw new InvalidHeaderError('signature was not specified')
if (['date', 'x-date', '(created)'].every((hdr) => parsed.params.headers.indexOf(hdr) < 0)) {
throw new MissingHeaderError('no signed date header')
}
// Check the algorithm against the official list
try {
validateAlgorithm(parsed.params.algorithm, 'rsa')
} catch (e) {
if (e instanceof InvalidAlgorithmError)
throw new InvalidParamsError(parsed.params.algorithm + ' is not ' + 'supported')
else throw e
}
// Build the signingString
for (i = 0; i < parsed.params.headers.length; i++) {
let h = parsed.params.headers[i].toLowerCase()
parsed.params.headers[i] = h
if (h === 'request-line') {
if (!options.strict) {
/*
* We allow headers from the older spec drafts if strict parsing isn't
* specified in options.
*/
parsed.signingString += request.method + ' ' + request.url + ' ' + request.cf?.httpProtocol
} else {
/* Strict parsing doesn't allow older draft headers. */
throw new StrictParsingError('request-line is not a valid header ' + 'with strict parsing enabled.')
}
} else if (h === '(request-target)') {
const { pathname, search } = new URL(request.url)
parsed.signingString += '(request-target): ' + `${request.method.toLowerCase()} ${pathname}${search}`
} else if (h === '(keyid)') {
parsed.signingString += '(keyid): ' + parsed.params.keyId
} else if (h === '(algorithm)') {
parsed.signingString += '(algorithm): ' + parsed.params.algorithm
} else if (h === '(opaque)') {
let opaque = parsed.params.opaque
if (opaque === undefined) {
throw new MissingHeaderError('opaque param was not in the ' + authzHeaderName + ' header')
}
parsed.signingString += '(opaque): ' + opaque
} else if (h === '(created)') {
parsed.signingString += '(created): ' + parsed.params.created
} else if (h === '(expires)') {
parsed.signingString += '(expires): ' + parsed.params.expires
} else {
let value = request.headers.get(h)
if (value === null) throw new MissingHeaderError(h + ' was not in the request')
parsed.signingString += h + ': ' + value
}
if (i + 1 < parsed.params.headers.length) parsed.signingString += '\n'
}
// Check against the constraints
let date
let skew
if (request.headers.date || request.headers.has('x-date')) {
if (request.headers.has('x-date')) {
date = new Date(request.headers.get('x-date') as string)
} else {
date = new Date(request.headers.date)
}
let now = new Date()
skew = Math.abs(now.getTime() - date.getTime())
if (skew > options.clockSkew * 1000) {
throw new ExpiredRequestError('clock skew of ' + skew / 1000 + 's was greater than ' + options.clockSkew + 's')
}
}
if (parsed.params.created) {
skew = parsed.params.created - Math.floor(Date.now() / 1000)
if (skew > options.clockSkew) {
throw new ExpiredRequestError(
'Created lies in the future (with ' + 'skew ' + skew + 's greater than allowed ' + options.clockSkew + 's'
)
}
if (Math.abs(skew) > options.clockSkew) {
throw new ExpiredRequestError(
'clock skew of ' + Math.abs(skew) + 's greater than allowed ' + options.clockSkew + 's'
)
}
}
if (parsed.params.expires) {
let expiredSince = Math.floor(Date.now() / 1000) - parsed.params.expires
if (expiredSince > options.clockSkew) {
throw new ExpiredRequestError(
'Request expired with skew ' + expiredSince + 's greater than allowed ' + options.clockSkew + 's'
)
}
}
headers.forEach(function (hdr) {
// Remember that we already checked any headers in the params
// were in the request, so if this passes we're good.
if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0)
throw new MissingHeaderError(hdr + ' was not a signed header')
})
parsed.params.algorithm = parsed.params.algorithm.toLowerCase()
parsed.algorithm = parsed.params.algorithm.toUpperCase()
parsed.keyId = parsed.params.keyId
parsed.opaque = parsed.params.opaque
parsed.signature = parsed.params.signature
return parsed
}