Ejemplo n.º 1
0
    def connect(self):
        self.user, self.printer = None, None
        try:
            # Exception for un-authenticated or un-authorized access
            self.user, self.printer = self.get_user_and_printer()
            if self.printer is None:
                self.close()
                return

            self.accept()

            self.path = self.scope['path']

            self.ref = str(time.time())

            async_to_sync(self.channel_layer.group_add)(
                channels.octoprinttunnel_group_name(self.printer.id),
                self.channel_name,
            )
            channels.send_msg_to_printer(
                self.printer.id, {
                    'ws.tunnel': {
                        'ref': self.ref,
                        'data': None,
                        'path': self.path,
                        'type': 'connect',
                    },
                    'as_binary': True,
                })
        except Exception:
            LOGGER.exception("Websocket failed to connect")
            self.close()
Ejemplo n.º 2
0
    def connect(self):
        try:
            # Exception for un-authenticated or un-authorized access
            self.printer = Printer.objects.select_related('user').get(
                user=self.current_user(),
                id=self.scope['url_route']['kwargs']['printer_id'])
            self.accept()

            self.path = self.scope['path'][len(f'/ws/octoprint/{self.printer.id}'):]  # FIXME
            self.ref = self.scope['path']

            async_to_sync(self.channel_layer.group_add)(
                channels.octoprinttunnel_group_name(self.printer.id),
                self.channel_name,
            )
            channels.send_msg_to_printer(
                self.printer.id,
                {
                    'ws.tunnel': {
                        'ref': self.ref,
                        'data': None,
                        'path': self.path,
                        'type': 'connect',
                    },
                    'as_binary': True,
                })
        except:
            LOGGER.exception("Websocket failed to connect")
            self.close()
Ejemplo n.º 3
0
    def receive(self, text_data=None, bytes_data=None, **kwargs):
        try:
            Presence.objects.touch(self.channel_name)
            channels.send_msg_to_printer(
                self.printer.id,
                {
                    'ws.tunnel': {
                        'ref': self.ref,
                        'data': text_data or bytes_data,
                        'path': self.path,
                        'type': 'tunnel_message',
                    },
                    'as_binary': True
                })

            redis.octoprinttunnel_update_sent_stats(
                now(),
                self.current_user().id,
                self.printer.id,
                'ws',
                len(text_data or bytes_data or '')
            )
        except:  # sentry doesn't automatically capture consumer errors
            import traceback; traceback.print_exc()
            self.close()
            sentryClient.captureException()
Ejemplo n.º 4
0
    def receive_json(self, data, **kwargs):
        if time.time() - self.last_touch > TOUCH_MIN_SECS:
            self.last_touch = time.time()
            Presence.objects.touch(self.channel_name)

        if 'passthru' in data:
            channels.send_msg_to_printer(self.printer.id, data)
Ejemplo n.º 5
0
 def send_should_watch_status(self, refresh=True):
     if refresh:
         self.refresh_from_db()
     channels.send_msg_to_printer(
         self.id, {'remote_status': {
             'should_watch': self.should_watch()
         }})
Ejemplo n.º 6
0
    def receive(self, text_data=None, bytes_data=None):
        # we are going to disable datachannel for shared printer connections
        # by tampering janus offer/answer messages

        msg = json.loads(text_data)
        if 'jsep' in msg and msg['jsep']['type'] == 'answer':
            sdp = msg['jsep']['sdp']

            if 'BUNDLE video\r\n' not in sdp:
                # frontend should request only video,
                # if thats's not the case, then something went wrong
                # with patching the offer (bellow)
                capture_message('bad sdp bundle', extras={'sdp': sdp})
                return

        channels.send_msg_to_printer(self.printer.id, {'janus': text_data})
Ejemplo n.º 7
0
    def disconnect(self, close_code):
        LOGGER.warn(f'OctoprintTunnelWebConsumer: Closed websocket with code: {close_code}')
        async_to_sync(self.channel_layer.group_discard)(
            channels.octoprinttunnel_group_name(self.printer.id),
            self.channel_name,
        )

        channels.send_msg_to_printer(
            self.printer.id,
            {
                'ws.tunnel': {
                    'ref': self.ref,
                    'data': None,
                    'path': self.path,
                    'type': 'tunnel_close',
                },
                'as_binary': True,
            })
Ejemplo n.º 8
0
    def receive(self, text_data=None, bytes_data=None, **kwargs):
        if self.printer.user.tunnel_usage_over_cap():
            return

        try:
            channels.send_msg_to_printer(
                self.printer.id,
                {
                    'ws.tunnel': {
                        'ref': self.ref,
                        'data': text_data or bytes_data,
                        'path': self.path,
                        'type': 'tunnel_message',
                    },
                    'as_binary': True
                })
        except:  # sentry doesn't automatically capture consumer errors
            import traceback; traceback.print_exc()
            sentryClient.captureException()
Ejemplo n.º 9
0
 def receive_json(self, data, **kwargs):
     Presence.objects.touch(self.channel_name)
     if 'passthru' in data:
         channels.send_msg_to_printer(self.printer.id, data)
Ejemplo n.º 10
0
 def send_octoprint_command(self, command, args={}):
     channels.send_msg_to_printer(
         self.id, {'commands': [{
             'cmd': command,
             'args': args
         }]})
Ejemplo n.º 11
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
Ejemplo n.º 12
0
 def receive(self, text_data=None, bytes_data=None):
     channels.touch_channel(channels.janus_web_group_name(self.printer.id),
                            self.channel_name)
     channels.send_msg_to_printer(self.printer.id, {'janus': text_data})
Ejemplo n.º 13
0
    def receive_json(self, data, **kwargs):
        channels.touch_channel(channels.web_group_name(self.printer.id),
                               self.channel_name)

        if 'passthru' in data:
            channels.send_msg_to_printer(self.printer.id, data)
Ejemplo n.º 14
0
 def receive(self, text_data=None, bytes_data=None):
     channels.send_msg_to_printer(self.printer.id, {'janus': text_data})
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
Ejemplo n.º 16
0
 def send_octoprint_command(self, command, args={}, initiator=None):
     channels.send_msg_to_printer(self.id, {'commands': [{'cmd': command, 'args': args, 'initiator': initiator or 'unknown'}]})
Ejemplo n.º 17
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