~ekez/negativefour

1673225b71e852410d42d574becbe87b60556d54 — Zeke Medley 9 months ago 080181f
Serve API authenticates all requests with internal API token

This means that some random user can't use their user token to
authenticate with the serve api like it was before smh my head.
7 files changed, 199 insertions(+), 129 deletions(-)

A README.md
M docs/index.md
M docs/negativefour.com.md
A docs/serve-api.md
M serve/app.js
A serve/dummy.sh
A serve/helpers/cli.sh
A README.md => README.md +9 -0
@@ 0,0 1,9 @@
# negativefour

negativefour is a project to make it easier to run webpages as tor
hidden services. negativefour works like many other static web hosting
services you are likely familiar with.

The project webpage is located at https://negativefour.com you can
also find documentation about howo negativefour works in the /docs
subdirectory and at https://man.sr.ht/~ekez/negativefour/ .

M docs/index.md => docs/index.md +1 -0
@@ 9,3 9,4 @@ websites on both the clearnet and as a tor hidden service.
- Abuse mitigation [here](docs/abuse-mitigation.md)
- Notes about configuration of our box [here](docs/multiple-sites-one-host.md)
- How the webpage is served [here](docs/negativefour.com.md)
- The serve API [here](docs/serve-api.md)

M docs/negativefour.com.md => docs/negativefour.com.md +0 -83
@@ 86,86 86,3 @@ WantedBy=multi-user.target
	     ProxyPassReverse / http://localhost:2998/
</VirtualHost>
```

### `/deploy`

All requests must have a `token` field which is a signed JSON web
token. The token must be signed by the same key that the frontend
uses.

POST JSON body params:

```
{
	name: <name of the webpage>,
	repo: <link to git repo containing static webpage>,
	token: <json web token containing user info>
}
```

POST JSON response:

```
{
	jobID: <id for querying the status of the deploy job>
}
```

An invalid request will result in a JSON reponse containing an error
field and the HTTP status code will be set appropriately.

```
{
	error: <error message for the invalid request>
}
```

### `/undeploy`

POST JSON body params:

```
{
	name: <name of the webpage>,
	token: <json web token containing user info>
}
```

POST JSON response:

```
{
	jobID: <id for querying the status of the undeploy job>
}
```

An invalid request will result in a JSON reponse containing an error
field and the HTTP status code will be set appropriately.

```
{
	error: <error message for the invalid request>
}
```

### `/status`

GET JSON body params:

```
{
	jobID: <previously provided jobID>,
	token: <json web token containing user info>
}
```

GET JSON response:

```
{
	stdout: <standard out from job so far>,
	stderr: <standard err from job so far>,
	exitCode: <present if job has exited - exit code of deploy script>,
	serverError: <present if there was a server starting the script - contains error message>
}
```

A docs/serve-api.md => docs/serve-api.md +92 -0
@@ 0,0 1,92 @@
# Serve API

All requests begin in the form:

```
{
	token: <JWT token signed with internal api key>
}
```

The token signature is then checked. If the signature validates
alright the token is unpacked and its contents are sent to the API
endpoints.

This is a long winded way of saying that the API paramaters shown
below must match the contents of the signed token you send.

## `/deploy`

POST JSON body params:

```
{
	name: <name of the webpage>,
	repo: <link to git repo containing static webpage>,
}
```

POST JSON response:

```
{
	jobID: <id for querying the status of the deploy job>
}
```

An invalid request will result in a JSON reponse containing an error
field and the HTTP status code will be set appropriately.

```
{
	error: <error message for the invalid request>
}
```

## `/undeploy`

POST JSON body params:

```
{
	name: <name of the webpage>,
}
```

POST JSON response:

```
{
	jobID: <id for querying the status of the undeploy job>
}
```

An invalid request will result in a JSON reponse containing an error
field and the HTTP status code will be set appropriately.

```
{
	error: <error message for the invalid request>
}
```

## `/status`

GET JSON body params:

```
{
	jobID: <previously provided jobID>,
}
```

GET JSON response:

```
{
	stdout: <standard out from job so far>,
	stderr: <standard err from job so far>,
	exitCode: <present if job has exited - exit code of deploy script>,
	serverError: <present if there was a server starting the script - contains error message>
}
```

M serve/app.js => serve/app.js +37 -46
@@ 12,6 12,26 @@ const jobStatusMap = new Map()

app.use(express.json())

// Middleware that checks that incoming requests are correctly signed.
const requireAuth = (req, res, next) => {
    const signedToken = req.body.token
    if (!signedToken) {
	res.status(400).json({
	    error: 'Invalid request - no token.'
	})
    }

    try {
	const decoded = jwt.verify(signedToken, process.env.JWT_INTERNAL_KEY)
	req.jwt = decoded
    } catch (err) {
	return res.status(401).json({
	    error: 'Invalid authentication token.'
	})
    }
    return next()
}

// Sends a message to the zekebots discord channel. Doesn't wait for
// the message to send and logs an error / success message as needed.
const sendDiscordAlert = message => {


@@ 27,28 47,19 @@ const sendDiscordAlert = message => {
// Posting to deploy starts a build and returns a unique ID for that
// build. The status of the build can then be queried by making a get
// request to deploy with that ID as a URL param.
app.post('/deploy', function(req, res) {
    const name = req.body.name
    const repo = req.body.repo
    const token = req.body.token
app.post('/deploy', requireAuth, function(req, res) {
    const name = req.jwt.name
    const repo = req.jwt.repo
    const user = req.jwt.user

    if (!name || !repo || !token) {
    if (!name || !repo || !user) {
	return res.status(400).json({
	    error: 'Invalid request.'
	})
    }

    let user
    try {
	const decoded = jwt.verify(token, process.env.JWT_KEY)
	user = decoded.email
    } catch (err) {
	return res.status(401).json({
	    error: 'Invalid authentication token.'
	    error: 'Invalid request - missing fields.'
	})
    }

    const child = execFile('./deploy.sh', [repo, name])
    // const child = execFile('./dummy.sh', [repo, name])
    const childProcessID = jobStatusMap.size.toString()
    jobStatusMap.set(childProcessID, {
	stdout: '',


@@ 87,23 98,12 @@ app.post('/deploy', function(req, res) {
    })
})

app.get('/status', function(req, res) {
    const token = req.body.token
    const jobID = req.body.jobID
app.get('/status', requireAuth, function(req, res) {
    const jobID = req.jwt.jobID

    if (!token || !jobID) {
    if (!jobID) {
	return res.status(400).json({
	    error: 'Invalid request.'
	})
    }

    let user
    try {
	const decoded = jwt.verify(token, process.env.JWT_KEY)
	user = decoded.email
    } catch (err) {
	return res.status(401).json({
	    error: 'Invalid authentication token.'
	    error: 'Invalid request - missing `jobID`.'
	})
    }



@@ 118,27 118,18 @@ app.get('/status', function(req, res) {
    res.status(200).json(jobInfo)
})

app.post('/delete', function(req, res) {
    const name = req.body.name
    const token = req.body.token
app.post('/delete', requireAuth, function(req, res) {
    const name = req.jwt.name
    const user = req.jwt.user

    if (!name || !token) {
    if (!name || !user) {
	return res.status(400).json({
	    error: 'Invalid request.'
	})
    }

    let user
    try {
	const decoded = jwt.verify(token, process.env.JWT_KEY)
	user = decoded.email
    } catch (err) {
	return res.status(401).json({
	    error: 'Invalid authentication token.'
	    error: 'Invalid request - missing fields.'
	})
    }

    const child = execFile('./undeploy.sh', [name])
    // const child = execFile('./dummy.sh', [repo, name])
    const childProcessID = jobStatusMap.size.toString()
    jobStatusMap.set(childProcessID, {
	stdout: '',

A serve/dummy.sh => serve/dummy.sh +1 -0
@@ 0,0 1,1 @@
echo hello

A serve/helpers/cli.sh => serve/helpers/cli.sh +59 -0
@@ 0,0 1,59 @@
#!/bin/bash

set -e
set -u
set -o pipefail

function deploy() {
    local secret=$1
    local url=$2

    local repo=$3
    local name=$4
    local user=$5

    local token=$(jwt encode --secret $secret "{\"name\":\"$name\",\"repo\":\"$repo\",\"user\":\"$user\"}")
    curl -s -X POST "${url}/deploy" \
	 -H 'Content-Type: application/json' \
	 -d "{\"token\":\"$token\"}" | jq
}

function status() {
    local secret=$1
    local url=$2

    local jobID=$3

    local token=$(jwt encode --secret $secret "{\"jobID\":\"$jobID\"}")
    curl -s -X GET "${url}/status" \
	 -H 'Content-Type: application/json' \
	 -d "{\"token\":\"$token\"}" | jq
}


function delete() {
    local secret=$1
    local url=$2

    local name=$3
    local user=$4

    local token=$(jwt encode --secret $secret "{\"name\":\"$name\",\"user\":\"$user\"}")
    curl -s -X POST "${url}/delete" \
	 -H 'Content-Type: application/json' \
	 -d "{\"token\":\"$token\"}" | jq
}

case $1 in
    deploy)
	deploy $2 $3 $4 $5 $6
	;;
    status)
	status $2 $3 $4
	;;
    delete)
	delete $2 $3 $4 $5
	;;
    *)
	echo "usage: $0 <deploy|status|delete>"
esac