Skip to content

Commit

Permalink
split http scripts handling to a new module
Browse files Browse the repository at this point in the history
  • Loading branch information
totaam committed Jan 28, 2025
1 parent f14101b commit 8361f6f
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 175 deletions.
2 changes: 2 additions & 0 deletions xpra/scripts/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ def impcheck(*modules) -> bool:
features.display = opts.windows
features.windows = opts.windows and impcheck("codecs")
features.rfb = b(opts.rfb_upgrade) and impcheck("server.rfb")
features.http = opts.http_scripts.lower() not in FALSE_OPTIONS


def enforce_server_features() -> None:
Expand Down Expand Up @@ -360,6 +361,7 @@ def enforce_server_features() -> None:
"display": "xpra.server.mixins.display,xpra.server.source.display",
"windows": "xpra.server.mixins.window,xpra.server.source.windows",
"rfb": "xpra.net.rfb,xpra.server.rfb",
"http": "xpra.server.mixins.http",
})
may_block_numpy()

Expand Down
16 changes: 6 additions & 10 deletions xpra/server/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from xpra.common import SSH_AGENT_DISPATCH, FULL_INFO, noop, ConnectionMessage
from xpra.net.common import (
may_log_packet, is_request_allowed,
ServerPacketHandlerType, PacketType, PacketElement, HttpResponse,
ServerPacketHandlerType, PacketType, PacketElement,
)
from xpra.scripts.config import str_to_bool
from xpra.os_util import WIN32, gi_import
Expand Down Expand Up @@ -68,6 +68,9 @@ def get_server_base_classes() -> tuple[type, ...]:
if features.network_state:
from xpra.server.mixins.networkstate import NetworkStateServer
classes.append(NetworkStateServer)
if features.http:
from xpra.server.mixins.http import HttpServer
classes.append(HttpServer)
if features.shell:
from xpra.server.mixins.shell import ShellServer
classes.append(ShellServer)
Expand Down Expand Up @@ -760,19 +763,12 @@ def _process_lock_toggle(self, proto, packet: PacketType) -> None:
log("lock set to %s for client %i", ss.lock, ss.counter)

######################################################################
# http server and http audio stream:
# add clients to http server info:
def get_http_info(self) -> dict[str, Any]:
info = ServerCore.get_http_info(self)
info = super().get_http_info()
info["clients"] = len(self._server_sources)
return info

def get_http_scripts(self) -> dict[str, Callable[[str], HttpResponse]]:
scripts = {}
for c in SERVER_BASES:
scripts.update(c.get_http_scripts(self))
httplog("scripts=%s", scripts)
return scripts

######################################################################
# client connections:
def init_sockets(self, sockets) -> None:
Expand Down
161 changes: 3 additions & 158 deletions xpra/server/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import signal
import platform
import threading
from urllib.parse import urlparse, parse_qsl, unquote
from weakref import WeakKeyDictionary
from time import sleep, time, monotonic
from threading import Lock
Expand Down Expand Up @@ -54,7 +53,7 @@
from xpra.net.digest import get_salt, gendigest, choose_digest
from xpra.platform import set_name, threaded_server_init
from xpra.platform.info import get_username
from xpra.platform.paths import get_app_dir, get_system_conf_dirs, get_user_conf_dirs, get_icon_filename
from xpra.platform.paths import get_app_dir, get_system_conf_dirs, get_user_conf_dirs
from xpra.platform.events import add_handler, remove_handler
from xpra.platform.dotxpra import DotXpra
from xpra.os_util import force_quit, get_machine_id, get_user_uuid, get_hex_uuid, getuid, gi_import, POSIX
Expand Down Expand Up @@ -137,26 +136,13 @@ def proto_crypto_caps(proto) -> dict[str, Any]:
return {}


def _filter_display_dict(display_dict, *whitelist):
displays_info = {}
for display, info in display_dict.items():
displays_info[display] = {k: v for k, v in info.items() if k in whitelist}
httplog("_filter_display_dict(%s)=%s", display_dict, displays_info)
return displays_info


def force_close_connection(conn) -> None:
try:
conn.close()
except OSError:
log("close_connection()", exc_info=True)


def invalid_path(uri: str) -> HttpResponse:
httplog(f"invalid request path {uri!r}")
return 404, {}, b""


# noinspection PyMethodMayBeStatic
class ServerCore(ControlHandler):
"""
Expand Down Expand Up @@ -191,7 +177,6 @@ def __init__(self):
self.ssh_upgrade = False
self.rdp_upgrade = False
self._html: bool = False
self._http_scripts: dict[str, Callable[[str], HttpResponse]] = {}
self._www_dir: str = ""
self._http_headers_dirs: list[str] = []
self.socket_info: dict[Any, dict] = {}
Expand Down Expand Up @@ -284,7 +269,6 @@ def init(self, opts) -> None:
from xpra.server.menu_provider import get_menu_provider
self.menu_provider = get_menu_provider()
self.init_html_proxy(opts)
self.init_http_scripts(opts.http_scripts)
self.init_auth(opts)
self.init_ssl(opts)
self.dotxpra = DotXpra(opts.socket_dir, opts.socket_dirs + opts.client_socket_dirs)
Expand Down Expand Up @@ -626,35 +610,6 @@ def open_url() -> None:
self._http_headers_dirs.append(os.path.abspath(os.path.join(self._www_dir, "../http-headers")))
self._html = True

def init_http_scripts(self, http_scripts: str):
if http_scripts.lower() not in FALSE_OPTIONS:
script_options: dict[str, Callable[[str], HttpResponse]] = {
"/Status": self.http_status_request,
"/Info": self.http_info_request,
"/Sessions": self.http_sessions_request,
"/Displays": self.http_displays_request,
}
if self.menu_provider:
# we have menu data we can expose:
script_options |= {
"/Menu": self.http_menu_request,
"/MenuIcon": self.http_menu_icon_request,
"/DesktopMenu": self.http_desktop_menu_request,
"/DesktopMenuIcon": self.http_desktop_menu_icon_request,
}
if http_scripts.lower() in ("all", "*"):
self._http_scripts = script_options
else:
for script in http_scripts.split(","):
if not script.startswith("/"):
script = "/" + script
handler = script_options.get(script)
if not handler:
httplog.warn(f"Warning: unknown script {script!r}")
else:
self._http_scripts[script] = handler
httplog("init_http_scripts(%s)=%s", http_scripts, self._http_scripts)

######################################################################
# authentication:
def init_auth(self, opts) -> None:
Expand Down Expand Up @@ -1552,118 +1507,8 @@ def new_websocket_client(wsh) -> None:
force_close_connection(conn)

def get_http_scripts(self) -> dict[str, Callable[[str], HttpResponse]]:
return self._http_scripts

def http_query_dict(self, path) -> dict:
return dict(parse_qsl(urlparse(path).query))

def send_json_response(self, data) -> HttpResponse:
import json # pylint: disable=import-outside-toplevel
return self.http_response(json.dumps(data), "application/json")

def send_icon(self, icon_type: str, icon_data: bytes) -> HttpResponse:
httplog("send_icon%s", (icon_type, Ellipsizer(icon_data)))
if not icon_data:
icon_filename = get_icon_filename("noicon.png")
icon_data = load_binary_file(icon_filename)
icon_type = "png"
httplog("using fallback transparent icon")
if icon_type == "svg" and icon_data:
from xpra.codecs.icon_util import svg_to_png # pylint: disable=import-outside-toplevel
# call svg_to_png via the main thread,
# and wait for it to complete via an Event:
icon: list[tuple[bytes, str]] = [(icon_data, icon_type)]
event = threading.Event()

def convert() -> None:
icon[0] = svg_to_png("", icon_data, 48, 48), "png"
event.set()

GLib.idle_add(convert)
event.wait()
icon_data, icon_type = icon[0]
if icon_type in ("png", "jpeg", "svg", "webp"):
mime_type = "image/" + icon_type
else:
mime_type = "application/octet-stream"
return self.http_response(icon_data, mime_type)

def http_menu_request(self, _uri: str) -> HttpResponse:
xdg_menu = self.menu_provider.get_menu_data(remove_icons=True)
return self.send_json_response(xdg_menu or "not available")

def http_desktop_menu_request(self, _uri: str) -> HttpResponse:
xsessions = self.menu_provider.get_desktop_sessions(remove_icons=True)
return self.send_json_response(xsessions or "not available")

def http_menu_icon_request(self, uri: str) -> HttpResponse:
parts = unquote(uri).split("/MenuIcon/", 1)
# ie: "/menu-icon/a/b" -> ['', 'a/b']
if len(parts) < 2:
return invalid_path(uri)
path = parts[1].split("/")
# ie: "a/b" -> ['a', 'b']
category_name = path[0]
if len(path) < 2:
# only the category is present
app_name = ""
else:
app_name = path[1]
httplog("http_menu_icon_request: category_name=%s, app_name=%s", category_name, app_name)
icon_type, icon_data = self.menu_provider.get_menu_icon(category_name, app_name)
return self.send_icon(icon_type, icon_data)

def http_desktop_menu_icon_request(self, uri: str):
parts = unquote(uri).split("/DesktopMenuIcon/", 1)
# ie: "/menu-icon/wmname" -> ['', 'sessionname']
if len(parts) < 2:
return invalid_path(uri)
# in case the sessionname is followed by a slash:
sessionname = parts[1].split("/")[0]
httplog(f"http_desktop_menu_icon_request: {sessionname=}")
icon_type, icon_data = self.menu_provider.get_desktop_menu_icon(sessionname)
return self.send_icon(icon_type, icon_data)

def http_displays_request(self, _uri: str):
displays = self.get_displays()
displays_info = _filter_display_dict(displays, "state", "wmname", "xpra-server-mode")
return self.send_json_response(displays_info)

def get_displays(self) -> dict[str, Any]:
from xpra.scripts.main import get_displays_info # pylint: disable=import-outside-toplevel
return get_displays_info(self.dotxpra)

def http_sessions_request(self, _uri):
sessions = self.get_xpra_sessions()
sessions_info = _filter_display_dict(sessions, "state", "username", "session-type", "session-name", "uuid")
return self.send_json_response(sessions_info)

def get_xpra_sessions(self) -> dict[str, Any]:
from xpra.scripts.main import get_xpra_sessions # pylint: disable=import-outside-toplevel
return get_xpra_sessions(self.dotxpra)

def http_info_request(self, _uri: str):
return self.send_json_response(self.get_http_info())

def get_http_info(self) -> dict[str, Any]:
return {
"mode": self.get_server_mode(),
"type": "Python",
"uuid": self.uuid,
}

def http_status_request(self, _uri: str) -> HttpResponse:
return self.http_response("ready")

def http_response(self, content, content_type: str = "text/plain") -> HttpResponse:
if not content:
return 404, {}, b""
if isinstance(content, str):
content = content.encode("latin1")
return 200, {
"Content-type": content_type,
"Content-Length": len(content),
}, content
# loose coupling with xpra.server.mixins.http:
return getattr(self, "_http_scripts", {})

def is_timedout(self, protocol: SocketProtocol) -> bool:
# subclasses may override this method (ServerBase does)
Expand Down
1 change: 1 addition & 0 deletions xpra/server/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@
display = True
windows = True
rfb = True
http = True
Loading

0 comments on commit 8361f6f

Please sign in to comment.