diff --git a/README.md b/README.md index 76accec..ca1a389 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,18 @@ # photo_collection Backend For a Cloud Photo Storage app + +## Development + +```sh +npm install +npm test +npm start +``` + +The server uses these optional environment variables: + +- `PORT` (defaults to `3030`) +- `MYSQL_HOST` (defaults to `127.0.0.1`) +- `MYSQL_USER` (defaults to `root`) +- `MYSQL_PASSWORD` (defaults to an empty password) +- `MYSQL_DATABASE` (defaults to `photo_collection`) diff --git a/lib/http.js b/lib/http.js new file mode 100644 index 0000000..3b08d54 --- /dev/null +++ b/lib/http.js @@ -0,0 +1,24 @@ +var qs = require('querystring'); + +exports.readFormBody = function(req, callback) { + var body = ''; + req.on('data', function(chunk) { + body += chunk; + if (body.length > 1e6) { + req.destroy(); + } + }); + req.on('end', function() { + callback(qs.parse(body)); + }); +}; + +exports.sendJson = function(resp, statusCode, payload) { + resp.statusCode = statusCode; + resp.setHeader('Content-Type', 'application/json'); + resp.end(JSON.stringify(payload)); +}; + +exports.sendError = function(resp, statusCode, message) { + exports.sendJson(resp, statusCode, { error: message }); +}; diff --git a/lib/static-images.js b/lib/static-images.js new file mode 100644 index 0000000..334c0d1 --- /dev/null +++ b/lib/static-images.js @@ -0,0 +1,43 @@ +var path = require('path'); + +var imageRoot = path.resolve(__dirname, '..', 'public', 'images'); +var contentTypes = { + '.gif': 'image/gif', + '.jpeg': 'image/jpeg', + '.jpg': 'image/jpeg', + '.png': 'image/png', + '.webp': 'image/webp' +}; + +exports.imageRoot = imageRoot; + +exports.resolveImagePath = function(pathname) { + var prefix = '/public/images/'; + if (pathname.indexOf(prefix) !== 0) { + return null; + } + + var relativePath; + try { + relativePath = decodeURIComponent(pathname.slice(prefix.length)); + } catch (err) { + if (err instanceof URIError) { + return null; + } + throw err; + } + if (!relativePath || relativePath.indexOf('\0') !== -1) { + return null; + } + + var resolvedPath = path.resolve(imageRoot, relativePath); + if (resolvedPath !== imageRoot && resolvedPath.indexOf(imageRoot + path.sep) === 0) { + return resolvedPath; + } + + return null; +}; + +exports.contentTypeFor = function(filePath) { + return contentTypes[path.extname(filePath).toLowerCase()] || 'application/octet-stream'; +}; diff --git a/package.json b/package.json index 353d50a..7fa2165 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "server.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node test/static-images.test.js", "start": "node server.js" }, "author": "", diff --git a/server.js b/server.js index 0810d1c..3837abb 100644 --- a/server.js +++ b/server.js @@ -1,17 +1,19 @@ var http = require('http'); var url = require('url'); -var join = require('path').join; -var qs = require('querystring'); +var path = require('path'); var fs = require('fs'); var mkdirp = require('mkdirp'); var formidable = require('formidable'); var fileServer = require('./lib/fileserver.js'); +var httpHelpers = require('./lib/http.js'); +var staticImages = require('./lib/static-images.js'); var mysql = require('mysql'); +var port = process.env.PORT || 3030; var db = mysql.createConnection({ - host: '127.0.0.1', - user: 'root', - password: '', - database: 'photo_collection' + host: process.env.MYSQL_HOST || '127.0.0.1', + user: process.env.MYSQL_USER || 'root', + password: process.env.MYSQL_PASSWORD || '', + database: process.env.MYSQL_DATABASE || 'photo_collection' }); db.connect(function(err) { if(err)throw err; @@ -22,9 +24,21 @@ http.createServer(function(req, resp) { case 'GET': var new_uri = url.parse(req.url); if(new_uri.pathname.indexOf('/public/images')== 0){ - // console.log(new_uri); - fs.createReadStream('.'+new_uri.pathname).pipe(resp); - // resp.end("a phto"); + var imagePath = staticImages.resolveImagePath(new_uri.pathname); + if (!imagePath) { + httpHelpers.sendError(resp, 400, 'Invalid image path'); + return; + } + + fs.stat(imagePath, function(err, stat) { + if (err || !stat.isFile()) { + httpHelpers.sendError(resp, 404, 'Image not found'); + return; + } + + resp.setHeader('Content-Type', staticImages.contentTypeFor(imagePath)); + fs.createReadStream(imagePath).pipe(resp); + }); return; } if(new_uri.pathname == '/api' ){ @@ -33,14 +47,17 @@ http.createServer(function(req, resp) { fs.createReadStream('./public/form.html').pipe(resp); return; } - var para = new_uri.query.split('&'); - // console.log(para[0].split('=')); - // console.log(para[1].split('=')); - db.query("select * from users where email = ?", para[0].split('=')[1], function (err, row) { + var queryParameters = url.parse(req.url, true).query; + if (!queryParameters.email || !queryParameters.password) { + httpHelpers.sendError(resp, 400, 'email and password are required'); + return; + } + + db.query("select * from users where email = ?", queryParameters.email, function (err, row) { if(err) throw err; console.log(row); if(row.length == 0){ - fileServer.addUser(db, para[0].split('=')[1], para[1].split('=')[1]); + fileServer.addUser(db, queryParameters.email, queryParameters.password); resp.end('New Acccount'); }else{ resp.setHeader('exist_before', true); @@ -51,17 +68,15 @@ http.createServer(function(req, resp) { } break; case 'POST': - var body = ''; var new_post_uri = url.parse(req.url); if(new_post_uri.pathname == '/addcollection'){ // console.log(new_post_uri); - body = ''; - req.on('data', function(chunk) { - body += chunk; - }); - req.on('end', function() { - var parameters = qs.parse(body); + httpHelpers.readFormBody(req, function(parameters) { // console.log(parameters); + if (!parameters.collection_name || !parameters.user_no) { + httpHelpers.sendError(resp, 400, 'collection_name and user_no are required'); + return; + } db.query("insert into collections (collection_name, user_no) values (?,?)", [parameters.collection_name, parameters.user_no], function (err, row) { if(err) throw err; var jsonresp = fileServer.listCollection(db, parameters.user_no, resp); @@ -72,26 +87,24 @@ http.createServer(function(req, resp) { } if(new_post_uri.pathname == '/login'){ // console.log(new_post_uri); - body = ''; - req.on('data', function(chunk) { - body += chunk; - }); - req.on('end', function() { - var parameters = qs.parse(body); + httpHelpers.readFormBody(req, function(parameters) { // console.log(parameters); + if (!parameters.email || !parameters.password) { + httpHelpers.sendError(resp, 400, 'email and password are required'); + return; + } fileServer.login(db, parameters, resp); }); return; } if(new_post_uri.pathname == '/listcollection'){ // console.log(new_post_uri); - body = ''; - req.on('data', function(chunk) { - body += chunk; - }); - req.on('end', function() { - var parameters = qs.parse(body); + httpHelpers.readFormBody(req, function(parameters) { // console.log(parameters); + if (!parameters.user_no) { + httpHelpers.sendError(resp, 400, 'user_no is required'); + return; + } fileServer.listCollection(db, parameters.user_no, resp); }); return; @@ -112,17 +125,30 @@ http.createServer(function(req, resp) { form.parse(req, function (err, fields, files) { if(err)throw err; // console.log("Successfully formidable"); + if (!fields.file_details || !files.uploaded_file) { + httpHelpers.sendError(resp, 400, 'file_details and uploaded_file are required'); + return; + } + + var collectionDirectory = String(fields.file_details).replace(/[^a-zA-Z0-9_-]/g, ''); + if (!collectionDirectory) { + httpHelpers.sendError(resp, 400, 'file_details is invalid'); + return; + } + db.query("select * from photographs where collection_number = ?", fields.file_details, function (err, row) { if(err) throw err; // console.log(row); - var new_url = "public/images/"+fields.file_details; + var new_url = "public/images/"+collectionDirectory; mkdirp(new_url, function (err) { if(err)throw err; // console.log("Successfully made dir"); - fs.rename(files.uploaded_file.path, new_url+"/"+row.length+".jpg", function (err) { + var extension = path.extname(files.uploaded_file.name).toLowerCase() || '.jpg'; + var fileName = Date.now() + '-' + row.length + extension; + fs.rename(files.uploaded_file.path, new_url+"/"+fileName, function (err) { if (err) throw err; // console.log("Successfully Uploaded"); - fileServer.addPhoto(db, new_url+"/"+row.length+".jpg", fields.file_details, resp); + fileServer.addPhoto(db, new_url+"/"+fileName, fields.file_details, resp); }); // console.log(files.uploaded_file.name); }); @@ -179,8 +205,7 @@ http.createServer(function(req, resp) { default: break; } -}).listen(3030 || process.env.PORT, function() { - console.log("Listening to 3030"); +}).listen(port, function() { + console.log("Listening to "+port); }); fileServer.printcwd("runing"); - diff --git a/test/static-images.test.js b/test/static-images.test.js new file mode 100644 index 0000000..a4edd6f --- /dev/null +++ b/test/static-images.test.js @@ -0,0 +1,18 @@ +var assert = require('assert'); +var path = require('path'); +var staticImages = require('../lib/static-images'); + +var safePath = staticImages.resolveImagePath('/public/images/1/photo.jpg'); +assert.strictEqual( + safePath, + path.join(staticImages.imageRoot, '1', 'photo.jpg') +); + +assert.strictEqual(staticImages.resolveImagePath('/public/images/../server.js'), null); +assert.strictEqual(staticImages.resolveImagePath('/public/images/%2e%2e/server.js'), null); +assert.strictEqual(staticImages.resolveImagePath('/public/images/%'), null); +assert.strictEqual(staticImages.resolveImagePath('/public/images/'), null); +assert.strictEqual(staticImages.contentTypeFor('/tmp/photo.PNG'), 'image/png'); +assert.strictEqual(staticImages.contentTypeFor('/tmp/photo.txt'), 'application/octet-stream'); + +console.log('static image tests passed');