#!/usr/bin/python3
# a python/websockets thingy for rolling dice.
# Dependency: twisted web
# License: Public domain, except for the twisted websocket code following
# in a few lines.
# 
# This is an aid for having pen-and-paper RPGs during Corona or
# generally at a distance.  You can
#
# * roll dice with a bit of excitement
# * upload images (like, say, maps)
# * take a photo (of a sketch, say)
# * clicking on the image will put a common marker on the image
# * hitting a-z and then clicking on the image will put a lettered
#   marker there (hit Esc or Home to abort positioning the lettered marker, hit
#   Delete to remove the corresponding marker).
#
# All this in the convenience of a single python file (at the price of
# this being a wild mixture of python, javascript, html, css, and a dash
# of binary for the images; consider this part latter-day obfuscated code).
#
# Installation: Put the python file somewhere publicly accessible, run
#
# python3 wuerfler.py
#
# and tell your friends to access http://<yourmachine>:3344.
#
# In reality, you'll probably have to run this via https, as most
# user agents these days want https for webcam and websockets.
# See the foot of this file for an nginx configuration that ought to
# work on a virtual host.
#
# You may want to prepare maps and upload them from the command line.
# The recommended way to do that is to put them on a server (presumably
# https-reachably) and then use a shell alias like
#
# function setmapl() {
# 	curl -F image_url=https://where.your.maps.are/midkart/"$1" https://wuerfler.host/img
# 	echo ""
# }


DEBUG = True

############## stuff taken from the remotes/origin/websocket-4173-4
# of https://github.com/msdemlei/twisted.git
# under the twisted license:
#Copyright (c) 2001-2013
#Allen Short
#Andy Gayton
#Andrew Bennetts
#Antoine Pitrou
#Apple Computer, Inc.
#Benjamin Bruheim
#Bob Ippolito
#Canonical Limited
#Christopher Armstrong
#David Reid
#Donovan Preston
#Eric Mangold
#Eyal Lotem
#Itamar Turner-Trauring
#James Knight
#Jason A. Mobarak
#Jean-Paul Calderone
#Jessica McKellar
#Jonathan Jacobs
#Jonathan Lange
#Jonathan D. Simms
#Jürgen Hermann
#Kevin Horn
#Kevin Turner
#Mary Gardiner
#Matthew Lefkowitz
#Massachusetts Institute of Technology
#Moshe Zadka
#Paul Swartz
#Pavel Pergamenshchik
#Ralph Meijer
#Sean Riley
#Software Freedom Conservancy
#Travis B. Hartwell
#Thijs Triemstra
#Thomas Herve
#Timothy Allen
#
#Permission is hereby granted, free of charge, to any person obtaining
#a copy of this software and associated documentation files (the
#"Software"), to deal in the Software without restriction, including
#without limitation the rights to use, copy, modify, merge, publish,
#distribute, sublicense, and/or sell copies of the Software, and to
#permit persons to whom the Software is furnished to do so, subject to
#the following conditions:
#
#The above copyright notice and this permission notice shall be
#included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import base64
import re
from hashlib import sha1
from struct import pack, unpack

from zope.interface import implementer, providedBy, directlyProvides, Interface

from twisted.python import log
from twisted.python.constants import Values, ValueConstant
from twisted.internet.protocol import Protocol
from twisted.protocols.tls import TLSMemoryBIOProtocol
from twisted.web.resource import IResource
from twisted.web.server import NOT_DONE_YET



class _WSException(Exception):
    """
    Internal exception for control flow inside the WebSockets frame parser.
    """



class CONTROLS(Values):
    """
    Control frame specifiers.

    @since: 13.2
    """

    CONTINUE = ValueConstant(0)
    TEXT = ValueConstant(1)
    BINARY = ValueConstant(2)
    CLOSE = ValueConstant(8)
    PING = ValueConstant(9)
    PONG = ValueConstant(10)



class STATUSES(Values):
    """
    Closing status codes.

    @since: 13.2
    """

    NORMAL = ValueConstant(1000)
    GOING_AWAY = ValueConstant(1001)
    PROTOCOL_ERROR = ValueConstant(1002)
    UNSUPPORTED_DATA = ValueConstant(1003)
    NONE = ValueConstant(1005)
    ABNORMAL_CLOSE = ValueConstant(1006)
    INVALID_PAYLOAD = ValueConstant(1007)
    POLICY_VIOLATION = ValueConstant(1008)
    MESSAGE_TOO_BIG = ValueConstant(1009)
    MISSING_EXTENSIONS = ValueConstant(1010)
    INTERNAL_ERROR = ValueConstant(1011)
    TLS_HANDSHAKE_FAILED = ValueConstant(1056)



# The GUID for WebSockets, from RFC 6455.
_WS_GUID = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"



def _makeAccept(key):
    """
    Create an B{accept} response for a given key.

    @type key: C{str}
    @param key: The key to respond to.

    @rtype: C{str}
    @return: An encoded response.
    """
    return base64.b64encode(sha1(b"%s%s" % (key, _WS_GUID)).digest())



def _mask(buf, key):
    """
    Mask or unmask a buffer of bytes with a masking key.

    @type buf: C{str}
    @param buf: A buffer of bytes.

    @type key: C{str}
    @param key: The masking key. Must be exactly four bytes.

    @rtype: C{str}
    @return: A masked buffer of bytes.
    """
    key = list(key)
    buf = list(buf)
    for i, char in enumerate(buf):
        buf[i] = char ^ key[i % 4]
    return bytes(buf)



def _makeFrame(buf, opcode, fin, mask=None):
    """
    Make a frame.

    This function always creates unmasked frames, and attempts to use the
    smallest possible lengths.

    @type buf: C{str}
    @param buf: A buffer of bytes.

    @type opcode: C{CONTROLS}
    @param opcode: Which type of frame to create.

    @type fin: C{bool}
    @param fin: Whether or not we're creating a final frame.

    @type mask: C{int} or C{NoneType}
    @param mask: If specified, the masking key to apply on the created frame.

    @rtype: C{str}
    @return: A packed frame.
    """
    bufferLength = len(buf)
    if mask is not None:
        lengthMask = 0x80
    else:
        lengthMask = 0

    if bufferLength > 0xffff:
        length = bytes([lengthMask | 0x7f])+pack(">Q", bufferLength)
    elif bufferLength > 0x7d:
        length = bytes([lengthMask | 0x7e])+pack(">H", bufferLength)
    else:
        length = bytes([lengthMask | bufferLength])

    if fin:
        header = 0x80
    else:
        header = 0x01

    header = bytes([header | opcode.value])
    if mask is not None:
        buf = bytes([mask])+_mask(buf, mask)
    frame = header+length+buf
    return frame



def _parseFrames(frameBuffer, needMask=True):
    """
    Parse frames in a highly compliant manner. It modifies C{frameBuffer}
    removing the parsed content from it.

    @param frameBuffer: A buffer of bytes.
    @type frameBuffer: C{list}

    @param needMask: If C{True}, refuse any frame which is not masked.
    @type needMask: C{bool}
    """
    start = 0
    payload = b"".join(frameBuffer)

    while True:
        # If there's not at least two bytes in the buffer, bail.
        if len(payload) - start < 2:
            break

        # Grab the header. This single byte holds some flags and an opcode
        header = payload[start]
        if header & 0x70:
            # At least one of the reserved flags is set. Pork chop sandwiches!
            raise _WSException("Reserved flag in frame (%d)" % (header,))

        fin = header & 0x80

        # Get the opcode, and translate it to a local enum which we actually
        # care about.
        opcode = header & 0xf
        try:
            opcode = CONTROLS.lookupByValue(opcode)
        except ValueError:
            raise _WSException("Unknown opcode %d in frame" % opcode)

        # Get the payload length and determine whether we need to look for an
        # extra length.
        length = payload[start + 1]
        masked = length & 0x80

        if not masked and needMask:
            # The client must mask the data sent
            raise _WSException("Received data not masked")

        length &= 0x7f

        # The offset we'll be using to walk through the frame. We use this
        # because the offset is variable depending on the length and mask.
        offset = 2

        # Extra length fields.
        if length == 0x7e:
            if len(payload) - start < 4:
                break

            length = payload[start + 2:start + 4]
            length = unpack(">H", length)[0]
            offset += 2
        elif length == 0x7f:
            if len(payload) - start < 10:
                break

            # Protocol bug: The top bit of this long long *must* be cleared;
            # that is, it is expected to be interpreted as signed.
            length = payload[start + 2:start + 10]
            length = unpack(">Q", length)[0]
            offset += 8

        if masked:
            if len(payload) - (start + offset) < 4:
                # This is not strictly necessary, but it's more explicit so
                # that we don't create an invalid key.
                break

            key = payload[start + offset:start + offset + 4]
            offset += 4

        if len(payload) - (start + offset) < length:
            break

        data = payload[start + offset:start + offset + length]

        if masked:
            data = _mask(data, key)

        if opcode == CONTROLS.CLOSE:
            if len(data) >= 2:
                # Gotta unpack the opcode and return usable data here.
                code = STATUSES.lookupByValue(unpack(">H", data[:2])[0])
                data = code, data[2:]
            else:
                # No reason given; use generic data.
                data = STATUSES.NONE, b""

        yield opcode, data, bool(fin)
        start += offset + length

    if len(payload) > start:
        frameBuffer[:] = [payload[start:]]
    else:
        frameBuffer[:] = []



class IWebSocketsFrameReceiver(Interface):
    """
    An interface for receiving WebSockets frames.

    @since: 13.2
    """

    def makeConnection(transport):
        """
        Notification about the connection.

        @param transport: A L{WebSocketsTransport} instance wrapping an
            underlying transport.
        @type transport: L{WebSocketsTransport}.
        """


    def frameReceived(opcode, data, fin):
        """
        Callback when a frame is received.

        @type opcode: C{CONTROLS}
        @param opcode: The type of frame received.

        @type data: C{bytes}
        @param data: The content of the frame received.

        @type fin: C{bool}
        @param fin: Whether or not the frame is final.
        """



class WebSocketsTransport(object):
    """
    A frame transport for WebSockets.

    @ivar _transport: A reference to the real transport.

    @since: 13.2
    """

    _disconnecting = False

    def __init__(self, transport):
        self._transport = transport


    def sendFrame(self, opcode, data, fin):
        """
        Build a frame packet and send it over the wire.

        @type opcode: C{CONTROLS}
        @param opcode: The type of frame to send.

        @type data: C{bytes}
        @param data: The content of the frame to send.

        @type fin: C{bool}
        @param fin: Whether or not we're sending a final frame.
        """
        packet = _makeFrame(data, opcode, fin)
        self._transport.write(packet)


    def loseConnection(self, code=STATUSES.NORMAL, reason=""):
        """
        Close the connection.

        This includes telling the other side we're closing the connection.

        If the other side didn't signal that the connection is being closed,
        then we might not see their last message, but since their last message
        should, according to the spec, be a simple acknowledgement, it
        shouldn't be a problem.

        @param code: The closing frame status code.
        @type code: L{STATUSES}

        @param reason: Optionally, a utf-8 encoded text explaining why the
            connection was closed.
        @param reason: C{bytes}
        """
        # Send a closing frame. It's only polite. (And might keep the browser
        # from hanging.)
        if not self._disconnecting:
            data = b"%s%s" % (pack(">H", code.value), reason)
            frame = _makeFrame(data, CONTROLS.CLOSE, True)
            self._transport.write(frame)
            self._disconnecting = True
            self._transport.loseConnection()



class WebSocketsProtocol(Protocol):
    """
    A protocol parsing WebSockets frames and interacting with a
    L{IWebSocketsFrameReceiver} instance.

    @ivar _receiver: The L{IWebSocketsFrameReceiver} provider handling the
        frames.
    @type _receiver: L{IWebSocketsFrameReceiver} provider

    @ivar _buffer: The pending list of frames not processed yet.
    @type _buffer: C{list}

    @since: 13.2
    """
    _buffer = None


    def __init__(self, receiver):
        self._receiver = receiver


    def connectionMade(self):
        """
        Log the new connection and initialize the buffer list.
        """
        peer = self.transport.getPeer()
        log.msg(format="Opening connection with %(peer)s", peer=peer)
        self._buffer = []
        self._receiver.makeConnection(WebSocketsTransport(self.transport))


    def _parseFrames(self):
        """
        Find frames in incoming data and pass them to the underlying protocol.
        """
        for opcode, data, fin in _parseFrames(self._buffer):
            self._receiver.frameReceived(opcode, data, fin)
            if opcode == CONTROLS.CLOSE:
                # The other side wants us to close.
                code, reason = data
                msgFormat = "Closing connection: %(code)r"
                if reason:
                    msgFormat += " (%(reason)r)"
                log.msg(format=msgFormat, reason=reason, code=code)

                # Close the connection.
                self.transport.loseConnection()
                return
            elif opcode == CONTROLS.PING:
                # 5.5.2 PINGs must be responded to with PONGs.
                # 5.5.3 PONGs must contain the data that was sent with the
                # provoking PING.
                self.transport.write(_makeFrame(data, CONTROLS.PONG, True))


    def dataReceived(self, data):
        """
        Append the data to the buffer list and parse the whole.

        @type data: C{bytes}
        @param data: The buffer received.
        """
        self._buffer.append(data)
        try:
            self._parseFrames()
        except _WSException:
            # Couldn't parse all the frames, something went wrong, let's bail.
            log.err()
            self.transport.loseConnection()



@implementer(IWebSocketsFrameReceiver)
class _WebSocketsProtocolWrapperReceiver():
    """
    A L{IWebSocketsFrameReceiver} which accumulates data frames and forwards
    the payload to its C{wrappedProtocol}.

    @ivar _wrappedProtocol: The connected protocol
    @type _wrappedProtocol: C{IProtocol} provider.

    @ivar _transport: A reference to the L{WebSocketsTransport}
    @type _transport: L{WebSocketsTransport}

    @ivar _messages: The pending list of payloads received.
    @types _messages: C{list}
    """

    def __init__(self, wrappedProtocol):
        self._wrappedProtocol = wrappedProtocol


    def makeConnection(self, transport):
        """
        Keep a reference to the given C{transport} and instantiate the list of
        messages.
        """
        self._transport = transport
        self._messages = []


    def frameReceived(self, opcode, data, fin):
        """
        For each frame received, accumulate the data (ignoring the opcode), and
        forwarding the messages if C{fin} is set.

        @type opcode: C{CONTROLS}
        @param opcode: The type of frame received.

        @type data: C{bytes}
        @param data: The content of the frame received.

        @type fin: C{bool}
        @param fin: Whether or not the frame is final.
        """
        if not opcode in (CONTROLS.BINARY, CONTROLS.TEXT, CONTROLS.CONTINUE):
            return
        self._messages.append(data)
        if fin:
            content = b"".join(self._messages)
            self._messages[:] = []
            self._wrappedProtocol.dataReceived(content)



class WebSocketsProtocolWrapper(WebSocketsProtocol):
    """
    A L{WebSocketsProtocol} which wraps a regular C{IProtocol} provider,
    ignoring the frame mechanism.

    @ivar _wrappedProtocol: The connected protocol
    @type _wrappedProtocol: C{IProtocol} provider.

    @ivar defaultOpcode: The opcode used when C{transport.write} is called.
        Defaults to L{CONTROLS.TEXT}, can be L{CONTROLS.BINARY}.
    @type defaultOpcode: L{CONTROLS}

    @since: 13.2
    """

    def __init__(self, wrappedProtocol, defaultOpcode=CONTROLS.TEXT):
        self.wrappedProtocol = wrappedProtocol
        self.defaultOpcode = defaultOpcode
        WebSocketsProtocol.__init__(
            self, _WebSocketsProtocolWrapperReceiver(wrappedProtocol))


    def makeConnection(self, transport):
        """
        Upon connection, provides the transport interface, and forwards ourself
        as the transport to C{self.wrappedProtocol}.

        @type transport: L{twisted.internet.interfaces.ITransport} provider.
        @param transport: The transport to use for the protocol.
        """
        directlyProvides(self, providedBy(transport))
        WebSocketsProtocol.makeConnection(self, transport)
        self.wrappedProtocol.makeConnection(self)


    def write(self, data):
        """
        Write to the websocket protocol, transforming C{data} in a frame.

        @type data: C{bytes}
        @param data: Data buffer used for the frame content.
        """
        if DEBUG:
            print("Sending: {}".format(data))
        self._receiver._transport.sendFrame(self.defaultOpcode, data, True)


    def writeSequence(self, data):
        """
        Send all chunks from C{data} using C{write}.

        @type data: C{list} of C{bytes}
        @param data: Data buffers used for the frames content.
        """
        for chunk in data:
            self.write(chunk)


    def loseConnection(self):
        """
        Try to lose the connection gracefully when closing by sending a close
        frame.
        """
        self._receiver._transport.loseConnection()


    def __getattr__(self, name):
        """
        Forward all non-local attributes and methods to C{self.transport}.
        """
        return getattr(self.transport, name)


    def connectionLost(self, reason):
        """
        Forward C{connectionLost} to C{self.wrappedProtocol}.

        @type reason: L{twisted.python.failure.Failure}
        @param reason: A failure instance indicating the reason why the
            connection was lost.
        """
        self.wrappedProtocol.connectionLost(reason)



@implementer(IResource)
class WebSocketsResource(object):
    """
    A resource for serving a protocol through WebSockets.

    This class wraps a factory and connects it to WebSockets clients. Each
    connecting client will be connected to a new protocol of the factory.

    Due to unresolved questions of logistics, this resource cannot have
    children.

    @param lookupProtocol: A callable returning a tuple of
        (protocol instance, matched protocol name or C{None}) when called with
        a valid connection. It's called with a list of asked protocols from the
        client and the connecting client request. If the returned protocol name
        is specified, it is used as I{Sec-WebSocket-Protocol} value. If the
        protocol is a L{WebSocketsProtocol} instance, it will be connected
        directly, otherwise it will be wrapped by L{WebSocketsProtocolWrapper}.
        For simple use cases using a factory, you can use
        L{lookupProtocolForFactory}.
    @type lookupProtocol: C{callable}.

    @since: 13.2
    """
    isLeaf = True

    def __init__(self, lookupProtocol):
        self._lookupProtocol = lookupProtocol


    def getChildWithDefault(self, name, request):
        """
        Reject attempts to retrieve a child resource.  All path segments beyond
        the one which refers to this resource are handled by the WebSocket
        connection.

        @type name: C{bytes}
        @param name: A single path component from a requested URL.

        @type request: L{twisted.web.iweb.IRequest} provider
        @param request: The request received.
        """
        raise RuntimeError(
            "Cannot get IResource children from WebSocketsResource")


    def putChild(self, path, child):
        """
        Reject attempts to add a child resource to this resource.  The
        WebSocket connection handles all path segments beneath this resource,
        so L{IResource} children can never be found.

        @type path: C{bytes}
        @param path: A single path component.

        @type child: L{IResource} provider
        @param child: A resource to put underneat this one.
        """
        raise RuntimeError(
            "Cannot put IResource children under WebSocketsResource")


    def render(self, request):
        """
        Render a request.

        We're not actually rendering a request. We are secretly going to handle
        a WebSockets connection instead.

        @param request: The connecting client request.
        @type request: L{Request<twisted.web.http.Request>}

        @return: a string if the request fails, otherwise C{NOT_DONE_YET}.
        """
        request.defaultContentType = None
        # If we fail at all, we'll fail with 400 and no response.
        failed = False

        if request.method != b"GET":
            # 4.2.1.1 GET is required.
            failed = True

        upgrade = request.getHeader(b"Upgrade")
        if upgrade is None or b"websocket" not in upgrade.lower():
            # 4.2.1.3 Upgrade: WebSocket is required.
            failed = True

        connection = request.getHeader(b"Connection")
        if connection is None or b"upgrade" not in connection.lower():
            # 4.2.1.4 Connection: Upgrade is required.
            failed = True

        key = request.getHeader(b"Sec-WebSocket-Key")
        if key is None:
            # 4.2.1.5 The challenge key is required.
            failed = True

        version = request.getHeader(b"Sec-WebSocket-Version")
        if version != b"13":
            # 4.2.1.6 Only version 13 works.
            failed = True
            # 4.4 Forward-compatible version checking.
            request.setHeader(b"Sec-WebSocket-Version", b"13")

        if failed:
            request.setResponseCode(400)
            return b""

        askedProtocols = request.requestHeaders.getRawHeaders(
            b"Sec-WebSocket-Protocol")
        protocol, protocolName = self._lookupProtocol(askedProtocols, request)

        # If a protocol is not created, we deliver an error status.
        if not protocol:
            request.setResponseCode(502)
            return b""

        # We are going to finish this handshake. We will return a valid status
        # code.
        # 4.2.2.5.1 101 Switching Protocols
        request.setResponseCode(101)
        # 4.2.2.5.2 Upgrade: websocket
        request.setHeader(b"Upgrade", b"WebSocket")
        # 4.2.2.5.3 Connection: Upgrade
        request.setHeader(b"Connection", b"Upgrade")
        # 4.2.2.5.4 Response to the key challenge
        request.setHeader(b"Sec-WebSocket-Accept", _makeAccept(key))
        # 4.2.2.5.5 Optional codec declaration
        if protocolName:
            request.setHeader(b"Sec-WebSocket-Protocol", protocolName)

        # Provoke request into flushing headers and finishing the handshake.
        request.write(b"")

        # And now take matters into our own hands. We shall manage the
        # transport's lifecycle.
        transport, request.transport = request.transport, None

        if not isinstance(protocol, WebSocketsProtocol):
            protocol = WebSocketsProtocolWrapper(protocol)

        # Connect the transport to our factory, and make things go. We need to
        # do some stupid stuff here; see #3204, which could fix it.
        if isinstance(transport.protocol, TLSMemoryBIOProtocol):
            transport.protocol.wrappedProtocol = protocol
        else:
            transport.protocol = protocol
        protocol.makeConnection(transport)

        return NOT_DONE_YET



def lookupProtocolForFactory(factory):
    """
    Return a suitable C{lookupProtocol} argument for L{WebSocketsResource}
    which ignores the protocol names and just return a protocol instance built
    by C{factory}.

    @since: 13.2
    """

    def lookupProtocol(protocolNames, request):
        protocol = factory.buildProtocol(request.transport.getPeer())
        return protocol, None

    return lookupProtocol


######################## End stuff from twisted

import random
import sys

from twisted.internet import reactor
from twisted.internet.protocol import Factory
from twisted.web import resource
from twisted.web import server
from twisted.web import util


# base64-encoded PNG of the default marker
MARKER_IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAB8AAAAgAgMAAACX7EvSAAAACVBMVEUAABD/Q/v/VvpBXLjAAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAAEzkAABM5AY/CVgEAAAAHdElNRQfkBAYSASCsPWy/AAAAdklEQVQY012P0QkAIQxD9aMjdB9H8KOB23+SS9oqxwniw4ZnHBi9gAu4UAQDdsJo4r0jCJ65ReCemgmGR4MxaYIp2DqYJhgWAxb0bAZmUPMBigSPAOcGnaHaD5SHxjJLnfWkztezlfqo6Kx/OLKzZuezXpP/egF3+iGpnuC1iQAAAABJRU5ErkJggg=="


class Doc(resource.Resource):
	isLeaf = True
	content = (("""
<html xmlns="http://www.w3.org/1999/html">
<head><title>Roll the dice</title>
<script type="text/javascript">//<![CDATA[
var ws;

function make_inverse_transformation(container, absolute) {
	// returns a function to transform from pixel to relative
	// coordinates in container.
	// With absolute, it's window-relative, without, it's container-
	// relative coordiates.
	var container_rect = container.getBoundingClientRect();
	if (absolute) {
		var container_x0 = window.pageXOffset+container_rect.x;
		var container_y0 = window.pageYOffset+container_rect.y;
	} else {
		var container_x0 = 0;
		var container_y0 = 0;
	}
	
	return function(pix_x, pix_y) {
		return new Array(
			(pix_x-container_x0)/container_rect.width,
			(pix_y-container_y0)/container_rect.height);
	}
}


function create_websocket() {
	display("CC");
	ws = new WebSocket(
		((window.location.protocol === "https:") ? "wss://" : "ws://") 
		+ window.location.host + "/w");

	var image_area = document.getElementById("imageAndMarker");
	var image_element = document.getElementById("img");
	var linecanvas = document.getElementById("linecanvas");

	function make_transformation(absolute) {
		// returns a function to transform from relative to pixel
		// coordinates in our image container.
		// With absolute, it's window-relative, without, it's container-
		// relative coordiates.
		var container_rect = document.getElementById("img"
			).getBoundingClientRect();
		if (absolute) {
			var container_x0 = window.pageXOffset+container_rect.x;
			var container_y0 = window.pageYOffset+container_rect.y;
		} else {
			var container_x0 = 0;
			var container_y0 = 0;
		}
		
		return function(rel_x, rel_y) {
			return new Array(
				container_x0+parseFloat(rel_x)*container_rect.width,
				container_y0+parseFloat(rel_y)*container_rect.height);
		}
	}

	// drawing lines; we don't keep a local memory of the lines.  Instead,
	// the server always transmits a complete set of lines.

	function draw_line(context, start_pos, end_pos) {
		// draws a line on the canvas though context
		// colour remains set as a side effect.
		context.beginPath()
		context.moveTo(start_pos[0], start_pos[1]);
		context.lineTo(end_pos[0], end_pos[1]);
		context.stroke();
	}

	function add_line(context, start_pos, end_pos) {
		// adds a line to the current path in context
		context.moveTo(start_pos[0], start_pos[1]);
		context.lineTo(end_pos[0], end_pos[1]);
	}

	function draw_lines(items) {
		// draws the line specified in items (which is what we get
		// from websocket: a sequence of rel_x_1 rel_y_1 rel_x_2 rel_y_2).
		// as a side effect, we're clearing the canvas.
		var trafo = make_transformation(0);
		var context = linecanvas.getContext('2d');
		context.clearRect(0, 0, linecanvas.width, linecanvas.height);

		context.beginPath();
		context.strokeStyle = 'rgb(240, 120, 220)';
		context.lineWidth = 3;
		for (var i=0; i<items.length; i+=4) {
			add_line(context,
				trafo(items[i], items[i+1]),
				trafo(items[i+2], items[i+3]));
		}
		context.stroke();
	}

	function set_drag_handler(move_handler, finish_handler) {
		linecanvas.addEventListener("mousemove", move_handler, false);
		var finisher = function(ev) {
			linecanvas.removeEventListener("mouseup", finisher, true);
			linecanvas.removeEventListener("mousemove", move_handler, false);
			finish_handler(ev);
		};
		linecanvas.addEventListener("mouseup", finisher, true);
	}

	function startdrag(ev) {
		ev.preventDefault();
		ev.stopPropagation();

		var bounds = linecanvas.getBoundingClientRect();
		var root_x = bounds.x-window.pageXOffset;
		var root_y = bounds.y-window.pageYOffset;

		var start_pos = new Array(
			ev.clientX-root_x,
    	ev.clientY-root_y);
   	var cur_end = new Array(start_pos);
    var context = linecanvas.getContext('2d');
    var backing_store = null;
    var extra_pix = 3 // should probably be = line width;

    set_drag_handler(
    	function (ev) {
				ev.preventDefault();
				ev.stopPropagation();

				if (backing_store) {
					context.putImageData(backing_store,
    				Math.min(start_pos[0], cur_end[0])-extra_pix,
    				Math.min(start_pos[1], cur_end[1])-extra_pix);
				}

				cur_end = new Array(
					ev.clientX-root_x,
    			ev.clientY-root_y);
   			backing_store = context.getImageData(
   				Math.min(start_pos[0], cur_end[0])-extra_pix,
   				Math.min(start_pos[1], cur_end[1])-extra_pix,
   				Math.abs(start_pos[0]-cur_end[0])+2*extra_pix,
   				Math.abs(start_pos[1]-cur_end[1])+2*extra_pix);
				draw_line(context, start_pos, cur_end);
    	},

    	function(ev) {
				if (backing_store) {
					context.putImageData(backing_store,
    				Math.min(start_pos[0], cur_end[0])-extra_pix,
    				Math.min(start_pos[1], cur_end[1])-extra_pix);
				}
				
				// filter out simple clicks
				if (Math.max(
						Math.abs(start_pos[0]-cur_end[0]),
						Math.abs(start_pos[1]-cur_end[1]))>10) {

					ev.preventDefault();
					ev.stopPropagation();

					var invtrafo = make_inverse_transformation(image_element, 0);
					var p_start = invtrafo(start_pos[0], start_pos[1]);
					var p_end = invtrafo(cur_end[0], cur_end[1]);
					ws.send("lines "+p_start[0]+" "+p_start[1]
						+" "+p_end[0]+" "+p_end[1]);
				}
    	});
	}

	linecanvas.addEventListener("mousedown", startdrag, false);

	// drawing markers

	var cur_markers = [];

	function create_marker(label) {
		var marker = document.createElement('p');
		marker.innerHTML = label;
		marker.className = 'freeMarker';
		return marker;
	}

	function remove_marker(label) {
		if (cur_markers[label])  {
			image_area.removeChild(cur_markers[label]);
			cur_markers[label] = undefined;
		}
	}

	function remove_all_markers(ev) {
		for (var label in cur_markers) {
			remove_marker(label);
		}
	}

	function get_marker_for(label) {
		if (label=='default') {
			return document.getElementById("marker");
		} else if (is_marker_key(label.toLowerCase())) {
			if (!cur_markers[label]) {
				cur_markers[label] = create_marker(label);
				image_area.appendChild(cur_markers[label]);
			}
			return cur_markers[label];
		} else {
			return null;
		}
	}

	function move_marker_to(label, pos) {
		// moves the marker with label ("default" counts) to pos (which is [x, y]).
		var marker = get_marker_for(label);

		/* for the letter markers, try to center them */
		var delta_x = 0; 
		var delta_y = 0;
		if (label!="default") {
			var marker_rect = marker.getBoundingClientRect();
			delta_x = marker_rect.width/2.;
			delta_y = marker_rect.height/2;
		}

		marker.style.left = (pos[0]-delta_x)+"px";
		marker.style.top = (pos[1]-delta_y)+"px";
	}

	function draw_markers(items) {
		// draws the markers specified in items (which is what we get
		// from websockets: a sequence of label rel_x rel_y as strings
		if (items.length==1) {
			// just a spurious blank, don't try to draw anything
			return;
		}
		var trafo = make_transformation(1);

		for (var i=0; i<items.length; i+=3) {
			move_marker_to(items[i], trafo(items[i+1], items[i+2]));
		}
	}
	
	// Main event handler

	ws.onmessage = function (ev) {
		var items = ev.data.split(" ");
		switch (items[0]) {
			case 'disable':
				disable_rolls();
				break;
			case 'enable':
				enable_rolls();
				break;
			case 'new_image':
				update_image();
				break;
			case 'rolled':
				items.shift();
				Roller(items);
				break;
			case 'remove':
				if (items[1]=="ALL") {
					remove_all_markers();
				} else {
					remove_marker(items[1]);
				};
				break;
			case 'move':
				items.shift();
				draw_markers(items);
				break;
			case 'lines':
				items.shift();
				draw_lines(items);
				break;
			default:
				// ignore unknown for now.
				break;
		}
	}

	// connection management

	function reconnect(ev) {
		display("XX");
	 	window.setTimeout(create_websocket, 5000);
	}

	ws.onerror = function() {display("EE"); ws.close();}
	ws.onclose = reconnect;

	ws.onopen = function(ev) {
		display("--");
	}
}


window.addEventListener('load', function() {
	create_websocket();
	}, false);


function keep_alive() {
	ws.send("NOP");
	window.setTimeout(keep_alive, 5000);
}

window.setTimeout(keep_alive, 5000);

function display(thing) {
	document.getElementById("cur").firstChild.data = thing;
}

function update_image() {
	document.getElementById("img").src = "/img?"+Math.random().toString();
}

function Roller(items) {
	var target = document.getElementById("cur");
	var cur_opac = 1;
	var base_delta = 0.2;
	var opac_delta = base_delta;
	target.style.opcacity = cur_opac;

	function animate_next() {
		if (cur_opac>=1) {
			// something is shown, decide if that's stable or whether to go on
			if (items.length>0) {
				opac_delta = -base_delta;
				base_delta = base_delta*0.65;
				animate_step();
			} else {
				ws.send("enable");
			}
		} else {
			// things are faded out, fade in the next item
			// I can't see why, but some circumstances may make items empty
			// here.  So, re-check, though I can't see how it happens
			if (items.length>0) {
				display(items.pop());
			}
			opac_delta = base_delta;
			animate_step();
		}
	}

	function animate_step() {
		cur_opac = cur_opac+opac_delta;
		target.style.opacity = cur_opac;
		if (0<cur_opac && cur_opac<1) {
			window.setTimeout(animate_step, 20);
		} else {
			animate_next();
		}
	}

	animate_next();
}

function disable_rolls() {
	for (var b of document.querySelectorAll('button')) {
		b.disabled = true;
	}
}

function enable_rolls() {
	for (var b of document.querySelectorAll('button')) {
		b.disabled = false;
	}
}

function roll(what) {
	ws.send("disable");
	ws.send(what);
}

function send_form(a_form) {
	var data = new FormData(a_form);

  var xhr = new XMLHttpRequest();
	xhr.open('POST', a_form.action);
	xhr.send(data);
	return false;
}

function is_marker_key(key) {
	return (key>='a' && key<='z') ||
		(key>='0' && key<='9');
}

function setup_markers() {
	var next_marker = null;
	var marker_display = document.getElementById('nextMarker');
	var image_area = document.getElementById("img");
	var focus = document.getElementById("focus");

	function set_next_marker(key) {
		next_marker = key;
		marker_display.firstChild.data = key;
		marker_display.style.display = 'block';
	}

	function clear_next_marker() {
		next_marker = null;
		marker_display.style.display = 'none';
	}

	function handle_key(ev) {
    focus.value = "";
    focus.focus();

		if (ev.altKey || ev.ctrlKey || ev.metaKey) {
			return;
		}

		if (is_marker_key(ev.key)) {
			set_next_marker(ev.key.toUpperCase());
		} else if (ev.key=='Escape' || ev.key=='Home') {
			clear_next_marker();
		} else if (ev.key=='Delete') {
			if (next_marker) {
				ws.send("remove "+next_marker);
				clear_next_marker();
			}
		} else if (ev.key=='-') {
			ws.send("popline");
		} else if (ev.key=='_') {
			ws.send("clearlines");
		} else if (ev.key=='?') {
			show_help();
		} else {
			return;
		}
    ev.preventDefault();
    ev.stopPropagation();
	}

	function position_marker(ev) {
    var invtrafo = make_inverse_transformation(
			document.getElementById("img"), 1);
		var pos = invtrafo(ev.pageX, ev.pageY);

		if (pos[0]<0 || pos[0]>1
			|| pos[1]<0 || pos[1]>1) {
			return;
		}

		if (next_marker===null) {
			ws.send("move default "+pos[0]+" "+pos[1]);
		} else {
			ws.send("move "+next_marker+" "+pos[0]+" "+pos[1]);
			clear_next_marker();
		}
    focus.focus();
	}

	window.addEventListener('keyup', handle_key, false);
	focus.addEventListener('keyup', handle_key, false);
	focus.focus();
	// I'm listening on the window rather the image because the image
	// is covered by the canvas and doesn't receive clicks.
	// I'm filtering clicks outside of the image in position_marker.
	window.addEventListener("mouseup", position_marker, false);
	document.getElementById("clearmarkers"
		).addEventListener("click", function() {
				ws.send("remove ALL");
			}, false);
}

(function() {

  var video = null;
  var photocanvas = null;
  var photo = null;
  var shutter = null;

  function startup() {
    video = document.getElementById('video');
    photocanvas = document.getElementById('photocanvas');
    photo = document.getElementById('photo');
    shutter = document.getElementById('takephoto');

    shutter.addEventListener('click', function(ev){
      takepicture();
      ev.preventDefault();
    }, false);
  }


  // Fill the photo with an indication that none has been
  // captured.

  function takepicture() {
    navigator.mediaDevices.getUserMedia({video: true, audio: false})
    .then(function(stream) {
      video.srcObject = stream;
    	video.play();
    })
    .catch(function(err) {
      console.log("Can't take a picture: " + err);
    });

    video.addEventListener("canplay", snap, false);
  }

	function snap() {
    var context = photocanvas.getContext('2d');
    photocanvas.width = video.width;
    photocanvas.height = video.height;
    context.drawImage(video, 0, 0, photocanvas.width, photocanvas.height);
    video.srcObject.getTracks().forEach(function(track) {
  		track.stop();
		});
		video.srcObject = null;
  
    var ser_image = photocanvas.toDataURL('image/png');
		var data = new FormData();
		data.append('enc_image', ser_image);
		var xhr = new XMLHttpRequest();
    xhr.open('POST', "/img");
		xhr.send(data);
  }

  // Set up our event listener to run the startup process
  // once loading is complete.
  window.addEventListener('load', startup, false);
})();


function setup_image_sizer() {
	// make sure the image fits the image area, scaling if necessary either
	// vertically or horizontally.
	var image = document.getElementById("img");
	var container = document.getElementById("imageAndMarker");
	var linecanvas = document.getElementById("linecanvas");
	var scale_marker_callback = false;

	function scale_image(ev) {
    var container_rect = container.getBoundingClientRect();
    var scale = Math.min(
    	container_rect.width/1.0/image.naturalWidth,
    	container_rect.height/1.0/image.naturalHeight);

		var im_width = scale*image.naturalWidth;
		var im_height = scale*image.naturalHeight;
		image.style.width = im_width+"px";
		image.style.height = im_height+"px";

		// let the line canvas overlap us
		linecanvas.style.display = "block";
		linecanvas.style.left = window.pageXOffset+container_rect.x+"px";
		linecanvas.style.top = window.pageYOffset+container_rect.y+"px";
		linecanvas.style.width = image.style.width;
		linecanvas.style.height = image.style.height;
		linecanvas.width = im_width;
		linecanvas.height = im_height;

		// after a scale, wait a bit before scaling the markers; we don't
		// want dozens of updates a second during an interactive
		// resize.
		if (scale_marker_callback) {
			clearTimeout(scale_marker_callback);
		}
		scale_marker_callback = setTimeout(function() {
			ws.send("send_markers");
		}, 200);
	}

	window.addEventListener('resize', scale_image, false);
	image.addEventListener('load', scale_image, false);
	scale_image();
}

function show_help() {
	var help = document.getElementById("help");
	help.style.display = 'block';
}

function hide_help() {
	document.getElementById("help").style.display = 'none';
}

window.addEventListener('load', function() {
	setup_markers();
	setup_image_sizer();
}, false);
//]]></script>
<style type="text/css">
button.roll {
	width: 6rem;
	height: 6rem;
	font-weight: bold;
	font-size: 2em;
}

p.freeMarker {
	display: block;
	width: auto;
	position: absolute;
	font-weight: bold;
	color: #44e;
	font-size: 15pt;
	background: rgba(255,0,0,0.5);
	border: 1pt solid rgba(0,255,255,0.5);
	margin: 0px 0px 0px 0px;
}

#ui {
	display: flex;
	flex-flow: row nowrap;
	height: 100%;
}

#buttonbar {
	height: 100%;
	width: 10.5rem;
	flex-basis: 10.5rem;
	flex-grow: 0;
	flex-shrink: 0;
	display: flex;
	flex-flow: column;
	justify-content: space-evenly;
	text-align:center;
	overflow: hidden;
}

#buttonbar button {
	margin-left: auto;
	margin-right: auto;
}


#cur {
	font-weight: bold;
	font-size: 3em;
	text-align: center;
	padding: 5px;
}

#img {
	width: 100%;
}

#marker {
	display: block;
	position: absolute;
	left: -60px;
	top: -60px;
}

#nextMarker {
	position: absolute;
	color: #0044ff;
	font-weight: bold;
	font-size: 17pt;
	top: 0pt;
	left: 0pt;
	border: 1pt solid #ccc;
}

#imageAndMarker {
	flex-basis: 640pt;
	flex-grow: 1;
	flex-shrink: 1;
	z-index: 15;
	max-height: 100%;
	min-width: 100pt;
}

#linecanvas {
	z-index: 100;
	position: absolute;
	display: none;
}

#focus {
	border: 1px solid grey;
	width: 10pt;
	height: 8pt;
}

.small {
	font-size: 7pt;
}

#help {
	display: none;
	position: fixed;
	top: 3ex;
	right: 3ex;
	width: 30em;
	background: white;
	border: 5px solid blue;
	padding: 1ex;
	z-index: 10000;
}
</style>
</head>
"""
)+("""<body>
<canvas id="linecanvas"></canvas>
<div id="ui">
<div id="buttonbar">
<p id="nextMarker" style="display:none">-</p>
<button class="roll" onclick="roll('d6')">6</button>
<button class="roll" onclick="roll('d20')">20</button>
<button class="roll" onclick="roll('d100')">100</button>
<p id="cur">EE</p>
<form method="POST" action="/img" id="imgform">
<input name="imgfile" type="file"/><br/>
<button onclick="return send_form(this.form)">Upload file</button><br/>
<button id="takephoto" onclick="return false">Take photo</button>
<button id="clearmarkers" onclick="return false">Clear markers</button>
<div class="small">
<input id="focus" type="text" size="1"/>
? for help</div>
</form>
</div>

<div id="imageAndMarker">
<img id="img" src="/img"/>
<img id="marker" src="data:image/png;base64,{marker_image}"/>
</div>
</div>

<div id="help">
<dl>
<dt>Click</dt>
<dd>Set marker</dd>
<dt>Drag</dt>
<dd>Draw a line</dd>
<dt>a-z0-9</dt>
<dd>prepare for set (click on image) or remove (delete) 
the corresponding marker</dd>
<dt>ESC or Home</dt>
<dd>un-prime the next marker</dd>
<dt>-</dt>
<dd>Remove the last line drawn</dd>
<dt>_</dt>
<dd>Remove the all lines</dd>
</dl>
<button onclick="hide_help()"
	style="display:block; margin:1ex auto">Close</button>
</div>

<video id="video" width="640" height="480" style="display:none"/>
<canvas id="photocanvas" style="display:none"></canvas>

</body>
</html>""").format(
	marker_image=MARKER_IMAGE)).encode("utf-8")

	def render(self, request):
		request.setHeader("content-type", "text/html; charset=utf-8")
		return self.content


class WuerfelProtocol(Protocol):
	line_pattern = re.compile(b"lines ([\d.]* [\d.]* [\d.]* [\d.]*)+$")

	def __init__(self, factory):
		self.factory = factory

	def connectionMade(self):
		self.factory.activeProtocols.add(self)
		# webkit doesn't want to be written to just yet; delay a bit
		reactor.callLater(0.01, self.send_markers)

	def connectionLost(self, reason):
		self.factory.activeProtocols.remove(self)

	def dataReceived(self, data):
		if DEBUG:
			print("Incoming message: {}".format(data))

		if data==b"send_markers":
			self.send_markers()

		elif data in (b"disable", b"enable", b"new_image"):
			self.factory.broadcast(data)
			return

		elif data.startswith(b"remove ") or data.startswith(b"move "):
			self.factory.manage_markers(data)
			self.factory.broadcast(data)
			return

		elif data.startswith(b"lines "):
			if self.line_pattern.match(data):
				self.factory.lines.append(data[6:])
				self.factory.broadcast_lines()
			return
		
		elif data==b"popline":
			if self.factory.lines:
				self.factory.lines.pop()
				self.factory.broadcast_lines()

		elif data==b"clearlines":
			self.factory.lines = []
			self.factory.broadcast_lines()

		sides = {
			b'd6': 6,
			b'd20': 20,
			b'd100': 100,}.get(data)
		if sides:
			payload = "rolled "+(" ".join(str(random.randint(1, sides))
				for i in range(random.randint(3, 8))))
			self.factory.broadcast(payload.encode("ascii"))

	def send_markers(self):
		"""arranges for the current markers and lines to be sent to the protocol.
		"""
		msg = b"move "+b" ".join(
			marker+b" "+pos for marker, pos in self.factory.markers.items())
		self.transport.write(msg)
		msg = b"lines "+b" ".join(self.factory.lines)
		self.transport.write(msg)


class WuerflerFactory(Factory):
	"""the websockets switchboard.

	This manages a list of connected clients and can broadcast to
	all of them.

	It also keeps 

	* a list of markers it keeps current by snooping the traffic of 
	  the connected clients.
	* a list of lines drawn; each line is a string with blank-separated
	  image-relatve coordiates, "x0 y0 x1 y1".
	"""

	def __init__(self):
		self.activeProtocols = set()
		self.markers = {}
		self.lines = []
		Factory.__init__(self)

	def manage_markers(self, message):
		"""inspects a message that manipulates markers and updates our
		local representation accordingly.

		This is called by the individual protocols when they receive a
		marker-manipulating message.
		"""
		try:
			items = message.split()

			if items[0]==b"remove":
				if items[1]==b"ALL":
					self.markers = {}
					self.lines = []
				else:
					if items[1] in self.markers:
						del self.markers[items[1]]

			elif items[0]==b"move":
				# multi-point moving should never be broadcast; still, we're
				# convervative an cut away any further arguments.
				self.markers[items[1]] = b" ".join(items[2:4])

		except Exception as ex:
			sys.stderr.write("Marker manipulation error: {}\n".format(ex))

	def buildProtocol(self, addr):
		return WuerfelProtocol(self)

	def broadcast(self, payload):
		for p in self.activeProtocols:
			try:
				p.transport.write(payload)
			except:
				pass

	def broadcast_lines(self):
		self.broadcast(b"lines "+(b" ".join(self.lines)))


class Wuerfler(WebSocketsResource):

	def __init__(self):
		self.factory = WuerflerFactory()
		WebSocketsResource.__init__(self, 
			lookupProtocolForFactory(self.factory))


class ImagePage(resource.Resource):
	"""the page holding the image.  A POST will update it.

	To let it communicate to the clients that a new image is there,
	it needs to be constructed with the websocckets factory.
	"""
	image = base64.b64decode(b'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')

	def __init__(self, factory):
		self.factory = factory
		resource.Resource.__init__(self)

	def render_GET(self, request):
		if self.image.startswith(b"http"):
			return util.redirectTo(self.image, request)
		elif b"PNG" in self.image:
			request.setHeader("content-type", "image/png")
		elif b"JFIF" in self.image:
			request.setHeader("content-type", "image/jpeg")
		else:
			request.setHeader("content-type", "application/octet-stream")
		return self.image

	def render_POST(self, request):
		"""uploads a new image (or, really, whatever; caveat client).

		I'm lazy on the javascript side and don't do binary conversion
		of what I get from the photocanvas.  So, I support base64 encoded uploads
		in enc_image (and in imgfile from the form).
		"""
		request.setHeader("content-type", "text/plain")
		try:
			img_data = None

			if b"enc_image" in request.args:
				img_data = base64.b64decode(request.args[b"enc_image"][0
					].split(b",", 1)[-1])
			elif b"image_url" in request.args:
				img_data = request.args[b"image_url"][0]
			else:
				img_data = request.args[b"imgfile"][0]
				if not (b"PNG" in img_data or b"JFIF" in img_data):
					img_data = None

			if img_data is not None:
				self.image = img_data
				self.factory.broadcast(b"new_image")
				return b"OK"
		except:
			import traceback; traceback.print_exc()

		request.setResponseCode(500)
		return b"FAIL"


class StaticData(resource.Resource):
	"""some static data passed in during construction.
	"""
	def __init__(self, data, media_type):
		self.data, self.media_type = data, media_type
	
	def render(self, request):
		request.setHeader("content-type", self.media_type)
		return self.data


class Root(resource.Resource):
	def __init__(self):
		self.wuerfler = Wuerfler()
		self.root_page = Doc()
		self.fav_icon = StaticData(
			base64.b64decode(MARKER_IMAGE.encode("ascii")),
			"image/png")
		self.image_page = ImagePage(self.wuerfler.factory)
		resource.Resource.__init__(self)

	def getChild(self, name, request):
		if name==b"w":
			return self.wuerfler
		elif name==b"favicon.ico":
			return self.fav_icon
		elif name==b"img":
			return self.image_page
		else:
			return self.root_page


if __name__=="__main__":
	srv = reactor.listenTCP(3344, 
		server.Site(Root(), timeout=300), 
		interface="")
	log.startLogging(sys.stdout)
	reactor.run()


# finally, here's the nginx snippet that might work for https termination,
# preconfigured to work with letsencrypt and to force-redirect to https:
# host name is w.example.org

"""
server {
    listen 80;
    server_name w.example.org
    location /.well-known/acme-challenge {
        root /var/www/acme/jitsi;
    }
    location / {
      return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name w.example.org
    ssl_certificate /etc/nginx/ssl/jitsi.pem;
    ssl_certificate_key /etc/nginx/ssl/jitsi.key;
    location / {
        proxy_pass http://localhost:3344;
    }
    location /w {
        proxy_pass http://alnilam.ari.uni-heidelberg.de:3344/w;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
   }
}
"""
