2021-02-12 18:24:30 +00:00
#!/usr/bin/python3
2023-12-01 16:31:09 +00:00
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
2021-03-01 10:25:04 +00:00
2024-01-05 19:16:18 +00:00
__version__ = ' 0.45.11 '
2021-08-16 13:24:37 +00:00
2023-12-01 16:31:09 +00:00
from distutils . util import strtobool
from json . decoder import JSONDecodeError
2023-01-29 21:36:55 +00:00
2023-12-01 16:31:09 +00:00
import eventlet
import eventlet . wsgi
import getopt
import os
import signal
import socket
import sys
2023-01-29 21:36:55 +00:00
2023-12-01 16:31:09 +00:00
from changedetectionio import store
from changedetectionio . flask_app import changedetection_app
2023-01-29 21:36:55 +00:00
2023-12-01 16:31:09 +00:00
# Only global so we can access it in the signal handler
app = None
datastore = None
2023-01-29 21:36:55 +00:00
2023-12-01 16:31:09 +00:00
# Parent wrapper or OS sends us a SIGTERM/SIGINT, do everything required for a clean shutdown
def sigshutdown_handler ( _signo , _stack_frame ) :
global app
2021-02-12 18:43:05 +00:00
global datastore
2023-12-01 16:31:09 +00:00
name = signal . Signals ( _signo ) . name
print ( f ' Shutdown: Got Signal - { name } ( { _signo } ), Saving DB to disk and calling shutdown ' )
datastore . sync_to_json ( )
print ( f ' Sync JSON to disk complete. ' )
# This will throw a SystemExit exception, because eventlet.wsgi.server doesn't know how to deal with it.
# Solution: move to gevent or other server in the future (#2014)
datastore . stop_thread = True
app . config . exit . set ( )
sys . exit ( )
def main ( ) :
global datastore
global app
datastore_path = None
do_cleanup = False
host = ' '
ipv6_enabled = False
port = os . environ . get ( ' PORT ' ) or 5000
ssl_mode = False
# On Windows, create and use a default path.
if os . name == ' nt ' :
datastore_path = os . path . expandvars ( r ' % APPDATA % \ changedetection.io ' )
os . makedirs ( datastore_path , exist_ok = True )
else :
# Must be absolute so that send_from_directory doesnt try to make it relative to backend/
datastore_path = os . path . join ( os . getcwd ( ) , " ../datastore " )
2021-04-30 06:47:13 +00:00
2023-12-01 16:31:09 +00:00
try :
opts , args = getopt . getopt ( sys . argv [ 1 : ] , " 6Ccsd:h:p: " , " port " )
except getopt . GetoptError :
print ( ' backend.py -s SSL enable -h [host] -p [port] -d [datastore path] ' )
sys . exit ( 2 )
2021-04-30 06:47:13 +00:00
2023-12-01 16:31:09 +00:00
create_datastore_dir = False
2021-04-30 06:47:13 +00:00
2023-12-01 16:31:09 +00:00
for opt , arg in opts :
if opt == ' -s ' :
ssl_mode = True
2021-04-30 06:47:13 +00:00
2023-12-01 16:31:09 +00:00
if opt == ' -h ' :
host = arg
2022-03-06 17:19:05 +00:00
2023-12-01 16:31:09 +00:00
if opt == ' -p ' :
port = int ( arg )
2021-04-30 06:47:13 +00:00
2023-12-01 16:31:09 +00:00
if opt == ' -d ' :
datastore_path = arg
2021-04-30 06:47:13 +00:00
2023-12-01 16:31:09 +00:00
if opt == ' -6 ' :
print ( " Enabling IPv6 listen support " )
ipv6_enabled = True
2021-04-30 06:47:13 +00:00
2023-12-01 16:31:09 +00:00
# Cleanup (remove text files that arent in the index)
if opt == ' -c ' :
do_cleanup = True
2022-02-08 20:12:44 +00:00
2023-12-01 16:31:09 +00:00
# Create the datadir if it doesnt exist
if opt == ' -C ' :
create_datastore_dir = True
2022-02-08 20:12:44 +00:00
2023-12-01 16:31:09 +00:00
# isnt there some @thingy to attach to each route to tell it, that this route needs a datastore
app_config = { ' datastore_path ' : datastore_path }
2021-04-30 06:47:13 +00:00
2023-12-01 16:31:09 +00:00
if not os . path . isdir ( app_config [ ' datastore_path ' ] ) :
if create_datastore_dir :
os . mkdir ( app_config [ ' datastore_path ' ] )
2021-04-30 06:47:13 +00:00
else :
2023-12-01 16:31:09 +00:00
print (
" ERROR: Directory path for the datastore ' {} ' does not exist, cannot start, please make sure the directory exists or specify a directory with the -d option. \n "
" Or use the -C parameter to create the directory. " . format ( app_config [ ' datastore_path ' ] ) , file = sys . stderr )
sys . exit ( 2 )
2021-04-30 06:47:13 +00:00
2023-12-01 16:31:09 +00:00
try :
datastore = store . ChangeDetectionStore ( datastore_path = app_config [ ' datastore_path ' ] , version_tag = __version__ )
except JSONDecodeError as e :
# Dont' start if the JSON DB looks corrupt
print ( " ERROR: JSON DB or Proxy List JSON at ' {} ' appears to be corrupt, aborting " . format ( app_config [ ' datastore_path ' ] ) )
print ( str ( e ) )
return
2023-09-14 11:07:01 +00:00
2023-12-01 16:31:09 +00:00
app = changedetection_app ( app_config , datastore )
2021-12-29 23:04:38 +00:00
2023-12-01 16:31:09 +00:00
signal . signal ( signal . SIGTERM , sigshutdown_handler )
signal . signal ( signal . SIGINT , sigshutdown_handler )
2021-12-29 23:04:38 +00:00
2023-12-01 16:31:09 +00:00
# Go into cleanup mode
if do_cleanup :
datastore . remove_unused_snapshots ( )
2022-03-30 08:51:10 +00:00
2023-12-01 16:31:09 +00:00
app . config [ ' datastore_path ' ] = datastore_path
2022-03-30 08:51:10 +00:00
2023-03-19 20:12:22 +00:00
2023-12-01 16:31:09 +00:00
@app.context_processor
def inject_version ( ) :
return dict ( right_sticky = " v {} " . format ( datastore . data [ ' version_tag ' ] ) ,
new_version_available = app . config [ ' NEW_VERSION_AVAILABLE ' ] ,
has_password = datastore . data [ ' settings ' ] [ ' application ' ] [ ' password ' ] != False
)
2021-12-29 22:57:30 +00:00
2023-12-01 16:31:09 +00:00
# Monitored websites will not receive a Referer header when a user clicks on an outgoing link.
# @Note: Incompatible with password login (and maybe other features) for now, submit a PR!
@app.after_request
def hide_referrer ( response ) :
if strtobool ( os . getenv ( " HIDE_REFERER " , ' false ' ) ) :
response . headers [ " Referrer-Policy " ] = " no-referrer "
2021-12-15 23:05:01 +00:00
return response
2023-12-01 16:31:09 +00:00
# Proxy sub-directory support
# Set environment var USE_X_SETTINGS=1 on this script
# And then in your proxy_pass settings
#
# proxy_set_header Host "localhost";
# proxy_set_header X-Forwarded-Prefix /app;
2022-09-15 13:25:23 +00:00
2023-12-01 16:31:09 +00:00
if os . getenv ( ' USE_X_SETTINGS ' ) :
print ( " USE_X_SETTINGS is ENABLED \n " )
from werkzeug . middleware . proxy_fix import ProxyFix
app . wsgi_app = ProxyFix ( app . wsgi_app , x_prefix = 1 , x_host = 1 )
2022-12-14 14:08:34 +00:00
2023-12-01 16:31:09 +00:00
s_type = socket . AF_INET6 if ipv6_enabled else socket . AF_INET
2021-02-12 18:24:30 +00:00
2023-12-01 16:31:09 +00:00
if ssl_mode :
# @todo finalise SSL config, but this should get you in the right direction if you need it.
eventlet . wsgi . server ( eventlet . wrap_ssl ( eventlet . listen ( ( host , port ) , s_type ) ,
certfile = ' cert.pem ' ,
keyfile = ' privkey.pem ' ,
server_side = True ) , app )
2022-06-13 10:41:53 +00:00
2023-12-01 16:31:09 +00:00
else :
eventlet . wsgi . server ( eventlet . listen ( ( host , int ( port ) ) , s_type ) , app )
2021-05-27 13:55:05 +00:00