Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
    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)
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
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)
Ejemplo n.º 11
0
def handle_parse_err(exc):
    error_log.warning('Parsing error with code=%s occurred', exc.code)
    return ws.http.utils.build_response(400), True
Ejemplo n.º 12
0
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