Example #1
0
def octoprint_http_tunnel(request, pk):
    get_printer_or_404(pk, request)
    if request.user.tunnel_usage_over_cap():
        return HttpResponse(
            '<html><body><center><h1>Over Free Limit</h1><hr><h3 style="color: red;">Your month-to-date usage of OctoPrint Tunneling is over the free limit. Support this project and get unlimited tunneling by <a target="_blank" href="https://app.thespaghettidetective.com/ent_pub/pricing/">upgrading to The Spaghetti Detective Pro plan</a>, or wait for the reset of free limit at the start of the next month.</h3></center></body></html>',
            status=412)

    prefix = URL_PREFIX.format(pk=pk)
    method = request.method.lower()
    path = request.get_full_path()[len(prefix):]

    IGNORE_HEADERS = [
        'HTTP_HOST',
        'HTTP_ORIGIN',
        'HTTP_REFERER',
        'HTTP_COOKIE',
    ]

    # Recreate http headers, because django put headers in request.META as "HTTP_XXX_XXX". Is there a better way?
    req_headers = {
        k[5:].replace("_", " ").title().replace(" ", "-"): v
        for (k, v) in request.META.items() if k.startswith("HTTP")
        and not k.startswith('HTTP_X_') and k not in IGNORE_HEADERS
    }

    if 'CONTENT_TYPE' in request.META:
        req_headers['Content-Type'] = request.META['CONTENT_TYPE']

    ref = f'{pk}.{method}.{time.time()}.{path}'

    channels.send_msg_to_printer(
        pk, {
            "http.tunnel": {
                "ref": ref,
                "method": method,
                "headers": req_headers,
                "path": path,
                "data": request.body
            },
            'as_binary': True,
        })

    data = cache.octoprinttunnel_http_response_get(ref)
    if data is None:
        return HttpResponse(
            '<html><body><center><h1>Timed Out</h1><hr><h3 style="color: red;">Either your OctoPrint is offline, or The Spaghetti Detective plugin version is lower than 1.4.0.</h3></center></body></html>',
            status=504)

    content_type = data['response']['headers'].get('Content-Type') or None
    resp = HttpResponse(
        status=data["response"]["status"],
        content_type=content_type,
    )
    for k, v in data['response']['headers'].items():
        if k in ['Content-Length', 'Content-Encoding']:
            continue

        if k == 'Etag':
            v = fix_etag(v)

        resp[k] = v

    url_path = urllib.parse.urlparse(path).path
    if data['response'].get('compressed', False):
        content = zlib.decompress(data['response']['content'])
    else:
        content = data['response']['content']

    cache.octoprinttunnel_update_stats(request.user.id, len(content))

    if content_type and content_type.startswith('text/html'):
        content = rewrite_html(prefix, ensure_bytes(content))
    elif url_path.endswith('jquery.js') or url_path.endswith('jquery.min.js'):
        content = inject_ajax_prefilter(prefix, content)
    elif url_path.endswith('socket.js'):
        content = re.sub(_R_SOCKJS_TRANSPORTS, _rewrite_sockjs_transports,
                         ensure_bytes(content))
    elif url_path.endswith('packed_client.js'):
        content = re.sub(_R_SOCKJS_TRANSPORTS, _rewrite_sockjs_transports,
                         ensure_bytes(content))
    elif url_path.endswith('packed_libs.js'):
        content = re.sub(_R_WS_CONNECT_PATH, _rewrite_ws_connect_path,
                         ensure_bytes(content))
        content = inject_ajax_prefilter(prefix, content)
    elif url_path.endswith('sockjs.js'):
        content = re.sub(_R_WS_CONNECT_PATH, _rewrite_ws_connect_path,
                         ensure_bytes(content))
    elif url_path.endswith('sockjs.min.js'):
        content = re.sub(_R_WS_CONNECT_PATH, _rewrite_ws_connect_path,
                         ensure_bytes(content))

    resp.write(content)
    return resp
Example #2
0
def octoprint_http_tunnel(request, printer_id):
    get_printer_or_404(printer_id, request)
    if request.user.tunnel_usage_over_cap():
        return HttpResponse('Your month-to-date usage of OctoPrint Tunneling is over the free limit. Upgrade to The Spaghetti Detective Pro plan for unlimited tunneling, or wait for the reset of free limit at the start of the next month.')

    prefix = f'/octoprint/{printer_id}'  # FIXME
    method = request.method.lower()
    path = request.get_full_path()[len(prefix):]

    IGNORE_HEADERS = [
        'HTTP_HOST', 'HTTP_ORIGIN', 'HTTP_REFERER', 'HTTP_COOKIE',
    ]

    # Recreate http headers, because django put headers in request.META as "HTTP_XXX_XXX". Is there a better way?
    req_headers = {
        k[5:].replace("_", " ").title().replace(" ", "-"): v
        for (k, v) in request.META.items()
        if k.startswith("HTTP") and not k.startswith('HTTP_X_') and k not in IGNORE_HEADERS
    }

    if 'CONTENT_TYPE' in request.META:
        req_headers['Content-Type'] = request.META['CONTENT_TYPE']

    ref = f'{printer_id}.{method}.{time.time()}.{path}'

    channels.send_msg_to_printer(
        printer_id,
        {
            "http.tunnel": {
                "ref": ref,
                "method": method,
                "headers": req_headers,
                "path": path,
                "data": request.body
            },
            'as_binary': True,
        })

    data = cache.octoprinttunnel_http_response_get(ref)
    if data is None:
        return HttpResponse('Timed out. Either your OctoPrint is offline, or The Spaghetti Detective plugin version is lower than 1.4.0.')

    content_type = data['response']['headers'].get('Content-Type') or None
    resp = HttpResponse(
        status=data["response"]["status"],
        content_type=content_type,
    )
    for k, v in data['response']['headers'].items():
        if k in ['Content-Length', 'Content-Encoding']:
            continue
        resp[k] = v

    url_path = urllib.parse.urlparse(path).path
    content = data['response']['content']

    cache.octoprinttunnel_update_stats(
        request.user.id,
        (len(content)+ 240) * 1.2 * 2  # x1.2 because sent data volume is 20% of received. x2 because all data need to go in and out. 240 bytes header overhead
    )

    if content_type and content_type.startswith('text/html'):
        content = rewrite_html(prefix, ensure_bytes(content))
    elif url_path.endswith('jquery.js') or url_path.endswith('jquery.min.js'):
        content = inject_ajax_prefilter(prefix, content)
    elif url_path.endswith('socket.js'):
        content = re.sub(_R_SOCKJS_TRANSPORTS,
                         _rewrite_sockjs_transports, ensure_bytes(content))
    elif url_path.endswith('packed_client.js'):
        content = re.sub(_R_SOCKJS_TRANSPORTS,
                         _rewrite_sockjs_transports, ensure_bytes(content))
    elif url_path.endswith('packed_libs.js'):
        content = re.sub(_R_WS_CONNECT_PATH,
                         _rewrite_ws_connect_path, ensure_bytes(content))
        content = inject_ajax_prefilter(prefix, content)
    elif url_path.endswith('sockjs.js'):
        content = re.sub(_R_WS_CONNECT_PATH,
                         _rewrite_ws_connect_path, ensure_bytes(content))
    elif url_path.endswith('sockjs.min.js'):
        content = re.sub(_R_WS_CONNECT_PATH,
                         _rewrite_ws_connect_path, ensure_bytes(content))

    resp.write(content)

    return resp
def _octoprint_http_tunnel(request, octoprinttunnel):
    user = octoprinttunnel.printer.user
    if user.tunnel_usage_over_cap():
        return HttpResponse(OVER_FREE_LIMIT_HTML,
                            status=OVER_FREE_LIMIT_STATUS_CODE)

    # if plugin is disconnected, halt
    if channels.num_ws_connections(
            channels.octo_group_name(octoprinttunnel.printer.id)) < 1:
        return HttpResponse(NOT_CONNECTED_HTML,
                            status=NOT_CONNECTED_STATUS_CODE)

    version = (cache.printer_settings_get(octoprinttunnel.printer.pk)
               or {}).get('tsd_plugin_version', '')
    is_v1 = version and not is_plugin_version_supported(version)
    if is_v1:
        return HttpResponse(NOT_CONNECTED_HTML,
                            status=NOT_CONNECTED_STATUS_CODE)

    method = request.method.lower()
    path = request.get_full_path()

    IGNORE_HEADERS = [
        'HTTP_HOST',
        'HTTP_ORIGIN',
        'HTTP_REFERER',  # better not to tell
        'HTTP_AUTHORIZATION',  # handled explicitely
        'HTTP_COOKIE',  # handled explicitely
        'HTTP_ACCEPT_ENCODING',  # should be handled by TSD server
    ]

    req_headers = {
        k[5:].replace('_', ' ').title().replace(' ', '-'): v
        for (k, v) in request.META.items()
        if (k.startswith('HTTP') and k not in IGNORE_HEADERS
            and not k.startswith('HTTP_X_FORWARDED')  # meant for TSD server
            )
    }

    if 'CONTENT_TYPE' in request.META:
        req_headers['Content-Type'] = request.META['CONTENT_TYPE']

    if 'HTTP_COOKIE' in request.META:
        # let's not forward cookies of TSD server
        stripped_cookies = '; '.join([
            cookie.strip() for cookie in request.META['HTTP_COOKIE'].split(';')
            if DJANGO_COOKIE_RE.match(cookie.strip()) is None
        ])
        if stripped_cookies:
            req_headers['Cookie'] = stripped_cookies

    if hasattr(request, 'auth_header'):
        # let's not forward basic auth header of external tunnel
        stripped_auth_heaader = ', '.join([
            h for h in request.META['HTTP_AUTHORIZATION'].split(',')
            if h != request.auth_header
        ])
        if stripped_auth_heaader:
            req_headers['Authorization'] = stripped_auth_heaader

    ref = f'v2.{octoprinttunnel.id}.{method}.{time.time()}.{path}'

    channels.send_msg_to_printer(
        octoprinttunnel.printer.id, {
            'http.tunnelv2': {
                'ref': ref,
                'method': method,
                'headers': req_headers,
                'path': path,
                'data': request.body
            },
            'as_binary': True,
        })

    data = cache.octoprinttunnel_http_response_get(ref)
    if data is None:
        # request timed out
        return HttpResponse(NOT_CONNECTED_HTML, status=TIMED_OUT_STATUS_CODE)

    content_type = data['response']['headers'].get('Content-Type') or None
    status_code = data['response']['status']

    resp = HttpResponse(
        status=status_code,
        content_type=content_type,
    )

    to_ignore = (
        'content-length',  # set by django
        'content-encoding',  # if its set, it is probably incorrect/unapplicable
        'x-frame-options',  # response must load in TSD's iframe
    )
    for k, v in data['response']['headers'].items():
        if k.lower() in to_ignore:
            continue

        if k.lower() == 'etag':
            # pre 1.6.? octoprint has invalid etag format for some responses
            v = fix_etag(v)

        resp[k] = v

    # plugin connects over http to octoprint,
    # but TSD needs cookies working over https.
    # without this, cookies set in response might not be used
    # in some browsers (FF gives wwarning)
    for cookie in (data['response'].get('cookies', ()) or ()):
        if (request.is_secure() and 'secure' not in cookie.lower()):
            cookie += '; Secure'

        if 'Expires=' not in cookie and 'Max-Age=' not in cookie:
            cookie += '; Max-Age=7776000'  # 3 months

        resp['Set-Cookie'] = cookie

    if data['response'].get('compressed', False):
        content = zlib.decompress(data['response']['content'])
    else:
        content = data['response']['content']

    cache.octoprinttunnel_update_stats(user.id, len(content))

    resp.write(content)
    return resp