157 lines
5.6 KiB
Python
157 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import enum
|
|
import logging
|
|
import hashlib
|
|
import uuid
|
|
import weakref
|
|
from typing import *
|
|
import urllib.parse
|
|
|
|
import multidict
|
|
|
|
from hippolyzer.lib.base.datatypes import Vector3
|
|
from hippolyzer.lib.base.message.message_handler import MessageHandler
|
|
from hippolyzer.lib.proxy.caps_client import CapsClient
|
|
from hippolyzer.lib.proxy.circuit import ProxiedCircuit
|
|
from hippolyzer.lib.proxy.objects import ObjectManager
|
|
from hippolyzer.lib.proxy.transfer_manager import TransferManager
|
|
from hippolyzer.lib.proxy.xfer_manager import XferManager
|
|
|
|
if TYPE_CHECKING:
|
|
from hippolyzer.lib.proxy.sessions import Session
|
|
from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow
|
|
from hippolyzer.lib.proxy.message import ProxiedMessage
|
|
|
|
|
|
class CapType(enum.Enum):
|
|
NORMAL = enum.auto()
|
|
TEMPORARY = enum.auto()
|
|
WRAPPER = enum.auto()
|
|
PROXY_ONLY = enum.auto()
|
|
|
|
|
|
class CapsMultiDict(multidict.MultiDict[Tuple[CapType, str]]):
|
|
def add(self, key, value) -> None:
|
|
# Prepend rather than append when adding caps.
|
|
# Necessary so the most recent for a region URI is returned
|
|
# when doing lookups by name.
|
|
vals = [value] + self.popall(key, [])
|
|
for val in vals:
|
|
super().add(key, val)
|
|
|
|
|
|
class ProxiedRegion:
|
|
def __init__(self, circuit_addr, seed_cap: str, session, handle=None):
|
|
# A client may make a Seed request twice, and may get back two (valid!) sets of
|
|
# Cap URIs. We need to be able to look up both, so MultiDict is necessary.
|
|
self.handle: Optional[int] = handle
|
|
self._name: Optional[str] = None
|
|
self.circuit: Optional[ProxiedCircuit] = None
|
|
self.circuit_addr = circuit_addr
|
|
self._caps = CapsMultiDict()
|
|
if seed_cap:
|
|
self._caps["Seed"] = (CapType.NORMAL, seed_cap)
|
|
self.session: Optional[Callable[[], Session]] = weakref.ref(session)
|
|
self.message_handler: MessageHandler[ProxiedMessage] = MessageHandler()
|
|
self.http_message_handler: MessageHandler[HippoHTTPFlow] = MessageHandler()
|
|
self.eq_manager = EventQueueManager(self)
|
|
self.xfer_manager = XferManager(self)
|
|
self.transfer_manager = TransferManager(self)
|
|
self.caps_client = CapsClient(self)
|
|
self.objects = ObjectManager(self)
|
|
|
|
@property
|
|
def name(self):
|
|
if self._name:
|
|
return self._name
|
|
return "Pending %r" % (self.circuit_addr,)
|
|
|
|
@name.setter
|
|
def name(self, val):
|
|
self._name = val
|
|
|
|
@property
|
|
def caps(self):
|
|
return multidict.MultiDict((x, y[1]) for x, y in self._caps.items())
|
|
|
|
@property
|
|
def global_pos(self):
|
|
if self.handle is None:
|
|
raise ValueError("Can't determine global region position without handle")
|
|
return Vector3(self.handle >> 32, self.handle & 0xFFffFFff)
|
|
|
|
@property
|
|
def is_alive(self):
|
|
if not self.circuit:
|
|
return False
|
|
return self.circuit.is_alive
|
|
|
|
def update_caps(self, caps: Mapping[str, str]):
|
|
for cap_name, cap_url in caps.items():
|
|
if isinstance(cap_url, str) and cap_url.startswith('http'):
|
|
self._caps.add(cap_name, (CapType.NORMAL, cap_url))
|
|
|
|
def register_wrapper_cap(self, name: str):
|
|
"""
|
|
Wrap an existing, non-unique cap with a unique URL
|
|
|
|
caps like ViewerAsset may be the same globally and wouldn't let us infer
|
|
which session / region the request was related to without a wrapper
|
|
"""
|
|
parsed = list(urllib.parse.urlsplit(self._caps[name][1]))
|
|
seed_id = self._caps["Seed"][1].split("/")[-1].encode("utf8")
|
|
# Give it a unique domain tied to the current Seed URI
|
|
parsed[1] = f"{name}-{hashlib.sha256(seed_id).hexdigest()[:16]}.hippo-proxy.localhost"
|
|
wrapper_url = urllib.parse.urlunsplit(parsed)
|
|
self._caps.add(name + "ProxyWrapper", (CapType.WRAPPER, wrapper_url))
|
|
return wrapper_url
|
|
|
|
def register_proxy_cap(self, name: str):
|
|
"""
|
|
Register a cap to be completely handled by the proxy
|
|
"""
|
|
cap_url = f"https://caps.hippo-proxy.localhost/cap/{uuid.uuid4()!s}"
|
|
self._caps.add(name, (CapType.PROXY_ONLY, cap_url))
|
|
return cap_url
|
|
|
|
def register_temporary_cap(self, name: str, cap_url: str):
|
|
"""Register a Cap that only has meaning the first time it's used"""
|
|
self._caps.add(name, (CapType.TEMPORARY, cap_url))
|
|
|
|
def resolve_cap(self, url: str, consume=True) -> Optional[Tuple[str, str, CapType]]:
|
|
for name, cap_info in self._caps.items():
|
|
cap_type, cap_url = cap_info
|
|
if url.startswith(cap_url):
|
|
if cap_type == CapType.TEMPORARY and consume:
|
|
# Resolving a temporary cap pops it out of the dict
|
|
temporary_caps = self._caps.popall(name)
|
|
temporary_caps.remove(cap_info)
|
|
self._caps.extend((name, x) for x in temporary_caps)
|
|
return name, cap_url, cap_type
|
|
return None
|
|
|
|
def mark_dead(self):
|
|
logging.info("Marking %r dead" % self)
|
|
if self.circuit:
|
|
self.circuit.is_alive = False
|
|
self.objects.clear()
|
|
|
|
def __repr__(self):
|
|
return "<%s %s>" % (self.__class__.__name__, self.name)
|
|
|
|
|
|
class EventQueueManager:
|
|
def __init__(self, region: ProxiedRegion):
|
|
# TODO: Per-EQ InjectionTracker so we can inject fake responses on 499
|
|
self._queued_events = []
|
|
self._region = weakref.proxy(region)
|
|
|
|
def queue_event(self, event: dict):
|
|
self._queued_events.append(event)
|
|
|
|
def take_events(self):
|
|
events = self._queued_events
|
|
self._queued_events = []
|
|
return events
|