From e11c77026fb58985093e7edd01e540521295aa46 Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Fri, 23 Mar 2018 16:33:57 -0600 Subject: [PATCH] First commit --- .gitignore | 1 + README.md | 12 ++ cloud/README.md | 3 + configs/README.md | 3 + configs/cjdroute.conf | 384 ++++++++++++++++++++++++++++++++++++++ scripts/README.md | 3 + scripts/status | 105 +++++++++++ www/README.md | 3 + www/api.php | 46 +++++ www/lib/login.php | 71 +++++++ www/required.php | 305 ++++++++++++++++++++++++++++++ www/settings.template.php | 15 ++ 12 files changed, 951 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cloud/README.md create mode 100644 configs/README.md create mode 100644 configs/cjdroute.conf create mode 100644 scripts/README.md create mode 100644 scripts/status create mode 100644 www/README.md create mode 100644 www/api.php create mode 100644 www/lib/login.php create mode 100644 www/required.php create mode 100644 www/settings.template.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..397a30c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*settings.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..0db778d --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Netsyms Business Accelerator + +The Business Accelerator is a physical network device for installation at a small business. It helps combine the speed and offline benefits of local services with the reliability, availability, and ease of managed cloud services. + +## About + +The Business Accelerator is based on the Raspberry Pi 3 B+, running an image based on Raspbian Stretch Lite. This repository contains notable modifications to the base image. + +Modifications include: + +- Using [CJDNS](https://source.netsyms.com/Netsyms/prototype-cjdns-pi) to bypass NAT for secure cloud sync and remote tech support +- Installing NGINX, PHP 7.0, and SQLite 3 for serving content \ No newline at end of file diff --git a/cloud/README.md b/cloud/README.md new file mode 100644 index 0000000..d2d5c81 --- /dev/null +++ b/cloud/README.md @@ -0,0 +1,3 @@ +# Cloud Web Content + +This folder contains files for running on the Business Apps cloud server. \ No newline at end of file diff --git a/configs/README.md b/configs/README.md new file mode 100644 index 0000000..f000d42 --- /dev/null +++ b/configs/README.md @@ -0,0 +1,3 @@ +# Configuration Files + +This folder contains Debian configuration files with notable changes from common defaults. \ No newline at end of file diff --git a/configs/cjdroute.conf b/configs/cjdroute.conf new file mode 100644 index 0000000..9102a42 --- /dev/null +++ b/configs/cjdroute.conf @@ -0,0 +1,384 @@ +{ + // Private key: + // Your confidentiality and data integrity depend on this key, keep it secret! + "privateKey": "<<>>", + // This key corresponds to the public key and ipv6 address: + "publicKey": "<<>>", + "ipv6": "<<>>", + "authorizedPasswords": + [ + {"password": "<<>>", "user": "default-login"} + ], + + "admin": + { + "bind": "127.0.0.1:11234", + "password": "NONE" + }, + + // Interfaces to connect to the switch core. + "interfaces": + { + // The interface which connects over UDP/IP based VPN tunnel. + "UDPInterface": + [ + { + // Bind to this port. + "bind": "0.0.0.0:2085", + // Set the DSCP value for Qos. Default is 0. + // "dscp": 46, + + // Nodes to connect to (IPv4 only). + "connectTo": + { + "92.241.12.189:22569": { + "password": "6mDHySCSJYVgyJqphpgnokqKrCq045mF", + "publicKey": "9qz459vnkb1v36ypq84m29g2q7dn8gndg9bh0w1499urnkx9nmt0.k", + "peerName": "h.start-com.ru", + "Contact": "vvk@start-com.ru" + }, + "173.208.215.53:6129": { + "login": "public", + "password":"wvmrnb31sgtw03d73buyjybwr6vrw1z", + "publicKey":"p2r0nst699p2gc9cztcmkr5z05522h51jvj5xw7z4x135cz4j9p0.k", + "peerName":"Don't Sell.Me" + }, + "198.199.124.143:27313": { + "contact": "kasm@kasm.eu", + "location": "Europe/Amsterdam", + "login": "public-peer", + "password": "9md1m1sntffbrkq1brb85rw312fr5gb", + "peerName": "h.kasm.eu", + "publicKey": "g34d0nbyltxj3sqnc0t5gps32ks4tyl3m9w9qzpk832f9dmp6fh0.k" + }, + "192.169.7.142:14400":{ + "contact":"Igel@hyperboria.ca", + "gpg":"A84DFFE62B451511", + "password":"alfa-charlie-alfa-bravo", + "peerName":"igel-losangeles", + "publicKey":"mh9m0411cfcg7xhdc8n6ckls1tjgnvvbdfzdgqf5196tfkw96rr0.k" + }, + "138.68.245.159:50505":{ + "contact":"chapman.shoop@riseup.net", + "login":"public-peer", + "password":"7ztkh2m3p97z0fcyn50wmtx863n6b3j", + "peerName":"salesforce-tower", + "publicKey":"6d2kt2hbcp7v0pw9q6f1u2s039kfnt4m4123rjxg26hsgrc12v80.k" + }, + "162.254.117.11:8351":{ + "contact":"cornfeedhobo@fuzzlabs.org", + "gpg":"6610FE2B6BD98C42", + "location":"Chicago, IL, USA", + "login":"public-access", + "password":"39XBepVM7TxGWAoROgUzixlbAnkh7WRY", + "peerName":"vishnu", + "publicKey":"dhndkly9mhmckbcrb1rgl051ty9fg5zq0tmnms8wxns08fu7kvv0.k" + }, + "192.34.85.155:2359":{ + "contact":"Igel@hyperboria.ca", + "gpg":"A84DFFE62B451511", + "password":"alfa-charlie-alfa-bravo", + "peerName":"igel-boston", + "publicKey":"rdxg1nzvmjdj4fyguqydmnl659p7m3x26r6un4ql966q4xt988j0.k" + }, + "104.200.29.163:53053":{ + "contact":"ansuz@transitiontech.ca", + "gpg":"024A7C03E67ED8CF", + "password":"cLjDBorhsYJUmJrESGueHsRY4HXcFyj", + "peerName":"transitiontech", + "publicKey":"1941p5k8qqvj17vjrkb9z97wscvtgc1vp8pv1huk5120cu42ytt0.k" + }, + "107.170.57.34:63472":{ + "contact":"code@ventricle.us", + "gpg":"7FE895160E3314027CD3B5D37392CF088BB4345C", + "location":"digitalocean nyc2", + "login":"public-peer", + "password":"ppm6j89mgvss7uvtntcd9scy6166mwb", + "peerName":"cord.ventricle.us", + "publicKey":"1xkf13m9r9h502yuffsq1cg13s5648bpxrtf2c3xcq1mlj893s90.k" + }, + "185.140.54.73:30800":{ + "contact":"iczero4@gmail.com", + "gpg":"613CE9DA0E9A3F70EC97760E4BAC4EBB8461FC7E", + "login":"public", + "password":"fwlmbx2f3udkd0ymknq4pwwgu2bjklx", + "peerName":"ic2.hellomouse.cf", + "publicKey":"c15sfmskdpmj2qw5lfvgfuzggyyk1bjzj4lu3yf6h1x2ckclwdd0.k" + }, + "173.62.245.186:55249":{ + "contact":"natebrune@gmail.com", + "country":"us", + "gpg":"C95CE6BC6735BAD7", + "ipv6":"fcda:9958:9093:49f2:2677:6df6:2a5a:b01d", + "password":"Public", + "peerName":"NAT", + "publicKey":"vgxqyputh4ldhxktg9msmr61pw938l0ymhkmryljsyzvmr0dtwy0.k", + "website":"https://github.com/NateBrune" + }, + "198.58.100.240:22237":{ + "contact":"jhj@trnsz.com", + "login":"default-login", + "password":"pqr5brz16vzzu6vhjuj7tv3n078kr5f", + "peerName":"trnsz", + "publicKey":"ubbtkp0txwjh44v8kkznvhjqqwr1hd2jzv5ms9zlkfk25svxvtg0.k" + }, + "149.56.98.167:3703":{ + "contact":"code@ventricle.us", + "gpg":"7FE895160E3314027CD3B5D37392CF088BB4345C", + "location":"ovh beauharnois", + "login":"public-peer", + "password":"ppm6j89mgvss7uvtntcd9scy6166mwb", + "peerName":"larynx.ventricle.us", + "publicKey":"jg035j9hup776kwz1k4n0bwpggxp1qmts6t715x53g8vutxktzz0.k" + }, + "165.227.44.84:34838":{ + "contact":"wattersm@watters.ws", + "gpg":"E2A3328281D1DA0A08D34FC2058F0C51586CA8C6", + "location":"Digital Ocean tor1", + "login":"public-access", + "password":"8n2w2qu2lfndhgx8xwgp18vyq7fhvux", + "peerName":"linux1.tor1.watters.ws", + "publicKey":"b465hml7z3g1vj22ktqdrc3z17mwjxl44cg0mj903n9vycxzqpv0.k" + }, + "149.56.19.79:55159":{ + "contact":"infrastructure@stashcrypto.com", + "login":"default-login", + "password":"dgv86ktpblc2h4y93fsqpshcg2lbp5d", + "peerName":"git.stashcrypto.net", + "publicKey":"zbfurpx9n6whzwu6vrlfgmw8g56rmchfmhxxtpg0hwhl84vqf1y0.k" + }, + "158.69.119.35:9218":{ + "contact":"infrastructure@stashcrypto.com", + "login":"default-login", + "password":"w5huch4mn6tkgfp3j9sr8p8r13j3j33", + "peerName":"seed.stashcrypto.net", + "publicKey":"rzg61b3fsb675732g5rn8g1x61ypm1z7402n072qmrbbhgzm93f0.k" + }, + } + }, + { + // Bind to this port. + "bind": "[::]:2085", + // Set the DSCP value for Qos. Default is 0. + // "dscp": 46, + + // Nodes to connect to (IPv6 only). + "connectTo": + { + "[2602:ff65:0:1::fc00]:2359":{ + "contact":"Igel@hyperboria.ca", + "gpg":"A84DFFE62B451511", + "password":"alfa-charlie-alfa-bravo", + "peerName":"igel-boston", + "publicKey":"rdxg1nzvmjdj4fyguqydmnl659p7m3x26r6un4ql966q4xt988j0.k" + }, + "[2604:a880:0:1010::f:4001]:63472":{ + "contact":"code@ventricle.us", + "gpg":"7FE895160E3314027CD3B5D37392CF088BB4345C", + "location":"digitalocean nyc2", + "login":"public-peer", + "password":"ppm6j89mgvss7uvtntcd9scy6166mwb", + "peerName":"cord.ventricle.us", + "publicKey":"1xkf13m9r9h502yuffsq1cg13s5648bpxrtf2c3xcq1mlj893s90.k" + }, + "[2a05:dfc7:dfc8:1d3::1]:30800":{ + "contact":"iczero4@gmail.com", + "gpg":"613CE9DA0E9A3F70EC97760E4BAC4EBB8461FC7E", + "login":"public", + "password":"fwlmbx2f3udkd0ymknq4pwwgu2bjklx", + "peerName":"ic2.hellomouse.cf", + "publicKey":"c15sfmskdpmj2qw5lfvgfuzggyyk1bjzj4lu3yf6h1x2ckclwdd0.k" + }, + "[2607:5300:61:44f::]:55159":{ + "contact":"infrastructure@stashcrypto.com", + "login":"default-login", + "password":"dgv86ktpblc2h4y93fsqpshcg2lbp5d", + "peerName":"git.stashcrypto.net", + "publicKey":"zbfurpx9n6whzwu6vrlfgmw8g56rmchfmhxxtpg0hwhl84vqf1y0.k" + } + } + } + ] +, + "ETHInterface": + [ + // Alternatively bind to just one device and either beacon and/or + // connect to a specified MAC address + { + // Bind to this device (interface name, not MAC) + // "all" is a pseudo-name which will try to connect to all devices. + "bind": "all", + + // Auto-connect to other cjdns nodes on the same network. + // Options: + // + // 0 -- Disabled. + // + // 1 -- Accept beacons, this will cause cjdns to accept incoming + // beacon messages and try connecting to the sender. + // + // 2 -- Accept and send beacons, this will cause cjdns to broadcast + // messages on the local network which contain a randomly + // generated per-session password, other nodes which have this + // set to 1 or 2 will hear the beacon messages and connect + // automatically. + // + "beacon": 2, + + // Node(s) to connect to manually + // Note: does not work with "all" pseudo-device-name + "connectTo": + { + // Credentials for connecting look similar to UDP credentials + // except they begin with the mac address, for example: + // "01:02:03:04:05:06":{"password":"a","publicKey":"b"} + } + } + ] + + }, + + // Configuration for the router. + "router": + { + // supernodes, if none are specified they'll be taken from your peers + "supernodes": [ + //"6743gf5tw80ExampleExampleExampleExamplevlyb23zfnuzv0.k", + ] + + // The interface which is used for connecting to the cjdns network. + "interface": + { + // The type of interface (only TUNInterface is supported for now) + "type": "TUNInterface" + // The type of tunfd (only "android" for now) + // If "android" here, the tunDevice should be used as the pipe path + // to transfer the tun file description. + // "tunfd" : "android" + + // The name of a persistent TUN device to use. + // This for starting cjdroute as its own user. + // *MOST USERS DON'T NEED THIS* + //"tunDevice": "tun0" + }, + + // System for tunneling IPv4 and ICANN IPv6 through cjdns. + // This is using the cjdns switch layer as a VPN carrier. + "ipTunnel": + { + // Nodes allowed to connect to us. + // When a node with the given public key connects, give them the + // ip4 and/or ip6 addresses listed. + "allowedConnections": + [ + // Give the client an address on 192.168.1.0/24, and an address + // it thinks has all of IPv6 behind it. + // ip4Prefix is the set of addresses which are routable from the tun + // for example, if you're advertizing a VPN into a company network + // which exists in 10.123.45.0/24 space, ip4Prefix should be 24 + // default is 32 for ipv4 and 128 for ipv6 + // so by default it will not install a route + // ip4Alloc is the block of addresses which are allocated to the + // for example if you want to issue 4 addresses to the client, those + // being 192.168.123.0 to 192.168.123.3, you would set this to 30 + // default is 32 for ipv4 and 128 for ipv6 (1 address) + // { + // "publicKey": "f64hfl7c4uxt6krmhPutTheRealAddressOfANodeHere7kfm5m0.k", + // "ip4Address": "192.168.1.24", + // "ip4Prefix": 0, + // "ip4Alloc": 32, + // "ip6Address": "2001:123:ab::10", + // "ip6Prefix": 0 + // "ip6Alloc": 64, + // }, + + // It's ok to only specify one address and prefix/alloc are optional. + // { + // "publicKey": "ydq8csdk8p8ThisIsJustAnExampleAddresstxuyqdf27hvn2z0.k", + // "ip4Address": "192.168.1.25", + // "ip4Prefix": 0, + // } + ], + + "outgoingConnections": + [ + // Connect to one or more machines and ask them for IP addresses. + // "6743gf5tw80ExampleExampleExampleExamplevlyb23zfnuzv0.k", + // "pw9tfmr8pcrExampleExampleExampleExample8rhg1pgwpwf80.k", + // "g91lxyxhq0kExampleExampleExampleExample6t0mknuhw75l0.k" + ] + } + }, + + // Dropping permissions. + // In the event of a serious security exploit in cjdns, leak of confidential + // network traffic and/or keys is highly likely but the following rules are + // designed to prevent the attack from spreading to the system on which cjdns + // is running. + // Counter-intuitively, cjdns is *more* secure if it is started as root because + // non-root users do not have permission to use chroot or change usernames, + // limiting the effectiveness of the mitigations herein. + "security": + [ + // Change the user id to sandbox the cjdns process after it starts. + // If keepNetAdmin is set to 0, IPTunnel will be unable to set IP addresses + // and ETHInterface will be unable to hot-add new interfaces + // Use { "setuser": 0 } to disable. + // Default: enabled with keepNetAdmin + { "setuser": "nobody", "keepNetAdmin": 1 }, + + // Chroot changes the filesystem root directory which cjdns sees, blocking it + // from accessing files outside of the chroot sandbox, if the user does not + // have permission to use chroot(), this will fail quietly. + // Use { "chroot": 0 } to disable. + // Default: enabled (using "/var/run") + { "chroot": "/var/run/" }, + + // Nofiles is a deprecated security feature which prevents cjdns from opening + // any files at all, using this will block setting of IP addresses and + // hot-adding ETHInterface devices but for users who do not need this, it + // provides a formidable sandbox. + // Default: disabled + { "nofiles": 0 }, + + // Noforks will prevent cjdns from spawning any new processes or threads, + // this prevents many types of exploits from attacking the wider system. + // Default: enabled + { "noforks": 1 }, + + // Seccomp is the most advanced sandboxing feature in cjdns, it uses + // SECCOMP_BPF to filter the system calls which cjdns is able to make on a + // linux system, strictly limiting it's access to the outside world + // This will fail quietly on any non-linux system + // Default: enabled + { "seccomp": 1 }, + + // The client sets up the core using a sequence of RPC calls, the responses + // to these calls are verified but in the event that the client crashes + // setup of the core completes, it could leave the core in an insecure state + // This call constitutes the client telling the core that the security rules + // have been fully applied and the core may run. Without it, the core will + // exit within a few seconds with return code 232. + // Default: enabled + { "setupComplete": 1 } + ], + + // Logging + "logging": + { + // Uncomment to have cjdns log to stdout rather than making logs available + // via the admin socket. + // "logTo":"stdout" + }, + + // If set to non-zero, cjdns will not fork to the background. + // Recommended for use in conjunction with "logTo":"stdout". + "noBackground":0 + + // Pipe file will store in this path, recommended value: /tmp (for unix), + // \\.\pipe (for windows) + // /data/local/tmp (for rooted android) + // /data/data/AppName (for non-root android) + // This only needs to be specified if cjdroute's guess is incorrect + // "pipe":"/tmp" +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..b17fe45 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,3 @@ +# Scripts + +This folder contains custom scripts that do useful things. \ No newline at end of file diff --git a/scripts/status b/scripts/status new file mode 100644 index 0000000..ac46839 --- /dev/null +++ b/scripts/status @@ -0,0 +1,105 @@ +#!/bin/bash +# System status script +# Feel free to copypaste, modify, and share; that's what we did. + +# Text colors +RED='\e[1;31m' +WHT='\e[1;37m' +GRN='\e[1;32m' +BLU='\e[0;34m' +CLR='\e[0m' + +INACTIVE="$RED[INACTIVE]$CLR" +ACTIVE="$GRN[ACTIVE]$CLR" + +echo -e "$BLU" +toilet -f smblock.tlf "Netsyms Business" +toilet -f smblock.tlf "Accelerator" +echo -e "$CLR" +echo -e "---------------------------------------------------$WHT" +echo -e "Engineered by$BLU Netsyms Technologies$CLR" +echo -e '' +echo -e 'Home: https://netsyms.com' +echo -e 'Support: https://support.netsyms.com' +echo -e 'Source: https://source.netsyms.com' +echo -e 'Business: https://netsyms.biz' +echo -e '---------------------------------------------------' +echo -e 'The programs included with this system are free' +echo -e 'software; the exact distribution terms for each' +echo -e 'program are described in the individual files in' +echo -e '/usr/share/doc/*/copyright.' + +echo -e '---------------------------------------------------' +upSeconds="$(/usr/bin/cut -d. -f1 /proc/uptime)" +secs=$((${upSeconds}%60)) +mins=$((${upSeconds}/60%60)) +hours=$((${upSeconds}/3600%24)) +days=$((${upSeconds}/86400)) +UPTIME=`printf "%02d days, %02dh %02dm" "$days" "$hours" "$mins"` + +freekb=$(cat /proc/meminfo | grep MemFree | awk {'print $2'}) +totalkb=$(cat /proc/meminfo | grep MemTotal | awk {'print $2'}) + +freemb=$(($freekb/1000)) +totalmb=$(($totalkb/1000)) + +percentfree=$(echo "scale=0; $freekb*100/$totalkb" | bc) + +echo -e "Date .................................. $WHT$(date +%b\ %e\ %Y)$CLR" +echo -e "Time ..................................... $WHT$(date +%H:%M:%S)$CLR" +echo -e "Uptime ........................... $WHT$UPTIME$CLR" +echo -e "Processes ..................................... $WHT$(ps ax | wc -l | tr -d " ")$CLR" +echo -e "Load averages ....................$WHT$(uptime | awk -F'[a-z]:' '{ print $2}')$CLR" +if (( $percentfree < 30 )); then + echo -e "Memory used .......................... $RED$(($totalmb-$freemb)) MB ($((100-$percentfree))%)$CLR" + echo -e "Memory free .......................... $RED$freemb MB ($percentfree%)$CLR" +else + echo -e "Memory used .......................... $GRN$(($totalmb-$freemb)) MB ($((100-$percentfree))%)$CLR" + echo -e "Memory free .......................... $GRN$freemb MB ($percentfree%)$CLR" +fi +cpu=$( 45 )); then + cpu="$RED$cpu\u00b0C$CLR" +else + cpu="$GRN$cpu\u00b0C$CLR" +fi +echo -e "CPU temperature .............................. $cpu" + +# +# Service Status +# +if [ $(systemctl status cjdns.service | grep 'Active: ' | awk '{ print $2 }') = 'active' ]; then + echo -e "cjdns Service ............................ $ACTIVE" +else + echo -e "cjdns Service .......................... $INACTIVE" +fi +if [ $(systemctl status nginx.service | grep 'Active: ' | awk '{ print $2 }') = 'active' ]; then + echo -e "nginx Service ............................ $ACTIVE" +else + echo -e "nginx Service .......................... $INACTIVE" +fi +if [ $(systemctl status php7.0-fpm.service | grep 'Active: ' | awk '{ print $2 }') = 'active' ]; then + echo -e "php-fpm Service .......................... $ACTIVE" +else + echo -e "php-fpm Service ........................ $INACTIVE" +fi + +# +# IP/Network Info +# + +echo -e "---------------------------------------------------$WHT" +echo -e "Local IP$CLR" +ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' +echo -e "---------------------------------------------------$WHT" +echo -e "Public IP$CLR" +dig +short myip.opendns.com @resolver1.opendns.com +echo -e "---------------------------------------------------$WHT" +echo -e "CJDNS IP$CLR" +sudo grep -m 1 '"ipv6"' /etc/cjdroute.conf | awk '{ print $2 }' | sed 's/[",]//g' +echo -e '---------------------------------------------------' +echo -e "For network bandwidth usage data, run$WHT vnstat$CLR" +echo -e "To refresh this status message, run$WHT status$CLR" +#vnstat -i eth0+tun0+wlan0 --style 0 +echo -e '---------------------------------------------------' diff --git a/www/README.md b/www/README.md new file mode 100644 index 0000000..b10b404 --- /dev/null +++ b/www/README.md @@ -0,0 +1,3 @@ +# Local Web Content + +This folder contains files for the Accelerator's webroot. \ No newline at end of file diff --git a/www/api.php b/www/api.php new file mode 100644 index 0000000..8d88948 --- /dev/null +++ b/www/api.php @@ -0,0 +1,46 @@ + "OK"])); + break; + case "auth": + $errmsg = ""; + if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) { + exit(json_encode(["status" => "OK", "msg" => "Login successful."])); + } else { + exit(json_encode(["status" => "ERROR", "msg" => "Login incorrect."])); + } + break; + case "hastotp": + if (userHasTOTP($VARS['username'])) { + exit(json_encode(["status" => "OK", "otp" => true])); + } else { + exit(json_encode(["status" => "OK", "otp" => false])); + } + break; + case "verifytotp": + if (verifyTOTP($VARS['username'], $VARS['code'])) { + exit(json_encode(["status" => "OK", "valid" => true])); + } else { + exit(json_encode(["status" => "ERROR", "msg" => "Authentication code incorrect.", "valid" => false])); + } + break; + default: + http_response_code(404); + die(json_encode("404 Not Found: the requested action is not available.")); +} diff --git a/www/lib/login.php b/www/lib/login.php new file mode 100644 index 0000000..802fdf5 --- /dev/null +++ b/www/lib/login.php @@ -0,0 +1,71 @@ +get('accounts', 'password', ['username' => $username]); + return (comparePassword($password, $hash)); +} + +function user_exists($username) { + return $database->has('accounts', ['username' => strtolower($username)]); +} + + +//////////////////////////////////////////////////////////////////////////////// +// 2-factor authentication // +//////////////////////////////////////////////////////////////////////////////// + +/** + * Check if a user has TOTP setup + * @global $database $database + * @param string $username + * @return boolean true if TOTP secret exists, else false + */ +function userHasTOTP($username) { + global $database; + $username = strtolower($username); + $secret = $database->get('accounts', 'authsecret', ['username' => $username]); + if (is_empty($secret)) { + return false; + } + return true; +} + +/** + * Verify a TOTP multiauth code + * @global $database + * @param string $username + * @param int $code + * @return boolean true if it's legit, else false + */ +function verifyTOTP($username, $code) { + global $database; + $username = strtolower($username); + $authsecret = $database->get('accounts', 'authsecret', ['username' => $username]); + if (is_empty($authsecret)) { + return false; + } + $totp = new TOTP(null, $authsecret); + return $totp->verify($code); +} + diff --git a/www/required.php b/www/required.php new file mode 100644 index 0000000..9b8e20c --- /dev/null +++ b/www/required.php @@ -0,0 +1,305 @@ + 'sqlite', + 'database_file' => DB_FILE, + 'charset' => DB_CHARSET + ]); +} catch (Exception $ex) { + header('HTTP/1.1 500 Internal Server Error'); + sendError("Database error. $ex"); +} + + +if (!DEBUG) { + error_reporting(0); +} else { + error_reporting(E_ALL); + ini_set('display_errors', 'On'); +} + + +$VARS; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $VARS = $_POST; + define("GET", false); +} else { + $VARS = $_GET; + define("GET", true); +} + +/** + * Checks if a string or whatever is empty. + * @param $str The thingy to check + * @return boolean True if it's empty or whatever. + */ +function is_empty($str) { + return (is_null($str) || !isset($str) || $str == ''); +} + +/** + * Securely verify a password and its hash + * @param String $password + * @param String $hash the hash to compare to + * @return boolean True if password OK, else false + */ +function comparePassword($password, $hash) { + return password_verify($password, $hash); +} + +function dieifnotloggedin() { + if ($_SESSION['loggedin'] != true) { + sendError("Session expired. Please log out and log in again."); + } +} + +/** + * Check if the previous database action had a problem. + * @param array $specials int=>string array with special response messages for SQL errors + */ +function checkDBError($specials = []) { + global $database; + $errors = $database->error(); + if (!is_null($errors[1])) { + foreach ($specials as $code => $text) { + if ($errors[1] == $code) { + sendError($text); + } + } + sendError("A database error occurred:
" . $errors[2] . ""); + } +} + +/* + * http://stackoverflow.com/a/20075147 + */ +if (!function_exists('base_url')) { + + function base_url($atRoot = FALSE, $atCore = FALSE, $parse = FALSE) { + if (isset($_SERVER['HTTP_HOST'])) { + $http = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off' ? 'https' : 'http'; + $hostname = $_SERVER['HTTP_HOST']; + $dir = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']); + + $core = preg_split('@/@', str_replace($_SERVER['DOCUMENT_ROOT'], '', realpath(dirname(__FILE__))), NULL, PREG_SPLIT_NO_EMPTY); + $core = $core[0]; + + $tmplt = $atRoot ? ($atCore ? "%s://%s/%s/" : "%s://%s/") : ($atCore ? "%s://%s/%s/" : "%s://%s%s"); + $end = $atRoot ? ($atCore ? $core : $hostname) : ($atCore ? $core : $dir); + $base_url = sprintf($tmplt, $http, $hostname, $end); + } else + $base_url = 'http://localhost/'; + + if ($parse) { + $base_url = parse_url($base_url); + if (isset($base_url['path'])) + if ($base_url['path'] == '/') + $base_url['path'] = ''; + } + + return $base_url; + } + +} + +/** + * Check if a given ipv4 address is in a given cidr + * @param string $ip IP to check in IPV4 format eg. 127.0.0.1 + * @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed + * @return boolean true if the ip is in this range / false if not. + * @author Thorsten Ott + */ +function ip4_in_cidr($ip, $cidr) { + if (strpos($cidr, '/') == false) { + $cidr .= '/32'; + } + // $range is in IP/CIDR format eg 127.0.0.1/24 + list( $cidr, $netmask ) = explode('/', $cidr, 2); + $range_decimal = ip2long($cidr); + $ip_decimal = ip2long($ip); + $wildcard_decimal = pow(2, ( 32 - $netmask)) - 1; + $netmask_decimal = ~ $wildcard_decimal; + return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) ); +} + +/** + * Check if a given ipv6 address is in a given cidr + * @param string $ip IP to check in IPV6 format + * @param string $cidr CIDR netmask + * @return boolean true if the IP is in this range, false otherwise. + * @author MW. + */ +function ip6_in_cidr($ip, $cidr) { + $address = inet_pton($ip); + $subnetAddress = inet_pton(explode("/", $cidr)[0]); + $subnetMask = explode("/", $cidr)[1]; + + $addr = str_repeat("f", $subnetMask / 4); + switch ($subnetMask % 4) { + case 0: + break; + case 1: + $addr .= "8"; + break; + case 2: + $addr .= "c"; + break; + case 3: + $addr .= "e"; + break; + } + $addr = str_pad($addr, 32, '0'); + $addr = pack("H*", $addr); + + $binMask = $addr; + return ($address & $binMask) == $subnetAddress; +} + +/** + * Check if the REMOTE_ADDR is on Cloudflare's network. + * @return boolean true if it is, otherwise false + */ +function validateCloudflare() { + if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + // Using IPv6 + $cloudflare_ips_v6 = [ + "2400:cb00::/32", + "2405:8100::/32", + "2405:b500::/32", + "2606:4700::/32", + "2803:f800::/32", + "2c0f:f248::/32", + "2a06:98c0::/29" + ]; + $valid = false; + foreach ($cloudflare_ips_v6 as $cidr) { + if (ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) { + $valid = true; + break; + } + } + } else { + // Using IPv4 + $cloudflare_ips_v4 = [ + "103.21.244.0/22", + "103.22.200.0/22", + "103.31.4.0/22", + "104.16.0.0/12", + "108.162.192.0/18", + "131.0.72.0/22", + "141.101.64.0/18", + "162.158.0.0/15", + "172.64.0.0/13", + "173.245.48.0/20", + "188.114.96.0/20", + "190.93.240.0/20", + "197.234.240.0/22", + "198.41.128.0/17" + ]; + $valid = false; + foreach ($cloudflare_ips_v4 as $cidr) { + if (ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) { + $valid = true; + break; + } + } + } + return $valid; +} + +/** + * Makes a good guess at the client's real IP address. + * + * @return string Client IP or `0.0.0.0` if we can't find anything + */ +function getClientIP() { + // If CloudFlare is in the mix, we should use it. + // Check if the request is actually from CloudFlare before trusting it. + if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) { + if (validateCloudflare()) { + return $_SERVER["HTTP_CF_CONNECTING_IP"]; + } + } + + if (isset($_SERVER["REMOTE_ADDR"])) { + return $_SERVER["REMOTE_ADDR"]; + } + + return "0.0.0.0"; // This will not happen unless we aren't a web server +} + +/** + * Check if the client's IP has been doing too many brute-force-friendly + * requests lately. + * Kills the script with a "friendly" error and response code 429 + * (Too Many Requests) if the last access time in the DB is too near. + * + * Also updates the rate_limit table with the latest data and purges old rows. + * @global type $database + */ +function engageRateLimit() { + global $database; + $delay = date("Y-m-d H:i:s", strtotime("-2 seconds")); + $database->delete('rate_limit', ["lastaction[<]" => $delay]); + if ($database->has('rate_limit', ["AND" => ["ipaddr" => getClientIP()]])) { + http_response_code(429); + // JSONify it so API clients don't scream too loud + die(json_encode(["status" => "ERROR", "msg" => "You're going too fast. Slow down, mkay?"])); + } else { + // Add a record for the IP address + $database->insert('rate_limit', ["ipaddr" => getClientIP(), "lastaction" => date("Y-m-d H:i:s")]); + } +} + diff --git a/www/settings.template.php b/www/settings.template.php new file mode 100644 index 0000000..101192a --- /dev/null +++ b/www/settings.template.php @@ -0,0 +1,15 @@ +