def send_error(self, status_code, message, headers): response = http.status_codes.RESPONSES.get(status_code, "Unknown") body = """ <html> <head> <title>%d %s</title> </head> <body>%s</body> </html> """ % (status_code, response, message) if not headers: headers = odict.ODictCaseless() assert isinstance(headers, odict.ODictCaseless) headers["Server"] = [self.c.config.server_version] headers["Connection"] = ["close"] headers["Content-Length"] = [len(body)] headers["Content-Type"] = ["text/html"] resp = HTTPResponse( (1, 1), # if HTTP/2 is used, this value is ignored anyway status_code, response, headers, body, ) # if no protocol is assigned yet - just assume HTTP/1 # TODO: maybe check ALPN and use HTTP/2 if required? protocol = self.c.client_conn.protocol or http1.HTTP1Protocol(self.c.client_conn) self.c.client_conn.send(protocol.assemble(resp))
def send_connect_request(conn, host, port, update_state=True): upstream_request = HTTPRequest( "authority", "CONNECT", None, host, port, None, (1, 1), odict.ODictCaseless(), "" ) # we currently only support HTTP/1 CONNECT requests protocol = http1.HTTP1Protocol(conn) conn.send(protocol.assemble(upstream_request)) resp = HTTPResponse.from_protocol(protocol, upstream_request.method) if resp.status_code != 200: raise proxy.ProxyError(resp.status_code, "Cannot establish SSL " + "connection with upstream proxy: \r\n" + repr(resp)) if update_state: conn.state.append(("http", { "state": "connect", "host": host, "port": port} )) return resp
def mock_protocol(data=''): rfile = cStringIO.StringIO(data) wfile = cStringIO.StringIO() return http1.HTTP1Protocol(rfile=rfile, wfile=wfile)
def run(self): r = self.flow.request form_out_backup = r.form_out try: self.flow.response = None # If we have a channel, run script hooks. if self.channel: request_reply = self.channel.ask("request", self.flow) if request_reply is None or request_reply == KILL: raise KillSignal() elif isinstance(request_reply, HTTPResponse): self.flow.response = request_reply if not self.flow.response: # In all modes, we directly connect to the server displayed if self.config.mode == "upstream": server_address = self.config.mode.get_upstream_server( self.flow.client_conn )[2:] server = ServerConnection(server_address) server.connect() if r.scheme == "https": send_connect_request(server, r.host, r.port) server.establish_ssl( self.config.clientcerts, sni=self.flow.server_conn.sni ) r.form_out = "relative" else: r.form_out = "absolute" else: server_address = (r.host, r.port) server = ServerConnection(server_address) server.connect() if r.scheme == "https": server.establish_ssl( self.config.clientcerts, sni=self.flow.server_conn.sni ) r.form_out = "relative" server.send(self.flow.server_conn.protocol.assemble(r)) self.flow.server_conn = server self.flow.server_conn.protocol = http1.HTTP1Protocol(self.flow.server_conn) self.flow.response = HTTPResponse.from_protocol( self.flow.server_conn.protocol, r.method, body_size_limit=self.config.body_size_limit, ) if self.channel: response_reply = self.channel.ask("response", self.flow) if response_reply is None or response_reply == KILL: raise KillSignal() except (proxy.ProxyError, http.HttpError, tcp.NetLibError) as v: self.flow.error = Error(repr(v)) if self.channel: self.channel.ask("error", self.flow) except KillSignal: # KillSignal should only be raised if there's a channel in the # first place. self.channel.tell("log", proxy.Log("Connection killed", "info")) finally: r.form_out = form_out_backup
def handle_flow(self): flow = HTTPFlow(self.c.client_conn, self.c.server_conn, self.live) try: try: if not flow.client_conn.protocol: # instantiate new protocol if connection does not have one yet # the first request might be a CONNECT - which is currently only supported with HTTP/1 flow.client_conn.protocol = http1.HTTP1Protocol(self.c.client_conn) req = HTTPRequest.from_protocol( flow.client_conn.protocol, body_size_limit=self.c.config.body_size_limit ) except tcp.NetLibError: # don't throw an error for disconnects that happen # before/between requests. return False self.c.log( "request", "debug", [repr(req)] ) ret = self.process_request(flow, req) if ret: # instantiate new protocol if connection does not have one yet # TODO: select correct protocol based on ALPN (?) flow.client_conn.protocol = http1.HTTP1Protocol(self.c.client_conn) # flow.client_conn.protocol = http2.HTTP2Protocol(self.c.client_conn, is_server=True) if ret is not None: return ret # Be careful NOT to assign the request to the flow before # process_request completes. This is because the call can raise an # exception. If the request object is already attached, this results # in an Error object that has an attached request that has not been # sent through to the Master. flow.request = req request_reply = self.c.channel.ask("request", flow) if request_reply is None or request_reply == KILL: raise KillSignal() # The inline script may have changed request.host self.process_server_address(flow) if isinstance(request_reply, HTTPResponse): flow.response = request_reply else: self.get_response_from_server(flow) # no further manipulation of self.c.server_conn beyond this point # we can safely set it as the final attribute value here. flow.server_conn = self.c.server_conn self.c.log( "response", "debug", [repr(flow.response)] ) response_reply = self.c.channel.ask("response", flow) if response_reply is None or response_reply == KILL: raise KillSignal() self.send_response_to_client(flow) if self.check_close_connection(flow): return False # We sent a CONNECT request to an upstream proxy. if flow.request.form_in == "authority" and flow.response.code == 200: # TODO: Possibly add headers (memory consumption/usefulness # tradeoff) Make sure to add state info before the actual # processing of the CONNECT request happens. During an SSL # upgrade, we may receive an SNI indication from the client, # which resets the upstream connection. If this is the case, we # must already re-issue the CONNECT request at this point. self.c.server_conn.state.append( ( "http", { "state": "connect", "host": flow.request.host, "port": flow.request.port } ) ) if not self.process_connect_request( (flow.request.host, flow.request.port)): return False # If the user has changed the target server on this connection, # restore the original target server flow.live.restore_server() return True # Next flow please. except ( http.HttpAuthenticationError, http.HttpError, proxy.ProxyError, tcp.NetLibError, ) as e: self.handle_error(e, flow) except KillSignal: self.c.log("Connection killed", "info") finally: flow.live = None # Connection is not live anymore. return False
def get_response_from_server(self, flow): self.c.establish_server_connection() for attempt in (0, 1): try: if not self.c.server_conn.protocol: # instantiate new protocol if connection does not have one yet # TODO: select correct protocol based on ALPN (?) self.c.server_conn.protocol = http1.HTTP1Protocol(self.c.server_conn) # self.c.server_conn.protocol = http2.HTTP2Protocol(self.c.server_conn) # self.c.server_conn.protocol.perform_connection_preface() self.c.server_conn.send(self.c.server_conn.protocol.assemble(flow.request)) # Only get the headers at first... flow.response = HTTPResponse.from_protocol( self.c.server_conn.protocol, flow.request.method, body_size_limit=self.c.config.body_size_limit, include_body=False, ) break except (tcp.NetLibError, http.HttpErrorConnClosed) as v: self.c.log( "error in server communication: %s" % repr(v), level="debug" ) if attempt == 0: # In any case, we try to reconnect at least once. This is # necessary because it might be possible that we already # initiated an upstream connection after clientconnect that # has already been expired, e.g consider the following event # log: # > clientconnect (transparent mode destination known) # > serverconnect # > read n% of large request # > server detects timeout, disconnects # > read (100-n)% of large request # > send large request upstream self.c.server_reconnect() else: raise # call the appropriate script hook - this is an opportunity for an # inline script to set flow.stream = True flow = self.c.channel.ask("responseheaders", flow) if flow is None or flow == KILL: raise KillSignal() else: # now get the rest of the request body, if body still needs to be # read but not streaming this response if flow.response.stream: flow.response.content = CONTENT_MISSING else: if isinstance(self.c.server_conn.protocol, http1.HTTP1Protocol): # streaming is only supported with HTTP/1 at the moment flow.response.content = self.c.server_conn.protocol.read_http_body( flow.response.headers, self.c.config.body_size_limit, flow.request.method, flow.response.code, False ) flow.response.timestamp_end = utils.timestamp()