diff --git a/Dockerfile b/Dockerfile index e34dfec..507f01f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,10 +12,10 @@ RUN chmod +x entrypoint.sh COPY index.py index.py COPY dbutils.py dbutils.py +COPY read.py read.py ADD core core ADD titles titles -ADD config config ADD log log ADD cert cert -ENTRYPOINT [ "/app/entrypoint.sh" ] \ No newline at end of file +ENTRYPOINT [ "/app/entrypoint.sh" ] diff --git a/core/aimedb.py b/core/aimedb.py index 3ac8b9d..5388600 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -145,7 +145,15 @@ class AimedbProtocol(Protocol): def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBLookupRequest(data) user_id = self.data.card.get_user_id_from_card(req.access_code) + is_banned = self.data.card.get_card_banned(req.access_code) + is_locked = self.data.card.get_card_locked(req.access_code) + if is_banned and is_locked: + ret.head.status = ADBStatus.BAN_SYS_USER + elif is_banned: + ret.head.status = ADBStatus.BAN_SYS + elif is_locked: + ret.head.status = ADBStatus.LOCK_USER ret = ADBLookupResponse.from_req(req.head, user_id) self.logger.info( @@ -157,8 +165,17 @@ class AimedbProtocol(Protocol): req = ADBLookupRequest(data) user_id = self.data.card.get_user_id_from_card(req.access_code) + is_banned = self.data.card.get_card_banned(req.access_code) + is_locked = self.data.card.get_card_locked(req.access_code) + ret = ADBLookupExResponse.from_req(req.head, user_id) - + if is_banned and is_locked: + ret.head.status = ADBStatus.BAN_SYS_USER + elif is_banned: + ret.head.status = ADBStatus.BAN_SYS + elif is_locked: + ret.head.status = ADBStatus.LOCK_USER + self.logger.info( f"access_code {req.access_code} -> user_id {ret.user_id}" ) @@ -237,7 +254,7 @@ class AimedbProtocol(Protocol): def handle_register(self, data: bytes, resp_code: int) -> bytes: req = ADBLookupRequest(data) user_id = -1 - + if self.config.server.allow_user_registration: user_id = self.data.user.create_user() diff --git a/core/allnet.py b/core/allnet.py index b4fbd00..279a3d8 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -34,6 +34,61 @@ class ALLNET_STAT(Enum): bad_machine = -2 bad_shop = -3 +class DLI_STATUS(Enum): + START = 0 + GET_DOWNLOAD_CONFIGURATION = 1 + WAIT_DOWNLOAD = 2 + DOWNLOADING = 3 + + NOT_SPECIFY_DLI = 100 + ONLY_POST_REPORT = 101 + STOPPED_BY_APP_RELEASE = 102 + STOPPED_BY_OPT_RELEASE = 103 + + DOWNLOAD_COMPLETE_RECENTLY = 110 + + DOWNLOAD_COMPLETE_WAIT_RELEASE_TIME = 120 + DOWNLOAD_COMPLETE_BUT_NOT_SYNC_SERVER = 121 + DOWNLOAD_COMPLETE_BUT_NOT_FIRST_RESUME = 122 + DOWNLOAD_COMPLETE_BUT_NOT_FIRST_LAUNCH = 123 + DOWNLOAD_COMPLETE_WAIT_UPDATE = 124 + + DOWNLOAD_COMPLETE_AND_ALREADY_UPDATE = 130 + + ERROR_AUTH_FAILURE = 200 + + ERROR_GET_DLI_HTTP = 300 + ERROR_GET_DLI = 301 + ERROR_PARSE_DLI = 302 + ERROR_INVALID_GAME_ID = 303 + ERROR_INVALID_IMAGE_LIST = 304 + ERROR_GET_DLI_APP = 305 + + ERROR_GET_BOOT_ID = 400 + ERROR_ACCESS_SERVER = 401 + ERROR_NO_IMAGE = 402 + ERROR_ACCESS_IMAGE = 403 + + ERROR_DOWNLOAD_APP = 500 + ERROR_DOWNLOAD_OPT = 501 + + ERROR_DISK_FULL = 600 + ERROR_UNINSTALL = 601 + ERROR_INSTALL_APP = 602 + ERROR_INSTALL_OPT = 603 + + ERROR_GET_DLI_INTERNAL = 900 + ERROR_ICF = 901 + ERROR_CHECK_RELEASE_INTERNAL = 902 + UNKNOWN = 999 # Not the actual enum val but it needs to be here as a catch-all + + @classmethod + def from_int(cls, num: int) -> "DLI_STATUS": + try: + return cls(num) + except ValueError: + return cls.UNKNOWN + class AllnetServlet: def __init__(self, core_cfg: CoreConfig, cfg_folder: str): super().__init__() @@ -303,6 +358,7 @@ class AllnetServlet: def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: req_raw = request.content.getvalue() + client_ip = Utils.get_ip_addr(request) try: req_dict: Dict = json.loads(req_raw) except Exception as e: @@ -320,11 +376,17 @@ class AllnetServlet: self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage") return "NG" - dl_report_data = DLReport(dl_data, dl_data_type) + rep = DLReport(dl_data, dl_data_type) - if not dl_report_data.validate(): - self.logger.warning(f"Failed to parse DL Report: Invalid format - {dl_report_data.err}") + if not rep.validate(): + self.logger.warning(f"Failed to parse DL Report: Invalid format - {rep.err}") return "NG" + + msg = f"{rep.serial} @ {client_ip} reported {rep.rep_type.name} download state {rep.rf_state.name} for {rep.gd} v{rep.dav}:"\ + f" {rep.tdsc}/{rep.tsc} segments downloaded for working files {rep.wfl} with {rep.dfl if rep.dfl else 'none'} complete." + + self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data) + self.logger.info(msg) return "OK" @@ -727,13 +789,13 @@ class DLReport: self.ot = data.get("ot") self.rt = data.get("rt") self.as_ = data.get("as") - self.rf_state = data.get("rf_state") + self.rf_state = DLI_STATUS.from_int(data.get("rf_state")) self.gd = data.get("gd") self.dav = data.get("dav") self.wdav = data.get("wdav") # app only self.dov = data.get("dov") self.wdov = data.get("wdov") # app only - self.__type = report_type + self.rep_type = report_type self.err = "" def validate(self) -> bool: @@ -741,14 +803,6 @@ class DLReport: self.err = "serial not provided" return False - if self.dfl is None: - self.err = "dfl not provided" - return False - - if self.wfl is None: - self.err = "wfl not provided" - return False - if self.tsc is None: self.err = "tsc not provided" return False @@ -757,18 +811,6 @@ class DLReport: self.err = "tdsc not provided" return False - if self.at is None: - self.err = "at not provided" - return False - - if self.ot is None: - self.err = "ot not provided" - return False - - if self.rt is None: - self.err = "rt not provided" - return False - if self.as_ is None: self.err = "as not provided" return False @@ -789,11 +831,11 @@ class DLReport: self.err = "dov not provided" return False - if (self.wdav is None or self.wdov is None) and self.__type == DLIMG_TYPE.app: + if (self.wdav is None or self.wdov is None) and self.rep_type == DLIMG_TYPE.app: self.err = "wdav or wdov not provided in app image" return False - if (self.wdav is not None or self.wdov is not None) and self.__type == DLIMG_TYPE.opt: + if (self.wdav is not None or self.wdov is not None) and self.rep_type == DLIMG_TYPE.opt: self.err = "wdav or wdov provided in opt image" return False diff --git a/core/data/schema/card.py b/core/data/schema/card.py index d8f5fc0..a95684e 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -64,6 +64,27 @@ class CardData(BaseData): return int(card["user"]) + def get_card_banned(self, access_code: str) -> Optional[bool]: + """ + Given a 20 digit access code as a string, check if the card is banned + """ + card = self.get_card_by_access_code(access_code) + if card is None: + return None + if card["is_banned"]: + return True + return False + def get_card_locked(self, access_code: str) -> Optional[bool]: + """ + Given a 20 digit access code as a string, check if the card is locked + """ + card = self.get_card_by_access_code(access_code) + if card is None: + return None + if card["is_locked"]: + return True + return False + def delete_card(self, card_id: int) -> None: sql = aime_card.delete(aime_card.c.id == card_id) diff --git a/docker-compose.yml b/docker-compose.yml index c43b6e5..6a35355 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,22 +5,23 @@ services: build: . volumes: - ./aime:/app/aime + - ./configs/config:/app/config environment: CFG_DEV: 1 CFG_CORE_SERVER_HOSTNAME: 0.0.0.0 CFG_CORE_DATABASE_HOST: ma.db CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached - CFG_CORE_AIMEDB_KEY: keyhere + CFG_CORE_AIMEDB_KEY: CFG_CHUNI_SERVER_LOGLEVEL: debug ports: - - "80:80" - - "8443:8443" - - "22345:22345" + - "80:80" + - "8443:8443" + - "22345:22345" - - "8080:8080" - - "8090:8090" + - "8080:8080" + - "8090:8090" depends_on: db: @@ -28,21 +29,29 @@ services: db: hostname: ma.db - image: mysql:8.0.31-debian + image: yobasystems/alpine-mariadb:10.11.5 environment: MYSQL_DATABASE: aime MYSQL_USER: aime MYSQL_PASSWORD: aime MYSQL_ROOT_PASSWORD: AimeRootPassword + MYSQL_CHARSET: utf8mb4 + MYSQL_COLLATION: utf8mb4_general_ci + ##Note: expose port 3306 to allow read.py importer into database, comment out when not needed + #ports: + # - "3306:3306" + ##Note: uncomment to allow mysql to create a persistent database, leave commented if you want to rebuild database from scratch often + #volumes: + # - ./AimeDB:/var/lib/mysql healthcheck: - test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-pAimeRootPassword"] timeout: 5s retries: 5 - memcached: hostname: ma.memcached - image: memcached:1.6.17-bullseye + image: memcached:1.6.22-alpine3.18 + command: [ "memcached", "-m", "1024", "-I", "128m" ] phpmyadmin: hostname: ma.phpmyadmin @@ -53,5 +62,5 @@ services: PMA_PASSWORD: AimeRootPassword APACHE_PORT: 8080 ports: - - "8080:8080" + - "9090:8080" diff --git a/docs/INSTALL_DOCKER.md b/docs/INSTALL_DOCKER.md new file mode 100644 index 0000000..edaf75b --- /dev/null +++ b/docs/INSTALL_DOCKER.md @@ -0,0 +1,246 @@ +# ARTEMiS - Docker Installation Guide + +This step-by-step guide will allow you to install a Contenerized Version of ARTEMiS inside Docker, some steps can be skipped assuming you already have pre-requisite components and modules installed. + +This guide assumes using Debian 12(bookworm-stable) as a Host Operating System for most of packages and modules. + +## Pre-Requisites: + +- Linux-Based Operating System (e.g. Debian, Ubuntu) +- Docker (https://get.docker.com) +- Python 3.9+ +- (optional) Git + +## Install Python3.9+ and Docker + +``` +(if this is a fresh install of the system) +sudo apt update && sudo apt upgrade + +(installs python3 and pip) +sudo apt install python3 python3-pip + +(installs docker) +curl -fsSL https://get.docker.com -o get-docker.sh +sh get-docker.sh + +(optionally install git) +sudo apt install git +``` + +## Get ARTEMiS + +If you installed git, clone into your choice of ARTEMiS git repository, e.g.: +``` +git clone +``` +If not, download the source package, and unpack it to the folder of your choice. + +## Prepare development/home configuration + +To build our Docker setup, first we need to create some folders and copy some files around +- Create 'aime', 'configs', 'AimeDB', and 'logs' folder in ARTEMiS root folder (where all source files exist) +- Inside configs folder, create 'config' folder, and copy all .yaml files from example_config to config (thats all files without nginx_example.conf) +- Edit .yaml files inside configs/config to suit your server needs +- Edit core.yaml inside configs/config: +``` +set server.listen_address: to "0.0.0.0" +set title.hostname: to machine's IP address, e.g. "192.168.x.x", depending on your network, or actual hostname if your configuration is already set for dns resolve +set database.host: to "ma.db" +set database.memcached_host: to "ma.memcached" +set aimedb.key: to "" +``` + +## Running Docker Compose + +After configuring, go to ARTEMiS root folder, and execute: + +``` +docker compose up -d +``` + +("-d" argument means detached or daemon, meaning you will regain control of your terminal and Containers will run in background) + +This will start pulling and building required images from network, after it's done, a development server should be running, with server accessible under machine's IP, frontend with port 8090, and PHPMyAdmin under port 9090. + +- To turn off the server, from ARTEMiS root folder, execute: + +``` +docker compose down +``` + +- If you changed some files around, and don't see your changes applied, execute: + +``` +(turn off the server) +docker compose down +(rebuild) +docker compose build +(turn on) +docker compose up -d +``` + +- If you need to see logs from containers running, execute: + +``` +docker compose logs +``` + +- add '-f' to the end if you want to follow logs. + +## Running commands + +If you need to execute python scripts supplied with the application, use `docker compose exec app python3