From 04a0a0e0b7ee7de1ce6fba26110c9b050c5325aa Mon Sep 17 00:00:00 2001 From: gus <0x67757300@gmail.com> Date: Thu, 21 Dec 2023 12:10:52 -0300 Subject: [PATCH] Add web module --- README.md | 4 +- ghunt/cli.py | 19 ++- ghunt/helpers/ia.py | 6 +- ghunt/modules/web.py | 205 ++++++++++++++++++++++++++++ ghunt/objects/apis.py | 8 +- ghunt/static/__init__.py | 5 + ghunt/static/assets/css/style.css | 21 +++ ghunt/static/assets/img/favicon.png | Bin 0 -> 3561 bytes ghunt/static/index.html | 36 +++++ requirements.txt | 5 +- 10 files changed, 295 insertions(+), 14 deletions(-) create mode 100644 ghunt/modules/web.py create mode 100644 ghunt/static/__init__.py create mode 100644 ghunt/static/assets/css/style.css create mode 100644 ghunt/static/assets/img/favicon.png create mode 100644 ghunt/static/index.html diff --git a/README.md b/README.md index 2d5ce4b0..e36a6b23 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Features : - Fully async - JSON export - Browser extension to ease login +- HTTP API # ✔️ Requirements - Python >= 3.10 @@ -57,7 +58,7 @@ The extension is available on the following stores :\ Then, profit : ```bash -usage: ghunt [-h] {login,email,gaia,drive} ... +usage: ghunt [-h] {login,email,gaia,drive,web} ... positional arguments: {login,email,gaia,drive} @@ -65,6 +66,7 @@ positional arguments: email (--json) Get information on an email address. gaia (--json) Get information on a Gaia ID. drive (--json) Get information on a Drive file or folder. + web (--api) Launch web app. options: -h, --help show this help message and exit diff --git a/ghunt/cli.py b/ghunt/cli.py index c445b2c3..59c0c30f 100644 --- a/ghunt/cli.py +++ b/ghunt/cli.py @@ -26,22 +26,31 @@ def parse_and_run(): parser_drive.add_argument("file_id", help="Example: 1N__vVu4c9fCt4EHxfthUNzVOs_tp8l6tHcMBnpOZv_M") parser_drive.add_argument('--json', type=str, help="File to write the JSON output to.") + ### Web module + parser_drive = subparsers.add_parser('web', help="Launch web app.") + parser_drive.add_argument('--host', type=str, help="Host. Defaults to 0.0.0.0.", default='0.0.0.0') + parser_drive.add_argument('--port', type=int, help="Port. Defaults to 8080.", default=8080) + parser_drive.add_argument('--api', action='store_true', help="API only. No front-end.") + ### Parsing args = parser.parse_args(args=None if sys.argv[1:] else ['--help']) process_args(args) def process_args(args: argparse.Namespace): - import trio + import anyio match args.module: case "login": from ghunt.modules import login - trio.run(login.check_and_login, None, args.clean) + anyio.run(login.check_and_login, None, args.clean) case "email": from ghunt.modules import email - trio.run(email.hunt, None, args.email_address, args.json) + anyio.run(email.hunt, None, args.email_address, args.json) case "gaia": from ghunt.modules import gaia - trio.run(gaia.hunt, None, args.gaia_id, args.json) + anyio.run(gaia.hunt, None, args.gaia_id, args.json) case "drive": from ghunt.modules import drive - trio.run(drive.hunt, None, args.file_id, args.json) \ No newline at end of file + anyio.run(drive.hunt, None, args.file_id, args.json) + case "web": + from ghunt.modules import web + anyio.run(web.hunt, None, args.host, args.port, args.api) diff --git a/ghunt/helpers/ia.py b/ghunt/helpers/ia.py index 39acb71d..302548f7 100644 --- a/ghunt/helpers/ia.py +++ b/ghunt/helpers/ia.py @@ -2,7 +2,7 @@ from ghunt.apis.vision import VisionHttp import httpx -import trio +import anyio from base64 import b64encode @@ -18,7 +18,7 @@ async def detect_face(vision_api: VisionHttp, as_client: httpx.AsyncClient, imag rate_limited, are_faces_found, faces_results = await vision_api.detect_faces(as_client, image_content=encoded_image) if not rate_limited: break - await trio.sleep(0.5) + await anyio.sleep(0.5) else: exit("\n[-] Vision API keeps rate-limiting.") @@ -30,4 +30,4 @@ async def detect_face(vision_api: VisionHttp, as_client: httpx.AsyncClient, imag else: gb.rc.print(f"🎭 No face detected.", style="italic bright_black") - return faces_results \ No newline at end of file + return faces_results diff --git a/ghunt/modules/web.py b/ghunt/modules/web.py new file mode 100644 index 00000000..2ff5808e --- /dev/null +++ b/ghunt/modules/web.py @@ -0,0 +1,205 @@ +from ghunt.helpers.utils import get_httpx_client +from ghunt.objects.base import GHuntCreds +from ghunt.objects.encoders import GHuntEncoder +from ghunt.apis.peoplepa import PeoplePaHttp +from ghunt.apis.drive import DriveHttp +from ghunt.helpers import gmaps, playgames, auth, calendar +from ghunt.helpers.drive import get_users_from_file + +import json +import httpx +import humanize +import uvicorn +from uhttp import App, Request, Response + + +app = App() + + +def _jsonify(obj): + return Response( + status=200, + headers={'content-type': 'application/json'}, + body=json.dumps(obj, cls=GHuntEncoder, indent=4).encode() + ) + + +@app.get(r'/email/(?P[\w@.-]+)') +async def _email(request: Request): + body = { + 'account': { + 'id': '', + 'name': '', + 'email': '', + 'picture': '', + 'cover': '', + 'last_updated': '' + }, + 'maps': { + 'stats': {}, + 'reviews': [], + 'photos': [], + }, + 'calendar': { + 'details': {}, + 'events': [] + }, + 'player': { + 'id': '', + 'name': '', + 'gamertag': '', + 'title': '', + 'avatar': '', + 'banner': '', + 'experience': '', + } + } + + people_pa = PeoplePaHttp(request.state['creds']) + found, person = await people_pa.people_lookup( + request.state['client'], + request.params['email'], + params_template='max_details' + ) + if not found: + return _jsonify(body) + body['account'].update( + id=person.personId, + name=person.names['PROFILE'].fullname, + email=person.emails['PROFILE'].value, + picture=person.profilePhotos['PROFILE'].url, + cover=person.coverPhotos['PROFILE'].url, + last_updated=person.sourceIds['PROFILE'].lastUpdated.strftime( + '%Y/%m/%d %H:%M:%S (UTC)' + ) + ) + + err, stats, reviews, photos = await gmaps.get_reviews( + request.state['client'], + person.personId + ) + if not err: + body['maps'].update(stats=stats, reviews=reviews, photos=photos) + + found, details, events = await calendar.fetch_all( + request.state['creds'], + request.state['client'], + request.params['email'] + ) + if found: + body['calendar'].update(details=details, events=events) + + player_results = await playgames.search_player( + request.state['creds'], + request.state['client'], + request.params['email'] + ) + if player_results: + _, player = await playgames.player( + request.state['creds'], + request.state['client'], + player_results[0].id + ) + body['games'].update( + id=player.profile.id, + name=player.profile.display_name, + gamertag=player.profile.gamertag, + title=player.profile.title, + avatar=player.profile.avatar_url, + banner=player.profile.banner_url_landscape, + experience=player.profile.experience_info.current_xp, + ) + + return _jsonify(body) + + +@app.get(r'/gaia/(?P\d+)') +async def _gaia(request: Request): + body = { + 'id': '', + 'name': '', + 'email': '', + 'picture': '', + 'cover': '', + 'last_updated': '' + } + + people_pa = PeoplePaHttp(request.state['creds']) + found, person = await people_pa.people( + request.state['client'], + request.params['gaia'], + params_template='max_details' + ) + if found: + body.update( + id=person.personId, + name=person.names['PROFILE'].fullname, + email=person.emails['PROFILE'].value, + picture=person.profilePhotos['PROFILE'].url, + cover=person.coverPhotos['PROFILE'].url, + lastUpdated=person.sourceIds['PROFILE'].lastUpdated.strftime( + '%Y/%m/%d %H:%M:%S (UTC)' + ) + ) + + return _jsonify(body) + + +@app.get(r'/drive/(?P\w+)') +async def _drive(request: Request): + body = { + 'id': '', + 'title': '', + 'size': '', + 'icon': '', + 'thumbnail': '', + 'description': '', + 'created': '', + 'modified': '', + 'users': [], + } + + drive = DriveHttp(request.state['creds']) + found, file = await drive.get_file( + request.state['client'], request.params['drive'] + ) + if found: + body.update( + id=file.id, + title=file.title, + size=humanize.naturalsize(file.file_size), + icon=file.icon_link, + thumbnail=file.thumbnail_link, + description=file.description, + created=file.created_date.strftime('%Y/%m/%d %H:%M:%S (UTC)'), + modified=file.modified_date.strftime('%Y/%m/%d %H:%M:%S (UTC)'), + users=get_users_from_file(file) + ) + + return _jsonify(body) + + +@app.after +def _cors(request: Request, response: Response): + if request.headers.get('origin'): + response.headers['access-control-allow-origin'] = '*' + + +async def hunt(as_client: httpx.AsyncClient, host: str, port: int, api: bool): + @app.startup + def setup_ghunt(state): + state['client'] = as_client or get_httpx_client() + state['creds'] = GHuntCreds() + state['creds'].load_creds() + if not state['creds'].are_creds_loaded(): + raise RuntimeError('Missing credentials') + if not auth.check_cookies(state['creds'].cookies): + raise RuntimeError('Invalid cookies') + + if not api: + from ghunt import static + app.mount(static.app) + + config = uvicorn.Config(app, host=host, port=port) + server = uvicorn.Server(config) + await server.serve() diff --git a/ghunt/objects/apis.py b/ghunt/objects/apis.py index 170212bf..a25567c1 100644 --- a/ghunt/objects/apis.py +++ b/ghunt/objects/apis.py @@ -6,7 +6,7 @@ from ghunt.helpers.auth import * import httpx -import trio +import anyio from datetime import datetime, timezone from typing import * @@ -25,7 +25,7 @@ def __init__(self): self.creds: GHuntCreds = None self.headers: Dict[str, str] = {} self.cookies: Dict[str, str] = {} - self.gen_token_lock: trio.Lock = None + self.gen_token_lock: anyio.Lock = None self.authentication_mode: str = "" self.require_key: str = "" @@ -39,7 +39,7 @@ def _load_api(self, creds: GHuntCreds, headers: Dict[str, str]): raise GHuntCorruptedHeadersError(f"The provided headers when loading the endpoint seems corrupted, please check it : {headers}") if self.authentication_mode == "oauth": - self.gen_token_lock = trio.Lock() + self.gen_token_lock = anyio.Lock() cookies = {} if self.authentication_mode in ["sapisidhash", "cookies_only"]: @@ -149,4 +149,4 @@ def recursive_merge(obj1, obj2, module_name: str) -> any: if not get_class_name(obj).startswith(class_name): raise GHuntObjectsMergingError("The two objects being merged aren't from the same class.") - self = recursive_merge(self, obj, module_name) \ No newline at end of file + self = recursive_merge(self, obj, module_name) diff --git a/ghunt/static/__init__.py b/ghunt/static/__init__.py new file mode 100644 index 00000000..99e2b3ac --- /dev/null +++ b/ghunt/static/__init__.py @@ -0,0 +1,5 @@ +import os +from uhttp_static import static + + +app = static(os.path.dirname(__file__)) diff --git a/ghunt/static/assets/css/style.css b/ghunt/static/assets/css/style.css new file mode 100644 index 00000000..50e16da6 --- /dev/null +++ b/ghunt/static/assets/css/style.css @@ -0,0 +1,21 @@ +body { + max-width: 700px; + margin: 2rem auto; + padding: 0 1rem; +} + +h1 { + text-align: center; + margin-bottom: 1rem; + color: var(--bs-emphasis-color); + font-weight: bold; + font-size: 3em; +} + +a { + text-decoration: none; +} + +* { + box-shadow: none !important; +} diff --git a/ghunt/static/assets/img/favicon.png b/ghunt/static/assets/img/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d9a527baf949f48990f262cb32427f796bd7ac1a GIT binary patch literal 3561 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 literal 0 HcmV?d00001 diff --git a/ghunt/static/index.html b/ghunt/static/index.html new file mode 100644 index 00000000..e316f83a --- /dev/null +++ b/ghunt/static/index.html @@ -0,0 +1,36 @@ + + + + + + GHunt + + + + + + +

GHunt

+
+
+ + + +
+
+ + + diff --git a/requirements.txt b/requirements.txt index 28224d98..5e9d314a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,7 @@ trio==0.21.0 autoslot==2021.10.1 humanize==4.4.0 inflection==0.5.1 -jsonpickle==2.2.0 \ No newline at end of file +jsonpickle==2.2.0 +uvicorn==0.25.0 +uhttp==1.3.4 +uhttp-static==0.1.2