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 00000000..d9a527ba Binary files /dev/null and b/ghunt/static/assets/img/favicon.png differ 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