Publish api fix #191

Merged
bones7242 merged 4 commits from publish-api-fix into redesign-1 2017-10-07 02:47:59 +02:00
8 changed files with 300 additions and 187 deletions
Showing only changes of commit 7baf2dd75c - Show all commits

View file

@ -2,21 +2,23 @@ const db = require('../models');
const logger = require('winston');
module.exports = {
authenticateApiPublish (username, password) {
authenticateChannelCredentials (channelName, userPassword) {
return new Promise((resolve, reject) => {
if (username === 'none') {
if (!channelName) {
resolve(true);
return;
}
const userName = channelName.substring(1);
logger.debug(`authenticateChannelCredentials > channelName: ${channelName} username: ${userName} pass: ${userPassword}`);
db.User
.findOne({where: {userName: username}})
.findOne({where: { userName }})
.then(user => {
if (!user) {
logger.debug('no user found');
resolve(false);
return;
}
if (!user.validPassword(password, user.password)) {
if (!user.validPassword(userPassword, user.password)) {
logger.debug('incorrect password');
resolve(false);
return;

View file

@ -7,24 +7,23 @@ module.exports = {
publish (publishParams, fileName, fileType) {
return new Promise((resolve, reject) => {
let publishResults = {};
// 1. make sure the name is available
publishHelpers.checkClaimNameAvailability(publishParams.name)
// 2. publish the file
.then(result => {
if (result === true) {
return lbryApi.publishClaim(publishParams);
} else {
return new Error('That name is already in use by spee.ch.');
}
})
// 3. upsert File record (update is in case the claim has been published before by this daemon)
// 1. publish the file
lbryApi.publishClaim(publishParams)
// 2. upsert File record (update is in case the claim has been published before by this daemon)
.then(tx => {
logger.info(`Successfully published ${fileName}`, tx);
publishResults = tx;
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
})
.then(user => {
if (user) { logger.debug('successfully found user in User table') } else { logger.error('user for publish not found in User table') };
let certificateId;
if (user) {
certificateId = user.channelClaimId;
logger.debug('successfully found user in User table');
} else {
certificateId = null;
logger.debug('user for publish not found in User table');
};
const fileRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
@ -39,17 +38,17 @@ module.exports = {
nsfw : publishParams.metadata.nsfw,
};
const claimRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
title : publishParams.metadata.title,
description : publishParams.metadata.description,
address : publishParams.claim_address,
outpoint : `${publishResults.txid}:${publishResults.nout}`,
height : 0,
contentType : fileType,
nsfw : publishParams.metadata.nsfw,
certificateId: user.channelClaimId,
amount : publishParams.bid,
name : publishParams.name,
claimId : publishResults.claim_id,
title : publishParams.metadata.title,
description: publishParams.metadata.description,
address : publishParams.claim_address,
outpoint : `${publishResults.txid}:${publishResults.nout}`,
height : 0,
contentType: fileType,
nsfw : publishParams.metadata.nsfw,
certificateId,
amount : publishParams.bid,
};
const upsertCriteria = {
name : publishParams.name,

View file

@ -30,8 +30,16 @@ module.exports = {
logger.error('Publish Error:', useObjectPropertiesIfNoKeys(error));
if (error.code === 'ECONNREFUSED') {
return 'Connection refused. The daemon may not be running.';
} else if (error.response.data.error) {
return error.response.data.error.message;
} else if (error.response) {
if (error.response.data) {
if (error.response.data.message) {
return error.response.data.message;
} else if (error.response.data.error) {
return error.response.data.error.message;
}
return error.response.data;
}
return error.response;
} else {
return error;
}

View file

@ -1,78 +1,130 @@
const logger = require('winston');
const config = require('config');
const fs = require('fs');
const db = require('../models');
const config = require('config');
module.exports = {
validateFile (file, name, license, nsfw) {
validateApiPublishRequest (body, files) {
if (!body) {
throw new Error('no body found in request');
}
if (!body.name) {
throw new Error('no name field found in request');
}
if (!body.nsfw) {
throw new Error('no nsfw field found in request');
}
if (!files) {
throw new Error('no files found in request');
}
if (!files.file) {
throw new Error('no file with key of [file] found in request');
}
},
validatePublishSubmission (file, claimName, nsfw) {
try {
module.exports.validateFile(file);
module.exports.validateClaimName(claimName);
module.exports.validateNSFW(nsfw);
} catch (error) {
throw error;
}
},
validateFile (file) {
if (!file) {
throw new Error('No file was submitted or the key used was incorrect. Files posted through this route must use a key of "speech" or null');
logger.debug('publish > file validation > no file found');
throw new Error('no file provided');
}
// check the file name
if (/'/.test(file.name)) {
logger.debug('publish > file validation > file name had apostrophe in it');
throw new Error('apostrophes are not allowed in the file name');
}
// check file type and size
switch (file.type) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
if (file.size > 10000000) {
logger.debug('publish > file validation > .jpeg/.jpg/.png was too big');
throw new Error('Sorry, images are limited to 10 megabytes.');
}
break;
case 'image/gif':
if (file.size > 50000000) {
throw new Error('Your image exceeds the 50 megabyte limit.');
logger.debug('publish > file validation > .gif was too big');
throw new Error('Sorry, .gifs are limited to 50 megabytes.');
}
break;
case 'video/mp4':
if (file.size > 50000000) {
throw new Error('Your video exceeds the 50 megabyte limit.');
logger.debug('publish > file validation > .mp4 was too big');
throw new Error('Sorry, videos are limited to 50 megabytes.');
}
break;
default:
throw new Error('The ' + file.Type + ' content type is not supported. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
logger.debug('publish > file validation > unrecognized file type');
throw new Error('The ' + file.type + ' content type is not supported. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
}
// validate claim name
const invalidCharacters = /[^A-Za-z0-9,-]/.exec(name);
return file;
},
validateClaimName (claimName) {
const invalidCharacters = /[^A-Za-z0-9,-]/.exec(claimName);
if (invalidCharacters) {
throw new Error('The url name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"');
throw new Error('The claim name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"');
}
// validate license
},
validateLicense (license) {
if ((license.indexOf('Public Domain') === -1) && (license.indexOf('Creative Commons') === -1)) {
throw new Error('Only posts with a "Public Domain" license, or one of the Creative Commons licenses are eligible for publishing through spee.ch');
throw new Error('Only posts with a "Public Domain" or "Creative Commons" license are eligible for publishing through spee.ch');
}
},
cleanseNSFW (nsfw) {
switch (nsfw) {
case true:
case false:
case 'true':
case 'false':
case 'on':
case 'true':
case 1:
case '1':
return true;
case false:
case 'false':
case 'off':
case 0:
case '0':
case 1:
case '1':
break;
return false;
default:
throw new Error('NSFW value was not accepted. NSFW must be set to either true, false, "on", or "off"');
return null;
}
},
createPublishParams (name, filePath, title, description, license, nsfw, channel) {
logger.debug(`Creating Publish Parameters for "${name}"`);
const claimAddress = config.get('WalletConfig.LbryClaimAddress');
const defaultChannel = config.get('WalletConfig.DefaultChannel');
// filter nsfw and ensure it is a boolean
if (nsfw === false) {
nsfw = false;
} else if (typeof nsfw === 'string') {
if (nsfw.toLowerCase === 'false' || nsfw.toLowerCase === 'off' || nsfw === '0') {
nsfw = false;
cleanseChannelName (channelName) {
if (channelName) {
if (channelName.indexOf('@') !== 0) {
channelName = `@${channelName}`;
}
} else if (nsfw === 0) {
nsfw = false;
} else {
nsfw = true;
}
// provide defaults for title & description
if (title === null || title === '') {
return channelName;
},
validateNSFW (nsfw) {
if (nsfw === true || nsfw === false) {
return;
}
throw new Error('NSFW must be set to either true or false');
},
createPublishParams (filePath, name, title, description, license, nsfw, channelName) {
logger.debug(`Creating Publish Parameters`);
// provide defaults for title
if (title === null || title.trim() === '') {
title = name;
}
// provide default for description
if (description === null || description.trim() === '') {
description = `${name} published via spee.ch`;
}
// provide default for license
if (license === null || license.trim() === '') {
license = 'All Rights Reserved';
}
// create the publish params
const publishParams = {
name,
@ -86,16 +138,12 @@ module.exports = {
license,
nsfw,
},
claim_address: claimAddress,
claim_address: config.get('WalletConfig.LbryClaimAddress'),
};
// add channel if applicable
if (channel !== 'none') {
publishParams['channel_name'] = channel;
} else {
publishParams['channel_name'] = defaultChannel;
// add channel to params, if applicable
if (channelName) {
publishParams['channel_name'] = channelName;
}
logger.debug('publishParams:', publishParams);
return publishParams;
},
deleteTemporaryFile (filePath) {

View file

@ -33,6 +33,7 @@
"express": "^4.15.2",
"express-handlebars": "^3.0.0",
"express-session": "^1.15.5",
"form-data": "^2.3.1",
"helmet": "^3.8.1",
"mysql2": "^1.3.5",
"nodemon": "^1.11.0",

View file

@ -2,46 +2,56 @@
// validation function which checks the proposed file's type, size, and name
function validateFile(file) {
if (!file) {
throw new Error('no file provided');
}
if (/'/.test(file.name)) {
throw new Error('apostrophes are not allowed in the file name');
}
// validate size and type
switch (file.type) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
case 'image/gif':
if (file.size > 50000000){
throw new Error('Sorry, images are limited to 50 megabytes.');
}
break;
case 'video/mp4':
if (file.size > 50000000){
throw new Error('Sorry, videos are limited to 50 megabytes.');
}
break;
default:
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.')
}
if (!file) {
console.log('no file found');
throw new Error('no file provided');
}
if (/'/.test(file.name)) {
console.log('file name had apostrophe in it');
throw new Error('apostrophes are not allowed in the file name');
}
// validate size and type
switch (file.type) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
if (file.size > 10000000){
console.log('file was too big');
throw new Error('Sorry, images are limited to 10 megabytes.');
}
break;
case 'image/gif':
if (file.size > 50000000){
console.log('file was too big');
throw new Error('Sorry, .gifs are limited to 50 megabytes.');
}
break;
case 'video/mp4':
if (file.size > 50000000){
console.log('file was too big');
throw new Error('Sorry, videos are limited to 50 megabytes.');
}
break;
default:
console.log('file type is not supported');
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.')
}
}
// validation function that checks to make sure the claim name is valid
function validateClaimName (name) {
// ensure a name was entered
if (name.length < 1) {
throw new NameError("You must enter a name for your url");
}
// validate the characters in the 'name' field
const invalidCharacters = /[^A-Za-z0-9,-]/g.exec(name);
if (invalidCharacters) {
throw new NameError('"' + invalidCharacters + '" characters are not allowed in the url.');
}
// ensure a name was entered
if (name.length < 1) {
throw new NameError("You must enter a name for your url");
}
// validate the characters in the 'name' field
const invalidCharacters = /[^A-Za-z0-9,-]/g.exec(name);
if (invalidCharacters) {
throw new NameError('"' + invalidCharacters + '" characters are not allowed in the url.');
}
}
function validateChannelName (name) {
name = name.substring(name.indexOf('@') + 1);
name = name.substring(name.indexOf('@') + 1);
// ensure a name was entered
if (name.length < 1) {
throw new ChannelNameError("You must enter a name for your channel");
@ -60,9 +70,9 @@ function validatePassword (password) {
}
function cleanseClaimName(name) {
name = name.replace(/\s+/g, '-'); // replace spaces with dashes
name = name.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
return name;
name = name.replace(/\s+/g, '-'); // replace spaces with dashes
name = name.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
return name;
}
// validation functions to check claim & channel name eligibility as the inputs change
@ -99,14 +109,14 @@ function checkAvailability(name, successDisplayElement, errorDisplayElement, val
// check to make sure it is available
isNameAvailable(name, apiUrl)
.then(result => {
console.log('result:', result)
if (result === true) {
console.log('result:', result)
if (result === true) {
hideError(errorDisplayElement);
showSuccess(successDisplayElement)
} else {
} else {
hideSuccess(successDisplayElement);
showError(errorDisplayElement, errorMessage);
}
}
})
.catch(error => {
hideSuccess(successDisplayElement);
@ -119,58 +129,69 @@ function checkAvailability(name, successDisplayElement, errorDisplayElement, val
}
function checkClaimName(name){
const successDisplayElement = document.getElementById('input-success-claim-name');
const errorDisplayElement = document.getElementById('input-error-claim-name');
checkAvailability(name, successDisplayElement, errorDisplayElement, validateClaimName, isNameAvailable, 'Sorry, that url ending has been taken by another user', '/api/isClaimAvailable/');
const successDisplayElement = document.getElementById('input-success-claim-name');
const errorDisplayElement = document.getElementById('input-error-claim-name');
checkAvailability(name, successDisplayElement, errorDisplayElement, validateClaimName, isNameAvailable, 'Sorry, that url ending has been taken', '/api/isClaimAvailable/');
}
function checkChannelName(name){
const successDisplayElement = document.getElementById('input-success-channel-name');
const errorDisplayElement = document.getElementById('input-error-channel-name');
name = `@${name}`;
checkAvailability(name, successDisplayElement, errorDisplayElement, validateChannelName, isNameAvailable, 'Sorry, that Channel has been taken by another user', '/api/isChannelAvailable/');
checkAvailability(name, successDisplayElement, errorDisplayElement, validateChannelName, isNameAvailable, 'Sorry, that Channel name has been taken by another user', '/api/isChannelAvailable/');
}
// validation function which checks all aspects of the publish submission
function validateFilePublishSubmission(stagedFiles, claimName, channelName){
return new Promise(function (resolve, reject) {
// 1. make sure only 1 file was selected
if (!stagedFiles) {
return reject(new FileError("Please select a file"));
} else if (stagedFiles.length > 1) {
return reject(new FileError("Only one file is allowed at a time"));
}
// 2. validate the file's name, type, and size
try {
validateFile(stagedFiles[0]);
} catch (error) {
return reject(error);
}
// 3. validate that a channel was chosen
if (channelName === 'new' || channelName === 'login') {
return reject(new ChannelNameError("Please select a valid channel"));
console.log(`validating publish submission > name: ${claimName} channel: ${channelName} file:`, stagedFiles);
return new Promise(function (resolve, reject) {
// 1. make sure 1 file was staged
if (!stagedFiles) {
reject(new FileError("Please select a file"));
return;
} else if (stagedFiles.length > 1) {
reject(new FileError("Only one file is allowed at a time"));
return;
}
// 2. validate the file's name, type, and size
try {
validateFile(stagedFiles[0]);
} catch (error) {
reject(error);
return;
}
// 3. validate that a channel was chosen
if (channelName === 'new' || channelName === 'login') {
reject(new ChannelNameError("Please log in to a channel"));
return;
};
// 4. validate the claim name
try {
validateClaimName(claimName);
} catch (error) {
return reject(error);
}
// if all validation passes, check availability of the name
isNameAvailable(claimName, '/api/isClaimAvailable/')
.then(() => {
resolve();
})
.catch(error => {
reject(error);
});
});
// 4. validate the claim name
try {
validateClaimName(claimName);
} catch (error) {
reject(error);
return;
}
// if all validation passes, check availability of the name (note: do we need to re-validate channel name vs. credentials as well?)
return isNameAvailable(claimName, '/api/isClaimAvailable/')
.then(result => {
if (result) {
resolve();
} else {
reject(new NameError('that url ending is already taken'));
}
})
.catch(error => {
reject(error);
});
});
}
// validation function which checks all aspects of the publish submission
function validateNewChannelSubmission(channelName, password){
// validation function which checks all aspects of a new channel submission
function validateNewChannelSubmission(userName, password){
const channelName = `@${userName}`;
return new Promise(function (resolve, reject) {
// 1. validate name
// 1. validate name
try {
validateChannelName(channelName);
} catch (error) {
@ -184,12 +205,17 @@ function validateNewChannelSubmission(channelName, password){
}
// 3. if all validation passes, check availability of the name
isNameAvailable(channelName, '/api/isChannelAvailable/') // validate the availability
.then(() => {
console.log('channel is avaliable');
resolve();
.then(result => {
if (result) {
console.log('channel is available');
resolve();
} else {
console.log('channel is not available');
reject(new ChannelNameError('that channel name has already been taken'));
}
})
.catch( error => {
console.log('error: channel is not avaliable');
console.log('error evaluating channel name availability', error);
reject(error);
});
});

View file

@ -4,10 +4,10 @@ const multipartMiddleware = multipart();
const db = require('../models');
const { publish } = require('../controllers/publishController.js');
const { getClaimList, resolveUri } = require('../helpers/lbryApi.js');
const { createPublishParams, validateFile, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js');
const { createPublishParams, validateApiPublishRequest, validatePublishSubmission, cleanseNSFW, cleanseChannelName, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js');
const errorHandlers = require('../helpers/errorHandlers.js');
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
const { authenticateApiPublish } = require('../auth/authentication.js');
const { authenticateChannelCredentials } = require('../auth/authentication.js');
module.exports = (app) => {
// route to run a claim_list request on the daemon
@ -71,52 +71,80 @@ module.exports = (app) => {
});
});
// route to run a publish request on the daemon
app.post('/api/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl }, res) => {
// google analytics
sendGoogleAnalytics('PUBLISH', headers, ip, originalUrl);
// validate that a file was provided
const file = files.speech || files.null;
const name = body.name || file.name.substring(0, file.name.indexOf('.'));
const title = body.title || null;
const description = body.description || null;
const license = body.license || 'No License Provided';
const nsfw = body.nsfw || null;
const channelName = body.channelName || 'none';
const channelPassword = body.channelPassword || null;
logger.debug(`name: ${name}, license: ${license}, nsfw: ${nsfw}`);
app.post('/api/publish', multipartMiddleware, (req, res) => {
logger.debug(req);
const body = req.body;
const files = req.files;
try {
validateFile(file, name, license, nsfw);
validateApiPublishRequest(body, files);
} catch (error) {
postToStats('publish', originalUrl, ip, null, null, error.message);
logger.debug('rejected >>', error.message);
res.status(400).send(error.message);
logger.debug('publish request rejected, insufficient request parameters');
res.status(400).json({success: false, message: error.message});
return;
}
// required inputs
const file = files.file;
const fileName = file.name;
const filePath = file.path;
const fileType = file.type;
// channel authorization
authenticateApiPublish(channelName, channelPassword)
const name = body.name;
let nsfw = body.nsfw;
// cleanse nsfw
nsfw = cleanseNSFW(nsfw);
// validate file, name, license, and nsfw
try {
validatePublishSubmission(file, name, nsfw);
} catch (error) {
logger.debug('publish request rejected');
res.status(400).json({success: false, message: error.message});
return;
}
logger.debug(`name: ${name}, nsfw: ${nsfw}`);
// optional inputs
const license = body.license || null;
const title = body.title || null;
const description = body.description || null;
let channelName = body.channelName || null;
channelName = cleanseChannelName(channelName);
const channelPassword = body.channelPassword || null;
logger.debug(`license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}"`);
// check channel authorization
authenticateChannelCredentials(channelName, channelPassword)
.then(result => {
if (!result) {
res.status(401).send('Authentication failed, you do not have access to that channel');
throw new Error('authentication failed');
throw new Error('Authentication failed, you do not have access to that channel');
}
return createPublishParams(name, filePath, title, description, license, nsfw, channelName);
// make sure the claim name is available
return checkClaimNameAvailability(name);
})
.then(result => {
if (!result) {
throw new Error('That name is already in use by spee.ch.');
}
// create publish parameters object
return createPublishParams(filePath, name, title, description, license, nsfw, channelName);
})
// create publish parameters object
.then(publishParams => {
logger.debug('publishParams:', publishParams);
// publish the asset
return publish(publishParams, fileName, fileType);
})
// publish the asset
.then(result => {
postToStats('publish', originalUrl, ip, null, null, 'success');
res.status(200).json(result);
// postToStats('publish', originalUrl, ip, null, null, 'success');
res.status(200).json({
success: true,
message: {
url : `spee.ch/${result.claim_id}/${name}`,
lbryTx: result,
},
});
})
.catch(error => {
logger.error('publish api error', error);
res.status(400).json({success: false, message: error.message});
});
});
// route to get a short claim id from long claim Id
app.get('/api/shortClaimId/:longId/:name', ({ originalUrl, ip, params }, res) => {
// serve content

View file

@ -1,6 +1,6 @@
const logger = require('winston');
const publishController = require('../controllers/publishController.js');
const publishHelpers = require('../helpers/publishHelpers.js');
const { publish } = require('../controllers/publishController.js');
const { createPublishParams } = require('../helpers/publishHelpers.js');
const errorHandlers = require('../helpers/errorHandlers.js');
const { postToStats } = require('../controllers/statsController.js');
@ -40,12 +40,13 @@ module.exports = (app, siofu, hostedContentPath) => {
NOTE: need to validate that client has the credentials to the channel they chose
otherwise they could circumvent security client side.
*/
let channelName = file.meta.cannel;
if (channelName === 'none') channelName = null;
// prepare the publish parameters
const publishParams = publishHelpers.createPublishParams(file.meta.name, file.pathName, file.meta.title, file.meta.description, file.meta.license, file.meta.nsfw, file.meta.channel);
const publishParams = createPublishParams(file.pathName, file.meta.name, file.meta.title, file.meta.description, file.meta.license, file.meta.nsfw, channelName);
logger.debug(publishParams);
// publish the file
publishController.publish(publishParams, file.name, file.meta.type)
publish(publishParams, file.name, file.meta.type)
.then(result => {
postToStats('PUBLISH', '/', null, null, null, 'success');
socket.emit('publish-complete', { name: publishParams.name, result });