const logger = require('winston'); const { getClaimId, getLocalFileRecord } = require('../controllers/serveController.js'); const serveHelpers = require('../helpers/serveHelpers.js'); const { handleErrorResponse } = require('../helpers/errorHandlers.js'); const lbryUri = require('../helpers/lbryUri.js'); const SERVE = 'SERVE'; const SHOW = 'SHOW'; const NO_CHANNEL = 'NO_CHANNEL'; const NO_CLAIM = 'NO_CLAIM'; const NO_FILE = 'NO_FILE'; function isValidClaimId (claimId) { return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId)); } function isValidShortId (claimId) { return claimId.length === 1; // it should really evaluate the short url itself } function isValidShortIdOrClaimId (input) { return (isValidClaimId(input) || isValidShortId(input)); } function clientAcceptsHtml ({accept}) { return accept && accept.match(/text\/html/); } function requestIsFromBrowser (headers) { return headers['user-agent'] && headers['user-agent'].match(/Mozilla/); }; function clientWantsAsset ({accept, range}) { const imageIsWanted = accept && accept.match(/image\/.*/) && !accept.match(/text\/html/) && !accept.match(/text\/\*/); const videoIsWanted = accept && range; return imageIsWanted || videoIsWanted; } function determineResponseType (hasFileExtension, headers) { let responseType; if (hasFileExtension) { responseType = SERVE; // assume a serve request if file extension is present if (clientAcceptsHtml(headers)) { // if the request comes from a browser, change it to a show request responseType = SHOW; } } else { responseType = SHOW; if (clientWantsAsset(headers) && requestIsFromBrowser(headers)) { // this is in case someone embeds a show url logger.debug('Show request came from browser but wants an image/video. Changing response to serve...'); responseType = SERVE; } } return responseType; } function serveAssetToClient (claimId, name, res) { return getLocalFileRecord(claimId, name) .then(fileInfo => { // logger.debug('fileInfo:', fileInfo); if (fileInfo === NO_FILE) { return res.status(307).redirect(`/api/claim/get/${name}/${claimId}`); } return serveHelpers.serveFile(fileInfo, claimId, name, res); }) .catch(error => { throw error; }); } function flipClaimNameAndIdForBackwardsCompatibility (identifier, name) { // this is a patch for backwards compatability with '/name/claim_id' url format if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) { const tempName = name; name = identifier; identifier = tempName; } return [identifier, name]; } function logRequestData (responseType, claimName, channelName, claimId) { logger.debug('responseType ===', responseType); logger.debug('claim name === ', claimName); logger.debug('channel name ===', channelName); logger.debug('claim id ===', claimId); } module.exports = (app) => { // route to serve a specific asset using the channel or claim id app.get('/:identifier/:claim', ({ headers, ip, originalUrl, params }, res) => { // decide if this is a show request let hasFileExtension; try { ({ hasFileExtension } = lbryUri.parseModifier(params.claim)); } catch (error) { return res.status(400).json({success: false, message: error.message}); } let responseType = determineResponseType(hasFileExtension, headers); if (responseType !== SERVE) { return res.status(200).render('index'); } // parse the claim let claimName; try { ({ claimName } = lbryUri.parseClaim(params.claim)); } catch (error) { return res.status(400).json({success: false, message: error.message}); } // parse the identifier let isChannel, channelName, channelClaimId, claimId; try { ({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(params.identifier)); } catch (error) { return res.status(400).json({success: false, message: error.message}); } if (!isChannel) { [claimId, claimName] = flipClaimNameAndIdForBackwardsCompatibility(claimId, claimName); } // log the request data for debugging logRequestData(responseType, claimName, channelName, claimId); // get the claim Id and then serve the asset getClaimId(channelName, channelClaimId, claimName, claimId) .then(fullClaimId => { if (fullClaimId === NO_CLAIM) { return res.status(404).json({success: false, message: 'no claim id could be found'}); } else if (fullClaimId === NO_CHANNEL) { return res.status(404).json({success: false, message: 'no channel id could be found'}); } serveAssetToClient(fullClaimId, claimName, res); // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success'); }) .catch(error => { handleErrorResponse(originalUrl, ip, error, res); // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail'); }); }); // route to serve the winning asset at a claim or a channel page app.get('/:claim', ({ headers, ip, originalUrl, params, query }, res) => { // decide if this is a show request let hasFileExtension; try { ({ hasFileExtension } = lbryUri.parseModifier(params.claim)); } catch (error) { return res.status(400).json({success: false, message: error.message}); } let responseType = determineResponseType(hasFileExtension, headers); if (responseType !== SERVE) { return res.status(200).render('index'); } // parse the claim let claimName; try { ({claimName} = lbryUri.parseClaim(params.claim)); } catch (error) { return res.status(400).json({success: false, message: error.message}); } // log the request data for debugging logRequestData(responseType, claimName, null, null); // get the claim Id and then serve the asset getClaimId(null, null, claimName, null) .then(fullClaimId => { if (fullClaimId === NO_CLAIM) { return res.status(404).json({success: false, message: 'no claim id could be found'}); } serveAssetToClient(fullClaimId, claimName, res); // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success'); }) .catch(error => { handleErrorResponse(originalUrl, ip, error, res); // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail'); }); }); };