Skip to content

Commit

Permalink
sauce: agent: separate and update
Browse files Browse the repository at this point in the history
  • Loading branch information
makinbacon21 committed Oct 25, 2024
1 parent 5fdb54e commit 98dd215
Show file tree
Hide file tree
Showing 26 changed files with 5,298 additions and 4,017 deletions.
4,012 changes: 0 additions & 4,012 deletions agent/package-lock.json

This file was deleted.

File renamed without changes.
File renamed without changes.
File renamed without changes.
2,457 changes: 2,457 additions & 0 deletions mc-agent/package-lock.json

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions mc-agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "sauce-agent",
"version": "0.1.0",
"author": "Swarthmore College Computer Society",
"license": "MIT",
"scripts": {
"start": "nodemon --exec npx ts-node --files src/index.ts",
"build": "cd src && tsc"
},
"dependencies": {
"argon2": "^0.31.2",
"axios": "^1.6.7",
"dotenv": "^16.4.1",
"express": "^4.18.2",
"passport": "^0.7.0",
"passport-http-bearer": "^1.0.1",
"rate-limiter-flexible": "^4.0.1",
"tslog": "^4.9.2",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.15",
"@types/passport": "^1.0.16",
"@types/passport-http-bearer": "^1.0.41",
"cross-env": "^7.0.3",
"nodemon": "^3.0.3",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
},
"_moduleDirectories": [
"src"
]
}
16 changes: 16 additions & 0 deletions mc-agent/sauce-mc-agent.service.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[Unit]
Description=SAUCE Minecraft Whitelist Management Agent
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/srv/sauce/mc-agent
# We will keep system node on this VM relatively constant
ExecStart=/usr/bin/node /srv/sauce/mc-agent/build/src/index.js
Restart=on-failure
RestartSec=5s


[Install]
WantedBy=multi-user.target
56 changes: 56 additions & 0 deletions mc-agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'dotenv/config';
import 'reflect-metadata';

import argon2 from 'argon2';
import express from 'express';
import passport from 'passport';
import { Strategy as BearerStrategy } from 'passport-http-bearer';

import { minecraftRouter } from './minecraft';
import { denyRateLimited, logger, penalizeLimiter } from './util';

const app = express();

app.use(denyRateLimited);

app.use(express.json());
app.use(express.raw({ type: 'text/plain' }));

passport.use(
'bearer',
new BearerStrategy(async function (token, done) {
if (await argon2.verify(process.env.SECRET_HASH, token)) {
done(null, true);
} else {
done(null, false);
}
}),
);

app.use(passport.initialize());
app.use(
passport.authenticate('bearer', { session: false, failWithError: true }),
(err, req, res, next) => {
logger.warn(`Invalid token provided from ${req.ip}`);
penalizeLimiter(req, res, next);
res.sendStatus(401);
},
);
app.use('/mcWhitelist', minecraftRouter);

app.use((err, req, res, next) => {
logger.error(err);

res.status(500).send(err.toString());
});

const bindAddr = process.env.BIND_ADDR || null;
const port = process.env.PORT || 3001;
if (bindAddr) {
logger.info(`Listening on ${bindAddr}:${port}`);
app.listen(+port, bindAddr);
} else {
logger.warn('Listening on all interfaces. This is a security risk!');
logger.info(`Listening on port ${port}`);
app.listen(+port);
}
File renamed without changes.
File renamed without changes.
65 changes: 65 additions & 0 deletions mc-agent/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { RequestHandler, Response } from 'express';
import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
import { Logger } from 'tslog';

export const catchErrors = (action: RequestHandler): RequestHandler => {
return async (req, res, next): Promise<any> => {
try {
return await action(req, res, next);
} catch (err) {
next(err);
}
};
};

export const logger = new Logger();

const setRateLimitHeaders = (limiterRes: RateLimiterRes | null, res: Response) => {
if (limiterRes) {
res.setHeader(
'X-RateLimit-Limit',
(limiterRes.remainingPoints || 0) + (limiterRes.consumedPoints || 0),
);
res.setHeader('X-RateLimit-Remaining', limiterRes.remainingPoints || 0);
res.setHeader(
'X-RateLimit-Reset',
Math.ceil(new Date(Date.now() + limiterRes.msBeforeNext).getTime() / 1000),
);
}
};

// paranoid, only allows one failed key per IP every five seconds
// this thing should only be getting requests from the SAUCE frontend server, which should have the
// key configured statically - we aren't locking out people for typos
const requestRateLimiter = new RateLimiterMemory({
keyPrefix: 'ratelimit_auth_failures',
points: 1, // 1 requests
duration: 5, // per 5 seconds
});

export const denyRateLimited: RequestHandler = catchErrors(async (req, res, next) => {
try {
const limiterRes = await requestRateLimiter.get(req.ip);
setRateLimitHeaders(limiterRes, res);

if (limiterRes && (limiterRes.remainingPoints || 0) <= 0) {
res.setHeader('Retry-After', Math.ceil(limiterRes.msBeforeNext / 1000));
res.sendStatus(429);
} else {
next();
}
} catch (e) {
if (e instanceof Error) {
throw e;
} else {
setRateLimitHeaders(e, res);
res.setHeader('Retry-After', Math.ceil(e.msBeforeNext / 1000));
res.sendStatus(429);
}
}
});

// penalize for incorrect attempts
export const penalizeLimiter: RequestHandler = catchErrors(async (req, res, next) => {
await requestRateLimiter.penalty(req.ip);
});
File renamed without changes.
4 changes: 4 additions & 0 deletions user-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PORT=8526
BIND_ADDR='localhost'
SECRET_HASH='$argon2id$v=19$m=16,t=2,p=1$ZGVrVGJGZUZLbGNpVGhYUQ$7ZcQcfF7Q71Al4+DyQMkcA'
MINECRAFT_SERVER_EXEC_PATH='/path/to/mc/server/server_exec.sh'
27 changes: 27 additions & 0 deletions user-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build
/dist

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

*.icloud

/fstest
63 changes: 63 additions & 0 deletions user-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SAUCE Agent

This is a small local agent to perform certain tasks on our server that require root permissions. It
communicates with the main process via HTTP requests, which allows the main SAUCE process to be
located in a container or elsewhere.

## Running

This process generally needs to be started with root access, which can make things a little bit
weird. `sudo npm start` won't work for unclear reasons (you'll just end up running without root
permissions). Instead, you'll need to do `npm run build` then `sudo node build/src/index.js`.

## API

The service exposes a simple HTTP API for performing tasks on the host.

### Authentication

The API is authorized by presenting a bearer token which is hashed via Argon2 and compared to the
stored hash from the SECRET_HASH environment variable (which can be set in a `.env` file). All HTTP
requests should present this token by setting the `Authorization` header:

```text
Authorization: Bearer [token]
```

### Methods

For all methods that have `username` and/or `classYear` parameters:

- `username` must be a valid POSIX username; that is, it must match the regex `^[a-z][-a-z0-9]*$`
- `classYear` must match the regex `^(\d\d|faculty|staff)$`

#### POST `/newUser/<classYear>/<username>`

Performs new user creation actions for the given username in class `classYear`.

#### GET `/forwardFile/<classYear>/<username>`

Returns the content (in the response body, in plaintext) of the `.forward` file in the home
directory of the given user.

#### POST `/forwardFile/<classYear>/<username>`

Overwrites the `.forward` file in the home directory of the given user with the contents of the
request body. The body should be sent in plaintext, and the request should have the
`Content-Type: text/plain` header.

#### POST `/mcWhitelist/<mc-uuid>`

Whitelists the Minecraft account indicated by the provided UUID. Specifically, runs the command
`[exec-script] command whitelist add [username]`, where `[exec-script]` is specified by the
`MINECRAFT_SERVER_EXEC_PATH` environment variable (on SCCS's systems, this points to a specific
Minecraft server management script) and `[username]` is the Minecraft username corresponding to the
provided Minecraft UUID.

#### DELETE `/mcWhitelist/<mc-uuid>`

Un-whitelists the Minecraft account indicated by the provided UUID. Specifically, runs the command
`[exec-script] command whitelist remove [username]`, where `[exec-script]` is specified by the
`MINECRAFT_SERVER_EXEC_PATH` environment variable (on SCCS's systems, this points to a specific
Minecraft server management script) and `[username]` is the Minecraft username corresponding to the
provided Minecraft UUID.
File renamed without changes.
Loading

0 comments on commit 98dd215

Please sign in to comment.