Initial commit

This commit is contained in:
David Westgate 2021-02-28 00:57:28 -08:00
commit 5e28e20b45
16 changed files with 4094 additions and 0 deletions

57
res/account.html Executable file
View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="./css/styles.css">
<script type="text/javascript" src="scripts/api.js" ></script>
<script type="text/javascript" src="scripts/view.js" ></script>
<script>
window.onload = function () {
list();
}
</script>
<title>Web Monitor Project</title>
</head>
<body>
<div class="header">Web Monitor</div>
<!--div class ="navbar">
<div class = "navbar_item">Navbar item</div>
</div-->
<div class = "main_container">
<div id = "active_monitors_label">Active Monitors</div>
<div class="button" onclick=list()>Refresh</div>
<table>
<thead>
<th>Name</th>
<th>URL</th>
<th>Status</th>
<th>Select</th>
</thead>
<tbody id="table_list_body">
</tbody>
</table>
<div class="button"onclick="deleteSelected()"> Delete Selected</div>
<hr />
<div >Add new Monitor</div>
<table >
<thead>
<th>Name</th>
<th>URL</th>
</thead>
<tbody>
<tr>
<td><input type="text" id= "new_name" name="Monitor Name"></td>
<td><input type="text" id="new_url" name = "Monitor URL"></td>
</tr>
</tbody>
</table>
<div class="button" style = "text-align: center;color:blue;" onclick="addMonitor()">Add </div><br>
<div class="button" style = "text-align: center;color:blue;" onclick="logout()">Logout </div>
<p>&nbsp;</p>
</div>
<div class="footer">
<div class = "license">2021 David Westgate</div>
</div>
</body>

146
res/css/styles.css Executable file
View File

@ -0,0 +1,146 @@
/*TAG Styles*/
body{
background-color: rgb(93, 95, 95);
font-family: Arial, Helvetica, sans-serif;
font-size:16px;
/*background-image: url("../images/background.jpg");*/
}
div{
}
table{
margin-left: auto;
margin-right: auto;
}
th{
}
td{
}
/*Class Styles*/
.header{
background-color: #F1F1F1;
text-align: center;
padding: 20px;
border-radius: 15px;
font-size: 200%;
font-weight: bolder;
}
.navbar {
overflow: hidden;
background-color: rgb(65, 17, 17);
border-radius: 4px;
display: block;
}
.navbar_item{
float: left;
display: inline;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
.navbar_item:hover{
background-color: #ddd;
color: black;
cursor: pointer;
}
.main_container{
text-align: center;
display: block;
}
.prompt{
color: #e6e3c8;
display: inline-block;
padding: 5px;
margin: 5px;
}
.field{
display: inline-block;
padding: 5px;
margin: 5px;
border-radius: 5px;
}
.footer{
display: block;
background-color:#F1F1F1;
text-align: center;
padding: 10px;
border-radius: 15px;
display: block;
margin-top: 10px;
position: absolute;
bottom: 0;
width: 95%;
}
.footer_item{
display: inline;
}
.button{
cursor: pointer;
color:blue;
margin:5px;
background-color: rgb(194, 194, 194);
border-radius: 5px;
display: inline-block;
padding: 5px;
}
.button:hover{
background-color: #ddd;
color: black;
cursor: pointer;
}
/* Tooltip text */
.tooltiptext {
visibility: hidden;
width: 120px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
top: -5px;
left: 105%;
/* Position the tooltip text - see examples below! */
position: absolute;
z-index: 1;
}
/* Show the tooltip text when you mouse over the tooltip container */
.td:hover .tooltiptext {
visibility: visible;
}
.tooltiptext::after {
content: " ";
position: absolute;
top: 50%;
right: 100%; /* To the left of the tooltip */
margin-top: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent black transparent transparent;
}
.form_container{
display: block;
text-align: center;
}
/*ID styles*/
#active_monitors_label{
font-size: 150%;
font-weight: bolder;
margin: 10px;
}

48
res/index.html Executable file
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link type="text/css" href="css/styles.css" rel="stylesheet"> </link>
<script type="text/javascript" src="scripts/api.js" ></script>
<script type="text/javascript" src="scripts/view.js" ></script>
<title>Web Monitor</title>
</head>
<body>
<div class="header">Web Monitor</div>
<!--div class ="navbar">
<div class = "navbar_item">Navbar item</div>
</div-->
<div class="main_container">
<div class="form_container">
<div class="prompt" >E-mail</div><input class="field" type="text" id="email">
</div>
<div class="form_container">
<div class="prompt">Password</div><input class="field" type="password" id="password">
</div>
<div class="form_container" id="confirm_password_container" style="visibility: hidden;" form_container>
<div class="prompt">Confirm Password</div><input class="field" type="password" id="confirm_password">
</div>
<div style="text-align: center;">
<div class="button" style="display: inline-block;" id= "login_button" onclick="login()">Login</div><br>
<div class="button" style="display: inline-block;"id= "show_registration_button" onclick="showreg()">New User? Register Here</div><br>
<div class = "button" style="display: none;"id="confirm_register_button" onclick="register()">Confirm Registration</div><br>
<div class = "button" style="display: none;"id="cancle_register_button" onclick="cancle()">Cancel Registration</div><br>
</div>
</div>
<div class="footer">
<div class = "footer_item" id = "license">2021 David Westgate</div>
</div>
</body>
</html>

BIN
res/res/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

2
res/robots.txt Executable file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

197
res/scripts/api.js Normal file
View File

@ -0,0 +1,197 @@
function deleteSelected(){
var deleteList = document.getElementsByClassName("deleteBox");
var resource = "/api/delete?"
var counter = 0;
for(var i =0; i < deleteList.length; ++i){
var checkbox = deleteList[i];
if(checkbox.checked != false){
if(counter > 0)
resource+="&";
resource +=('id='+checkbox.id);
counter++;
}
}
if(counter >0 ){
var xhttp = new XMLHttpRequest();
xhttp.open('DELETE', resource,true);
xhttp.send();
xhttp.onreadystatechange = function() {
if(this.readyState == 4){
list();
}
};
}
}
function addMonitor(){
var new_name = document.getElementById("new_name");
var new_url = document.getElementById("new_url");
if(!validURL(new_url.value))
alert("invalid URL");
else{
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if(this.readyState == 4){
if(200 <= this.status && this.status< 300){
list();
}
else{
alert("Error, URL invalid. Status: "+this.status);
}
}
};
xhttp.open("POST", "/api/newMonitor",true);
xhttp.send(JSON.stringify({"name": new_name.value, "url": new_url.value}));
}
}
function logout(){
document.cookie="ssid=0";
window.location.href = '/index.html';
}
function login(){
var email = document.getElementById("email").value;
var password = document.getElementById("password").value;
var obj = {"command":"login","method":"POST","email":email,"password":password};
if(validEmail(email) && password !== ""){
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4) {
if(200 <= this.status && this.status < 300){
window.location.href = '/account.html'
}
else{
alert("Error. Status: "+this.status);
}
}
};
xhttp.open(obj.method, "/api/"+obj.command, true);
//xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.send(JSON.stringify(obj));
}
else{
alert("Invalid username or password");
}
}
function register(){
var email = document.getElementById("email").value;
var password = document.getElementById("password").value;
var password_confirm = document.getElementById("confirm_password").value;
//Client side input checks
if(password != password_confirm){
alert("passwords do not match");
}
else if(!validEmail(email)){
alert("Invalid email address");
}
else if(!validPassword(password)){
alert("Invalid password: Must be 8 characters");
}
else{
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 ) {
if(200 <= this.status && this.status < 300){
alert("Success");
window.location.href = '/index.html';
}
else{
alert("Error. Status: "+this.status);
}
}
}
xhttp.open("POST", "/api/register", true);
xhttp.send(JSON.stringify({"email":email,"password":password}));
}
}
function list(){
var table = document.getElementById("table_list_body");
//table.innerHTML="<tr><td>Name</td><td>URL</td><td>Active?</td></tr>";
table.innerHTML="";
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4) {
if(200 <= this.status && this.status < 300){
var array = JSON.parse(xhttp.responseText);
for(var i = 0; i < array.length; ++i){
var next = array[i];
var row = document.createElement("tr");
var nameCell = document.createElement("td");
nameCell.innerText = next.name;
var urlCell = document.createElement("td");
urlCell.innerText = next.url.substr(0,25);
var activeCell = document.createElement("td");
var tooltiptext = document.createElement("span");
console.log(next.last_change);
if(next.last_change.length >3){
activeCell.onmouseover = function(){tooltiptext.style.visibility="visible"}
activeCell.innerText="Active"
activeCell.style.color="#90ee90";
}
else{
activeCell.innerText="Inactive"
activeCell.style.color="red";
activeCell.onmouseover= activeCell.style.cursor="pointer";
activeCell.onclick=function(){alert("URL may not support ETAG or last-modified headers");};
}
activeCell.style.fontWeight="bold";
var checkboxCell = document.createElement("td");
var checkbox = document.createElement("input");
checkbox.className="deleteBox";
checkbox.id= next.rowid;
checkbox.type="checkbox";
row.appendChild(nameCell);
row.appendChild(urlCell);
row.appendChild(activeCell);
checkboxCell.appendChild(checkbox);
row.appendChild(checkboxCell)
table.appendChild(row);
/*
table.innerHTML+=("<tr>"+
"<td>"+next.name+"</td>"+
"<td>"+next.url+"...</td>"+
"<td>"+next.notify+"</td>"+
'<td><input type="checkbox" class="deleteBox" id="'+next.rowid+'"></input></td>'+
"</tr>");*/
}
}
else{
alert("Error. Status: "+this.status);
window.location.href = '/index.html';
}
}
};
xhttp.open("GET", "/api/list", true);
xhttp.send();
}
function validEmail(email){
const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return regex.test(email);
}
function validPassword(password){
return (password.length >= 6)
}
function validName(){
}
function validURL(url){
const regex = /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
return regex.test(url);
}

33
res/scripts/view.js Normal file
View File

@ -0,0 +1,33 @@
function showreg(){
var show_registration_button = document.getElementById("show_registration_button");
show_registration_button.style.display = "none";
var confirm_register_button = document.getElementById("confirm_register_button");
confirm_register_button.style.display = "inline-block";
var cancle_register_button = document.getElementById("cancle_register_button");
cancle_register_button.style.display = "inline-block";
var confirm_password_container = document.getElementById("confirm_password_container");
confirm_password_container.style.visibility="visible";
var login_button = document.getElementById("login_button");
login_button.style.display="none";
}
function cancle(){
var show_registration_button = document.getElementById("show_registration_button");
show_registration_button.style.display = "inline-block";
var confirm_register_button = document.getElementById("confirm_register_button");
confirm_register_button.style.display = "none";
var cancle_register_button = document.getElementById("cancle_register_button");
cancle_register_button.style.display = "none";
var confirm_password_container = document.getElementById("confirm_password_container");
confirm_password_container.style.visibility="hidden";
var login_button = document.getElementById("login_button");
login_button.style.display="inline-block";
}

60
server/config.js Normal file
View File

@ -0,0 +1,60 @@
const branches ={
"development": {
"config_id": "development",
"hostname": "someserver.net",
"node_port": 8081,
"database": "./app-db-dev.db",
"ssl": false,
"key": "somesecretkey", //Static key for ease of debugging
"countries": ['US', 'CA', 'N/A'],
"algorithm" : 'aes-256-cbc',
"root": "/srv/webserver",
"cookie_timeout": 3600000,
"transporter":{
"host":"smtp.someserver.net",
"port":587,
"secure":false,
"auth": {
"user": "someone@someserver.net",
"pass": "abadpassword",
},
"tls":{
"rejectUnauthorized": false
},
"debug":false,
"authMethod":"LOGIN"
}
},
"production": {
"config_id": "production",
"hostname": "someserver.net",
"node_port": 8080,
"database": "./app-db-dev.db",
"ssl": false,
"key": Math.floor((Math.random() * Math.pow(10,20)) ), //Dynamic key on start for security
"countries": ['US', 'CA', 'N/A'],
"algorithm" : 'aes-256-cbc',
"root": "/srv/webserver",
"cookie_timeout": 3600000,
"transporter":{
"host":"smtp.someserver.net",
"port":587,
"secure":false,
"auth": {
"user": "someone@someserver.net",
"pass": "abadpassword",
},
"tls":{
"rejectUnauthorized": false
},
"debug":false,
"authMethod":"LOGIN"
}
}
}
//Set branch here
var config = branches.development;
module.exports = {
config
};

180
server/controller.js Executable file
View File

@ -0,0 +1,180 @@
"use strict";
const {createUser} = require('./model');
const {authenticate} = require('./model');
const {createMonitor} = require('./model');
const {getAllMonitors} = require('./model');
const {removeMonitors} = require('./model');
const { getPostData } = require('./utils');
const {encrypt} = require('./utils');
const {decrypt} = require('./utils');
const {getSSID} = require('./utils');
const {getLastModified} = require('./utils');
const {getETAG} = require('./utils');
const {getHeaders} = require('./utils');
//POST->request: Body->{email:email,password:password}
//POST->response(success): Body->{email:email}, code: 201
//POST->response(fail, already exists): Body->null, code: 409 TODO: more code/handling
//POST->response(fail, bad email address): Body->null, code: 409 TODO: more code/handling
//POST->response(fail, DB/other): Body->null, code: 500
function registerUser(request,response) {
//TODO encrypt password before storage
const body = getPostData(request);
body.then((jsonstring)=>{
var email = JSON.parse(jsonstring).email;
var password = JSON.parse(jsonstring).password;
let newUser = createUser(email,password);
newUser.then((res) =>{
response.writeHead(201, { 'Content-Type': 'application/json' })
response.end();
}).catch((err) =>{
response.writeHead(409, { 'Content-Type': 'application/json' })
response.end();
});
}).catch((err)=>{
response.writeHead(500, { 'Content-Type': 'application/json' })
response.end();
});
}
//POST->request: params->empty,Body->{email:email,password:password}
//POST->response(success): Body->{email:email}, code=200
//POST->response(fail, wrong user/pass): Body->{}, code=401
function loginUser(request,response){
//TODO decrypt password before validating
const body = getPostData(request);
body.then((jsonstring)=>{
var email = JSON.parse(jsonstring).email;
var password = JSON.parse(jsonstring).password;
let login = authenticate(email,password);
login.then((row) =>{
if(row.email != null && row.email == email){
console.log("then & match");
var date = new Date();
var session_id =""
session_id = session_id.concat(email , ';',request.socket.remoteAddress,';', date.getTime());
var encrypted = encrypt(session_id);
response.writeHead(200, { 'Content-Type': 'application/json',
'Set-Cookie': 'ssid='+encrypted+';SameSite=Lax;Path=/;Expires=' +new Date(date.getTime()+3600000).toGMTString()});
response.end(JSON.stringify(row));
}
else{
//handle null case
}
}).catch((err) =>{
console.log("catch, login dbBB");
response.writeHead(409, { 'Content-Type': 'application/json' })
response.end(JSON.stringify({'message':err}));
});
}).catch((err)=>{
console.log("catch, body");
response.writeHead(500, { 'Content-Type': 'application/json' })
response.end(JSON.stringify({'message':err}));
});
}
//GET->request: params->email=email Body->null cookie->ssid
//GET->response (success): Body->[{monitor1},{monitor2},...] code=200
//GET->response (fail, bad cookie auth): Body->{} code=401
function listMonitors(request, response){
var ssid = getSSID(request);
if(ssid.email == undefined){
response.writeHead(400, { 'Content-Type': 'application/json' })
response.end();
}
else{
var email = ssid.email;
const list = getAllMonitors(email);
list.then((rows) =>{
response.writeHead(200, { 'Content-Type': 'application/json' })
response.end(JSON.stringify(rows));
}).catch((err) =>{
console.log("Nrows")
console.log(err);
response.writeHead(401, { 'Content-Type': 'application/json' })
response.end();
});
}
}
//DELETE->request: params->email=email&mid=mid cookie->ssid
//DELETE->response (success): body->{mid:mid}: code 200
//DELETE->response (fail, bad/no resource): Body->{} code 404
//DELETE->response (fail, bad cookie/auth): Body->{} code: 401
function deleteMonitors(request,response, idArray){
var ssid = getSSID(request);
if(ssid.email == undefined){
console.log("undefined");
response.writeHead(401, { 'Content-Type': 'application/json' })
response.end();
}
else{
console.log(idArray);
const remove = removeMonitors(idArray);
remove.then(()=>{ //TODO update resolve with ids of removed data
response.writeHead(200, { 'Content-Type': 'application/json' })
response.end(); //TODO include removed ids in response
}).catch((err)=>{
response.writeHead(401, { 'Content-Type': 'application/json' })
response.end();
});
}
}
//POST->request: params->email=email,name=name,url=url, body->null cookie->ssid
//POST->response (success) body->{name:name,url:url}: code 202
//POST->response (fail, bad url) body->{}: code 400
//POST->response (fail, DB error): body={}: code 500
//POST->response (fail, cookie/auth): body={}: code 401
async function newMonitor(request, response){
var ssid = getSSID(request);
if(ssid.email == undefined){
response.writeHead(401, { 'Content-Type': 'application/json' })
response.end();
}
else{
const bodyPromise = getPostData(request);
bodyPromise.then((data)=>{
var name = JSON.parse(data).name;
var url = JSON.parse(data).url;
const headersPromise = getHeaders(url);
headersPromise.then((headers)=>{
var etag = getETAG(headers);
var last_modified = getLastModified(headers);
last_modified = last_modified +';'+etag;
//if(last_modified == ';')
// last_modified= "Update data or resource unavalible"
const monitorPromise = createMonitor(name,url, ssid.email, last_modified);
monitorPromise.then(()=>{ //TODO row inserted, refere to here
response.writeHead(202, { 'Content-Type': 'application/json' })
response.end();//TODO respond with row inserted
}).catch(()=>{
response.writeHead(500, { 'Content-Type': 'application/json' })
response.end();
});
}).catch((err)=>{
response.writeHead(400, { 'Content-Type': 'application/json' })
response.end(err);
}); //TODO add catch for headers promise
}).catch(()=>{
response.writeHead(400, { 'Content-Type': 'application/json' })
response.end();
});
}
}
module.exports = {
registerUser,
loginUser ,
listMonitors,
newMonitor,
deleteMonitors
}

30
server/mailer.js Normal file
View File

@ -0,0 +1,30 @@
var nodemailer = require("nodemailer");
const {config} = require('./config');
class Mailer{
constructor() {
this.transporter = nodemailer.createTransport(config.transporter);
}
async monitorUpdateMail(useremail, monitorname, monitorurl){
var subjecttext = "New website update for "+monitorname;
var bodytext = "Hello "+useremail+", your monitored website "+monitorname+ " has been updated at "+monitorurl;
var bodytextHTML = "<p>"+bodytext+"</p>";
await this.transporter.sendMail({
from: '"Web Monitor Service" <noreply@'+config.hostname+'>', // sender address
to: useremail, // list of receivers
subject: subjecttext, // Subject line
text: bodytext, // plain text body
html: bodytextHTML, // html body
});
}
registerConfirmMail(useremail){
//TODO
}
}
module.exports = {
Mailer: Mailer
}

276
server/model.js Executable file
View File

@ -0,0 +1,276 @@
const sqlite3 = require('sqlite3').verbose();
const {config} = require('./config');
//success->resolve(row)
//fail-> reject(err)
function authenticate(email,password){
return new Promise((resolve,reject) =>{
let db = new sqlite3.Database(config.database, (err) => {
if (err) {
reject(err.message);
}
});
var sql = 'SELECT email, password FROM users WHERE email=(?) AND password=(?)';
var params = [email,password];
db.get(sql, params, (err,row)=>{
if(err){
//console.error(err.message);
reject(err.message);
}
if(row){
resolve(row);
}
else{
reject('email not found');
}
});
db.close((err)=>{
if(err){
reject(err.message);
}
});
});
}
//success->resolve(row)
//fail-> reject(err)
function createUser(email,password){
return new Promise((resolve, reject) => {
let db = new sqlite3.Database(config.database, (err) => {
if (err) {
reject (err.message);
}
});
db.serialize( () => {
db.run('CREATE TABLE IF NOT EXISTS users(email TEXT NOT NULL PRIMARY KEY, password TEXT NOT NULL)', (err) => {
if (err) {
reject (err.message);
}
});
var sql = 'INSERT INTO users (email, password) VALUES (?,?)';
var params = [email, password];
db.run(sql, params, (err) => {
if (err) {
reject (err.message);
}
else{
resolve(email); //TODO resolve with entire ROW
}
});
});
db.close((err) => {
if (err) {
reject (err.message);
}
});
})
}
//success->resolve(row)
//fail-> reject(err)
function updateLastModified(rowid, last_change){
return new Promise((resolve,reject) =>{
//console.log("data in model is "+data);
let db = new sqlite3.Database(config.database, (err) => {
if (err) {
reject(err.message);
}
});
var sql = 'UPDATE monitors SET last_change = (?) WHERE rowid=(?)';
var params = [last_change, rowid];
db.run(sql, params, (err) => {
if (err) {
reject(err.message);
}
else{
resolve(); //TODO resolve entire row
}
});
db.close((err) => {
if (err) {
reject(err.message);
}
});
});
}
//success->resolve(row.last_change)
//fail-> reject(err.message)
function getLastModified(rowid){
return new Promise((resolve,reject)=>{
let db = new sqlite3.Database(config.database, (err) => {
if (err) {
resolve({accepted:false, message:err.message});
}
});
var sql = 'SELECT last_change FROM monitors WHERE rowid=(?)';
var params = [rowid];
db.get(sql,params, (err,row) =>{
if(err){
reject(err.mesasage);
}
else if(row){
resolve(row.last_change);
}
else{
reject("row not found")
}
});
});
}
//success->resolve(row)
//fail-> reject(err.message)
function createMonitor(name, url, email, last_modified){
return new Promise((resolve, reject) =>{
let db = new sqlite3.Database(config.database, (err) => {
if (err) {
reject(err.message);
}
});
db.serialize( () => {
var sql = 'INSERT INTO monitors (email, name, url, notify, last_change) VALUES (?,?,?,?,?)';
var params = [email, name, url,1,last_modified];
db.run(sql, params, (err) => {
if (err) {
reject(err.message);
}
else{
resolve(); //TODO update query to return row inserted
}
});
});
db.close((err) => {
if (err) {
resolve({accepted:false, message:err.message});
}
});
});
}
//success->resolve(row)
//fail-> reject(err.message)
function removeMonitors(rowids){
return new Promise((resolve,reject) =>{
let db = new sqlite3.Database(config.database, (err) => {
if (err) {
reject(err.message);
}
});
var sql = 'DELETE FROM monitors WHERE rowid in (?';
for(var i = 1; i < rowids.length; ++i)
sql+=',?';
sql+=')';
//console.log("sql is "+sql);
var params = rowids;
db.run(sql,params, function(err){
if(err){
reject(err.message)
}
else{
resolve(); //TODO resolve with reference to removed rows
}
});
});
}
//success->resolve(rows)
//fail-> reject(err.message)
function getAllMonitors(email){
return new Promise((resolve, reject) =>{
let db = new sqlite3.Database(config.database, (err) => {
if (err) {
reject(err.message);
}
});
db.serialize( () => {
db.run('CREATE TABLE IF NOT EXISTS monitors('
+'email TEXT NOT NULL'
+',name TEXT NOT NULL'
+',url TEXT NOT NULL'
+',notify INTEGER NOT NULL'
+',last_change INTEGER'
+',last_check INTEGER'
+',check_frequency INTEGER'
+',page_data BLOB'
+',FOREIGN KEY(email) REFERENCES users(email)'
+')', (err) => {
if (err) {
reject(err.message);
}
});
var sql = 'SELECT rowid,email,name,url,notify,last_change,last_check FROM monitors WHERE email=(?)';
var params = [email];
db.all(sql, params, (err,rows)=>{
if(err){
reject(err.message);
}
if(rows){
resolve(rows);
}
else{
resolve();
}
});
});
db.close((err)=>{
if(err){
reject(err.message);
}
});
});
}
//success->resolve(rows)
//fail-> reject(err.message)
function getAllMonitorsGlobal(){
return new Promise((resolve, reject) =>{
let db = new sqlite3.Database(config.database, (err) => {
if (err) {
reject(err.message);
}
});
var sql = 'SELECT rowid,email,name,url,notify,last_change,last_check FROM monitors';
db.all(sql, [], (err,rows)=>{
if(err){
reject(err.message);
}
if(rows){
resolve(rows);
}
else{
reject(err.message);
}
});
db.close((err)=>{
if(err){
reject(err.message);
}
});
});
}
module.exports={
createUser,
authenticate,
getAllMonitors,
createMonitor,
removeMonitors,
getLastModified,
updateLastModified,
getAllMonitorsGlobal
}

2728
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

13
server/package.json Normal file
View File

@ -0,0 +1,13 @@
{
"dependencies": {
"crypto-js": "^4.0.0",
"file-type": "^16.2.0",
"geoip-country": "^4.0.56",
"mime": "^2.5.2",
"node-cron": "^2.0.3",
"node-fetch": "^2.6.1",
"nodemailer": "^6.4.16",
"smtp-server": "^3.8.0",
"sqlite3": "^5.0.2"
}
}

49
server/scheduler.js Normal file
View File

@ -0,0 +1,49 @@
var cron = require('node-cron');
var {getAllMonitorsGlobal} = require('./model');
const {getLastModified} = require('./utils');
const {getETAG} = require('./utils');
const {getHeaders} = require('./utils');
const {updateLastModified} = require('./model');
const {Mailer} = require('./mailer');
const {config} = require('./config');
//let Mailer = mailer.Mailer;
async function scheduler(){
var mailer = new Mailer();
//TEST: mailer.monitorUpdateMail('davidjwestgate@gmail.com','some_name','some_url');
//TODO move cron schedule to config file
var sched = cron.schedule('0 0 * * * *', async() => {
console.log('running a task every minute');
var allGlobalMonitorsPromise = getAllMonitorsGlobal();
allGlobalMonitorsPromise.then(async(rows)=>{
for(var i = 0; i < rows.length; ++i){
var headersPromise = await getHeaders(rows[i].url); //TODO handle resolve/reject
var last_change_db = rows[i].last_change;
// console.log('last_change_db: '+last_change_db)
var last_modified_live = getLastModified(headersPromise)+';'+getETAG(headersPromise);
//console.log('last_modified_live: '+last_modified_live)
if(last_change_db == last_modified_live){
console.log("no change for "+rows[i].name);
}
else{
console.log("change for "+rows[i].name);
var updateLastModifiedPromise = await updateLastModified(rows[i].rowid,last_modified_live);
updateLastModifiedPromise.finally(()=>{
//TODO: Set email_Are_live in config file
// mailer.monitorUpdateMail(rows[i].email,rows[i].name,rows[i].url);
});
}
}
}).catch((err)=>{
console.log(err);
});
});
}
module.exports={
scheduler
}

77
server/server.js Executable file
View File

@ -0,0 +1,77 @@
var http = require('http');
var readline = require('readline');
const {config} = require('./config');
const {check_whitelist} = require('./utils');
const {serve_resource} = require('./utils');
const {registerUser} = require('./controller');
const {loginUser} = require('./controller');
const {listMonitors} = require('./controller');
const {newMonitor} = require('./controller');
const {deleteMonitors} = require('./controller');
const {scheduler} = require('./scheduler');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
//Main server listener
function server_listener(request, response){
var ip = request.socket.remoteAddress;
var whitelist = {'stop':false, 'country':'N/A', 'ip':ip};
check_whitelist(whitelist);
//whitelist banned
if(whitelist.stop){
//console.log('Denied Request from '+whitelist.country + ' ip: '+request.connection.remoteAddress +' res: '+ request.url);
response.end('Acess Denied');
}
//Allowed users
else{
var url = new URL(request.url,`http://${request.headers.host}`);
var path = url.pathname
var params = new URLSearchParams(url.searchParams);
//API calls
if(path.startsWith("/api")){
switch(request.socket.parser.incoming.method){
case "GET":
if(path.endsWith("/list"))
listMonitors(request,response);
else if(path.endsWith("/logout"))
logoutUser(request,response);
break;
case "POST":
if(path.endsWith("/register"))
registerUser(request,response);
else if(path.endsWith("/login"))
loginUser(request,response);
else if(path.endsWith("/newMonitor"))
newMonitor(request,response);
break;
case "PUT":
break;
case "DELETE":
if(path.endsWith("/delete")){
var idArray = JSON.parse('['+params.getAll('id')+']');
deleteMonitors(request,response,idArray);
}
break;
}
}
//Resource requests
else{
serve_resource(request,response,whitelist);
}
}
}
scheduler();
var server = http.createServer(server_listener);
server.listen(config.node_port);
rl.question("Server running (Enter to stop)\n",(answer) =>{
server.close();
process.exit(1);
});

198
server/utils.js Executable file
View File

@ -0,0 +1,198 @@
const geoip = require('geoip-country');
const fs = require('fs');
const mime = require('mime');
const CryptoJS = require("crypto-js");
const http = require('https');
const {config} = require('./config');
function getLastModified(headers){
var last = headers['last-modified']
if(last)
return last;
else
return '';
}
function getETAG(headers){
var etag = headers.etag
if(etag)
return etag
else
return '';
}
function getHeaders(url){
return new Promise((resolve,reject)=>{
try{
var request = http.request(url, {method: 'HEAD'}, res =>{
//console.log(res);
resolve(res.headers);
});
request.end();
}
catch(err){
reject(err.message);
}
});
}
function validEmail(email){
const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return email.test(regex);
}
function validName(name){
}
function validURL(url){
const regex = /^((https?):\/\/)?([w|W]{3}\.)+[a-zA-Z0-9\-\.]{3,}\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?$/;
return regex.test(url);
}
function parseCookies (request) {
var cookies = {};
if(request.headers && request.headers.cookie){
request.headers && request.headers.cookie.split(';').forEach(function(cookie) {
var parts = cookie.match(/(.*?)=(.*)$/)
cookies[ parts[1].trim() ] = (parts[2] || '').trim();
});
}
return cookies;
}
//Return true if cookie exists, and decrypted SSID is valid
function getSSID(request){
var cookies = parseCookies(request);
if(cookies.ssid != undefined && cookies.ssid != null)
var encrypted = cookies.ssid.toString();
else
return {};
if(encrypted != null && encrypted != undefined && encrypted != ""){
//console.log("encrypted ssid cookie is " +encrypted);
var decrpyted = decrypt(encrypted);
//console.log('decrypted is '+decrpyted);
var userdata = decrpyted.split(';');
var email = userdata[0];
var ip = userdata[1];
var date = userdata[2];
if(ip != request.socket.remoteAddress){
return {};
}
else if( date > (new Date().getTime+config.cookie_timeout)){
return {};
}
else{
return {"email":email,"ip":ip,"date":date};
}
}
else{
return {};
}
}
//Check to see if request IP is in allowed region
function check_whitelist(whitelist){
var geo = geoip.lookup(whitelist.ip);
if(geo != null)
whitelist.country = geo.country;
//Country whitelist
if(!config.countries.includes(whitelist.country))
whitelist.stop = true;
}
//Serve a directory listing in HTML when a folder requested with no index.
function response_write_dir(request, response ,systemPath){
response.writeHead(200, { 'Content-Type': 'text/html' });
var list = fs.readdirSync(systemPath);
response.write('<html><body>');
for(var i = 0; i < list.length; ++i){
response.write('<a href='+request.url+'/'+list[i]+'>'+list[i]+'</a><br>');
}
response.write('</body></html>');
}
function serve_resource(request, response, whitelist){
var url = new URL(request.url,`http://${request.headers.host}`);
var systemPath = config.root + url.pathname;
var resType = (url.pathname.includes('.')) ? 'file':'dir';
var contentType = mime.getType(url.pathname);
//Remove trailing '/'s
while(systemPath.endsWith('/')){
systemPath = systemPath.substr(0,systemPath.length-1);
}
//Log for requests
console.log('Request from '+request.socket.remoteAddress+':'+request.socket.remotePort+', '
+request.socket.remoteFamily +'\nFor resource '+url.pathname+'\nCountry: '+whitelist.country+'\tMethod: '
+ request.socket.parser.incoming.method+'\n');
//If navigating to directory and index resource exists, serve index.html
if(resType == 'dir' && fs.existsSync(systemPath+'/index.html'))
systemPath = systemPath+'/index.html';
fs.readFile(systemPath, function(err, content) {
if (err) {
if(err.code == 'ENOENT'){
response.writeHead(404, { 'Content-Type': 'text/html' });
response.write('<html><body>Error: Resrouce not found</body></html>');
response.end(null, 'utf-8');
}
else {//If reading a directory with no index.html, serve file list in HTML
response_write_dir(request,response,systemPath);
response.end(null, 'utf-8');
}
}
else {
response.writeHead(200, { 'Content-Type': contentType });
response.end(content, 'utf-8');
}
});
}
function getPostData(request) {
return new Promise((resolve, reject) => {
try {
let body = ''
request.on('data', (chunk) => {
body += chunk.toString()
})
request.on('end', () => {
resolve(body)
})
} catch (err) {
reject(err.message)
}
})
}
function encrypt(string){
return CryptoJS.AES.encrypt(string,config.key).toString();
}
function decrypt(string){
return CryptoJS.AES.decrypt(string, config.key).toString(CryptoJS.enc.Utf8);
}
module.exports = {
check_whitelist,
serve_resource,
getPostData,
encrypt,
decrypt,
getSSID,
validEmail,
validName,
validURL,
getHeaders,
getLastModified,
getETAG
}