446 lines
18 KiB
Python
Executable File
446 lines
18 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import router # Custom library that has router controls
|
|
import urllib.request
|
|
import json
|
|
|
|
import time
|
|
import traceback
|
|
import os
|
|
import datetime
|
|
import sys
|
|
import glob
|
|
import string
|
|
|
|
whitelist = set(string.ascii_letters + string.digits + "/" + "@" + "." + "-" + "_")
|
|
|
|
|
|
def sanitize(
|
|
txt,
|
|
): # Is a very simple sanitizer that allows all ascii_letters numbers and the / and @
|
|
if type(txt) == type(1) or type(txt) == type(
|
|
True
|
|
): # Makes sure that if it is an int or a bool it will not be sanitized because that is not neccessary.
|
|
return txt
|
|
else:
|
|
return "".join([ch for ch in txt if ch in whitelist])
|
|
|
|
|
|
def error(e):
|
|
return "".join(
|
|
traceback.format_exception(etype=type(e), value=e, tb=e.__traceback__)
|
|
)
|
|
|
|
|
|
def temp(): # Returns the temprature of the RPI
|
|
return readFile("/sys/class/thermal/thermal_zone0/temp") / 1000
|
|
|
|
|
|
def writeFile(
|
|
location, info, permission=True
|
|
): # Will write info in json format to a file
|
|
with open(location, "w") as f:
|
|
json.dump(info, f)
|
|
if permission:
|
|
os.system("chown -R www-data:www-data " + location)
|
|
|
|
|
|
# Loads the location of a certain file and returns that file if it is json
|
|
def readFile(location):
|
|
with open(location) as f:
|
|
return json.load(f)
|
|
|
|
|
|
try:
|
|
# Looks at the configuration at understands the config
|
|
try:
|
|
configFilePath = __file__[: __file__.rindex("/") + 1]
|
|
# Makes sure the python file area is owned by root and not accessable by www-data for security reasons
|
|
os.system("chmod 750 -R " + configFilePath)
|
|
os.system("chown -R root:root " + configFilePath)
|
|
configFilePath = configFilePath + "config.json"
|
|
# Will find the location where the config should be located.
|
|
location = __file__[: __file__.rindex("/python/restart.py") + 1] + "html/"
|
|
# Creates config with the enviromental variables
|
|
developmentMachine = os.getenv("WEBSITE_DEVELOPER", "false") == "true"
|
|
# This stores a list for the default config
|
|
envConfiguration = [
|
|
[["passwordOptions", "cost"], int(os.getenv("PASSWORD_ROUNDS", "10"))],
|
|
[["database", "username"], os.getenv("DATABASE_USERNAME", "admin")],
|
|
[["database", "name"], os.getenv("DATABASE_NAME", "website")],
|
|
[["database", "password"], os.getenv("DATABASE_PASSWORD", "password")],
|
|
[["database", "host"], os.getenv("DATABASE_HOST", "localhost")],
|
|
[["database", "port"], int(os.getenv("DATABASE_PORT", "3306"))],
|
|
[
|
|
["database", "backupLocation"],
|
|
os.getenv("WEBSITE_BACKUP_LOCATION", "/backup"),
|
|
],
|
|
[
|
|
["database", "backupLength"],
|
|
int(os.getenv("WEBSITE_BACKUP_LENGTH", "604800")),
|
|
],
|
|
[["developer"], developmentMachine],
|
|
[["throttle"], int(os.getenv("WEBSITE_THROTTLE", "5"))],
|
|
[["throttleTime"], int(os.getenv("WEBSITE_THROTTLE_TIME", "30"))],
|
|
[["fanStart"], int(os.getenv("WEBSITE_FAN_START", "43"))],
|
|
[["fanStop"], int(os.getenv("WEBSITE_FAN_STOP", "35"))],
|
|
[["matomoDomain"], os.getenv("MATOMO_DOMAIN", "example.com")],
|
|
[["matomoSiteId"], int(os.getenv("MATOMO_SITE_ID", "1"))],
|
|
[["turnstileSecret"], os.getenv("TURNSTILE_SECRET", "")],
|
|
[["turnstileSitekey"], os.getenv("TURNSTILE_SITEKEY", "")],
|
|
[["TMDBApiKey"], os.getenv("TMDB_API_KEY", "")],
|
|
]
|
|
if os.path.exists(location + "/config.json"):
|
|
configuration = readFile(location + "/config.json")
|
|
else:
|
|
# Sets an empty config if none is found to be filled with the env vars
|
|
configuration = {}
|
|
print("Setting config with enviromental variables")
|
|
# Goes through config to make sure values are sanitized and that they exist
|
|
except:
|
|
print("Could not create config")
|
|
raise Exception
|
|
# Sanitizes and makes sure that config is complete.
|
|
try:
|
|
for x in envConfiguration:
|
|
if len(x[0]) == 1:
|
|
# Checks if the key value pair exists or if it has to be created and the enviromental variable value has to be used
|
|
if x[0][0] in configuration:
|
|
configuration[x[0][0]] = sanitize(configuration[x[0][0]])
|
|
else:
|
|
configuration[x[0][0]] = sanitize(x[1])
|
|
else:
|
|
# Checks if the key value pair exists or if it has to be created and the enviromental variable value has to be used
|
|
if x[0][0] in configuration:
|
|
if x[0][1] in configuration[x[0][0]]:
|
|
configuration[x[0][0]][x[0][1]] = sanitize(
|
|
configuration[x[0][0]][x[0][1]]
|
|
)
|
|
else:
|
|
configuration[x[0][0]][x[0][1]] = sanitize(x[1])
|
|
else:
|
|
configuration[x[0][0]] = {x[0][1]: sanitize(x[1])}
|
|
except:
|
|
print("Failed sanitizing the config")
|
|
raise Exception
|
|
writeFile(f"{location}/config.json", configuration)
|
|
developmentMachine = configuration["developer"]
|
|
backupLocation = configuration["database"]["backupLocation"]
|
|
# Makes sure that the permissions are not wrong
|
|
os.system("chown -R www-data:www-data " + location)
|
|
os.system(f"chmod 644 -R {backupLocation}")
|
|
os.system("chmod 750 -R " + location)
|
|
f = open(location + "/maintenance-mode", "w")
|
|
f.close()
|
|
while (
|
|
True
|
|
): # Imports this library in a slow way because it is a pain and likes to not work
|
|
try:
|
|
import database
|
|
|
|
break
|
|
except:
|
|
continue
|
|
internetOnDeafult = ["23", "0", "5", "0", "2147483000", "1"]
|
|
internet = False
|
|
|
|
def writeLog(message, kind): # Will automatically add to the log
|
|
print(message, time.time())
|
|
try:
|
|
database.appendValue("log", [str(kind), message, str(time.time())])
|
|
except:
|
|
print("Could not log message")
|
|
|
|
# Checks if an action is neccessary to do on the wifi
|
|
|
|
def internetAction(times, rule, status):
|
|
timeStart = int(rule[0]) * 60 + int(rule[1])
|
|
timeEnd = int(rule[2]) * 60 + int(rule[3])
|
|
times = int(times[3]) * 60 + int(times[4])
|
|
if timeEnd < timeStart:
|
|
if timeEnd > times:
|
|
times += 60 * 24
|
|
timeEnd += 60 * 24
|
|
if times >= timeStart and times <= timeEnd:
|
|
if status:
|
|
writeLog("Internet turning off", 6)
|
|
if not developmentMachine:
|
|
status = router.turnOffInternet()
|
|
else:
|
|
status = False
|
|
writeLog("Internet turned off", 6)
|
|
else:
|
|
if not status:
|
|
writeLog("Internet turning on", 7)
|
|
if not developmentMachine:
|
|
status = router.turnOnInternet()
|
|
else:
|
|
status = True
|
|
writeLog("Internet turned on", 7)
|
|
return status
|
|
|
|
def callTime(): # Will return the time
|
|
time = datetime.datetime.now()
|
|
time2 = [
|
|
time.strftime("%b"),
|
|
time.strftime("%d"),
|
|
time.strftime("%Y"),
|
|
time.strftime("%H"),
|
|
time.strftime("%M"),
|
|
time.strftime("%S"),
|
|
time.strftime("%-m"),
|
|
]
|
|
return time2
|
|
|
|
def buttonPress(status): # Will run this script everytime the button is pressed
|
|
minimum = database.search("internet", "id=(SELECT MIN(id) FROM internet)")
|
|
minimum = int(minimum[5]) - 1
|
|
if minimum == 0:
|
|
minimum = -1
|
|
if status:
|
|
writeLog("Internet Schedule changed to off due to button", 5)
|
|
database.appendValue(
|
|
"internet",
|
|
["0", "0", "23", "59", str(time.time() + 3600), str(minimum)],
|
|
)
|
|
else:
|
|
writeLog("Internet Schedule changed to on due to button", 5)
|
|
database.appendValue(
|
|
"internet", ["2", "1", "2", "1", str(time.time() + 3600), str(minimum)]
|
|
)
|
|
|
|
def backupDatabase(): # Used to backup the database
|
|
timeData = callTime()
|
|
month = timeData[0]
|
|
day = timeData[1]
|
|
year = timeData[2]
|
|
hour = timeData[3]
|
|
minute = timeData[4]
|
|
file = f"{int(time.time())}or{month}-{day}-{year}at{hour}:{minute}.sql"
|
|
database.backUp(file)
|
|
return file
|
|
|
|
def dailyMaintenance(): # Will run daily and on boot
|
|
try:
|
|
if not os.path.isfile(
|
|
location + "restore.json"
|
|
): # Makes sure that it does not disrupt a restore
|
|
file = backupDatabase()
|
|
writeLog(f"Ran backup on server and saved it to {file}", 18)
|
|
else:
|
|
writeLog("Skipped backup due to a restore command", 18)
|
|
except:
|
|
writeLog("Database backup failed", 9)
|
|
# Will find all backup files and check them.
|
|
backupLocation = configuration["database"]["backupLocation"]
|
|
files = glob.glob(f"{backupLocation}/*.sql")
|
|
for x in range(len(files)):
|
|
files[x] = files[x][len(backupLocation) + 1 :]
|
|
for x in files:
|
|
try:
|
|
if (
|
|
int(x[: x.find("or")])
|
|
< time.time() - configuration["database"]["backupLength"]
|
|
):
|
|
os.remove(f"{backupLocation}/{x}")
|
|
writeLog(f"Removed old backup due to age, named {x}", 20)
|
|
except:
|
|
os.remove(f"{backupLocation}/{x}")
|
|
# Gets a list of all backups to store in a file for easier access.
|
|
files = glob.glob(f"{backupLocation}/*.sql")
|
|
for x in range(len(files)):
|
|
files[x] = files[x][len(backupLocation) + 1 :]
|
|
files.sort(reverse=True)
|
|
writeFile(location + "/backups.json", files)
|
|
# Will repair all databases and update them
|
|
repaired, updates = database.repair()
|
|
for x in updates:
|
|
writeLog(f"Database updated to version {x}", 19)
|
|
for x in repaired:
|
|
writeLog(f"Database {x} was corrupted/missing and was restored", 9)
|
|
# Will clean the golf games database
|
|
deleted = database.trueSearch(
|
|
f"SELECT ID FROM golfGame WHERE turnStartTime<{time.time()-86400}"
|
|
)
|
|
for x in deleted:
|
|
writeLog(f"Game #{x[0]} deleted because it is too old", 16)
|
|
database.command(
|
|
f"DELETE FROM golfGame WHERE turnStartTime<{time.time()-86400}"
|
|
) # Removes games that have not been touched for more than 24 hours
|
|
database.command(
|
|
"DELETE FROM golfGamePlayers WHERE NOT EXISTS (SELECT * FROM golfGame WHERE golfGamePlayers.gameID = golfGame.ID)"
|
|
) # Removes players from games that do not exist
|
|
database.command(
|
|
"DELETE FROM golfGameCards WHERE NOT EXISTS (SELECT * FROM golfGame WHERE golfGameCards.gameID = golfGame.ID)"
|
|
) # Removes players from games that do not exist
|
|
database.command(
|
|
f"DELETE FROM cookieClicker WHERE lastUpdate<{(time.time()-86400*30)*1000}"
|
|
) # Removes cookie clicker data that has not been touched for a month
|
|
writeLog("Server maintenance ran succesfully.", 12)
|
|
|
|
# Waits until the database is ready
|
|
while True:
|
|
try:
|
|
db, cursor = database.connect()
|
|
cursor.close()
|
|
db.close()
|
|
break
|
|
except Exception as e:
|
|
print(f"Database connection failed: {e}")
|
|
time.sleep(1)
|
|
continue
|
|
# Runs stuff that runs every boot
|
|
dailyMaintenance()
|
|
# Will turn on the internet to make sure that it is on
|
|
if not developmentMachine:
|
|
internetOn = router.turnOnInternet()
|
|
else:
|
|
internetOn = True
|
|
# Will try to open the json location and will create a new file if the old one is gone and will store the date in it.
|
|
try:
|
|
info = readFile(location + "data.json")
|
|
info.append(info[-1])
|
|
info.append(callTime())
|
|
info.append(callTime())
|
|
info.append(callTime())
|
|
except:
|
|
info = [callTime(), callTime()]
|
|
writeFile(location + "data.json", info)
|
|
# Makes sure that an extra backup does not happen on boot
|
|
lastBackup = callTime()[1]
|
|
try:
|
|
os.remove(location + "maintenance-mode")
|
|
except:
|
|
1
|
|
fanOn = False
|
|
writeLog("Server has finished booting procedure", 12)
|
|
# Will update the time every minute to make sure electricity outages are reported to the minute precise and will request a check to see if the wifi status needs to be changed
|
|
while True:
|
|
try:
|
|
info = readFile(location + "data.json")
|
|
info[-1] = callTime()
|
|
except:
|
|
writeLog("Electricity log error", 9)
|
|
info = [callTime(), callTime()]
|
|
writeFile(location + "data.json", info)
|
|
if lastBackup != callTime()[1]:
|
|
f = open(location + "maintenance-mode", "w")
|
|
f.close()
|
|
try:
|
|
# Will run the daily script
|
|
dailyMaintenance()
|
|
except:
|
|
writeLog("Daily maintenance failed.", 9)
|
|
os.remove(location + "maintenance-mode")
|
|
lastBackup = callTime()[1]
|
|
try:
|
|
minimum = database.search("internet", "id=(SELECT MIN(id) FROM internet)")
|
|
if not minimum:
|
|
database.appendValue("internet", internetOnDeafult)
|
|
minimum = internetOnDeafult
|
|
writeLog("No internet schedule found creating a new one", 8)
|
|
while int(minimum[4]) < time.time():
|
|
oldMinimum = minimum
|
|
database.delete("internet", f"id={minimum[5]}")
|
|
minimum = database.search(
|
|
"internet", "id=(SELECT MIN(id) FROM internet)"
|
|
)
|
|
if not minimum:
|
|
database.appendValue("internet", internetOnDeafult)
|
|
minimum = internetOnDeafult
|
|
writeLog(
|
|
f"Changing internet schedule from; {oldMinimum[0]}:{oldMinimum[1]} to {oldMinimum[2]}:{oldMinimum[3]}, to {minimum[0]}:{minimum[1]} to {minimum[2]}:{minimum[3]}",
|
|
8,
|
|
)
|
|
skip = False
|
|
except Exception:
|
|
writeLog("Schedule could not be updated, skipped internet check", 9)
|
|
skip = True
|
|
try:
|
|
if not skip:
|
|
internetOn = internetAction(callTime(), minimum[0:4], internetOn)
|
|
except:
|
|
writeLog("Internet check failed", 9)
|
|
if not developmentMachine:
|
|
internetOn = router.turnOnInternet()
|
|
else:
|
|
internetOn = True
|
|
# Will check every 2 seconds if the button is pressed and when it is show it on the led and then wait another second to verify that it is an actual press
|
|
while True:
|
|
time.sleep(2)
|
|
if os.path.isfile(location + "restart.json"): # Used to restart the server
|
|
writeLog("Server is being restarted", 12)
|
|
os.remove(location + "restart.json")
|
|
os.system(f"python3 {__file__} restart")
|
|
exit()
|
|
if os.path.isfile(location + "update.json"): # Used to update the server
|
|
writeLog("Server is being updated.", 19)
|
|
os.remove(location + "update.json")
|
|
os.system(
|
|
f"git --work-tree={location[:-6]} --git-dir={location[:-5]}.git reset --hard"
|
|
)
|
|
os.system(
|
|
f"git --work-tree={location[:-6]} --git-dir={location[:-5]}.git pull > {location}updateInfo.log"
|
|
)
|
|
os.system(f"{location[:-5]}python/update.sh")
|
|
os.system(f"composer --working-dir={location} update")
|
|
os.system(f"chown www-data:www-data {location}updateInfo.log")
|
|
writeLog("Server updated successfully.", 19)
|
|
if os.path.isfile(
|
|
location + "restore.json"
|
|
): # Used to restore a previous version of the database
|
|
try:
|
|
file = readFile(location + "restore.json")
|
|
f = open(location + "maintenance-mode", "w")
|
|
backups = readFile(location + "backups.json")
|
|
# Finds the latest backup if latest is specified
|
|
if file == "latest":
|
|
biggest = 0
|
|
for x in backups:
|
|
try:
|
|
check = int(x[: x.find("or")])
|
|
if check > biggest:
|
|
biggest = check
|
|
file = x
|
|
except:
|
|
1
|
|
if file not in backups:
|
|
writeLog(f"Could not find backup", 9)
|
|
raise Exception
|
|
f.close()
|
|
backupDatabase()
|
|
database.restore(file)
|
|
dailyMaintenance()
|
|
writeLog(f"Backup {file} was succesfully restored", 18)
|
|
except:
|
|
writeLog(f"Restore for {file} failed", 9)
|
|
# Makes sure to clean up after it is done
|
|
try:
|
|
os.remove(location + "restore.json")
|
|
except:
|
|
1
|
|
try:
|
|
os.remove(location + "maintenance-mode")
|
|
except:
|
|
1
|
|
# Alternative to simulate a button press by putting button into this folder
|
|
elif os.path.isfile(location + "button.json"):
|
|
try:
|
|
buttonPress(internetOn)
|
|
os.remove(location + "button.json")
|
|
except:
|
|
writeLog("Button press failed", 9)
|
|
if time.time() % 60 <= 2:
|
|
break
|
|
except Exception as e:
|
|
try:
|
|
with open(location + "error.log", "w") as f:
|
|
f.write(error(e))
|
|
f = open(location + "maintenance-mode", "w")
|
|
f.close()
|
|
os.system("chmod 750 -R " + location)
|
|
os.system("chown -R www-data:www-data " + location)
|
|
except Exception:
|
|
print("crash")
|
|
while True:
|
|
time.sleep(1)
|