class ApricotParser(object): ''' Apricot Http Parser ''' def __init__(self, parseType="request"): # create abstract parser self.parser = AbstractParser() # determine httptools parser if parseType.lower() == "request": self.parsed = HttpRequestParser(self.parser) else: self.parsed = HttpResponseParser(self.parser) # set null url info self.url = None self.host = None self.schema = None self.port = None self.path = None self.params = {} async def feed_async(self, data=b''): self.feed(data) def feed(self, data=b''): ''' feed data to parser ''' self.raw_data = data self.parsed.feed_data(memoryview(data)) self.set_attributes() # set apricot attributes def set_attributes(self): ''' set ApricotParser attributes ''' # check parser type if isinstance(self.parsed, HttpRequestParser): self.method = self.parsed.get_method().decode() else: self.status = self.parsed.get_status_code() # set basic attr's try: self.headers = self.parser.headers self.body = self.parser.body self.http_ver = self.parsed.get_http_version() self.keep_alive = self.parsed.should_keep_alive() except Exception as e: print("error in parser: " + str(e)) if self.keep_alive == None: self.keep_alive = False # parse url and get info self.get_url_info() def get_url_info(self): ''' Parse url info ''' if 'Host' in self.headers: try: # make url self.url = b'http://' self.url += self.headers['Host'].encode() # add path if request if isinstance(self.parsed, HttpRequestParser): path = self.raw_data.split(b'\r\n')[0].split()[1] self.url += path self.path = path # parse url and get basic info URL = parse_url(self.url) self.schema = URL.schema.decode() self.host = URL.host.decode() self.port = URL.port # get url query parameters if any if URL.query is not None: for param in URL.query.split(b'&'): parts = param.split(b'=') key = parts[0].decode() value = b'='.join(parts[1:]).decode() self.params[key] = value # decode url from bytes to string self.url = self.url.decode() except: pass
class HttpProtocol: """The protocol class constructs the incoming request and response.""" def __init__(self, bufsize: int = 2048): self._bufsize = bufsize self._data_hooks = [] # type: List[Callable] self._url_hooks = [] # type: List[Callable] self._parser = None # type: PARSER_TYPE self._is_done = False self._body = bytes() self.msg = None def add_url_hook(self, func: Callable): """Add hook to be executed during on_url()""" self._url_hooks.append(func) def add_data_hook(self, func: Callable): """Add hook to be executed during recv()""" self._data_hooks.append(func) def set_bufsize(self, bufsize: int): """Set the bytes size to read from the socket""" self._bufsize = bufsize def set_request_parser(self): """Set a new request parser with self as callback""" self._parser = HttpRequestParser(self) def set_response_parser(self): """Set a new response parser with self as callback""" self._parser = HttpResponseParser(self) def set_request(self, request: dict): """Set new request""" request['version'] = 'HTTP/1.1' request['method'] = 'GET' request['headers'] = {} request['body'] = {} request['url'] = { 'scheme': '', 'netloc': '', 'path': '', 'params': '', 'query': {}, 'fragment': '' } self.msg = request def set_response(self, response: dict): """Set new response""" response['version'] = 'HTTP/1.1' response['status'] = 200 response['reason'] = 'OK' response['headers'] = {} response['body'] = {} self.msg = response def feed(self, data: bytes): """feed data to the underlying parser""" for hook in self._data_hooks: hook(data) self._parser.feed_data(data) def recv(self, sock: socket.socket): """Receive all incoming http data on a socket and execute hooks""" while not self._is_done: data = sock.recv(self._bufsize) self.feed(data) # HTTPTOOLS CALLBACK METHODS def on_message_begin(self): """Triggered on first bytes""" def on_status(self, status: bytes): """Status integer callback""" if self.msg is not None: self.msg['status'] = self._parser.get_status_code() self.msg['reason'] = status.decode() def on_url(self, url: bytes): """Request message attribute will have url and method at this point""" url = parse_url(url) if self.msg is not None: self.msg['url'] = url self.msg['method'] = self._parser.get_method().decode() for hook in self._url_hooks: hook(url) def on_header(self, name: bytes, value: bytes): """Alter request header as they come in if necessary""" if self.msg is not None: self.msg['headers'][name.decode().lower()] = value.decode() def on_headers_complete(self): """End of headers trigger""" def on_body(self, body: bytes): """Optionally access body stream""" self._body += body def on_message_complete(self): """Trigger when message finishes""" if self.msg is not None: self.msg['version'] = 'HTTP/' + self._parser.get_http_version() if self.msg['headers'].get('content-encoding', '') == 'gzip': self._body = gzip.decompress(self._body) self.msg['body'] = load_json(self._body) self._is_done = True def on_chunk_header(self): """Chunked message header""" def on_chunk_complete(self): """Chunked response end"""