summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortomsmeding <hallo@tomsmeding.nl>2015-08-29 09:54:34 +0200
committertomsmeding <hallo@tomsmeding.nl>2015-08-29 09:54:34 +0200
commit537486da6bf93c66180714102174f14706cab1f6 (patch)
tree35e1671cbe2d44d3fb7a18b464bcc7aeefeb2ce7
Initial
-rw-r--r--.gitignore3
-rwxr-xr-xclient.js274
-rw-r--r--package.json26
-rwxr-xr-xserverstore.js161
4 files changed, 464 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ba2c80
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+images
+node_modules
+.DS_Store
diff --git a/client.js b/client.js
new file mode 100755
index 0000000..00d4abb
--- /dev/null
+++ b/client.js
@@ -0,0 +1,274 @@
+#!/usr/bin/env node
+
+var fs=require("fs"),
+ path=require("path"),
+ http=require("http"),
+ crypto=require("crypto"),
+ dialog=require("dialog"),
+ kbd=require("kbd");
+
+var HOSTNAME="localhost",HTTPPORT=42420;
+
+var userid="0",password="-";
+
+
+var HOMEDIR=process.env.HOME||process.env.HOMEPATH||process.env.HOMEDIR||process.cwd();
+var WATCHDIR=HOMEDIR+"/Desktop";
+var ignored=[];
+
+var currentState=[];
+
+//lots of code taken/modified from tomsmeding/gvajnez
+function collectDirState(dir){
+ if(!dir)dir=directory;
+ //console.log("collectDirState("+dir+")");
+ var list=fs.readdirSync(dir);
+ var result=[];
+ var statinfo,i,j;
+ for(i=0;i<list.length;i++){
+ for(j=0;j<ignored.length;j++){
+ if(etc.endsWith(path.normalize(dir+"/"+list[i]),"/"+ignored[j]))break;
+ }
+ if(j<ignored.length)continue;
+ statinfo=fs.statSync(dir+"/"+list[i]);
+ /*if(statinfo.isDirectory()){
+ result=result.concat(collectDirState(dir+"/"+list[i]));
+ } else if(statinfo.isFile()){*/
+ result.push({
+ name:path.resolve(dir+"/"+list[i]),
+ mode:statinfo.mode,
+ mtime:statinfo.mtime.getTime()
+ });
+ //}
+ }
+ return result;
+}
+
+function collectChanges(dir,state){
+ if(!dir)dir=directory;
+ var i,j,obj;
+ var changes=[];
+ for(i=0;i<state.length;i++){
+ for(j=0;j<currentState.length;j++){
+ if(state[i].name==currentState[j].name)break;
+ }
+ if(j==currentState.length||
+ currentState[j].mode!=state[i].mode||
+ currentState[j].mtime!=state[i].mtime){
+ //either file didn't exist yet, or metadata has changed
+ changes.push(state[i]);
+ }
+ }
+ for(i=0;i<currentState.length;i++){
+ for(j=0;j<state.length;j++){
+ if(currentState[i].name==state[j].name)break;
+ }
+ if(j==state.length){
+ //file doesn't exist anymore
+ changes.push(currentState[i]);
+ }
+ }
+ return changes;
+}
+
+function handleChanges(changes){
+ var i,match,namedate,now;
+ for(i=0;i<changes.length;i++){
+ if(!fs.existsSync(changes[i].name))continue; //probably delete event
+ match=changes[i].name.match(/\/Screen Shot (\d{4}-\d{2}-\d{2}) at (\d{2}\.\d{2}\.\d{2}).png$/);
+ if(!match)continue;
+ namedate=new Date(match[1]+" "+match[2].replace(/\./g,":"));
+ if(!namedate)continue;
+ changes[i].mtime=new Date(changes[i].mtime);
+ now=new Date();
+ if((now-changes[i].mtime)/1000>15||(now-namedate)/1000>15)continue; //15 seconds limit
+ sendfile(changes[i].name);
+ }
+}
+
+function authhash(challenge,password){
+ var s=challenge+password;
+ var hasher=crypto.createHash("sha256");
+ hasher.update(s);
+ return hasher.digest("hex");
+}
+
+function getchallenge(cb){
+ console.log("Going to request challenge...");
+ var req=http.request({
+ hostname:HOSTNAME,
+ port:HTTPPORT,
+ path:"/challenge",
+ method:"GET",
+ keepAlive:true //speed up the next request
+ },function(res){
+ var body="";
+ res.on("data",function(data){
+ body+=data;
+ });
+ res.on("end",function(){
+ if(res.statusCode!=200){
+ dialog.warn("Could not request challenge! Is your internet connection alive?\n\n"+body);
+ return;
+ }
+ console.log("challenge = "+body);
+ cb(body);
+ });
+ });
+ req.on("error",function(err){
+ console.log(err);
+ });
+ req.end();
+}
+function sendfile(fname,retries){
+ retries=retries!=null?retries:3;
+ var barefname;
+ var idx=fname.lastIndexOf("/");
+ if(idx==-1)barefname=fname;
+ else barefname=fname.slice(idx+1);
+ getchallenge(function(challenge){
+ var req=http.request({
+ hostname:HOSTNAME,
+ port:HTTPPORT,
+ path:"/image/"+userid+"/"+authhash(challenge,password)+"/"+escape(barefname),
+ method:"POST",
+ headers:{
+ "Content-Type":"image/png"
+ }
+ },function(res){
+ var body="";
+ res.on("data",function(data){
+ body+=data;
+ });
+ res.on("end",function(){
+ if(res.statusCode!=200){
+ if(retries>0)sendfileChallenge(fname,challenge,retries-1);
+ else {
+ dialog.warn("Could not upload image! Are your credentials still okay?\n\n"+body);
+ return;
+ }
+ }
+ console.log("Successful upload");
+ fs.unlink(fname); //not sync, take your time
+ dialog.info(body);
+ });
+ });
+ req.on("error",function(err){
+ console.log(err);
+ });
+ req.end(fs.readFileSync(fname));
+ });
+}
+
+
+function userExists(userid,cb){
+ var req=http.request({
+ hostname:HOSTNAME,
+ port:HTTPPORT,
+ path:"/exists/"+userid,
+ method:"GET"
+ },function(res){
+ if(res.statusCode==200)cb(true);
+ else if(res.statusCode==404)cb(false);
+ else {
+ console.log("Server returned status code "+res.statusCode+" for exists query!");
+ }
+ });
+ req.on("error",function(err){
+ console.log(err);
+ });
+ req.end();
+}
+
+function checkLogin(userid,password,cb){
+ getchallenge(function(challenge){
+ var req=http.request({
+ hostname:HOSTNAME,
+ port:HTTPPORT,
+ path:"/checklogin/"+userid+"/"+authhash(challenge,password),
+ method:"GET"
+ },function(res){
+ if(res.statusCode==200)cb(true);
+ else if(res.statusCode==404||res.statusCode==403)cb(false);
+ else {
+ console.log("Server returned status code "+res.statusCode+" for checklogin query!");
+ }
+ });
+ req.on("error",function(err){
+ console.log(err);
+ });
+ req.end();
+ });
+}
+
+function registerUser(userid,password){
+ var req=http.request({
+ hostname:HOSTNAME,
+ port:HTTPPORT,
+ path:"/registerx/"+userid,
+ method:"POST",
+ headers:{
+ "Content-Type":"text/plain"
+ }
+ },function(res){
+ var body="";
+ res.on("data",function(data){
+ body+=data;
+ });
+ res.on("end",function(){
+ if(res.statusCode==200)console.log("Successfully registered user "+userid);
+ else if(res.statusCode==409)console.log("Conflict: "+body);
+ else console.log("Error: "+body);
+ });
+ });
+ req.on("error",function(err){
+ console.log(err);
+ });
+ req.end(password);
+}
+
+
+
+process.stdout.write("Username? ");
+userid=kbd.getLineSync().replace(/[^a-zA-Z0-9_-]/g,"");
+process.stdout.write("Password? ");
+kbd.setEcho(false);
+password=kbd.getLineSync();
+kbd.setEcho(true);
+console.log("\nChecking existence...");
+
+userExists(userid,function(exists){
+ if(exists){
+ checkLogin(userid,password,function(ok){
+ if(ok)console.log("User login ok.");
+ else {
+ console.log("Username or password incorrect!");
+ process.exit();
+ }
+ });
+ return;
+ }
+ process.stdout.write("That username doesn't seem to exist. Register it? [y/N] ");
+ var response=kbd.getLineSync()[0];
+ if(response=="y"||response=="Y")registerUser(userid,password);
+ else {
+ console.log("Not registered. Exiting.");
+ process.exit();
+ }
+});
+
+var timeout=null;
+var watcher=fs.watch(WATCHDIR,{persistent:true,recursive:false},function(ev,fname){
+ //console.log("change in directory "+WATCHDIR+" (fname "+fname+")");
+ if(timeout)return;
+ timeout=setTimeout(function(){
+ var newstate=collectDirState(WATCHDIR);
+ var changes=collectChanges(WATCHDIR,newstate).map(function(o){o.name=o.name.replace(/^\.\//,"");return o;});
+ currentState=newstate;
+ if(changes.length!=0)handleChanges(changes);
+ timeout=null;
+ //console.log(currentState);
+ },500);
+});
+currentState=collectDirState(WATCHDIR);
+console.log("-- (Client ready.)");
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..53a1bf3
--- /dev/null
+++ b/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "serverstore",
+ "version": "1.0.0",
+ "description": "A Puush alternative",
+ "main": "serverstore.js",
+ "dependencies": {
+ "body-parser": "^1.13.3",
+ "dialog": "^0.1.8",
+ "express": "^4.13.3",
+ "kbd": "^0.0.8",
+ "glob": "^5.0.14",
+ "node-persist": "^0.0.6"
+ },
+ "devDependencies": {},
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [
+ "image",
+ "hosting",
+ "server",
+ "screenshot"
+ ],
+ "author": "Tom Smeding <hallo@tomsmeding.nl> (http://tomsmeding.com)",
+ "license": "MIT"
+}
diff --git a/serverstore.js b/serverstore.js
new file mode 100755
index 0000000..b2d7085
--- /dev/null
+++ b/serverstore.js
@@ -0,0 +1,161 @@
+#!/usr/bin/env node
+
+var fs=require("fs"),
+ path=require("path"),
+ app=require("express")(),
+ bodyParser=require("body-parser"),
+ crypto=require("crypto"),
+ Persist=require("node-persist"),
+ glob=require("glob");
+
+var HTTPPORT=42420;
+
+Persist.initSync({});
+
+var challenge=null;
+
+function renewChallenge(){
+ var entropy=crypto.randomBytes(256);
+ var hasher=crypto.createHash("sha256");
+ hasher.update(entropy);
+ challenge=hasher.digest("hex");
+}
+setInterval(renewChallenge,8000);
+renewChallenge();
+
+var gencode=(function(){
+ const startn=42424242;
+ const alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ var lastn=Persist.getItemSync("gencode_lastn");
+ if(lastn==null)lastn=startn;
+ return function gencode(){
+ var code="",coden=lastn;
+ while(coden){
+ code+=alphabet[coden%alphabet.length];
+ coden=~~(coden/alphabet.length);
+ }
+ if(lastn==0x7fffffff)lastn=0; //maximum value of a 32-bit int
+ else lastn++;
+ Persist.setItemSync("gencode_lastn",lastn);
+ if(lastn==startn)throw new Error("RUN OUT OF INDICES PANIC PANIC");
+ while(code.length<6)code+="a";
+ return code;
+ };
+})();
+
+
+app.use(bodyParser.raw({
+ limit:"3mb",
+ type:"image/*"
+}));
+
+app.use(bodyParser.text({
+ type:"text/plain"
+}));
+
+
+app.get("/challenge",function(req,res){
+ res.end(challenge);
+});
+
+
+app.param("userid",function(req,res,next,userid){
+ var password=Persist.getItemSync("user_"+userid);
+ if(!password){
+ res.sendStatus(404);
+ res.end("Non-existent userid");
+ return;
+ }
+ req.ssuser=[userid,password];
+ next();
+});
+app.param("authhash",function(req,res,next,authhash){
+ var s=challenge+req.ssuser[1];
+ var hasher=crypto.createHash("sha256");
+ hasher.update(s);
+ var hashres=hasher.digest("hex");
+ if(hashres!=authhash){
+ res.sendStatus(403);
+ res.end("Invalid answer to challenge");
+ return;
+ }
+ next();
+});
+app.param("fname",function(req,res,next,fname){
+ req.sscode=gencode();
+ req.ssfname=req.sscode+"_"+fname.replace(/[\x00-\x1F\/]/g,"").replace(/^\.+/,"");
+ next();
+});
+app.post("/image/:userid/:authhash/:fname",function(req,res){
+ try {
+ if(!fs.statSync("images").isDirectory())throw new Error;
+ } catch(e){
+ fs.mkdirSync("images");
+ if(!fs.statSync("images").isDirectory())throw new Error;
+ }
+ if(!fs.existsSync("images/"+req.ssuser[0])){
+ fs.mkdirSync("images/"+req.ssuser[0]);
+ }
+ fs.writeFileSync("images/"+req.ssuser[0]+"/"+req.ssfname,req.body);
+ res.end("http://"+req.hostname+(HTTPPORT!=80?":"+HTTPPORT:"")+"/ssimg/"+req.ssuser[0]+"/"+req.sscode);
+});
+
+
+app.param("reguserid",function(req,res,next,reguserid){
+ req.ssreguserid=reguserid;
+ next();
+});
+app.post("/registerx/:reguserid",function(req,res){ //pass password in body
+ var password=req.body;
+ if(Persist.getItemSync("user_"+req.ssreguserid)){
+ res.sendStatus(409); //Conflict
+ res.end("That userid already exists");
+ return;
+ }
+ Persist.setItemSync("user_"+req.ssreguserid,password);
+ res.sendStatus(200);
+ res.end();
+});
+
+
+app.get("/exists/:reguserid",function(req,res){
+ res.sendStatus(Persist.getItemSync("user_"+req.ssreguserid)?200:404);
+ res.end();
+});
+
+app.get("/checklogin/:userid/:authhash",function(req,res){
+ res.sendStatus(200);
+ res.end();
+});
+
+
+app.param("ssimgcode",function(req,res,next,ssimgcode){
+ req.ssimgcode=ssimgcode.replace(/[^a-zA-Z0-9]/g,"");
+ if(req.ssimgcode.length!=6){
+ res.sendStatus(404);
+ res.end("Invalid or unknown image code");
+ return;
+ }
+ next();
+});
+app.get("/ssimg/:userid/:ssimgcode",function(req,res){
+ var files=glob.sync(__dirname+"/images/"+req.ssuser[0]+"/"+req.ssimgcode+"*");
+ if(files.length==0){
+ res.sendStatus(404);
+ res.end("Unknown image code");
+ return;
+ }
+ if(files.length>1){
+ console.log("More than one file matched; internal error");
+ console.log(files);
+ res.sendStatus(500);
+ res.end("More than one file matched; internal error");
+ return;
+ }
+ res.sendFile(files[0]);
+});
+
+
+app.listen(42420,function(){
+ console.log("Server started.");
+});