Skip to content

Commit

Permalink
Merge pull request #5 from kocxyz/mod-support-no-client-only
Browse files Browse the repository at this point in the history
Provide Endpoint to receive Server Mod Information
  • Loading branch information
Ipmake authored Oct 22, 2023
2 parents 41c84ab + 037edfc commit e4c2d6c
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 17 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ FROM node:lts-alpine
WORKDIR /opt/proxy

COPY . .
RUN npm install
RUN apk add python3 && npm install

ENTRYPOINT [ "npm", "start" ]
6 changes: 5 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@
"host": "127.0.0.1",
"port": 6380
},
"postgres": "postgres://viper:@127.0.0.1:5434/viper"
"postgres": "postgres://viper:@127.0.0.1:5434/viper",
"mod": {
"dirPath": "mods",
"configDirPath": "configs"
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"express": "^4.18.2",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^2.0.6",
"jszip": "^3.10.1",
"knockoutcity-mod-loader": "^1.0.0-alpha.15",
"prisma": "^5.4.1",
"redis": "^4.6.7"
},
Expand Down
6 changes: 5 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,9 @@ export default {
port: process.env.REDIS_PORT || config.redis.port,
password: process.env.REDIS_PASSWORD || config.redis.password || undefined,
},
postgres: process.env.DATABASE_URL || config.postgres
postgres: process.env.DATABASE_URL || config.postgres,
mod: {
dirPath: process.env.MOD_DIR_PATH || config.mod.dirPath,
configDirPath: process.env.MOD_CONFIG_DIR_PATH || config.mod.configDirPath,
},
} satisfies config
85 changes: 71 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import { PrismaClient } from '@prisma/client'
import Logger from './logger.js'

import { authError, authResponse, authErrorData } from './interfaces'
import { EvaluationResult, ModEvaluator, ModLoader, OutGenerator } from 'knockoutcity-mod-loader';
import { createZipFromFolder } from './ziputil';

import path from 'node:path';
import os from 'node:os';

const log = new Logger();

if(config.name == "ServerName") log.warn("Please change the name in the config.json or via the environment (SERVER_NAME)");
if (config.name == "ServerName") log.warn("Please change the name in the config.json or via the environment (SERVER_NAME)");

const redis = createClient({
socket: {
Expand Down Expand Up @@ -59,18 +64,70 @@ app.get('/stats/status', async (req, res) => {
});
});

const modLoader = new ModLoader({
modDir: config.mod.dirPath,
});

const createModZip = async (modPath: string): Promise<Buffer> => {
const zip = await createZipFromFolder(modPath);
return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
};

app.get('/mods/list', async (req, res) => {
const mods = modLoader.loadModManifests();

return res.json(
await Promise.all(
mods
.filter((mod) => mod.manifest.type === 'server-client')
.map(async (mod) => ({
name: mod.manifest.name,
version: mod.manifest.version,
}))
)
);
});

app.get('/mods/download', async (req, res) => {
const mods = modLoader.loadMods();
const clientServerMods = mods.filter((mod) => mod.manifest.type === 'server-client');

if (clientServerMods.length === 0) {
return res.status(400).send();
}

const evaluationResults: EvaluationResult[] = [];
for (const mod of clientServerMods) {
const modEvaluator = new ModEvaluator(mod, { modsConfigDir: config.mod.configDirPath });
evaluationResults.push(await modEvaluator.evaulate())
}

const tempDirPath = path.join(os.tmpdir(), 'generated-mod-output');
const outGenerator = new OutGenerator({ baseDir: tempDirPath});
await outGenerator.generate(evaluationResults);

const zipBuffer = await createModZip(tempDirPath);
return (
res
.header('Content-Disposition', `attachment; filename="mods.zip"`)
.contentType('application/zip')
.send(zipBuffer)
);
});


app.use(async (req: express.Request, res: express.Response, next: express.NextFunction) => {
log.info(`Request from ${req.ip} to ${req.url}`);
res.set('X-Powered-By', 'KoCity Proxy');

if(!req.body.credentials) {
if (!req.body.credentials) {
log.info("No credentials");
return next();
}

const authkey = req.body.credentials.username

if(!authkey) {
if (!authkey) {
log.info("Invalid credentials");
return res.status(401).send("Invalid credentials");
}
Expand All @@ -80,27 +137,27 @@ app.use(async (req: express.Request, res: express.Response, next: express.NextFu
server: config.publicAddr
}).catch((err: authError): null => {
res.status(401).send("Unauthorized");
if(err.response) log.err(`${(err.response.data as authErrorData).type} ${(err.response.data as authErrorData).message}`);
if (err.response) log.err(`${(err.response.data as authErrorData).type} ${(err.response.data as authErrorData).message}`);
else log.err(err.message);
return null;
});

if(!response) return log.info("Request denied");
if (!response) return log.info("Request denied");

if(!response.data?.username) {
if (!response.data?.username) {
log.info("Request denied");
return res.status(401).send("Unauthorized");
}

if(!response.data.velanID) {
if (!response.data.velanID) {
let localUser = await prisma.users.findFirst({
where: {
username: response.data.username,
}
});

let velanID: number | undefined;
if(!localUser) {
if (!localUser) {
const createdUser = await axios.post(`http://${config.internal.host}:${config.internal.port}/api/auth`, {
credentials: {
username: response.data.username,
Expand All @@ -124,17 +181,17 @@ app.use(async (req: express.Request, res: express.Response, next: express.NextFu
velanID
}).catch((err: authError): null => {
res.status(401).send("Unauthorized");
if(err.response) log.err(`${(err.response.data as authErrorData).type} ${(err.response.data as authErrorData).message}`);
if (err.response) log.err(`${(err.response.data as authErrorData).type} ${(err.response.data as authErrorData).message}`);
else log.err(err.message);
return null;
});
if(!saved) return log.info("Request denied");
if (!saved) return log.info("Request denied");

response.data.velanID = velanID;
}
if(!response.data.velanID) return log.info("Request denied");
if (!response.data.velanID) return log.info("Request denied");

await prisma.users.update({
await prisma.users.update({
where: {
id: Number(response.data.velanID)
},
Expand All @@ -148,7 +205,7 @@ app.use(async (req: express.Request, res: express.Response, next: express.NextFu
req.body.credentials.username = `${response.data.color ? `:${response.data.color}FF:` : ''}${response.data.username}`
req.headers['content-length'] = Buffer.byteLength(JSON.stringify(req.body)).toString();
next();
})
})

const proxy = createProxyMiddleware({
target: `http://${config.internal.host}:${config.internal.port}`,
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export interface config {
password?: string,
},
postgres: string,
mod: {
/** The path to the mods directory */
dirPath: string,
/** The path to the mods configuration directory */
configDirPath: string,
},
}


Expand Down
63 changes: 63 additions & 0 deletions src/ziputil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Source: https://github.com/Stuk/jszip/issues/386

import fs from 'node:fs';
import fsp from 'node:fs/promises';
import path from 'node:path';
import JSZip from 'jszip';

/**
* Compresses a folder to the specified zip file.
* @param {string} srcDir
* @param {string} destFile
*/
export const compressFolder = async (srcDir: string, destFile: string): Promise<void> => {
//node write stream wants dest dir to already be created
await fsp.mkdir(path.dirname(destFile), { recursive: true });

const zip = await createZipFromFolder(srcDir);

return new Promise((resolve, reject) => {
zip
.generateNodeStream({ streamFiles: true, compression: 'DEFLATE' })
.pipe(fs.createWriteStream(destFile))
.on('error', (err) => reject(err))
.on('finish', resolve);
});
};

/**
* Returns a flat list of all files and subfolders for a directory (recursively).
* @param {string} dir
* @returns {Promise<string[]>}
*/
const getFilePathsRecursively = async (dir: string): Promise<string[]> => {
// returns a flat array of absolute paths of all files recursively contained in the dir
const list = await fsp.readdir(dir);
const statPromises = list.map(async (file) => {
const fullPath = path.resolve(dir, file);
const stat = await fsp.stat(fullPath);
if (stat && stat.isDirectory()) {
return getFilePathsRecursively(fullPath);
}
return fullPath;
});

// cast to string[] is ts hack
// see: https://github.com/microsoft/TypeScript/issues/36554
return (await Promise.all(statPromises)).flat(Number.POSITIVE_INFINITY) as string[];
};

/**
* Creates an in-memory zip stream from a folder in the file system
* @param {string} dir
* @returns {Promise<JSZip>}
*/
export const createZipFromFolder = async (dir: string): Promise<JSZip> => {
const filePaths = await getFilePathsRecursively(dir);
return filePaths.reduce((z, filePath) => {
const relative = path.relative(dir, filePath);
return z.file(relative, fs.createReadStream(filePath), {
unixPermissions: '777', //you probably want less permissive permissions
});
}, new JSZip());
};

0 comments on commit e4c2d6c

Please sign in to comment.