/*
 * Copyright (C) 2021 Thomas Van Acker
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * This project is hosted on https://git.bitscuit.be/bitscuit/Fawkes
 *
*/

// Imports
const http = require("http");
const url = require("url");
const fs = require("fs");
const util = require("util");
const crypto = require("crypto");
const spawn = require("child_process").spawn;
const exec = require("child_process").exec;

// Vars
const hostname = "192.168.0.128"; // Enter your local IP address here
const port = 8001; // Enter a port number here

var files = {};
var fawkes = {};


// Create HTTP server
const server = http.createServer((req, res) => {
	// Parse url
	const requrl = url.parse(req.url);
	var reqpath = requrl.pathname;
	if(reqpath == "/") reqpath = "/index.html";

	// Respond to requests
	try{
		// Return requested page
		if(reqpath.startsWith("/dl/")){
			res.statusCode = 200;
			res.write(fs.readFileSync("fawkes/files/"+reqpath.substr(4)));
			res.end();
		}else{
			res.statusCode = 200;
			res.write(fs.readFileSync("html"+reqpath));
			res.end();
		}

	}catch(error){
		// 404 if page not found
		res.statusCode = 404;
		res.setHeader("Content-Type", "text/plain");
		res.end("404 - Page Not Found");
	}
});


// Create socket.io server
const io = require("socket.io")(server); // Can only be done when server is created

io.on("connection", (socket) => {
	console.log("New user connected!");

	// Give user unique ID
	var userId = sha256("USER"+rand(0,9999999)+"TIME"+Date.now()).substr(0, 10);
	console.log("userId = "+userId);

	// Init directory structure
	fs.mkdirSync("fawkes/files/"+userId);
	
	// Init socket listeners
	
	// Disconnect
	socket.on("disconnect", () => {
		console.log(userId+": disconnect");

		// Remove all files from this user
		try{
			fs.rmSync("fawkes/files/"+userId, {recursive: true});
			fs.rmSync("fawkes/files/"+userId+"cloaked", {recursive: true});
		}catch(error){
			console.log(userId+": couldn't delete some files");
		}
	});

	// File upload
	socket.on("FILE_START", (data) => {
		console.log(userId+": file upload ("+data.name+", "+data.length+" bytes)");

		// Check length
		if(data.length > 20000000){
			console.log(userId+": file too large (data.length = "+data.length+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"File too large. Max size = 20MB"});
			return;
		}

		// Check if exists
		if((userId+data.name) in files){
			console.log(userId+": file already exists (data.name = "+data.name+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"File had already been uploaded."});
			return;
		}

		// Create file
		var name = data.name;
		var path = "fawkes/files/"+userId+"/"+name+".png";
		var file = {
			length: data.length,
			name: data.name,
			path: path,
			numSegments: data.length/100000 + 1,
			nextSegment: 0
		};
		files[userId+data.name] = file;

		// Ask client to begin upload
		socket.emit("FILE_ACK", {name:file.name, nextSegment:file.nextSegment, numSegments:file.numSegments});
	});

	// File contents upload
	socket.on("FILE_DATA", (data) => {
		console.log(userId+": file data ("+data.name+", segment "+data.segment+", "+data.data.length+" bytes)");

		// Check if file exists
		if(!((userId+data.name) in files)){
			console.log(userId+": received segment for unknown file (data.name = "+data.name+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"Received segment for unknown file."});
			return;
		}

		// Get file
		var file = files[userId+data.name];

		// Check size
		if(data.data.length > 100000){
			console.log(userId+": segment too large (data.name = "+data.name+", data.data.length = "+data.data.length+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"Segment too large."});
			return;
		}

		// Check segment number
		if(data.segment != file.nextSegment || data.segment >= file.numSegments){
			console.log(userId+": unexpected segment (data.name = "+data.name+", data.segment = "+data.segment+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"Unexpected segment."});
			return;
		}

		// Write data
		fs.appendFile(file.path, data.data, 'Binary', function(err){
			if(err){
				// Error
				console.log(userId+": couldn't write to file (error = "+err+")");
				socket.emit("FILE_ERROR", {name:data.name, error:"Internal server error. Couldn't write to file."});

			}else{
				file.nextSegment++;
				if(file.nextSegment < file.numSegments-1){
					// Ask for new segment
					socket.emit("FILE_ACK", {name:file.name, nextSegment:file.nextSegment, numSegments:file.numSegments});

				}else{
					// When file fully uploaded
					socket.emit("FILE_DONE", {name:file.name});
				}
			}
		});
	});

	// Fawkes start
	socket.on("FAWKES_START", () => {
		// Start binary
		var process = spawn("fawkes/protection", ["-d", "fawkes/files/"+userId, "--mode", "low"]);

		// Update vars
		fawkes[userId] = {
			running: true,
			images: [],
			imageSentDone: []
		};

		// Add listeners
		process.stdout.setEncoding("utf8");
		process.stdout.on("data", function(data){
			// Received output from process
			data = data.toString();
			console.log("fawkes stdout: "+data);

			// Parse output
			var lines = data.split("\n");
			for(var l=0; l<lines.length; l++){

				// For each line
				var splices = lines[l].split(" ");
				if(splices.length == 0) continue;

				if(splices[0] == "Find"){
					var numFaces = splices[1];
					var imageId = splices[4].substr(0, splices[4].indexOf("."));
					console.log("fawkes found "+numFaces+" faces in image "+imageId);

					// Update file info
					files[userId+imageId].numFaces = numFaces;
					fawkes[userId].images.push(userId+imageId);
					fawkes[userId].imageSentDone.push(numFaces == 0);

					// Update state if no faces found
					if(numFaces == 0){
						var d = {name:imageId, face:0, numFaces:0};
						console.log(d);
						socket.emit("FAWKES_STATE", d);
					}

				}else if(splices[0] == "processing"){
					// Processing next face
					console.log("fawkes processing face "+splices[2]);

					// Calculate image
					var image;
					var f = splices[2] - 1;
					for(var i=0; i<fawkes[userId].images.length; i++){
						console.log("f = "+f);
						if(f < files[fawkes[userId].images[i]].numFaces){
							image = fawkes[userId].images[i];
							console.log("image = "+image);
							break;
						}else{
							f -= files[fawkes[userId].images[i]].numFaces;

							// If didn't send it yet, send 'done' message for this image
							if(!fawkes[userId].imageSentDone[i]){
								fawkes[userId].imageSentDone[i] = true;

								var nf = files[fawkes[userId].images[i]].numFaces;
								var d = {name:fawkes[userId].images[i].substr(userId.length), face:nf+1, numFaces:nf};
								console.log(d);
								socket.emit("FAWKES_STATE", d);
							}
						}
					}

					// Update client states
					var d = {name:image.substr(userId.length), face:f+1, numFaces:files[image].numFaces};
					console.log(d);
					socket.emit("FAWKES_STATE", d);

				}

			}
			
		});
		process.stderr.setEncoding("utf8");
		process.stderr.on("data", function(data){
			// Received output from process
			console.log("fawkes stderr: "+data);
		});
		process.on("close", function(code){
			// Fawkes exited
			console.log("Fawkes exited with code "+code);

			// Check if success
			if(code == 0){
				// Create archive
				console.log("creating archive...");
				var archiveName = "fawkes-"+Date.now()+"-"+userId+".tar.gz";
				var child = exec(
					"mkdir fawkes/files/"+userId+"cloaked && "+
					"mv fawkes/files/"+userId+"/*_cloaked* fawkes/files/"+userId+"cloaked/ && "+
					"tar -czvf fawkes/files/"+archiveName+" -C fawkes/files/"+userId+"cloaked ."
					, (error, stdout, stderr) => {
					console.log("archive creation done: "+error+", "+stdout+", "+stderr);
					// Zip done
					if(error){
						socket.emit("FAWKES_DONE", {success:false, message:"Internal server error. Couldn't create archive."});
					}else{
						socket.emit("FAWKES_DONE", {success:true, downloadUrl:"/dl/"+archiveName});
					}
				});

			}else{
				// Error
				socket.emit("FAWKES_DONE", {success:false, message:"Internal server error. Fawkes exited with an exception."});
			}
		});

		// Update file states
		socket.emit("FAWKES_STARTED");
	});
});


// Start HTTP server
server.listen(port, hostname, () => {
	console.log("Server running at http://"+hostname+":"+port);
});




// Helper methods
function rand(min, max){
	return Math.floor(Math.random()*(max-min) + min);
}
function sha256(message){
	return crypto.createHash("sha256").update(message).digest("hex");
}




























