-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add authorization field parser
- Loading branch information
1 parent
8d5eb6f
commit a8f447f
Showing
8 changed files
with
240 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { | ||
capture, | ||
either, | ||
maybe, | ||
namedCapture, | ||
sequence, | ||
suffix, | ||
} from "npm:compose-regexp"; | ||
|
||
const tchar = /[!#$%&'*+.^_`|~\dA-Za-z-]/; | ||
const token = suffix("+", tchar); | ||
const authScheme = namedCapture("authScheme", token); | ||
const SP = / /; | ||
const ALPHA = /[A-Za-z]/; | ||
const DIGIT = /\d/; | ||
const token68 = sequence( | ||
suffix("+", either(ALPHA, DIGIT, /[-._~+/]/)), | ||
suffix("*", "="), | ||
); | ||
|
||
const challenge = sequence( | ||
authScheme, | ||
maybe( | ||
suffix("+", SP), | ||
either( | ||
namedCapture("token68", token68), | ||
namedCapture("authParam", /.+/), | ||
), | ||
), | ||
); | ||
|
||
const OWS = /[ \t]*/; | ||
const BWS = OWS; | ||
|
||
const element = sequence( | ||
maybe(/.*?/, suffix("*", OWS, ",", OWS, maybe(/.*?/))), | ||
); | ||
|
||
const DQUOTE = /"/; | ||
const obsText = /[\x80-\xFF]/; | ||
const HTAB = /\t/; | ||
const qdtext = either(HTAB, SP, "\x21", /[\x23-\x5B/, /[\x5D-\x7E]/, obsText); | ||
const VCHAR = /[\x21-\x7E]/; | ||
const quotedPair = sequence("\\", either(HTAB, SP, VCHAR), obsText); | ||
|
||
const quotedString = sequence( | ||
DQUOTE, | ||
suffix("*", either(qdtext, quotedPair)), | ||
DQUOTE, | ||
); | ||
|
||
const authParam = sequence( | ||
namedCapture("key", token), | ||
BWS, | ||
"=", | ||
BWS, | ||
capture(either(token, quotedString)), | ||
); | ||
|
||
if (import.meta.main) { | ||
console.log("challenge:", challenge); | ||
console.log("element: ", element); | ||
console.log("authParam: ", authParam); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export { | ||
assert, | ||
assertEquals, | ||
assertIsError, | ||
assertThrows, | ||
} from "https://deno.land/std@0.184.0/testing/asserts.ts"; | ||
export { describe, it } from "https://deno.land/std@0.184.0/testing/bdd.ts"; | ||
export type { Authorization } from "./types.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
export { toLowerCase } from "https://deno.land/x/prelude_js@1.2.0/to_lower_case.ts"; | ||
export { trim } from "https://deno.land/x/prelude_js@1.2.0/trim.ts"; | ||
export { head } from "https://deno.land/x/prelude_js@1.2.0/head.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
export { parseAuthorization } from "./parse.ts"; | ||
export type { Authorization, AuthParam } from "./types.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
import { duplicate, parseListField } from "./utils.ts"; | ||
import { head, toLowerCase } from "./deps.ts"; | ||
import type { Authorization, AuthParam } from "./types.ts"; | ||
|
||
const enum Msg { | ||
InvalidSyntax = "unexpected Authorization input", | ||
DuplicatedKeys = "auth param keys should be case insensitive unique", | ||
} | ||
|
||
const reAuthorization = | ||
/^(?<authScheme>[!#$%&'*+.^_`|~\dA-Za-z-]+)(?: +(?:(?<token68>(?:[A-Za-z]|\d|[-._~+/])+=*)|(?<authParam>.+)))?$/; | ||
|
||
/** Parse string into {@link Authorization}. | ||
* | ||
* @throws {SyntaxError} If the input is invalid. | ||
* @throws {Error} If the auth param key is duplicated. | ||
*/ | ||
export function parseAuthorization(input: string): Authorization { | ||
const result = reAuthorization.exec(input); | ||
|
||
if (!result || !result.groups) throw SyntaxError(Msg.InvalidSyntax); | ||
|
||
const { authScheme, token68, authParam: authParamStr } = result.groups; | ||
const token = authParamStr ? parseAuthParam(authParamStr) : token68 ?? null; | ||
|
||
return { authScheme: authScheme!, token }; | ||
} | ||
|
||
const reAuthParam = | ||
/^(?<key>[!#$%&'*+.^_`|~\dA-Za-z-]+)[ \t]*=[ \t]*(?<value>[!#$%&'*+.^_`|~\dA-Za-z-]+|"(?:\t| |!|[\x23-\x5B/, /[\x5D-\x7E]|[\x80-\xFF]|\\(?:\t| |[\x21-\x7E])[\x80-\xFF])*")$/; | ||
|
||
/** Parse string into {@link AuthParam}. | ||
* @throws {Error} If the auth param key is duplicated. | ||
*/ | ||
export function parseAuthParam(input: string): AuthParam { | ||
const list = parseListField(input); | ||
|
||
const entries = list.map((el) => { | ||
const result = reAuthParam.exec(el); | ||
|
||
if (!result || !result.groups) throw SyntaxError(Msg.InvalidSyntax); | ||
|
||
return [result.groups.key, result.groups.value] as const; | ||
}); | ||
|
||
const duplicates = duplicate( | ||
entries | ||
.map<string>(head) | ||
.map(toLowerCase), | ||
); | ||
|
||
if (duplicates.length) throw Error(Msg.DuplicatedKeys); | ||
|
||
return Object.fromEntries(entries); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { parseAuthorization, parseAuthParam } from "./parse.ts"; | ||
import { | ||
assertEquals, | ||
assertIsError, | ||
assertThrows, | ||
describe, | ||
it, | ||
} from "./_dev_deps.ts"; | ||
import authParam from "./auth_param.json" assert { type: "json" }; | ||
import authorization from "./authorization.json" assert { type: "json" }; | ||
|
||
describe("parseAuthorization", () => { | ||
authorization.forEach((suite) => { | ||
it(suite.name, () => { | ||
if (suite.must_fail) { | ||
assertThrows(() => parseAuthorization(suite.header)); | ||
} else { | ||
assertEquals<unknown>(parseAuthorization(suite.header), suite.expected); | ||
} | ||
}); | ||
}); | ||
|
||
it("should be syntax error if the input is invalid syntax", () => { | ||
let err; | ||
|
||
try { | ||
parseAuthorization(""); | ||
} catch (e) { | ||
err = e; | ||
} finally { | ||
assertIsError(err, SyntaxError, "unexpected Authorization input"); | ||
} | ||
}); | ||
|
||
it("should be error if the auth param keys include duplication", () => { | ||
let err; | ||
|
||
try { | ||
parseAuthorization("test a=a, A=a"); | ||
} catch (e) { | ||
err = e; | ||
} finally { | ||
assertIsError( | ||
err, | ||
Error, | ||
"auth param keys should be case insensitive unique", | ||
); | ||
} | ||
}); | ||
}); | ||
|
||
describe("parseAuthParam", () => { | ||
authParam.forEach((v) => { | ||
it(v.name, () => { | ||
if (v.must_fail) { | ||
assertThrows(() => parseAuthParam(v.header)); | ||
} else { | ||
assertEquals<Record<string, unknown>>( | ||
parseAuthParam(v.header), | ||
v.expected!, | ||
); | ||
} | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
export interface Authorization { | ||
readonly authScheme: string; | ||
readonly token: string | Record<string, string> | null; | ||
} | ||
|
||
export interface AuthParam { | ||
readonly [k: string]: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
import { trim } from "./deps.ts"; | ||
|
||
export function duplicate<T>(list: readonly T[]): T[] { | ||
const duplicates = new Set<T>(); | ||
|
||
list.forEach((value, index) => { | ||
if (list.indexOf(value) !== index) { | ||
duplicates.add(value); | ||
} | ||
}); | ||
|
||
return [...duplicates]; | ||
} | ||
|
||
export function parseListField(input: string): string[] { | ||
return input | ||
.split(",") | ||
.map(trim) | ||
.filter(Boolean); | ||
} |