Setup + Passwordless
This commit is contained in:
parent
d872bc1fdb
commit
50d48ca7cd
10 changed files with 230 additions and 28 deletions
6
@types/ws.d.ts
vendored
6
@types/ws.d.ts
vendored
|
|
@ -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<T extends keyof MessageTypes, P extends 0|1 = 0> = {
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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" ]
|
||||
CMD [ "node", "/app/gui.js" ]
|
||||
|
|
@ -1,2 +1 @@
|
|||
port: 3000
|
||||
password: "default"
|
||||
port: 3000
|
||||
3
config/setup.json
Normal file
3
config/setup.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"setuped": true
|
||||
}
|
||||
|
|
@ -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<boolean> });
|
||||
|
||||
|
|
@ -58,22 +60,80 @@ async function messageAndResponse<T extends keyof MessageTypes>(socket: WebSocke
|
|||
const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
||||
|
||||
const [store, dispatch] = useStore();
|
||||
const [socket, setSocket] = React.useState<undefined|WebSocket|null>();
|
||||
const [socket, setSocket] = React.useState<undefined|WebSocket>();
|
||||
const [publicWS, setPublicWS] = React.useState<undefined|WebSocket>();
|
||||
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<HTMLFormElement>) => {
|
||||
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<HTMLFormElement>) => {
|
||||
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 <Typography color='primary'>{socket === undefined ? 'Loading...' : 'WebSocket Error. Please try to reload and make sure the password ist correct.'}</Typography>;
|
||||
if (usePassword === 'waiting')
|
||||
return <></>;
|
||||
|
||||
if (socket === undefined) {
|
||||
if (usePassword === 'no') {
|
||||
connect(undefined)
|
||||
return <></>;
|
||||
}
|
||||
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
|
||||
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
||||
<LockOutlined />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5" color="text.primary">
|
||||
Login
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={connect} sx={{ mt: 1 }}>
|
||||
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" required label={'Password'} />
|
||||
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Login</Button>
|
||||
<Typography color="text.secondary" align='center' component="p" variant='body2'>
|
||||
You need to login in order to use this tool.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
if (isSetuped === 'no') {
|
||||
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
|
||||
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
||||
<PowerSettingsNew />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5" color="text.primary">
|
||||
Confirm
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={setup} sx={{ mt: 1 }}>
|
||||
<TextField name="port" margin='normal' type="number" fullWidth variant="filled" required label={'Port'} defaultValue={3000} />
|
||||
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" label={'Password'} />
|
||||
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Confirm</Button>
|
||||
<Typography color="text.secondary" align='center' component="p" variant='body2'>
|
||||
Please enter data that will be set to use this tool.
|
||||
<br />
|
||||
Leave blank to use no password (NOT RECOMMENDED)!
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
const messageHandler: FrontEndMessanges = {
|
||||
auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data,
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<T extends keyof MessageTypes>(event: T, listener: (msg: WSMessage<T>, 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}`)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = <T extends Record<string, any>>(file: string, isSess?: b
|
|||
return {} as T;
|
||||
};
|
||||
|
||||
export type WriteObjects = {
|
||||
gui: GUIConfig
|
||||
}
|
||||
|
||||
const writeYamlCfgFile = <T extends keyof WriteObjects>(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<GUIConfig>(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
|
||||
};
|
||||
1
tsc.ts
1
tsc.ts
|
|
@ -13,6 +13,7 @@ const isGUI = !(argv.length > 1 && argv[1] === 'false');
|
|||
if (!isTest)
|
||||
buildIgnore = [
|
||||
'*/\\.env',
|
||||
'./config/setup.json'
|
||||
];
|
||||
|
||||
if (!isGUI)
|
||||
|
|
|
|||
Loading…
Reference in a new issue