-
Notifications
You must be signed in to change notification settings - Fork 236
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
179 additions
and
87 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 |
---|---|---|
@@ -1,58 +1,51 @@ | ||
import { WorkerError } from "./common.js"; | ||
import { WorkerError } from "./common.js" | ||
|
||
function parseBasicAuth(request) { | ||
const Authorization = request.headers.get('Authorization'); | ||
|
||
const [scheme, encoded] = Authorization.split(' '); | ||
|
||
// The Authorization header must start with Basic, followed by a space. | ||
if (!encoded || scheme !== 'Basic') { | ||
throw new WorkerError(400, 'malformed authorization header'); | ||
} | ||
|
||
const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0)) | ||
const decoded = new TextDecoder().decode(buffer).normalize(); | ||
|
||
const index = decoded.indexOf(':'); | ||
// Encoding function | ||
export function encodeBasicAuth(username, password) { | ||
const credentials = `${username}:${password}` | ||
const encodedCredentials = Buffer.from(credentials).toString("base64") | ||
return `Basic ${encodedCredentials}` | ||
} | ||
|
||
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) { | ||
throw WorkerError(400, 'invalid authorization value'); | ||
// Decoding function | ||
export function decodeBasicAuth(encodedString) { | ||
const [scheme, encodedCredentials] = encodedString.split(" ") | ||
if (scheme !== "Basic") { | ||
throw new WorkerError(400, "Invalid authentication scheme") | ||
} | ||
|
||
return { | ||
user: decoded.substring(0, index), | ||
pass: decoded.substring(index + 1), | ||
}; | ||
const credentials = Buffer.from(encodedCredentials, "base64").toString("utf-8") | ||
const [username, password] = credentials.split(":") | ||
return { username, password } | ||
} | ||
|
||
// return true if auth passes or is not required, | ||
// return auth page if auth is required | ||
// throw WorkerError if auth failed | ||
export function verifyAuth(request, env) { | ||
// pass auth if 'BASIC_AUTH' is not present | ||
if (!('BASIC_AUTH' in env)) return null | ||
if (!env.BASIC_AUTH) return null | ||
|
||
const passwdMap = new Map(Object.entries(env['BASIC_AUTH'])) | ||
const passwdMap = new Map(Object.entries(env.BASIC_AUTH)) | ||
|
||
// pass auth if 'BASIC_AUTH' is empty | ||
if (passwdMap.size === 0) return null | ||
|
||
if (request.headers.has('Authorization')) { | ||
const { user, pass } = parseBasicAuth(request) | ||
if (passwdMap.get(user) === undefined) { | ||
if (request.headers.has("Authorization")) { | ||
const { username, password } = decodeBasicAuth(request.headers.get("Authorization")) | ||
if (passwdMap.get(username) === undefined) { | ||
throw new WorkerError(401, "user not found for basic auth") | ||
} else if (passwdMap.get(user) !== pass) { | ||
} else if (passwdMap.get(username) !== password) { | ||
throw new WorkerError(401, "incorrect passwd for basic auth") | ||
} else { | ||
return null | ||
} | ||
} else { | ||
return new Response('HTTP basic auth is required', { | ||
return new Response("HTTP basic auth is required", { | ||
status: 401, | ||
headers: { | ||
// Prompts the user for credentials. | ||
'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"', | ||
"WWW-Authenticate": "Basic charset=\"UTF-8\"", | ||
}, | ||
}); | ||
}) | ||
} | ||
} |
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,82 @@ | ||
import { expect, test } from "vitest" | ||
import { areBlobsEqual, BASE_URL, createFormData, randomBlob, staticPages, workerFetchWithAuth } from "./testUtils.js" | ||
import { encodeBasicAuth, decodeBasicAuth } from "../src/auth.js" | ||
|
||
test("basic auth encode and decode", async () => { | ||
const userPasswdPairs = [ | ||
["user1", "passwd1"], | ||
["あおい", "まなか"], | ||
["1234#", "اهلا"], | ||
] | ||
for (const [user, passwd] of userPasswdPairs) { | ||
const encoded = encodeBasicAuth(user, passwd) | ||
const decoded = decodeBasicAuth(encoded) | ||
expect(decoded.username).toStrictEqual(user) | ||
expect(decoded.password).toStrictEqual(passwd) | ||
} | ||
}) | ||
|
||
test("basic auth", async () => { | ||
const usersKv = { | ||
"user1": "passwd1", | ||
"user2": "passwd2", | ||
} | ||
|
||
// access index | ||
for (const page of staticPages) { | ||
expect((await workerFetchWithAuth(usersKv, `${BASE_URL}/${page}`, {})).status).toStrictEqual(401) | ||
} | ||
expect((await workerFetchWithAuth(usersKv, BASE_URL, { | ||
headers: { "Authorization": encodeBasicAuth("user1", usersKv["user1"]) }, | ||
})).status).toStrictEqual(200) | ||
|
||
// upload with no auth | ||
const blob1 = randomBlob(1024) | ||
const uploadResp = await workerFetchWithAuth(usersKv, BASE_URL, { | ||
method: "POST", | ||
body: createFormData({ c: blob1 }), | ||
}) | ||
expect(uploadResp.status).toStrictEqual(401) | ||
|
||
// upload with true auth | ||
const uploadResp1 = await workerFetchWithAuth(usersKv, BASE_URL, { | ||
method: "POST", | ||
body: createFormData({ c: blob1 }), | ||
headers: { "Authorization": encodeBasicAuth("user2", usersKv["user2"]) }, | ||
}) | ||
expect(uploadResp1.status).toStrictEqual(200) | ||
|
||
// upload with wrong auth | ||
const uploadResp2 = await workerFetchWithAuth(usersKv, BASE_URL, { | ||
method: "POST", | ||
body: createFormData({ c: blob1 }), | ||
headers: { "Authorization": encodeBasicAuth("user1", "wrong-password") }, | ||
}) | ||
expect(uploadResp2.status).toStrictEqual(401) | ||
|
||
// revisit without auth | ||
const uploadJson = JSON.parse(await uploadResp1.text()) | ||
const url = uploadJson["url"] | ||
const revisitResp = await workerFetchWithAuth(usersKv, url) | ||
expect(revisitResp.status).toStrictEqual(200) | ||
expect(areBlobsEqual(await revisitResp.blob(), blob1)).toBeTruthy() | ||
|
||
// update with no auth | ||
const blob2 = randomBlob(1024) | ||
const admin = uploadJson["admin"] | ||
const updateResp = await workerFetchWithAuth(usersKv, admin, { | ||
method: "PUT", | ||
body: createFormData({ c: blob2 }), | ||
}) | ||
expect(updateResp.status).toStrictEqual(200) | ||
const revisitUpdatedResp = await workerFetchWithAuth(usersKv, url) | ||
expect(revisitUpdatedResp.status).toStrictEqual(200) | ||
expect(areBlobsEqual(await revisitUpdatedResp.blob(), blob2)).toBeTruthy() | ||
|
||
// delete with no auth | ||
const deleteResp = await workerFetchWithAuth(usersKv, admin, { | ||
method: "DELETE", | ||
}) | ||
expect(deleteResp.status).toStrictEqual(200) | ||
expect((await workerFetchWithAuth(usersKv, url)).status).toStrictEqual(404) | ||
}) |
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
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
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,55 @@ | ||
import { env, ctx } from "cloudflare:test" | ||
|
||
import { expect } from "vitest" | ||
import crypto from "crypto" | ||
import worker from "../src/index.js" | ||
|
||
export const BASE_URL = env["BASE_URL"] | ||
export const RAND_NAME_REGEX = /^[ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678]+$/ | ||
|
||
export const staticPages = ["", "index.html", "index", "tos", "tos.html", "api", "api.html"] | ||
|
||
export async function workerFetch(req, options) { | ||
// we are not using SELF.fetch since it sometimes do not print worker log to console | ||
// return await SELF.fetch(req, options) | ||
return await worker.fetch(new Request(req, options), env, ctx) | ||
} | ||
|
||
export async function workerFetchWithAuth(usersKv, req, options) { | ||
const newEnv = Object.assign({ BASIC_AUTH: usersKv }, env) | ||
return await worker.fetch(new Request(req, options), newEnv, ctx) | ||
} | ||
|
||
export async function upload(kv) { | ||
const uploadResponse = await workerFetch(new Request(BASE_URL, { | ||
method: "POST", | ||
body: createFormData(kv), | ||
})) | ||
expect(uploadResponse.status).toStrictEqual(200) | ||
expect(uploadResponse.headers.get("Content-Type")).toStrictEqual("application/json;charset=UTF-8") | ||
return JSON.parse(await uploadResponse.text()) | ||
} | ||
|
||
export function createFormData(kv) { | ||
const fd = new FormData() | ||
Object.entries(kv).forEach(([k, v]) => { | ||
if ((v === Object(v)) && "filename" in v && "value" in v) { | ||
fd.set(k, new File([v.value], v.filename)) | ||
} else { | ||
fd.set(k, v) | ||
} | ||
}) | ||
return fd | ||
} | ||
|
||
export function randomBlob(len) { | ||
const buf = Buffer.alloc(len) | ||
return new Blob([crypto.randomFillSync(buf, 0, len)]) | ||
} | ||
|
||
export async function areBlobsEqual(blob1, blob2) { | ||
return Buffer.from(await blob1.arrayBuffer()).compare( | ||
Buffer.from(await blob2.arrayBuffer()), | ||
) === 0 | ||
} | ||
|