Маленький HTTP сервер на php

Discussion in 'PHP' started by ckpunmkug, 7 Mar 2019.

  1. ckpunmkug

    ckpunmkug Member

    Joined:
    20 Mar 2017
    Messages:
    73
    Likes Received:
    72
    Reputations:
    10
    Выполняет роль прокладки для обмена данными, между мордой в браузере и приложением требующим непрерывное соединение. Я сделал его что бы прикрутить морду к phpdbg. Можно так же использовать для создания вэб-морды например к xdebug или другой хрени. Плюс он может принимать сигналы и работать с консолью.

    После запуска php example.php по умолчанию будет слушать 127.0.0.1:8080.

    Параметры класса TCPServer
    TCPServer->wrapper = null
    - сюда создаётся класс для с соединением соеденения.
    TCPServer->timeout = 3600 - максимальное время ожидания в секундах входящего соединения
    TCPServer->interrupt = true - обрабатывать или нет прерывание с клавиатуры Ctr-C
    TCPServer->start(string $url = "tcp://127.0.0.1:8080")
    - что слушать

    Параметры класса HTTPServer
    HTTPServer->connection = null
    - сюда TCPServer передаст соединение после accept
    HTTPServer->timeout = 60 - максимальное время простоя соединения в секундах
    HTTPServer->max_length['header'] = 0x100000 - максимальный размер http header
    HTTPServer->max_length['body'] = 0x1000000 - максимальный размер http body
    HTTPServer->__construct(string $callable) - фукнция которую вызовет класс после обработки http request

    Параметры функции HTTPRouter
    string $method
    - Метод из request line
    string $url - URL из request line
    array $headers - Массив строк параметров из http header
    string $content - Cодержимое http body
    array $response - Параметры по умолчанию для формирования ответа

    Содержимое array $response
    $response["code"] = 0
    - Код http ответа
    Значения параметров заголовка по умолчанию
    $response["headers"]["Content-Type"] = "Content-Type: text/html; charset=UTF-8" - содержимое текст в кодировке утф
    $response["headers"]["Cache-Control"] = "Cache-Control: no-cache,no-store" - не сохранять не кэшировать
    $response["headers"]["Content-Encoding"] = "Content-Encoding: identity" - использовать содержимое http response body таким какое оно есть
    $response["content" = "" - содержимое http response body

    TCPServer.php
    Code:
    <?php
    class TCPServer {
    	var $socket = null;
    	var $connection = null;
    	var $wrapper = null;
    	var $timeout = 3600;
    	var $interrupt = true;
    	var $childs = [];
    	function start(string $url = "tcp://127.0.0.1:8080") {
    		if ($this->interrupt) {
    			declare(ticks = 1);
    			pcntl_signal(SIGINT, function($signal) {
    				exit(0);
    			} );
    		}
    		$controller = posix_getpid();
    		$pid = pcntl_fork();
    		if ($pid == -1) {
    			trigger_error("can't fork", E_USER_ERROR);
    			exit(255);
    		}
    		if ($pid != 0) {
    			while(true) {
    				$siginfo = [];
    				pcntl_sigwaitinfo([SIGUSR1, SIGUSR2], $siginfo);
    				switch ($siginfo["signo"]) {
    					case SIGUSR1:
    						array_push($this->childs, $siginfo["pid"]);
    						break;
    					case SIGUSR2:
    						$key = array_search($siginfo["pid"], $this->childs);
    						unset($this->childs[$key]);
    						break;
    				}
    			}
    		}
    		$this->socket = stream_socket_server($url);
    		if (!is_resource($this->socket)) {
    			throw new Exception("can't create stream socket server");
    		}
    		while (true) {
    			$this->connection = stream_socket_accept($this->socket, $this->timeout);
    			$pid = pcntl_fork();
    			if ($pid == -1) {
         				trigger_error("can't fork", E_USER_ERROR);
         				exit(255);
    			}
    			if ($pid != 0) {
    				fclose($this->connection);
    				continue;
    			}
    			if (!is_resource($this->connection)) {
    				trigger_error("can't accept stream socket", E_USER_WARNING);
    				return false;
    			}
    			stream_set_blocking($this->connection, false);
    			$wrapper = clone $this->wrapper;
    			$wrapper->connection = &$this->connection;
    			posix_kill($controller, SIGUSR1);
    			if (!$wrapper->start()) {
    				trigger_error("session protocol wrapper error", E_USER_WARNING);
    			}
    			unset($wrapper);
    			fclose($this->connection);
    			fclose($this->socket);
    			posix_kill($controller, SIGUSR2);
    			exit(0);
    		}
    	}
    	function __destruct() {
    		if (is_resource($this->connection)) {
    			fclose($this->connection);
    			trigger_error("TCP connection closed", E_USER_NOTICE);
    		}
    		if (is_resource($this->socket)) {
    			fclose($this->socket);
    			trigger_error("TCP socket closed", E_USER_NOTICE);
    		}
    		foreach($this->childs as $pid) {
    			posix_kill($pid, SIGTERM);
    			trigger_error("PID {$pid} terminated", E_USER_NOTICE);
    		}
    	}
    }
    
    HTPServer.php
    Code:
    <?php
    class HTTPServer {
        var $connection = null;
        var $callable = "";
        var $timeout = 60;
        var $max_length = [
            'header' => 0x100000,
            'body' => 0x1000000
        ];
        var $request = [
            "method" => "",
            "url" => "",
            "headers" => [],
            "content" => ""
        ];
        var $response = [
            "code" => 0,
            "headers" => [
                "Content-Type" => "Content-Type: text/html; charset=UTF-8",
                "Cache-Control" => "Cache-Control: no-cache,no-store",
                "Content-Encoding" => "Content-Encoding: identity"
            ],
            "content" => ""
        ];
        function __construct(string $callable) {
            $this->callable = $callable;
        }
        function start() {
            if (!$this->receive_header()) {
                trigger_error("can't receive header", E_USER_WARNING);
                return false;
            }
            if ($this->request['method'] == 'POST') {
                if (!$this->receive_body()) {
                    trigger_error("can't receive body", E_USER_WARNING);
                    return false;
                }
            }
            $response = call_user_func(
                $this->callable,
                $this->request['method'],
                $this->request['url'],
                $this->request['headers'],
                $this->request['content'],
                $this->response
            );
            if (!is_array($response)) {
                $this->response['code'] = 500;
            } else {
                $this->response = $response;
            }
            if (!$this->send_response()) {
                trigger_error("can't send response", E_USER_WARNING);
                return false;
            }
            return true;
        }
        function receive_header() {
            $header = "";
            stream_set_timeout($this->connection, $this->timeout);
            while (true) {
                $string = stream_get_contents($this->connection);
                if (!is_string($string)) {
                    trigger_error("can't get contents for header from stream", E_USER_WARNING);
                    return false;
                }
                $header .= $string;
                if (strlen($header) > $this->max_length['header']) {
                    trigger_error("header length is greater than maximum", E_USER_WARNING);
                    return false;
                }
                $position = strpos($header, "\r\n\r\n");
                if (is_int($position)) {
                    break;
                }
            }
            $this->request['content'] = substr($header, ($position+4));
            $header = substr($header, 0, $position);
            $headers = explode("\r\n", $header);
            if (preg_match("/^([A-Z]+)\s+(.+)\s+HTTP\/1\.1$/", $headers[0], $matches) != 1) {
                trigger_error("can't parse request line", E_USER_WARNING);
                return false;
            }
            $this->request['method'] = $matches[1];
            $this->request['url'] = $matches[2];
            unset($headers[0]);
            $this->request['headers'] = array_values($headers);
            return true;
        }
        function receive_body() {
            $content_length = null;
            foreach ($this->request['headers'] as $string) {
                if (preg_match("/^Content\-Length\:\s+([0-9]+)$/", $string, $matches) == 1) {
                    $content_length = intval($matches[1]);
                    break;
                }
            }
            if (!is_int($content_length)) {
                trigger_error("can't find Content-Length from headers", E_USER_WARNING);
                return false;
            }
            stream_set_timeout($this->connection, $this->timeout);
            $body = &$this->request['content'];
            while (strlen($body) < $content_length) {
                $string = stream_get_contents($this->connection);
                if (!is_string($string)) {
                    trigger_error("can't get body contents from stream", E_USER_WARNING);
                    return false;
                }
                $body .= $string;
                if (strlen($body) > $this->max_length['body']) {
                    trigger_error("body length is greater than maximum", E_USER_WARNING);
                    return false;
                }
            }
            return true;
        }
        function send_response() {
            switch ($this->response['code']) {
                case 200:
                    $response = "HTTP/1.1 200 OK\r\n";
                    $response .= implode("\r\n", $this->response['headers'])."\r\n";
                    break;
                case 404:
                    $response = "HTTP/1.1 404 Not Found\r\n";
                    break;
                case 401:
                    $response = "HTTP/1.1 401 Unauthorized\r\n";
                    $response .= implode("\r\n", $this->response['headers'])."\r\n";
                    break;
                case 403:
                    $response = "HTTP/1.1 403 Forbidden\r\n";
                    break;
                case 500:
                    $response = "HTTP/1.1 500 Internal Server Error\r\n";
                    break;
                default:
                    trigger_error("unsupported response code", E_USER_WARNING);
                    return false;
            }
            $content = &$this->response['content'];
            if (!empty($content)) {
                $content_length = strlen($content);
                $response .= "Content-Length: ".strval($content_length)."\r\n";
            }
            $response .= "\r\n";
            $response .= $content;
            stream_set_timeout($this->connection, $this->timeout);
            if (!is_int(fwrite($this->connection, $response))) {
                trigger_error("can't write response", E_USER_WARNING);
                return false;
            }
            return true;
        }
    }
    
    example1.php - Демонстрирует приём GET и POST запросов
    Code:
    <?php
    require __DIR__."/TCPServer.php";
    require __DIR__."/HTTPServer.php";
    function HTTPRouter(
        string $method,
        string $url,
        array $headers,
        string $content,
        array $response
    ) {
        if ($url == '/' && $method == 'GET') {
            $response['code'] = 200;
            array_push(
                $response['headers'],
                "Set-Cookie: CookieName=CookieValue"
            );
            $response['content'] =
    <<<HEREDOC
    <html>
        <body>
            <form action="/" target="transceiver" method="post">
                <input type="text" name="name1" value="value1" />
                <input type="submit" value="submit" />
            </form>
            <iframe name="transceiver" src=""></iframe>
        </body>
    HEREDOC;
            return $response;
        }
        if ($url == '/' && $method == 'POST') {
            $response['code'] = 200;
            $response['content'] =
    <<<HEREDOC
    <html>
        <body>
    {$content}
        </body>
    HEREDOC;
            return $response;
        }
        $response['code'] = 404;
        return $response;
    }
    $TCPServer = new TCPServer;
    $TCPServer->wrapper = new HTTPServer('HTTPRouter');
    $TCPServer->start();
    
    example2.php - Демонстрирует как прикрутить HTTP авторизацию
    Code:
    <?php
    require __DIR__."/TCPServer.php";
    require __DIR__."/HTTPServer.php";
    function HTTPRouter(
        string $method,
        string $url,
        array $headers,
        string $content,
        array $response
    ) {
        $is_authorized = is_authorized($headers);
        if ($is_authorized === null) {
            $response ['code'] = 401;
            $response ['headers'] = ['WWW-Authenticate: Basic'];
            return $response;
        }
        if ($is_authorized === false) {
            $response ['code'] = 403;
            return $response;
        }
        if ($url == '/' && $is_authorized === true) {
            $response['code'] = 200;
            $response['content'] =
    <<<HEREDOC
    <html>
        <body>
    Authorization was successful
        </body>
    HEREDOC;
            return $response;
        }
        $response['code'] = 404;
        return $response;
    }
    function is_authorized($headers) {
        $base64_string = null;
        foreach ($headers as $string) {
            if (preg_match("/^Authorization\:\sBasic\s(.+)$/", $string, $matches) == 1) {
                $base64_string = $matches[1];
                break;
            }
        }
        if (!is_string($base64_string)) {
            return null;
        }
        $key = md5($base64_string);
        if ($key != "970cf1450cab27c98bade1876e6ed21a") {
            return false;
        }
        return true;
    }
    $TCPServer = new TCPServer;
    $TCPServer->wrapper = new HTTPServer('HTTPRouter');
    $TCPServer->start();
    
    Не жую. Читайте исходники и примеры.
     
    #1 ckpunmkug, 7 Mar 2019
    Last edited: 2 May 2019
    look2009, LuzhkOFF and BenderMR like this.
  2. barnaki

    barnaki Elder - Старейшина

    Joined:
    2 Nov 2008
    Messages:
    676
    Likes Received:
    140
    Reputations:
    4
    php не язык для демонов. очень сильные утечки памяти. гиг утекает за пол часа и приходится перезапускать процесс. такие вещи лучше на питоне писать. ну или на чем угодно но не на php
    ps. попробуйте запустить процесс и посмотреть через сколько он умрет с сообщением out of memory
     
  3. ckpunmkug

    ckpunmkug Member

    Joined:
    20 Mar 2017
    Messages:
    73
    Likes Received:
    72
    Reputations:
    10
    Мозг череп не жмёт? Хотел бы демона писать, использовал бы си. Ведь написал что это прокладка которую удобно сращивать с другими php скриптами. Будет сильно утекать вставлю авторестарт.
     
  4. barnaki

    barnaki Elder - Старейшина

    Joined:
    2 Nov 2008
    Messages:
    676
    Likes Received:
    140
    Reputations:
    4
    зачем хамить сразу ? авторестарт конечно хорошо. но зачем если можно просто подходящий инструмент взять. а утекать оно обязательно будет ))). это же пэхэпэ
     
  5. ckpunmkug

    ckpunmkug Member

    Joined:
    20 Mar 2017
    Messages:
    73
    Likes Received:
    72
    Reputations:
    10
    Цель проги удерживать соедининие между прогой и например отладчиком. При этом к проге обращается браузер, для отправки команд в соеденение. Прога висит в памяти, держит соединение и слушает порт, добавляем fork после accept и все данные которые не очистились будут удаляться вместе с завершением дочернего процесса.
    не аргумент.
     
  6. b3

    b3 Banned

    Joined:
    5 Dec 2004
    Messages:
    2,170
    Likes Received:
    1,155
    Reputations:
    202
  7. ckpunmkug

    ckpunmkug Member

    Joined:
    20 Mar 2017
    Messages:
    73
    Likes Received:
    72
    Reputations:
    10
    Нельзя создать и удерживать соединение с каким либо сервисом.
    Нельзя обмениваться сигналами, и работать с консолью.
    Читайте внимательней для чего был создан.
    Данный сервер, в первую очередь, предназначен для сохрания дескрипторов в открытом состоянии.
     
    #7 ckpunmkug, 14 Mar 2019
    Last edited: 14 Mar 2019
  8. ckpunmkug

    ckpunmkug Member

    Joined:
    20 Mar 2017
    Messages:
    73
    Likes Received:
    72
    Reputations:
    10
    Добавил в TCPServer многопоток на fork.
    Обработку соединения процессами потомками.
    Контроллер завершения потомков.

    После этого перестало глючить с хромом который открывал соединение, но не передавал данные.