Initial commit
This commit is contained in:
commit
71a0e9729a
31
README.md
Normal file
31
README.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# discord webhook proxy server
|
||||
|
||||
a shitty web server that lets you post messages into it and have them forwarded to a discord webhook. designed to hook up easily to a simple `<form>` on your website that randos can use to cyberbully you.
|
||||
|
||||
## environment variables
|
||||
|
||||
- `WEBHOOK_URL`: required. the discord webhook url you want your messages to be sent to. this can also be passed to the program as a command-line argument instead of an environment variable, if you want.
|
||||
- `PORT`: default `80`. the port to listen on.
|
||||
- `TRUST_X_FORWARDED_HOST`, `TRUST_X_FORWARDED_PROTO`, `TRUST_X_FORWARDED_FOR`, `TRUST_X_REAL_IP`: these correspond to headers commonly set by reverse proxies to convey information about the original request as they proxy it to the app. if you deploy this app behind a reverse proxy, set the variables that correspond to the headers your reverse proxy sets. (any non-empty value will be considered "true.") trusting a header that can be controlled directly by a requester will allow identity spoofing, so don't use these variables to trust headers that aren't explicitly set by your proxy (unless you're into that sort of thing).
|
||||
|
||||
## api
|
||||
|
||||
### `POST /send`
|
||||
|
||||
accepts `Content-Type: application/x-www-form-urlencoded` (the default for HTML forms) or `application/json`. fields:
|
||||
|
||||
- `content`: message content. max length 4096
|
||||
- `title`: optional. message title. max length 256
|
||||
- `color`: optional. sets the color of the embed displayed. a 6-digit case-insensitive hex color prefixed with a leading `#`; no other color formats are accepted.
|
||||
|
||||
by default the server returns HTML responses so it's at lease somewhat friendly for use in a bare HTML `<form>`. if you want to be fancy you can submit the form with JS and set your request's `Accept` header to `application/json` to get slightly more structured responses (204 with no content on success, 400 with an `{"error": "the message"}` object on failure
|
||||
|
||||
## example form
|
||||
|
||||
```html
|
||||
<form action="https://your.thing/send" method="post">
|
||||
<input type="text" name="title" id="title" />
|
||||
<textarea name="content"></textarea>
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
```
|
39
example.html
Normal file
39
example.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>bweep bwoop</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- this will work fine with noscript -->
|
||||
<form action="http://localhost:4567/send" method="post">
|
||||
<p><input type="text" name="title" /></p>
|
||||
<p><textarea name="content"></textarea></p>
|
||||
<p><input type="color" name="color" /></p>
|
||||
<p><button type="submit">Send</button></p>
|
||||
</form>
|
||||
<!-- look at that progressive enhancement -->
|
||||
<script>
|
||||
document.querySelector('form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
fetch(`http://localhost:4567/send`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(Object.fromEntries(new FormData(e.target).entries())),
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
alert(`failed to send: ${error}`);
|
||||
}
|
||||
alert('sent!');
|
||||
e.target.reset();
|
||||
}).catch(error => {
|
||||
alert('either your network died or you have a CORS issue, check the network log');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
112
index.mjs
Normal file
112
index.mjs
Normal file
|
@ -0,0 +1,112 @@
|
|||
import assert from 'node:assert';
|
||||
import {createServer} from 'node:http';
|
||||
import {parse as parseQueryString} from 'node:querystring';
|
||||
|
||||
const webhook = process.argv[2] || process.env.WEBHOOK_URL;
|
||||
assert(webhook, 'give me a webhook url please');
|
||||
|
||||
const port = process.env.PORT || 80;
|
||||
|
||||
createServer(async (request, response) => {
|
||||
const host = (process.env.TRUST_X_FORWARDED_HOST && request.headers['x-forwarded-host']) || `localhost:${port}`;
|
||||
const proto = (process.env.TRUST_X_FORWARDED_PROTO && request.headers['x-forwarded-proto']) || 'http';
|
||||
const url = new URL(`${proto}://${host}${request.url}`);
|
||||
|
||||
/** @type {import('node:http').OutgoingHttpHeaders} */
|
||||
const cors = {
|
||||
'Vary': 'Origin',
|
||||
'Access-Control-Allow-Origin': request.headers['origin'] || '*',
|
||||
}
|
||||
|
||||
try {
|
||||
assert(url.pathname === '/send', 'you seem lost');
|
||||
if (request.method === 'OPTIONS') {
|
||||
response.writeHead(request.headers['Access-Control-Request-Method'] === 'POST' ? 204 : 400, {
|
||||
...cors,
|
||||
'Access-Control-Allow-Methods': 'POST',
|
||||
}).end();
|
||||
}
|
||||
assert(request.method === 'POST', 'we dont do that here');
|
||||
assert(
|
||||
(
|
||||
request.headers['content-type']?.startsWith('application/x-www-form-urlencoded')
|
||||
|| request.headers['content-type']?.startsWith('application/json')
|
||||
),
|
||||
'what the fuck am i looking at',
|
||||
);
|
||||
|
||||
const body = await new Promise((resolve, reject) => {
|
||||
let chunks = [];
|
||||
request.on('data', chunk => chunks.push(chunk));
|
||||
request.on('end', () => {
|
||||
resolve(Buffer.concat(chunks).toString('utf-8'));
|
||||
});
|
||||
request.on('error', () => {
|
||||
request.removeAllListeners('data');
|
||||
request.removeAllListeners('end');
|
||||
reject(new Error('hello????'));
|
||||
});
|
||||
});
|
||||
|
||||
let {title, content, color} =
|
||||
request.headers['content-type'].includes('application/json')
|
||||
? JSON.parse(body)
|
||||
: parseQueryString(body);
|
||||
|
||||
assert(
|
||||
(
|
||||
(typeof content === 'string' && content.length > 0)
|
||||
&& (title == null || typeof title === 'string')
|
||||
&& (color == null || typeof color === 'string')
|
||||
),
|
||||
"jesse what the fuck are you talking about",
|
||||
);
|
||||
assert((title == null || title.length <= 256) && (content.length <= 4096), "quit yapping");
|
||||
|
||||
if (color === '#000000') {
|
||||
color = undefined;
|
||||
} else if (color != null) {
|
||||
assert(color[0] === '#', 'what are you trying to do here');
|
||||
color = parseInt(color.slice(1), 16);
|
||||
assert(!isNaN(color) && color > 0 && color <= 0xFFFFFF, 'ok now youre just making shit up');
|
||||
}
|
||||
|
||||
if (request.headers['accept'].startsWith('application/json')) {
|
||||
response.writeHead(204, {'Content-Type': 'application/json', ...cors}).end();
|
||||
} else {
|
||||
response.writeHead(200, {'Content-Type': 'text/html'});
|
||||
response.end(`<html><body><h1>submitted!</h1><script>document.write('<p><button onclick="window.history.back()">« go back</button></p>')</script></body></html>`);
|
||||
}
|
||||
|
||||
const ip = (
|
||||
(process.env.TRUST_X_FORWARDED_FOR && request.headers['x-forwarded-for']?.split(',')[0].trim())
|
||||
|| (process.env.TRUST_X_REAL_IP && request.headers['x-real-ip'])
|
||||
|| request.socket.remoteAddress
|
||||
|| 'the void'
|
||||
);
|
||||
|
||||
fetch(process.env.WEBHOOK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
embeds: [{
|
||||
title,
|
||||
description: content,
|
||||
color,
|
||||
footer: {text: `${ip} • ${url}`},
|
||||
timestamp: new Date().toISOString(),
|
||||
}],
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (request.headers['accept']?.startsWith('application/json')) {
|
||||
response.writeHead(400, {'Content-Type': 'application/json', ...cors});
|
||||
response.end(JSON.stringify({error: error.message || 'you done fucked up'}));
|
||||
} else {
|
||||
response.writeHead(400, {'Content-Type': 'text/html'});
|
||||
response.end(`<html><body><h1>you done fucked up</h1><pre>${error.message}</pre></body></html>`);
|
||||
}
|
||||
}
|
||||
}).listen(port);
|
Loading…
Reference in a new issue