# Copyright (c) 2008, Opera Software ASA
# license see LICENSE.
 
"""Overview:
 
The proxy / server is build as an extension of the asyncore.dispatcher class.
There are two instantiation of SimpleServer to listen on the given ports
for new connection, on for HTTP and the other for STP (ScopeTransferProtocol).
They do dispatch a connection to the appropriate classes, HTTPScopeInterface
for HTTP and ScopeConnection for STP. The client is the application which uses
the HTTP interface to connect to scope, the host is the Opera instance
which exposes the scope interface as a STP connection.
 
There are also two queues, one for HTTP and one for STP to return a scope
message to the client. Getting a new scope message is performed as GET request
with the path /get-message. If the STP queue is not empty then
the first of that queue is returned, otherwise the request is put
in the HTTP waiting-queue. If a new message arrives on the STP sockets it works
the other way around: if the waiting-queue is not empty, the message is
returned to the first waiting connection, otherwise it's put on
the STP message queue.
 
In contrast to the previous Java version there is only one waiting connection
for scope messages, the messages are dispatched to the target service
on the client side. The target service is added to the response
as custom header 'X-Scope-Message-Service'.
 
STP/1 messages have a header and a payload. The header is translated
to custom header fields, the payload is the body of the response:
 
    X-Scope-Message-Service for the service name
    X-Scope-Message-Command for the command id
    X-Scope-Message-Status for the status code
    X-Scope-Message-Tag for the tag
 
The server is named Dragonkeeper to stay in the started names pace.
 
The server supports only one host and one client. The main purpose is
developing Opera Dragonfly.
 
See also http://dragonfly.opera.com/app/scope-interface for more details.
"""
 
import re
import httpconnection
from time import time
from common import CRLF, RESPONSE_BASIC, RESPONSE_OK_CONTENT
from common import NOT_FOUND, BAD_REQUEST, get_timestamp, Singleton
# from common import pretty_dragonfly_snapshot
from utils import MessageMap, pretty_print_XML, pretty_print
from stpwebsocket import STPWebSocket
 
# the two queues
connections_waiting = []
scope_messages = []
 
SERVICE_LIST = """<services>%s</services>"""
SERVICE_ITEM = """<service name="%s"/>"""
XML_PRELUDE = """<?xml version="1.0"?>%s"""
 
class Scope(Singleton):
    """Access layer for HTTPScopeInterface instances to the scope connection"""
 
    version_map = {
        "stp-1": "STP/1",
        "stp-0": "STP/0",
        }
 
    def __init__(self):
        self.send_command = self.empty_call
        self.services_enabled = {}
        self.version = 'stp-0'
        self._service_list = []
        self._connection = None
        self._http_connection = None
 
    def empty_call(self, msg):
        pass
 
    def set_connection(self, connection):
        """ to register the scope connection"""
        self._connection = connection
        self.send_command = connection.send_command_STP_0
 
    def get_scope_connection(self):
        return self._connection
 
    def set_service_list(self, list):
        """to register the service list"""
        self._service_list = list
 
    def return_service_list(self, http_connection):
        """to get the service list.
        in STP/1 the request of the service list does trigger to (re) connect
        the client. Only after the Connect command was performed successfully
        the service list is returned to the client. any state must be reset"""
        # empty the scope message queue
        while scope_messages:
            scope_messages.pop()
        if self.version == 'stp-0':
            http_connection.return_service_list(self._service_list)
        elif self.version == 'stp-1':
            if self._connection:
                self._http_connection = http_connection
                self._connection.connect_client(self._connect_callback)
            else:
                http_connection.return_service_list(self._service_list)
        else:
            print "Unsupported version in scope.return_service_list(conn)"
 
    def set_STP_version(self, version):
        """to register the STP version.
        the version gets set as soon as the STP/1 token is received"""
        if version == "stp-1":
            self.version = "stp-1"
            self.send_command = self._connection.send_command_STP_1
        else:
            print "This stp version is not jet supported"
 
    def get_STP_version(self):
        return self.version_map[self.version]
 
    def reset(self):
        self._service_list = []
        self.send_command = self.empty_call
        self.services_enabled = {}
        self._connection = None
 
    def _connect_callback(self):
        if MessageMap.has_map():
            self._http_connection.return_service_list(self._service_list)
            self._http_connection = None
        else:
            MessageMap(self._service_list, self._connection,
                self._connect_callback, self._http_connection.context)
 
scope = Scope()
 
class HTTPScopeInterface(httpconnection.HTTPConnection):
    """To expose a HTTP interface of the scope interface.
 
    Documentation of the scope interface itself see:
 
        http://dragonfly.opera.com/app/scope-interface/
 
    The first part of the path is the command name, other parts are arguments.
    If there is no matching command, the path is served.
 
    GET methods:
        STP/0
            /services
                to get a list of available services
            /enable/<service name>
                to enable the given service
            /get-message
                to get a pending message or to wait for the next one
                header informations are added as custom header fields like:
                    X-Scope-Message-Service for the service name
        STP/1
            /services
                to get a list of available services
            /get-message
                to get a pending message or to wait for the next one
                header informations are added as custom header fields like:
                    X-Scope-Message-Service for the service name
                    X-Scope-Message-Command for the command id
                    X-Scope-Message-Status for the status code
                    X-Scope-Message-Tag for the tag
                the response body is the message in JSON format
                (except timeout responses which are still sent as xml)
 
            /stp-1-channel
                create a web socket channel
 
    POST methods:
        STP/0:
            /post-command/<service name>
                request body: message
        STP/1:
            /post-command/<service-name>/<command-id>/<tag>
                request body: message in JSON format
 
    The STP/1 HTTP interface supports only JSON format for the messages.
 
    """
 
    # scope specific responses
 
    # RESPONSE_SERVICELIST % ( timestamp, content length content )
    # HTTP/1.1 200 OK
    # Date: %s
    # Server: Dragonkeeper/0.8
    # Cache-Control: no-cache
    # Content-Type: application/xml
    # Content-Length: %s
    #
    # %s
 
    RESPONSE_SERVICELIST = RESPONSE_OK_CONTENT % (
        '%s',
        'Cache-Control: no-cache' + CRLF,
        'application/xml',
        '%s',
        '%s',
    )
 
    # RESPONSE_OK_OK % ( timestamp )
    # HTTP/1.1 200 OK
    # Date: %s
    # Server: Dragonkeeper/0.8
    # Cache-Control: no-cache
    # Content-Type: application/xml
    # Content-Length: 5
    #
    # <ok/>
 
    RESPONSE_OK_OK = RESPONSE_OK_CONTENT % (
        '%s',
        'Cache-Control: no-cache' + CRLF,
        'application/xml',
        len("<ok/>"),
        "<ok/>",
    )
 
    # RESPONSE_TIMEOUT % ( timestamp )
    # HTTP/1.1 200 OK
    # Date: %s
    # Server: Dragonkeeper/0.8
    # Cache-Control: no-cache
    # Content-Type: application/xml
    # Content-Length: 10
    #
    # <timeout/>
 
    RESPONSE_TIMEOUT = RESPONSE_OK_CONTENT % (
        '%s',
        'Cache-Control: no-cache' + CRLF,
        'application/xml',
        len('<timeout/>'),
        '<timeout/>',
    )
 
    # SCOPE_MESSAGE_STP_0 % ( timestamp, service, message length, message )
    # HTTP/1.1 200 OK
    # Date: %s
    # Server: Dragonkeeper/0.8
    # Cache-Control: no-cache
    # X-Scope-Message-Service: %s
    # Content-Type: application/xml
    # Content-Length: %s
    #
    # %s
 
    SCOPE_MESSAGE_STP_0 = RESPONSE_OK_CONTENT % (
        '%s',
        'Cache-Control: no-cache' + CRLF + \
        'X-Scope-Message-Service: %s' + CRLF,
        'application/xml',
        '%s',
        '%s',
    )
 
    # SCOPE_MESSAGE_STP_1 % ( timestamp, service, command, status,
    #                                           tag, message length, message )
    # HTTP/1.1 200 OK
    # Date: %s
    # Server: Dragonkeeper/0.8
    # Cache-Control: no-cache
    # X-Scope-Message-Service: %s
    # X-Scope-Message-Command: %s
    # X-Scope-Message-Status: %s
    # X-Scope-Message-Tag: %s
    # Content-Type: text/plain
    # Content-Length: %s
    #
    # %s
    SCOPE_MESSAGE_STP_1 = RESPONSE_OK_CONTENT % (
        '%s',
        'Cache-Control: no-cache' + CRLF + \
        'X-Scope-Message-Service: %s' + CRLF + \
        'X-Scope-Message-Command: %s' + CRLF + \
        'X-Scope-Message-Status: %s' + CRLF + \
        'X-Scope-Message-Tag: %s' + CRLF,
        'text/plain',
        '%s',
        '%s',
    )
 
    def __init__(self, conn, addr, context):
        httpconnection.HTTPConnection.__init__(self, conn, addr, context)
        self.debug = context.debug
        self.debug_format = context.format
        self.debug_format_payload = context.format_payload
        self.context = context
        # for backward compatibility
        self.scope_message = self.get_message
        self.send_command = self.post_command
 
    # ============================================================
    # GET commands ( first part of the path )
    # ============================================================
    def services(self):
        """to get the service list"""
        if connections_waiting:
            print ">>> failed, connections_waiting is not empty"
        scope.return_service_list(self)
        self.timeout = 0
 
    def return_service_list(self, serviceList):
        content = SERVICE_LIST % "".join(
            [SERVICE_ITEM % service.encode('utf-8')
            for service in serviceList])
        self.out_buffer += self.RESPONSE_SERVICELIST % (
            get_timestamp(),
            len(content),
            content)
 
    def get_stp_version(self):
        content = scope.get_STP_version()
        self.out_buffer += RESPONSE_OK_CONTENT % (
            get_timestamp(),
            '',
            "text/plain",
            len(content),
            content)
        self.timeout = 0
 
    def enable(self):
        """to enable a scope service"""
        service = self.arguments[0]
        if scope.services_enabled[service]:
            print ">>> service is already enabled", service
        else:
            scope.send_command("*enable %s" % service)
            scope.services_enabled[service] = True
 
            if service.startswith('stp-'):
                scope.set_STP_version(service)
 
        self.out_buffer += self.RESPONSE_OK_OK % get_timestamp()
        self.timeout = 0
 
    def get_message(self):
        """general call to get the next scope message"""
        if scope_messages:
            if scope.version == 'stp-1':
                self.return_scope_message_STP_1(scope_messages.pop(0), self)
            else:
                self.return_scope_message_STP_0(scope_messages.pop(0), self)
            self.timeout = 0
        else:
            connections_waiting.append(self)
        # TODO correct?
 
    def stp_1_channel(self):
        if 'Upgrade' in self.headers and self.headers['Upgrade'] == 'WebSocket':
            self.del_channel()
            self.timeout = 0
            STPWebSocket(self.socket,
                         self.headers,
                         self.in_buffer,
                         self.path,
                         self.context,
                         scope.get_scope_connection())
        else:
            self.out_buffer += BAD_REQUEST % get_timestamp()
            self.timeout = 0
 
    # ============================================================
    # POST commands
    # ============================================================
    def post_command(self):
        """send a command to scope"""
        raw_data = self.raw_post_data
        is_ok = False
        if scope.version == "stp-1":
            args = self.arguments
            """
            message type: 1 = command, 2 = response, 3 = event, 4 = error
            message TransportMessage
            {
                required string service = 1;
                required uint32 commandID = 2;
                required uint32 format = 3;
                optional uint32 status = 4;
                optional uint32 tag = 5;
                required binary payload = 8;
            }
            /send-command/" + service + "/" + command_id + "/" + tag
            """
            scope.send_command({
                    0: 1, # message type
                    1: args[0],
                    2: int(args[1]),
                    3: 1,
                    5: int(args[2]),
                    8: self.raw_post_data,
                })
            is_ok = True
        else:
            service = self.arguments[0]
            if service in scope.services_enabled:
                if not raw_data.startswith("<?xml") and \
                     not raw_data.startswith("STP/1"):
                    raw_data = XML_PRELUDE % raw_data
                msg = "%s %s" % (service, raw_data.decode('UTF-8'))
                scope.send_command(msg)
                is_ok = True
            else:
                print "tried to send a command before %s was enabled" % service
        self.out_buffer += (is_ok and
                            self.RESPONSE_OK_OK or
                            BAD_REQUEST) % get_timestamp()
        self.timeout = 0
 
    def snapshot(self):
        """store a markup snapshot"""
        raw_data = self.raw_post_data
        if raw_data:
            name, data = raw_data.split(CRLF, 1)
            f = open(name + ".xml", 'wb')
            # f.write(pretty_dragonfly_snapshot(data))
            data = data.replace("'=\"\"", "")
            data = re.sub(r'<script(?:[^/>]|/[^>])*/>[ \r\n]*', '', data)
            f.write(data.replace("'=\"\"", ""))
            f.close()
        self.out_buffer += self.RESPONSE_OK_OK % get_timestamp()
        self.timeout = 0
 
    # ============================================================
    # STP 0
    # ============================================================
    def return_scope_message_STP_0(self, msg, sender):
        """ return a message to the client"""
        service, payload = msg
        if self.debug:
            pretty_print_XML("\nsend to client: %s" % service, payload, self.debug_format)
        self.out_buffer += self.SCOPE_MESSAGE_STP_0 % (
            get_timestamp(),
            service,
            len(payload),
            payload)
        self.timeout = 0
        if not sender == self:
            self.handle_write()
 
    # ============================================================
    # STP 1
    # ============================================================
    def return_scope_message_STP_1(self, msg, sender):
        """ return a message to the client
        message TransportMessage
        {
            required string service = 1;
            required uint32 commandID = 2;
            required uint32 format = 3;
            optional uint32 status = 4;
            optional uint32 tag = 5;
            required binary payload = 8;
        }
        """
        if not msg[8]:
            # workaround, status 204 does not work
            msg[8] = ' '
        if self.debug:
            pretty_print("send to client:", msg,
                                self.debug_format, self.debug_format_payload)
        self.out_buffer += self.SCOPE_MESSAGE_STP_1 % (
            get_timestamp(),
            msg[1], # service
            msg[2], # command
            msg[4], # status
            msg[5], # tag
            len(msg[8]),
            msg[8], # payload
        )
        self.timeout = 0
        if not sender == self:
            self.handle_write()
 
    def timeouthandler(self):
        if self in connections_waiting:
            connections_waiting.remove(self)
            if not self.command in ["get_message", "scope_message"]:
                print ">>> failed, wrong connection type in queue"
            self.out_buffer += self.RESPONSE_TIMEOUT % get_timestamp()
        else:
            self.out_buffer += NOT_FOUND % (get_timestamp(), 0, '')
        self.timeout = 0
 
    def flush(self):
        if self.timeout:
            self.timeouthandler()
 
 
 
    # ============================================================
    # Implementations of the asyncore.dispatcher class methods
    # ============================================================
    def writable(self):
        if self.timeout and time() > self.timeout and not self.out_buffer:
            self.timeouthandler()
        return bool(self.out_buffer)
 
    def handle_close(self):
        if self in connections_waiting:
            connections_waiting.remove(self)
        self.close()