def process_connection(self, quick_reply_with=None): """ Continually serve an http connection. :param quick_reply_with: If given will be sent to the client immediately as a response without parsing his request. :return: This method doesn't return until the connection is closed. """ if quick_reply_with: assert isinstance(quick_reply_with, ws.http.structs.HTTPResponse) self.push_exchange() self.respond(quick_reply_with) while True: error_log.debug( 'HTTP Connection is open. Pushing new http exchange ' 'context and parsing request...') self.push_exchange() with record_rusage(self.exchange): request, body_start = ws.http.parser.parse(self.sock) self.exchange['request'] = request self.exchange['body_start'] = body_start response = self.handle_request(request) error_log.debug3('Request handler succeeded.') response = self.respond(response) client_persists = request and request_is_persistent(request) server_persists = response and response_is_persistent(response) if not (client_persists and server_persists): break
def resolve_route(self, route): if self.reindex_is_scheduled: self.reindex_files() route = ws.http.utils.decode_uri_component(route) if not route.startswith(self.route_prefix): error_log.debug('Route %s does not start with prefix %s', route, self.route_prefix) return None elif route == self.route_prefix: return config.get('resources', 'index_page', fallback=None) rel_path = route[len(self.route_prefix):] file_path = os.path.abspath(os.path.join(self.document_root, rel_path)) try: stat = os.stat(file_path) file_key = (stat.st_ino, stat.st_dev) except FileNotFoundError: return None if file_key not in self.file_keys: error_log.debug('File key %s of route %s is not inside indexed ' 'file keys.') return None return file_path
def accept(self): """ :rtype: ws.sockets.ClientSocket """ s, a = super().accept() error_log.debug('Accepted connection from %s on socket %s', a, s.fileno()) return Socket(fileno=s.detach()), a
def resolve_route_depreciated(route, route_prefix, dir_prefix): if not route.startswith(route_prefix): error_log.debug('Route %s does not start with prefix %s', route, route_prefix) return None rel_path = route[len(route_prefix):] file_path = os.path.join(dir_prefix, rel_path) resolved = os.path.abspath(os.path.realpath(file_path)) # if a symlink get's created after this if statement an exploit is possible if not resolved.startswith(dir_prefix): error_log.debug('Resolved route %s is not inside dir %s', resolved, dir_prefix) return None return resolved
def worker_status(request_stats): error_log.debug('Serving worker stats.') body = [] for stat_name, stat_entry in request_stats.items(): total, count = stat_entry['total'], stat_entry['count'] if count: line = '{}:{}'.format(stat_name, total / count).encode('ascii') body.append(line) body = b'\n'.join(body) ib = io.BytesIO() ib.write(body) ib.seek(0) return ws.http.utils.build_response( status_code=200, headers={'Content-Length': len(body), 'Content-Type': 'text/html'}, body=ib )
def respond(self, response=None, *, closing=False): """ :param response: Response object to send to client :param closing: Boolean switch whether the http connection should be closed after this response. """ assert isinstance(closing, bool) self.exchange['response'] = response if response: assert isinstance(response, ws.http.structs.HTTPResponse) request = self.exchange['request'] if closing: error_log.debug('Closing connection. (explicitly)') conn = 'close' elif request: try: conn = str(request.headers['Connection'], encoding='ascii') except (KeyError, UnicodeDecodeError): error_log.debug('Getting Connection header from request ' 'failed. Closing connection.') conn = 'close' else: error_log.debug('No request parsed. Closing connection.') conn = 'close' response.headers['Connection'] = conn self.exchange['written'] = True for chunk in response.iter_chunks(): self.sock.sendall(chunk) return response
def fork_workers(self, count=1): error_log.info('Forking %s workers', self.process_count_limit) assert isinstance(count, int) for _ in range(count): fd_transport = ws.sockets.FDTransport() pid = os.fork() if pid: self.execution_context = self.ExecutionContext.main error_log.debug('Forked worker with pid=%s', pid) ws.sockets.randomize_ssl_after_fork() fd_transport.mode = 'sender' wp = WorkerProcess(pid=pid, fd_transport=fd_transport) self.workers[wp.pid] = wp else: self.execution_context = self.ExecutionContext.worker ws.signals.reset_handlers(excluding={ws.signals.SIGTERM}) ws.signals.signal(ws.signals.SIGCHLD, ws.signals.SIG_IGN) # noinspection PyBroadException try: ws.logs.setup_worker_handlers() fd_transport.mode = 'receiver' for other_worker in self.workers.values(): other_worker.close_ipc() self.sock.close() os.close(0) os.close(1) os.close(2) with profile(WORKER_PROFILING_ON): worker = ws.worker.Worker(fd_transport=fd_transport) exit_code = worker.work() except BaseException: error_log.exception('Worker failed.') exit_code = 1 # noinspection PyProtectedMember os._exit(exit_code)
def reap_children(self, signum, stack_frame): # TODO use a lock error_log.debug3('reap_children() called.') if self.reaping: return self.reaping = True try: while True: pid, exit_indicator = os.waitpid(-1, os.WNOHANG) if pid: error_log.debug('Reaped pid %s', pid) assert pid not in self.reaped_pids self.reaped_pids.add(pid) else: break except OSError as err: error_log.debug( 'During reaping of zombie child: wait() sys call ' 'failed with ERRNO=%s and MSG=%s. This is mostly ' 'not a problem.', err.errno, err.strerror) finally: self.reaping = False
def record_handled_connection(self, ip_address, status_codes): assert isinstance(ip_address, (str, bytes)) assert all(isinstance(sc, int) for sc in status_codes) err_count = sum(sc in CONSIDERED_CLIENT_ERRORS for sc in status_codes) if not err_count: return error_log.debug('Error count of address %s increased by %s.', ip_address, err_count) if len(self.bad_connection_records) == self.cleanup_threshold: error_log.warning( "Reached max recorded addresses for rate limiting " "purposes. Dumping/freeing recorded bad connections.") self.bad_connection_records = {} if ip_address not in self.bad_connection_records: self.bad_connection_records[ip_address] = dict( ip_address=ip_address, first_recorded_on=time.time(), err_count=err_count) else: self.bad_connection_records[ip_address]['err_count'] += err_count
def execute_script(request, socket, body_start=b''): assert isinstance(socket, (ws.sockets.SSLSocket, ws.sockets.Socket)) assert isinstance(body_start, (bytes, bytearray)) assert can_handle_request(request) uri = request.request_line.request_target cgi_script = find_cgi_script(uri) error_log.info('Executing CGI script %s', cgi_script.name) script_env = prepare_cgi_script_env(request, socket) error_log.debug('CGIScript environment will be: %s', script_env) has_body = 'Content-Length' in request.headers try: proc = subprocess.Popen( args=os.path.abspath(cgi_script.script_path), env=script_env, stdin=subprocess.PIPE if has_body else socket.fileno(), stdout=socket.fileno()) except (OSError, ValueError): error_log.exception( 'Failed to open subprocess for cgi script {}'.format( cgi_script.name)) return ws.http.utils.build_response(500) # noinspection PyBroadException try: if not has_body: return error_log.debug('Request to CGI has body. Writing to stdin...') start = time.time() length = int(request.headers['Content-Length']) read_bytes = 0 chunk_size = 4096 timeout_reached = False while read_bytes < length and not timeout_reached: if body_start: chunk = body_start body_start = b'' else: chunk = socket.recv(chunk_size) read_bytes += len(chunk) if not chunk: break while chunk: avail = select.select([], [proc.stdin], [], cgi_script.timeout) _, wlist, _ = avail if wlist: written = wlist[0].write(chunk) chunk = chunk[written:] else: timeout_reached = True break if time.time() - start > cgi_script.timeout: timeout_reached = True if timeout_reached: error_log.warning( 'CGI script %s took too long to read body. ' 'Leaving process alive but no more data will ' 'be piped to it.', cgi_script) except ws.sockets.TimeoutException: error_log.warning("Client sent data too slowly - socket timed out.") except Exception: error_log.exception('Failed to write body to CGI script.') finally: try: proc.stdin.close() except OSError as err: error_log.warning( 'Closing CGI stdin pipe failed. ERRNO=%s MSG=%s.', err.errno, err.strerror)