From 50d48ca7cdd12dc529e72b5d3f93bc5a61ba928f Mon Sep 17 00:00:00 2001 From: AnidlSupport Date: Tue, 28 Feb 2023 17:23:42 +0100 Subject: [PATCH] Setup + Passwordless --- @types/ws.d.ts | 6 +- Dockerfile | 6 +- config/gui.yml | 3 +- config/setup.json | 3 + gui/react/src/provider/MessageChannel.tsx | 127 ++++++++++++++++++++-- gui/server/index.ts | 3 +- gui/server/serviceHandler.ts | 8 ++ gui/server/websocket.ts | 49 ++++++++- modules/module.cfg-loader.ts | 52 +++++++-- tsc.ts | 1 + 10 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 config/setup.json diff --git a/@types/ws.d.ts b/@types/ws.d.ts index 193145a..f8e66b0 100644 --- a/@types/ws.d.ts +++ b/@types/ws.d.ts @@ -1,3 +1,4 @@ +import { GUIConfig } from "../modules/module.cfg-loader" import { AuthResponse, CheckTokenResponse, EpisodeListResponse, FolderTypes, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from "./messageHandler" export type WSMessage = { @@ -32,5 +33,8 @@ export type MessageTypes = { 'type': [undefined, 'funi'|'crunchy'|undefined], 'setup': ['funi'|'crunchy'|undefined, undefined], 'openFile': [[FolderTypes, string], undefined], - 'openURL': [string, undefined] + 'openURL': [string, undefined], + 'setuped': [undefined, boolean], + 'setupServer': [GUIConfig, boolean], + 'requirePassword': [undefined, boolean] } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 63bbb23..9c83c85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,13 +15,13 @@ RUN echo 'ffmpeg: "./bin/ffmpeg/ffmpeg"\nmkvmerge: "./bin/mkvtoolnix/mkvmerge"' RUN npm install -g pnpm RUN pnpm i -RUN pnpm run build-ubuntu-cli +RUN pnpm run build-ubuntu-gui # Move build to new Clean Image FROM node WORKDIR "/app" -COPY --from=builder /app/lib/_builds/multi-downloader-nx-ubuntu64-cli ./ +COPY --from=builder /app/lib/_builds/multi-downloader-nx-ubuntu64-gui ./ # Install mkvmerge and ffmpeg @@ -35,4 +35,4 @@ RUN apt-get install ffmpeg -y RUN mv /usr/bin/mkvmerge /app/bin/mkvtoolnix/mkvmerge RUN mv /usr/bin/ffmpeg /app/bin/ffmpeg/ffmpeg -CMD [ "/bin/bash" ] \ No newline at end of file +CMD [ "node", "/app/gui.js" ] \ No newline at end of file diff --git a/config/gui.yml b/config/gui.yml index cd67df3..b11e0a7 100644 --- a/config/gui.yml +++ b/config/gui.yml @@ -1,2 +1 @@ -port: 3000 -password: "default" \ No newline at end of file +port: 3000 \ No newline at end of file diff --git a/config/setup.json b/config/setup.json new file mode 100644 index 0000000..4ffe8a0 --- /dev/null +++ b/config/setup.json @@ -0,0 +1,3 @@ +{ + "setuped": true +} \ No newline at end of file diff --git a/gui/react/src/provider/MessageChannel.tsx b/gui/react/src/provider/MessageChannel.tsx index 40be4eb..6dc099f 100644 --- a/gui/react/src/provider/MessageChannel.tsx +++ b/gui/react/src/provider/MessageChannel.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import type { AuthData, AuthResponse, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler'; +import type { MessageHandler } from '../../../../@types/messageHandler'; import useStore from '../hooks/useStore'; import type { MessageTypes, WSMessage, WSMessageWithID } from '../../../../@types/ws'; import type { Handler, RandomEvent, RandomEvents } from '../../../../@types/randomEvents'; -import { Backdrop, Typography } from '@mui/material'; +import { Avatar, Box, Button, TextField, Typography } from '@mui/material'; import { v4 } from "uuid"; - +import { useSnackbar } from "notistack"; +import { LockOutlined, PowerSettingsNew } from '@mui/icons-material' +import { GUIConfig } from '../../../../modules/module.cfg-loader'; export type FrontEndMessanges = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise }); @@ -58,22 +60,80 @@ async function messageAndResponse(socket: WebSocke const MessageChannelProvider: FCWithChildren = ({ children }) => { const [store, dispatch] = useStore(); - const [socket, setSocket] = React.useState(); + const [socket, setSocket] = React.useState(); + const [publicWS, setPublicWS] = React.useState(); + const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting'); + const [isSetuped, setIsSetuped] = React.useState<'waiting'|'yes'|'no'>('waiting'); + + const { enqueueSnackbar } = useSnackbar(); React.useEffect(() => { - const wws = new WebSocket(`ws://localhost:3000/ws?${new URLSearchParams({ - password: prompt('This website requires a password') ?? '' - })}`, ); + const wss = new WebSocket(`ws://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`); + wss.addEventListener('open', () => { + setPublicWS(wss); + }); + wss.addEventListener('error', () => { + enqueueSnackbar('Unable to connect to server. Please reload the page to try again.', { variant: 'error' }); + }); + }, []); + + React.useEffect(() => { + (async () => { + if (!publicWS) + return; + setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no'); + setIsSetuped((await messageAndResponse(publicWS, { name: 'setuped', data: undefined })).data ? 'yes' : 'no'); + })(); + }, [publicWS]); + + const connect = (ev?: React.FormEvent) => { + let search = new URLSearchParams(); + if (ev) { + ev.preventDefault(); + const formData = new FormData(ev.currentTarget); + const password = formData.get('password')?.toString(); + if (!password) + return enqueueSnackbar('Please provide both a username and password', { + variant: 'error' + }); + search = new URLSearchParams({ + password + }); + } + + const wws = new WebSocket(`ws://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/ws?${search}`, ); wws.addEventListener('open', () => { console.log('[INFO] [WS] Connected'); setSocket(wws); }); wws.addEventListener('error', (er) => { console.error(`[ERROR] [WS]`, er); - setSocket(null); + enqueueSnackbar('Unable to connect to server. Please check the password and try again.', { + variant: 'error' + }); }) - }, []); - + }; + + const setup = async (ev: React.FormEvent) => { + ev.preventDefault(); + if (!socket) + return enqueueSnackbar('Invalid state: socket not found', { variant: 'error' }); + const formData = new FormData(ev.currentTarget); + const password = formData.get('password'); + const data = { + port: parseInt(formData.get('port')?.toString() ?? '') ?? 3000, + password: password ? password.toString() : undefined + } as GUIConfig; + await messageAndResponse(socket!, { name: 'setupServer', data }); + enqueueSnackbar(`The following settings have been set: Port=${data.port}, Password=${data.password ?? 'noPasswordRequired'}`, { + variant: 'success', + persist: true + }); + enqueueSnackbar('Please restart the server now.', { + variant: 'info', + persist: true + }); + } const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []); @@ -103,8 +163,51 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => { }; }, [ socket ]); - if (socket === undefined || socket === null) - return {socket === undefined ? 'Loading...' : 'WebSocket Error. Please try to reload and make sure the password ist correct.'}; + if (usePassword === 'waiting') + return <>; + + if (socket === undefined) { + if (usePassword === 'no') { + connect(undefined) + return <>; + } + return + + + + + Login + + + + + + You need to login in order to use this tool. + + + ; + } + + if (isSetuped === 'no') { + return + + + + + Confirm + + + + + + + Please enter data that will be set to use this tool. +
+ Leave blank to use no password (NOT RECOMMENDED)! +
+
+
; + } const messageHandler: FrontEndMessanges = { auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data, diff --git a/gui/server/index.ts b/gui/server/index.ts index a286a95..7286322 100644 --- a/gui/server/index.ts +++ b/gui/server/index.ts @@ -4,6 +4,7 @@ import cors from 'cors'; import ServiceHandler from './serviceHandler'; import open from 'open'; import path from 'path'; +import { PublicWebSocket } from './websocket'; process.title = 'AniDL'; @@ -23,7 +24,7 @@ const server = app.listen(cfg.gui.port, () => { console.log(`[INFO] GUI server started on port ${cfg.gui.port}`); }); - +new PublicWebSocket(server); new ServiceHandler(server); open(`http://localhost:${cfg.gui.port}`); \ No newline at end of file diff --git a/gui/server/serviceHandler.ts b/gui/server/serviceHandler.ts index dad38b8..adc5aa5 100644 --- a/gui/server/serviceHandler.ts +++ b/gui/server/serviceHandler.ts @@ -3,6 +3,7 @@ import { Server } from 'http'; import { IncomingMessage } from 'http'; import { MessageHandler } from '../../@types/messageHandler'; import Funi from '../../funi'; +import { setSetuped, writeYamlCfgFile } from '../../modules/module.cfg-loader'; import CrunchyHandler from './services/crunchyroll'; import FunimationHandler from './services/funimation'; import WebSocketHandler from './websocket'; @@ -18,6 +19,13 @@ export default class ServiceHandler { } private handleMessanges() { + this.ws.events.on('setupServer', ({ data }, respond) => { + writeYamlCfgFile('gui', data); + setSetuped(true); + respond(true); + process.exit(0); + }); + this.ws.events.on('setup', ({ data }) => { if (data === 'funi') { this.service = new FunimationHandler(this.ws); diff --git a/gui/server/websocket.ts b/gui/server/websocket.ts index efa6dc2..3568360 100644 --- a/gui/server/websocket.ts +++ b/gui/server/websocket.ts @@ -4,6 +4,7 @@ import { RandomEvent, RandomEvents } from "../../@types/randomEvents"; import { MessageTypes, UnknownWSMessage, WSMessage } from "../../@types/ws"; import { EventEmitter } from "events"; import { cfg } from "."; +import { isSetuped } from "../../modules/module.cfg-loader"; declare interface ExternalEvent { on(event: T, listener: (msg: WSMessage, respond: (data: MessageTypes[T][1]) => void) => void): this; @@ -44,9 +45,12 @@ export default class WebSocketHandler { }); server.on('upgrade', (request, socket, head) => { + if (!this.wsServer.shouldHandle(request)) + return; if (!this.authenticate(request)) { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); + console.log(`[INFO] [WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`) return; } this.wsServer.handleUpgrade(request, socket, head, socket => { @@ -67,8 +71,51 @@ export default class WebSocketHandler { } private authenticate(request: IncomingMessage): boolean { - return cfg.gui.password === new URL(`http://${request.headers.host}${request.url}`).searchParams.get('password'); + const search = new URL(`http://${request.headers.host}${request.url}`).searchParams; + return cfg.gui.password === (search.get('password') ?? undefined); } } +export class PublicWebSocket { + private wsServer: ws.Server; + + constructor(server: Server) { + this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' }); + + this.wsServer.on('connection', (socket, req) => { + console.log(`[INFO] [WS] Connection to public ws from '${req.socket.remoteAddress}'`); + socket.on('error', (er) => console.log(`[ERROR] [WS] ${er}`)); + socket.on('message', (msg) => { + const data = JSON.parse(msg.toString()) as UnknownWSMessage + switch (data.name) { + case 'setuped': + this.send(socket, data.id, data.name, isSetuped()); + break; + case 'requirePassword': + this.send(socket, data.id, data.name, cfg.gui.password !== undefined); + break; + } + }); + }); + + server.on('upgrade', (request, socket, head) => { + if (!this.wsServer.shouldHandle(request)) + return; + this.wsServer.handleUpgrade(request, socket, head, socket => { + this.wsServer.emit('connection', socket, request); + }); + }); + } + + private send(client: ws.WebSocket, id: string, name: string, data: any) { + client.send(JSON.stringify({ + data, + id, + name + }), (er) => { + if (er) + console.log(`[ERROR] [WS] ${er}`) + }); + } +} \ No newline at end of file diff --git a/modules/module.cfg-loader.ts b/modules/module.cfg-loader.ts index ebe0414..7f2291e 100644 --- a/modules/module.cfg-loader.ts +++ b/modules/module.cfg-loader.ts @@ -15,6 +15,7 @@ const dirCfgFile = path.join(workingDir, 'config', 'dir-path'); const guiCfgFile = path.join(workingDir, 'config', 'gui') const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults'); const sessCfgFile = path.join(workingDir, 'config', 'session'); +const setupFile = path.join(workingDir, 'config', 'setup'); const tokenFile = { funi: path.join(workingDir, 'config', 'funi_token'), cr: path.join(workingDir, 'config', 'cr_token') @@ -47,6 +48,22 @@ const loadYamlCfgFile = >(file: string, isSess?: b return {} as T; }; +export type WriteObjects = { + gui: GUIConfig +} + +const writeYamlCfgFile = (file: T, data: WriteObjects[T]) => { + const fn = path.join(workingDir, 'config', `${file}.yml`); + if (fs.existsSync(fn)) + fs.removeSync(fn); + fs.writeFileSync(fn, yaml.stringify(data)); +} + +export type GUIConfig = { + port: number, + password?: string +}; + export type ConfigObject = { dir: { content: string, @@ -61,10 +78,7 @@ export type ConfigObject = { cli: { [key: string]: any }, - gui: { - port: number, - password: string - } + gui: GUIConfig } const loadCfg = () : ConfigObject => { @@ -80,10 +94,7 @@ const loadCfg = () : ConfigObject => { cli: loadYamlCfgFile<{ [key: string]: any }>(cliCfgFile), - gui: loadYamlCfgFile<{ - port: number, - password: string - }>(guiCfgFile) + gui: loadYamlCfgFile(guiCfgFile) }; const defaultDirs = { fonts: '${wdir}/fonts/', @@ -221,6 +232,28 @@ const saveFuniToken = (data: { const cfgDir = path.join(workingDir, 'config'); +const isSetuped = (): boolean => { + const fn = `${setupFile}.json`; + if (!fs.existsSync(fn)) + return false; + return JSON.parse(fs.readFileSync(fn).toString()).setuped; +} + +const setSetuped = (bool: boolean) => { + const fn = `${setupFile}.json`; + if (bool) { + fs.writeFileSync(fn, JSON.stringify({ + setuped: true + }, null, 2)) + } else { + if (fs.existsSync(fn)) { + fs.removeSync(fn); + } + + } +} + + export { loadBinCfg, loadCfg, @@ -230,6 +263,9 @@ export { saveCRToken, loadCRToken, loadCRSession, + isSetuped, + setSetuped, + writeYamlCfgFile, sessCfgFile, cfgDir }; \ No newline at end of file diff --git a/tsc.ts b/tsc.ts index 4f1b827..1266ad7 100644 --- a/tsc.ts +++ b/tsc.ts @@ -13,6 +13,7 @@ const isGUI = !(argv.length > 1 && argv[1] === 'false'); if (!isTest) buildIgnore = [ '*/\\.env', + './config/setup.json' ]; if (!isGUI)