├── nosauth ├── __init__.py ├── all_certs.pem └── api.py ├── example.py ├── setup.py ├── LICENSE ├── .gitignore └── README.md /nosauth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from nosauth import api 2 | 3 | api = api.NtLauncher(locale="pl_PL", gfLang="pl") 4 | 5 | if not api.auth(username="admin", password="admin"): 6 | print("Couldn't auth!") 7 | exit() 8 | 9 | accounts = api.getAccounts() 10 | if len(accounts) == 0: 11 | print("You don't have any any account") 12 | 13 | for uid, displayName in accounts: 14 | print("Account key:", uid, "Account name:", displayName) 15 | 16 | uid, displayName = accounts[0] 17 | token = api.getToken(uid) 18 | 19 | if token: 20 | print(token) 21 | 22 | else: 23 | print("Couldn't obtain token!") 24 | 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup 3 | 4 | # The directory containing this file 5 | HERE = pathlib.Path(__file__).parent 6 | 7 | # The text of the README file 8 | README = (HERE / "README.md").read_text() 9 | 10 | setup( 11 | name="nosauth", 12 | version="0.1.5", 13 | description="Library that lets you obtain auth token so you can login to NosTale official servers", 14 | long_description=README, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/morsisko/NosTale-Auth", 17 | author="morsisko", 18 | license="MIT", 19 | classifiers=[ 20 | "License :: OSI Approved :: MIT License", 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 3", 23 | "Development Status :: 5 - Production/Stable", 24 | ], 25 | packages=["nosauth"], 26 | install_requires=["requests"], 27 | package_data={"nosauth": ["all_certs.pem"]}, 28 | include_package_data=True, 29 | ) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Michael 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /nosauth/all_certs.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | YkcHK389cwyUhmBoGkTD8J0k8Yu7Vg== 52 | -----END PRIVATE KEY----- 53 | -----BEGIN CERTIFICATE----- 54 | MIIEAzCCAusCAQEwDQYJKoZIhvcNAQELBQAwcTELMAkGA1UEBhMCREUxEjAQBgNV 55 | BAgTCUthcmxzcnVoZTEbMBkGA1UEBxMSQmFkZW4tV3VlcnR0ZW1iZXJnMRowGAYD 56 | VQQKExFHYW1lZm9yZ2UgNEQgR21iSDEVMBMGA1UEAxMMRXZlbnQgSW5nZXN0MB4X 57 | DTE4MDQwMzE2MzExNFoXDTIyMDQwMjE2MzExNFowHjEcMBoGA1UEAwwTRXZlbnQg 58 | SW5nZXN0IENsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPMk 59 | abcgTSq6p1M5SkRGZyHXmGc+qZyub63K2oJLc+qJ8scaurMkrT3zRx95KN2nCMaY 60 | w+w1u7Yh+4S+YOVphfw7JTIAel649/blOViuTtUYNTKSwQJUVABA79dLxPpxwNYW 61 | /LitK7JCXTfR7GJqGQgI/p9FdG1058U3vDFzEgamEZvRE608Mi5H0d7sK2zVPwz6 62 | h/DHeW1stm74lBK4QFjL+54ZMroy4XM1j2jgYxn5SiMXFgXX5Z+oZGBDtRwi9zmd 63 | g3ySU4lOA7kHJp+WRSwU3iX+Gptty/h2tXj9oAUSPStUkyu+aPQjX104UN7pwmoj 64 | X3nelMMPGlgPDlWj+lyGp9AjARo4UGhZp/r3o8XTQvHDv5GwCfhO14T0WTN0Pe2x 65 | qE23K/aPsaWG4qrqcUKfT9ewAcOjI1gD8lwA34qQylpetK8z5AJ2bIg/Ff690sR2 66 | rw8IrCqXecTny2zbDs1y7jMtSAY6NWPltNaNrnjPYoj/2O0EpzS0RB7rTS76u+Ve 67 | 0fDeSKJ+FoOkg0egYrckSsXx38WqDSLR9Rormy+JLKFuMpEC6/CgBbV/jU0aYYqb 68 | /76OTeCUyzRywvbuSYvBGznj/i+I53R5D61vnOdkcaTG7YFnWFSLDMDt81Itst6x 69 | sW7u3tSgIjyRxnI3APKDjDKKayomg6qrA3Hx+aOzAgMBAAEwDQYJKoZIhvcNAQEL 70 | BQADggEBAEeP2YSV9BPEY6pII+SC9rz5YqEfyMuAHKx3WnmzCN6TC0dtjF8Hfxuc 71 | MwtLqEAAdgev6r/jed7RIRoYOG1u4mMYLvGQUbRJrsmcfbGCMlkz8vypBDT8jmo/ 72 | HdqcnXHADB1AHf9ZauMV/gF73o6AP1Mw6yhm4Ce8u3oLLLExpxqaXIuJnRKAekGm 73 | HFuzKD4ZzySGcO1E9mX9/nYSswhM6Y6G7fNwh4P3O1vg5wxGIA5JPQ/Api+tsE6j 74 | erhQSKjldCU/BaQya3o2TcvyssqrfvJecwRf+DQJqYem0ylVTGHOaK4vCYos78cm 75 | 6a9pL6+QknrxrH7VXSkTCatdNvS+muo= 76 | -----END CERTIFICATE----- 77 | -----BEGIN CERTIFICATE----- 78 | MIID1jCCAr6gAwIBAgIUF8kFgnLYnllxmZS9DhkJXRjcJT4wDQYJKoZIhvcNAQEL 79 | BQAwcTELMAkGA1UEBhMCREUxEjAQBgNVBAgTCUthcmxzcnVoZTEbMBkGA1UEBxMS 80 | QmFkZW4tV3VlcnR0ZW1iZXJnMRowGAYDVQQKExFHYW1lZm9yZ2UgNEQgR21iSDEV 81 | MBMGA1UEAxMMRXZlbnQgSW5nZXN0MB4XDTE4MDIwMzE1MTQwMFoXDTIzMDIwMjE1 82 | MTQwMFowcTELMAkGA1UEBhMCREUxEjAQBgNVBAgTCUthcmxzcnVoZTEbMBkGA1UE 83 | BxMSQmFkZW4tV3VlcnR0ZW1iZXJnMRowGAYDVQQKExFHYW1lZm9yZ2UgNEQgR21i 84 | SDEVMBMGA1UEAxMMRXZlbnQgSW5nZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 85 | MIIBCgKCAQEAomUm6pk4+rxapgxldFZAqxEk9go97d+9ep0YyHyTgGoJmLKuFb3G 86 | XvDleTrx+WqPxXJq8BEZqdoZr2y6fX/J+e4wlvOAfAXTydszSXogq0rx/rAA2LRJ 87 | 6ZLEynWDMNwjyblZ4PzFL9cEqP47FnxYezd/B0nscw6I5fbX8gQ4RTuNI7gkcnVc 88 | nyAeNhoaNw/u/5Vm3xtPN/UJ606yOWTX6DiJ1CJR+Zq03G1gUOwUVAJEecIvoFa8 89 | Oy61RJvFLZYF/oI3i4cZgUXWQg73WNbemM9sOj2QNw8Bw5CtxkG4mm7Lcby4AKMT 90 | +CmfeEUSGvg3KIb/PK0+2KFaBw0cLrCgKwIDAQABo2YwZDAOBgNVHQ8BAf8EBAMC 91 | AQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUQzg3OZy8Owv3b/6zlJ3o 92 | lvSiLF0wHwYDVR0jBBgwFoAUQzg3OZy8Owv3b/6zlJ3olvSiLF0wDQYJKoZIhvcN 93 | AQELBQADggEBAIl0gWZA0owskgqbDxwHR4SQJUmXGjg7KbMviY4GCji86hKOEN9d 94 | vm0YS876M+8Xe1rx2gYQTRSAVoUE4yl7xwllm87yF6DYYABdk3icZM7+7deTX03n 95 | oD6HlvcRquYHXk9+GfmimnN00fe04Zhk35QpxpKnmaEagEAwKH/gRLbHUOB2i1gW 96 | d/EW4eV/UsCZJ2WhnXYPVlKriEyeQTdSRKyO6Fwc2Sq61uevWZeaHA8tg01T7QpR 97 | o/aOOIqkhNJnaEkOkokpK7aboOoPdhhCO76No3nnQocQsJ1n65c1cAWhVezr8F01 98 | /uffiVqkHWglwRVngM4OZEm4TlIYippCeEM= 99 | -----END CERTIFICATE----- 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NosTale-Auth 2 | Simple library that lets you generate "magic" value for the NoS0577 login packet 3 | 4 | # Python installation 5 | `pip install nosauth` 6 | 7 | Please refer to the example.py for working example 8 | 9 | # The packet 10 | New login packet `NoS0577` is used when you login with Gameforge launcher 11 | 12 | That's how it looks like: 13 | `"NoS0577 " + SESSION_TOKEN + " " + INSTALLATION_GUID + " 003662BF" + " " + REGION_CODE + char(0xB) + "0.9.3.3126" + " 0 " + MD5_STR(MD5_FILE("NostaleClientX.exe") + MD5_FILE("NostaleClient.exe"))` 14 | 15 | * `NoS0577` - The header of the packet, const value 16 | * `SESSION_TOKEN` - Value generated by this library, after the value there are two spaces in the login packet 17 | * `INSTALLATION_GUID` - Id that is generated during installation, for login purposes it probably may be random, stored in the windows registry under key name `InstallationId` in `SOFTWARE\\WOW6432Node\\Gameforge4d\\TNTClient\\MainApp` 18 | * `003662BF` - Random value converted to HEX 19 | * `REGION_CODE` - GF code of your region - `4` for PL 20 | * `char(0xB)` - Single character with ASCII code `0xB` 21 | * `0.9.3.3114` - Current version of client, may be obtained from the NostaleClientX.exe file version 22 | * `0` - const value 23 | * `MD5_STR(MD5_FILE("NostaleClientX.exe") + MD5_FILE("NostaleClient.exe"))` - MD5 generated from concatenation of MD5 uppercase strings of NostaleClientX.exe and NostaleClient.exe 24 | 25 | # The useless stuff 26 | 27 | The client makes some useless stuff (at least - for us) like 28 | 29 | 1. When you press "Start" The launcher generates mostly like pseudo-random GUID and saves it to the environment variable called `_TNT_SESSION_ID` 30 | 2. Launcher launches the client with `gf` parameter 31 | 3. Client reads the `_TNT_SESSION_ID` value from the system environment variables, the value is further used to identify the client in the launcher (in case you run multiple NosTale clients) 32 | 4. Now the client and the launcher talk over newly created [pipe](https://docs.microsoft.com/en-us/windows/desktop/ipc/pipes) using JSON-RPC protocol. 33 | 5. The client queries the launcher using the `_TNT_SESSION_ID` value, the client requests info such as `USERNAME` and `code`, then it translates the `code` into `SESSION_TOKEN` using simple algorithm and sends it along with login packet 34 | 35 | # Core part 36 | 37 | ## User-Agent 38 | 39 | There are three types of User-Agents. For the majority of requests you will use the Chromium one (for example this specified in `Accounts` section below). 40 | For the second and third type the User-Agent looks like: `Chrome/Cversion (MAGIC) GameforgeClient/2.1.22`, for example `Chrome/C2.1.22.784 (6a28914b) GameforgeClient/2.1.22` 41 | 42 | Where the `MAGIC` is: 43 | 44 | ### For the second type 45 | 46 | The second type of User-Agent is used for example during request to `/api/v1/patching/download/nostale/default?branchToken`. There are instructions how to generate this user-agent: 47 | 48 | NOTE: The ouput of all hashing algorithms is hexlified string, not raw bytes! 49 | Firsly, you need to grab first number from your `TNT-Installation-Id`. 50 | 51 | In case the first number is even `(number % 2 == 0)` or there are only letters in your `TNT-Installation-Id`: 52 | * `MAGIC` = first 8 characters from left of `SHA256(SHA256(Cert) + SHA1(version) + SHA256(TNT-Installation-Id))` 53 | 54 | Otherwise (when the first number is odd): 55 | * `MAGIC` = the last 8 characters of `SHA256(SHA1(Cert) + SHA256(version) + SHA1(TNT-Installation-Id))` 56 | 57 | ### For the third type 58 | 59 | The third type of User-Agent is used for example while getting the auth code, so it's probably the most important one for you. 60 | 61 | NOTE: The ouput of all hashing algorithms is hexlified string, not raw bytes! 62 | Firsly, you need to grab first number from your `TNT-Installation-Id`. 63 | 64 | In case the first number is even `(number % 2 == 0)` or there are only letters in your `TNT-Installation-Id`: 65 | * `MAGIC` = 2 first chars of your account id + first 8 characters from left of `SHA256(SHA256(Cert) + SHA1(version) + SHA256(TNT-Installation-Id) + SHA1(account-id))` 66 | 67 | Otherwise (when the first number is odd): 68 | * `MAGIC` = 2 first chars of your account id + the last 8 characters of `SHA256(SHA1(Cert) + SHA256(version) + SHA1(TNT-Installation-Id) + SHA256(account-id))` 69 | 70 | 71 | Where: 72 | * `Cert` - Gameforge PEM cert embedded into launcher 73 | * `version` - The current version of the launcher, for example "C2.1.22.784" 74 | * `account-id` - Id of the account you are trying to log-in 75 | 76 | ### Obtaining token example: 77 | This is an example of obtaining the third type of token with following data: 78 | * `version` = C2.1.22.784 79 | * `Cert` = cert.pem file from this repository 80 | * `account-id` = fb50ca7a-6ba2-11eb-9439-0242ac130002 81 | * `TNT-Installation-Id` = a777c5e7-c9ac-407b-99b4-1a5934137f43 82 | 83 | The first number of `TNT-Installation-Id` is 7, it is odd number so: 84 | * `SHA1(Cert)` = 6a62b8e71fac63afc5abcb927a63f83aaa2ccb5b 85 | * `SHA256(version) = SHA256("C2.1.22.784")` = bb3dc2ed5d66d85d099d97513c52fbe699e61e5e8c71f91b9137566514c04e51 86 | * `SHA1(TNT-Installation-Id) = SHA1("a777c5e7-c9ac-407b-99b4-1a5934137f43")` = 8b3c8dbe01fbb1d18ec288b74f072915f8d268b4 87 | * `SHA256(Account-Id) = SHA256("fb50ca7a-6ba2-11eb-9439-0242ac130002")` = bcabe70d5883ceead32fe116322824be320b18a98a241a5370a5de5e34763697 88 | * `2 first chars of account id` = fb 89 | * `SHA256(SHA1(Cert) + SHA256(version) + SHA1(TNT-Installation-Id) + SHA256(account-id)) = SHA256(6a62b8e71fac63afc5abcb927a63f83aaa2ccb5b + bb3dc2ed5d66d85d099d97513c52fbe699e61e5e8c71f91b9137566514c04e51 + 8b3c8dbe01fbb1d18ec288b74f072915f8d268b4 + bcabe70d5883ceead32fe116322824be320b18a98a241a5370a5de5e34763697)` = 825e786ede7e1421cda70988d9e493a4adeb9ce2986a48848763bd5cb506b95a 90 | * `Last 8 characters of the previous one` = b506b95a 91 | * Result = `2 first chars of account id` + `Last 8 characters of the previous one` = fbb506b95a 92 | 93 | 94 | ## Auth 95 | 96 | To obtain the token first you need to auth yourself. To do so you need to send `POST` request to `https://spark.gameforge.com/api/v1/auth/sessions`, you send it `only once`. 97 | 98 | In the request header you need to specify `TNT-Installation-Id` from the windows registry. 99 | In the body of the request you need to specify `JSON` content: 100 | * `email` - your email 101 | * `locale` - example: `pl_PL` 102 | * `password` - your password 103 | 104 | In the response you will get `JSON` content: 105 | * `token` - value that is used later in API requests, it is NOT that one to use in login packet 106 | 107 | ## Accounts 108 | 109 | Since some time you may bind multiple game accounts to your GF account. To handle it you need to make `GET` request to `https://spark.gameforge.com/api/v1/user/accounts` 110 | 111 | In the request header you need to specify: 112 | * `TNT-Installation-Id` - value from windows registry 113 | * `User-Agent` - Eg. `Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36` 114 | * `Authorization` - `Bearer ` + `TOKEN_FROM_AUTH_REQUEST` 115 | 116 | In the response you get `JSON` array of all the accounts. Top level keys in the list are `account ids`. 117 | 118 | ## Almost done 119 | 120 | To obtain the right token you need to make `POST` request to `https://spark.gameforge.com/api/v1/auth/thin/codes` 121 | In the request header you need to specify: 122 | * `TNT-Installation-Id` - value from windows registry 123 | * `User-Agent` - This is the third type token, for the details look at the `User-Agent` paragraph at the beginning of the document, eg. `Chrome/C2.1.22.784 (fbb506b95a) GameforgeClient/2.1.22` 124 | * `Authorization` - `Bearer ` + `TOKEN_FROM_AUTH_REQUEST` 125 | 126 | In the request `JSON` body you need to specify: 127 | * `platformGameAccountId` - the id of selected account from previous section 128 | * `gsid` - field consisting of `client_session_id` + `-` + `random 4 digit number`. The `client_session_id` can be random, but preferably it should be the same as the `client_session_id` generated for `start_time` request sent to `https://events.gameforge.com` 129 | 130 | In the response you get `JSON` content with: 131 | * `code` - The value you are looking for 132 | 133 | You may call the `api/v1/auth/thin/codes` multiple times with the auth token obtained from `api/v1/auth/thin/sessions` 134 | 135 | ## Finally, the SESSION_TOKEN 136 | 137 | To use the `code` in login packet you need to convert it to `SESSION_TOKEN`. The conversion is very simple. It changes the `code` into hex string. 138 | 139 | Lets say you got `code` equal to `a857263a-3fc1-4c60-ad78-9b6d9a2a0691`, after the conversion it will look like `61383537323633612D336663312D346336302D616437382D396236643961326130363931` because you convert characters from `code` element by element into hexstring, so: 140 | * `a` -> 97 -> 0x61 -> `61` 141 | * `8` -> 56 -> 0x38 -> `38` 142 | 143 | and so on, so the string will look like `6138...` 144 | -------------------------------------------------------------------------------- /nosauth/api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import binascii 3 | import hashlib 4 | import uuid 5 | import datetime 6 | import random 7 | 8 | try: 9 | import importlib.resources as pkg_resources 10 | except ImportError: 11 | # Try backported to PY<37 `importlib_resources`. 12 | import importlib_resources as pkg_resources 13 | 14 | class NtLauncher: 15 | BROWSER_USERAGENT = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36" 16 | DEFAULT_CHROME_VERSION = "C2.2.19.1700" 17 | DEFAULT_GF_VERSION = "2.2.19" 18 | 19 | def __init__(self, locale, gfLang, installation_id=None, chromeVersion=None, gfVersion=None, cert=None): 20 | self.locale = locale 21 | self.gfLang = gfLang 22 | self.installation_id = installation_id 23 | self.chromeVersion = chromeVersion 24 | self.gfVersion = gfVersion 25 | self.cert = cert 26 | self.token = None 27 | 28 | if self.chromeVersion == None: 29 | self.chromeVersion = NtLauncher.DEFAULT_CHROME_VERSION 30 | 31 | if self.gfVersion == None: 32 | self.gfVersion = NtLauncher.DEFAULT_GF_VERSION 33 | 34 | if self.cert == None: 35 | data = pkg_resources.read_binary(__package__, "all_certs.pem") 36 | start = data.find(b"-----BEGIN CERTIFICATE-----") 37 | end = data.find(b"-----END CERTIFICATE-----", start) 38 | 39 | self.cert = data[start:end+1+len(b"-----END CERTIFICATE-----")] 40 | 41 | def auth(self, username, password): 42 | self.username = username 43 | self.password = password 44 | 45 | if not self.installation_id: 46 | m = hashlib.md5((username + password).encode()).digest() 47 | self.installation_id = str(uuid.UUID(bytes_le=m)) #it generates just unique uuid for username+password, so others who use this library won't have the same installation_id 48 | 49 | if not self.send_start_time(): 50 | return False 51 | 52 | URL = "https://spark.gameforge.com/api/v1/auth/sessions" 53 | HEADERS = { 54 | "User-Agent" : NtLauncher.BROWSER_USERAGENT, 55 | "TNT-Installation-Id" : self.installation_id, 56 | "Origin" : "spark://www.gameforge.com", 57 | } 58 | 59 | CONTENT = { 60 | "email" : self.username, 61 | "locale" : self.locale, 62 | "password" : self.password, 63 | } 64 | 65 | r = requests.post(URL, headers=HEADERS, json=CONTENT) 66 | if r.status_code != 201: 67 | return False 68 | 69 | response = r.json() 70 | self.token = response["token"] 71 | return True 72 | 73 | def send_start_time(self): 74 | HEADERS = { 75 | "Host" : "events.gameforge.com", 76 | "User-Agent" : "GameforgeClient/" + self.gfVersion, 77 | "Content-Type" : "application/json", 78 | "Connection" : "Keep-Alive" 79 | } 80 | 81 | PAYLOAD = """{ 82 | "client_installation_id": "%INSTALLATION_ID%", 83 | "client_locale": "pol_pol", 84 | "client_session_id": "%SESSION_ID%", 85 | "client_version_info": { 86 | "branch": "master", 87 | "commit_id": "27942713", 88 | "version": "%CHROME_VERSION%" 89 | }, 90 | "id": 0, 91 | "localtime": "%LOCAL_TIME%", 92 | "start_count": 1, 93 | "start_time": %START_TIME%, 94 | "type": "start_time" 95 | } 96 | """ 97 | 98 | payload = PAYLOAD.replace("%INSTALLATION_ID%", self.installation_id) 99 | payload = payload.replace("%SESSION_ID%", str(uuid.uuid4())) 100 | payload = payload.replace("%CHROME_VERSION%", self.chromeVersion[1:]) 101 | 102 | def rreplace(s, old, new, occurrence): 103 | li = s.rsplit(old, occurrence) 104 | return new.join(li) 105 | 106 | eu = datetime.timezone(datetime.timedelta(hours=1)) #Eu timezone 107 | date = datetime.datetime.now(eu) 108 | date = date.replace(microsecond=0) 109 | 110 | payload = payload.replace("%LOCAL_TIME%", rreplace(date.isoformat(), ":", "", 1)) 111 | payload = payload.replace("%START_TIME%", str(random.randint(1500, 10000))) 112 | 113 | with pkg_resources.path(__package__, "all_certs.pem") as path: 114 | certPath = str(path) 115 | 116 | r = requests.post("https://events.gameforge.com", headers=HEADERS, data=payload, cert=certPath, verify=certPath) 117 | 118 | if r.status_code != 200: 119 | return False 120 | 121 | return True 122 | 123 | 124 | def getAccounts(self): 125 | if not self.token: 126 | return False 127 | 128 | URL = "https://spark.gameforge.com/api/v1/user/accounts" 129 | 130 | HEADERS = { 131 | "User-Agent" : NtLauncher.BROWSER_USERAGENT, 132 | "TNT-Installation-Id" : self.installation_id, 133 | "Origin" : "spark://www.gameforge.com", 134 | "Authorization" : "Bearer {}".format(self.token), 135 | "Connection" : "Keep-Alive" 136 | } 137 | 138 | r = requests.get(URL, headers=HEADERS) 139 | 140 | if r.status_code != 200: 141 | return False 142 | 143 | accounts = [] 144 | response = r.json() 145 | 146 | for key in response.keys(): 147 | accounts.append((key, response[key]["displayName"])) 148 | 149 | return accounts 150 | 151 | def _convertToken(self, guid): 152 | return binascii.hexlify(guid.encode()).decode() 153 | 154 | def getFirstNumber(self, uuid): 155 | for char in uuid: 156 | if char.isdigit(): 157 | return char 158 | 159 | return None 160 | 161 | def generateSecondTypeUserAgentMagic(self): 162 | firstLetter = self.getFirstNumber(self.installation_id) 163 | 164 | if firstLetter == None or int(firstLetter) % 2 == 0: 165 | hashOfCert = hashlib.sha256(self.cert).hexdigest() 166 | hashOfVersion = hashlib.sha1(self.chromeVersion.encode("ascii")).hexdigest() 167 | hashOfInstallationId = hashlib.sha256(self.installation_id.encode("ascii")).hexdigest() 168 | hashOfSum = hashlib.sha256((hashOfCert + hashOfVersion + hashOfInstallationId).encode("ascii")).hexdigest() 169 | return hashOfSum[:8] 170 | 171 | else: 172 | hashOfCert = hashlib.sha1(self.cert).hexdigest() 173 | hashOfVersion = hashlib.sha256(self.chromeVersion.encode("ascii")).hexdigest() 174 | hashOfInstallationId = hashlib.sha1(self.installation_id.encode("ascii")).hexdigest() 175 | hashOfSum = hashlib.sha256((hashOfCert + hashOfVersion + hashOfInstallationId).encode("ascii")).hexdigest() 176 | return hashOfSum[-8:] 177 | 178 | def generateThirdTypeUserAgentMagic(self, account_id): 179 | firstLetter = self.getFirstNumber(self.installation_id) 180 | firstTwoLettersOfAccountId = account_id[:2] 181 | 182 | if firstLetter == None or int(firstLetter) % 2 == 0: 183 | hashOfCert = hashlib.sha256(self.cert).hexdigest() 184 | hashOfVersion = hashlib.sha1(self.chromeVersion.encode("ascii")).hexdigest() 185 | hashOfInstallationId = hashlib.sha256(self.installation_id.encode("ascii")).hexdigest() 186 | hashOfAccountId = hashlib.sha1(account_id.encode("ascii")).hexdigest() 187 | hashOfSum = hashlib.sha256((hashOfCert + hashOfVersion + hashOfInstallationId + hashOfAccountId).encode("ascii")).hexdigest() 188 | return firstTwoLettersOfAccountId + hashOfSum[:8] 189 | 190 | else: 191 | hashOfCert = hashlib.sha1(self.cert).hexdigest() 192 | hashOfVersion = hashlib.sha256(self.chromeVersion.encode("ascii")).hexdigest() 193 | hashOfInstallationId = hashlib.sha1(self.installation_id.encode("ascii")).hexdigest() 194 | hashOfAccountId = hashlib.sha256(account_id.encode("ascii")).hexdigest() 195 | hashOfSum = hashlib.sha256((hashOfCert + hashOfVersion + hashOfInstallationId + hashOfAccountId).encode("ascii")).hexdigest() 196 | return firstTwoLettersOfAccountId + hashOfSum[-8:] 197 | 198 | def getToken(self, account, raw=False): 199 | if not self.token: 200 | return False 201 | 202 | URL = "https://spark.gameforge.com/api/v1/auth/thin/codes" 203 | 204 | HEADERS = { 205 | "User-Agent" : "Chrome/{} ({}) GameforgeClient/{}".format(self.chromeVersion, self.generateThirdTypeUserAgentMagic(account), self.gfVersion), 206 | "TNT-Installation-Id" : self.installation_id, 207 | "Origin" : "spark://www.gameforge.com", 208 | "Authorization" : "Bearer {}".format(self.token), 209 | "Connection" : "Keep-Alive" 210 | } 211 | 212 | CONTENT = { 213 | "platformGameAccountId" : account, 214 | "gsid" : "{}-{}".format(uuid.uuid4(), random.randint(1000, 9999)) 215 | } 216 | 217 | r = requests.post(URL, headers=HEADERS, json=CONTENT) 218 | 219 | if r.status_code != 201: 220 | return False 221 | 222 | if raw: 223 | return r.json()["code"] 224 | 225 | return self._convertToken(r.json()["code"]) 226 | --------------------------------------------------------------------------------