diff --git a/docker-compose.yml b/docker-compose.yml index 6a35355..beab3a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: volumes: - ./aime:/app/aime - ./configs/config:/app/config + - ./cert:/app/cert environment: CFG_DEV: 1 @@ -14,7 +15,8 @@ services: CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached CFG_CORE_AIMEDB_KEY: CFG_CHUNI_SERVER_LOGLEVEL: debug - + + ##Note: comment 80 and 8443 when you plan to use with nginx ports: - "80:80" - "8443:8443" @@ -64,3 +66,18 @@ services: ports: - "9090:8080" + ##Note: uncomment to allow use nginx with artemis, don't forget to comment 80 and 8443 ports on artemis + #nginx: + # hostname: ma.nginx + # image: nginx:latest + # ports: + # - "80:80" + # - "443:443" + # - "8443:8443" + # volumes: + ##Note: copy example_config/example_nginx.conf to configs/nginx folder, edit it and rename to nginx.conf + # - ./configs/nginx:/etc/nginx/conf.d + # - ./cert:/etc/nginx/cert + # - ./logs/nginx:/var/log/nginx + # depends_on: + # - app \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 0903979..e05a771 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -258,6 +258,31 @@ python dbutils.py upgrade Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! +### Config + +Config file is located in `config/mai2.yaml`. + +| Option | Info | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| `crypto` | This option is used to enable the TLS Encryption | + + +If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key +is the version ID for Japanese (SDEZ) versions and `"{versionID}_int"` for Export (SDGA) versions, and the value +is an array containing `[key, iv, salt]` in order. + +Just copy your salt in here, no need to convert anything. + + +```yaml +crypto: + encrypted_only: False + keys: + 23: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] + "23_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] + "23_chn": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] +``` + ## Hatsune Miku Project Diva ### SBZV diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index a3781d6..e2ae746 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -2,6 +2,7 @@ server: enable: True loglevel: "info" news_msg: "" + use_https: False # for CRYSTAL PLUS and later or SUPERSTAR and later team: name: ARTEMiS # If this is set, all players that are not on a team will use this one by default. diff --git a/example_config/core.yaml b/example_config/core.yaml index 5042c61..613883f 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -46,7 +46,7 @@ allnet: allow_online_updates: False update_cfg_folder: "" save_billing: True - allnet_lite_key: [] + allnet_lite_keys: [] billing: standalone: True diff --git a/example_config/mai2.yaml b/example_config/mai2.yaml index f0d7754..52dd4be 100644 --- a/example_config/mai2.yaml +++ b/example_config/mai2.yaml @@ -1,6 +1,7 @@ server: enable: True loglevel: "info" + use_https: False # for DX and later deliver: enable: False diff --git a/example_config/nginx_example.conf b/example_config/nginx_example.conf index b01a822..5823ba4 100644 --- a/example_config/nginx_example.conf +++ b/example_config/nginx_example.conf @@ -66,6 +66,52 @@ server { } } +# WAHLAP Billing, they use 443 port +# comment this out if running billing standalone +# still not work for some reason, please set +# billing=127.0.0.1 in segatools.ini for now and looking for fix +server { + listen 443 ssl; + server_name bl.sys-all.cn; + + ssl_certificate /path/to/cert/server.pem; + ssl_certificate_key /path/to/cert/server.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers "ALL:@SECLEVEL=0"; + ssl_prefer_server_ciphers off; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_request_headers on; + proxy_pass http://127.0.0.1:8080/; + } +} + +server { + listen 443 ssl; + server_name bl.sys-allnet.cn; + + ssl_certificate /path/to/cert/server.pem; + ssl_certificate_key /path/to/cert/server.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers "ALL:@SECLEVEL=0"; + ssl_prefer_server_ciphers off; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_request_headers on; + proxy_pass http://127.0.0.1:8080/; + } +} + # Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend server { listen 80; diff --git a/titles/chuni/config.py b/titles/chuni/config.py index f0e15f3..95720bc 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -25,6 +25,12 @@ class ChuniServerConfig: return CoreConfig.get_config_field( self.__config, "chuni", "server", "news_msg", default="" ) + + @property + def use_https(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "chuni", "server", "use_https", default=False + ) class ChuniTeamConfig: diff --git a/titles/chuni/index.py b/titles/chuni/index.py index 1392588..cc25289 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -190,10 +190,26 @@ class ChuniServlet(BaseServlet): return True def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.server.hostname) + title_port_int = Utils.get_title_port(self.core_cfg) + title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) - return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", self.core_cfg.server.hostname) + if self.game_cfg.server.use_https and ( + (game_code == "SDBT" and game_ver >= 145) or # JP use TLS from CRYSTAL PLUS + game_code != "SDBT" # SDGS and SDHJ all version can use TLS + ): + proto = "https" + else: + proto = "http" + + if proto == "https": + t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" + else: + t_port = f":{title_port_int}" if title_port_int != 80 else "" + + return ( + f"{proto}://{self.core_cfg.server.hostname}{t_port}/{game_code}/{game_ver}/", + f"{self.core_cfg.server.hostname}", + ) def get_routes(self) -> List[Route]: return [ diff --git a/titles/mai2/config.py b/titles/mai2/config.py index efd3ba5..66c1c94 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -20,6 +20,12 @@ class Mai2ServerConfig: self.__config, "mai2", "server", "loglevel", default="info" ) ) + + @property + def use_https(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "server", "use_https", default=False + ) class Mai2DeliverConfig: def __init__(self, parent: "Mai2Config") -> None: diff --git a/titles/mai2/index.py b/titles/mai2/index.py index d59ce87..b302985 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -38,7 +38,7 @@ class Mai2Servlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) self.game_cfg = Mai2Config() - self.hash_table: Dict[int, Dict[str, str]] = {} + self.hash_table: Dict[str, Dict[str, str]] = {} if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): self.game_cfg.update( yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) @@ -97,16 +97,21 @@ class Mai2Servlet(BaseServlet): self.logger.initted = True for version, keys in self.game_cfg.crypto.keys.items(): - if version < Mai2Constants.VER_MAIMAI_DX: + if int(str(version).split('_')[0]) < Mai2Constants.VER_MAIMAI_DX: continue if len(keys) < 3: continue + if isinstance(version, int): + version_idx = version + else: + version_idx = int(version.split("_")[0]) + self.hash_table[version] = {} method_list = [ method - for method in dir(self.versions[version]) + for method in dir(self.versions[version_idx]) if not method.startswith("__") ] @@ -115,6 +120,21 @@ class Mai2Servlet(BaseServlet): # remove the first 6 chars and the final 7 chars to get the canonical # endpoint name. method_fixed = inflection.camelize(method)[6:-7] + + # This only applies for maimai DX International and later for some reason. + if ( + isinstance(version, str) + and version.endswith("_int") + and version_idx >= Mai2Constants.VER_MAIMAI_DX_UNIVERSE + ): + method_fixed += "MaimaiExp" + elif ( + isinstance(version, str) + and version.endswith("_chn") + and version_idx >= Mai2Constants.VER_MAIMAI_DX_UNIVERSE # 1.00, 1.11 and 1.20 all use DX, but they add MaimaiChn in 1.20, we set 1.20 to use UNIVERSE code + ): + method_fixed += "MaimaiChn" + hash = MD5.new((method_fixed + keys[2]).encode()) # truncate unused bytes like the game does @@ -157,14 +177,29 @@ class Mai2Servlet(BaseServlet): ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return ( - f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", - f"{self.core_cfg.server.hostname}", - ) + title_port_int = Utils.get_title_port(self.core_cfg) + title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) + + if self.game_cfg.server.use_https: + if (game_code == "SDEZ" and game_ver >= 114) or (game_code == "SDGA" and game_ver >= 110): # SDEZ and SDGA use tls from Splash version + proto = "" # game will auto add https:// in uri with original code + elif game_code == "SDGB" and game_ver >= 130: # SDGB use tls from 1.30 + # game will check if uri start with "http:", if yes, set IsHttpConnection = true + # so we can return https://example.com or http://example.com, all will work + proto = "https://" + else: + # "maimai", SDEZ 1.00 ~ 1.13, SDGA 1.00 ~ 1.06 and SDGB 1.01, 1.20 use http:// + proto = "http://" + else: + proto = "http://" + + if proto == "" or proto == "https://": + t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" + else: + t_port = f":{title_port_int}" if title_port_int != 80 else "" return ( - f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", + f"{proto}{self.core_cfg.server.hostname}{t_port}/{game_code}/{game_ver}/", f"{self.core_cfg.server.hostname}", ) @@ -310,7 +345,7 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES elif version >= 145 and version <150: # BUDDiES PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS - elif version >=150: + elif version >= 150: # PRiSM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM elif game_code == "SDGA": # Int @@ -334,47 +369,58 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES elif version >= 145 and version <150: # BUDDiES PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS - elif version >=150: + elif version >= 150: # PRiSM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM elif game_code == "SDGB": # Chn if version < 110: # Muji internal_ver = Mai2Constants.VER_MAIMAI_DX elif version >= 110 and version < 120: # Muji - internal_ver = Mai2Constants.VER_MAIMAI_DX + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH # still DX, but need Splash to set encryption key elif version >= 120 and version < 130: # Muji (LMAO) - internal_ver = Mai2Constants.VER_MAIMAI_DX + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE # still DX, but need UNIVERSE to set encryption key elif version >= 130 and version < 140: # FESTiVAL internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL elif version >= 140 and version < 150: # BUDDiES internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES - elif version >=150: + elif version >= 150: # PRiSM internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: + + if game_code == "SDGA": + crypto_cfg_key = f"{internal_ver}_int" + hash_table_key = f"{internal_ver}_int" + elif game_code == "SDGB": + crypto_cfg_key = f"{internal_ver}_chn" + hash_table_key = f"{internal_ver}_chn" + else: + crypto_cfg_key = internal_ver + hash_table_key = internal_ver + # If we get a 32 character long hex string, it's a hash and we're # dealing with an encrypted request. False positives shouldn't happen # as long as requests are suffixed with `Api`. - if internal_ver not in self.hash_table: + if hash_table_key not in self.hash_table: self.logger.error( "v%s does not support encryption or no keys entered", version, ) return Response(zlib.compress(b'{"stat": "0"}')) - elif endpoint.lower() not in self.hash_table[internal_ver]: + elif endpoint.lower() not in self.hash_table[hash_table_key]: self.logger.error( "No hash found for v%s endpoint %s", version, endpoint ) return Response(zlib.compress(b'{"stat": "0"}')) - endpoint = self.hash_table[internal_ver][endpoint.lower()] + endpoint = self.hash_table[hash_table_key][endpoint.lower()] try: crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]), ) req_raw = crypt.decrypt(req_raw) @@ -392,7 +438,10 @@ class Mai2Servlet(BaseServlet): if ( not encrypted and self.game_cfg.crypto.encrypted_only - and version >= 110 + and ( + # SDEZ start from 1.10, SDGA and SDGB keep use encryption from 1.00 + internal_ver >= Mai2Constants.VER_MAIMAI_DX_PLUS or (game_code == "SDGA" or game_code == "SDGB") + ) ): self.logger.error( "Unencrypted v%s %s request, but config is set to encrypted only: %r", @@ -416,9 +465,9 @@ class Mai2Servlet(BaseServlet): endpoint = ( endpoint.replace("MaimaiExp", "") - if game_code == Mai2Constants.GAME_CODE_DX_INT + if game_code == Mai2Constants.GAME_CODE_DX_INT and version >= 120 else endpoint.replace("MaimaiChn", "") - if game_code == Mai2Constants.GAME_CODE_DX_CHN + if game_code == Mai2Constants.GAME_CODE_DX_CHN and version >= 120 else endpoint ) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" @@ -444,15 +493,17 @@ class Mai2Servlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - if not encrypted or version < 110: + if not encrypted or ( + internal_ver < Mai2Constants.VER_MAIMAI_DX_PLUS and game_code == "SDEZ" + ): return Response(zipped) padded = pad(zipped, 16) crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]), ) return Response(crypt.encrypt(padded))