def __init__(self, http_version, status_code, reason=None, headers=(), content=None, trailers=None, timestamp_start=None, timestamp_end=None): if isinstance(http_version, str): http_version = http_version.encode("ascii", "strict") if isinstance(reason, str): reason = reason.encode("ascii", "strict") if not isinstance(headers, nheaders.Headers): headers = nheaders.Headers(headers) if isinstance(content, str): raise ValueError("Content must be bytes, not {}".format( type(content).__name__)) if trailers is not None and not isinstance(trailers, nheaders.Headers): trailers = nheaders.Headers(trailers) self.http_version = http_version self.status_code = status_code self.reason = reason self.headers = headers self.content = content self.trailers = trailers self.timestamp_start = timestamp_start self.timestamp_end = timestamp_end
def make(cls, method: str, url: str, content: Union[bytes, str] = "", headers: Union[Dict[str, AnyStr], Iterable[Tuple[bytes, bytes]]] = ()): """ Simplified API for creating request objects. """ req = cls("absolute", method, "", "", "", "", "HTTP/1.1", (), b"") req.url = url # Headers can be list or dict, we differentiate here. if isinstance(headers, dict): req.headers = nheaders.Headers(**headers) elif isinstance(headers, Iterable): req.headers = nheaders.Headers(headers) else: raise TypeError( "Expected headers to be an iterable or dict, but is {}.". format(type(headers).__name__)) # Assign this manually to update the content-length header. if isinstance(content, bytes): req.content = content elif isinstance(content, str): req.text = content else: raise TypeError( "Expected content to be str or bytes, but is {}.".format( type(content).__name__)) return req
def make(cls, status_code: int = 200, content: Union[bytes, str] = b"", headers: Union[Dict[str, AnyStr], Iterable[Tuple[bytes, bytes]]] = ()): """ Simplified API for creating response objects. """ resp = cls(b"HTTP/1.1", status_code, status_codes.RESPONSES.get(status_code, "").encode(), (), None) # Headers can be list or dict, we differentiate here. if isinstance(headers, dict): resp.headers = nheaders.Headers(**headers) elif isinstance(headers, Iterable): resp.headers = nheaders.Headers(headers) else: raise TypeError( "Expected headers to be an iterable or dict, but is {}.". format(type(headers).__name__)) # Assign this manually to update the content-length header. if isinstance(content, bytes): resp.content = content elif isinstance(content, str): resp.text = content else: raise TypeError( "Expected content to be str or bytes, but is {}.".format( type(content).__name__)) return resp
def _read_headers(rfile): """ Read a set of headers. Stop once a blank line is reached. Returns: A headers object Raises: exceptions.HttpSyntaxException """ ret = [] while True: line = rfile.readline() if not line or line == b"\r\n" or line == b"\n": break if line[0] in b" \t": if not ret: raise exceptions.HttpSyntaxException("Invalid headers") # continued header ret[-1] = (ret[-1][0], ret[-1][1] + b'\r\n ' + line.strip()) else: try: name, value = line.split(b":", 1) value = value.strip() if not name: raise ValueError() ret.append((name, value)) except ValueError: raise exceptions.HttpSyntaxException( "Invalid header line: %s" % repr(line)) return headers.Headers(ret)
def _read_headers(lines: Iterable[bytes]) -> headers.Headers: """ Read a set of headers. Stop once a blank line is reached. Returns: A headers object Raises: exceptions.HttpSyntaxException """ ret: List[Tuple[bytes, bytes]] = [] for line in lines: if line[0] in b" \t": if not ret: raise ValueError("Invalid headers") # continued header ret[-1] = (ret[-1][0], ret[-1][1] + b'\r\n ' + line.strip()) else: try: name, value = line.split(b":", 1) value = value.strip() if not name: raise ValueError() ret.append((name, value)) except ValueError: raise ValueError(f"Invalid header line: {line!r}") return headers.Headers(ret)
def collect_response_headers(response, omit=None): response_headers = nheaders.Headers() for name in response.headers: if omit and name in omit: continue response_headers[name] = response.headers[name] return response_headers
def health_check_response(flow): # if log_debug: # bubble_log.debug('health_check_response: special bubble health check request, responding with OK') response_headers = nheaders.Headers() response_headers[HEADER_HEALTH_CHECK] = 'OK' response_headers[HEADER_CONTENT_LENGTH] = '3' if flow.response is None: flow.response = http.HTTPResponse(http_version='HTTP/1.1', status_code=200, reason='OK', headers=response_headers, content=b'OK\n') else: flow.response.headers = nheaders.Headers() flow.response.headers = response_headers flow.response.status_code = 200 flow.response.reason = 'OK' flow.response.stream = lambda chunks: [b'OK\n']
def tarpit_response(flow, host): # if log_debug: # bubble_log.debug('health_check_response: special bubble health check request, responding with OK') response_headers = nheaders.Headers() if host is None: host = PUBLIC_IP response_headers[HEADER_LOCATION] = 'http://' + host + ':' + str( TARPIT_PORT) + '/admin/index.php' if flow.response is None: flow.response = http.HTTPResponse(http_version='HTTP/1.1', status_code=301, reason='Moved Permanently', headers=response_headers, content=b'') else: flow.response.headers = nheaders.Headers() flow.response.headers = response_headers flow.response.status_code = 301 flow.response.reason = 'Moved Permanently'
def __init__(self, first_line_format, method, scheme, host, port, path, http_version, headers=(), content=None, trailers=None, timestamp_start=None, timestamp_end=None): if isinstance(method, str): method = method.encode("ascii", "strict") if isinstance(scheme, str): scheme = scheme.encode("ascii", "strict") if isinstance(host, str): host = host.encode("idna", "strict") if isinstance(path, str): path = path.encode("ascii", "strict") if isinstance(http_version, str): http_version = http_version.encode("ascii", "strict") if not isinstance(headers, nheaders.Headers): headers = nheaders.Headers(headers) if isinstance(content, str): raise ValueError("Content must be bytes, not {}".format( type(content).__name__)) if trailers is not None and not isinstance(trailers, nheaders.Headers): trailers = nheaders.Headers(trailers) self.first_line_format = first_line_format self.method = method self.scheme = scheme self.host = host self.port = port self.path = path self.http_version = http_version self.headers = headers self.content = content self.trailers = trailers self.timestamp_start = timestamp_start self.timestamp_end = timestamp_end
def bubble_handle_request(self, flow): client_addr = flow.client_conn.address[0] server_addr = flow.server_conn.address[0] flow = update_host_and_port(flow) if flow.client_conn.tls_established: sni = flow.client_conn.connection.get_servername() is_http = False else: sni = None is_http = True # Determine if this request should be filtered host_header = flow.request.host_header host = flow.request.host path = flow.request.path if sni or host_header: log_url = flow.request.scheme + '://' + host + path # If https, we have already checked that the client/server are legal in bubble_conn_check.py # If http, we validate client/server here if is_http: fqdns = [host] if is_bubble_health_check(path): # Health check health_check_response(flow) return None elif is_bubble_request(server_addr, fqdns): if log_debug: bubble_log.debug( 'bubble_handle_request: redirecting to https for LOCAL bubble=' + server_addr + ' (bubble_host (' + bubble_host + ') in fqdns or bubble_host_alias (' + bubble_host_alias + ') in fqdns) for client=' + client_addr + ', fqdns=' + repr(fqdns) + ', path=' + path) add_flow_ctx(flow, CTX_BUBBLE_ABORT, 301) add_flow_ctx(flow, CTX_BUBBLE_LOCATION, 'https://' + host + path) return None elif is_sage_request(server_addr, fqdns): if log_debug: bubble_log.debug( 'bubble_handle_request: redirecting to https for SAGE server=' + server_addr + ' for client=' + client_addr) add_flow_ctx(flow, CTX_BUBBLE_ABORT, 301) add_flow_ctx(flow, CTX_BUBBLE_LOCATION, 'https://' + host + path) return None elif is_not_from_vpn(client_addr): if log_warning: bubble_log.warning( 'bubble_handle_request: sending to tarpit: non-VPN client=' + client_addr + ', url=' + log_url + ' host=' + host) bubble_activity_log(client_addr, server_addr, 'http_tarpit_non_vpn', fqdns) tarpit_response(flow, host) return None if is_bubble_special_path(path): add_flow_ctx(flow, CTX_BUBBLE_SPECIAL, True) else: matcher_response = self.get_matchers(flow, sni or host_header) if matcher_response: has_decision = 'decision' in matcher_response and matcher_response[ 'decision'] is not None if has_decision and matcher_response[ 'decision'] == 'pass_thru': if log_info: bubble_log.info( 'bubble_handle_request: REQ-DECISION PASSTHRU ' + log_url + ' passthru response returned, passing thru...') add_flow_ctx(flow, CTX_BUBBLE_PASSTHRU, True) bubble_activity_log(client_addr, server_addr, 'http_passthru', log_url) return host elif has_decision and matcher_response[ 'decision'].startswith('abort_'): decision = str(matcher_response['decision']) if log_debug: bubble_log.debug( 'bubble_handle_request: found abort code: ' + decision + ', aborting') if decision == 'abort_ok': abort_code = 200 elif decision == 'abort_not_found': abort_code = 404 else: if log_debug: bubble_log.debug( 'bubble_handle_request: unknown abort code: ' + decision + ', aborting with 404 Not Found') abort_code = 404 flow.request.headers = nheaders.Headers([]) flow.request.content = b'' add_flow_ctx(flow, CTX_BUBBLE_ABORT, abort_code) bubble_activity_log(client_addr, server_addr, 'http_abort' + str(abort_code), log_url) if log_info: bubble_log.info( 'bubble_handle_request: REQ-DECISION: BLOCK ' + log_url + ' (' + decision + ')') return None elif has_decision and matcher_response[ 'decision'] == 'no_match': if log_info: decision = str(matcher_response['decision']) bubble_log.info( 'bubble_handle_request: REQ-DECISION: ALLOW ' + log_url + ' (' + decision + ')') bubble_activity_log(client_addr, server_addr, 'http_no_match', log_url) return host elif ('matchers' in matcher_response and 'request_id' in matcher_response and len(matcher_response['matchers']) > 0): req_id = matcher_response['request_id'] if log_info: bubble_log.info( 'bubble_handle_request: REQ-DECISION: FILTER ' + log_url + ' found request_id: ' + req_id + ' with matchers: ' + repr(matcher_response['matchers'])) add_flow_ctx(flow, CTX_BUBBLE_MATCHERS, matcher_response['matchers']) add_flow_ctx(flow, CTX_BUBBLE_REQUEST_ID, req_id) bubble_activity_log(client_addr, server_addr, 'http_match', log_url) else: if log_info: bubble_log.info( 'bubble_handle_request: REQ-DECISION: ALLOW ' + log_url + ' no rules returned') bubble_activity_log(client_addr, server_addr, 'http_no_rules', log_url) else: if log_info: bubble_log.info( 'bubble_handle_request: REQ-DECISION: ALLOW ' + log_url + 'no matcher_response') # bubble_activity_log(client_addr, server_addr, 'http_no_matcher_response', log_url) elif is_http and is_not_from_vpn(client_addr): if log_warning: bubble_log.warning( 'bubble_handle_request: sending to tarpit: non-VPN client=' + client_addr) bubble_activity_log(client_addr, server_addr, 'http_tarpit_non_vpn', [server_addr]) tarpit_response(flow, host) return None else: if log_warning: bubble_log.warning( 'bubble_handle_request: no sni/host found, not applying rules to path: ' + path) bubble_activity_log(client_addr, server_addr, 'http_no_sni_or_host', [server_addr]) return host