Compare commits
10 commits
54957dd711
...
88ba5e26cb
Author | SHA1 | Date | |
---|---|---|---|
88ba5e26cb | |||
f5be5beb42 | |||
451a02d2e8 | |||
7563679a7b | |||
ec6f858cf2 | |||
a01abce47b | |||
eaa2d29aff | |||
d332602725 | |||
379fd163fd | |||
9f8c705224 |
11 changed files with 160 additions and 34 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -173,3 +173,5 @@ dist
|
||||||
|
|
||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
build
|
21
Dockerfile
21
Dockerfile
|
@ -1,38 +1,23 @@
|
||||||
# use the official Bun image
|
|
||||||
# see all versions at https://hub.docker.com/r/oven/bun/tags
|
|
||||||
FROM oven/bun:1 as base
|
FROM oven/bun:1 as base
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
# install dependencies into temp directory
|
|
||||||
# this will cache them and speed up future builds
|
|
||||||
FROM base AS install
|
FROM base AS install
|
||||||
RUN mkdir -p /temp/dev
|
RUN mkdir -p /temp/dev
|
||||||
COPY package.json bun.lockb /temp/dev/
|
COPY package.json bun.lockb /temp/dev/
|
||||||
RUN cd /temp/dev && bun install --frozen-lockfile
|
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||||
|
|
||||||
# install with --production (exclude devDependencies)
|
|
||||||
RUN mkdir -p /temp/prod
|
|
||||||
COPY package.json bun.lockb /temp/prod/
|
|
||||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
|
||||||
|
|
||||||
# copy node_modules from temp directory
|
|
||||||
# then copy all (non-ignored) project files into the image
|
|
||||||
FROM base AS prerelease
|
FROM base AS prerelease
|
||||||
COPY --from=install /temp/dev/node_modules node_modules
|
COPY --from=install /temp/dev/node_modules node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# [optional] tests & build
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
# RUN bun test
|
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
# copy production dependencies and source code into final image
|
|
||||||
FROM base AS release
|
FROM base AS release
|
||||||
COPY --from=install /temp/prod/node_modules node_modules
|
COPY --from=prerelease /usr/src/app/build/garf-api .
|
||||||
COPY --from=prerelease /usr/src/app/index.ts .
|
|
||||||
COPY --from=prerelease /usr/src/app/package.json .
|
|
||||||
|
|
||||||
# run the app
|
|
||||||
USER bun
|
USER bun
|
||||||
EXPOSE 3000/tcp
|
EXPOSE 3000/tcp
|
||||||
ENTRYPOINT [ "bun", "run", "index.ts" ]
|
RUN ls -lah
|
||||||
|
CMD [ "garf-api" ]
|
||||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
24
package.json
24
package.json
|
@ -1,11 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "garf-api",
|
"name": "garf-api",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"scripts": {
|
||||||
"@types/bun": "latest"
|
"build": "bun build . --compile --outfile=build/garf-api"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"@types/bun": "latest",
|
||||||
}
|
"@types/node": "^20.11.29"
|
||||||
}
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
3
src/env.d.ts
vendored
3
src/env.d.ts
vendored
|
@ -2,5 +2,8 @@ declare module "bun" {
|
||||||
interface Env {
|
interface Env {
|
||||||
DISCORD_CLIENT_ID: string;
|
DISCORD_CLIENT_ID: string;
|
||||||
DISCORD_CLIENT_SECRET: string;
|
DISCORD_CLIENT_SECRET: string;
|
||||||
|
DISCORD_CLIENT_TOKEN: string;
|
||||||
|
ENCRYPTION_KEY: string;
|
||||||
|
ENCRYPTION_SALT: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
50
src/index.ts
50
src/index.ts
|
@ -1,11 +1,53 @@
|
||||||
import token from "./routes/token";
|
import token from "./routes/token";
|
||||||
|
import config from "./routes/config";
|
||||||
|
import user from "./routes/user";
|
||||||
|
import negotiate from "./routes/negotiate";
|
||||||
|
|
||||||
Bun.serve({
|
type WebSocketData = {
|
||||||
|
authenticated: boolean;
|
||||||
|
identity: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const terryIdentity = {
|
||||||
|
id: "869016244613951539",
|
||||||
|
username: "terry218742",
|
||||||
|
avatar: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = Bun.serve<WebSocketData>({
|
||||||
port: 3000,
|
port: 3000,
|
||||||
async fetch(req) {
|
async fetch(req, server) {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
if (url.pathname === "/") return new Response("hello yes this is garf");
|
|
||||||
if (url.pathname === "/api/token") return await token(req);
|
console.log(`${new Date().toUTCString()} ${req.method} ${url.pathname}`);
|
||||||
|
|
||||||
|
if (url.pathname === "/") return await config(req);
|
||||||
|
if (url.pathname === "/ws") return await negotiate(req, server);
|
||||||
|
if (url.pathname === "/token") return await token(req);
|
||||||
|
if (url.pathname === "/user") return await user(req);
|
||||||
return new Response("garf dont know what want...");
|
return new Response("garf dont know what want...");
|
||||||
},
|
},
|
||||||
|
websocket: {
|
||||||
|
message(ws, message) {
|
||||||
|
if (!ws.data.authenticated) ws.close();
|
||||||
|
server.publish("global", JSON.stringify({ message, identity: ws.data.identity }));
|
||||||
|
},
|
||||||
|
open(ws) {
|
||||||
|
const message = `${ws.data.identity.username} has joined the channel.`;
|
||||||
|
server.publish("global", JSON.stringify({ message, identity: terryIdentity }));
|
||||||
|
ws.subscribe("global");
|
||||||
|
},
|
||||||
|
close(ws, code, msg) {
|
||||||
|
const message = `${ws.data.identity.username} has left the channel.`;
|
||||||
|
server.publish("global", JSON.stringify({ message, identity: terryIdentity }));
|
||||||
|
ws.unsubscribe("global");
|
||||||
|
},
|
||||||
|
drain(ws) {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("Listening on 0.0.0.0:3000");
|
||||||
|
|
37
src/lib/encryption.ts
Normal file
37
src/lib/encryption.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { createCipheriv, createDecipheriv, scrypt, randomBytes } from "node:crypto";
|
||||||
|
|
||||||
|
const KEY = ((await scryptAsync(process.env.ENCRYPTION_KEY, 32)) as Buffer).toString("hex");
|
||||||
|
|
||||||
|
function scryptAsync(password: string, length: number) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const pwBuf = Buffer.from(password);
|
||||||
|
scrypt(pwBuf, process.env.ENCRYPTION_SALT, length, (err, key) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encrypt(data: string) {
|
||||||
|
const iv = randomBytes(16);
|
||||||
|
const cipher = createCipheriv("aes-256-ccm", KEY, iv);
|
||||||
|
|
||||||
|
let encrypted = cipher.update(data, "utf8", "base64url");
|
||||||
|
encrypted += cipher.final("base64url");
|
||||||
|
|
||||||
|
return { encrypted, iv: iv.toString("hex") };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decrypt(data: string, iv: string) {
|
||||||
|
const ivBuf = Buffer.from(iv, "hex");
|
||||||
|
const decipher = createDecipheriv("aes-256-ccm", KEY, ivBuf);
|
||||||
|
|
||||||
|
let decrypted = decipher.update(data, "base64url", "utf-8");
|
||||||
|
try {
|
||||||
|
decrypted += decipher.final("utf8");
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { decrypted };
|
||||||
|
}
|
8
src/routes/config.ts
Normal file
8
src/routes/config.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default async function (req: Request): Promise<Response> {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
|
||||||
|
}),
|
||||||
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
24
src/routes/negotiate.ts
Normal file
24
src/routes/negotiate.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import type { Server } from "bun";
|
||||||
|
import { decrypt } from "../lib/encryption";
|
||||||
|
import type { PartialDiscordUser } from "./token";
|
||||||
|
|
||||||
|
interface NegotiateRequest {
|
||||||
|
id: string;
|
||||||
|
iv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function (req: Request, server: Server): Promise<void | Response> {
|
||||||
|
if (req.method !== "GET") return new Response("garf expected a GET request...");
|
||||||
|
const url = new URL(req.url);
|
||||||
|
|
||||||
|
const id = url.searchParams.get("id");
|
||||||
|
const iv = url.searchParams.get("iv");
|
||||||
|
if (!id || !iv) return new Response("broken id and iv!!!");
|
||||||
|
|
||||||
|
const decrypted = await decrypt(id, iv);
|
||||||
|
if (!decrypted) return new Response("invalid identity!!!!!");
|
||||||
|
const identity = JSON.parse(decrypted.decrypted) as PartialDiscordUser;
|
||||||
|
|
||||||
|
const success = server.upgrade(req, { data: { authenticated: true, identity } });
|
||||||
|
return success ? undefined : new Response("WebSocket upgrade error", { status: 400 });
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { encrypt } from "../lib/encryption";
|
||||||
|
|
||||||
interface TokenRequest {
|
interface TokenRequest {
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
@ -10,8 +12,14 @@ interface TokenResponse {
|
||||||
scope: string;
|
scope: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PartialDiscordUser {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function (req: Request): Promise<Response> {
|
export default async function (req: Request): Promise<Response> {
|
||||||
if (req.method !== "post") return new Response("garf expected a POST request...");
|
if (req.method !== "POST") return new Response("garf expected a POST request...");
|
||||||
if (!req.headers.get("Content-Type")) return new Response("garf expected some jay sawn...");
|
if (!req.headers.get("Content-Type")) return new Response("garf expected some jay sawn...");
|
||||||
if (!req.headers.get("Content-Type")?.includes("application/json")) return new Response("garf expected some jay sawn...");
|
if (!req.headers.get("Content-Type")?.includes("application/json")) return new Response("garf expected some jay sawn...");
|
||||||
|
|
||||||
|
@ -31,5 +39,15 @@ export default async function (req: Request): Promise<Response> {
|
||||||
|
|
||||||
const { access_token } = (await response.json()) as TokenResponse;
|
const { access_token } = (await response.json()) as TokenResponse;
|
||||||
|
|
||||||
return new Response(access_token);
|
const userResponse = await fetch("https://discord.com/api/v10/users/@me", {
|
||||||
|
headers: { Authorization: "Bearer " + access_token },
|
||||||
|
});
|
||||||
|
const user = (await userResponse.json()) as PartialDiscordUser;
|
||||||
|
const { encrypted, iv } = await encrypt(JSON.stringify(user));
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ access_token, identity: { id: encrypted, iv } }), {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
3
src/routes/user.ts
Normal file
3
src/routes/user.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default async function (req: Request): Promise<Response> {
|
||||||
|
return new Response();
|
||||||
|
}
|
Loading…
Reference in a new issue