diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rwxr-xr-x | client.js | 274 | ||||
-rw-r--r-- | package.json | 26 | ||||
-rwxr-xr-x | serverstore.js | 161 |
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."); +}); |