spiderweb/webapp.py

478 lines
14 KiB
Python
Raw Permalink Normal View History

2023-01-07 22:15:43 +00:00
__author__ = "IU1BOW - Corrado"
2020-01-22 15:18:38 +00:00
import flask
2023-01-15 05:59:58 +00:00
import secrets
from flask import request, render_template
2021-11-28 17:31:32 +00:00
from flask_wtf.csrf import CSRFProtect
from flask_minify import minify
2020-01-22 15:18:38 +00:00
import json
2023-01-15 05:59:58 +00:00
import threading
2021-05-06 14:54:58 +00:00
import logging
import logging.config
2021-05-07 05:52:09 +00:00
from lib.dxtelnet import who
from lib.adxo import get_adxo_events
2021-05-25 10:12:29 +00:00
from lib.qry import query_manager
2021-12-12 14:34:59 +00:00
from lib.cty import prefix_table
2023-01-15 05:59:58 +00:00
from lib.plot_data_provider import ContinentsBandsProvider, SpotsPerMounthProvider, SpotsTrend, HourBand, WorldDxSpotsLive
2023-12-02 08:29:26 +00:00
import requests
import xmltodict
2024-03-10 07:09:01 +00:00
from lib.qry_builder import query_build, query_build_callsign, query_build_callsing_list
2021-11-28 17:31:32 +00:00
2024-03-24 10:53:27 +00:00
2021-05-06 14:54:58 +00:00
logging.config.fileConfig("cfg/webapp_log_config.ini", disable_existing_loggers=True)
logger = logging.getLogger(__name__)
2023-12-02 08:29:26 +00:00
logger.info("Starting SPIDERWEB")
2021-05-06 14:54:58 +00:00
2020-01-22 15:18:38 +00:00
app = flask.Flask(__name__)
2023-01-15 05:59:58 +00:00
app.config["SECRET_KEY"] = secrets.token_hex(16)
2021-11-28 17:31:32 +00:00
app.config.update(
2023-01-07 22:15:43 +00:00
SESSION_COOKIE_SECURE=True,
2023-02-04 16:49:40 +00:00
SESSION_COOKIE_HTTPONLY=False,
2023-01-07 22:15:43 +00:00
SESSION_COOKIE_SAMESITE="Strict",
2021-11-28 17:31:32 +00:00
)
2023-12-02 08:29:26 +00:00
version_file = open("cfg/version.txt", "r")
app.config["VERSION"] = version_file.read().strip()
version_file.close
logger.info("Version:"+app.config["VERSION"] )
2021-11-28 17:31:32 +00:00
2023-01-15 05:59:58 +00:00
inline_script_nonce = ""
2021-11-28 17:31:32 +00:00
csrf = CSRFProtect(app)
2023-02-19 21:47:28 +00:00
2023-01-07 22:15:43 +00:00
logger.debug(app.config)
2020-01-22 15:18:38 +00:00
2023-01-07 22:15:43 +00:00
if app.config["DEBUG"]:
minify(app=app, html=False, js=False, cssless=False)
else:
minify(app=app, html=True, js=True, cssless=False)
2023-12-02 08:29:26 +00:00
#removing whitespace from jinja2 html rendered
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
2024-03-24 06:47:27 +00:00
2023-01-07 22:15:43 +00:00
# load config file
with open("cfg/config.json") as json_data_file:
cfg = json.load(json_data_file)
2020-01-22 16:24:20 +00:00
2021-12-19 09:57:19 +00:00
logging.debug("CFG:")
2023-01-07 22:15:43 +00:00
logging.debug(cfg)
# load bands file
with open("cfg/bands.json") as json_bands:
band_frequencies = json.load(json_bands)
# load mode file
with open("cfg/modes.json") as json_modes:
modes_frequencies = json.load(json_modes)
# load continents-cq file
with open("cfg/continents.json") as json_continents:
continents_cq = json.load(json_continents)
2024-03-24 06:47:27 +00:00
#load visitour counter
visits_file_path = "data/visits.json"
try:
# Load the visits data from the file
with open(visits_file_path) as json_visitors:
visits = json.load(json_visitors)
except FileNotFoundError:
# If the file does not exist, create an empty visits dictionary
visits = {}
#save visits
def save_visits():
with open(visits_file_path, "w") as json_file:
json.dump(visits, json_file)
logging.info('visit saved on: '+ visits_file_path)
# saving scheduled
def schedule_save():
save_visits()
threading.Timer(1000, schedule_save).start()
# Start scheduling
schedule_save()
2023-01-07 22:15:43 +00:00
# read and set default for enabling cq filter
if cfg.get("enable_cq_filter"):
enable_cq_filter = cfg["enable_cq_filter"].upper()
2021-12-12 14:34:59 +00:00
else:
2023-01-07 22:15:43 +00:00
enable_cq_filter = "N"
2021-12-12 14:34:59 +00:00
2023-01-07 22:15:43 +00:00
# define country table for search info on callsigns
pfxt = prefix_table()
2021-12-12 14:34:59 +00:00
2023-01-07 22:15:43 +00:00
# create object query manager
qm = query_manager()
2021-05-25 10:12:29 +00:00
2023-01-07 22:15:43 +00:00
# the main query to show spots
# it gets url parameter in order to apply the build the right query
# and apply the filter required. It returns a json with the spots
2023-02-19 21:47:28 +00:00
def spotquery(parameters):
2023-01-07 22:15:43 +00:00
try:
2023-02-19 21:47:28 +00:00
if 'callsign' in parameters:
logging.debug('search callsign')
2024-03-10 07:09:01 +00:00
query_string = query_build_callsign(logger,parameters['callsign'] )
2023-01-07 22:15:43 +00:00
else:
2023-02-19 21:47:28 +00:00
logging.debug('search eith other filters')
2024-03-10 07:09:01 +00:00
query_string = query_build(logger,parameters,band_frequencies,modes_frequencies,continents_cq,enable_cq_filter)
2021-05-25 10:12:29 +00:00
qm.qry(query_string)
2023-01-07 22:15:43 +00:00
data = qm.get_data()
row_headers = qm.get_headers()
2021-05-25 10:12:29 +00:00
logger.debug("query done")
2023-01-07 22:15:43 +00:00
logger.debug(data)
2021-05-25 10:12:29 +00:00
2023-01-07 22:15:43 +00:00
if data is None or len(data) == 0:
2022-02-27 06:13:50 +00:00
logger.warning("no data found")
2021-05-25 10:12:29 +00:00
2023-01-07 22:15:43 +00:00
payload = []
2021-05-25 10:12:29 +00:00
for result in data:
2023-01-07 22:15:43 +00:00
# create dictionary from recorset
main_result = dict(zip(row_headers, result))
2021-12-12 14:34:59 +00:00
# find the country in prefix table
2023-01-07 22:15:43 +00:00
search_prefix = pfxt.find(main_result["dx"])
# merge recordset and contry prefix
main_result["country"] = search_prefix["country"]
main_result["iso"] = search_prefix["iso"]
2021-12-19 09:57:19 +00:00
payload.append({**main_result})
2023-01-07 22:15:43 +00:00
2020-03-08 07:48:40 +00:00
return payload
2023-01-07 22:15:43 +00:00
except Exception as e:
2021-05-06 14:54:58 +00:00
logger.error(e)
2020-03-08 07:48:40 +00:00
2023-01-07 22:15:43 +00:00
# find adxo events
adxo_events = None
2021-05-06 14:54:58 +00:00
def get_adxo():
global adxo_events
2023-01-07 22:15:43 +00:00
adxo_events = get_adxo_events()
threading.Timer(12 * 3600, get_adxo).start()
2021-05-06 14:54:58 +00:00
get_adxo()
2024-03-24 06:47:27 +00:00
2023-01-07 22:15:43 +00:00
# create data provider for charts
heatmap_cbp = ContinentsBandsProvider(logger, qm, continents_cq, band_frequencies)
bar_graph_spm = SpotsPerMounthProvider(logger, qm)
line_graph_st = SpotsTrend(logger, qm)
bubble_graph_hb = HourBand(logger, qm, band_frequencies)
geo_graph_wdsl = WorldDxSpotsLive(logger, qm, pfxt)
2023-01-01 22:03:51 +00:00
2023-01-07 22:15:43 +00:00
# ROUTINGS
2023-02-19 21:47:28 +00:00
@app.route("/spotlist", methods=["POST"])
@csrf.exempt
2020-02-01 06:12:53 +00:00
def spotlist():
2023-02-19 21:47:28 +00:00
logger.debug(request.json)
response = flask.Response(json.dumps(spotquery(request.json)))
2020-02-08 09:14:30 +00:00
return response
2020-02-01 06:12:53 +00:00
2023-01-07 22:15:43 +00:00
2020-11-08 10:44:50 +00:00
def who_is_connected():
2023-11-12 08:13:10 +00:00
host=cfg["telnet"]["host"]
port=cfg["telnet"]["port"]
user=cfg["telnet"]["user"]
password=cfg["telnet"]["password"]
response = who(host, port, user, password)
2023-02-19 21:47:28 +00:00
logger.debug("list of connected clusters:")
logger.debug(response)
2020-11-08 10:44:50 +00:00
return response
2023-01-15 05:59:58 +00:00
#Calculate nonce token used in inline script and in csp "script-src" header
def get_nonce():
global inline_script_nonce
inline_script_nonce = secrets.token_hex()
return inline_script_nonce
2023-01-07 22:15:43 +00:00
2024-03-24 06:47:27 +00:00
#check if it is a unique visitor
def visitor_count():
user_ip =request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
2024-03-24 06:47:27 +00:00
if user_ip not in visits:
visits[user_ip] = 1
else:
visits[user_ip] += 1
2023-01-07 22:15:43 +00:00
@app.route("/", methods=["GET"])
@app.route("/index.html", methods=["GET"])
2020-02-08 09:14:30 +00:00
def spots():
2024-03-24 06:47:27 +00:00
visitor_count();
2023-01-07 22:15:43 +00:00
response = flask.Response(
render_template(
"index.html",
2023-01-15 05:59:58 +00:00
inline_script_nonce=get_nonce(),
2023-01-07 22:15:43 +00:00
mycallsign=cfg["mycallsign"],
2023-11-12 08:13:10 +00:00
telnet=cfg["telnet"]["host"]+":"+cfg["telnet"]["port"],
2023-01-07 22:15:43 +00:00
mail=cfg["mail"],
menu_list=cfg["menu"]["menu_list"],
2024-03-24 06:47:27 +00:00
visits=len(visits),
2023-01-07 22:15:43 +00:00
enable_cq_filter=enable_cq_filter,
timer_interval=cfg["timer"]["interval"],
adxo_events=adxo_events,
continents=continents_cq,
bands=band_frequencies,
2023-12-02 08:29:26 +00:00
dx_calls=get_dx_calls(),
2023-01-07 22:15:43 +00:00
)
)
2020-02-08 09:14:30 +00:00
return response
2020-01-22 15:18:38 +00:00
2023-01-07 22:15:43 +00:00
2023-12-02 08:29:26 +00:00
#Show all dx spot callsigns
def get_dx_calls():
2024-03-24 10:53:27 +00:00
2023-12-02 08:29:26 +00:00
try:
2024-03-24 10:53:27 +00:00
query_string = query_build_callsing_list()
2023-12-02 08:29:26 +00:00
qm.qry(query_string)
data = qm.get_data()
row_headers = qm.get_headers()
payload = []
for result in data:
main_result = dict(zip(row_headers, result))
payload.append(main_result["dx"])
logger.debug("last DX Callsigns:")
logger.debug(payload)
return payload
except Exception as e:
return []
2023-01-07 22:15:43 +00:00
@app.route("/service-worker.js", methods=["GET"])
2020-03-14 16:48:56 +00:00
def sw():
2023-01-28 18:02:30 +00:00
return app.send_static_file("pwa/service-worker.js")
2020-03-14 16:48:56 +00:00
2023-01-07 22:15:43 +00:00
@app.route("/offline.html")
2020-03-14 16:48:56 +00:00
def root():
2023-02-11 16:30:09 +00:00
return app.send_static_file("html/offline.html")
2020-06-16 08:17:07 +00:00
2024-03-24 06:47:27 +00:00
#used for plots
@app.route("/world.json")
2023-01-01 22:03:51 +00:00
def world_data():
2023-01-07 22:15:43 +00:00
return app.send_static_file("data/world.json")
@app.route("/plots.html")
2020-06-16 08:17:07 +00:00
def plots():
2023-01-07 22:15:43 +00:00
whoj = who_is_connected()
response = flask.Response(
render_template(
"plots.html",
2023-01-15 05:59:58 +00:00
inline_script_nonce=get_nonce(),
2023-01-07 22:15:43 +00:00
mycallsign=cfg["mycallsign"],
2023-11-12 08:13:10 +00:00
telnet=cfg["telnet"]["host"]+":"+cfg["telnet"]["port"],
2023-01-07 22:15:43 +00:00
mail=cfg["mail"],
menu_list=cfg["menu"]["menu_list"],
2024-03-24 06:47:27 +00:00
visits=len(visits),
2023-01-07 22:15:43 +00:00
who=whoj,
continents=continents_cq,
bands=band_frequencies,
)
)
2020-06-16 08:17:07 +00:00
return response
2023-12-02 08:29:26 +00:00
@app.route("/propagation.html")
def propagation():
#get solar data in XML format and convert to json
solar_data={}
url = "https://www.hamqsl.com/solarxml.php"
try:
logging.debug("connection to: " + url)
req = requests.get(url)
logger.debug(req.content)
solar_data = xmltodict.parse(req.content)
logger.debug(solar_data)
except Exception as e1:
logging.error(e1)
response = flask.Response(
render_template(
"propagation.html",
inline_script_nonce=get_nonce(),
mycallsign=cfg["mycallsign"],
telnet=cfg["telnet"]["host"]+":"+cfg["telnet"]["port"],
mail=cfg["mail"],
menu_list=cfg["menu"]["menu_list"],
2024-03-24 06:47:27 +00:00
visits=len(visits),
2023-12-02 08:29:26 +00:00
solar_data=solar_data
)
)
2024-03-10 07:09:01 +00:00
#response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
2023-12-02 08:29:26 +00:00
return response
2023-01-07 22:15:43 +00:00
@app.route("/cookies.html", methods=["GET"])
2020-06-16 08:17:07 +00:00
def cookies():
2023-01-07 22:15:43 +00:00
response = flask.Response(
render_template(
"cookies.html",
2023-01-15 05:59:58 +00:00
inline_script_nonce=get_nonce(),
2023-01-07 22:15:43 +00:00
mycallsign=cfg["mycallsign"],
2023-11-12 08:13:10 +00:00
telnet=cfg["telnet"]["host"]+":"+cfg["telnet"]["port"],
2023-01-07 22:15:43 +00:00
mail=cfg["mail"],
menu_list=cfg["menu"]["menu_list"],
2024-03-24 06:47:27 +00:00
visits=len(visits),
2023-01-07 22:15:43 +00:00
)
)
2020-09-26 16:07:19 +00:00
return response
2020-06-16 08:17:07 +00:00
2023-01-07 22:15:43 +00:00
@app.route("/privacy.html", methods=["GET"])
2020-11-08 10:44:50 +00:00
def privacy():
2023-01-07 22:15:43 +00:00
response = flask.Response(
render_template(
"privacy.html",
2023-01-15 05:59:58 +00:00
inline_script_nonce=get_nonce(),
2023-01-07 22:15:43 +00:00
mycallsign=cfg["mycallsign"],
2023-11-12 08:13:10 +00:00
telnet=cfg["telnet"]["host"]+":"+cfg["telnet"]["port"],
2023-01-07 22:15:43 +00:00
mail=cfg["mail"],
menu_list=cfg["menu"]["menu_list"],
2024-03-24 06:47:27 +00:00
visits=len(visits),
2023-01-07 22:15:43 +00:00
)
)
2020-11-08 10:44:50 +00:00
return response
2023-01-07 22:15:43 +00:00
@app.route("/sitemap.xml")
2020-03-14 16:48:56 +00:00
def sitemap():
2023-01-07 22:15:43 +00:00
return app.send_static_file("sitemap.xml")
2020-03-14 16:48:56 +00:00
2023-01-07 22:15:43 +00:00
@app.route("/callsign.html", methods=["GET"])
2020-09-20 05:10:13 +00:00
def callsign():
2023-01-07 22:15:43 +00:00
# payload=spotquery()
callsign = request.args.get("c")
response = flask.Response(
render_template(
"callsign.html",
2023-01-15 05:59:58 +00:00
inline_script_nonce=get_nonce(),
2023-01-07 22:15:43 +00:00
mycallsign=cfg["mycallsign"],
2023-11-12 08:13:10 +00:00
telnet=cfg["telnet"]["host"]+":"+cfg["telnet"]["port"],
2023-01-07 22:15:43 +00:00
mail=cfg["mail"],
menu_list=cfg["menu"]["menu_list"],
2024-03-24 06:47:27 +00:00
visits=len(visits),
2023-01-07 22:15:43 +00:00
timer_interval=cfg["timer"]["interval"],
callsign=callsign,
adxo_events=adxo_events,
continents=continents_cq,
bands=band_frequencies,
)
)
2021-12-12 14:34:59 +00:00
return response
2023-01-07 22:15:43 +00:00
# API that search a callsign and return all informations about that
@app.route("/callsign", methods=["GET"])
2021-12-12 14:34:59 +00:00
def find_callsign():
2023-01-07 22:15:43 +00:00
callsign = request.args.get("c")
response = pfxt.find(callsign)
2021-12-12 14:34:59 +00:00
if response is None:
2023-01-07 22:15:43 +00:00
response = flask.Response(status=204)
2020-09-20 05:10:13 +00:00
return response
2023-01-07 22:15:43 +00:00
2023-02-19 21:47:28 +00:00
@app.route("/plot_get_heatmap_data", methods=["POST"])
@csrf.exempt
2023-01-01 22:03:51 +00:00
def get_heatmap_data():
2023-02-19 21:47:28 +00:00
#continent = request.args.get("continent")
continent = request.json['continent']
2024-03-30 05:45:14 +00:00
logger.debug(request.get_json())
2023-01-07 22:15:43 +00:00
response = flask.Response(json.dumps(heatmap_cbp.get_data(continent)))
2023-01-01 22:03:51 +00:00
logger.debug(response)
if response is None:
2023-01-07 22:15:43 +00:00
response = flask.Response(status=204)
2023-01-01 22:03:51 +00:00
return response
2023-01-07 22:15:43 +00:00
2023-02-19 21:47:28 +00:00
@app.route("/plot_get_dx_spots_per_month", methods=["POST"])
@csrf.exempt
2023-01-01 22:03:51 +00:00
def get_dx_spots_per_month():
2023-01-07 22:15:43 +00:00
response = flask.Response(json.dumps(bar_graph_spm.get_data()))
2023-01-01 22:03:51 +00:00
logger.debug(response)
if response is None:
2023-01-07 22:15:43 +00:00
response = flask.Response(status=204)
return response
2023-01-01 22:03:51 +00:00
2023-02-19 21:47:28 +00:00
@app.route("/plot_get_dx_spots_trend", methods=["POST"])
@csrf.exempt
2023-01-01 22:03:51 +00:00
def get_dx_spots_trend():
2023-01-07 22:15:43 +00:00
response = flask.Response(json.dumps(line_graph_st.get_data()))
2023-01-01 22:03:51 +00:00
logger.debug(response)
if response is None:
2023-01-07 22:15:43 +00:00
response = flask.Response(status=204)
return response
2023-01-01 22:03:51 +00:00
2023-02-19 21:47:28 +00:00
@app.route("/plot_get_hour_band", methods=["POST"])
@csrf.exempt
2023-01-01 22:03:51 +00:00
def get_dx_hour_band():
2023-01-07 22:15:43 +00:00
response = flask.Response(json.dumps(bubble_graph_hb.get_data()))
2023-01-01 22:03:51 +00:00
logger.debug(response)
if response is None:
2023-01-07 22:15:43 +00:00
response = flask.Response(status=204)
return response
2020-11-08 10:44:50 +00:00
2023-01-07 22:15:43 +00:00
2023-02-19 21:47:28 +00:00
@app.route("/plot_get_world_dx_spots_live", methods=["POST"])
@csrf.exempt
2023-01-01 22:03:51 +00:00
def get_world_dx_spots_live():
2023-01-07 22:15:43 +00:00
response = flask.Response(json.dumps(geo_graph_wdsl.get_data()))
2023-01-01 22:03:51 +00:00
logger.debug(response)
if response is None:
2023-01-07 22:15:43 +00:00
response = flask.Response(status=204)
return response
2023-11-14 19:52:11 +00:00
@app.route("/csp-reports", methods=['POST'])
@csrf.exempt
def csp_reports():
report_data = request.get_data(as_text=True)
logger.warning("CSP Report:")
logger.warning(report_data)
response=flask.Response(status=204)
return response
2023-01-07 22:15:43 +00:00
2021-11-28 17:31:32 +00:00
@app.after_request
def add_security_headers(resp):
2023-01-15 05:59:58 +00:00
2023-01-07 22:15:43 +00:00
resp.headers["Strict-Transport-Security"] = "max-age=1000"
resp.headers["X-Xss-Protection"] = "1; mode=block"
resp.headers["X-Frame-Options"] = "SAMEORIGIN"
resp.headers["X-Content-Type-Options"] = "nosniff"
resp.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
2023-12-02 08:29:26 +00:00
#resp.headers["Access-Control-Allow-Origin"]= "sidc.be prop.kc2g.com www.hamqsl.com"
2024-03-10 07:09:01 +00:00
#resp.headers["Cache-Control"] = "public, no-cache"
resp.headers["Cache-Control"] = "public, no-cache, must-revalidate, max-age=900"
2023-01-07 22:15:43 +00:00
resp.headers["Pragma"] = "no-cache"
2023-12-02 08:29:26 +00:00
resp.headers["ETag"] = app.config["VERSION"]
#resp.headers["Report-To"] = '{"group":"csp-endpoint", "max_age":10886400, "endpoints":[{"url":"/csp-reports"}]}'
2023-01-10 21:10:55 +00:00
resp.headers["Content-Security-Policy"] = "\
2023-01-01 22:03:51 +00:00
default-src 'self';\
2023-01-15 05:59:58 +00:00
script-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'nonce-"+inline_script_nonce+"';\
2023-12-02 08:29:26 +00:00
style-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net;\
2023-01-01 22:03:51 +00:00
object-src 'none';base-uri 'self';\
2023-12-02 08:29:26 +00:00
connect-src 'self' cdn.jsdelivr.net cdnjs.cloudflare.com sidc.be prop.kc2g.com www.hamqsl.com;\
2023-01-01 22:03:51 +00:00
font-src 'self' cdn.jsdelivr.net;\
frame-src 'self';\
frame-ancestors 'none';\
2023-01-15 05:59:58 +00:00
form-action 'none';\
2023-12-02 08:29:26 +00:00
img-src 'self' data: cdnjs.cloudflare.com sidc.be prop.kc2g.com ;\
2023-01-01 22:03:51 +00:00
manifest-src 'self';\
media-src 'self';\
2023-01-10 21:10:55 +00:00
worker-src 'self';\
2023-11-14 19:52:11 +00:00
report-uri /csp-reports;\
2023-01-01 22:03:51 +00:00
"
2021-11-28 17:31:32 +00:00
return resp
2023-01-10 21:10:55 +00:00
2023-12-02 08:29:26 +00:00
#report-to csp-endpoint;\
2023-01-10 21:10:55 +00:00
#script-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'nonce-sedfGFG32xs';\
#script-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'nonce-"+inline_script_nonce+"';\
2023-01-07 22:15:43 +00:00
if __name__ == "__main__":
2024-03-10 07:09:01 +00:00
app.run(host="0.0.0.0")