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

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

  1. ckpunmkug

    ckpunmkug Member

    Joined:
    20 Mar 2017
    Messages:
    20
    Likes Received:
    7
    Reputations:
    1
    Выполняет роль прокладки для обмена данными, между мордой в браузере и приложением требующим непрерывное соединение. Я сделал его что бы прикрутить морду к 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;
    	function start(string $url = "tcp://127.0.0.1:8080") {
    		if ($this->interrupt) {
    			declare(ticks = 1);
    			pcntl_signal(SIGINT, function($signal) {
    				exit(0);
    			} );
    		}
    		$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);
    			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;
    			if (!$wrapper->start()) {
    				trigger_error("session protocol wrapper error", E_USER_WARNING);
    			}
    			unset($wrapper);
    			fclose($this->connection);
    		}
    	}
    	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);
    		}
    	}
    }
    
    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: 7 Mar 2019
    LuzhkOFF and BenderMR like this.
  2. barnaki

    barnaki Level 8

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

    ckpunmkug Member

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

    barnaki Level 8

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

    ckpunmkug Member

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

    b3 Moderator

    Joined:
    5 Dec 2004
    Messages:
    1,879
    Likes Received:
    696
    Reputations:
    198
    _________________________
  7. ckpunmkug

    ckpunmkug Member

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