Created
June 15, 2021 12:28
-
-
Save mooyoul/6bb955ad1b19bf6fd2f69d5299b936c0 to your computer and use it in GitHub Desktop.
Static file service w/ Signed URL & Ranged Requests
This file contains hidden or 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
'use strict'; | |
const crypto = require('crypto'); | |
const express = require('express'); | |
const http = require('http'); | |
const morgan = require('morgan'); | |
const path = require('path'); | |
const { URL } = require('url'); | |
const app = express(); | |
const logger = morgan('combined'); | |
const BASE_URL = 'http://localhost:3000'; | |
const SIGNATURE_SECRET = 'super-secret-password'; | |
const FAKE_VIDEO_MODELS = [ | |
{ id: 1, filename: 'twice-tt.mp4' }, | |
{ id: 2, filename: 'giphy.mp4' }, | |
]; | |
const apiRouter = express.Router(); | |
apiRouter.post('/videos/:id', (req, res) => { | |
const id = Number(req.params.id); | |
const model = FAKE_VIDEO_MODELS.find((model) => model.id === id); | |
if (!model) { | |
return res.status(404).send({ | |
error: { | |
code: 'NOT_FOUND', | |
message: 'The requested video does not exist', | |
}, | |
}); | |
} | |
const ONE_HOUR = 3600 * 1000; | |
const expiresAt = Date.now() + ONE_HOUR; | |
const url = createSignedUrl(`/videos/${model.filename}`, expiresAt); | |
res.status(200).send({ | |
data: { url }, | |
}); | |
}); | |
const videoRouter = express.Router(); | |
const videoStaticRouter = express.static(path.join(__dirname, 'secret-static/videos')); | |
videoRouter.use(authenticate, videoStaticRouter); | |
app.use(logger); | |
app.use('/api', apiRouter); | |
app.use('/videos', videoRouter); | |
const server = http.createServer(app); | |
server.listen(3000, () => { | |
const { address, port } = server.address(); | |
console.log('server listening in %s:%s', address, port); | |
}); | |
function authenticate(req, res, next) { | |
const { token } = req.query; | |
if (!token) { | |
return res.sendStatus(401); | |
} | |
const pathname = req.baseUrl + req.path; | |
if (!verify(pathname, token)) { | |
return res.sendStatus(401); | |
} | |
next(); | |
} | |
function createSignedUrl(pathname, expiresAt) { | |
const pathHash = hashPath(pathname); | |
const token = [pathHash, expiresAt, sign(pathHash, expiresAt)].join('.'); | |
const signedUrl = new URL(BASE_URL); | |
signedUrl.pathname = pathname; | |
signedUrl.searchParams.set('token', token); | |
return signedUrl.toString(); | |
} | |
function hashPath(pathname) { | |
return crypto.createHash('sha256').update(pathname).digest('hex'); | |
} | |
function sign(pathHash, expiresAt) { | |
return crypto.createHmac('sha256', SIGNATURE_SECRET) | |
.update([pathHash, expiresAt].join(':')) | |
.digest('hex'); | |
} | |
function verify(pathname, token) { | |
const components = token.split('.'); | |
if (components.length !== 3) { | |
return false; | |
} | |
const [pathHash, expiresAt, actualSignature] = components; | |
const expectedSignature = sign(hashPath(pathname), expiresAt); | |
if (!crypto.timingSafeEqual(Buffer.from(actualSignature, 'hex'), Buffer.from(expectedSignature, 'hex'))) { | |
return false; | |
} | |
if (Number(expiresAt) < Date.now()) { | |
return false; | |
} | |
return true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment