-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
more WIP: modular server: implements storage
- Loading branch information
1 parent
0b39569
commit bc0ab92
Showing
12 changed files
with
573 additions
and
83 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
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
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,167 @@ | ||
/* eslint-env node */ | ||
/* eslint-disable camelcase */ | ||
const express = require('express'); | ||
const router = express.Router(); | ||
const cors = require('cors'); | ||
const ourCors = cors({ exposedHeaders: 'ETag', maxAge: 7200 }); | ||
const isSecureRequest = require('../util/isSecureRequest'); | ||
const { logRequest } = require('../logger'); | ||
const { getHost } = require('../util/getHost'); | ||
const core = require('../stores/core'); | ||
const { pipeline } = require('node:stream/promises'); | ||
|
||
// const accessStrings = { r: 'Read', rw: 'Read/write' }; | ||
|
||
router.options('/:username/*', | ||
ourCors | ||
); | ||
|
||
/** Express uses GET to generate head */ | ||
router.get('/:username/*', | ||
ourCors, | ||
cacheControl, | ||
validUserParam, | ||
validPathParam, | ||
checkToken.bind(null, 'r'), | ||
async function (req, res, next) { | ||
try { | ||
let condition; | ||
if (req.get('If-Match')) { | ||
condition = { name: 'If-Match', ETag: req.get('If-Match') }; | ||
} else if (req.get('If-None-Match')) { | ||
condition = { name: 'If-None-Match', ETag: req.get('If-None-Match') }; | ||
} | ||
const { readStream, contentLength, contentType, ETag } = await req.app.get('streaming store').get(req.params.username, req.blobPath, condition); | ||
if (!ETag) { | ||
return next(Object.assign(new Error(`No file exists at path “${req.blobPath}”`), { status: 404 })); | ||
} | ||
res.set('Content-Length', contentLength).set('Content-Type', contentType).set('ETag', ETag); | ||
if (readStream) { | ||
res.status(200); | ||
return pipeline(readStream, res); | ||
} else { | ||
res.status(304).end(); | ||
} | ||
} catch (err) { // Express v5 will propagate rejected promises automatically. | ||
next(err); | ||
} | ||
} | ||
); | ||
|
||
router.put('/:username/*', | ||
ourCors, | ||
noRanges, | ||
validUserParam, | ||
validPathParam, | ||
checkToken.bind(null, 'w'), | ||
async function (req, res, next) { | ||
const type = req.get('Content-Type') || 'application/binary'; | ||
let condition; | ||
if (req.get('If-Match')) { | ||
condition = { name: 'If-Match', ETag: req.get('If-Match') }; | ||
} else if (req.get('If-None-Match')) { | ||
condition = { name: 'If-None-Match', ETag: req.get('If-None-Match') }; | ||
} | ||
const result = await req.app.get('streaming store').put(req.params.username, req.blobPath, type, req, condition); | ||
switch (result) { | ||
case 'UPDATED': | ||
res.status(204).end(); | ||
break; | ||
case 'CREATED': | ||
res.status(201).end(); | ||
break; | ||
case 'CONFLICT': | ||
res.status(412).end(); | ||
break; | ||
default: | ||
next(new Error('result of store is unknown')); | ||
} | ||
} | ||
); | ||
|
||
function cacheControl (req, res, next) { | ||
res.set('Cache-Control', 'no-cache'); | ||
next(); | ||
} | ||
|
||
function validUserParam (req, res, next) { | ||
if (core.isValidUsername(req.params.username)) { return next(); } | ||
|
||
res.status(400).type('text/plain').end(); | ||
|
||
logRequest(req, req.params.username, 400, 0, 'invalid user'); | ||
} | ||
|
||
function validPathParam (req, res, next) { | ||
req.blobPath = req.url.slice(1 + req.params.username.length); | ||
if (core.VALID_PATH.test(req.blobPath)) { | ||
return next(); | ||
} | ||
|
||
res.status(400).type('text/plain').end(); | ||
|
||
logRequest(req, req.params.username, 400, 0, 'invalid user'); | ||
} | ||
|
||
async function checkToken (permission, req, res, next) { | ||
req.token = decodeURIComponent(req.get('Authorization')).split(/\s+/)[1]; | ||
// req.token = req.get('Authorization') | ||
// ? decodeURIComponent(req.get('Authorization')).split(/\s+/)[1] | ||
// : req.data.access_token || req.data.oauth_token; | ||
|
||
if (req.app.get('forceSSL') && !isSecureRequest(req)) { | ||
await req.app.get('streaming store').revokeAccess(req.params.username, req.token); | ||
return unauthorized(req, res, 400, 'invalid_request', 'HTTPS required'); | ||
} | ||
|
||
const isDir = /\/$/.test(req.blobPath); | ||
const isPublic = req.blobPath?.startsWith('/public/'); | ||
// const isPublic = /^\/public\//.test(req.blobPath); | ||
|
||
if (permission === 'r' && isPublic && !isDir) { return next(); } | ||
|
||
const permissions = await req.app.get('streaming store').permissions(req.params.username, req.token); | ||
if (!permissions) { | ||
return unauthorized(req, res, 401, 'invalid_token'); | ||
} | ||
|
||
const blobPathComponents = req.blobPath?.split('/'); | ||
const scopeName = blobPathComponents[1] === 'public' ? blobPathComponents[2] : blobPathComponents[1]; | ||
const scope = '/' + (scopeName ? scopeName + '/' : ''); | ||
const scopePermissions = permissions[scope] || []; | ||
if (scopePermissions.indexOf(permission) < 0) { | ||
return unauthorized(req, res, 403, 'insufficient_scope', `user has permissions '${JSON.stringify(scopePermissions)}' but lacks '${permission}'`); | ||
} | ||
if (permission === 'w' && isDir) { | ||
res.status(400).end(); | ||
return logRequest(req, req.params.username, 400, 0, 'can\'t write to directory'); | ||
} | ||
next(); | ||
} | ||
|
||
function noRanges (req, res, next) { | ||
if (req.get('Content-Range')) { | ||
const msg = 'Content-Range not allowed in PUT'; | ||
res.status(400).type('text/plain').send(msg); | ||
logRequest(req, req.params.username, 400, msg.length, msg); | ||
} else { | ||
next(); | ||
} | ||
} | ||
|
||
/** | ||
* Renders error response | ||
* @param {http.ClientRequest} req | ||
* @param {http.ServerResponse} res | ||
* @param {number} status - HTTP status | ||
* @param {string} errMsg - OAUTH code: invalid_request, access_denied, invalid_scope, etc. | ||
* @param {string|Error} [logMsg] - should concisely give details & be distinct from other calls | ||
*/ | ||
function unauthorized (req, res, status, errMsg, logMsg) { | ||
const realm = getHost(req); | ||
res.set('WWW-Authenticate', `Bearer realm="${realm}" error="${errMsg}"`); | ||
res.status(status).end(); | ||
logRequest(req, req.params.username, status, 0, logMsg || errMsg || ''); | ||
} | ||
|
||
module.exports = router; |
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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
Oops, something went wrong.