-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5fdb54e
commit 98dd215
Showing
26 changed files
with
5,298 additions
and
4,017 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Oops, something went wrong.