def __next__(self): if self.current_chunk: try: return next(self.current_chunk) except StopIteration: pass elif self.socket_broke: raise StopIteration() if self.connected_on + self.connection_timeout < time.time(): raise ClientSocketException(code='CS_CONNECTION_TIMED_OUT') try: chunk = self.sock.recv(self.__class__.buffer_size) except ws.sockets.TimeoutException as e: error_log.warning('Socket timed out while receiving request.') raise ClientSocketException(code='CS_PEER_SEND_IS_TOO_SLOW') from e error_log.debug3('Read chunk %s', chunk) if chunk == b'': error_log.info('Socket %d broke', self.sock.fileno()) self.socket_broke = True raise ClientSocketException(code='CS_PEER_NOT_SENDING') self.current_chunk = iter(chunk) return next(self.current_chunk)
def cgi_config(): scripts = {} scripts_dir = config['cgi']['scripts_dir'] for file_name in os.listdir(scripts_dir): script_conf_section = 'cgi_{}'.format(file_name) if script_conf_section not in config: error_log.warning( 'CGI script %s is not configured and it will ' 'not be run.', file_name) continue route = config[script_conf_section]['route'] msg = "Scripts {fn_1} and {fn_2} are in conflict for route {route}." if route in scripts: raise SysError(msg=msg.format(fn_1=file_name, fn_2=scripts[route], route=route), code='CGI_CONFIG_CONFLICT') script = CGIScript.from_config(file_name) scripts[script.route] = script return scripts
def handle_client_socket_err(exc): error_log.warning('Client socket error with code=%s occurred', exc.code) if exc.code in ('CS_PEER_SEND_IS_TOO_SLOW', 'CS_CONNECTION_TIMED_OUT'): return ws.http.utils.build_response(408), True elif exc.code == 'CS_PEER_NOT_SENDING': return None, True else: return ws.http.utils.build_response(400), True
def listen(self): assert all(isinstance(w, WorkerProcess) for w in self.workers.values()) assert len(self.workers) == self.process_count_limit error_log.info('Listening with backlog %s...', self.tcp_backlog_size) self.sock.listen(self.tcp_backlog_size) while True: # TODO add rate limiting on rate of clients sending connections # instead of on the HTTP syntax (which will be deferred to workers) try: sock, address = self.sock.accept() except OSError as err: error_log.warning('accept() raised ERRNO=%s with MSG=%s', err.errno, err.strerror) # TODO perhaps reopen failed listening sockets. assert err.errno not in (errno.EBADF, errno.EFAULT, errno.EINVAL, errno.ENOTSOCK, errno.EOPNOTSUPP) # don't break the listening loop just because one accept failed continue self.accepted_connections += 1 passed = self.distribute_connection(client_socket=sock, address=address) if not passed: # TODO fork and reply quickly with a 503 error_log.warning( 'Could not distribute connection %s / %s to ' 'workers. Dropping connection.', sock, address) sock.close(pass_silently=True) # duplicate the set so SIGCHLD handler doesn't cause problems to_remove = frozenset(self.reaped_pids) for pid in to_remove: old_worker = self.workers.pop(pid) old_worker.close_ipc() self.reaped_pids.remove(pid) # call outside of loop to avoid a race condition where a # worker_process get's forked with a pid in self.reaped_pids missing = self.process_count_limit - len(self.workers) if missing > 0: self.fork_workers(missing) for worker_process in self.workers.values(): worker_process.kill_if_hanged() if ws.signals.SIGUSR1 in self.received_signals: for pid in self.workers: ws.signals.kill(pid, ws.signals.SIGUSR1) self.received_signals.remove(ws.signals.SIGUSR1)
def terminate(self): if self.terminating: return try: self.sent_sigterm_on = time.time() os.kill(self.pid, ws.signals.SIGTERM) self.terminating = True except OSError as err: error_log.warning( 'Failed to sent SIGTERM to worker with pid %s. ' 'ERRNO=%s and MSG=%s', err.errno, err.strerror)
def kill_if_hanged(self, now=None): if not self.terminating: return False now = now or time.time() if now - self.sent_sigterm_on > self.sigterm_timeout: # don't fail if worker is already dead. try: ws.signals.kill(self.pid, ws.signals.SIGKILL) except OSError as err: error_log.warning( 'Killing worker with pid %s failed. ' 'ERRNO=%s and MSG=%s', err.errno, err.strerror) return True
def distribute_connection(self, client_socket, address): for i, worker in enumerate(self.workers_round_robin()): if not worker.can_work(): continue try: worker.send_connections([(client_socket, address)]) return True except OSError as err: if worker.pid in self.reaped_pids: continue error_log.warning( 'sending file descriptors to worker %s ' 'raised ERRNO=%s with MSG=%s', worker, err.errno, err.strerror) worker.terminate() return 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 __exit__(self, exc_type, exc_val, exc_tb): if not exc_val: return False if self.exchange['written']: error_log.warning( 'An exception occurred after worker had sent bytes over ' 'the socket(fileno=%s). Client will receive an invalid ' 'HTTP response.', self.sock.fileno() ) return False if exc_handler.can_handle(exc_val): response, suppress = exc_handler.handle(exc_val) if not response: # no need to send back a response return suppress else: error_log.exception('Could not handle exception. Client will ' 'receive a 500 Internal Server Error.') response, suppress = ws.http.utils.build_response(500), False response.headers['Connection'] = 'close' try: self.respond(response) except OSError as e: error_log.warning('During cleanup of worker tried to respond to ' 'client and close the connection but: ' 'caught OSError with ERRNO=%s and MSG=%s', e.errno, e.strerror) if e.errno == errno.ECONNRESET: error_log.warning('Client stopped listening prematurely.' ' (no Connection: close header was received)') return suppress else: raise return suppress
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)
def handle_parse_err(exc): error_log.warning('Parsing error with code=%s occurred', exc.code) return ws.http.utils.build_response(400), True
def peer_err_handler(exc): error_log.warning('PeerError occurred. msg={exc.msg} code={exc.code}' .format(exc=exc)) return ws.http.utils.build_response(400), True