-
Notifications
You must be signed in to change notification settings - Fork 0
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
7aebfb0
commit 8d886a9
Showing
8 changed files
with
177 additions
and
43 deletions.
There are no files selected for viewing
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 |
---|---|---|
@@ -1 +1,2 @@ | ||
node_modules | ||
SECRET |
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 |
---|---|---|
@@ -1,12 +1,13 @@ | ||
FROM alpine:3.9 as build | ||
FROM alpine:3.9 | ||
|
||
RUN apk add --no-cache make nodejs npm | ||
|
||
WORKDIR /root | ||
COPY package*.json ./ | ||
RUN npm set progress=false && npm config set depth 0 | ||
RUN npm install --only=production | ||
|
||
COPY src /root/src | ||
|
||
EXPOSE 3000 | ||
CMD ["npm", "start"] | ||
ENV TERM xterm-256color | ||
CMD ["node", "/root/src/server.js"] |
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
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 |
---|---|---|
@@ -1,19 +1,64 @@ | ||
# Makefile Hook | ||
# make-webhooks | ||
|
||
Webhooks for your project made simple. Most projects have a Makefile, why not | ||
just expose an API to run them? | ||
> Just try it: `make web`. ;) | ||
You are now exposing this projects `Makefile` to the web! | ||
|
||
> $ curl http://localhost/hello-world | ||
> {"status":0} | ||
Change environment variables. All vars are uppercased by default. | ||
Dont forget HTTP URL encoding! | ||
|
||
> curl http://localhost/hello-world\?GREET\=Anthony | ||
> curl "http://localhost/hello-world?GREET=Anthony" | ||
> curl "http://localhost/hello-world?greet=Anthony" | ||
You can use the webhooks return status as an exit code in another script. | ||
|
||
```sh | ||
#!/bin/bash | ||
|
||
if [ "$1" -eq "run-webhook" ]; then | ||
exit $(curl -s http://localhost/crash | jq '.status') | ||
fi | ||
``` | ||
echo 'test:\n\texit 0' > Makefile | ||
docker run -d --rm \ | ||
-v $(PWD):/webhook \ | ||
-p 3000:3000 \ | ||
--name make-webhook \ | ||
expelledboy/make-webhook | ||
curl http://localhost:3000/test | ||
# Cleanup | ||
docker stop make-webhook | ||
|
||
> ./script.sh; echo $? | ||
> 0 | ||
> ./script.sh run-webhook; echo $? | ||
> 2 | ||
Example usage in the command line. | ||
|
||
> `exit $(curl -s http://localhost/crash | jq '.status')`; echo $? | ||
> 2 | ||
But best yet, in a make target! | ||
|
||
```make | ||
run-webhook: | ||
@curl -fs http://localhost/crash | ||
``` | ||
|
||
> $ make run-webhook | ||
> make: *** [Makefile:86: run-webhook] Error 22 | ||
|
||
### Security | ||
|
||
We match the Bearer token in the Authorization header, either with environment | ||
variable `SECRET`, or the contents of the file located at `/webhooks/SECRET`. | ||
|
||
> $ echo mySecret > SECRET | ||
> $ curl http://localhost/hello-world | ||
> Unauthorized | ||
> $ curl -H 'Authorization: Bearer mySecret' http://localhost/hello-world | ||
> {"status":0} | ||
|
||
Can even do key rotations! | ||
|
||
> $ echo -n myNewSecret > SECRET | ||
> $ curl -H 'Authorization: Bearer mySecret' http://localhost/hello-world ; echo '' | ||
> Unauthorized | ||
> $ curl -H 'Authorization: Bearer myNewSecret' http://localhost/hello-world ; echo '' | ||
> {"status":0} |
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,5 @@ | ||
#!/bin/bash | ||
|
||
if [ "$1" == "run-webhook" ]; then | ||
exit $(curl -s http://localhost/crash | jq '.status') | ||
fi |
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,5 @@ | ||
|
||
You are now exposing this projects `Makefile` to the web! | ||
|
||
> $ curl http://localhost/hello-world | ||
> {"status":0} |
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
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 |
---|---|---|
@@ -1,43 +1,83 @@ | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const express = require("express"); | ||
const process = require("process"); | ||
const { exec } = require("child_process"); | ||
|
||
// Constants | ||
const HOST = "0.0.0.0"; | ||
const PORT = parseInt(process.env.PORT || "3000", 10); | ||
const { TOKEN } = process.env; | ||
const BASE_DIR = process.env.BASE_DIR || "/webhook"; | ||
const MAKEFILE = `${BASE_DIR}/Makefile`; | ||
const PORT = 3000; | ||
const MAKE_DIR = process.env.MAKE_DIR || "/webhooks"; | ||
const MAKEFILE = `${MAKE_DIR}/Makefile`; | ||
const SECRET_FILE = `${MAKE_DIR}/SECRET`; | ||
|
||
const app = express(); | ||
|
||
if (TOKEN) | ||
app.use((req, res, next) => { | ||
if (req.headers.authorization !== `Bearer ${TOKEN}`) | ||
return res.status(401).send("Unauthorized"); | ||
return next(); | ||
}); | ||
// Get hot reloaded secret | ||
const getSecret = () => { | ||
if (fs.existsSync(SECRET_FILE)) { | ||
return fs.readFileSync(SECRET_FILE); | ||
} | ||
return process.env.SECRET; | ||
}; | ||
|
||
// Security | ||
app.use((req, res, next) => { | ||
const secret = getSecret(); | ||
if (secret && req.headers.authorization !== `Bearer ${secret}`) { | ||
return res.status(401).send("Unauthorized"); | ||
} | ||
return next(); | ||
}); | ||
|
||
// Sanity check | ||
if (!fs.existsSync(MAKEFILE)) { | ||
console.error(`${MAKEFILE} does not exists`); | ||
process.exit(1); | ||
} | ||
|
||
// Print an optional container introduction on startup | ||
process.nextTick(() => { | ||
const opts = { cwd: MAKE_DIR }; | ||
exec(`make intro`, opts, (error, stdout) => console.log(stdout)); | ||
}); | ||
|
||
// All GET requests | ||
app.get("*", (req, res) => { | ||
// Parse URL | ||
const { name: target } = path.parse(req.path); | ||
const config = { env: req.query, cwd: BASE_DIR }; | ||
const ENV = Object.entries(req.query).reduce((acc, [key, value]) => { | ||
acc[key.toUpperCase()] = value; | ||
return acc; | ||
}, {}); | ||
|
||
exec(`make ${target}`, config, (error, stdout, stderr) => { | ||
// Marshal command results into json | ||
const handleExec = (error, stdout, stderr) => { | ||
if (error) { | ||
console.error(new Date(), { target, env: req.query, status: error.code }); | ||
return res.status(500).send(stderr); | ||
const response = { target, env: ENV, status: error.code }; | ||
console.log(stderr); | ||
return res.status(500).json(response); | ||
} | ||
console.log(stdout); | ||
return res.json({ status: 0 }); | ||
}; | ||
|
||
console.log(new Date(), { status: 0 }); | ||
return res.send("OK"); | ||
}); | ||
// Execute make target | ||
const opts = { env: ENV, cwd: MAKE_DIR }; | ||
const command = `make ${target}`; | ||
const env = Object.entries(ENV) | ||
.map(([key, value]) => `${key}=${value}`) | ||
.join(" "); | ||
console.log("## ==", new Date(), env, command); | ||
exec(command, opts, handleExec); | ||
}); | ||
|
||
// Start HTTP server | ||
app.listen(PORT, HOST); | ||
console.log(`# Running at http://${HOST}:${PORT}`); | ||
|
||
console.log(`==> Running at http://localhost:${PORT}`); | ||
// Terminate with Ctrl-C | ||
process.on("SIGINT", () => { | ||
console.log("# Interrupted"); | ||
process.exit(0); | ||
}); |