-
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(middleware): add HSTS header middleware factory
- Loading branch information
1 parent
979e510
commit 4ead771
Showing
7 changed files
with
262 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,13 @@ | ||
export { | ||
assert, | ||
assertEquals, | ||
assertThrows, | ||
} from "https://deno.land/std@0.177.0/testing/asserts.ts"; | ||
export { describe, it } from "https://deno.land/std@0.177.0/testing/bdd.ts"; | ||
export { | ||
assertSpyCall, | ||
assertSpyCalls, | ||
spy, | ||
} from "https://deno.land/std@0.177.0/testing/mock.ts"; | ||
export { Status } from "https://deno.land/std@0.177.0/http/http_status.ts"; | ||
export { equalsResponse } from "https://deno.land/x/http_utils@1.0.0-beta.13/response.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,12 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
export { type Middleware } from "https://deno.land/x/http_middleware@1.0.0/mod.ts"; | ||
export { | ||
isNonNegativeInteger, | ||
isString, | ||
} from "https://deno.land/x/isx@1.0.0-beta.24/mod.ts"; | ||
|
||
export const enum SecurityHeaders { | ||
StrictTransportSecurity = "strict-transport-security", | ||
} |
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,63 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
import { type Middleware, SecurityHeaders } from "./deps.ts"; | ||
import { StrictTransportSecurity, stringify } from "./sts.ts"; | ||
|
||
/** | ||
* @example | ||
* ```http | ||
* Strict-Transport-Security: max-age=15552000; includeSubDomains | ||
* ``` | ||
*/ | ||
const DefaultSts: StrictTransportSecurity = { | ||
maxAge: 60 * 60 * 24 * 180, // half-year, | ||
includeSubDomains: true, | ||
}; | ||
|
||
/** Create `Strict-Transport-Security` header field value middleware. | ||
* | ||
* @example | ||
* ```ts | ||
* import { hstsHeader } from "https://deno.land/x/hsts_middleware@$VERSION/mod.ts"; | ||
* import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; | ||
* | ||
* declare const request: Request; | ||
* const middleware = hsts(); | ||
* const response = await middleware( | ||
* request, | ||
* (request: Request) => new Response(), | ||
* ); | ||
* | ||
* assertEquals( | ||
* response.headers.get( | ||
* "strict-transport-security", | ||
* "max-age=15552000; includeSubDomains", | ||
* ), | ||
* ); | ||
* ``` | ||
* | ||
* @throws {TypeError} If the {@link StrictTransportSecurity.maxAge} is not non-negative integer. | ||
*/ | ||
export function hsts( | ||
strictTransportSecurity?: StrictTransportSecurity, | ||
): Middleware { | ||
const sts = strictTransportSecurity ?? DefaultSts; | ||
const stsValue = stringify(sts); | ||
|
||
return async (request, next) => { | ||
const response = await next(request); | ||
|
||
return withSts(response, stsValue); | ||
}; | ||
} | ||
|
||
export function withSts(response: Response, fieldValue: string): Response { | ||
response = response.clone(); | ||
|
||
if (!response.headers.has(SecurityHeaders.StrictTransportSecurity)) { | ||
response.headers.set(SecurityHeaders.StrictTransportSecurity, fieldValue); | ||
} | ||
|
||
return response; | ||
} |
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,75 @@ | ||
import { hsts, withSts } from "./middleware.ts"; | ||
import { | ||
assert, | ||
assertThrows, | ||
describe, | ||
equalsResponse, | ||
it, | ||
} from "./_dev_deps.ts"; | ||
|
||
describe("hsts", () => { | ||
it("should include sts header", async () => { | ||
const middleware = hsts(); | ||
|
||
const response = await middleware( | ||
new Request("test:"), | ||
() => new Response(), | ||
); | ||
|
||
assert( | ||
await equalsResponse( | ||
response, | ||
new Response(null, { | ||
headers: { | ||
"strict-transport-security": "max-age=15552000; includeSubDomains", | ||
}, | ||
}), | ||
true, | ||
), | ||
); | ||
}); | ||
|
||
it("should change sts header", async () => { | ||
const middleware = hsts({ | ||
maxAge: 100, | ||
includeSubDomains: true, | ||
preload: true, | ||
}); | ||
|
||
const response = await middleware( | ||
new Request("test:"), | ||
() => new Response(), | ||
); | ||
|
||
assert( | ||
await equalsResponse( | ||
response, | ||
new Response(null, { | ||
headers: { | ||
"strict-transport-security": | ||
"max-age=100; includeSubDomains; preload", | ||
}, | ||
}), | ||
true, | ||
), | ||
); | ||
}); | ||
|
||
it("should throw error if the sts is invalid", () => { | ||
assertThrows(() => hsts({ maxAge: NaN })); | ||
}); | ||
}); | ||
|
||
describe("withSts", () => { | ||
it("should add sts header", async () => { | ||
assert( | ||
await equalsResponse( | ||
withSts(new Response(), "max-age=100"), | ||
new Response(null, { | ||
headers: { "strict-transport-security": "max-age=100" }, | ||
}), | ||
true, | ||
), | ||
); | ||
}); | ||
}); |
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 { hsts } from "./middleware.ts"; | ||
export { type StrictTransportSecurity } from "./sts.ts"; | ||
export { type Middleware } from "./deps.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,53 @@ | ||
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
import { isNonNegativeInteger } from "./deps.ts"; | ||
|
||
/** HTTP `Strict-Transport-Security` header field. */ | ||
export interface StrictTransportSecurity { | ||
/** The number of seconds, after the reception of the STS header field, during which the UA regards the host. */ | ||
readonly maxAge: number; | ||
|
||
/** Whether the rule applies to all subdomains or not. */ | ||
readonly includeSubDomains?: boolean; | ||
|
||
/** Whether the domain do preload or not. | ||
* @see https://hstspreload.org/ | ||
*/ | ||
readonly preload?: boolean; | ||
} | ||
const enum Directive { | ||
maxAge = "max-age", | ||
includeSubDomains = "includeSubDomains", | ||
preload = "preload", | ||
} | ||
|
||
/** Serialize {@link StrictTransportSecurity} into string. | ||
* @throws {TypeError} If the {@link StrictTransportSecurity.maxAge} is not non-negative integer. | ||
*/ | ||
export function stringify(sts: StrictTransportSecurity): string { | ||
assertValidSts(sts); | ||
|
||
const maxAge = `${Directive.maxAge}=${sts.maxAge}`; | ||
const includeSubDomains = sts.includeSubDomains | ||
? Directive.includeSubDomains | ||
: undefined; | ||
const preload = sts.preload ? Directive.preload : undefined; | ||
const directives: string[] = [ | ||
maxAge, | ||
includeSubDomains, | ||
preload, | ||
].filter(Boolean) as string[]; | ||
|
||
return directives.join("; "); | ||
} | ||
|
||
enum Msg { | ||
InvalidMaxAge = "maxAge must be non-negative integer.", | ||
} | ||
|
||
function assertValidSts(sts: StrictTransportSecurity): asserts sts { | ||
if (!isNonNegativeInteger(sts.maxAge)) { | ||
throw TypeError(Msg.InvalidMaxAge); | ||
} | ||
} |
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,40 @@ | ||
import { assertEquals, assertThrows, describe, it } from "./_dev_deps.ts"; | ||
import { type StrictTransportSecurity, stringify } from "./sts.ts"; | ||
|
||
describe("stringify", () => { | ||
it("should return string if the input is valid StrictTransportSecurity", () => { | ||
const table: [StrictTransportSecurity, string][] = [ | ||
[{ maxAge: 0 }, "max-age=0"], | ||
[{ maxAge: 100 }, "max-age=100"], | ||
[ | ||
{ maxAge: 100, includeSubDomains: true }, | ||
"max-age=100; includeSubDomains", | ||
], | ||
[ | ||
{ maxAge: 100, includeSubDomains: true, preload: true }, | ||
"max-age=100; includeSubDomains; preload", | ||
], | ||
[ | ||
{ maxAge: 100, includeSubDomains: false, preload: false }, | ||
"max-age=100", | ||
], | ||
]; | ||
|
||
table.forEach(([sts, expected]) => { | ||
assertEquals(stringify(sts), expected); | ||
}); | ||
}); | ||
|
||
it("should throw error if the input is invalid StrictTransportSecurity", () => { | ||
const table: StrictTransportSecurity[] = [ | ||
{ maxAge: NaN }, | ||
{ maxAge: 1.1 }, | ||
{ maxAge: -1 }, | ||
{ maxAge: Infinity }, | ||
]; | ||
|
||
table.forEach((sts) => { | ||
assertThrows(() => stringify(sts)); | ||
}); | ||
}); | ||
}); |