summaryrefslogtreecommitdiff
path: root/backend/decky_loader
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2024-05-04 22:39:30 -0400
committerAAGaming <aagaming@riseup.net>2024-05-04 22:39:30 -0400
commit14ea7b964f65460c08f39d42e1621aabd1db22fc (patch)
tree42b62d1eb69a94aa80c1cacb8c89d549c21cf86c /backend/decky_loader
parent2a22f000c12bd3704a93e897ed71e644392baeef (diff)
downloaddecky-loader-14ea7b964f65460c08f39d42e1621aabd1db22fc.tar.gz
decky-loader-14ea7b964f65460c08f39d42e1621aabd1db22fc.zip
implement fetch and external resource request apis
Diffstat (limited to 'backend/decky_loader')
-rw-r--r--backend/decky_loader/helpers.py21
-rw-r--r--backend/decky_loader/plugin/imports/decky.py2
-rw-r--r--backend/decky_loader/plugin/imports/decky.pyi2
-rw-r--r--backend/decky_loader/plugin/plugin.py1
-rw-r--r--backend/decky_loader/utilities.py94
-rw-r--r--backend/decky_loader/wsrouter.py2
6 files changed, 108 insertions, 14 deletions
diff --git a/backend/decky_loader/helpers.py b/backend/decky_loader/helpers.py
index f4005cc5..21ba5ce5 100644
--- a/backend/decky_loader/helpers.py
+++ b/backend/decky_loader/helpers.py
@@ -1,3 +1,4 @@
+from platform import version
import re
import ssl
import uuid
@@ -14,7 +15,7 @@ from aiohttp import ClientSession
from .localplatform import localplatform
from .enums import UserType
from logging import getLogger
-from packaging.version import Version
+from packaging.version import Version # type: ignore
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
@@ -36,12 +37,13 @@ def get_csrf_token():
@middleware
async def csrf_middleware(request: Request, handler: Handler):
if str(request.method) == "OPTIONS" or \
- request.headers.get('Authentication') == csrf_token or \
+ request.headers.get('X-Decky-Auth') == csrf_token or \
str(request.rel_url) == "/auth/token" or \
str(request.rel_url).startswith("/plugins/load_main/") or \
str(request.rel_url).startswith("/static/") or \
str(request.rel_url).startswith("/steam_resource/") or \
str(request.rel_url).startswith("/frontend/") or \
+ str(request.rel_url.path) == "/fetch" or \
str(request.rel_url.path) == "/ws" or \
assets_regex.match(str(request.rel_url)) or \
dist_regex.match(str(request.rel_url)) or \
@@ -61,24 +63,27 @@ def mkdir_as_user(path: str):
localplatform.chown(path)
# Fetches the version of loader
+# TODO THIS IS ABSOLUTELY TERRIBLE AND NEVER SHOULDVE BEEN MERGED! packaging HAS NO TYPES AND WE COULD LITERALLY JUST USE A REGEX!!!!! REWRITE THIS!!!!!!!!!!!!!
def get_loader_version() -> str:
try:
# Normalize Python-style version to conform to Decky style
- v = Version(importlib.metadata.version("decky_loader"))
+ v = Version(importlib.metadata.version("decky_loader")) # type: ignore
- version_str = f'v{v.major}.{v.minor}.{v.micro}'
+ version_str = f'v{v.major}.{v.minor}.{v.micro}' # type: ignore
- if v.pre:
- version_str += f'-pre{v.pre[1]}'
+ if v.pre: # type: ignore
+ version_str += f'-pre{v.pre[1]}' # type: ignore
- if v.post:
- version_str += f'-dev{v.post}'
+ if v.post: # type: ignore
+ version_str += f'-dev{v.post}' # type: ignore
return version_str
except Exception as e:
logger.warn(f"Failed to execute get_loader_version(): {str(e)}")
return "unknown"
+user_agent = f"Decky/{get_loader_version()} (https://decky.xyz)"
+
# returns the appropriate system python paths
def get_system_pythonpaths() -> list[str]:
try:
diff --git a/backend/decky_loader/plugin/imports/decky.py b/backend/decky_loader/plugin/imports/decky.py
index 6c5173de..384c3860 100644
--- a/backend/decky_loader/plugin/imports/decky.py
+++ b/backend/decky_loader/plugin/imports/decky.py
@@ -12,7 +12,7 @@ Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `
A logging facility `logger` is available which writes to the recommended location.
"""
-__version__ = '0.1.0'
+__version__ = '1.0.0'
import os
import subprocess
diff --git a/backend/decky_loader/plugin/imports/decky.pyi b/backend/decky_loader/plugin/imports/decky.pyi
index 50a0f66c..a72c74c0 100644
--- a/backend/decky_loader/plugin/imports/decky.pyi
+++ b/backend/decky_loader/plugin/imports/decky.pyi
@@ -12,7 +12,7 @@ Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `
A logging facility `logger` is available which writes to the recommended location.
"""
-__version__ = '0.1.0'
+__version__ = '1.0.0'
import logging
diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py
index 6fa86858..00b5dad8 100644
--- a/backend/decky_loader/plugin/plugin.py
+++ b/backend/decky_loader/plugin/plugin.py
@@ -47,6 +47,7 @@ class PluginWrapper:
self.emitted_event_callback: EmittedEventCallbackType = emit_callback
+ # TODO enable this after websocket release
self.legacy_method_warning = False
def __str__(self) -> str:
diff --git a/backend/decky_loader/utilities.py b/backend/decky_loader/utilities.py
index 174b7cb0..d7d16f04 100644
--- a/backend/decky_loader/utilities.py
+++ b/backend/decky_loader/utilities.py
@@ -1,6 +1,8 @@
from __future__ import annotations
from os import stat_result
import uuid
+from urllib.parse import unquote
+from json.decoder import JSONDecodeError
from os.path import splitext
import re
from traceback import format_exc
@@ -8,6 +10,7 @@ from stat import FILE_ATTRIBUTE_HIDDEN # type: ignore
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
from aiohttp import ClientSession
+from aiohttp.web import Request, StreamResponse, Response, json_response, post
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
from logging import getLogger
@@ -26,12 +29,17 @@ class FilePickerObj(TypedDict):
filest: stat_result
is_dir: bool
+decky_header_regex = re.compile("X-Decky-(.*)")
+extra_header_regex = re.compile("X-Decky-Header-(.*)")
+
+excluded_default_headers = ["Host", "Origin", "Sec-Fetch-Site", "Sec-Fetch-Mode", "Sec-Fetch-Dest"]
+
class Utilities:
def __init__(self, context: PluginManager) -> None:
self.context = context
self.legacy_util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
"ping": self.ping,
- "http_request": self.http_request,
+ "http_request": self.http_request_legacy,
"install_plugin": self.install_plugin,
"install_plugins": self.install_plugins,
"cancel_plugin_install": self.cancel_plugin_install,
@@ -76,9 +84,33 @@ class Utilities:
context.ws.add_route("utilities/enable_rdt", self.enable_rdt)
context.ws.add_route("utilities/get_tab_id", self.get_tab_id)
context.ws.add_route("utilities/get_user_info", self.get_user_info)
- context.ws.add_route("utilities/http_request", self.http_request)
+ context.ws.add_route("utilities/http_request", self.http_request_legacy)
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
+ context.web_app.add_routes([
+ post("/methods/{method_name}", self._handle_legacy_server_method_call)
+ ])
+
+ for method in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'):
+ context.web_app.router.add_route(method, "/fetch", self.http_request)
+
+
+ async def _handle_legacy_server_method_call(self, request: Request) -> Response:
+ method_name = request.match_info["method_name"]
+ try:
+ args = await request.json()
+ except JSONDecodeError:
+ args = {}
+ res = {}
+ try:
+ r = await self.legacy_util_methods[method_name](**args)
+ res["result"] = r
+ res["success"] = True
+ except Exception as e:
+ res["result"] = str(e)
+ res["success"] = False
+ return json_response(res)
+
async def _call_legacy_utility(self, method_name: str, kwargs: Dict[Any, Any]) -> Any:
self.logger.debug(f"Calling utility {method_name} with legacy kwargs");
res: Dict[Any, Any] = {}
@@ -114,7 +146,63 @@ class Utilities:
async def uninstall_plugin(self, name: str):
return await self.context.plugin_browser.uninstall_plugin(name)
- async def http_request(self, method: str, url: str, extra_opts: Any = {}):
+ # Loosely based on https://gist.github.com/mosquito/4dbfacd51e751827cda7ec9761273e95#file-proxy-py
+ async def http_request(self, req: Request) -> StreamResponse:
+ if req.headers.get('X-Decky-Auth', '') != helpers.get_csrf_token() and req.query.get('auth', '') != helpers.get_csrf_token():
+ return Response(text='Forbidden', status=403)
+
+ url = req.headers["X-Decky-Fetch-URL"] if "X-Decky-Fetch-URL" in req.headers else unquote(req.query.get('fetch_url', ''))
+ self.logger.info(f"Preparing {req.method} request to {url}")
+
+ headers = dict(req.headers)
+
+ headers["User-Agent"] = helpers.user_agent
+
+ for excluded_header in excluded_default_headers:
+ self.logger.debug(f"Excluding default header {excluded_header}")
+ if excluded_header in headers:
+ del headers[excluded_header]
+
+ if "X-Decky-Fetch-Excluded-Headers" in req.headers:
+ for excluded_header in req.headers["X-Decky-Fetch-Excluded-Headers"].split(", "):
+ self.logger.debug(f"Excluding header {excluded_header}")
+ if excluded_header in headers:
+ del headers[excluded_header]
+
+ for header in req.headers:
+ match = extra_header_regex.search(header)
+ if match:
+ header_name = match.group(1)
+ header_value = req.headers[header]
+ self.logger.debug(f"Adding extra header {header_name}: {header_value}")
+ headers[header_name] = header_value
+
+ for header in list(headers.keys()):
+ match = decky_header_regex.search(header)
+ if match:
+ self.logger.debug(f"Removing decky header {header} from request")
+ del headers[header]
+
+ self.logger.debug(f"Final request headers: {headers}")
+
+ body = await req.read() # TODO can this also be streamed?
+
+ async with ClientSession() as web:
+ async with web.request(req.method, url, headers=headers, data=body, ssl=helpers.get_ssl_context()) as web_res:
+ res = StreamResponse(headers=web_res.headers, status=web_res.status)
+ if web_res.headers.get('Transfer-Encoding', '').lower() == 'chunked':
+ res.enable_chunked_encoding()
+
+ await res.prepare(req)
+ self.logger.debug(f"Starting stream for {url}")
+ async for data in web_res.content.iter_any():
+ await res.write(data)
+ if data:
+ await res.drain()
+ self.logger.debug(f"Finished stream for {url}")
+ return res
+
+ async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}):
async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **extra_opts)
text = await res.text()
diff --git a/backend/decky_loader/wsrouter.py b/backend/decky_loader/wsrouter.py
index 9cd98a1c..1ceeedd4 100644
--- a/backend/decky_loader/wsrouter.py
+++ b/backend/decky_loader/wsrouter.py
@@ -117,7 +117,7 @@ class WSRouter:
create_task(self._call_route(data["route"], data["args"], data["id"]))
else:
error = {"error":f'Route {data["route"]} does not exist.', "name": "RouteNotFoundError", "traceback": None}
- create_task(self.write({"type": MessageType.ERROR.value, "id": data["id"], "message": error}))
+ create_task(self.write({"type": MessageType.ERROR.value, "id": data["id"], "error": error}))
case _:
self.logger.error("Unknown message type", data)
finally: