Skip to content

Commit 398d843

Browse files
committed
init
0 parents  commit 398d843

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+9708
-0
lines changed

.eslintrc.cjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-env node */
2+
require('@rushstack/eslint-patch/modern-module-resolution')
3+
4+
module.exports = {
5+
root: true,
6+
'extends': [
7+
'plugin:vue/vue3-essential',
8+
'eslint:recommended',
9+
'@vue/eslint-config-typescript',
10+
'@vue/eslint-config-prettier/skip-formatting'
11+
],
12+
parserOptions: {
13+
ecmaVersion: 'latest'
14+
}
15+
}

.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
.DS_Store
12+
dist
13+
dist-ssr
14+
coverage
15+
*.local
16+
17+
/cypress/videos/
18+
/cypress/screenshots/
19+
20+
# Editor directories and files
21+
.vscode/*
22+
!.vscode/extensions.json
23+
.idea
24+
*.suo
25+
*.ntvs*
26+
*.njsproj
27+
*.sln
28+
*.sw?
29+
30+
*.tsbuildinfo
31+
32+
# wrangler files
33+
.wrangler
34+
.dev.vars

.prettierrc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "https://json.schemastore.org/prettierrc",
3+
"semi": false,
4+
"tabWidth": 2,
5+
"singleQuote": true,
6+
"printWidth": 100,
7+
"trailingComma": "none"
8+
}

.vscode/extensions.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"recommendations": [
3+
"Vue.volar",
4+
"Vue.vscode-typescript-vue-plugin",
5+
"dbaeumer.vscode-eslint",
6+
"esbenp.prettier-vscode"
7+
]
8+
}

env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />

functions/[filename].ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { GetObjectCommand, CopyObjectCommand, DeleteObjectCommand, GetObjectCommandOutput } from "@aws-sdk/client-s3";
2+
import { Upload } from "@aws-sdk/lib-storage";
3+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
4+
import mime from 'mime/lite';
5+
6+
import Env from './utils/Env';
7+
import { createS3Client, auth } from './utils/utils';
8+
9+
export const onRequestGet: PagesFunction<Env> = async (context) => {
10+
const { params, env } = context;
11+
const { filename } = params;
12+
const { BUCKET } = env;
13+
const s3 = createS3Client(env);
14+
const command = new GetObjectCommand({
15+
Bucket: BUCKET!,
16+
Key: filename as string
17+
});
18+
let response: GetObjectCommandOutput;
19+
try {
20+
response = await s3.send(command);
21+
} catch (e) {
22+
return new Response("Not found", { status: 404 });
23+
}
24+
const headers = new Headers();
25+
for (const [key, value] of Object.entries(response.Metadata)) {
26+
headers.set(key, value);
27+
}
28+
if (response.ContentType !== "application/octet-stream") {
29+
headers.set('content-type', response.ContentType);
30+
} else {
31+
headers.set('content-type', mime.getType(filename as string) || "application/octet-stream");
32+
}
33+
if (response.Metadata['x-store-type'] === "text") {
34+
headers.set('content-type', 'text/plain');
35+
}
36+
headers.set('content-length', response.ContentLength.toString());
37+
headers.set('last-modified', response.LastModified.toUTCString());
38+
39+
headers.set('etag', response.ETag);
40+
41+
if (headers.get("x-store-visibility") !== "public" && !auth(env, context.request)) {
42+
return new Response("Not found", { status: 404 });
43+
}
44+
return new Response(
45+
response.Body.transformToWebStream(),
46+
{
47+
headers,
48+
}
49+
);
50+
};
51+
52+
export const onRequestPut: PagesFunction<Env> = async (context) => {
53+
const { params, env, request } = context;
54+
if (!auth(env, request)) {
55+
return new Response("Unauthorized", { status: 401 });
56+
}
57+
const { filename } = params;
58+
const { BUCKET } = env;
59+
const s3 = createS3Client(env);
60+
const headers = new Headers(request.headers);
61+
const x_store_headers = [];
62+
for (const [key, value] of headers.entries()) {
63+
if (key.startsWith('x-store-')) {
64+
x_store_headers.push([key, value]);
65+
}
66+
}
67+
const parallelUploads3 = new Upload({
68+
client: s3,
69+
params: { Bucket: BUCKET, Key: filename as string, Body: request.body, Metadata: Object.fromEntries(x_store_headers) },
70+
queueSize: 4, // optional concurrency configuration
71+
partSize: 1024 * 1024 * 5, // optional size of each part, in bytes, at least 5MB
72+
leavePartsOnError: false, // optional manually handle dropped parts
73+
});
74+
await parallelUploads3.done();
75+
return new Response("OK", { status: 200 });
76+
}
77+
78+
export const onRequestPatch: PagesFunction<Env> = async (context) => {
79+
const { params, env, request } = context;
80+
if (!auth(env, request)) {
81+
return new Response("Unauthorized", { status: 401 });
82+
}
83+
const { filename } = params;
84+
const { BUCKET } = env;
85+
const s3 = createS3Client(env);
86+
const headers = new Headers(request.headers);
87+
const x_store_headers = [];
88+
for (const [key, value] of headers.entries()) {
89+
if (key.startsWith('x-store-')) {
90+
x_store_headers.push([key, value]);
91+
}
92+
}
93+
const command = new CopyObjectCommand({
94+
Bucket: BUCKET!,
95+
CopySource: `${BUCKET}/${filename}`,
96+
Key: filename as string,
97+
MetadataDirective: "REPLACE",
98+
Metadata: Object.fromEntries(x_store_headers),
99+
});
100+
await s3.send(command);
101+
return new Response("OK", { status: 200 });
102+
};
103+
104+
export const onRequestDelete: PagesFunction<Env> = async (context) => {
105+
const { params, env, request } = context;
106+
if (!auth(env, request)) {
107+
return new Response("Unauthorized", { status: 401 });
108+
}
109+
const { filename } = params;
110+
const { BUCKET } = env;
111+
const s3 = createS3Client(env);
112+
const command = new DeleteObjectCommand({
113+
Bucket: BUCKET!,
114+
Key: filename as string
115+
});
116+
const url = await getSignedUrl(
117+
s3,
118+
command,
119+
{ expiresIn: 3600 }
120+
);
121+
await fetch(url, {
122+
method: 'DELETE',
123+
});
124+
return new Response("OK", { status: 200 });
125+
}
126+
127+
export const onRequest: PagesFunction<Env> = async () => {
128+
return new Response("Method not allowed", { status: 405 });
129+
};

functions/_routes.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version": 1,
3+
"include": [
4+
"/*"
5+
],
6+
"exclude": [
7+
"index.html",
8+
"favicon.ico",
9+
"/utils/*",
10+
"/assets/*"
11+
]
12+
}

functions/api/_middleware.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Env from "../utils/Env";
2+
import { auth } from "../utils/utils";
3+
4+
const errorHandling: PagesFunction<Env> = async (context) => {
5+
try {
6+
return await context.next();
7+
} catch (err) {
8+
return new Response(`${err.message}\n${err.stack}`, { status: 500 });
9+
}
10+
}
11+
12+
const authentication: PagesFunction<Env> = async (context) => {
13+
const { env, request } = context;
14+
if (!auth(env, request)) {
15+
return new Response("Unauthorized", { status: 401 });
16+
}
17+
return await context.next();
18+
}
19+
20+
export const onRequest = [errorHandling, authentication];

functions/api/list.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ListObjectsV2Command } from "@aws-sdk/client-s3";
2+
3+
import Env from "../utils/Env";
4+
import { createS3Client } from "../utils/utils";
5+
6+
7+
export const onRequestGet: PagesFunction<Env> = async (context) => {
8+
const { env, request } = context;
9+
const { BUCKET } = env;
10+
const querys = new URL(request.url).searchParams;
11+
const maxKeys = querys.get("MaxKeys");
12+
const prefix = querys.get("Prefix");
13+
const continuationToken = querys.get("ContinuationToken");
14+
15+
const s3 = createS3Client(env);
16+
const command = new ListObjectsV2Command({
17+
Bucket: BUCKET!,
18+
MaxKeys: maxKeys ? parseInt(maxKeys) : undefined,
19+
Prefix: prefix,
20+
ContinuationToken: continuationToken
21+
});
22+
let response;
23+
try {
24+
response = await s3.send(command);
25+
} catch (e) {
26+
return new Response("Not found", { status: 404 });
27+
}
28+
return new Response(JSON.stringify(response), { status: 200 });
29+
}

functions/tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"module": "esnext",
5+
"moduleResolution": "Bundler",
6+
"lib": ["esnext"],
7+
"types": ["@cloudflare/workers-types/2023-07-01"]
8+
}
9+
}

functions/utils/Env.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
interface Env {
2+
REGION?: string,
3+
ENDPOINT: string,
4+
BUCKET: string,
5+
ACCESS_KEY_ID: string,
6+
SECRET_ACCESS_KEY: string,
7+
PASSWORD?: string,
8+
}
9+
10+
export default Env;

functions/utils/utils.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { S3Client } from "@aws-sdk/client-s3";
2+
import Env from './Env';
3+
import { parse } from "cookie";
4+
5+
function createS3Client(env: Env) {
6+
const { REGION, ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY } = env;
7+
return new S3Client({
8+
region: REGION ?? "auto",
9+
endpoint: ENDPOINT,
10+
credentials: {
11+
accessKeyId: ACCESS_KEY_ID,
12+
secretAccessKey: SECRET_ACCESS_KEY,
13+
},
14+
});
15+
}
16+
17+
const hmacEncode = async (data: string, key: string) => {
18+
const encoder = new TextEncoder();
19+
const encodedKey = encoder.encode(key);
20+
const key_encoded = await crypto.subtle.importKey(
21+
"raw",
22+
encodedKey,
23+
{ name: "HMAC", hash: "SHA-256" },
24+
false,
25+
["sign"]
26+
);
27+
const encodedData = encoder.encode(data);
28+
const signature = await crypto.subtle.sign("HMAC", key_encoded, encodedData);
29+
const base64Mac = btoa(String.fromCharCode(...new Uint8Array(signature)));
30+
return base64Mac;
31+
}
32+
33+
const hmacVerify = async (data: string, key: string, sign: string) => {
34+
const encoder = new TextEncoder();
35+
const encodedKey = encoder.encode(key);
36+
const key_encoded = await crypto.subtle.importKey(
37+
"raw",
38+
encodedKey,
39+
{ name: "HMAC", hash: "SHA-256" },
40+
false,
41+
["verify"]
42+
);
43+
const encodedData = encoder.encode(data);
44+
const signature = new Uint8Array(Array.from(atob(sign), c => c.charCodeAt(0)));
45+
const result = await crypto.subtle.verify("HMAC", key_encoded, signature, encodedData);
46+
return result;
47+
}
48+
49+
const isEqual = (a: string, b: string) => {
50+
if (a.length !== b.length) {
51+
// Minimise the possibility of a timing attack via how long encoding takes on the strings
52+
}
53+
const encoder = new TextEncoder();
54+
const encodedA = encoder.encode(a);
55+
const encodedB = encoder.encode(b);
56+
if (encodedA.byteLength !== encodedB.byteLength) {
57+
// Strings must be the same length in order to compare
58+
// with crypto.subtle.timingSafeEqual
59+
return false;
60+
}
61+
return crypto.subtle.timingSafeEqual(encodedA, encodedB);
62+
}
63+
64+
const auth = (env: Env, request: Request) => {
65+
const { PASSWORD } = env;
66+
// cookie PASSWORD
67+
const cookie = parse(request.headers.get('Cookie') ?? '');
68+
if (isEqual(cookie['PASSWORD'] ?? "", PASSWORD ?? "")) {
69+
return true;
70+
}
71+
// query HMAC
72+
const url = new URL(request.url);
73+
const path = url.pathname + url.search;
74+
const path_without_sign = path.replace(/&sign=[^&]+/, '');
75+
const sign = url.searchParams.get('sign');
76+
if (sign === null) return false;
77+
if (!hmacVerify(path_without_sign, PASSWORD, sign)) {
78+
return false;
79+
}
80+
const expire = url.searchParams.get('expire');
81+
if (expire === null) return false;
82+
if (Date.now() < parseInt(expire)) {
83+
return true;
84+
}
85+
return false;
86+
};
87+
88+
const sign = async (path: string, key: string) => {
89+
return await hmacEncode(path, key);
90+
}
91+
92+
export { createS3Client, auth, sign };

index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>FileWorker</title>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
<script type="module" src="/src/main.ts"></script>
11+
</body>
12+
</html>

0 commit comments

Comments
 (0)