From b415a0c71056cdbc7efa69acdc65fff64353201a Mon Sep 17 00:00:00 2001 From: Wheat Carrier Date: Sat, 6 Jan 2024 00:25:48 +0800 Subject: [PATCH] . --- package.json | 1 + src/config.ts | 61 ++++++++++++++++++++++++++++++- src/errors/authentication.ts | 42 +++++++++++++++++++++ src/errors/base.ts | 10 ++++- src/errors/error-codes.ts | 3 +- src/errors/index.ts | 1 + src/index.ts | 1 + src/server/manager/auth.ts | 48 ++++++++++++++++++++++++ src/server/manager/http-server.ts | 40 +++++++++++++++++++- src/server/webdav/index.ts | 2 +- src/sync/index.ts | 11 ------ src/sync/sync-from-local-only.ts | 1 - src/sync/sync-from-remote-only.ts | 1 - yarn.lock | 30 +++++++++++++++ 14 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 src/errors/authentication.ts create mode 100644 src/server/manager/auth.ts delete mode 100644 src/sync/index.ts delete mode 100644 src/sync/sync-from-local-only.ts delete mode 100644 src/sync/sync-from-remote-only.ts diff --git a/package.json b/package.json index d371f03..27b29aa 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "input": "^1.0.1", "ip": "^1.1.8", "js-yaml": "^4.1.0", + "json-web-token": "^3.2.0", "telegraf": "^4.15.0", "telegram": "^2.17.10", "uuid": "^9.0.0", diff --git a/src/config.ts b/src/config.ts index 97286c8..9dff603 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,48 @@ import yaml from 'js-yaml'; import os from 'os'; import path from 'path'; -export const config: any = {}; +type Config = { + telegram: { + api_id: number; + api_hash: string; + bot_token: string; + private_file_channel: string; + public_file_channel: string; + session_file: string; + }; + tgfs: { + users: { + [key: string]: { + password: string; + }; + }; + download: { + progress: boolean; + chunksize: number; + }; + jwt: { + secret: string; + algorithm: string; + expiration: number; + }; + }; + webdav: { + host: string; + port: number; + path: string; + }; + manager: { + host: string; + port: number; + path: string; + bot: { + token: string; + chat_id: number; + }; + }; +}; + +export const config = {} as Config; export const loadConfig = (configPath: string) => { const file = fs.readFileSync(configPath, 'utf8'); @@ -34,6 +75,11 @@ export const loadConfig = (configPath: string) => { chunksize: cfg['tgfs']['download']['chunk_size_kb'] ?? 1024, progress: cfg['tgfs']['download']['progress'] === 'true', }, + jwt: { + secret: cfg['tgfs']['jwt']['secret'], + algorithm: cfg['tgfs']['jwt']['algorithm'] ?? 'HS256', + expiration: cfg['tgfs']['jwt']['expiration'], + }, }; config.webdav = { @@ -110,6 +156,19 @@ export const createConfig = async () => { progress: 'true', chunk_size_kb: 1024, }, + jwt: { + secret: (() => { + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let secret = ''; + for (let i = 0; i < 64; i++) { + secret += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return secret; + })(), + algorithm: 'HS256', + expiration: 3600 * 24 * 7, + }, }; config.webdav = { diff --git a/src/errors/authentication.ts b/src/errors/authentication.ts new file mode 100644 index 0000000..9aa6f6e --- /dev/null +++ b/src/errors/authentication.ts @@ -0,0 +1,42 @@ +import HTTPErrors from 'http-errors'; + +import { BusinessError } from './base'; + +export class BadAuthentication extends BusinessError { + constructor( + public readonly message: string, + public readonly cause?: string, + ) { + super(message, 'BAD_AUTHENTICATION', cause, HTTPErrors.Unauthorized); + } +} + +export class MissingAuthenticationHeaders extends BadAuthentication { + constructor() { + super('Missing authentication headers', 'Missing authentication headers'); + } +} + +export class InvalidCredentials extends BadAuthentication { + constructor(public readonly cause: string) { + super('Bad authentication', cause); + } +} + +export class UserNotFound extends InvalidCredentials { + constructor(public readonly username: string) { + super(`User ${username} not found`); + } +} + +export class IncorrectPassword extends InvalidCredentials { + constructor(public readonly username: string) { + super(`Password for ${username} does not match`); + } +} + +export class JWTInvalid extends InvalidCredentials { + constructor() { + super('JWT token invalid'); + } +} diff --git a/src/errors/base.ts b/src/errors/base.ts index dff3bdd..f50f063 100644 --- a/src/errors/base.ts +++ b/src/errors/base.ts @@ -1,9 +1,14 @@ +import HTTPErrors from 'http-errors'; + import { ErrorCodes } from './error-codes'; export class TechnicalError extends Error { constructor( public readonly message: string, public readonly cause?: any, + public readonly httpError: { + new (message: string): HTTPErrors.HttpError; + } = HTTPErrors.InternalServerError, ) { super(message); } @@ -14,7 +19,10 @@ export class BusinessError extends TechnicalError { public readonly message: string, public readonly code: ErrorCodes, public readonly cause?: any, + public readonly httpError: { + new (message: string): HTTPErrors.HttpError; + } = HTTPErrors.InternalServerError, ) { - super(message, cause); + super(message, cause, httpError); } } diff --git a/src/errors/error-codes.ts b/src/errors/error-codes.ts index e387d81..6a47198 100644 --- a/src/errors/error-codes.ts +++ b/src/errors/error-codes.ts @@ -6,4 +6,5 @@ export type ErrorCodes = | 'INVALID_NAME' | 'RELATIVE_PATH' | 'UNKNOWN_COMMAND' - | 'DIR_IS_NOT_EMPTY'; + | 'DIR_IS_NOT_EMPTY' + | 'BAD_AUTHENTICATION'; diff --git a/src/errors/index.ts b/src/errors/index.ts index ec0cc6d..005ff9b 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,2 +1,3 @@ export * from './cmd'; export * from './path'; +export * from './authentication'; diff --git a/src/index.ts b/src/index.ts index ad75250..5571671 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,7 @@ const { argv }: any = yargs(hideBin(process.argv)) try { loadConfig(configPath); } catch (err) { + console.log(err); configPath = await createConfig(); loadConfig(configPath); } diff --git a/src/server/manager/auth.ts b/src/server/manager/auth.ts new file mode 100644 index 0000000..5599d28 --- /dev/null +++ b/src/server/manager/auth.ts @@ -0,0 +1,48 @@ +import jwt from 'json-web-token'; + +import { config } from 'src/config'; +import { IncorrectPassword, JWTInvalid, UserNotFound } from 'src/errors'; + +const sha256 = async (s: string) => { + const msgBuffer = new TextEncoder().encode(s); + const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + return hashHex; +}; + +type JWTPayload = { + username: string; + exp: number; +}; + +export const generateToken = async (username: string, password: string) => { + if (config.tgfs.users[username] === undefined) { + throw new UserNotFound(username); + } + if ((await sha256(config.tgfs.users[username].password)) !== password) { + // the password sent in is sha256 hashed + throw new IncorrectPassword(username); + } + return jwt.encode; +}; + +export const verifyToken = (token: string): Promise => { + return new Promise((resolve, reject) => { + const payload = jwt.decode( + config.tgfs.jwt.secret, + token, + (error, payload) => { + if (error) { + reject(new JWTInvalid()); + } + }, + ); + if (payload.exp < Date.now()) { + reject(new JWTInvalid()); + } + resolve(payload as JWTPayload); + }); +}; diff --git a/src/server/manager/http-server.ts b/src/server/manager/http-server.ts index 454fe80..5aa0834 100644 --- a/src/server/manager/http-server.ts +++ b/src/server/manager/http-server.ts @@ -1,18 +1,54 @@ -import express from 'express'; +import express, { Request, Response } from 'express'; +import { BadAuthentication } from 'src/errors'; +import { TechnicalError } from 'src/errors/base'; + +import { generateToken, verifyToken } from './auth'; import { db } from './db'; const app = express(); +app.use(async (req, res, next) => { + if (req.path === '/login') { + next(); + } + + const token = req.headers['authorization']; + if (token === undefined) { + res.redirect('/'); + } + + try { + await verifyToken(token); + } catch (err) { + res.redirect('/'); + } +}); + // set cors headers app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); next(); }); +app.post('/login', (req, res) => { + const token = generateToken(req.body.username, req.body.password); + res.setHeader('Set-Cookie', `token=${token}; HttpOnly`); + res.end(); +}); + app.get('/tasks', (req, res) => { - console.log(db.getTasks()); + console.log(req.headers); res.send(db.getTasks()); }); +app.use((err: Error, req: Request, res: Response, next: () => any) => { + if (err instanceof TechnicalError) { + const error = new err.httpError(err.message); + res.status(error.statusCode).send(error.message); + } else { + res.status(500).send('Internal Server Error'); + } +}); + export const managerServer = app; diff --git a/src/server/webdav/index.ts b/src/server/webdav/index.ts index 702810a..f874761 100644 --- a/src/server/webdav/index.ts +++ b/src/server/webdav/index.ts @@ -27,7 +27,7 @@ export const webdavServer = ( options?: webdav.WebDAVServerOptions, ) => { const userManager = new webdav.SimpleUserManager(); - Object.keys(config.tgfs.users).forEach((user: string) => { + Object.keys(config.tgfs.users).forEach((user) => { userManager.addUser(user, config.tgfs.users[user].password, true); }); diff --git a/src/sync/index.ts b/src/sync/index.ts deleted file mode 100644 index df7b73d..0000000 --- a/src/sync/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import chokidar from 'chokidar'; - -import { config } from 'src/config'; - -export const runSync = () => { - Object.values(config.sync).forEach((sync: { local: string }) => { - chokidar.watch(sync.local).on('all', (event, path) => { - console.log(event, path); - }); - }); -}; diff --git a/src/sync/sync-from-local-only.ts b/src/sync/sync-from-local-only.ts deleted file mode 100644 index 7d6dca0..0000000 --- a/src/sync/sync-from-local-only.ts +++ /dev/null @@ -1 +0,0 @@ -export const syncFromLocalOnly = () => {}; diff --git a/src/sync/sync-from-remote-only.ts b/src/sync/sync-from-remote-only.ts deleted file mode 100644 index 8a3dc0a..0000000 --- a/src/sync/sync-from-remote-only.ts +++ /dev/null @@ -1 +0,0 @@ -export const syncFromRemoteOnly = () => {}; diff --git a/yarn.lock b/yarn.lock index bd8297d..ad8b4fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1134,6 +1134,11 @@ base64-js@^1.3.1: resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64-url@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/base64-url/-/base64-url-2.3.3.tgz#645b71455c75109511f27d98450327e455f488ec" + integrity sha512-dLMhIsK7OplcDauDH/tZLvK7JmUZK3A7KiQpjNzsBrM6Etw7hzNI1tLEywqJk9NnwkgWuFKSlx/IUO7vF6Mo8Q== + big-integer@^1.6.48: version "1.6.51" resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz" @@ -2165,6 +2170,11 @@ is-typedarray@^1.0.0: resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is.object@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is.object/-/is.object-1.0.0.tgz#e4f4117e9f083b35c8df5cf817ea3efb0452fdfa" + integrity sha512-BdDP6tLXkf0nrCnksLobALJxkt2hmrVL6ge1oRuzGU4Lb9NpreEbhhuCcY6HMzx/qo3Dff9DJ3jf0x9+U0bNMQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2606,6 +2616,21 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-parse-safe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/json-parse-safe/-/json-parse-safe-2.0.0.tgz#27b4fddd31114936136b77d9bbe13f7ef5c897de" + integrity sha512-aK7Ccg46n9JkRil/M5s8ytNP9wk0Tc0TsTDVMOYxB2cGsL7XFJ1vfWsurkVZy6zR55KZ+2SWHEuzQeZK4PH6lg== + +json-web-token@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/json-web-token/-/json-web-token-3.2.0.tgz#06cbc8ab039f474f0f3fabc2e073e7fb6903b8d8" + integrity sha512-y+zDkON17Zk7X3Hf0Rv04k8N7cP1GDRonLLL7WkdnlHl1ur2hwta4MYzxG8nWhQWRBNlIp1erszhmo/65RSTIw== + dependencies: + base64-url "^2.3.2" + is.object "^1.0.0" + json-parse-safe "^2.0.0" + xtend "^4.0.2" + json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" @@ -3749,6 +3774,11 @@ xml-js@^1.6.2: dependencies: sax "^1.2.4" +xtend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"