#!/usr/bin/env python3
"""
4shared Desktop App SOAP Backend Server
Implements the DesktopApp SOAP API used by the 4shared Desktop Uploader (2006 beta onwards).
Namespace: http://api.soap.shared.pmstation.com/
Endpoint:  /servlet/services/DesktopApp  (2006 beta)
           /jax2/DesktopApp              (later versions)

Run:  python 4shared_soap_server.py
Then redirect the app to http://127.0.0.1:80/servlet/services/DesktopApp
(edit your hosts file or patch the binary to point at this server)
"""

from http.server import HTTPServer, BaseHTTPRequestHandler
import xml.etree.ElementTree as ET
import json
import os
import time
import hashlib
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("4shared")

# ---------------------------------------------------------------------------
# Configuration — edit these to match your setup
# ---------------------------------------------------------------------------
HOST = "0.0.0.0"
PORT = 80

# Simple flat-file user store: { "email": { "password": "...", "root_id": 1 } }
USERS = {
    "test@example.com": {"password": "password123", "root_id": 1000},
}

# File/folder store — in memory for now, swap for a DB as needed.
# Structure: { id: { "id", "name", "type" (0=folder,1=file), "parentId",
#                    "size", "link", "description", "passworded", "shared" } }
ITEMS: dict = {
    1000: {
        "id": 1000, "name": "My Files", "type": 0,
        "parentId": 0, "size": 0, "link": "",
        "description": "", "passworded": False, "shared": True,
    }
}
_next_id = 2000

SOAP_NS   = "http://schemas.xmlsoap.org/soap/envelope/"
API_NS    = "http://api.soap.shared.pmstation.com/"
XSD_NS    = "http://www.w3.org/2001/XMLSchema"
XSI_NS    = "http://www.w3.org/2001/XMLSchema-instance"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def next_id() -> int:
    global _next_id
    _next_id += 1
    return _next_id


def get_user(login: str, password: str):
    """Return user dict or None."""
    u = USERS.get(login)
    if u and u["password"] == password:
        return u
    return None


def soap_envelope(body_xml: str) -> bytes:
    return (
        '<?xml version="1.0" encoding="UTF-8"?>'
        f'<soapenv:Envelope xmlns:soapenv="{SOAP_NS}" xmlns:tns="{API_NS}">'
        "<soapenv:Body>"
        f"{body_xml}"
        "</soapenv:Body>"
        "</soapenv:Envelope>"
    ).encode()


def soap_fault(code: str, message: str) -> bytes:
    return soap_envelope(
        f"<soapenv:Fault>"
        f"<faultcode>{code}</faultcode>"
        f"<faultstring>{message}</faultstring>"
        f"</soapenv:Fault>"
    )


def item_xml(item: dict) -> str:
    """Serialize an accountItem to XML."""
    t = "true" if item.get("passworded") else "false"
    s = "true" if item.get("shared") else "false"
    return (
        f"<item>"
        f"<id>{item['id']}</id>"
        f"<name>{item['name']}</name>"
        f"<type>{item['type']}</type>"
        f"<parentId>{item['parentId']}</parentId>"
        f"<size>{item['size']}</size>"
        f"<link>{item.get('link','')}</link>"
        f"<description>{item.get('description','')}</description>"
        f"<passworded>{t}</passworded>"
        f"<shared>{s}</shared>"
        f"</item>"
    )


def items_xml(items: list) -> str:
    return "".join(item_xml(i) for i in items)


def children_of(parent_id: int) -> list:
    return [v for v in ITEMS.values() if v["parentId"] == parent_id]

# ---------------------------------------------------------------------------
# SOAP method handlers
# Return value: XML string that goes inside <{method}Response>
# ---------------------------------------------------------------------------

def handle_login(args: dict) -> str:
    """
    login(arg0=login, arg1=password) -> string
    Returns empty string on success, error message on failure.
    The 2006 beta treats non-empty return = failure.
    """
    login, password = get_login_password(args)
    log.info(f"login: user={login}")
    if get_user(login, password):
        return "<return></return>"
    return "<return>Invalid login or password</return>"


def get_login_password(args: dict):
    """
    Extract login/password from args regardless of whether the client sent
    named params (2006 beta: <login>, <password>) or positional params
    (later API: <arg0>, <arg1>).
    """
    login    = args.get("login")    or args.get("arg0", "")
    password = args.get("password") or args.get("arg1", "")
    return login, password


def handle_isExistsLoginPassword(args: dict) -> str:
    """isExistsLoginPassword(login, password) -> boolean  — vtable +0x24 in the beta"""
    login, password = get_login_password(args)
    log.info(f"isExistsLoginPassword: user={login}")
    ok = "true" if get_user(login, password) else "false"
    return f"<return>{ok}</return>"


def handle_uploaderLoggedIn(args: dict) -> str:
    """
    uploaderLoggedIn(login, password, toolName, toolVersion) -> boolean
    vtable +0x44 in the beta.
    """
    login, password = get_login_password(args)
    tool = args.get("toolName") or args.get("arg2", "")
    ver  = args.get("toolVersion") or args.get("arg3", "")
    log.info(f"uploaderLoggedIn: user={login} tool={tool} ver={ver}")
    ok = "true" if get_user(login, password) else "false"
    return f"<return>{ok}</return>"


def handle_getRoot(args: dict) -> str:
    login, password = get_login_password(args)
    u = get_user(login, password)
    if not u:
        return soap_fault("Client", "Auth failed").decode()
    root = ITEMS.get(u["root_id"])
    return item_xml(root) if root else "<return/>"


def handle_getItems(args: dict) -> str:
    login, password = get_login_password(args)
    folder_id = int(args.get("folderId") or args.get("arg2", 0))
    if not get_user(login, password):
        return "<return/>"
    kids = children_of(folder_id)
    return f"<return>{''.join(item_xml(i) for i in kids)}</return>"


def handle_getAllItems(args: dict) -> str:
    login, password = get_login_password(args)
    if not get_user(login, password):
        return "<return/>"
    u = get_user(login, password)
    all_items = [v for v in ITEMS.values() if v["parentId"] != 0 or v["id"] != u["root_id"]]
    return f"<return>{''.join(item_xml(i) for i in all_items)}</return>"


def handle_getAllFolders(args: dict) -> str:
    login, password = get_login_password(args)
    if not get_user(login, password):
        return "<return/>"
    folders = [v for v in ITEMS.values() if v["type"] == 0]
    return f"<return>{''.join(item_xml(i) for i in folders)}</return>"


def handle_getFreeSpace(args: dict) -> str:
    # Return 1 GB free
    return "<return>1073741824</return>"


def handle_getSpaceLimit(args: dict) -> str:
    # Return 2 GB limit
    return "<return>2147483648</return>"


def handle_getMaxFileSize(args: dict) -> str:
    # 1 GB max file
    return "<return>1073741824</return>"


def handle_getCurrentUploaderVersion(args: dict) -> str:
    # Return version 1 — the beta won't try to auto-update
    return "<return>1</return>"


def handle_isAccountActive(args: dict) -> str:
    login, password = get_login_password(args)
    ok = "true" if get_user(login, password) else "false"
    return f"<return>{ok}</return>"


def handle_isAccountBanned(args: dict) -> str:
    return "<return>false</return>"


def handle_isAccountPremium(args: dict) -> str:
    return "<return>false</return>"


def handle_hasRightUpload(args: dict) -> str:
    return "<return>true</return>"


def handle_createNewFolder(args: dict) -> str:
    login, password = get_login_password(args)
    parent_id = int(args.get("parentId") or args.get("arg2", 0))
    name = args.get("name") or args.get("arg3", "New Folder")
    if not get_user(login, password):
        return "<return>-1</return>"
    fid = next_id()
    ITEMS[fid] = {
        "id": fid, "name": name, "type": 0,
        "parentId": parent_id, "size": 0,
        "link": "", "description": "", "passworded": False, "shared": False,
    }
    log.info(f"createNewFolder: id={fid} name={name} parent={parent_id}")
    return f"<return>{fid}</return>"


def handle_creatNewFolder(args: dict) -> str:
    # Older typo'd version, same logic but returns boolean
    handle_createNewFolder(args)
    return "<return>true</return>"


def handle_uploadStartFile(args: dict) -> str:
    """
    uploadStartFile(login, password, folderId, filename, filesize) -> long (fileId)
    Registers a file about to be uploaded.
    """
    login, password = get_login_password(args)
    folder_id = int(args.get("folderId") or args.get("arg2", 0))
    filename  = args.get("fileName") or args.get("name") or args.get("arg3", "file")
    filesize  = int(args.get("fileSize") or args.get("size") or args.get("arg4", 0))
    if not get_user(login, password):
        return "<return>-1</return>"
    fid = next_id()
    ITEMS[fid] = {
        "id": fid, "name": filename, "type": 1,
        "parentId": folder_id, "size": filesize,
        "link": f"http://localhost:{PORT}/download/{fid}/{filename}",
        "description": "", "passworded": False, "shared": True,
    }
    log.info(f"uploadStartFile: id={fid} name={filename} size={filesize}")
    return f"<return>{fid}</return>"


def handle_uploadStartFileUpdate(args: dict) -> str:
    return handle_uploadStartFile(args)


def handle_uploadFinishFile(args: dict) -> str:
    """uploadFinishFile(login, password, fileId, filename) -> string (link)"""
    file_id = int(args.get("fileId") or args.get("arg2", 0))
    filename = args.get("fileName") or args.get("arg3", "")
    item = ITEMS.get(file_id)
    link = item["link"] if item else ""
    log.info(f"uploadFinishFile: id={file_id} link={link}")
    return f"<return>{link}</return>"


def handle_uploadStartedFileExists(args: dict) -> str:
    file_id = int(args.get("fileId") or args.get("arg2", 0))
    exists = "true" if file_id in ITEMS else "false"
    return f"<return>{exists}</return>"


def handle_uploadCancelFile(args: dict) -> str:
    file_id = int(args.get("fileId") or args.get("arg2", 0))
    ITEMS.pop(file_id, None)
    log.info(f"uploadCancelFile: id={file_id}")
    return ""


def handle_createUploadSessionKey(args: dict) -> str:
    login, password = get_login_password(args)
    folder_id = args.get("arg2", "0")
    key = hashlib.md5(f"{login}{folder_id}{time.time()}".encode()).hexdigest()
    log.info(f"createUploadSessionKey: key={key}")
    return f"<return>{key}</return>"


def handle_getUploadFormUrl(args: dict) -> str:
    folder_id = args.get("arg0", "0")
    return f"<return>http://localhost:{PORT}/upload?folder={folder_id}</return>"


def handle_getFileInfo(args: dict) -> str:
    file_id = int(args.get("fileId") or args.get("dirId") or args.get("itemId") or args.get("id") or args.get("arg2", 0))
    item = ITEMS.get(file_id)
    return item_xml(item) if item else "<return/>"


def handle_getDirInfo(args: dict) -> str:
    return handle_getFileInfo(args)


def handle_getItemInfo(args: dict) -> str:
    return handle_getFileInfo(args)


def handle_deleteFile(args: dict) -> str:
    file_id = int(args.get("fileId") or args.get("dirId") or args.get("id") or args.get("arg2", 0))
    ITEMS.pop(file_id, None)
    return ""


def handle_deleteFileFinal(args: dict) -> str:
    return handle_deleteFile(args)


def handle_deleteFolder(args: dict) -> str:
    return handle_deleteFile(args)


def handle_deleteFolderFinal(args: dict) -> str:
    return handle_deleteFile(args)


def handle_renameFile(args: dict) -> str:
    file_id = int(args.get("fileId") or args.get("dirId") or args.get("id") or args.get("arg2", 0))
    new_name = args.get("newName") or args.get("name") or args.get("arg3", "")
    if file_id in ITEMS:
        ITEMS[file_id]["name"] = new_name
    return f"<return>{file_id}</return>"


def handle_renameFolder(args: dict) -> str:
    return handle_renameFile(args)


def handle_getFileDownloadLink(args: dict) -> str:
    file_id = int(args.get("fileId") or args.get("arg2", 0))
    item = ITEMS.get(file_id)
    link = item["link"] if item else ""
    return f"<return>{link}</return>"


def handle_getDirectLink(args: dict) -> str:
    file_id = args.get("arg2", "0")
    return f"<return>http://localhost:{PORT}/download/{file_id}</return>"


def handle_signup(args: dict) -> str:
    # Return empty = success
    return "<return></return>"


def handle_signupUsername(args: dict) -> str:
    return "<return></return>"


def handle_getToolList(args: dict) -> str:
    return "<return/>"


def handle_getToolUrl(args: dict) -> str:
    return "<return></return>"


def handle_getToolVersion(args: dict) -> str:
    return "<return>1.0</return>"


def handle_run(args: dict) -> str:
    return "<return></return>"


def handle_getHistory(args: dict) -> str:
    return "<return/>"


def handle_getHistoryFromId(args: dict) -> str:
    return "<return/>"


def handle_getHistoriesFromId(args: dict) -> str:
    return "<return/>"


def handle_getHistoryLastId(args: dict) -> str:
    return "<return>0</return>"


def handle_getNotRecursiveHistories(args: dict) -> str:
    return "<return/>"


def handle_markSynchronized(args: dict) -> str:
    return ""


def handle_syncFinished(args: dict) -> str:
    return ""


def handle_downloadFinished(args: dict) -> str:
    return ""


def handle_getFiles(args: dict) -> str:
    return "<return/>"


def handle_getItemsPartial(args: dict) -> str:
    return handle_getItems(args)


def handle_getItemsCount(args: dict) -> str:
    folder_id = int(args.get("arg2", 0))
    count = len(children_of(folder_id))
    return f"<return>{count}</return>"


def handle_getNewFileDataCenter(args: dict) -> str:
    return "<return>1</return>"


def handle_getNotOwnedSizeLimit(args: dict) -> str:
    return "<return>2147483648</return>"


def handle_getOwnedSizeLimit(args: dict) -> str:
    return "<return>2147483648</return>"


def handle_getRecycleBinItems(args: dict) -> str:
    return "<return/>"


def handle_getDirLinkItems(args: dict) -> str:
    return "<return/>"


def handle_getSharedDirItems(args: dict) -> str:
    return "<return/>"


def handle_emptyRecycleBin(args: dict) -> str:
    return "<return>0</return>"


def handle_restoreFile(args: dict) -> str:
    return ""


def handle_restoreFiles(args: dict) -> str:
    return ""


def handle_pasteFilesDirs(args: dict) -> str:
    return "<return></return>"


def handle_decodeId(args: dict) -> str:
    return "<return></return>"


def handle_decodeLink(args: dict) -> str:
    return "<return>0</return>"


def handle_checkSharedDirAccess(args: dict) -> str:
    return "<return>1</return>"


def handle_checkSubdomain(args: dict) -> str:
    return "<return></return>"


def handle_getFolderSharingProperties(args: dict) -> str:
    return "<return/>"


def handle_setFolderSharingProperties(args: dict) -> str:
    return "<return></return>"


def handle_getSettings(args: dict) -> str:
    return "<return/>"


def handle_getAllSettings(args: dict) -> str:
    return "<return/>"


def handle_getSettingGroups(args: dict) -> str:
    return "<return/>"


def handle_checkSettings(args: dict) -> str:
    return "<return/>"


def handle_applySettings(args: dict) -> str:
    return "<return/>"


def handle_getFileDescription(args: dict) -> str:
    return "<return/>"


def handle_getDirDescription(args: dict) -> str:
    return "<return></return>"


def handle_setFileDescription(args: dict) -> str:
    return "<return></return>"


def handle_setDirDescription(args: dict) -> str:
    return "<return></return>"


def handle_reportAbuse(args: dict) -> str:
    return "<return></return>"


def handle_getPlaylistLink(args: dict) -> str:
    return "<return></return>"


def handle_getSharedPlaylistLink(args: dict) -> str:
    return "<return></return>"


def handle_getPreviewLink(args: dict) -> str:
    return "<return></return>"


def handle_getVideoPreviewLink(args: dict) -> str:
    return "<return></return>"


def handle_getSmallImageLink(args: dict) -> str:
    return "<return></return>"


def handle_getFileVersionLink(args: dict) -> str:
    return "<return></return>"


def handle_getMp3FileInfo(args: dict) -> str:
    return "<return/>"


def handle_getMp3FileInfos(args: dict) -> str:
    return "<return/>"


def handle_getExifFileInfo(args: dict) -> str:
    return "<return/>"


def handle_getExifFileInfos(args: dict) -> str:
    return "<return/>"


def handle_addToMyAccount(args: dict) -> str:
    return "<return></return>"


def handle_getFavorites(args: dict) -> str:
    return "<return/>"


def handle_addToFavorites(args: dict) -> str:
    return "<return>0</return>"


def handle_removeFromFavorites(args: dict) -> str:
    return "<return>0</return>"


def handle_simpleUploadStart(args: dict) -> str:
    return "<return/>"


def handle_getOperationDescriptions(args: dict) -> str:
    return "<return/>"

# ---------------------------------------------------------------------------
# Dispatch table
# ---------------------------------------------------------------------------
HANDLERS = {
    "login":                    handle_login,
    "isExistsLoginPassword":    handle_isExistsLoginPassword,
    "uploaderLoggedIn":         handle_uploaderLoggedIn,
    "getRoot":                  handle_getRoot,
    "getItems":                 handle_getItems,
    "getAllItems":               handle_getAllItems,
    "getAllFolders":             handle_getAllFolders,
    "getFreeSpace":              handle_getFreeSpace,
    "getSpaceLimit":             handle_getSpaceLimit,
    "getMaxFileSize":            handle_getMaxFileSize,
    "getCurrentUploaderVersion": handle_getCurrentUploaderVersion,
    "isAccountActive":           handle_isAccountActive,
    "isAccountBanned":           handle_isAccountBanned,
    "isAccountPremium":          handle_isAccountPremium,
    "hasRightUpload":            handle_hasRightUpload,
    "createNewFolder":           handle_createNewFolder,
    "creatNewFolder":            handle_creatNewFolder,
    "uploadStartFile":           handle_uploadStartFile,
    "uploadStartFileUpdate":     handle_uploadStartFileUpdate,
    "uploadFinishFile":          handle_uploadFinishFile,
    "uploadStartedFileExists":   handle_uploadStartedFileExists,
    "uploadCancelFile":          handle_uploadCancelFile,
    "createUploadSessionKey":    handle_createUploadSessionKey,
    "getUploadFormUrl":          handle_getUploadFormUrl,
    "getFileInfo":               handle_getFileInfo,
    "getDirInfo":                handle_getDirInfo,
    "getItemInfo":               handle_getItemInfo,
    "deleteFile":                handle_deleteFile,
    "deleteFileFinal":           handle_deleteFileFinal,
    "deleteFolder":              handle_deleteFolder,
    "deleteFolderFinal":         handle_deleteFolderFinal,
    "renameFile":                handle_renameFile,
    "renameFolder":              handle_renameFolder,
    "getFileDownloadLink":       handle_getFileDownloadLink,
    "getDirectLink":             handle_getDirectLink,
    "signup":                    handle_signup,
    "signupUsername":            handle_signupUsername,
    "getToolList":               handle_getToolList,
    "getToolUrl":                handle_getToolUrl,
    "getToolVersion":            handle_getToolVersion,
    "run":                       handle_run,
    "getHistory":                handle_getHistory,
    "getHistoryFromId":          handle_getHistoryFromId,
    "getHistoriesFromId":        handle_getHistoriesFromId,
    "getHistoryLastId":          handle_getHistoryLastId,
    "getNotRecursiveHistories":  handle_getNotRecursiveHistories,
    "markSynchronized":          handle_markSynchronized,
    "syncFinished":              handle_syncFinished,
    "downloadFinished":          handle_downloadFinished,
    "getFiles":                  handle_getFiles,
    "getItemsPartial":           handle_getItemsPartial,
    "getItemsCount":             handle_getItemsCount,
    "getNewFileDataCenter":      handle_getNewFileDataCenter,
    "getNotOwnedSizeLimit":      handle_getNotOwnedSizeLimit,
    "getOwnedSizeLimit":         handle_getOwnedSizeLimit,
    "getRecycleBinItems":        handle_getRecycleBinItems,
    "getDirLinkItems":           handle_getDirLinkItems,
    "getSharedDirItems":         handle_getSharedDirItems,
    "emptyRecycleBin":           handle_emptyRecycleBin,
    "restoreFile":               handle_restoreFile,
    "restoreFiles":              handle_restoreFiles,
    "pasteFilesDirs":            handle_pasteFilesDirs,
    "decodeId":                  handle_decodeId,
    "decodeLink":                handle_decodeLink,
    "checkSharedDirAccess":      handle_checkSharedDirAccess,
    "checkSubdomain":            handle_checkSubdomain,
    "getFolderSharingProperties":  handle_getFolderSharingProperties,
    "setFolderSharingProperties":  handle_setFolderSharingProperties,
    "getSettings":               handle_getSettings,
    "getAllSettings":             handle_getAllSettings,
    "getSettingGroups":          handle_getSettingGroups,
    "checkSettings":             handle_checkSettings,
    "applySettings":             handle_applySettings,
    "getFileDescription":        handle_getFileDescription,
    "getDirDescription":         handle_getDirDescription,
    "setFileDescription":        handle_setFileDescription,
    "setDirDescription":         handle_setDirDescription,
    "reportAbuse":               handle_reportAbuse,
    "getPlaylistLink":           handle_getPlaylistLink,
    "getSharedPlaylistLink":     handle_getSharedPlaylistLink,
    "getPreviewLink":            handle_getPreviewLink,
    "getVideoPreviewLink":       handle_getVideoPreviewLink,
    "getSmallImageLink":         handle_getSmallImageLink,
    "getFileVersionLink":        handle_getFileVersionLink,
    "getMp3FileInfo":            handle_getMp3FileInfo,
    "getMp3FileInfos":           handle_getMp3FileInfos,
    "getExifFileInfo":           handle_getExifFileInfo,
    "getExifFileInfos":          handle_getExifFileInfos,
    "addToMyAccount":            handle_addToMyAccount,
    "getFavorites":              handle_getFavorites,
    "addToFavorites":            handle_addToFavorites,
    "removeFromFavorites":       handle_removeFromFavorites,
    "simpleUploadStart":         handle_simpleUploadStart,
    "getOperationDescriptions":  handle_getOperationDescriptions,
}

# ---------------------------------------------------------------------------
# SOAP request parser
# ---------------------------------------------------------------------------

def parse_soap(body: bytes):
    """
    Parse a SOAP request body.
    Returns (method_name, args_dict) or raises ValueError.
    """
    root = ET.fromstring(body)

    # Find Body element
    body_el = root.find(f"{{{SOAP_NS}}}Body")
    if body_el is None:
        raise ValueError("No SOAP Body found")

    # First child of Body is the method element
    method_el = list(body_el)[0]

    # Strip namespace from tag to get method name
    tag = method_el.tag
    if "}" in tag:
        tag = tag.split("}")[1]

    # Collect all child elements as args
    args = {}
    for child in method_el:
        child_tag = child.tag
        if "}" in child_tag:
            child_tag = child_tag.split("}")[1]
        args[child_tag] = child.text or ""

    return tag, args

# ---------------------------------------------------------------------------
# HTTP handler
# ---------------------------------------------------------------------------

WSDL = open(__file__.replace(".py", ".wsdl"), "rb").read() if os.path.exists(__file__.replace(".py", ".wsdl")) else None


class SOAPHandler(BaseHTTPRequestHandler):

    # Serve on both the 2006 beta path and the later jax2 path
    SOAP_PATHS = {
        "/servlet/services/DesktopApp",
        "/jax2/DesktopApp",
        "/jax3/DesktopApp",
    }

    def log_message(self, fmt, *args):
        log.info(fmt % args)

    def do_GET(self):
        path = self.path.split("?")[0]
        query = self.path[len(path):]

        if path in self.SOAP_PATHS and "wsdl" in query.lower():
            # Serve WSDL if available
            if WSDL:
                self._respond(200, WSDL, "text/xml; charset=utf-8")
            else:
                self._respond(404, b"WSDL not found", "text/plain")
            return

        self._respond(200, b"4shared SOAP backend running", "text/plain")

    def do_POST(self):
        path = self.path.split("?")[0]
        if path not in self.SOAP_PATHS:
            self._respond(404, b"Not found", "text/plain")
            return

        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)
        log.debug(f"REQUEST:\n{body.decode(errors='replace')}")

        try:
            method, args = parse_soap(body)
        except Exception as e:
            log.error(f"Parse error: {e}")
            self._respond(500, soap_fault("Server", f"Parse error: {e}"), "text/xml; charset=utf-8")
            return

        log.info(f"SOAP method: {method}  args={args}")

        handler = HANDLERS.get(method)
        if handler is None:
            log.warning(f"Unknown method: {method}")
            response_body = "<return/>"
        else:
            try:
                response_body = handler(args)
            except Exception as e:
                log.error(f"Handler error in {method}: {e}")
                self._respond(500, soap_fault("Server", str(e)), "text/xml; charset=utf-8")
                return

        response_xml = soap_envelope(
            f'<tns:{method}Response xmlns:tns="{API_NS}">'
            f"{response_body}"
            f"</tns:{method}Response>"
        )
        log.debug(f"RESPONSE:\n{response_xml.decode()}")
        self._respond(200, response_xml, "text/xml; charset=utf-8")

    def _respond(self, code: int, body: bytes, content_type: str):
        self.send_response(code)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    server = HTTPServer((HOST, PORT), SOAPHandler)
    log.info(f"4shared SOAP server listening on {HOST}:{PORT}")
    log.info(f"Endpoint (2006 beta): http://127.0.0.1:{PORT}/servlet/services/DesktopApp")
    log.info(f"Endpoint (jax2):      http://127.0.0.1:{PORT}/jax2/DesktopApp")
    log.info("")
    log.info("Users configured:")
    for u in USERS:
        log.info(f"  {u}")
    log.info("")
    log.info("Point the app at this server by editing your hosts file or patching the binary.")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        log.info("Shutting down.")