Skip to content

Commit

Permalink
feat: add authorization field parser
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed Apr 22, 2023
1 parent 8d5eb6f commit a8f447f
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 0 deletions.
64 changes: 64 additions & 0 deletions _abnf.ts
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);
}
8 changes: 8 additions & 0 deletions _dev_deps.ts
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";
6 changes: 6 additions & 0 deletions deps.ts
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";
5 changes: 5 additions & 0 deletions mod.ts
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";
58 changes: 58 additions & 0 deletions parse.ts
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);
}
65 changes: 65 additions & 0 deletions parse_test.ts
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!,
);
}
});
});
});
11 changes: 11 additions & 0 deletions types.ts
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;
}
23 changes: 23 additions & 0 deletions utils.ts
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);
}

0 comments on commit a8f447f

Please sign in to comment.