def octoprinttunnel_message(self, msg, **kwargs): try: # msg == {'data': {'type': ..., 'data': ..., 'ref': ...}, ...} payload = msg['data'] if payload['ref'] != self.ref and payload['ref'] != 'ALL': return if payload['type'] == 'octoprint_close': self.close(self.OCTO_WS_ERROR_CODE) return if isinstance(payload['data'], bytes): self.send(bytes_data=payload['data']) else: self.send(text_data=payload['data']) cache.octoprinttunnel_update_stats( self.scope['user'].id, len(payload['data']) * 1.2 * 2 # x1.2 because sent data volume is 20% of received. x2 because all data need to go in and out ) except: # sentry doesn't automatically capture consumer errors import traceback traceback.print_exc() sentryClient.captureException()
def octoprinttunnel_message(self, msg, **kwargs): try: # msg == {'data': {'type': ..., 'data': ..., 'ref': ...}, ...} payload = msg['data'] if payload['ref'] != self.ref and payload['ref'] != 'ALL': return if payload['type'] == 'octoprint_close': self.close(self.OCTO_WS_ERROR_CODE) return if isinstance(payload['data'], bytes): self.send(bytes_data=payload['data']) else: self.send(text_data=payload['data']) cache.octoprinttunnel_update_stats(self.printer.user_id, len(payload['data'])) except: # sentry doesn't automatically capture consumer errors import traceback traceback.print_exc() sentryClient.captureException()
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, 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
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