diff --git a/.dockerignore b/.dockerignore index 61bedfc..92a334f 100755 --- a/.dockerignore +++ b/.dockerignore @@ -18,6 +18,8 @@ docker-compose.yml /backend/db/firegex.db /backend/db/firegex.db-journal +/backend/proxy/proxy + # misc **/.DS_Store **/.env.local diff --git a/.gitignore b/.gitignore index 6e34a1f..e416d2c 100755 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /backend/db/firegex.db /backend/db/firegex.db-journal +/backend/proxy/proxy # production /frontend/build diff --git a/Dockerfile b/Dockerfile index dce5ae9..50974a5 100755 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN npm run build #Building main conteiner FROM python:slim-buster -RUN apt-get update && apt-get -y install curl supervisor gettext-base build-essential libboost-dev nginx +RUN apt-get update && apt-get -y install curl supervisor gettext-base build-essential libboost-dev nginx libboost-regex-dev libboost-system-dev RUN curl -sL https://deb.nodesource.com/setup_16.x | bash RUN apt-get install nodejs @@ -31,6 +31,9 @@ COPY ./config/supervisord.conf /etc/supervisor/supervisord.conf COPY ./config/nginx.conf /tmp/nginx.conf COPY ./config/start_nginx.sh /tmp/start_nginx.sh +RUN c++ -O3 -o proxy/proxy proxy/proxy.cpp -pthread -lboost_system -lboost_regex +RUN chmod ug+x /execute/proxy/proxy + #Copy react app in the main container COPY --from=frontend /app/build/ ./frontend/ diff --git a/README.md b/README.md index 857f3e9..9bf8f22 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,8 @@ ## TODO -1. custom windows docker-compose -2. backend checks and errors -3. back and frontend password -4. compile c++ -O3 +1. back and frontend password +2. compile c++ -O3 # # Documentation diff --git a/backend/app.py b/backend/app.py index 126d8fc..b4885e6 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,5 +1,8 @@ -import sqlite3, random, string, subprocess, sys, threading, os -from flask import Flask, jsonify, request, abort +import sqlite3, random, string, subprocess, sys, threading, os, bcrypt, secrets, time +from flask import Flask, jsonify, request, abort, session +from functools import wraps +from flask_cors import CORS +from kthread import KThread class SQLite(): @@ -54,15 +57,150 @@ def gen_internal_port(): break return res + + + # DB init db = SQLite('firegex') db.connect() + +class KeyValueStorage: + def __init__(self): + pass + + def get(self, key): + q = db.query('SELECT value FROM keys_values WHERE key = ?', (key,)) + if len(q) == 0: + return None + else: + return q[0][0] + + def put(self, key, value): + if self.get(key) is None: + db.query('INSERT INTO keys_values (key, value) VALUES (?, ?);', (key,str(value))) + else: + db.query('UPDATE keys_values SET value=? WHERE key = ?;', (str(value), key)) + + + app = Flask(__name__) +conf = KeyValueStorage() +proxy_table = {} DEBUG = len(sys.argv) > 1 and sys.argv[1] == "DEBUG" +def is_loggined(): + if DEBUG: return True + return True if session.get("loggined") else False + +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if is_loggined() or DEBUG: + return f(*args, **kwargs) + else: + return abort(401) + + return decorated_function + +def get_service_data(id): + q = db.query('SELECT * FROM services WHERE service_id=?;',(id,)) + if len(q) == 0: return None + srv = q[0] + return { + 'id': srv[1], + 'status': srv[0], + 'public_port': srv[3], + 'internal_port': srv[2] + } + +def service_manager(id): + data = get_service_data(id) + if data is None: return + + + +@app.before_first_request +def before_first_request(): + + + services = [ + + ] + + for srv in services: + + + + + app.config['SECRET_KEY'] = secrets.token_hex(32) + if DEBUG: + app.config["STATUS"] = "run" + elif conf.get("password") is None: + app.config["STATUS"] = "init" + else: + app.config["STATUS"] = "run" + +@app.route("/api/status") +def get_status(): + return { + "status":app.config["STATUS"], + "loggined": is_loggined() + } + +@app.route("/api/login", methods = ['POST']) +def login(): + if not conf.get("password"): return abort(404) + req = request.get_json(force = True) + if not "password" in req or not isinstance(req["password"],str): + return abort(400) + if req["password"] == "": + return {"status":"Cannot insert an empty password!"} + time.sleep(.3) # No bruteforce :) + if bcrypt.checkpw(req["password"].encode(), conf.get("password").encode()): + session["loggined"] = True + return { "status":"ok" } + return {"status":"Wrong password!"} + +@app.route("/api/logout") +def logout(): + session["loggined"] = False + return { "status":"ok" } + +@app.route('/api/change-password', methods = ['POST']) +@login_required +def change_password(): + req = request.get_json(force = True) + if not "password" in req or not isinstance(req["password"],str): + return abort(400) + if req["password"] == "": + return {"status":"Cannot insert an empty password!"} + if req["expire"]: + app.config['SECRET_KEY'] = secrets.token_hex(32) + session["loggined"] = True + hash_psw = bcrypt.hashpw(req["password"].encode(), bcrypt.gensalt()) + conf.put("password",hash_psw.decode()) + return {"status":"ok"} + + +@app.route('/api/set-password', methods = ['POST']) +def set_password(): + if conf.get("password"): return abort(404) + req = request.get_json(force = True) + if not "password" in req or not isinstance(req["password"],str): + return abort(400) + if req["password"] == "": + return {"status":"Cannot insert an empty password!"} + + hash_psw = bcrypt.hashpw(req["password"].encode(), bcrypt.gensalt()) + conf.put("password",hash_psw.decode()) + app.config["STATUS"] = "run" + session["loggined"] = True + return {"status":"ok"} + @app.route('/api/general-stats') +@login_required def get_general_stats(): n_services = db.query(''' SELECT COUNT (*) FROM services; @@ -81,6 +219,7 @@ def get_general_stats(): } @app.route('/api/services') +@login_required def get_services(): res = [] for i in db.query('SELECT * FROM services;'): @@ -101,6 +240,7 @@ def get_services(): @app.route('/api/service/') +@login_required def get_service(serv): q = db.query('SELECT * FROM services WHERE service_id = ?;', (serv,)) if len(q) != 0: @@ -119,6 +259,7 @@ def get_service(serv): return abort(404) @app.route('/api/service//stop') +@login_required def get_service_stop(serv): db.query(''' UPDATE services SET status = 'stop' WHERE service_id = ?; @@ -129,6 +270,7 @@ def get_service_stop(serv): } @app.route('/api/service//pause') +@login_required def get_service_pause(serv): db.query(''' UPDATE services SET status = 'pause' WHERE service_id = ?; @@ -139,6 +281,7 @@ def get_service_pause(serv): } @app.route('/api/service//start') +@login_required def get_service_start(serv): db.query(''' UPDATE services SET status = 'wait' WHERE service_id = ?; @@ -149,6 +292,7 @@ def get_service_start(serv): } @app.route('/api/service//delete') +@login_required def get_service_delete(serv): db.query('DELETE FROM services WHERE service_id = ?;', (serv,)) db.query('DELETE FROM regexes WHERE service_id = ?;', (serv,)) @@ -159,6 +303,7 @@ def get_service_delete(serv): @app.route('/api/service//regen-port') +@login_required def get_regen_port(serv): db.query('UPDATE services SET internal_port = ? WHERE service_id = ?;', (gen_internal_port(), serv)) return { @@ -167,6 +312,7 @@ def get_regen_port(serv): @app.route('/api/service//regexes') +@login_required def get_service_regexes(serv): return jsonify([ { @@ -181,6 +327,7 @@ def get_service_regexes(serv): @app.route('/api/regex/') +@login_required def get_regex_id(regex_id): q = db.query('SELECT * FROM regexes WHERE regex_id = ?;', (regex_id,)) if len(q) != 0: @@ -197,6 +344,7 @@ def get_regex_id(regex_id): @app.route('/api/regex//delete') +@login_required def get_regex_delete(regex_id): db.query('DELETE FROM regexes WHERE regex_id = ?;', (regex_id,)) @@ -206,6 +354,7 @@ def get_regex_delete(regex_id): @app.route('/api/regexes/add', methods = ['POST']) +@login_required def post_regexes_add(): req = request.get_json(force = True) @@ -219,6 +368,7 @@ def post_regexes_add(): @app.route('/api/services/add', methods = ['POST']) +@login_required def post_services_add(): req = request.get_json(force = True) serv_id = from_name_get_id(req['name']) @@ -233,8 +383,7 @@ def post_services_add(): return {'status': 'ok'} if DEBUG: - from flask_cors import CORS - cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) + CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True ) if __name__ == '__main__': db.check_integrity({ @@ -254,10 +403,14 @@ if __name__ == '__main__': 'regex_id': 'INTEGER PRIMARY KEY', 'FOREIGN KEY (service_id)':'REFERENCES services (service_id)' }, + 'keys_values': { + 'key': 'VARCHAR(100) PRIMARY KEY', + 'value': 'VARCHAR(100) NOT NULL', + }, }) if DEBUG: app.run(host="0.0.0.0", port=8080 ,debug=True) else: - subprocess.run(["uwsgi","--socket","./uwsgi.sock","--master","--module","app:app"]) + subprocess.run(["uwsgi","--socket","./uwsgi.sock","--master","--module","app:app", "--enable-threads"]) diff --git a/backend/c_back/proxy b/backend/c_back/proxy deleted file mode 100755 index f7cdc36..0000000 Binary files a/backend/c_back/proxy and /dev/null differ diff --git a/backend/c_back/proxy_wrap.py b/backend/proxy/__init__.py similarity index 100% rename from backend/c_back/proxy_wrap.py rename to backend/proxy/__init__.py diff --git a/backend/c_back/proxy.cpp b/backend/proxy/proxy.cpp similarity index 100% rename from backend/c_back/proxy.cpp rename to backend/proxy/proxy.cpp diff --git a/backend/requirements.txt b/backend/requirements.txt index bf9438b..5dcca7b 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,9 +1,5 @@ -click==8.1.3 -colorama==0.4.4 -Flask==2.1.2 -itsdangerous==2.1.2 -Jinja2==3.1.2 -MarkupSafe==2.1.1 -Werkzeug==2.1.2 +Flask uwsgi -flask-cors \ No newline at end of file +flask-cors +bcrypt +kthread \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100755 index 0000000..a8afcdb --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,13 @@ +version: '3.9' + +services: + firewall: + restart: unless-stopped + build: . + ports: + - 80:80 + environment: + - NGINX_PORT=80 + volumes: + - /execute/db + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d2a0810..a47e003 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,11 +4,9 @@ services: firewall: restart: unless-stopped build: . - #network_mode: "host" - ports: - - 80:80 + network_mode: "host" environment: - - NGINX_PORT=80 + - NGINX_PORT=4444 volumes: - /execute/db \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c85dc26..e01df7e 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,17 +1,142 @@ -import React from 'react'; +import { Button, Group, Loader, LoadingOverlay, Notification, Space, TextInput, Title } from '@mantine/core'; +import { useForm } from '@mantine/hooks'; +import React, { useEffect, useState } from 'react'; +import { ImCross } from 'react-icons/im'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; import MainLayout from './components/MainLayout'; +import { PasswordSend, ServerStatusResponse, update_freq } from './js/models'; +import { getstatus, login, setpassword } from './js/utils'; import HomePage from './pages/HomePage'; import ServiceDetails from './pages/ServiceDetails'; function App() { - return - }> - } /> - } /> - } /> - - + + const [loading, setLoading] = useState(true); + const [systemStatus, setSystemStatus] = useState({status:"", loggined:false}) + const [reqError, setReqError] = useState() + const [error, setError] = useState() + const [loadinBtn, setLoadingBtn] = useState(false); + + const getStatus = () =>{ + getstatus().then( res =>{ + setSystemStatus(res) + setReqError(undefined) + setLoading(false) + }).catch(err=>{ + setReqError(err.toString()) + setLoading(false) + setTimeout(getStatus, update_freq) + }) + } + + useEffect(getStatus,[]) + + const form = useForm({ + initialValues: { + password:"", + }, + validationRules:{ + password: (value) => value !== "" + } + }) + + + if (loading){ + return + }else if (reqError){ + return
+ Errore nel caricamento del firewall! 🔥 + + Errore nella comunicazione con il backend + + Errore: {reqError} + + +
+ }else if (systemStatus.status === "init"){ + + const submitRequest = async (values:PasswordSend) => { + setLoadingBtn(true) + await setpassword(values).then(res => { + if(!res){ + setSystemStatus({loggined:true, status:"run"}) + }else{ + setError(res) + } + }).catch( err => setError(err.toString())) + setLoadingBtn(false) + } + + + return
+ Choose the password for access to the firewall 🔒 + +
+ + + + + + + {error?<> + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + :null} +
+ }else if (systemStatus.status === "run" && !systemStatus.loggined){ + const submitRequest = async (values:PasswordSend) => { + setLoadingBtn(true) + await login(values).then(res => { + if(!res){ + setSystemStatus({...systemStatus, loggined:true}) + }else{ + setError(res) + } + }).catch( err => setError(err.toString())) + setLoadingBtn(false) + } + + + return
+ Welcome to Firegex 🔥 + + Before you use the firewall, insert a password 🔒 + +
+ + + + + + + {error?<> + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + :null} +
+ }else if (systemStatus.status === "run" && systemStatus.loggined){ + return + }> + } /> + } /> + } /> + + + }else{ + return
+ Errore nel caricamento del firewall! 🔥 + + Errore nella comunicazione con il backend +
+ } } export default App; diff --git a/frontend/src/components/AddNewRegex.tsx b/frontend/src/components/AddNewRegex.tsx index 54d4e02..8dab492 100755 --- a/frontend/src/components/AddNewRegex.tsx +++ b/frontend/src/components/AddNewRegex.tsx @@ -73,7 +73,6 @@ function AddNewRegex({ closePopup, service }:{ closePopup:()=>void, service:stri return
void, service:stri data={['C -> S', 'S -> C', 'C <-> S']} label="Choose the source of the packets to filter" variant="filled" - required {...form.getInputProps('mode')} /> diff --git a/frontend/src/components/AddNewService.tsx b/frontend/src/components/AddNewService.tsx index 7ec36cf..01a8c33 100755 --- a/frontend/src/components/AddNewService.tsx +++ b/frontend/src/components/AddNewService.tsx @@ -41,7 +41,6 @@ function AddNewService({ closePopup }:{ closePopup:()=>void }) { return void }) { { clearInterval(updater) } }, []); + + const logout_action = () => { + logout().then(r => { + window.location.reload() + }).catch(r => { + errorNotify("Logout failed!",`Error: ${r}`) + }) + } + + + const form = useForm({ + initialValues: { + password:"", + expire:true + }, + validationRules:{ + password: (value) => value !== "" + } + }) + + const [loadingBtn, setLoadingBtn] = useState(false) + const [error, setError] = useState(null) + const [changePasswordModal, setChangePasswordModal] = useState(false); + + const submitRequest = async (values:ChangePassword) => { + setLoadingBtn(true) + await changepassword(values).then(res => { + if(!res){ + okNotify("Password change done!","The password of the firewall has been changed!") + setChangePasswordModal(false) + form.reset() + }else{ + setError(res) + } + }).catch( err => setError(err.toString())) + setLoadingBtn(false) + } + const {srv_id} = useParams() + + + const [open, setOpen] = useState(false); const closeModal = () => {setOpen(false);} @@ -45,6 +90,13 @@ function Header() { Filtered Connections: {generalStats.closed} Regexes: {generalStats.regexes}
+ + Firewall Access + } onClick={logout_action}>Logout + + } onClick={() => setChangePasswordModal(true)}>Change Password + +
{ location.pathname !== "/"? } - - + setChangePasswordModal(false)} closeOnClickOutside={false} centered> + + + + + + + + + + + + + {error?<> + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + :null} +
} diff --git a/frontend/src/components/MainLayout.tsx b/frontend/src/components/MainLayout.tsx index 49d4f6b..cd6f58d 100755 --- a/frontend/src/components/MainLayout.tsx +++ b/frontend/src/components/MainLayout.tsx @@ -1,13 +1,11 @@ -import { Container, MantineProvider, Space } from '@mantine/core'; -import { NotificationsProvider } from '@mantine/notifications'; import React from 'react'; +import { Container, Space } from '@mantine/core'; import Footer from './Footer'; import Header from './Header'; function MainLayout({ children }:{ children:any }) { return <> - - +
@@ -15,8 +13,7 @@ function MainLayout({ children }:{ children:any }) {