├── docs ├── asset ├── .gitignore ├── Gemfile ├── humans.txt ├── addons.md ├── classes │ ├── fieldbody.md │ ├── commandclient.md │ ├── logger.md │ ├── serverobserver.md │ ├── parsedbody.md │ ├── bootable.md │ ├── monitor.md │ ├── index.md │ ├── bodyparser.md │ ├── server.md │ ├── router.md │ ├── websocket-endpoint.md │ ├── internalrequest.md │ ├── middleware.md │ ├── request.md │ ├── client.md │ ├── response.md │ ├── httpdriver.md │ ├── host.md │ ├── functions.md │ ├── websocket.md │ └── options.md ├── README.md ├── more.md ├── _config.yml ├── websocket │ ├── index.md │ ├── input.md │ ├── handshake.md │ └── output.md ├── index.md ├── options.md ├── logging.md ├── io.md ├── http-advanced.md ├── production.md ├── hosts.md ├── middlewares.md └── performance.md ├── lib ├── FilterException.php ├── Monitor.php ├── Middleware.php ├── ServerObserver.php ├── NullBody.php ├── Bootable.php ├── FieldBody.php ├── Websocket │ ├── Message.php │ ├── Code.php │ ├── Rfc6455Client.php │ ├── Rfc6455Endpoint.php │ ├── Endpoint.php │ └── Handshake.php ├── HttpDriver.php ├── ClientException.php ├── ConsoleLogger.php ├── ClientSizeException.php ├── Console.php ├── InternalRequest.php ├── DebugProcess.php ├── Client.php ├── ParsedBody.php ├── Ticker.php ├── IpcLogger.php ├── Websocket.php ├── constants.php ├── WorkerProcess.php ├── Response.php ├── Logger.php ├── CommandClient.php ├── Request.php ├── StandardRequest.php ├── Process.php ├── Host.php └── VhostContainer.php ├── .gitmodules ├── test-autobahn ├── config │ └── fuzzingclient.json ├── report.php └── server.php ├── Makefile ├── LICENSE ├── phpunit.xml.dist ├── .php_cs.dist ├── README.md ├── bin ├── aerys-worker └── aerys ├── composer.json ├── demo.php └── etc └── mime /docs/asset: -------------------------------------------------------------------------------- 1 | .shared/asset -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /_site 2 | /.bundle 3 | /vendor 4 | /Gemfile.lock -------------------------------------------------------------------------------- /lib/FilterException.php: -------------------------------------------------------------------------------- 1 | metadata = $metadata; 15 | } 16 | 17 | public function getMetadata(): Promise { 18 | return $this->metadata; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/Websocket/Message.php: -------------------------------------------------------------------------------- 1 | binary = $binary; 13 | } 14 | 15 | public function isBinary(): bool { 16 | return $this->binary; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/Websocket/Code.php: -------------------------------------------------------------------------------- 1 | string, "mime" => string>>` 11 | 12 | Returns a `Promise` to the metadata array, as defined by [`ParsedBody`](parsedbody.md). -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | This directory contains the documentation for `amphp/aerys`. Documentation and code are bundled within a single repository for easier maintenance. Additionally, this preserves the documentation for older versions. 4 | 5 | ## Reading 6 | 7 | You can read this documentation either directly on GitHub or on our website. While the website will always contain the latest version, viewing on GitHub also works with older versions. 8 | 9 | ## Writing 10 | 11 | Our documentation is built using Jekyll. 12 | 13 | ``` 14 | sudo gem install bundler jekyll 15 | ``` 16 | 17 | ``` 18 | bundle install --path vendor/bundle 19 | bundle exec jekyll serve 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/more.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Additions 3 | permalink: /more 4 | --- 5 | Aerys has a powerful responder callable mechanism, coupled to middlewares with routing based upon promises and non-blocking I/O. Beyond that ... 6 | 7 | It has HTTP/1 and HTTP/2 drivers. It provides a possibility to define a custom driver, see the [`HttpDriver` class docs](classes/httpdriver.md). 8 | 9 | Furthermore, it is possible to control (in non-debug mode) the Server (externally or within Aerys) via the [`CommandClient` class](classes/commandclient.md). 10 | 11 | When the server is started up or shut down, it is possible to be notified of these events via the [`ServerObserver` class](classes/serverobserver.md). 12 | -------------------------------------------------------------------------------- /lib/HttpDriver.php: -------------------------------------------------------------------------------- 1 | getOption("configPath")); 31 | yield $command->stop(); 32 | # Successfully stopped server! 33 | ``` -------------------------------------------------------------------------------- /docs/classes/logger.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Logger 3 | permalink: /classes/logger 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | Aerys includes a logger that can be used to `STDOUT`. While being in production mode Aerys uses multiple workers, so all log data is sent to the master process and logged to `STDOUT` there. 10 | 11 | The only way to receive the `Logger` instance is to implement [`Bootable`](bootable.md), it will be passed as second argument to its `boot()` method. 12 | 13 | By default only messages of severity `warning` or higher will be shown. In debug mode (`-d` / `--debug` flag) the default is lowered to be `debug`. You can adjust the log level using the `-l / --log` option. 14 | 15 | The `Logger` class implements the [PSR-3 `Psr\Log\LoggerInterface`](http://www.php-fig.org/psr/psr-3/#3-psr-log-loggerinterface) and thus exposes the same methods: `emergency()`, `alert()`, `critical()`, `error()`, `warning()`, `notice()`, `info()`, `debug()` and the universal `log()`. 16 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | kramdown: 2 | input: GFM 3 | toc_levels: 2..3 4 | 5 | baseurl: "/aerys" 6 | layouts_dir: ".shared/layout" 7 | includes_dir: ".shared/includes" 8 | 9 | exclude: ["Gemfile", "Gemfile.lock", "README.md", "vendor"] 10 | safe: true 11 | 12 | repository: amphp/aerys 13 | gems: 14 | - "jekyll-github-metadata" 15 | - "jekyll-relative-links" 16 | 17 | defaults: 18 | - scope: 19 | path: "" 20 | type: "pages" 21 | values: 22 | layout: "docs" 23 | description: "Aerys is a non-blocking HTTP/1.1 and HTTP/2 application, WebSocket and static file server written in PHP based on Amp." 24 | keywords: ["amphp", "amp", "aerys", "http server", "http", "server", "application server", "php"] 25 | 26 | shared_asset_path: "/aerys/asset" 27 | 28 | navigation: 29 | - http 30 | - websocket 31 | - host 32 | - io 33 | - helpers 34 | - logging 35 | - options 36 | - http-advanced 37 | - performance 38 | - middlewares 39 | - production 40 | - more 41 | - classes 42 | -------------------------------------------------------------------------------- /docs/websocket/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WebSockets 3 | permalink: /websocket/ 4 | --- 5 | 6 | ```php 7 | # just a blackhole, no processing yet 8 | return (new Aerys\Host)->use(Aerys\websocket(new class implements Aerys\Websocket { 9 | public function onStart(Aerys\Websocket\Endpoint $endpoint) {} 10 | public function onHandshake(Aerys\Request $request, Aerys\Response $response) { } 11 | public function onOpen(int $clientId, $handshakeData) { } 12 | public function onData(int $clientId, Aerys\Websocket\Message $msg) { } 13 | public function onClose(int $clientId, int $code, string $reason) { } 14 | public function onStop() { } 15 | })); 16 | ``` 17 | 18 | Websockets are real-time full-duplex (two-way) connections between client and server. 19 | 20 | `Aerys\websocket()` is returning a callable handler which can be passed to either `Host::use()` or to the Router by specifying a `->route('GET', '/path/to/websocket', Aerys\websocket($handler))` route. It expects an instance of an implementation of `Aerys\Websocket`. 21 | -------------------------------------------------------------------------------- /lib/ConsoleLogger.php: -------------------------------------------------------------------------------- 1 | console = $console; 10 | if ($console->isArgDefined("color")) { 11 | $value = $console->getArg("color"); 12 | $this->setAnsiColorOption($value); 13 | } 14 | if ($console->isArgDefined("log")) { 15 | $level = $console->getArg("log"); 16 | $level = isset(self::LEVELS[$level]) ? self::LEVELS[$level] : $level; 17 | $this->setOutputLevel($level); 18 | } 19 | } 20 | 21 | private function setAnsiColorOption($value) { 22 | $value = ($value === "") ? "on" : $value; 23 | $this->setAnsify($value); 24 | if ($value === "on") { 25 | $this->console->forceAnsiOn(); 26 | } 27 | } 28 | 29 | final protected function output(string $message) { 30 | $this->console->output($message); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PHP_BIN := php 2 | COMPOSER_BIN := composer 3 | 4 | COVERAGE = coverage 5 | SRCS = lib test 6 | 7 | find_php_files = $(shell find $(1) -type f -name "*.php") 8 | src = $(foreach d,$(SRCS),$(call find_php_files,$(d))) 9 | 10 | .PHONY: test 11 | test: setup phpunit code-style 12 | 13 | .PHONY: clean 14 | clean: clean-coverage clean-vendor 15 | 16 | .PHONY: clean-coverage 17 | clean-coverage: 18 | test ! -e coverage || rm -r coverage 19 | 20 | .PHONY: clean-vendor 21 | clean-vendor: 22 | test ! -e vendor || rm -r vendor 23 | 24 | .PHONY: setup 25 | setup: vendor/autoload.php 26 | 27 | .PHONY: deps-update 28 | deps-update: 29 | $(COMPOSER_BIN) update 30 | 31 | .PHONY: phpunit 32 | phpunit: setup 33 | $(PHP_BIN) vendor/bin/phpunit 34 | 35 | .PHONY: code-style 36 | code-style: setup 37 | PHP_CS_FIXER_IGNORE_ENV=1 $(PHP_BIN) vendor/bin/php-cs-fixer --diff -v fix 38 | 39 | composer.lock: composer.json 40 | $(COMPOSER_BIN) install 41 | touch $@ 42 | 43 | vendor/autoload.php: composer.lock 44 | $(COMPOSER_BIN) install 45 | touch $@ 46 | -------------------------------------------------------------------------------- /lib/ClientSizeException.php: -------------------------------------------------------------------------------- 1 | attach($this); 21 | } 22 | 23 | function update(Aerys\Server $server): Amp\Promise { 24 | switch ($server->state()) { 25 | case Aerys\Server::STARTING: /* ... */ break; 26 | case Aerys\Server::STARTED: /* ... */ break; 27 | case Aerys\Server::STOPPING: /* ... */ break; 28 | case Aerys\Server::STOPPED: /* ... */ break; 29 | } 30 | return new Amp\Success; 31 | } 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /lib/Websocket/Rfc6455Client.php: -------------------------------------------------------------------------------- 1 | string, "mime" => string>|null` 20 | 21 | Contains an `array("filename" => $name, "mime" => $mimetype)`. 22 | 23 | Elements may be missing, but in case a filename is provided, mime is always set. 24 | 25 | ## `getMetadataArray(string $name): array string, "mime" => string>>` 26 | 27 | Similar to `getMetadata()`, but fetches it as an array, with indices equivalent to the data returned by `getArray($name)`. 28 | 29 | ## `getNames(): array` 30 | 31 | Returns the names of the passed fields. 32 | 33 | ## `getAll(): array<"fields" => array>, "metadata" => array string, "mime" => string>>>>` 34 | 35 | Returns two associative fields and metadata arrays (like for extended abstractions or debug). -------------------------------------------------------------------------------- /lib/Console.php: -------------------------------------------------------------------------------- 1 | climate = $climate; 18 | } 19 | 20 | public function output(string $msg) { 21 | return $this->climate->out($msg); 22 | } 23 | 24 | public function forceAnsiOn() { 25 | $this->climate->forceAnsiOn(); 26 | } 27 | 28 | public function isArgDefined(string $arg) { 29 | if (empty($this->hasParsedArgs)) { 30 | $this->parseArgs(); 31 | } 32 | 33 | return $this->climate->arguments->defined($arg); 34 | } 35 | 36 | public function getArg(string $arg) { 37 | if (empty($this->hasParsedArgs)) { 38 | $this->parseArgs(); 39 | } 40 | 41 | return $this->climate->arguments->get($arg); 42 | } 43 | 44 | private function parseArgs() { 45 | if (empty($this->hasParsedArgs)) { 46 | @$this->climate->arguments->parse(); 47 | $this->hasParsedArgs = true; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/InternalRequest.php: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | test 23 | 24 | 25 | 26 | 27 | lib 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/DebugProcess.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 14 | } 15 | 16 | protected function doStart(Console $console): \Generator { 17 | if ($console->isArgDefined("restart")) { 18 | $this->logger->critical("You cannot restart a debug aerys instance via command"); 19 | exit(1); 20 | } 21 | 22 | if (ini_get("zend.assertions") === "-1") { 23 | $this->logger->warning( 24 | "Running aerys in debug mode with assertions disabled is not recommended; " . 25 | "enable assertions in php.ini (zend.assertions = 1) " . 26 | "or disable debug mode (-d) to hide this warning." 27 | ); 28 | } else { 29 | ini_set("zend.assertions", "1"); 30 | } 31 | 32 | $server = yield from Internal\bootServer($this->logger, $console); 33 | yield $server->start(); 34 | $this->server = $server; 35 | } 36 | 37 | protected function doStop(): \Generator { 38 | if ($this->server) { 39 | yield $this->server->stop(); 40 | $this->server = null; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/classes/bootable.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bootables 3 | permalink: /classes/bootable 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | `Bootable`s provide a way to be injected the [`Server`](server.md) and [`\Psr\Log\LoggerInterface`](https://github.com/php-fig/log/blob/master/Psr/Log/LoggerInterface.php) (by default an instance of [`Logger`](logger.md)) instances on startup. 10 | 11 | ## `boot(Server, \Psr\Log\LoggerInterface): Middleware|callable|null` 12 | 13 | This method is called exactly once when the [`Server`](server.md) is in `Server::STARTING` state. 14 | 15 | You may return a [`Middleware`](middleware.md) and/or responder callable in order to use an alternate instance for middleware/responder. 16 | 17 | {:.note} 18 | > It is a bad idea to rely on the order in which the `boot()` methods of the `Bootable`s are called 19 | 20 | ## Example 21 | 22 | ```php 23 | class MyBootable implements Aerys\Bootable { 24 | function boot(Aerys\Server $server, Aerys\Logger $logger) { 25 | // we can now use $server in order to register a ServerObserver for example 26 | 27 | // In case we want to not use this instance for Middlewares or responder callables, 28 | // we can return an alternate one 29 | return new class implements Aerys\Middleware { 30 | function do(Aerys\InternalRequest $ireq) { /* ... */ } 31 | }; 32 | } 33 | } 34 | return (new Aerys\Host)->use(new MyBootable); 35 | ``` 36 | -------------------------------------------------------------------------------- /lib/Client.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 5 | ->setRules([ 6 | "@PSR1" => true, 7 | "@PSR2" => true, 8 | "braces" => [ 9 | "allow_single_line_closure" => true, 10 | "position_after_functions_and_oop_constructs" => "same", 11 | ], 12 | "array_syntax" => ["syntax" => "short"], 13 | "cast_spaces" => true, 14 | "combine_consecutive_unsets" => true, 15 | "function_to_constant" => true, 16 | "no_multiline_whitespace_before_semicolons" => true, 17 | "no_unused_imports" => true, 18 | "no_useless_else" => true, 19 | "no_useless_return" => true, 20 | "no_whitespace_before_comma_in_array" => true, 21 | "no_whitespace_in_blank_line" => true, 22 | "non_printable_character" => true, 23 | "normalize_index_brace" => true, 24 | "ordered_imports" => true, 25 | "php_unit_construct" => true, 26 | "php_unit_dedicate_assert" => true, 27 | "php_unit_fqcn_annotation" => true, 28 | "phpdoc_summary" => true, 29 | "phpdoc_types" => true, 30 | "psr4" => true, 31 | "return_type_declaration" => ["space_before" => "none"], 32 | "short_scalar_cast" => true, 33 | "single_blank_line_before_namespace" => true, 34 | ]) 35 | ->setFinder( 36 | PhpCsFixer\Finder::create() 37 | ->in(__DIR__ . "/lib") 38 | ->in(__DIR__ . "/test") 39 | ); -------------------------------------------------------------------------------- /docs/classes/monitor.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Monitor 3 | permalink: /classes/monitor 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | `Monitor`s expose a method `monitor()` to retrieve statistical and sanity information about its internal state. 10 | 11 | In particular the [`Server`](server.md) class extends `Monitor` and will call `monitor()` on every virtual host. 12 | 13 | ## `monitor(): array` 14 | 15 | When invoked, this method must return an array with all the data it wishes to make available. 16 | 17 | ## Example 18 | 19 | ```php 20 | class RequestCountingMonitor implements Aerys\Monitor, Aerys\Bootable { 21 | private $server; 22 | private $requestCounter = 0; 23 | 24 | function boot(Aerys\Server $server, \Psr\Log\LoggerInterface $log) { 25 | $this->server = $server; 26 | } 27 | 28 | function __invoke(Aerys\Request $req, Aerys\Response $res) { 29 | $this->requestCounter++; 30 | $res->write("

MyMonitor

    "); 31 | foreach($server->monitor()["hosts"] as $id => $host) { 32 | $res->write("
  • $id: {$host["handlers"][self::class][0]["requestCounter"]}
  • "); 33 | } 34 | $res->end("
") 35 | } 36 | 37 | function monitor(): array { 38 | return ["requestCounter" => $this->requestCounter]; 39 | } 40 | } 41 | ($hosts[] = new Aerys\Host)->name("foo.local")->use(new RequestCountingMonitor); 42 | ($hosts[] = new Aerys\Host)->name("bar.local")->use(new RequestCountingMonitor); 43 | return $hosts; 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | permalink: / 4 | --- 5 | Aerys is a non-blocking HTTP/1.1 and HTTP/2 application, WebSocket and static file server written in PHP. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | composer require amphp/aerys 11 | ``` 12 | 13 | ## First Run 14 | 15 | ```bash 16 | vendor/bin/aerys -d -c demo.php 17 | ``` 18 | 19 | {:.warning} 20 | > On production you'll want to drop the `-d` (debug mode) flag. For development it is pretty helpful though. `-c demo.php` tells the program where to find the config file. 21 | 22 | ## First Configuration 23 | 24 | ```php 25 | use(Aerys\root("/var/www/public_html")); 28 | ``` 29 | 30 | Save it as `config.php` and load it via `sudo php vendor/bin/aerys -d -c config.php`. The `sudo` may be necessary as it binds by default on port 80 - for this case there is an [`user` option to drop the privileges](options.md#common-options). 31 | 32 | That's all needed to serve files from a static root. Put an `index.html` there and try opening [`http://localhost/`](http://localhost/) in the browser. 33 | 34 | The host instance is at the root of each virtual host served by Aerys. By default it serves your content over port 80 on localhost. To configure an alternative binding, have a look [here](classes/host.md). 35 | 36 | The `root($path)` function returns a handler for static file serving and expects a document root path to serve files from as first parameter. 37 | 38 | {:.note} 39 | > Debug mode is most helpful when zend.assertions is set to 1. If it isn't set to 1 in your config, load the server with `php -d zend.assertions=1 vendor/bin/aerys -d -c config.php`. 40 | -------------------------------------------------------------------------------- /lib/Websocket/Rfc6455Endpoint.php: -------------------------------------------------------------------------------- 1 | gateway = $gateway; 12 | } 13 | 14 | public function send(string $data, int $clientId): Promise { 15 | return $this->gateway->send($data, false, $clientId); 16 | } 17 | 18 | public function sendBinary(string $data, int $clientId): Promise { 19 | return $this->gateway->send($data, true, $clientId); 20 | } 21 | 22 | public function broadcast(string $data, array $exceptIds = []): Promise { 23 | return $this->gateway->broadcast($data, false, $exceptIds); 24 | } 25 | 26 | public function broadcastBinary(string $data, array $exceptIds = []): Promise { 27 | return $this->gateway->broadcast($data, true, $exceptIds); 28 | } 29 | 30 | public function multicast(string $data, array $clientIds): Promise { 31 | return $this->gateway->multicast($data, false, $clientIds); 32 | } 33 | 34 | public function multicastBinary(string $data, array $clientIds): Promise { 35 | return $this->gateway->multicast($data, true, $clientIds); 36 | } 37 | 38 | public function close(int $clientId, int $code = Code::NORMAL_CLOSE, string $reason = "") { 39 | $this->gateway->close($clientId, $code, $reason); 40 | } 41 | 42 | public function getInfo(int $clientId): array { 43 | return $this->gateway->getInfo($clientId); 44 | } 45 | 46 | public function getClients(): array { 47 | return $this->gateway->getClients(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/classes/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Classes 3 | permalink: /classes/ 4 | --- 5 | Aerys provides a set of classes and interfaces, as well as [functions](functions.md): 6 | 7 | - [`BodyParser`](bodyparser.md) — Parser for bodies 8 | - [`Bootable`](bootable.md) — Registers entry point for [`Server`](server.md) and [`Logger`](logger.md) 9 | - [`Client`](client.md) — Client connection related information 10 | - [`CommandClient`](commandclient.md) — Controls the server master process 11 | - [`FieldBody`](fieldbody.md) — Field body message container (via [`BodyParser`](bodyparser.md)) 12 | - [`Host`](host.md) — Registers a virtual host 13 | - [`HttpDriver`](httpdriver.md) — Driver for interaction with the raw socket 14 | - [`InternalRequest`](internalrequest.md) — Request related information 15 | - [`Logger`](logger.md) — PSR-3 compatible logger 16 | - [`Middleware`](middleware.md) — Defines a middleware callable in `do()` method 17 | - [`Options`](options.md) — Accessor of options 18 | - [`ParsedBody`](parsedbody.md) — Holds request body data in parsed form 19 | - [`Request`](request.md) — General request interface for responder callables 20 | - [`Response`](response.md) — General response interface for responder callables 21 | - [`Router`](router.md) — Manages and accumulates routes 22 | - [`Server`](server.md) — The server, tying everything together 23 | - [`ServerObserver`](serverobserver.md) — Registers method to be notified upon Server state changes 24 | - [`Websocket`](websocket.md) — General websocket connection manager 25 | - [`Websocket\Endpoint`](websocket-endpoint.md) — Provides API to communicate with a websocket client 26 | -------------------------------------------------------------------------------- /test-autobahn/report.php: -------------------------------------------------------------------------------- 1 | red("Could not find autobahn test results json file"); 11 | exit(1); 12 | } 13 | 14 | $report = file_get_contents(REPORT_PATH); 15 | $report = json_decode($report, true)["Aerys"]; 16 | 17 | $climate->out("Autobahn test report:"); 18 | 19 | $passed = 0; 20 | $nonstrict = 0; 21 | $failed = 0; 22 | $total = 0; 23 | 24 | foreach ($report as $testNumber => $result) { 25 | $message = \sprintf("%9s: %s ", $testNumber, $result["behavior"]); 26 | 27 | switch ($result["behavior"]) { 28 | case "OK": 29 | $passed++; 30 | $climate->green($message); 31 | break; 32 | 33 | case "NON-STRICT": 34 | $nonstrict++; 35 | $climate->yellow($message); 36 | break; 37 | 38 | case "FAIL": 39 | $climate->red($message); 40 | $failed++; 41 | break; 42 | 43 | default: 44 | $climate->blue($message); 45 | break; 46 | } 47 | } 48 | 49 | $climate->br(); 50 | 51 | $total = $passed + $nonstrict + $failed; 52 | $counts = \sprintf("%d Total / %d Passed / %d Non-strict / %d Failed", $total, $passed, $nonstrict, $failed); 53 | 54 | if ($failed) { 55 | $climate->backgroundRed(\sprintf(" Tests failed: %s ", $counts)); 56 | } elseif ($nonstrict) { 57 | $climate->backgroundYellow(\sprintf(" Tests passed: %s ", $counts)); 58 | } else { 59 | $climate->backgroundGreen(\sprintf(" Tests passed: %s ", $counts)); 60 | } 61 | 62 | exit($failed === 0 ? 0 : 1); 63 | -------------------------------------------------------------------------------- /test-autobahn/server.php: -------------------------------------------------------------------------------- 1 | endpoint = $endpoint; 19 | } 20 | 21 | public function onHandshake(Request $request, Response $response) { } 22 | public function onOpen(int $clientId, $handshakeData) { } 23 | 24 | public function onData(int $clientId, Websocket\Message $msg) { 25 | if ($msg->isBinary()) { 26 | $this->endpoint->broadcastBinary(yield $msg); 27 | } else { 28 | $this->endpoint->broadcast(yield $msg); 29 | } 30 | } 31 | 32 | public function onClose(int $clientId, int $code, string $reason) { } 33 | public function onStop() { } 34 | }, [ 35 | "maxBytesPerMinute" => PHP_INT_MAX, 36 | "maxFrameSize" => PHP_INT_MAX, 37 | "maxFramesPerSecond" => PHP_INT_MAX, 38 | "maxMsgSize" => PHP_INT_MAX, 39 | "validateUtf8" => true 40 | ]); 41 | 42 | $router = router()->route("GET", "/ws", $websocket); 43 | 44 | // If none of our routes match try to serve a static file 45 | $root = root($docrootPath = __DIR__); 46 | 47 | return (new Host)->expose("127.0.0.1", 9001)->use($router); 48 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Options 3 | permalink: /options 4 | --- 5 | ```php 6 | const AERYS_OPTIONS = [ 7 | "sendServerToken" => true, 8 | ]; 9 | ``` 10 | 11 | The most common way to define options is via an `AERYS_OPTIONS` constant inside the configuration file. 12 | 13 | For external code, there is the possibility inside a `Bootable` to fetch the `Server` object during the `boot` function and call `$server->setOption($name, $value)`. 14 | 15 | Additionally, there are several ways to get an options value: 16 | - `$client->options->option_name` (for `Middleware`s via `InternalRequest->client` property) 17 | - `Request::getOption("option_name")` (for Response handlers) 18 | - `Server::getOption("option_name")` (for `Bootable`s) 19 | 20 | ## Common Options 21 | 22 | This describes the common options, not affecting performance; to fine-tune your servers limits, look at the [production specific options](production.md#options). 23 | 24 | - `defaultContentType` The default content type of responses (Default: `"text/html"`) 25 | - `defaultTextCharset` The default charset of `text/` content types (Default: `"utf-8"`) 26 | - `sendServerToken` Whether to send a `Server` header with each request (Default: `false`) 27 | - `normalizeMethodCase` Whether method names should be always automatically uppercased (Default: `true`) 28 | - `allowedMethods` An array of allowed methods - if the method is not in the list, the request is terminated with a 405 Method not allowed (Default: `["GET", "POST", "PUT", "PATCH", "HEAD", "OPTIONS", "DELETE"]`) 29 | - `shutdownTimeout` A timeout in milliseconds the server is given to gracefully shutdown (Default: `3000`) 30 | - `user` The user (only relevant for Unix) under which the server runs. [This is important from a security perspective in order to limit damage if there's ever a breach in your application!] (No default - set it yourself!) -------------------------------------------------------------------------------- /docs/logging.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Logging 3 | permalink: /logging 4 | --- 5 | Aerys follows [the PSR-3 standard for logging](http://www.php-fig.org/psr/psr-3/). 6 | 7 | Aerys uses the warning level by default as minimum - in debug mode it uses the debug level. It is possible to specify any default minimum log level via the `--log` command line option. E.g. `--log info`, which will log everything, except debug level logs. 8 | 9 | All logging output is sent to the STDOUT of the master process; thus, to log to a file, all needed is piping the output of the master process to the file. 10 | 11 | Additionally, use of ANSI colors (for nicer displaying in terminal) can be turned on or off via `--color on` respectively `--color off`. 12 | 13 | ## Usage 14 | 15 | ```php 16 | return (new Aerys\Host)->use(new class implements Bootable { 17 | private $logger; 18 | 19 | function boot(Aerys\Server $server, Psr\Log\LoggerInterface $logger) { 20 | $this->logger = $logger; 21 | } 22 | 23 | function __invoke(Aerys\Request $req, Aerys\Response $res) { 24 | $this->logger->debug("Request received!"); 25 | } 26 | }); 27 | ``` 28 | 29 | The `Aerys\Bootable` interface provides a `boot(Aerys\Server $server, Psr\Log\LoggerInterface $logger)` function, which is called with the `Aerys\Server` and especially an instance of `Psr\Log\LoggerInterface` before the `Aerys\Server` is actually started. [`STARTING` state] 30 | 31 | The passed instance of `Psr\Log\LoggerInterface` can then be stored in e.g. an object property for later use. [As specified by the standard](https://github.com/php-fig/log/blob/master/Psr/Log/LoggerInterface.php), methods from `debug()` to `emergency()` are available. 32 | 33 | The signature of these functions is `(string $message, array $context = [])` with `$context` array possibly containing an entry `"time" => $unixTimestamp` [if this one is not present, current time is assumed]. -------------------------------------------------------------------------------- /docs/classes/bodyparser.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: BodyParser 3 | permalink: /classes/bodyparser 4 | --- 5 | The `BodyParser` is the `Promise` to the [`ParsedBody`](parsedbody.md). 6 | 7 | You typically get a `BodyParser` instance by calling the `parseBody(Request, int $size = 0)` function. 8 | 9 | ## `Promise::onResolve(callable(ClientException|null, string))` 10 | 11 | If an instance of this class is yielded or `onResolve()` is used, it will either throw or pass a `ClientException` as first parameter, or return an instance of [`ParsedBody`](parsedbody.md) or pass it as second parameter, when all data has been fetched. 12 | 13 | ## `Amp\ByteStream\InputStream::read(): Promise` 14 | 15 | The returned Promise is resolved with a field name as soon as it starts being processed. 16 | 17 | ## `stream(string $name, int $size = 0): FieldBody` 18 | 19 | Returns the **next** `FieldBody` for a given `$name` and sets the size limit for that field to `$size` bytes. 20 | 21 | Note the emphasis on _next_, it thus is possible to fetch multiple equally named fields by calling `stream()` repeatedly. 22 | 23 | If `$size` <= 0, the last specified size is used, if none present, it's counting toward total size; if `$size` > 0, the current field has a size limit of `$size`. 24 | 25 | ## Example 26 | 27 | ```php 28 | # $req being an instance of Request 29 | $body = Aerys\parseBody($req); 30 | # Note this is 2 MB *TOTAL*, for all the file fields. 31 | $field = $body->stream("file", 2 << 20 /* 2 MB */); 32 | while (null !== $data = yield $field->read()) { 33 | $metadata = yield $field->getMetadata(); 34 | if (!isset($metadata["filename"])) { 35 | $res->setStatus(HTTP_STATUS["BAD_REQUEST"]); 36 | return; 37 | } 38 | // This obviously is only fine when this is an admin panel and user can be trusted 39 | // else further validation is required! 40 | $handle = Amp\file\open("files/".$metadata["filename"], "w+"); 41 | do { 42 | $handle->write($data); 43 | } while (null !== ($data = yield $field->read())); 44 | $field = $body->stream("file"); 45 | } 46 | ``` -------------------------------------------------------------------------------- /lib/ParsedBody.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 12 | $this->metadata = $metadata; 13 | } 14 | 15 | /** 16 | * Fetch a string parameter (or null if it doesn't exist). 17 | * 18 | * @param string $name 19 | * @return string|null 20 | */ 21 | public function get(string $name) { 22 | return $this->fields[$name][0] ?? null; 23 | } 24 | 25 | /** 26 | * Fetch an array parameter (or empty array if it doesn't exist). 27 | * 28 | * @param string $name 29 | * @return array 30 | */ 31 | public function getArray(string $name): array { 32 | return $this->fields[$name] ?? []; 33 | } 34 | 35 | /** 36 | * Contains an array("filename" => $name, "mime" => $mimetype) 37 | * Elements may be missing, but in case a filename is provided, mime is always set. 38 | * 39 | * @param string $name 40 | * @return array|null 41 | */ 42 | public function getMetadata(string $name) { 43 | return $this->metadata[$name][0] ?? null; 44 | } 45 | 46 | /** 47 | * Fetch an array of metadata. 48 | * 49 | * @param string $name 50 | * @return array 51 | */ 52 | public function getMetadataArray(string $name): array { 53 | return $this->metadata[$name] ?? []; 54 | } 55 | 56 | /** 57 | * Returns the names of the passed fields. 58 | * 59 | * @return array 60 | */ 61 | public function getNames(): array { 62 | return $this->names ?? $this->names = array_keys($this->fields); 63 | } 64 | 65 | /** 66 | * returns two associative fields and metadata arrays (like for extended abstractions or debug). 67 | * 68 | * @return array 69 | */ 70 | public function getAll(): array { 71 | return ["fields" => $this->fields, "metadata" => $this->metadata]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /docs/classes/server.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server 3 | permalink: /classes/server 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | The `Server` instance controls the whole listening and dispatches the parsed requests. 10 | 11 | ## `attach(ServerObserver)` 12 | 13 | Enables a [`ServerObserver`](serverobserver.md) instance to be notified of the updates. 14 | 15 | ## `detach(ServerObserver)` 16 | 17 | Disables notifications for the passed [`ServerObserver`](serverobserver.md) instance. 18 | 19 | ## `state()` 20 | 21 | Gets the current server state, which is one of the following class constants: 22 | 23 | * `Server::STARTING` 24 | * `Server::STARTED` 25 | * `Server::STOPPING` 26 | * `Server::STOPPED` 27 | 28 | ## `getOption(string)` 29 | 30 | Gets an [`option`](options.md) value. 31 | 32 | ## `setOption(string, $value)` 33 | 34 | Sets an [`option`](options.md) value. 35 | 36 | ## `stop(): Promise` 37 | 38 | Initiate shutdown sequence. The returned `Promise` will resolve when the server has successfully been stopped. 39 | 40 | ## `monitor(): array` 41 | 42 | See [`Monitor`](monitor.md), it returns an array of the following structure: 43 | 44 | ```php 45 | [ 46 | "state" => STARTING|STARTED|STOPPING|STOPPED, 47 | "bindings" => ["tcp://127.0.0.1:80", "tcp://ip:port", ...], 48 | "clients" => int, # number of clients 49 | "IPs" => int, # number of different connected IPs 50 | "pendingInputs" => int, # number of clients not being processed currently 51 | "hosts" => [ 52 | "localhost:80" => [ 53 | "interfaces" => [["127.0.0.1", 80], ["ip", port], ...], 54 | "name" => "localhost", 55 | "tls" => array with "ssl" context options, 56 | "handlers" => [ 57 | "MyMonitorClass" => [ 58 | MyMonitorClass->monitor(), 59 | MyMonitorClass->monitor(), 60 | ... # if there are multiple instances of a same handler 61 | ], 62 | "OtherMonitorClass" => [...], 63 | ... 64 | ], 65 | ], 66 | "name:port" => [...], 67 | ... 68 | ], 69 | ] 70 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aerys 2 | 3 | [![Build Status](https://travis-ci.org/amphp/aerys.svg?branch=master)](https://travis-ci.org/amphp/aerys) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/amphp/aerys/blob/master/LICENSE) 5 | 6 | Aerys is a non-blocking HTTP/1.1 and HTTP/2 application, WebSocket and static file server written in PHP based on [Amp](https://github.com/amphp/amp). 7 | 8 | ## Deprecation 9 | 10 | This repository is deprecated in favor of [`amphp/http-server`](https://github.com/amphp/http-server). 11 | It still exists to keep the documentation and also Packagist working as before. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | composer require amphp/aerys 17 | ``` 18 | 19 | ## Documentation 20 | 21 | - [Official Documentation](http://amphp.org/aerys/) 22 | - [Getting Started with Aerys](http://blog.kelunik.com/2015/10/21/getting-started-with-aerys.html) 23 | - [Getting Started with Aerys WebSockets](http://blog.kelunik.com/2015/10/20/getting-started-with-aerys-websockets.html) 24 | 25 | ## Running a Server 26 | 27 | ```bash 28 | php bin/aerys -c demo.php 29 | ``` 30 | 31 | Simply execute the `aerys` binary (with PHP 7) to start a server listening on `http://localhost/` using 32 | the default configuration file (packaged with the repository). 33 | 34 | Add a `-d` switch to see some debug output like the routes called etc.: 35 | 36 | ```bash 37 | php bin/aerys -d -c demo.php 38 | ``` 39 | 40 | ## Config File 41 | 42 | Use the `-c, --config` switches to define the config file: 43 | 44 | ```bash 45 | php bin/aerys -c /path/to/my/config.php 46 | ``` 47 | 48 | Use the `-h, --help` switches for more instructions. 49 | 50 | ## Static File Serving 51 | 52 | To start a static file server simply pass a root handler as part of your config file. 53 | 54 | ```php 55 | return (new Aerys\Host) 56 | ->expose("*", 1337) 57 | ->use(Aerys\root(__DIR__ . "/public")); 58 | ``` 59 | 60 | ## Security 61 | 62 | If you discover any security related issues, please email `bobwei9@hotmail.com` or `me@kelunik.com` instead of using the issue tracker. 63 | 64 | ## License 65 | 66 | The MIT License (MIT). Please see [LICENSE](./LICENSE) for more information. 67 | -------------------------------------------------------------------------------- /docs/classes/router.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Router 3 | permalink: /classes/router 4 | --- 5 | 6 | The `Router` class is typically instantiated via the `router()` function. 7 | 8 | ## `use(callable|Middleware|Bootable|Monitor): self` 9 | 10 | Installs an action global to the router. 11 | 12 | ## `prefix(string): self` 13 | 14 | Prefixes every route (and even global actions) with a given prefix. 15 | 16 | ## `route(string $method, string $uri, callable|Middleware|Bootable|Monitor ...$actions): self` 17 | 18 | Installs a route to be matched on a given `$method` and `$uri` combination. 19 | 20 | In case of match, the route middlewares will be installed (including global `use()`'d middlewares) in _the order they were defined_. Similar for the chain of application callables. 21 | 22 | The Router is using [FastRoute from Nikita Popov](https://github.com/nikic/FastRoute) and inherits its dynamic possibilities. Hence it is possible to use dynamic routes, the matches will be in a third `$routes` array passed to the callable. This array will contain the matches keyed by the identifiers in the route. 23 | 24 | A trailing `/?` on the route will make the slash optional and, when the route is called with a slash, issue a `302 Temporary Redirect` to the canonical route without trailing slash. 25 | 26 | {:.note} 27 | > Variable path segments can be defined using braces, e.g. `/users/{userId}`. Custom regular expressions can be used with a colon after the placeholder name, e.g. `/users/{userId:\d+}`. For a full list of route definition possiblities, have a look at the [FastRoute documentation](https://github.com/nikic/FastRoute#usage). 28 | 29 | ## `monitor(): array` 30 | 31 | See [`Monitor`](monitor.md), it returns an array of the following structure: 32 | 33 | ```php 34 | [ 35 | "GET" => [ 36 | "/route" => [ 37 | "MyMonitorClass" => [ 38 | MyMonitorClass->monitor(), 39 | MyMonitorClass->monitor(), 40 | ... # if there are multiple instances of a same handler 41 | ], 42 | "OtherMonitorClass" => [...], 43 | ... 44 | ], 45 | ... 46 | ], 47 | "method" => [...], 48 | ... 49 | ] 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/io.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Handling I/O 3 | permalink: /io 4 | --- 5 | Aerys is built on top of [the non-blocking concurrency framework Amp](https://amphp.org/amp). Thus it inherits full support of all its primitives and it is possible to use all the non-blocking libraries built on top it. That's also why several things need to be `yield`ed, as they are `Promise`s. [Coroutines](https://amphp.org/amp/coroutines) let you await their resolution using `yield`, so you can write your code almost like blocking code. Most importantly, if the request or WebSocket handlers are returning a `Generator`, these are automatically run as coroutines. 6 | 7 | {:.note} 8 | > In general, you should make yourself familiar with [the Promise **concept**](https://amphp.org/amp/promises), with [`yield`ing](https://amphp.org/amp/coroutines) and be aware of the several [combinator](https://amphp.org/amp/promises/combinators) and [coroutine helper](https://amphp.org/amp/coroutines/helpers) functions, to really succeed at Aerys. 9 | 10 | ## Blocking I/O 11 | 12 | Nearly every built-in function of PHP is doing blocking I/O, that means, the executing thread (equivalent to the process in the case of Aerys) will effectively be halted until the response is received. A few examples of such functions: `mysqli_query`, `file_get_contents`, `usleep` and many more. 13 | 14 | A good rule of thumb is: Every function doing I/O is doing it in a blocking way, unless you know for sure it doesn't. 15 | 16 | Thus there are [libraries built on top of Amp](https://amphp.org/packages) providing implementations that work with non-blocking I/O. You should use these instead of the built-in functions. 17 | 18 | {:.warning} 19 | > Don't use any blocking I/O functions in Aerys. 20 | 21 | ```php 22 | // Here's a bad example, DO NOT do something like that! 23 | 24 | return (new Aerys\Host)->use(function (Aerys\Request $req, Aerys\Response $res) { 25 | $res->end("Some data"); 26 | sleep(5); // Equivalent to a blocking I/O function with a 5 second timeout 27 | }); 28 | 29 | // Access this route twice. You'll have to wait until the 5 seconds are over until the second request is handled. Start Aerys with only one worker (`-w 1` / `-d`), otherwise your second request might be handled by another worker and the effect not be visible. 30 | ``` 31 | -------------------------------------------------------------------------------- /lib/Ticker.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 23 | } 24 | 25 | public function update(Server $server): Promise { 26 | switch ($server->state()) { 27 | case Server::STARTED: 28 | $this->watcherId = Loop::repeat(1000, [$this, "updateTime"]); 29 | $this->updateTime(); 30 | break; 31 | case Server::STOPPED: 32 | Loop::cancel($this->watcherId); 33 | $this->watcherId = null; 34 | break; 35 | } 36 | 37 | return new Success; 38 | } 39 | 40 | /** 41 | * Add a callback to invoke each time the time context updates. 42 | * 43 | * Callbacks are invoked with two parameters: currentTime and currentHttpDate. 44 | * 45 | * @param callable $useCallback 46 | * @return void 47 | */ 48 | public function use(callable $useCallback) { 49 | $this->useCallbacks[] = $useCallback; 50 | } 51 | 52 | /** 53 | * Updates the context with the current time. 54 | * 55 | * @return void 56 | */ 57 | public function updateTime() { 58 | // Date string generation is (relatively) expensive. Since we only need HTTP 59 | // dates at a granularity of one second we're better off to generate this 60 | // information once per second and cache it. 61 | $now = (int) round(microtime(true)); 62 | $this->currentTime = $now; 63 | $this->currentHttpDate = gmdate("D, d M Y H:i:s", $now) . " GMT"; 64 | foreach ($this->useCallbacks as $useCallback) { 65 | $this->tryUseCallback($useCallback); 66 | } 67 | } 68 | 69 | private function tryUseCallback(callable $useCallback) { 70 | try { 71 | $useCallback($this->currentTime, $this->currentHttpDate); 72 | } catch (\Throwable $uncaught) { 73 | $this->logger->critical($uncaught); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /bin/aerys-worker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | arguments->add([ 40 | "ipc" => [ 41 | "prefix" => "i", 42 | "required" => true, 43 | ], 44 | "log" => [ 45 | "prefix" => "l", 46 | "required" => true, 47 | ], 48 | "color" => [ 49 | "longPrefix" => "color", 50 | "castTo" => "string", 51 | "required" => true, 52 | ], 53 | "config" => [ 54 | "prefix" => "c", 55 | "required" => true, 56 | ], 57 | ]); 58 | 59 | $console = new Aerys\Console($climate); 60 | 61 | $ipcUri = $console->getArg("ipc"); 62 | 63 | if (!$ipcSock = @stream_socket_client($ipcUri)) { 64 | fwrite(STDERR, "Failed initializing IPC connection"); 65 | exit(1); 66 | } 67 | 68 | Amp\Loop::run(function () use ($ipcSock, $console) { 69 | $logger = new Aerys\IpcLogger($console, $ipcSock); 70 | 71 | ob_start(function($output) use ($logger) { 72 | static $linebuf = ""; 73 | $linebuf .= $output; 74 | 75 | if (($end = strrpos($linebuf, "\n")) !== false) { 76 | $logger->warning("Data written to STDOUT in worker (PID: ".getmypid()."):\n".substr($linebuf, 0, $end)); 77 | $linebuf = substr($linebuf, $end + 1); 78 | } 79 | }, 1, PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_FLUSHABLE); 80 | 81 | $process = new Aerys\WorkerProcess($logger, $ipcSock); 82 | 83 | yield from $process->start($console); 84 | }); 85 | -------------------------------------------------------------------------------- /docs/classes/websocket-endpoint.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Websocket\Endpoint 3 | permalink: /classes/websocket-endpoint 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | The `Websocket\Endpoint` interface is the door to communicating with the client. `$clientId` is here in every case the client identifier passed in via [`Websocket` interface functions](websocket.md). 10 | 11 | ## `send(string $data, int $clientId): Promise` 12 | 13 | Sends UTF-8 compatible data to a given client. 14 | 15 | The Promise will be fulfilled when the internal buffers aren't too saturated. Yielding these promises is a good way to prevent too much data pending in memory. 16 | 17 | ## `sendBinary(string $data, int $clientId): Promise` 18 | 19 | Similar to `send()`, except for sending binary data. 20 | 21 | ## `broadcast(string $data, array $exceptIds = []): Promise` 22 | 23 | Sends UTF-8 compatible data to all clients except those given as second argument. 24 | 25 | The Promise will be fulfilled when the internal buffers aren't too saturated. Yielding these promises is a good way to prevent too much data pending in memory. 26 | 27 | ## `boardcastBinary(string $data, array $exceptIds = []): Promise` 28 | 29 | Similar to `broadcast()`, except for sending binary data. 30 | 31 | ## `multicast(string $data, array $clientIds): Promise` 32 | 33 | Sends UTF-8 compatible data to a given set of clients. 34 | 35 | The Promise will be fulfilled when the internal buffers aren't too saturated. Yielding these promises is a good way to prevent too much data pending in memory. 36 | 37 | ## `sendMulticast(string $data, int $clientId): Promise` 38 | 39 | Similar to `multicast()`, except for sending binary data. 40 | 41 | ## `close(int $clientId, int $code = Websocket\Code::NORMAL_CLOSE, string $reason = "")` 42 | 43 | Closes the websocket connection to a `$clientId` with a `$code` and a `$reason`. 44 | 45 | ## `getInfo(int $clientId): array` 46 | 47 | This returns an array with the following (self-explaining) keys: 48 | 49 | - `bytes_read` 50 | - `bytes_sent` 51 | - `frames_read` 52 | - `frames_sent` 53 | - `messages_read` 54 | - `messages_sent` 55 | - `connected_at` 56 | - `closed_at` 57 | - `last_read_at` 58 | - `last_sent_at` 59 | - `last_data_read_at` 60 | - `last_data_sent_at` 61 | 62 | The values are all integers. Keys ending in `_at` all have an UNIX timestamp as value. 63 | 64 | ## `getClients(): array` 65 | 66 | Gets an array with all the client identifiers. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amphp/aerys", 3 | "homepage": "https://github.com/amphp/aerys", 4 | "description": "A non-blocking HTTP/Websocket server", 5 | "keywords": [ 6 | "http", 7 | "websocket", 8 | "server", 9 | "async", 10 | "non-blocking", 11 | "aerys", 12 | "amp", 13 | "amphp" 14 | ], 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Daniel Lowrey", 19 | "email": "rdlowrey@php.net", 20 | "role": "Creator / Lead Developer" 21 | }, 22 | { 23 | "name": "Bob Weinand", 24 | "role": "Developer / API Coordinator" 25 | }, 26 | { 27 | "name": "Niklas Keller", 28 | "role": "Quality Assurance / Developer" 29 | }, 30 | { 31 | "name": "Aaron Piotrowski", 32 | "email": "aaron@trowski.com" 33 | } 34 | ], 35 | "require": { 36 | "php": ">=7", 37 | "amphp/amp": "^2", 38 | "amphp/file": "^0.2 || ^0.3", 39 | "amphp/byte-stream": "^1", 40 | "amphp/socket": "^0.10", 41 | "league/climate": "^3", 42 | "nikic/fast-route": "^1", 43 | "psr/log": "^1", 44 | "ocramius/package-versions": "^1.1" 45 | }, 46 | "require-dev": { 47 | "amphp/phpunit-util": "^1", 48 | "amphp/artax": "^3", 49 | "friendsofphp/php-cs-fixer": "^2.3", 50 | "http2jp/hpack-test-case": "^1", 51 | "phpunit/phpunit": "^6" 52 | }, 53 | "autoload": { 54 | "psr-4": { 55 | "Aerys\\": "lib/" 56 | }, 57 | "files": [ 58 | "lib/functions.php", 59 | "lib/constants.php", 60 | "lib/Internal/functions.php" 61 | ] 62 | }, 63 | "autoload-dev": { 64 | "psr-4": { 65 | "Aerys\\Test\\": "test/" 66 | } 67 | }, 68 | "bin": ["bin/aerys"], 69 | "config": { 70 | "platform": { 71 | "php": "7.0.0" 72 | } 73 | }, 74 | "repositories": [ 75 | { 76 | "type": "package", 77 | "package": { 78 | "name": "http2jp/hpack-test-case", 79 | "version": "1.0", 80 | "source": { 81 | "url": "https://github.com/http2jp/hpack-test-case", 82 | "type": "git", 83 | "reference": "origin/master" 84 | } 85 | } 86 | } 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /docs/classes/internalrequest.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: InternalRequest 3 | permalink: /classes/internalrequest 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | This is a value class exposing the whole data of the clients request via public properties. It is only accessible from within [`Middleware`s](middleware.md) as well as [`HttpDriver`](httpdriver.md). 10 | 11 | Values marked with a **_must_** not be altered in order to not bring the server down. 12 | 13 | ## `$client` 14 | 15 | Holds a reference to the [`Client`](client.md). 16 | 17 | ## `$responseWriter` 18 | 19 | A Generator instance following the [`HttpDriver::writer`](httpdriver.md) protocol. 20 | 21 | ## `$streamId` 22 | 23 | An integer (combined with `$client->id`) providing an unique identifier of a request during its lifetime. 24 | 25 | ## `$trace` 26 | 27 | Literal string trace for HTTP/1, for HTTP/2 an array of [name, value] arrays in the original order. 28 | 29 | ## `$protocol` 30 | 31 | HTTP protocol version string. 32 | 33 | ## `$method` 34 | 35 | HTTP method string. 36 | 37 | ## `$headers` 38 | 39 | Associative array of HTTP headers containing arrays of values. The header field names are always lowercased. (E.g. `["connection" => ["Keep-Alive", "Upgrade"], "host" => ["example.com"]]`) 40 | 41 | ## `$body` 42 | 43 | An instance of [`Message`](https://amphp.org/byte-stream/message). 44 | 45 | ## `$maxBodySize` 46 | 47 | Integer describing the current maximum allowed size. 48 | 49 | Altering this value should be followed by a call to `HttpDriver::upgradeBodySize(InternalRequest)`. 50 | 51 | ## `$uri` 52 | 53 | The URI string consisting of the path and query components. 54 | 55 | ## `$uriScheme` 56 | 57 | The scheme [typically `"http"` or `"https"`] (either from target URI or whether the connection is encrypted or not). 58 | 59 | ## `$uriHost` 60 | 61 | The host string (either from target URI or Host header). 62 | 63 | ## `$uriPort` 64 | 65 | Integer accessed port [may vary from `$client->serverPort`, if client explicitly specified it]. 66 | 67 | ## `$uriPath` 68 | 69 | String path component of the URI. 70 | 71 | ## `$uriQuery` 72 | 73 | String query component of the URI. 74 | 75 | ## `$cookies` 76 | 77 | Cookies array in form of name => value pairs 78 | 79 | ## `$time` 80 | 81 | Unix time at request initialization. 82 | 83 | ## `$httpDate` 84 | 85 | HTTP compatibly formatted date string at request initialization. 86 | 87 | ## `$locals` 88 | 89 | Array with "local" variables, to be used by [`Middleware`s](middleware.md) in combination with [`Request::getLocalVar($key)` and `Request::setLocalVar($key, $value)`](request.md). 90 | -------------------------------------------------------------------------------- /docs/websocket/input.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WebSocket Input 3 | permalink: /websocket/input 4 | --- 5 | 6 | ```php 7 | # This example prints to STDOUT. Do that only for testing purposes! 8 | 9 | class MyWs implements Aerys\Websocket { 10 | public function onStart(Aerys\Websocket\Endpoint $endpoint) { 11 | // $endpoint is for sending 12 | } 13 | 14 | public function onHandshake(Aerys\Request $request, Aerys\Response $response) { 15 | 16 | } 17 | 18 | public function onOpen(int $clientId, $handshakeData) { 19 | print "Successful Handshake for user with client id $clientId\n"; 20 | } 21 | 22 | public function onData(int $clientId, Aerys\Websocket\Message $msg) { 23 | print "User with client id $clientId sent: " . yield $msg . "\n"; 24 | } 25 | 26 | public function onClose(int $clientId, int $code, string $reason) { 27 | print "User with client id $clientId closed connection with code $code\n"; 28 | } 29 | 30 | public function onStop() { 31 | // when server stops, not important for now 32 | } 33 | } 34 | ``` 35 | 36 | ```php 37 | $router = Aerys\router() 38 | ->route('GET', '/ws', Aerys\websocket(new MyWs)); 39 | 40 | $root = Aerys\root(__DIR__ . "/public"); 41 | 42 | return (new Aerys\Host)->use($router)->use($root); 43 | ``` 44 | 45 | ```html 46 | 47 | 64 | ``` 65 | 66 | Each connection is identified by an unique client id, which is passed to `onOpen()`, `onData()` and `onClose()`. 67 | 68 | `onOpen($clientId, $handshakeData)` is called at the moment where the websocket connection has been successfully established (i.e. after the handshake has been sent). For `$handshakeData`, check the [Handshake handling](handshake.md) out. 69 | 70 | `onData($clientId, $msg)` is called upon each received Websocket frame. At the time when `onData()` is called, the message may not yet have been fully received. Thus use `yield $msg` to wait on data to complete. The return value of that `yield` is a string with the full data. 71 | 72 | `onClose($clientId, $code, $reason)` is called when any direction (ingoing or outgoing) of the websocket connection gets closed. 73 | 74 | {:.note} 75 | > Possibly it is not intuitive to have `onData()` called before the full message has been received, but it allows for incremental processing where needed, like large uploads over websockets. See the [usage and performance considerations about this](../performance.md#body). 76 | -------------------------------------------------------------------------------- /docs/websocket/handshake.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WebSocket Handshakes 3 | permalink: /websocket/handshake 4 | --- 5 | 6 | ```php 7 | # This example prints to STDOUT. Do that only for testing purposes! 8 | 9 | class MyWs implements Aerys\Websocket { 10 | private $clients = []; 11 | 12 | public function onStart(Aerys\Websocket\Endpoint $endpoint) { 13 | // $endpoint is necessary for sending 14 | } 15 | 16 | public function onHandshake(Aerys\Request $req, Aerys\Response $res) { 17 | if ($req->getParam("password") != "reallyverysecure") { 18 | # if status set to anything else than 101, no WebSocket connection will be established 19 | $res->setStatus(403); 20 | $res->end("Nope. Valid password required."); 21 | } 22 | # Nothing necessary for successful handshake (though one may set cookies for example) 23 | } 24 | 25 | public function onOpen(int $clientId, $request) { 26 | $this->clients[$clientId] = $request->getConnectionInfo(); 27 | print "Successful Handshake for user with client id $clientId from {$this->clients[$clientId]['client_addr']}\n"; 28 | } 29 | 30 | public function onData(int $clientId, Aerys\Websocket\Message $msg) { 31 | print "User with client id $clientId from {$this->clients[$clientId]['client_addr']} sent: " . (yield $msg) . "\n"; 32 | } 33 | 34 | public function onClose(int $clientId, int $code, string $reason) { 35 | unset($this->clients[$clientId]); 36 | print "User with client id $clientId closed connection with code $code\n"; 37 | } 38 | 39 | public function onStop() { 40 | // when server stops, not important for now 41 | } 42 | } 43 | ``` 44 | 45 | ```php 46 | $router = Aerys\router() 47 | ->route('GET', '/ws', Aerys\websocket(new MyWs)); 48 | 49 | $root = Aerys\root(__DIR__ . "/public"); 50 | 51 | return (new Aerys\Host)->use($router)->use($root); 52 | ``` 53 | 54 | ```html 55 | 56 | 69 | ``` 70 | 71 | `onHandshake($req, $res)` is like a normal request handler, it is the time to determine whether a request shall be successful or not. (E.g. validating a session cookie, a password, ...) 72 | 73 | Setting the status (via `Aerys\Response::setStatus()`) to any other value than 101 prevents establishing the websocket connection and sends a normal HTTP reply back. 74 | 75 | The return value of the `onHandshake()` call is passed as second argument to `onOpen()` in order to allow passing authentication information and assigning it to a $clientId, as there is no clientId yet before the connection has been established. 76 | -------------------------------------------------------------------------------- /docs/websocket/output.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WebSocket Output 3 | permalink: /websocket/output 4 | --- 5 | 6 | ```php 7 | class MiniChat implements Aerys\Websocket { 8 | private $ws; 9 | 10 | public function onStart(Aerys\Websocket\Endpoint $endpoint) { 11 | $this->ws = $endpoint; 12 | } 13 | 14 | public function onHandshake(Aerys\Request $request, Aerys\Response $response) { 15 | // not important for now, can be used to check origin for example 16 | } 17 | 18 | public function onOpen(int $clientId, $handshakeData) { 19 | $this->ws->broadcast("Welcome new client $clientId!"); 20 | } 21 | 22 | public function onData(int $clientId, Aerys\Websocket\Message $msg) { 23 | $text = yield $msg; 24 | $this->ws->send("Message received ... Sending in 5 seconds ...", $clientId); 25 | yield new Amp\Pause(5000); 26 | $this->ws->broadcast("Client $clientId said: $text"); 27 | } 28 | 29 | public function onClose(int $clientId, int $code, string $reason) { 30 | $this->ws->broadcast("User with client id $clientId closed connection with code $code"); 31 | } 32 | 33 | public function onStop() { 34 | // when server stops, not important for now 35 | } 36 | } 37 | ``` 38 | 39 | ```php 40 | $router = Aerys\router() 41 | ->route('GET', '/ws', Aerys\websocket(new MiniChat)) 42 | 43 | $root = Aerys\root(__DIR__ . "/public"); 44 | 45 | return (new Aerys\Host)->use($router)->use($root); 46 | ``` 47 | 48 | ```html 49 | 50 | 71 | ``` 72 | 73 | The `Websocket\Endpoint` interface exposes two important functions: `send()` and `close()`. It is passed inside the `onStart` handler of the `Websocket` interface upon server startup. 74 | 75 | `send($data, $clientId)` expects a string to send to a single client id given as second parameter. 76 | 77 | `close($clientId, $code = Aerys\Websocket\Code::NORMAL_CLOSE, $reason = "")` closes a specific client connection. No further messages should be sent after it. Note that `onClose` is also called after a manual `close()`. 78 | 79 | `broadcast($data, $exceptClientIds = [])` will send a string to every connected client. It is possible to specifically exclude some clients by passing an array of client ids to exclude as second parameter. 80 | 81 | {:.note} 82 | > While it is possible to `send()` a same message to each client, it might be more efficient to just `multicast($data, $clientIds)` an array of clients. 83 | -------------------------------------------------------------------------------- /docs/classes/middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Middleware 3 | permalink: /classes/middleware 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | Middlewares are a powerful tool to intercept requests and manipulate them with low-level access to the [`InternalRequest`](internalrequest.md) instance. 10 | 11 | {:.warning} 12 | > We do not validate anything on the `InternalRequest` instance and objects only accessible through it from outside though. It's a value object with only public properties. It's your responsibility to not fuck up the objects and make things go bad. You can manipulate nearly everything request and client related here - and if you do, make attention to really know what you do. 13 | 14 | {:.note} 15 | > These internal classes only accessible via the `InternalRequest` instance tend to be more volatile regarding their API. Even though we employ semver, we reserve the rights to break these APIs in minors (although not bugfix releases). 16 | 17 | ## `do(InternalRequest): \Generator|null` 18 | 19 | The single method of the `Middleware` interface. It is called each time a request is being dispatched on a host or route this middleware is used on. 20 | 21 | In case this method isn't returning a Generator, there's not much magic to it. Otherwise though: 22 | 23 | One needs to first `yield` once, which will return the headers of the response in an associative array with the header names (the keys) being all lowercase. The value is an array of strings. (This is needed due to things like `set-cookie`, requiring multiple headers with the same name.) 24 | 25 | Further yields will return you the data as a stream of strings, until null is returned to indicate the end of the stream. In between false may be returned, indicating that you should try to flush any data you are temporariliy buffering. 26 | 27 | The first non-null yield must be the headers, then you may yield strings. It's also possible to return instead of yield, in order to finish remove this middleware from the request. 28 | 29 | For internal processing there are a few pseudo-headers in response, namely `":status"` and `":reason"`. 30 | 31 | ## Example 32 | 33 | Assuming the functions being methods of a class implementing `Middleware`. 34 | 35 | ```php 36 | function do(Aerys\InternalRequest $ireq) { 37 | // add a dot after each byte when client specified X-INSERT-DOT header 38 | if (!empty($ireq->headers["x-insert-dot"][0])) { // header names are lowercase 39 | return; // no header, no processing 40 | } 41 | 42 | $headers = yield; // we may also manipulate $headers before we return it 43 | 44 | if ($headers[":status"] != 200) { // only bother with successful 200 OK requests 45 | return $headers; // at these points we stop the middleware processing 46 | } 47 | 48 | $data = yield $headers; 49 | do { 50 | $processed = implode(".", [-1 => ""] + str_split($data)); 51 | } while (($data = yield $processed) !== null); 52 | /* yup, the yield may return false, but it's coerced to "" when used as string, 53 | * so it doesn't matter here. */ 54 | 55 | return "."; // and a final dot! 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/http-advanced.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advanced HTTP APIs 3 | permalink: /http-advanced 4 | --- 5 | ## Streaming Responses 6 | 7 | ```php 8 | $db = new Amp\Mysql\Pool("host=localhost;user=user;pass=pass;db=db"); 9 | return (new Aerys\Host)->use(function(Aerys\Request $req, Aerys\Response $res) use ($db) { 10 | $result = yield $db->prepare("SELECT data FROM table WHERE key = ?", [$req->getParam("key") ?? "default"]); 11 | while ($row = yield $result->fetchObject()) { 12 | $res->write($row->data); 13 | $res->write("\n"); 14 | $res->flush(); 15 | } 16 | $res->end(); # is implicit if streaming has been started, but useful to signal end of data to wait on other things now 17 | }); 18 | ``` 19 | 20 | `Response::write($data)` is an useful API to incrementally send data. 21 | 22 | This does *not* guarantee that data is immediately sent; it may be buffered temporarily for performance or implementation reasons [example: the http driver may buffer up to `Options->outputBufferSize` bytes to reduce number of TCP frames]. 23 | 24 | There is a `Response::flush()` method which actually flushes all the buffers immediately. 25 | 26 | ## Pushing Resources 27 | 28 | ```php 29 | return (new Aerys\Host) 30 | ->use(Aerys\root("/path/to/folder")) # contains image.png 31 | ->use(function(Aerys\Request $req, Aerys\Response $res) { 32 | $res->push("/image.png"); 33 | $res->end('A nice image:
'); 34 | }) 35 | ; 36 | ``` 37 | 38 | `Response::push(string $uri, array $headers = null)` dispatches a push promise (with HTTP/2; with HTTP/1 only a `Link` header with a `preload` directive is sent). 39 | 40 | Push promises are a powerful tool to reduce latencies and provide a better experience. When pushing, an internal request is dispatched just like it were requested by a client. 41 | 42 | If the `$headers` parameter is `null`, certain headers are copied from the original request to match it as closely as possible. 43 | 44 | ## Managing Routes 45 | 46 | ```php 47 | # This is the foo/router.php file 48 | return Aerys\router() 49 | ->route("GET", "/", function(Aerys\Request $req, Aerys\Response $res) { $res->end("to-be-prefixed root"); }) 50 | ->use(function(Aerys\Request $req, Aerys\Response $res) { $res->end("fallback route, only for this router"); })) 51 | ; 52 | ``` 53 | 54 | ```php 55 | $realRouter = Aerys\router() 56 | ->use((include "foo/router.php")->prefix("/foo")) 57 | ->route("GET", "/", function(Aerys\Request $req, Aerys\Response $res) { $res->end("real root"); }) 58 | ->use(function(Aerys\Request $req, Aerys\Response $res) { $res->end("general fallback route"); })) 59 | ; 60 | 61 | return (new Aerys\Host)->use($realRouter); 62 | ``` 63 | 64 | A `Router` can also `use()` `Bootable`s, `callable`s, `Middleware`s _and_ other `Router` instances. 65 | 66 | This gives a certain flexibility allowing merging router definitions, easy definition of a common fallback callable or middleware for a group of routes. 67 | 68 | For that purpose `Router::prefix($prefix)` exists, it allows to prefix all the routes with a certain `$prefix`. 69 | -------------------------------------------------------------------------------- /lib/IpcLogger.php: -------------------------------------------------------------------------------- 1 | setAnsify($console->getArg("color")); 20 | $level = $console->getArg("log"); 21 | $level = isset(self::LEVELS[$level]) ? self::LEVELS[$level] : $level; 22 | $this->setOutputLevel($level); 23 | 24 | $onWritable = $this->callableFromInstanceMethod("onWritable"); 25 | $this->ipcSock = $ipcSock; 26 | stream_set_blocking($ipcSock, false); 27 | $this->writeWatcherId = Loop::onWritable($ipcSock, $onWritable); 28 | Loop::disable($this->writeWatcherId); 29 | } 30 | 31 | protected function output(string $message) { 32 | if (empty($this->isDead)) { 33 | $msg = pack("N", \strlen($message)) . $message; 34 | if ($this->pendingQueue === null) { 35 | $this->writeQueue[] = $msg; 36 | Loop::enable($this->writeWatcherId); 37 | } else { 38 | $this->pendingQueue[] = $msg; 39 | } 40 | } 41 | } 42 | 43 | private function onWritable() { 44 | if ($this->isDead) { 45 | return; 46 | } 47 | 48 | if ($this->writeBuffer === "") { 49 | $this->writeBuffer = implode("", $this->writeQueue); 50 | $this->writeQueue = []; 51 | } 52 | 53 | $eh = set_error_handler([$this, 'onDeadIpcSock']); 54 | $bytes = fwrite($this->ipcSock, $this->writeBuffer); 55 | set_error_handler($eh); 56 | if ($bytes === false) { 57 | $this->onDeadIpcSock(); 58 | return; 59 | } 60 | 61 | if ($bytes !== \strlen($this->writeBuffer)) { 62 | $this->writeBuffer = substr($this->writeBuffer, $bytes); 63 | return; 64 | } 65 | 66 | if ($this->writeQueue) { 67 | $this->writeBuffer = implode("", $this->writeQueue); 68 | $this->writeQueue = []; 69 | return; 70 | } 71 | 72 | $this->writeBuffer = ""; 73 | Loop::disable($this->writeWatcherId); 74 | if ($deferred = $this->writeDeferred) { 75 | $this->writeDeferred = null; 76 | $deferred->resolve(); 77 | } 78 | } 79 | 80 | private function onDeadIpcSock() { 81 | $this->isDead = true; 82 | $this->writeBuffer = ""; 83 | $this->writeQueue = []; 84 | Loop::cancel($this->writeWatcherId); 85 | } 86 | 87 | public function flush() { // BLOCKING 88 | if ($this->isDead || ($this->writeBuffer === "" && empty($this->writeQueue))) { 89 | return; 90 | } 91 | 92 | stream_set_blocking($this->ipcSock, true); 93 | $this->onWritable(); 94 | stream_set_blocking($this->ipcSock, false); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docs/classes/request.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Request 3 | permalink: /classes/request 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | The `Request` interface generally finds its only use in responder callables (or [`Websocket::onOpen()`](websocket.html#onopenint-clientid-handshakedata)). [`Middleware`s](middleware.md) do never see the `Request`; the `StandardRequest` class is supposed to be a simple request API reading from and manipulating [`InternalRequest`](internalrequest.md) under the hood. 10 | 11 | ## `getMethod(): string` 12 | 13 | Returns the used method, e.g. `"GET"`. 14 | 15 | ## `getUri(): string` 16 | 17 | Returns the requested URI (path and query string), e.g. `"/foo?bar"`. 18 | 19 | ## `getProtocolVersion(): string` 20 | 21 | Currently it will return one of the three supported versions: `"1.0"`, `"1.1"` or `"2.0"`. 22 | 23 | ## `getHeader(string): string | null` 24 | 25 | Gets the first value of all the headers with that name. 26 | 27 | ## `getHeaderArray(string): array` 28 | 29 | Gets an array with headers. HTTP allows for multiple headers with the same name, so this returns an array. Usually only a single header is needed and expected, in this case there is `getHeader()`. 30 | 31 | ## `getAllHeaders(): array>` 32 | 33 | Returns all the headers in an associative map with the keys being normalized header names in lowercase. 34 | 35 | ## `getParam(string): string | null` 36 | 37 | Gets the first value of all the query string parameters with that name. 38 | 39 | ## `getParamArray(string): array` 40 | 41 | Gets an array with the values of the query string parameters with that name. 42 | 43 | ## `getAllParams(): array>` 44 | 45 | Gets the decoded query string as associative array. 46 | 47 | ## `getBody(int): Amp\ByteStream\Message` 48 | 49 | Returns a representation of the request body. The [`Amp\ByteStream\Message`](http://amphp.org/byte-stream/message) can be `yield`ed to get the actual string. 50 | 51 | The parameter `$bodySize` defaults to `-1` to take the globally configured maximum, otherwise it's the maximum body size. 52 | 53 | There also exists a [`parseBody()`](parsedbody.md) function for processing of a typical HTTP form data. 54 | 55 | ## `getCookie(string): string | null` 56 | 57 | Gets a cookie value by name. 58 | 59 | ## `getConnectionInfo(): array` 60 | 61 | Returns various information about the request, a map of the array is: 62 | 63 | ```php 64 | [ 65 | "client_port" => int, 66 | "client_addr" => string, 67 | "server_port" => int, 68 | "server_addr" => string, 69 | "is_encrypted" => bool, 70 | "crypto_info" => array, # Like returned via stream_get_meta_data($socket)["crypto"] 71 | ] 72 | ``` 73 | 74 | ## `getLocalVar(string)` / `setLocalVar(string, $value)` 75 | 76 | These methods are only important when using [`Middleware`s](middleware.md). They manipulate the [`InternalRequest->locals`](internalrequest.html#locals) array. 77 | 78 | ## `getOption(string)` 79 | 80 | Gets an [option](options.md) value. 81 | 82 | ## `StandardRequest::__construct(InternalRequest)` 83 | 84 | The constructor accepts an [`InternalRequest`](internalrequest.md) object the `StandardRequest` class is reading and writing to. 85 | 86 | {:.note} 87 | > It may be helpful in integration tests to provide a `StandardRequest` class initialized with an adequately preset `InternalRequest` object. 88 | -------------------------------------------------------------------------------- /lib/Websocket/Endpoint.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function send(string $data, int $clientId): Promise; 17 | 18 | /** 19 | * Broadcast a UTF-8 text message to all clients (except those given in the optional array). 20 | * 21 | * @param string $data Data to send. 22 | * @param int[] $exceptIds List of IDs to exclude from the broadcast. 23 | * 24 | * @return \Amp\Promise 25 | */ 26 | public function broadcast(string $data, array $exceptIds = []): Promise; 27 | 28 | /** 29 | * Send a UTF-8 text message to a set of clients. 30 | * 31 | * @param string $data Data to send. 32 | * @param int[]|null $clientIds Array of client IDs. 33 | * 34 | * @return \Amp\Promise 35 | */ 36 | public function multicast(string $data, array $clientIds): Promise; 37 | 38 | /** 39 | * Send a binary message to the given client(s). 40 | * 41 | * @param string $data Data to send. 42 | * @param int $clientId 43 | * 44 | * @return \Amp\Promise 45 | */ 46 | public function sendBinary(string $data, int $clientId): Promise; 47 | 48 | /** 49 | * Send a binary message to all clients (except those given in the optional array). 50 | * 51 | * @param string $data Data to send. 52 | * @param int[] $exceptIds List of IDs to exclude from the broadcast. 53 | * 54 | * @return \Amp\Promise 55 | */ 56 | public function broadcastBinary(string $data, array $exceptIds = []): Promise; 57 | 58 | /** 59 | * Send a binary message to a set of clients. 60 | * 61 | * @param string $data Data to send. 62 | * @param int[]|null $clientIds Array of client IDs. 63 | * 64 | * @return \Amp\Promise 65 | */ 66 | public function multicastBinary(string $data, array $clientIds): Promise; 67 | 68 | /** 69 | * Close the client connection with a code and UTF-8 string reason. 70 | * 71 | * @param int $clientId 72 | * @param int $code 73 | * @param string $reason 74 | */ 75 | public function close(int $clientId, int $code = Code::NORMAL_CLOSE, string $reason = ""); 76 | 77 | /** 78 | * @param int $clientId 79 | * 80 | * @return array [ 81 | * 'bytes_read' => int, 82 | * 'bytes_sent' => int, 83 | * 'frames_read' => int, 84 | * 'frames_sent' => int, 85 | * 'messages_read' => int, 86 | * 'messages_sent' => int, 87 | * 'connected_at' => int, 88 | * 'closed_at' => int, 89 | * 'last_read_at' => int, 90 | * 'last_send_at' => int, 91 | * 'last_data_read_at' => int, 92 | * 'last_data_sent_at' => int, 93 | * ] 94 | */ 95 | public function getInfo(int $clientId): array; 96 | 97 | /** 98 | * @return int[] Array of client IDs. 99 | */ 100 | public function getClients(): array; 101 | } 102 | -------------------------------------------------------------------------------- /lib/Websocket.php: -------------------------------------------------------------------------------- 1 | † **_must_** not be altered in order to not bring the server down. 8 | 9 | `$id` | An unique client id (unique as long as the client object is alive). 10 | `$socket` | The client socket resource. 11 | `$clientAddr` | The IP address of the client. 12 | `$clientPort` | The port of the client. 13 | `$serverAddr` | The IP address this server was accessed at. 14 | `$serverPort` | The port the client connected to. 15 | `$isEncrypted` | Whether the stream is encrypted 16 | `$cryptoInfo` | Only relevant if `$isEncrypted == true`. Is equivalent to the `stream_get_meta_data($socket)["crypto"]` array. 17 | `$requestParser` | Is a Generator returned by [`HttpDriver::parser()`](httpdriver.md). 18 | `$readWatcher` | The read watcher identifier returned by `Amp\onReadable` for the `$socket`. May be disabled or enabled to stop or resume reading from it, especially in [`HttpDriver`](httpdriver.md). 19 | `$writeWatcher` | The write watcher identifier returned by `Amp\onWritable` for the `$socket`. 20 | `$writeBuffer` | The data pending to be written to the `$socket`. The Server will remove data from this buffer as soon as they're written. 21 | `$bufferSize` | Size of the internal buffers, supposed to be compared against `$options->softStreamCap`. It is decreased by the Server upon each successful `fwrite()` by the amount of written bytes. 22 | `$bufferDeferred` | Eventually containing a `Deferred` when `$bufferSize` is exceeding `$options->softStreamCap`. 23 | `$onWriteDrain` | A callable for when the `$writeBuffer` will be empty again. [The `Server` may overwrite it.] 24 | `$shouldClose` | Boolean whether the next request will terminate the connection. 25 | `$isDead` | One of `0`, `Client::CLOSED_RD`, `Client::CLOSED_WR` or `Client::CLOSED_RDWR`, where `Client::CLOSED_RDWR === Client::CLOSED_RD | Client::CLOSED_WR` indicating whether read or write streams are closed (or both). 26 | `$isExported` | Boolean whether the `$export` callable has been called. 27 | `$remainingRequests` | Number of remaining requests until the connection will be forcefully killed. 28 | `$pendingResponses` | The number of responses not yet completely replied to. 29 | `$options` | The [`Options`](options.md) instance. 30 | `$httpDriver` | The [`HttpDriver`](httpdriver.md) instance used by the client. 31 | `$exporter` | A callable requiring the `Client` object as first argument. It unregisters the client from the [`Server`](server.md) and returns a callable, which, when called, decrements counters related to rate-limiting. (Unstable, may be altered in near future) 32 | `$bodyEmitters` | An array of `Emitter`s whose `Iterator`s have been passed to [`InternalRequest->body`](internalrequest.md). You may `fail()` **and then** `unset()` them. If the `$client->bodyPromisors[$internalRequest->streamId]` entry exists, this means the body is still being processed. 33 | `$parserEmitLock` | A boolean available for use by a [`HttpDriver`](httpdriver.md) instance (to regulate parser halts and avoid resuming already active Generators). 34 | `$allowsPush` | Boolean whether the client allows push promises (HTTP/2 only). 35 | -------------------------------------------------------------------------------- /lib/constants.php: -------------------------------------------------------------------------------- 1 | "Continue", 10 | 101 => "Switching Protocols", 11 | 200 => "OK", 12 | 201 => "Created", 13 | 202 => "Accepted", 14 | 203 => "Non-Authoritative Information", 15 | 204 => "No Content", 16 | 205 => "Reset Content", 17 | 206 => "Partial Content", 18 | 300 => "Multiple Choices", 19 | 301 => "Moved Permanently", 20 | 302 => "Found", 21 | 303 => "See Other", 22 | 304 => "Not Modified", 23 | 305 => "Use Proxy", 24 | 307 => "Temporary Redirect", 25 | 400 => "Bad Request", 26 | 401 => "Unauthorized", 27 | 402 => "Payment Required", 28 | 403 => "Forbidden", 29 | 404 => "Not Found", 30 | 405 => "Method Not Allowed", 31 | 406 => "Not Acceptable", 32 | 407 => "Proxy Authentication Required", 33 | 408 => "Request Timeout", 34 | 409 => "Conflict", 35 | 410 => "Gone", 36 | 411 => "Length Required", 37 | 412 => "Precondition Failed", 38 | 413 => "Request Entity Too Large", 39 | 414 => "Request URI Too Long", 40 | 415 => "Unsupported Media Type", 41 | 416 => "Requested Range Not Satisfiable", 42 | 417 => "Expectation Failed", 43 | 418 => "I'm A Teapot", 44 | 426 => "Upgrade Required", 45 | 428 => "Precondition Required", 46 | 429 => "Too Many Requests", 47 | 431 => "Request Header Fields Too Large", 48 | 500 => "Internal Server Error", 49 | 501 => "Not Implemented", 50 | 502 => "Bad Gateway", 51 | 503 => "Service Unavailable", 52 | 504 => "Gateway Timeout", 53 | 505 => "HTTP Version Not Supported", 54 | 511 => "Network Authentication Required", 55 | ]; 56 | const HTTP_STATUS = [ 57 | "ACCEPTED" => 202, 58 | "BAD_GATEWAY" => 502, 59 | "BAD_REQUEST" => 400, 60 | "CONFLICT" => 409, 61 | "CONTINUE" => 100, 62 | "CREATED" => 201, 63 | "EXPECTATION_FAILED" => 417, 64 | "FORBIDDEN" => 403, 65 | "FOUND" => 302, 66 | "GATEWAY_TIMEOUT" => 504, 67 | "GONE" => 410, 68 | "HTTP_VERSION_NOT_SUPPORTED" => 505, 69 | "INTERNAL_SERVER_ERROR" => 500, 70 | "LENGTH_REQUIRED" => 411, 71 | "METHOD_NOT_ALLOWED" => 405, 72 | "MOVED_PERMANENTLY" => 301, 73 | "MULTIPLE_CHOICES" => 300, 74 | "NETWORK_AUTHENTICATION_REQUIRED" => 511, 75 | "NON_AUTHORITATIVE_INFORMATION" => 203, 76 | "NOT_ACCEPTABLE" => 406, 77 | "NOT_FOUND" => 404, 78 | "NOT_IMPLEMENTED" => 501, 79 | "NOT_MODIFIED" => 304, 80 | "NO_CONTENT" => 204, 81 | "OK" => 200, 82 | "PARTIAL_CONTENT" => 206, 83 | "PAYMENT_REQUIRED" => 402, 84 | "PRECONDITION_FAILED" => 412, 85 | "PRECONDITION_REQUIRED" => 428, 86 | "PROXY_AUTHENTICATION_REQUIRED" => 407, 87 | "REQUESTED_RANGE_NOT_SATISFIABLE" => 416, 88 | "REQUEST_ENTITY_TOO_LARGE" => 413, 89 | "REQUEST_HEADER_FIELDS_TOO_LARGE" => 431, 90 | "REQUEST_TIMEOUT" => 408, 91 | "REQUEST_URI_TOO_LONG" => 414, 92 | "RESET_CONTENT" => 205, 93 | "SEE_OTHER" => 303, 94 | "SERVICE_UNAVAILABLE" => 503, 95 | "SWITCHING_PROTOCOLS" => 101, 96 | "TEAPOT" => 418, 97 | "TEMPORARY_REDIRECT" => 307, 98 | "TOO_MANY_REQUESTS" => 429, 99 | "UNAUTHORIZED" => 401, 100 | "UNSUPPORTED_MEDIA_TYPE" => 415, 101 | "UPGRADE_REQUIRED" => 426, 102 | "USE_PROXY" => 305, 103 | ]; 104 | -------------------------------------------------------------------------------- /lib/WorkerProcess.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 18 | $this->ipcSock = $ipcSock; 19 | } 20 | 21 | protected function doStart(Console $console): \Generator { 22 | // Shutdown the whole server in case we needed to stop during startup 23 | register_shutdown_function(function () use ($console) { 24 | if (!$this->server) { 25 | // ensure a clean reactor for clean shutdown 26 | Loop::run(function () use ($console) { 27 | yield (new CommandClient((string) $console->getArg("config")))->stop(); 28 | }); 29 | } 30 | }); 31 | 32 | $server = yield from Internal\bootServer($this->logger, $console); 33 | if (\extension_loaded("sockets") && PHP_VERSION_ID > 70007 && stripos(PHP_OS, "WIN") !== 0) { 34 | // needs socket_export_stream() and no Windows 35 | $config = (string) $console->getArg("config"); 36 | $getSocketTransferImporter = function () use ($config, &$socketTransfer) { 37 | return $socketTransfer ?? $socketTransfer = [new CommandClient($config), "importServerSockets"]; 38 | }; 39 | 40 | if (in_array(strtolower(PHP_OS), ["darwin", "freebsd"])) { // the OS not supporting *round-robin* so_reuseport client distribution 41 | $importer = $getSocketTransferImporter(); 42 | } else { 43 | // prefer local so_reuseport binding in favor of transferring tcp server sockets (i.e. only use socket transfer for unix domain sockets) 44 | $importer = \Amp\coroutine(function ($addrContextMap, $socketBinder) use ($getSocketTransferImporter) { 45 | $boundSockets = $unixAddrContextMap = []; 46 | 47 | foreach ($addrContextMap as $address => $context) { 48 | if (!strncmp("unix://", $address, 7)) { 49 | $unixAddrContextMap[$address] = $context; 50 | } else { 51 | $boundSockets[$address] = $socketBinder($address, $context); 52 | } 53 | } 54 | 55 | return $unixAddrContextMap ? $boundSockets + yield $getSocketTransferImporter()($unixAddrContextMap) : $boundSockets; 56 | }); 57 | } 58 | 59 | yield $server->start($importer); 60 | } else { 61 | yield $server->start(); 62 | } 63 | $this->server = $server; 64 | Loop::unreference(Loop::onReadable($this->ipcSock, function ($watcherId) { 65 | $this->logger->info("Received stop command"); 66 | Loop::cancel($watcherId); 67 | yield from $this->stop(); 68 | })); 69 | yield (new CommandClient($console->getArg("config")))->started(); 70 | } 71 | 72 | protected function doStop(): \Generator { 73 | if ($this->server) { 74 | yield $this->server->stop(); 75 | } 76 | if (\method_exists($this->logger, "flush")) { 77 | $this->logger->flush(); 78 | } 79 | } 80 | 81 | protected function exit() { 82 | if (\method_exists($this->logger, "flush")) { 83 | $this->logger->flush(); 84 | } 85 | parent::exit(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /docs/classes/response.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Response 3 | permalink: /classes/response 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | The `Response` interface (extends `\Amp\ByteStream\OutputStream`) generally finds its only use in responder callables (or [`Websocket::onOpen()`](websocket.md#onopenint-clientid-handshakedata)). [`Middleware`s](middleware.md) do never see the `Response`; the `StandardResponse` class is communicating headers, data and flushes to a Generator under the hood. 10 | 11 | ## `setStatus(int $code): Response` 12 | 13 | Sets the numeric HTTP status code (between 100 and 599). 14 | 15 | If not assigned this value defaults to 200. 16 | 17 | ## `setReason(string $phrase): Response` 18 | 19 | Sets the optional HTTP reason phrase. 20 | 21 | ## `addHeader(string $field, string $value): Response` 22 | 23 | Appends the specified header. 24 | 25 | ## `setHeader(string $field, string $value): Response` 26 | 27 | Sets the specified header. 28 | 29 | This method will replace any existing headers for the specified field. 30 | 31 | ## `setCookie(string $name, string $value, array $flags = []): Response` 32 | 33 | Provides an easy API to set cookie headers. 34 | 35 | Those who prefer using addHeader() may do so. 36 | 37 | Valid `$flags` are per [RFC 6265](https://tools.ietf.org/html/rfc6265#section-5.2.1): 38 | 39 | - `"Expires" => date("r", $timestamp)` - A timestamp when the cookie will become invalid (set to a date in the past to delete it) 40 | - `"Max-Age" => $seconds` - A number in seconds when the cookie must be expired by the client 41 | - `"Domain" => $domain` - The domain where the cookie is available 42 | - `"Path" => $path` - The path the cookie is restricted to 43 | - `"Secure"` - Only send this cookie to the server over TLS 44 | - `"HttpOnly"` - The client must hide this cookie from any scripts (e.g. Javascript) 45 | 46 | ## `write(string $partialBodyChunk): \Amp\Promise` 47 | 48 | Incrementally streams parts of the response body. 49 | 50 | Applications that can afford to buffer an entire response in memory or can wait for all body data to generate may use `Response::end()` to output the entire response in a single call. 51 | 52 | {:.note} 53 | > Headers are sent upon the first invocation of Response::write(). 54 | 55 | ## `flush()` 56 | 57 | Forces a flush message [`false` inside `Middleware`s and `HttpDriver`] to be propagated and any buffers forwarded to the client. 58 | 59 | Calling this method only makes sense when streaming output via `Response::write()`. Invoking it before calling `write()` or after `end()` is a logic error. 60 | 61 | ## `end(string $finalBodyChunk = null)` 62 | 63 | End any streaming response output with an optional final message by `$finalBodyChunk`. 64 | 65 | User applications are **not** required to call `Response::end()` after streaming or sending response data (though it's not incorrect to do so) — the server will automatically call `end()` as needed. 66 | 67 | Passing the optional `$finalBodyChunk` parameter is a shortcut equivalent to 68 | the following: 69 | 70 | $response->write($finalBodyChunk); 71 | $response->end(); 72 | 73 | {:.note} 74 | > Thus it is also fine to call this function without previous `write()` calls, to send it all at once. 75 | 76 | ## `push(string $url, array $headers = null): Response` 77 | 78 | Indicate resources which a client very likely needs to fetch. (e.g. `Link: preload` header or HTTP/2 Push Promises) 79 | 80 | If a push promise is actually being sent, it will be dispatched with the `$headers` if not `null`, else the server will try to reuse some headers from the request 81 | 82 | ## `state(): int` 83 | 84 | Retrieves the current response state 85 | 86 | The response state is a bitmask of the following flags: 87 | 88 | - `Response::NONE` 89 | - `Response::STARTED` 90 | - `Response::STREAMING` 91 | - `Response::ENDED` 92 | -------------------------------------------------------------------------------- /docs/classes/httpdriver.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: HttpDriver 3 | permalink: /classes/httpdriver 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | `HttpDriver` is an interface managing the raw input from and to the client socket directly. 10 | 11 | It is only possible to have one driver per **port**. There are some few possible applications of it (e.g. a PROXY protocol wrapping HTTP/1.1 communication). There are currently two classes implementing it `Http1Driver` (for HTTP/1) and `Http2Driver` (for HTTP/2). 12 | 13 | ## `setup(array $parseEmitters, callable $responseWriter)` 14 | 15 | Called upon initialization (possibly even before [`Bootable::boot`](bootable.md) was called). 16 | 17 | `$parseEmitter` is an array of `callable`s, keyed by `HttpDriver` constants. 18 | 19 | When an instance of `InternalRequest` is passed, in particular `client`, `headers`, `method`, `protocol`, `trace` and `uri*` properties should be initialized. 20 | 21 | Several callables have an optional `$streamId` parameter. If only one request is handled simultaneously on a same connection, this parameter can be ignored, otherwise one must set it to the same value than `InternalRequest->streamId` to enable the server to feed the body data to the right request. 22 | 23 | Depending on the callback type, different signatures are expected: 24 | 25 | - `HttpDriver::RESULT`: the request has no entity body. Expects an `InternalRequest` as only parameter. 26 | - `HttpDriver::ENTITY_HEADERS`: the request will be followed by subsequent body (`HttpDriver::ENTITY_PART` / `HttpDriver::ENITITY_RESULT`). Expects `InternalRequest` as only parameter. 27 | - `HttpDriver::ENTITY_PART`: contains the next part of the entity body. The signature is `(Client, string $body, int $streamId = 0)` 28 | - `HttpDriver::ENTITY_RESULT`: signals the end of the entity body. The signature is `(Client, int $streamId = 0)`. 29 | - `HttpDriver::SIZE_WARNING`: to be used when the body size exceeds the current size limits (by default `Options->maxBodySize`, might have been upgraded via `upgradeBodySize()`). Before emitting this, all the data up to the limit **must** be emitted via `HttpDriver::ENTITY_PART` first. The signature is `(Client, int $streamId = 0)`. 30 | - `HttpDriver::ERROR`: signals a protocol error. Here are two additional trailing arguments to this callback: a HTTP status code followed by a string error message. The signature is `(Client, int $status, string $message)`. 31 | 32 | `$responseWriter` is a `callable(Client $client, bool $final = false)`, supposed to be called after updates to [`$client->writeBuffer`](client.md) with the `$final` parameter signaling the response end [this is important for managing timeouts and counters]. 33 | 34 | ## `filters(InternalRequest $ireq, array $userFilters): array` 35 | 36 | Returns an array of callables working according to the [`Middleware`](middleware.md) protocol. [Not actual `Middleware` instances, but only the direct `callable`!] 37 | 38 | ## `writer(InternalRequest $ireq): \Generator` 39 | 40 | The Generator is receiving with the first `yield` an array with the headers containing a map of field name to array of string values (pseudo-headers starting with a colon do not map to an array, but directly to a value). 41 | 42 | Subsequent `yield`s are string data with eventual intermittent `false` to signal flushing. 43 | 44 | Then a final `null` will be returned by `yield`. 45 | 46 | ## `upgradeBodySize(InternalRequest $ireq)` 47 | 48 | May be called any time the body size limits wish to be increased. 49 | 50 | It should take the necessary measures so that further `HttpDriver::ENTITY_PART` may be sent. 51 | 52 | ## `parser(Client $client): \Generator` 53 | 54 | Inside the parser `yield` always returns raw string data from the socket. 55 | 56 | {:.note} 57 | > You _can_ rely on keep-alive timeout terminating the `\Amp\ByteStream\Message` with a `ClientException`, when no further data comes in. No need to manually handle that here. 58 | -------------------------------------------------------------------------------- /docs/classes/host.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Host 3 | permalink: /classes/host 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | Hosts are the most fundamental entity of configuration; they describe how Aerys can be reached and what it dispatches to. Its functions in general return $this, so one can easily chain calls. 10 | 11 | ## `use(Middleware | Bootable | callable(Request, Response) | Monitor | HttpDriver)` 12 | 13 | The way everything is attached to the Host. Currently it accepts `Bootable`s, `Middleware`s, callables (the passed argument can also be all three at the same time) and `Monitor`s or a `HttpDriver` instance. 14 | 15 | When the server is run `Bootable`s, `Middleware`s and callables are called in the order they are passed to `use()`. The `Bootable`s are all called extacly once right before the Server is started. `Middleware`s are all invoked each time before the callables are invoked. Then the callables are invoked one after the other *until* the response has been started - the remaining callables are ignored. 16 | 17 | {:.note} 18 | > Be careful with `Middleware`s, only use them if you really need them. They are expensive as they're called at **each** request. You also can use route-specific `Middleware`s to only invoke them when needed. 19 | 20 | {:.note} 21 | > There can be only **one** HttpDriver instance per **port** (or UNIX domain socket). That means, if you have multiple `Host` instances listening on the same port, they all need to share the same `HttpDriver` instance! 22 | 23 | See also the documentation for [`Middleware`s](middleware.md) and [`Bootable`s](bootable.md). 24 | 25 | ## `name(string)` 26 | 27 | A name for non-wildcard domains. Like `"www.example.com"`. There only may be one single wildcard Host per interface. All the other `Host`s must have a name specified. 28 | 29 | For the case where the actual port does not match the port specified in the `Host` header, it is possible to append `:port` where `port` is either the port number to match against, or a wildcard `"*"` to allow any port. 30 | 31 | For example: 32 | 33 | - `"*:8080"` to specify a wildcard host, where requests must provide a `Host` header adressing port 8080, 34 | - or `"localhost:*"` specifying a host, which must be addressed on `"localhost"`, but no port matching will happen. 35 | 36 | ## `expose(string $address, int $port = 0)` 37 | 38 | You can specify interfaces the server should listen on with IP and port. By default, if `expose()` is never called, it listens on all IPv4 and IPv6 interfaces on port 80, or 443 if encryption is enabled, basically an implicit `expose("*", $https ? 443 : 80)`. The port number must be between 1 and 65535. 39 | 40 | The generic addresses for IPv4 is `"0.0.0.0"`, for IPv6 it is `"::"` and `"*"` for both IPv4 and IPv6. 41 | 42 | You also can specify a filesystem path as address. The server will then create an UNIX domain socket at that location. The port number must be 0. 43 | 44 | ## `encrypt(string $certificatePath, string $keyPath = null, array $additionalSslSettings = [])` 45 | 46 | This needs to be set on every `Host` which wants to use https. You may not have both encrypted and unencrypted hosts listening on the same interface and port combination. 47 | 48 | The `$keyPath` may be set to `null` if the certificate file also contains the private key. 49 | 50 | The `$additionalSslSettings` array is passed directly as SSL context options and thus equivalent to what is specified by the PHP documentation at [http://php.net/context.ssl](http://php.net/context.ssl). The `$certificatePath` and `$keyPath` parameters are equivalent to the `local_cert` and `local_pk` options, respectively. 51 | 52 | ## Example 53 | 54 | ```php 55 | return (new Aerys\Host) 56 | ->expose("127.0.0.1", 80) // Yup, this is the only host here, 57 | ->name("localhost") // so expose() and name() aren't necessary 58 | ->use(function(Aerys\Request $req, Aerys\Response $res) { 59 | $res->end("

Hello world!

"); 60 | }); 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/production.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Running on Production 3 | permalink: /production 4 | --- 5 | ## General 6 | 7 | - Set your `ulimit -n` (maximum open file descriptors) high enough to manage all your connections. Recommended is at least `$workers * (Options->maxConnections + 100)`. [100 is an arbitrary number usually big enough for all the persisting file descriptors. If not enough, add more.] 8 | - Ratelimit the number of connections from a single IP (at least if you have no clever load-balancer) via for example iptables, to avoid too many connections being dropped off. Be aware that websocket and HTTP/2 connections are persistent. It's recommended to carefully balance the maximum connections per IP (proxys!) and the maxConnections option. It just is a simple layer of security against trivial DoS attacks, but won't help against DDoS, which will be able to just hold all the connections open. 9 | - In case you are using a properly configured load-balancer in front of Aerys servers, you should set the number of connections near to the maximum the host system can handle. 10 | - Aerys has a file server, which isn't too bad (use libuv if you use it!), but for heavy loads, a CDN is recommended. 11 | - Avoid a low `memory_limit` setting, it is one of the few things able to kill the server ungracefully. If you have a memory leak, fix it, instead of relying on the master process to restart it. 12 | 13 | ## Options 14 | 15 | Defaults are chosen in a moderate way between security and performance on a typical machine. 16 | 17 | - `maxConnections` is important to prevent the server from going out of memory in combination with maximum body and header size and (for HTTP/2) `maxConcurrentStreams` option. 18 | - `maxBodySize` is recommended to be set to the lowest necessary for your application. If it is too high, people may fill your memory with useless data. (It is always possible to increase it at runtime, see [usage of Request::getBody($size)](classes/request#getbodyint-ampbytestreammessage).) 19 | - `maxHeaderSize` should never need to be touched except if you have 50 KB of cookies ... 20 | - `softStreamCap` is a limit where `Response::write()` returns an unresolved Promise until buffer is empty enough again. If you do not have much memory, consider lowering it, if you have enough, possibly set it a bit higher. It is not recommended to have it higher than available memory divided by `maxConnections` and `maxStreams` and 2 (example: for 8 GB memory, 256 KB buffer is fine). [Should be a multiple of `outputBufferSize`.] 21 | - `maxConcurrentStreams` is the maximum of concurrent HTTP/2 streams on a single connection. Do not set it too high (but neither too low to not limit concurrency) to avoid trivial attacks. 22 | - `maxFramesPerSecond` is the maximum number of frames a HTTP/2 client may send per second before being throttled. Do not set it too high (but neither too low to not limit concurrency) to avoid attacks consisting of many tiny frames. 23 | - `maxInputVars` limits the number of input vars processed by `Response` and `BodyParser`. This is especially important to be small enough in order to prevent HashDos attacks and overly much processing. 24 | - `maxFieldLen` limits field name lengths in order to avoid excessive buffering (which would defeat any possibilities of incremental parsing). 25 | - `ioGranularity` is buffering input and output - data will be usually sent after the defined amount of buffered data and fed into the `Body` after receiving at least that amount of data. 26 | - `maxRequestsPerConnection` limits the number of requests before the connection is closed (for a HTTP/1.1 connection it also controls the Keep-Alive header). Setting it to `PHP_INT_MAX` allows for an unlimited amount of connections. 27 | - `socketBacklogSize` is the queue size of sockets pending acceptance (i.e. being in queue for an `accept()` call by the `Server`). 28 | - `deflateContentTypes` is a regular expression containing the content-types of responses to be deflated. If you use a bit more exotic content-types for deflatable content not starting with `text/` or ending with `/xml` or `+xml` or equal to `application/(json|(x-)?javascript)`, you should extend the regex appropriately. 29 | -------------------------------------------------------------------------------- /demo.php: -------------------------------------------------------------------------------- 1 | 60, 15 | //"deflateMinimumLength" => 0, 16 | "sendServerToken" => true, 17 | ]; 18 | 19 | /* --- http://localhost:1337/ ------------------------------------------------------------------- */ 20 | 21 | $router = router() 22 | ->route("GET", "/", function(Request $req, Response $res) { 23 | $res->end("

Hello, world.

"); 24 | }) 25 | ->route("GET", "/router/{myarg}", function(Request $req, Response $res, array $routeArgs) { 26 | $body = "

Route Args at param 3

".print_r($routeArgs, true).""; 27 | $res->end($body); 28 | }) 29 | ->route("POST", "/", function(Request $req, Response $res) { 30 | $res->end("

Hello, world (POST).

"); 31 | }) 32 | ->route("GET", "error1", function(Request $req, Response $res) { 33 | // ^ the router normalizes the leading forward slash in your URIs 34 | $nonexistent->methodCall(); 35 | }) 36 | ->route("GET", "/error2", function(Request $req, Response $res) { 37 | throw new Exception("wooooooooo!"); 38 | }) 39 | ->route("GET", "/directory/?", function(Request $req, Response $res) { 40 | // The trailing "/?" in the URI allows this route to match /directory OR /directory/ 41 | $res->end("

Dual directory match

"); 42 | }) 43 | ->route("GET", "/long-poll", function(Request $req, Response $res) { 44 | while (true) { 45 | $res->write("hello!
"); 46 | $res->flush(); 47 | yield new Amp\Delayed(1000); 48 | } 49 | }) 50 | ->route("POST", "/body1", function(Request $req, Response $res) { 51 | $body = yield $req->getBody(); 52 | $res->end("

Buffer Body Echo:

{$body}
"); 53 | }) 54 | ->route("POST", "/body2", function(Request $req, Response $res) { 55 | $body = ""; 56 | while (null != $chunk = yield $req->getBody()->read()) { 57 | $body .= $chunk; 58 | } 59 | $res->end("

Stream Body Echo:

{$body}
"); 60 | }) 61 | ->route("GET", "/favicon.ico", function(Request $req, Response $res) { 62 | $res->setStatus(404); 63 | $res->end(Aerys\makeGenericBody(404)); 64 | }) 65 | ->route("ZANZIBAR", "/zanzibar", function (Request $req, Response $res) { 66 | $res->end("

ZANZIBAR!

"); 67 | }); 68 | 69 | $websocket = websocket(new class implements Websocket { 70 | private $endpoint; 71 | 72 | public function onStart(Websocket\Endpoint $endpoint) { 73 | $this->endpoint = $endpoint; 74 | } 75 | 76 | public function onHandshake(Request $request, Response $response) { /* check origin header here */ } 77 | public function onOpen(int $clientId, $handshakeData) { } 78 | 79 | public function onData(int $clientId, Websocket\Message $msg) { 80 | // broadcast to all connected clients 81 | $this->endpoint->broadcast(yield $msg); 82 | } 83 | 84 | public function onClose(int $clientId, int $code, string $reason) { } 85 | public function onStop() { } 86 | }); 87 | 88 | $router->route("GET", "/ws", $websocket); 89 | 90 | // If none of our routes match try to serve a static file 91 | $root = root($docrootPath = __DIR__); 92 | 93 | // If no static files match fallback to this 94 | $fallback = function(Request $req, Response $res) { 95 | $res->end("

Fallback \o/

"); 96 | }; 97 | 98 | return (new Host)->expose("*", 1337)->use($router)->use($root)->use($fallback); 99 | -------------------------------------------------------------------------------- /docs/hosts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Virtual Hosts 3 | permalink: /hosts 4 | --- 5 | Aerys supports virtual hosts by default. Each host needs to be an `Aerys\Host` instance and is automatically registered upon creation of that instance. 6 | 7 | ```php 8 | return (new Aerys\Host) 9 | ->expose("127.0.0.1", 8080) // IPv4 10 | ->expose("::1", 8080) // IPv6 11 | ->expose("/path/to/unix/domain/socket.sock") // UNIX domain socket 12 | ->name("localhost") // actually redundant as localhost is the default 13 | ->use(Aerys\root("/var/www/public_html")); 14 | ``` 15 | 16 | `expose(string $interface, int $port)` binds a host to a specific interface specified by the given IP address (`"*"` binds to _every_ IP interface) and port combination or an UNIX domain socket path. This is where the server will be accessible. The method can be called multiple times to define multiple interfaces to listen on. 17 | 18 | `name(string $name)` gives the host a name. The server determines which host is actually accessed (relevant when having multiple hosts on the same interface-port combination), depending on the `Host` header. For the case where the actual port does not match the port specified in the `Host` header, it is possible to append `:port` where `port` is either the port number to match against, or a wildcard `"*"` to allow any port. 19 | 20 | The example above will be thus accessible via `http://localhost:8080/` on the loopback interface (i.e. only locally) and via the UNIX domain socket located at `/path/to/unix/domain/socket.sock`. 21 | 22 | ## Encryption 23 | 24 | Aerys supports TLS that can be enabled per host. 25 | 26 | ```php 27 | return (new Aerys\Host) 28 | ->expose("*", 443) // bind to everywhere on port 443 29 | ->encrypt("/path/to/certificate.crt", "/path/to/private.key") 30 | ->use(Aerys\root("/var/www/public_html")); 31 | ``` 32 | 33 | `encrypt(string $certificate, string|null $key, array $options = [])` enables encryption on a host. It also sets the default port to listen on to `443`. 34 | 35 | The `$key` parameter may be set to `null` if the certificate file also contains the private key. 36 | 37 | The `$options` array is passed directly as SSL context options and thus equivalent to what is specified by the PHP documentation at [http://php.net/context.ssl](http://php.net/context.ssl). The `$certificate` and `$key` parameters are equivalent to the `local_cert` and `local_pk` options, respectively. 38 | 39 | {:.note} 40 | > Due to implementation details, all hosts on a same interface must be either encrypted or not encrypted. Hence it is impossible to e.g. have both http://localhost:8080 and https://localhost:8080 at the same time. 41 | 42 | ## Adding Handlers 43 | 44 | ```php 45 | return (new Aerys\Host) 46 | ->use(Aerys\router()->route('GET', '/', function(Aerys\Request $req, Aerys\Response $res) { 47 | $res->end("default route"); 48 | })) 49 | ->use(Aerys\root("/var/www/public_html")) # a file foo.txt exists in that folder 50 | ->use(function(Aerys\Request $req, Aerys\Response $res) { 51 | $res->end("My 404!"); 52 | }); 53 | ``` 54 | 55 | `Aerys\Host::use()` is the ubiquitous way to install handlers, `Middleware`s, `Bootable`s, and the `HttpDriver`. 56 | 57 | Handlers are executed in the order they are passed to `use()`, as long as no previous handler has started the response. 58 | 59 | With the concrete example here: 60 | 61 | - the path is `/`: the first handler is executed, and, as the route is matched, a response is initiated (`end()` or `write()`), thus subsequent handlers are not executed. 62 | - the path is `/foo.txt`: first handler is executed, but the response is not started (as no route starting a response was matched), then the second, which responds with the contents of the `foo.txt` file. 63 | - the path is `/inexistent`: first and second handlers are executed, but they don't start a response, so the last handler is executed too, returning `My 404!`. 64 | 65 | The execution order of `Middleware`s and `Bootable`s solely depends on the order they are passed to `use()` and are always all called. Refer to [the `Middleware`s guide](..md). 66 | 67 | A custom `HttpDriver` instance can be only set once per port. It needs to be set on _all_ the host instances bound on a same port. Refer to the [`HttpDriver`](../classes/httpdriver.md). 68 | -------------------------------------------------------------------------------- /bin/aerys: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | arguments->add([ 64 | "debug" => [ 65 | "prefix" => "d", 66 | "longPrefix" => "debug", 67 | "description" => "Start the server in debug mode", 68 | "noValue" => true, 69 | ], 70 | "help" => [ 71 | "prefix" => "h", 72 | "longPrefix" => "help", 73 | "description" => "Display the help screen", 74 | "noValue" => true, 75 | ], 76 | "log" => [ 77 | "prefix" => "l", 78 | "longPrefix" => "log", 79 | "description" => "Set the minimum log output level", 80 | "defaultValue" => "warning", 81 | ], 82 | "workers" => [ 83 | "prefix" => "w", 84 | "longPrefix" => "workers", 85 | "description" => "Manually specify worker count", 86 | "castTo" => "int", 87 | ], 88 | "color" => [ 89 | "longPrefix" => "color", 90 | "description" => "Use ANSI codes in output", 91 | "castTo" => "string", 92 | "defaultValue" => "auto", 93 | ], 94 | "config" => [ 95 | "prefix" => "c", 96 | "longPrefix" => "config", 97 | "description" => "Define a custom server config path", 98 | "required" => true, 99 | ], 100 | "restart" => [ 101 | "prefix" => "r", 102 | "longPrefix" => "restart", 103 | "description" => "Gracefully restart the workers", 104 | "noValue" => true, 105 | ], 106 | "user" => [ 107 | "prefix" => "u", 108 | "longPrefix" => "user", 109 | "description" => "Indicates the user the server will switch to" 110 | ] 111 | ]); 112 | 113 | $console = new Aerys\Console($climate); 114 | try { 115 | if ($console->isArgDefined("help")) { 116 | echo $help; 117 | exit(0); 118 | } 119 | } catch (Exception $e) { 120 | echo "Invalid arguments: " . $e->getMessage() . "\n\n"; 121 | echo $help; 122 | exit(1); 123 | } 124 | 125 | Amp\Loop::run(function () use ($console) { 126 | $logger = new Aerys\ConsoleLogger($console); 127 | $process = ($console->isArgDefined("debug") || PHP_SAPI === "phpdbg") 128 | ? new Aerys\DebugProcess($logger) 129 | : new Aerys\WatcherProcess($logger) 130 | ; 131 | yield from $process->start($console); 132 | }); 133 | -------------------------------------------------------------------------------- /docs/classes/functions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Functions 3 | permalink: /classes/functions 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | ## `router(array $options = []): Router` 10 | 11 | Returns an instance of [`Router`](router.md). 12 | 13 | There is currently only one option: 14 | 15 | - `max_cache_entries`: number of cached routes (direct map of route-to-result) 16 | 17 | ## `websocket(Websocket|Bootable $app, array $options = []): Bootable` 18 | 19 | Requires an instance of [`Websocket`](websocket.md) or an instance of [`Bootable`](bootable.md) returning an instance of [`Websocket`](websocket.md). 20 | 21 | It wraps your [`Websocket`](websocket.md) implementing instance into something usable with [`Host::use()`](host.md) (or in [`Router`](router.md)). 22 | 23 | ## `root(string $docroot, array $options = []): Bootable` 24 | 25 | Defines a static file root handler based on `$docroot`. 26 | 27 | It returns an instance of something usable with [`Host::use()`](host.md) (or in [`Router`](router.md)). 28 | 29 | Available `$options` are: 30 | 31 | - `indexes`: An array of files serving as default files when a directory is requested (Default: `["index.html", "index.htm"]`) 32 | - `useEtagInode`: Boolean whether inodes should be included in the etag (Default: `true`) 33 | - `expiresPeriod`: TTL of client side cached files (`Expires` header) (Default: 7 days) 34 | - `mimeFile`: Path to file containing mime types (Default: `etc/mime`) 35 | - `mimeTypes`: Associative array of manually defined mime types in format `$extension => $mime` 36 | - `defaultMimeType`: Mime type of files not having a mime type defined (Default: `text/plain`) 37 | - `defaultTextCharset`: Default charset of text/ mime files (Default: `utf-8`) 38 | - `useAggressiveCacheHeaders`: Boolean whether aggressive pre-check/post-check headers should be used 39 | - `aggressiveCacheMultiplier`: Number between 0 and 1 when post-check will be active (Only relevant with `useAggressiveCacheHeaders` — Default: 0.9) 40 | - `cacheEntryTtl`: TTL of in memory cache of file stat info (Default: 10 seconds) 41 | - `cacheEntryMaxCount`: Maximum number of in memory file stat info cache entries (Default: 2048) 42 | - `bufferedFileMaxCount`: Maximum number of in memory file content cache entries (Default: 50) 43 | - `bufferedFileMaxSize`: Maximum size of a file to be cached (Default: 524288) 44 | 45 | ## `parseBody(Request $req, $size = 0): BodyParser` 46 | 47 | Creates a [`BodyParser`](bodyparser.md) instance which can be `yield`ed to get the full body string, where `$size` is the maximum accepted body size. 48 | 49 | ## `parseCookie(string $cookies): array` 50 | 51 | Parses a `Cookie` header string into an associative array of format `$name => $value`. 52 | 53 | ## `responseFilter(array $filters, InternalRequest $ireq): \Generator` 54 | 55 | Returns a [middleware](middleware.md) Generator managing multiple filters. Can be `yield from` from another middleware or passed into the `responseCodec()` function. 56 | 57 | ## `responseCodec(\Generator $filter, InternalRequest $ireq): \Generator` 58 | 59 | Returns a Generator which can be used to construct a `StandardResponse` object (its signature is `__construct(\Generator $codec, Client)` and implements [`Response`](response.md)). 60 | 61 | This function may be useful for testing the combination of application callable and middlewares via a custom `InternalRequest->responseWriter. 62 | 63 | ## `initServer(\Psr\Log\LoggerInterface, array, array $options = []): Server` 64 | 65 | This function is only useful, if you want to run Aerys as a small server within a bigger project, or have a specialized process manager etc., outside of the standard `bin/aerys` binary. For normal usage of Aerys it isn't needed. 66 | 67 | It does a full initialization of all dependencies of [`Server`](server.md) and then returns an instance of `Server`, given only a PSR-3 logger, the individual [`Host`](host.md) instances and the server [`Options`](options.md). 68 | 69 | The caller of this method then shall initialize the `Server` by calling `Server->start(): Promise`. 70 | 71 | ### Example 72 | 73 | ```php 74 | \Amp\run(function() use ($logger /* any PSR-3 Logger */) { 75 | $handler = function(Aerys\Request $req, Aerys\Response $res) { 76 | $res->end("A highly specialized handler!"); 77 | }; 78 | $host = (new Aerys\Host)->use($handler); 79 | $server = Aerys\initServer($logger, [$host], ["debug" => true]); 80 | yield $server->start(); 81 | # Aerys is running! 82 | }); 83 | ``` 84 | -------------------------------------------------------------------------------- /lib/Websocket/Handshake.php: -------------------------------------------------------------------------------- 1 | response = $response; 21 | $this->acceptKey = $acceptKey; 22 | 23 | $response->setStatus($this->status); 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function setStatus(int $code): Response { 30 | if (!($code === 101 || $code >= 300)) { 31 | throw new \Error( 32 | "Invalid websocket handshake status ({$code}); 101 or 300-599 required" 33 | ); 34 | } 35 | $this->response->setStatus($code); 36 | $this->status = $code; 37 | return $this; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function setReason(string $phrase): Response { 44 | $this->response->setReason($phrase); 45 | return $this; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function addHeader(string $field, string $value): Response { 52 | $this->response->addHeader($field, $value); 53 | return $this; 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function setHeader(string $field, string $value): Response { 60 | $this->response->setHeader($field, $value); 61 | return $this; 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function write(string $partialBodyChunk): \Amp\Promise { 68 | if ($this->status === 101) { 69 | throw new \Error( 70 | "Cannot write(); entity body content disallowed for Switching Protocols Response" 71 | ); 72 | } 73 | if (!$this->isStarted) { 74 | $this->handshake(); 75 | } 76 | $this->isStarted = true; 77 | return $this->response->write($partialBodyChunk); 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | */ 83 | public function flush() { 84 | if ($this->status === 101) { 85 | throw new \Error( 86 | "Cannot flush(); entity body content disallowed for Switching Protocols Response" 87 | ); 88 | } 89 | // We don't assign websocket headers in flush() because calling 90 | // this method before Response output starts is an error and will 91 | // throw when invoked on the wrapped Response. 92 | $this->response->flush(); 93 | } 94 | 95 | /** 96 | * {@inheritDoc} 97 | */ 98 | public function end(string $finalBodyChunk = ""): \Amp\Promise { 99 | if ($this->status === 101 && $finalBodyChunk !== "") { 100 | throw new \Error( 101 | "Cannot end() with body data; entity body content disallowed for Switching Protocols Response" 102 | ); 103 | } 104 | if (!$this->isStarted) { 105 | $this->handshake(); 106 | } 107 | $this->isStarted = true; 108 | return $this->response->end($finalBodyChunk); 109 | } 110 | 111 | /** 112 | * {@inheritDoc} 113 | */ 114 | public function setCookie(string $name, string $value, array $flags = []): Response { 115 | $this->response->setCookie($name, $value, $flags); 116 | return $this; 117 | } 118 | 119 | /** 120 | * {@inheritDoc} 121 | */ 122 | public function push(string $url, array $headers = null): Response { 123 | return $this->response->push($url, $headers); 124 | } 125 | 126 | /** 127 | * {@inheritDoc} 128 | */ 129 | public function state(): int { 130 | return $this->response->state(); 131 | } 132 | 133 | private function handshake() { 134 | if ($this->status === 101) { 135 | $concatKeyStr = $this->acceptKey . self::ACCEPT_CONCAT; 136 | $secWebSocketAccept = base64_encode(sha1($concatKeyStr, true)); 137 | $this->response->setHeader("Upgrade", "websocket"); 138 | $this->response->setHeader("Connection", "upgrade"); 139 | $this->response->setHeader("Sec-WebSocket-Accept", $secWebSocketAccept); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/Response.php: -------------------------------------------------------------------------------- 1 | value pairs and/or unkeyed values as per https://tools.ietf.org/html/rfc6265#section-5.2.1 56 | * @return self 57 | */ 58 | public function setCookie(string $name, string $value, array $flags = []): Response; 59 | 60 | /** 61 | * Incrementally stream parts of the response entity body. 62 | * 63 | * This method may be repeatedly called to stream the response body. 64 | * Applications that can afford to buffer an entire response in memory or 65 | * can wait for all body data to generate may use Response::end() to output 66 | * the entire response in a single call. 67 | * 68 | * Note: Headers are sent upon the first invocation of Response::write(). 69 | * 70 | * @param string $partialBodyChunk A portion of the response entity body 71 | * @return \Amp\Promise to be succeeded whenever local buffers aren't full 72 | */ 73 | public function write(string $partialBodyChunk): \Amp\Promise; 74 | 75 | /** 76 | * Request that buffered stream data be flushed to the client. 77 | * 78 | * This method only makes sense when streaming output via Response::write(). 79 | * Invoking it before calling write() or after end() is a logic error. 80 | */ 81 | public function flush(); 82 | 83 | /** 84 | * Signify the end of streaming response output. 85 | * 86 | * User applications are NOT required to call Response::end() after streaming 87 | * or sending response data (though it's not incorrect to do so) -- the server 88 | * will automatically call end() as needed. 89 | * 90 | * Passing the optional $finalBodyChunk parameter is a shortcut equivalent to 91 | * the following: 92 | * 93 | * $response->write($finalBodyChunk); 94 | * $response->end(); 95 | * 96 | * Note: Invoking Response::end() with a non-empty $finalBodyChunk parameter 97 | * without having previously invoked Response::write() is equivalent to calling 98 | * Response::send($finalBodyChunk). 99 | * 100 | * @param string $finalBodyChunk Optional final body data to send 101 | * @return \Amp\Promise to be succeeded whenever local buffers aren't full 102 | */ 103 | public function end(string $finalBodyChunk = ""): \Amp\Promise; 104 | 105 | /** 106 | * Indicate resources which a client likely needs to fetch. (e.g. Link: preload or HTTP/2 Server Push). 107 | * 108 | * @param string $url The URL this request should be dispatched to 109 | * @param array $headers Optional custom headers, else the server will try to reuse headers from the last request 110 | * @return Response 111 | */ 112 | public function push(string $url, array $headers = null): Response; 113 | 114 | /** 115 | * Retrieve the current response state. 116 | * 117 | * The response state is a bitmask of the following flags: 118 | * 119 | * - Response::NONE 120 | * - Response::STARTED 121 | * - Response::STREAMING 122 | * - Response::ENDED 123 | * 124 | * @return int 125 | */ 126 | public function state(): int; 127 | } 128 | -------------------------------------------------------------------------------- /docs/middlewares.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Middlewares 3 | permalink: /middlewares 4 | --- 5 | Middlewares can be `use()`'d via a `Host` instance or the `Router`. 6 | 7 | They are able to manipulate responses as well as the request data before the application callable can read from them. 8 | 9 | Even internal state of the connection can be altered by them. They have powers to break in deeply into the internals of the server. 10 | 11 | {:.warning} 12 | > Middlewares are technically able to directly break some assumptions of state by the server by altering certain values. Keep middlewares footprint as small as possible and only change what really is needed! 13 | 14 | For example websockets are using a middleware to export the socket from within the server accessible via `InternalRequest->client->socket`. 15 | 16 | Most middlewares though will only need to manipulate direct request input (headers) and operate on raw response output. 17 | 18 | ## Middleware::do 19 | 20 | ```php 21 | return (new Aerys\Host)->use(new class implements Aerys\Middleware { 22 | function do(Aerys\InternalRequest $ireq) { 23 | $headers = yield; 24 | 25 | if (!$headers["x-capitalized"]) { 26 | # early abort 27 | return $headers; 28 | } 29 | 30 | $data = yield $headers; 31 | 32 | while ($data !== null) { 33 | # $data is false upon Response::flush() 34 | if ($data === false) { 35 | # we have to empty eventual buffers here, but as we don't buffer, no problem 36 | } 37 | 38 | $data = strtoupper(yield $data); 39 | } 40 | } 41 | 42 | # an application callable; the way you have middlewares and callables inside one single class 43 | function __invoke(Aerys\Request $req, Aerys\Response $res) { 44 | if (!$req->getParam('nocapitalize')) { 45 | $res->setHeader("X-CAPITALIZED", "0"); 46 | } else { 47 | $res->setHeader("X-CAPITALIZED", "1"); 48 | } 49 | 50 | # Middlewares will only receive headers here, upon the first write()/end()/flush() call 51 | $res->write("this "); 52 | $res->write("will "); 53 | $res->flush(); 54 | $res->end("be CAPITALIZED!!!"); 55 | } 56 | }); 57 | ``` 58 | 59 | `Middleware`s may return an instance of `Generator` in their `do()` method. 60 | 61 | The first `yield` is always being sent in an array of headers in format `[$field => [$value, ...], ...]` (field names are lowercased). 62 | 63 | Subsequent `yield`s will return either `false` (flush), a string (data) or `null` (end). [Note that `null` and `false` were chosen as casting them to string is resulting in an empty string.] 64 | 65 | The first value `yield`ed by the middleware must be the array of headers. 66 | 67 | Later `yield`s may return as much data as they want. 68 | 69 | You can use `return` instead of `yield` in order to immediately detach with eventual final data. (In case you buffered a bit first and still hold the headers, use `return $data . yield $headers;`.) 70 | 71 | {:.note} 72 | > You cannot wait for promises inside middlewares. This is by design (as flushes should be propagating immediately etc.). If you need I/O, either move it to a responder in front of your actual responder or move it into your responder and call it explicitly. 73 | 74 | ## InternalRequest 75 | 76 | ```php 77 | return (new Aerys\Host) 78 | ->use(new class implements Aerys\Middleware { 79 | # Middlewares don't have to return a Generator, they can also just terminate immediately 80 | function do(Aerys\InternalRequest $ireq) { 81 | // set maximum allowed body size generally for this host to 256 KB 82 | $ireq->maxBodySize = 256 * 1024; // 256 KB 83 | $ireq->client->httpDriver->upgradeBodySize($ireq); 84 | 85 | // define a random number 86 | $ireq->locals["tutorial.random"] = random_int(0, 19); 87 | } 88 | }) 89 | ->use(function (Aerys\Request $req, Aerys\Response $res) { 90 | $res->write("This is the good number: " . $res->getLocalVar("tutorial.random") . "\n\n"); 91 | // send the body contents back to the sender 92 | $res->end(yield $req->getBody()); 93 | }) 94 | ; 95 | ``` 96 | 97 | Middlewares provide access to [`InternalRequest`](classes/internalrequest.md). That class is a bunch of properties, [detailed here](classes/internalrequest.md). 98 | 99 | These properties expose the internal request data as well as connection specific data via [`InternalRequest->client`](classes/client.md) property. 100 | 101 | In particular, note the `InternalRequest->locals` property. There are the [`Request::getLocalVar($key)` respectively `Request::setLocalVar($key, $value)`](classes/request.md) methods which access or mutate that array. It is meant to be a general point of data exchange between the middlewares and the application callables. -------------------------------------------------------------------------------- /docs/classes/websocket.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WebSocket 3 | permalink: /classes/websocket 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | The `Websocket` interface is the general interface for your websocket class. To set it as a responder, just pass an instance of it to the `websocket()` function whose result must be passed to [`Host::use()`](host.md#usemiddleware--bootable--callablerequest-response--monitor--httpdriver) or a specific route (see [`Router::route`](router.md#routestring-method-string-uri-callablemiddlewarebootablemonitor-actions-self)). 10 | 11 | {:.note} 12 | > `websocket()` returns a responder callable, it falls under the same rules as every responder callable passed to `use()`: after the first callable started the response, the following ones will be ignored. Make attention to not e.g. `(new Host)->use($router)->use($websocket)` and be then surprised why you get an invalid response with code 200 (OK). 13 | 14 | Example: 15 | 16 | ```php 17 | $websocket = Aerys\websocket(new MyAwesomeWebsocket); 18 | (new Aerys\Host)->use($websocket); 19 | ``` 20 | 21 | ## `onStart(Websocket\Endpoint)` 22 | 23 | This method is called when the [`Server`](server.md) is in `STARTING` mode. The sole argument is the [`Websocket\Endpoint`](websocket-endpoint.md) instace via which you do the whole communication to the outside. 24 | 25 | ## `onHandshake(Request, Response)` 26 | 27 | This is the chance to deny the handshake. The [`Request`](request.md) and [`Response`](response.md) are just like for any normal HTTP request. 28 | 29 | To prevent a successful handshake, set the response to a status not equal to 101 (Switching Protocols). 30 | 31 | In order to map data (like identification information) to a client, you can return a value which will be passed to `onOpen()` as second parameter 32 | 33 | ## `onOpen(int $clientId, $handshakeData)` 34 | 35 | In case of a successful handshake, this method gets called. `$clientId` is an opaque and unique integer valid through a whole websocket session you can use for identifying a specific client. `$handshakeData` will contain whatever was returned before in `onHandshake()`. 36 | 37 | ## `onData(int $clientId, Websocket\Message)` 38 | 39 | This method gets called each time a new data frame sequence is received. 40 | 41 | {:.note} 42 | > The second parameter is not a string, but a [`Websocket\Message` extends `Amp\ByteStream\Message`](http://amphp.org/byte-stream/message), which implements Promise. The yielded Promise will return a string or fail with a ClientException if the client disconnected before transmitting the full data. 43 | 44 | ## `onClose(int $clientId, int $code, string $reason)` 45 | 46 | This method is called after the client has (been) disconnected: you must not use any [`Websocket\Endpoint`](websocket-endpoint.md) API with the passed client id in this method. 47 | 48 | ## `onStop(): Generator|Promise|null` 49 | 50 | When the [`Server`](server.md) enters `STOPPING` state, this method is called. It is guaranteed that no further calls to any method except `onClose()` will happen after this method was called. 51 | 52 | This means, you may only send, but not receive from this moment on. The clients are only forcefully closed after this methods call and the eventual returned Promise resolved. 53 | 54 | ## Full example 55 | 56 | ```php 57 | class MyAwesomeWebsocket implements Aerys\Websocket { 58 | private $endpoint; 59 | 60 | public function onStart(Aerys\Websocket\Endpoint $endpoint) { 61 | $this->endpoint = $endpoint; 62 | } 63 | 64 | public function onHandshake(Aerys\Request $request, Aerys\Response $response) { 65 | // Do eventual session verification and manipulate Response if needed to abort 66 | } 67 | 68 | public function onOpen(int $clientId, $handshakeData) { 69 | $this->endpoint->send("Heyho!", $clientId); 70 | } 71 | 72 | public function onData(int $clientId, Aerys\Websocket\Message $msg) { 73 | // send back what we get in 74 | $msg = yield $msg; // Do not forget to yield here to get a string 75 | yield $this->endpoint->send($msg, $clientId); 76 | } 77 | 78 | public function onClose(int $clientId, int $code, string $reason) { 79 | // client disconnected, we may not send anything to him anymore 80 | } 81 | 82 | public function onStop() { 83 | $this->endpoint->broadcast("Byebye!"); 84 | } 85 | } 86 | 87 | $websocket = Aerys\websocket(new MyAwesomeWebsocket); 88 | $router = (new Aerys\Router) 89 | ->route('GET', '/websocket', $websocket) 90 | ->route('GET', '/', function(Aerys\Request $req, Aerys\Response $res) { $res->send(' 91 | '); }); 100 | return (new Aerys\Host)->use($router); 101 | ``` 102 | -------------------------------------------------------------------------------- /lib/Logger.php: -------------------------------------------------------------------------------- 1 | 8, 19 | self::INFO => 7, 20 | self::NOTICE => 6, 21 | self::WARNING => 5, 22 | self::ERROR => 4, 23 | self::CRITICAL => 3, 24 | self::ALERT => 2, 25 | self::EMERGENCY => 1, 26 | ]; 27 | 28 | private $outputLevel = self::LEVELS[self::DEBUG]; 29 | private $ansify = true; 30 | 31 | abstract protected function output(string $message); 32 | 33 | final public function emergency($message, array $context = []) { 34 | return $this->log(self::EMERGENCY, $message, $context); 35 | } 36 | 37 | final public function alert($message, array $context = []) { 38 | return $this->log(self::ALERT, $message, $context); 39 | } 40 | 41 | final public function critical($message, array $context = []) { 42 | return $this->log(self::CRITICAL, $message, $context); 43 | } 44 | 45 | final public function error($message, array $context = []) { 46 | return $this->log(self::ERROR, $message, $context); 47 | } 48 | 49 | final public function warning($message, array $context = []) { 50 | return $this->log(self::WARNING, $message, $context); 51 | } 52 | 53 | final public function notice($message, array $context = []) { 54 | return $this->log(self::NOTICE, $message, $context); 55 | } 56 | 57 | final public function info($message, array $context = []) { 58 | return $this->log(self::INFO, $message, $context); 59 | } 60 | 61 | final public function debug($message, array $context = []) { 62 | return $this->log(self::DEBUG, $message, $context); 63 | } 64 | 65 | final public function log($level, $message, array $context = []) { 66 | if ($this->canEmit($level)) { 67 | $message = $this->format($level, $message, $context); 68 | return $this->output($message); 69 | } 70 | } 71 | 72 | private function format($level, $message, array $context = []) { 73 | $time = @date("Y-m-d H:i:s", $context["time"] ?? time()); 74 | $level = isset(self::LEVELS[$level]) ? $level : "unknown"; 75 | $level = $this->ansify ? $this->ansify($level) : "$level:"; 76 | 77 | foreach ($context as $key => $replacement) { 78 | // avoid invalid casts to string 79 | if (!is_array($replacement) && (!is_object($replacement) || method_exists($replacement, '__toString'))) { 80 | $replacements["{{$key}}"] = $replacement; 81 | } 82 | } 83 | if (isset($replacements)) { 84 | $message = strtr($message, $replacements); 85 | } 86 | 87 | return "[{$time}] {$level} {$message}"; 88 | } 89 | 90 | private function ansify($level) { 91 | switch ($level) { 92 | case self::EMERGENCY: 93 | case self::ALERT: 94 | case self::CRITICAL: 95 | case self::ERROR: 96 | return "{$level}"; 97 | case self::WARNING: 98 | return "{$level}"; 99 | case self::NOTICE: 100 | return "{$level}"; 101 | case self::INFO: 102 | return "{$level}"; 103 | case self::DEBUG: 104 | return "{$level}"; 105 | } 106 | } 107 | 108 | private function canEmit(string $logLevel) { 109 | return isset(self::LEVELS[$logLevel]) 110 | ? ($this->outputLevel >= self::LEVELS[$logLevel]) 111 | : false; 112 | } 113 | 114 | final protected function setOutputLevel(int $outputLevel) { 115 | if ($outputLevel < min(self::LEVELS)) { 116 | $outputLevel = min(self::LEVELS); 117 | } elseif ($outputLevel > max(self::LEVELS)) { 118 | $outputLevel = max(self::LEVELS); 119 | } 120 | 121 | $this->outputLevel = $outputLevel; 122 | } 123 | 124 | final protected function setAnsify(string $mode) { 125 | switch ($mode) { 126 | case "auto": 127 | case "on": 128 | $this->ansify = true; 129 | break; 130 | case "off": 131 | $this->ansify = false; 132 | break; 133 | default: 134 | $this->ansify = true; 135 | break; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/CommandClient.php: -------------------------------------------------------------------------------- 1 | path = self::socketPath($config); 29 | $this->parser = new Parser(self::parser($this->readMessages)); 30 | } 31 | 32 | public static function socketPath(string $config) { 33 | // that base64_encode instead of the standard hex representation of sha1 is necessary to avoid overly long paths for unix domain sockets 34 | return \sys_get_temp_dir() . "/aerys_" . \strtr(\base64_encode(\sha1(selectConfigFile($config), true)), "+/", "-_").".tmp"; 35 | } 36 | 37 | private function send($message): Promise { 38 | return $this->lastSend = call(function () use ($message) { 39 | if ($this->lastSend) { 40 | yield $this->lastSend; 41 | } 42 | 43 | if (!$this->socket) { 44 | $this->socket = yield new Coroutine(self::connect($this->path)); 45 | } 46 | 47 | $message = \json_encode($message) . "\n"; 48 | yield $this->socket->write($message); 49 | 50 | while (empty($this->readMessages)) { 51 | if (($chunk = yield $this->socket->read()) === null) { 52 | $this->socket->close(); 53 | throw new ClosedException("Connection went away ..."); 54 | } 55 | 56 | $this->parser->push($chunk); 57 | } 58 | 59 | $this->lastSend = null; 60 | 61 | $message = \array_shift($this->readMessages); 62 | 63 | if (isset($message["exception"])) { 64 | throw new \RuntimeException($message["exception"]); 65 | } 66 | 67 | return $message; 68 | }); 69 | } 70 | 71 | private static function connect(string $path): \Generator { 72 | $unix = \in_array("unix", \stream_get_transports(), true); 73 | if ($unix) { 74 | $uri = "unix://$path.sock"; 75 | } else { 76 | $uri = yield File\get($path); 77 | } 78 | return yield Socket\connect($uri); 79 | } 80 | 81 | private static function parser(array &$messages): \Generator { 82 | do { 83 | $messages[] = @\json_decode(yield "\n", true); 84 | } while (true); 85 | } 86 | 87 | public function __destruct() { 88 | if ($this->socket) { 89 | $this->socket->close(); 90 | } 91 | } 92 | 93 | public function restart(): Promise { 94 | return $this->send(["action" => "restart"]); 95 | } 96 | 97 | public function stop(): Promise { 98 | return $this->send(["action" => "stop"]); 99 | } 100 | 101 | public function started(): Promise { 102 | return $this->send(["action" => "started"]); 103 | } 104 | 105 | public function importServerSockets($addrCtxMap): Promise { 106 | return call(function () use ($addrCtxMap) { 107 | $reply = yield $this->send(["action" => "import-sockets", "addrCtxMap" => array_map(function ($context) { return $context["socket"]; }, $addrCtxMap)]); 108 | 109 | $sockets = $reply["count"]; 110 | $serverSockets = []; 111 | $deferred = new Deferred; 112 | 113 | $sock = \socket_import_stream($this->socket->getResource()); 114 | Loop::onReadable($this->socket->getResource(), function ($watcherId) use (&$serverSockets, &$sockets, $sock, $deferred, $addrCtxMap) { 115 | $data = ["controllen" => \socket_cmsg_space(SOL_SOCKET, SCM_RIGHTS) + 4]; // 4 == sizeof(int) 116 | if (!\socket_recvmsg($sock, $data)) { 117 | Loop::cancel($watcherId); 118 | $deferred->fail(new \RuntimeException("Server sockets could not be received from watcher process")); 119 | } 120 | $address = $data["iov"][0]; 121 | $newSock = $data["control"][0]["data"][0]; 122 | \socket_listen($newSock, $addrCtxMap[$address]["socket"]["backlog"] ?? 0); 123 | 124 | $newSocket = \socket_export_stream($newSock); 125 | \stream_context_set_option($newSocket, $addrCtxMap[$address]); // put eventual options like ssl back (per worker) 126 | $serverSockets[$address] = $newSocket; 127 | 128 | if (!--$sockets) { 129 | Loop::cancel($watcherId); 130 | $deferred->resolve($serverSockets); 131 | } 132 | }); 133 | 134 | $this->lastSend = $deferred->promise(); // Guards second watcher on socket by blocking calls to send() 135 | $this->lastSend->onResolve(function () { $this->lastSend = null; }); 136 | return $this->lastSend; 137 | }); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/Request.php: -------------------------------------------------------------------------------- 1 | internalRequest = $internalRequest; 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function getMethod(): string { 24 | return $this->internalRequest->method; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getUri(): string { 31 | return $this->internalRequest->uri; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getProtocolVersion(): string { 38 | return $this->internalRequest->protocol; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function getHeader(string $field) { 45 | return $this->internalRequest->headers[strtolower($field)][0] ?? null; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function getHeaderArray(string $field): array { 52 | return $this->internalRequest->headers[strtolower($field)] ?? []; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getAllHeaders(): array { 59 | return $this->internalRequest->headers; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getBody(int $bodySize = -1): Message { 66 | $ireq = $this->internalRequest; 67 | if ($bodySize > -1) { 68 | if ($bodySize > ($ireq->maxBodySize ?? $ireq->client->options->maxBodySize)) { 69 | $ireq->maxBodySize = $bodySize; 70 | $ireq->client->httpDriver->upgradeBodySize($this->internalRequest); 71 | } 72 | } 73 | 74 | if ($ireq->body != $this->body) { 75 | $this->body = $ireq->body; 76 | $ireq->body->onResolve(function ($e, $data) { 77 | if ($e instanceof ClientSizeException) { 78 | $ireq = $this->internalRequest; 79 | $bodyEmitter = $ireq->client->bodyEmitters[$ireq->streamId]; 80 | $ireq->body = new Message(new IteratorStream($bodyEmitter->iterate())); 81 | $bodyEmitter->emit($data); 82 | } 83 | }); 84 | } 85 | return $ireq->body; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function getParam(string $name) { 92 | return ($this->queryParams ?? $this->queryParams = $this->parseParams())[$name][0] ?? null; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function getParamArray(string $name): array { 99 | return ($this->queryParams ?? $this->queryParams = $this->parseParams())[$name] ?? []; 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function getAllParams(): array { 106 | return $this->queryParams ?? $this->queryParams = $this->parseParams(); 107 | } 108 | 109 | private function parseParams() { 110 | if (empty($this->internalRequest->uriQuery)) { 111 | return $this->queryParams = []; 112 | } 113 | 114 | $pairs = explode("&", $this->internalRequest->uriQuery); 115 | if (count($pairs) > $this->internalRequest->client->options->maxInputVars) { 116 | throw new ClientSizeException; 117 | } 118 | 119 | $this->queryParams = []; 120 | foreach ($pairs as $pair) { 121 | $pair = explode("=", $pair, 2); 122 | // maxFieldLen should not be important here ... if it ever is, create an issue... 123 | $this->queryParams[urldecode($pair[0])][] = urldecode($pair[1] ?? ""); 124 | } 125 | 126 | return $this->queryParams; 127 | } 128 | 129 | /** 130 | * {@inheritdoc} 131 | */ 132 | public function getCookie(string $name) { 133 | $ireq = $this->internalRequest; 134 | 135 | return $ireq->cookies[$name] ?? null; 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function getLocalVar(string $key) { 142 | return $this->internalRequest->locals[$key] ?? null; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function setLocalVar(string $key, $value) { 149 | $this->internalRequest->locals[$key] = $value; 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function getConnectionInfo(): array { 156 | $client = $this->internalRequest->client; 157 | return [ 158 | "client_port" => $client->clientPort, 159 | "client_addr" => $client->clientAddr, 160 | "server_port" => $client->serverPort, 161 | "server_addr" => $client->serverAddr, 162 | "is_encrypted"=> $client->isEncrypted, 163 | "crypto_info" => $client->cryptoInfo, 164 | ]; 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | */ 170 | public function getOption(string $option) { 171 | return $this->internalRequest->client->options->{$option}; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /docs/classes/options.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Options 3 | permalink: /classes/options 4 | --- 5 | 6 | * Table of Contents 7 | {:toc} 8 | 9 | The `Options` class exposes no methods, just properties. The properties may only be set during `Server` startup, after that, they're locked from further writes. 10 | 11 | ## `$debug` 12 | 13 | Indicates whether debug mode is active. 14 | 15 | Type: boolean 16 | 17 | ## `$user` 18 | 19 | Only relevant under *nix systems when the server is started as root; it indicates to which user the server will switch to after startup. 20 | 21 | Type: string — Default: current user 22 | 23 | ## `$maxConnections` 24 | 25 | Maximum number of total simultaneous connections the server accepts. If that number is exceeded, new connections are dropped. 26 | 27 | Type: integer greater than 0 — Default: `1000` 28 | 29 | ## `$connectionsPerIP` 30 | 31 | Maximum number of allowed connections from an individual /32 IPv4 or /56 IPv6 range. 32 | 33 | Type: integer greater than 0 — Default: `30` 34 | 35 | ## `$maxRequestsPerConnection` 36 | 37 | Maximum number of requests on a single connection. (On HTTP/1.1 it controls the Keep-Alive header.) 38 | 39 | Set to `PHP_INT_MAX` to effectively disable this limit. 40 | 41 | Type: integer greater than 0 — Default: `1000` 42 | 43 | ## `$connectionTimeout` 44 | 45 | Time in seconds until a connection is closed (if no further data comes in). 46 | 47 | Type: integer greater than 0 — Default: `6` 48 | 49 | ## `$defaultContentType` 50 | 51 | Content type of responses, if not otherwise specified by the request handler. 52 | 53 | Type: string — Default: `"text/html"` 54 | 55 | ## `$defaultTextCharset` 56 | 57 | Text charset of text/ content types, if not otherwise specified by the request handler. 58 | 59 | Type: string — Default: `"utf-8"` 60 | 61 | ## `$sendServerToken` 62 | 63 | Whether a `Server` header field should be sent along with the response. 64 | 65 | Type: boolean — Default: `false` 66 | 67 | ## `$socketBacklogSize` 68 | 69 | Size of the backlog, i.e. how many connections may be pending in an unaccepted state. 70 | 71 | Type: integer greater than or equal to 16 — Default: `128 72 | 73 | ## `$normalizeMethodCase` 74 | 75 | Whether method names shall be lowercased. 76 | 77 | Type: boolean — Default: `true` 78 | 79 | ## `$maxConcurrentStreams` 80 | 81 | Maximum number of concurrent HTTP/2 streams per connection. 82 | 83 | Type: integer greater than 0 — Default: `20` 84 | 85 | ## `$maxFramesPerSecond` 86 | 87 | Maximum number of frames a HTTP/2 client is allowed to send per second. 88 | 89 | Type: integer — Default: `60` 90 | 91 | ## `$allowedMethods` 92 | 93 | Array of allowed HTTP methods. [The [`Router`](router.md) class will extend this array with the used methods.] 94 | 95 | Type: array<string> — Default: `["GET", "POST", "PUT", "PATCH", "HEAD", "OPTIONS", "DELETE"]` 96 | 97 | ## `$deflateEnable` 98 | 99 | Whether HTTP body compression should be active. 100 | 101 | Type: boolean — Default: `extension_loaded("zlib")` 102 | 103 | ## `$deflateContentTypes` 104 | 105 | A regular expression to match the content-type against in order to determine whether a request shall be deflated or not. 106 | 107 | Type: string — Default: `"#^(?:text/.*+|[^/]*+/xml|[^+]*\+xml|application/(?:json|(?:x-)?javascript))$#i"` 108 | 109 | ## `$configPath` 110 | 111 | Path to the used configuration file 112 | 113 | Type: string 114 | 115 | ## `$maxFieldLen` 116 | 117 | Maximum length of a field name of parsed bodies. 118 | 119 | Type: integer greater than 0 — Default: `16384` 120 | 121 | ## `$maxInputVars` 122 | 123 | Maximum number of input vars (in query string or parsed body). 124 | 125 | Type: integer greater or equal to 0 — Default: `200` 126 | 127 | ## `$maxBodySize` 128 | 129 | Default maximum size of HTTP bodies. [Can be increased by calling [`HttpDriver::upgradeBodySize($ireq)`](httpdriver.md) or more commonly [`Response::getBody($size)`](response.md).] 130 | 131 | Type: integer greater than or equal to 0 — Default: `131072` (128 KiB) 132 | 133 | ## `$maxHeaderSize` 134 | 135 | Maximum header size of a HTTP request. 136 | 137 | Type: integer greater than 0 — Default: `32768` 138 | 139 | ## `$ioGranularity` 140 | 141 | Granularity at which reads from the socket and into the bodies are performed. 142 | 143 | Type: integer greater than 0 — Default: `32768` 144 | 145 | ## `$softStreamCap` 146 | 147 | Limit at which the internal buffers are considered saturated and resolution of `Promise`s returned by `Response::write()` is delayed until the buffer sizes fall below it. 148 | 149 | Type: integer greater than or equal to 0 — Default: `131072` 150 | 151 | ## `$deflateMinimumLength` 152 | 153 | Minimum length before any compression is applied. 154 | 155 | Type: integer greater than or equal to 0 — Default: `860` 156 | 157 | ## `$deflateBufferSize` 158 | 159 | Buffer size before data is compressed (except it is ended or flushed before). 160 | 161 | Type: integer greater than 0 — Default: `8192` 162 | 163 | ## `$chunkBufferSize` 164 | 165 | Buffer size before data is being chunked (except it is ended or flushed before). 166 | 167 | Type: integer greater than 0 — Default: `8192` 168 | 169 | ## `$outputBufferSize` 170 | 171 | Buffer size before data is written onto the stream (except it is ended or flush before). 172 | 173 | Type: integer greater than 0 — Default: `8192` 174 | 175 | ## `$shutdownTimeout` 176 | 177 | Milliseconds before the Server is forcefully shut down after graceful stopping has been initiated. 178 | 179 | Type: integer greater or equal to 0 — Default: `3000` 180 | -------------------------------------------------------------------------------- /etc/mime: -------------------------------------------------------------------------------- 1 | 323 text/h323 2 | acx application/internet-property-stream 3 | ai application/postscript 4 | aif audio/x-aiff 5 | aifc audio/x-aiff 6 | aiff audio/x-aiff 7 | asf video/x-ms-asf 8 | asr video/x-ms-asf 9 | asx video/x-ms-asf 10 | au audio/basic 11 | avi video/x-msvideo 12 | axs application/olescript 13 | bas text/plain 14 | bcpio application/x-bcpio 15 | bin application/octet-stream 16 | bmp image/bmp 17 | c text/plain 18 | cat application/vnd.ms-pkiseccat 19 | cdf application/x-cdf 20 | cdf application/x-netcdf 21 | cer application/x-x509-ca-cert 22 | class application/octet-stream 23 | clp application/x-msclip 24 | cmx image/x-cmx 25 | cod image/cis-cod 26 | cpio application/x-cpio 27 | crd application/x-mscardfile 28 | crl application/pkix-crl 29 | crt application/x-x509-ca-cert 30 | csh application/x-csh 31 | css text/css 32 | dcr application/x-director 33 | der application/x-x509-ca-cert 34 | dir application/x-director 35 | dll application/x-msdownload 36 | dms application/octet-stream 37 | doc application/msword 38 | dot application/msword 39 | dvi application/x-dvi 40 | dxr application/x-director 41 | eps application/postscript 42 | etx text/x-setext 43 | evy application/envoy 44 | exe application/octet-stream 45 | fif application/fractals 46 | flr x-world/x-vrml 47 | gif image/gif 48 | gtar application/x-gtar 49 | gz application/x-gzip 50 | h text/plain 51 | hdf application/x-hdf 52 | hlp application/winhlp 53 | hqx application/mac-binhex40 54 | hta application/hta 55 | htc text/x-component 56 | htm text/html 57 | html text/html 58 | htt text/webviewhtml 59 | ico image/x-icon 60 | ief image/ief 61 | iii application/x-iphone 62 | ins application/x-internet-signup 63 | isp application/x-internet-signup 64 | jfif image/pipeg 65 | jpe image/jpeg 66 | jpeg image/jpeg 67 | jpg image/jpeg 68 | js application/x-javascript 69 | latex application/x-latex 70 | lha application/octet-stream 71 | lsf video/x-la-asf 72 | lsx video/x-la-asf 73 | lzh application/octet-stream 74 | m13 application/x-msmediaview 75 | m14 application/x-msmediaview 76 | m3u audio/x-mpegurl 77 | man application/x-troff-man 78 | mdb application/x-msaccess 79 | me application/x-troff-me 80 | mht message/rfc822 81 | mhtml message/rfc822 82 | mid audio/mid 83 | mny application/x-msmoney 84 | mov video/quicktime 85 | movie video/x-sgi-movie 86 | m4a audio/mp4 87 | mp2 video/mpeg 88 | mp3 audio/mpeg 89 | mpa video/mpeg 90 | mpe video/mpeg 91 | mpeg video/mpeg 92 | mpg video/mpeg 93 | mpp application/vnd.ms-project 94 | mpv2 video/mpeg 95 | ms application/x-troff-ms 96 | msg application/vnd.ms-outlook 97 | mvb application/x-msmediaview 98 | nc application/x-netcdf 99 | nws message/rfc822 100 | oda application/oda 101 | ogg audio/ogg 102 | oga audio/ogg 103 | p10 application/pkcs10 104 | p12 application/x-pkcs12 105 | p7b application/x-pkcs7-certificates 106 | p7c application/x-pkcs7-mime 107 | p7m application/x-pkcs7-mime 108 | p7r application/x-pkcs7-certreqresp 109 | p7s application/x-pkcs7-signature 110 | pbm image/x-portable-bitmap 111 | pdf application/pdf 112 | pfx application/x-pkcs12 113 | pgm image/x-portable-graymap 114 | pko application/ynd.ms-pkipko 115 | pma application/x-perfmon 116 | pmc application/x-perfmon 117 | pml application/x-perfmon 118 | pmr application/x-perfmon 119 | pmw application/x-perfmon 120 | png image/png 121 | pnm image/x-portable-anymap 122 | pot application/vnd.ms-powerpoint 123 | ppm image/x-portable-pixmap 124 | pps application/vnd.ms-powerpoint 125 | ppt application/vnd.ms-powerpoint 126 | prf application/pics-rules 127 | ps application/postscript 128 | pub application/x-mspublisher 129 | qt video/quicktime 130 | ra audio/x-pn-realaudio 131 | ram audio/x-pn-realaudio 132 | ras image/x-cmu-raster 133 | rgb image/x-rgb 134 | rmi audio/mid 135 | roff application/x-troff 136 | rtf application/rtf 137 | rtx text/richtext 138 | scd application/x-msschedule 139 | sct text/scriptlet 140 | sh application/x-sh 141 | shar application/x-shar 142 | sit application/x-stuffit 143 | snd audio/basic 144 | spc application/x-pkcs7-certificates 145 | spl application/futuresplash 146 | src application/x-wais-source 147 | sst application/vnd.ms-pkicertstore 148 | stl application/vnd.ms-pkistl 149 | stm text/html 150 | svg image/svg+xml 151 | swf application/x-shockwave-flash 152 | t application/x-troff 153 | tar application/x-tar 154 | tcl application/x-tcl 155 | tex application/x-tex 156 | texi application/x-texinfo 157 | texinfo application/x-texinfo 158 | tgz application/x-compressed 159 | tif image/tiff 160 | tiff image/tiff 161 | tr application/x-troff 162 | trm application/x-msterminal 163 | tsv text/tab-separated-values 164 | txt text/plain 165 | uls text/iuls 166 | ustar application/x-ustar 167 | vcf text/x-vcard 168 | vrml x-world/x-vrml 169 | wav audio/x-wav 170 | wcm application/vnd.ms-works 171 | wdb application/vnd.ms-works 172 | webma audio/webm 173 | wks application/vnd.ms-works 174 | wmf application/x-msmetafile 175 | wps application/vnd.ms-works 176 | wri application/x-mswrite 177 | wrl x-world/x-vrml 178 | wrz x-world/x-vrml 179 | xaf x-world/x-vrml 180 | xbm image/x-xbitmap 181 | xla application/vnd.ms-excel 182 | xlc application/vnd.ms-excel 183 | xlm application/vnd.ms-excel 184 | xls application/vnd.ms-excel 185 | xlt application/vnd.ms-excel 186 | xlw application/vnd.ms-excel 187 | xof x-world/x-vrml 188 | xpm image/x-xpixmap 189 | xwd image/x-xwindowdump 190 | z application/x-compress 191 | zip application/zip -------------------------------------------------------------------------------- /lib/Process.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 22 | } 23 | 24 | /** 25 | * Start the process. 26 | * 27 | * @param \Aerys\Console $console 28 | * @return \Generator 29 | */ 30 | public function start(Console $console): \Generator { 31 | try { 32 | if ($this->state) { 33 | throw new \Error( 34 | "A process may only be started once" 35 | ); 36 | } 37 | 38 | $this->registerSignalHandler(); 39 | $this->registerShutdownHandler(); 40 | $this->registerErrorHandler(); 41 | 42 | $this->state = self::STARTED; 43 | 44 | yield from $this->doStart($console); 45 | 46 | // Once we make it this far we no longer want to terminate 47 | // the process in the event of an uncaught exception inside 48 | // the event loop -- log it instead. 49 | Loop::setErrorHandler([$this->logger, "critical"]); 50 | } catch (\Throwable $uncaught) { 51 | $this->exitCode = 1; 52 | $this->logger->critical($uncaught); 53 | if (method_exists($this->logger, "flush")) { 54 | $this->logger->flush(); 55 | } 56 | static::exit(); 57 | } 58 | } 59 | 60 | /** 61 | * Stop the process. 62 | * 63 | * @return \Generator 64 | */ 65 | public function stop(): \Generator { 66 | try { 67 | switch ($this->state) { 68 | case self::STOPPED: 69 | case self::STOPPING: 70 | return; 71 | case self::STARTED: 72 | break; 73 | } 74 | $this->state = self::STOPPING; 75 | yield from $this->doStop(); 76 | } catch (\Throwable $uncaught) { 77 | $this->exitCode = 1; 78 | $this->logger->critical($uncaught); 79 | } 80 | } 81 | 82 | private function registerSignalHandler() { 83 | if (PHP_SAPI === "phpdbg") { 84 | // phpdbg captures SIGINT so don't bother inside the debugger 85 | return; 86 | } 87 | 88 | $onSignal = [$this, "stop"]; 89 | 90 | $loop = Loop::get()->getHandle(); 91 | if (is_resource($loop) && get_resource_type($loop) == "uv_loop") { 92 | Loop::unreference(Loop::onSignal(\UV::SIGINT, $onSignal)); 93 | Loop::unreference(Loop::onSignal(\UV::SIGTERM, $onSignal)); 94 | } elseif (extension_loaded("pcntl")) { 95 | Loop::unreference(Loop::onSignal(\SIGINT, $onSignal)); 96 | Loop::unreference(Loop::onSignal(\SIGTERM, $onSignal)); 97 | } 98 | } 99 | 100 | private function registerShutdownHandler() { 101 | register_shutdown_function(function () { 102 | try { 103 | if (!$err = \error_get_last()) { 104 | return; 105 | } 106 | 107 | switch ($err["type"]) { 108 | case E_ERROR: 109 | case E_PARSE: 110 | case E_USER_ERROR: 111 | case E_CORE_ERROR: 112 | case E_CORE_WARNING: 113 | case E_COMPILE_ERROR: 114 | case E_COMPILE_WARNING: 115 | case E_RECOVERABLE_ERROR: 116 | break; 117 | default: 118 | return; 119 | } 120 | 121 | $this->exitCode = 1; 122 | $msg = "{$err["message"]} in {$err["file"]} on line {$err["line"]}"; 123 | 124 | $previous = Loop::get(); 125 | 126 | try { 127 | Loop::set((new Loop\DriverFactory)->create()); 128 | Loop::run(function () use ($msg) { 129 | $this->logger->critical($msg); 130 | yield from $this->stop(); 131 | }); 132 | } finally { 133 | Loop::set($previous); 134 | } 135 | } finally { 136 | $this->exit(); 137 | } 138 | }); 139 | } 140 | 141 | private function registerErrorHandler() { 142 | set_error_handler(function ($errno, $msg, $file, $line) { 143 | if (!(error_reporting() & $errno)) { 144 | return; 145 | } 146 | 147 | $msg = "{$msg} in {$file} on line {$line}"; 148 | 149 | switch ($errno) { 150 | case E_ERROR: 151 | case E_PARSE: 152 | case E_USER_ERROR: 153 | case E_CORE_ERROR: 154 | case E_COMPILE_ERROR: 155 | case E_RECOVERABLE_ERROR: 156 | $this->logger->error($msg); 157 | break; 158 | case E_CORE_WARNING: 159 | case E_COMPILE_WARNING: 160 | case E_WARNING: 161 | case E_USER_WARNING: 162 | $this->logger->warning($msg); 163 | break; 164 | case E_NOTICE: 165 | case E_USER_NOTICE: 166 | case E_DEPRECATED: 167 | case E_USER_DEPRECATED: 168 | case E_STRICT: 169 | $this->logger->notice($msg); 170 | break; 171 | default: 172 | $this->logger->warning($msg); 173 | break; 174 | } 175 | }); 176 | } 177 | 178 | /** 179 | * This function only exists as protected so we can test for its invocation. 180 | */ 181 | protected function exit() { 182 | exit($this->exitCode); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /docs/performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Performance Tips 3 | permalink: /performance 4 | --- 5 | ## Bottlenecks 6 | 7 | Aerys in general is not a bottleneck. Misconfiguration, use of blocking I/O or inefficient applications are. 8 | 9 | Aerys is well-optimized and can handle tens of thousands of requests per second on typical hardware while maintaining a high level of concurrency of thousands of clients. 10 | 11 | But that performance will decrease drastically with inefficient applications. Aerys has the nice advantage of classes and handlers being always loaded, so there's no time lost with compilation and initialization. 12 | 13 | A common trap is to begin operating on big data with simple string operations, requiring many inefficient big copies, which is why it is strongly recommended to use [incremental body parsing](#body) when processing larger incoming data, instead of processing the data all at once. 14 | 15 | The problem really is CPU cost. Inefficient I/O management (as long as it is non-blocking!) is just delaying individual requests. It is recommended to dispatch simultaneously and eventually bundle multiple independent I/O requests via Amp combinators, but a slow handler will slow down every other request too. While one handler is computing, all the other handlers can't continue. Thus it is imperative to reduce computation times of the handlers to a minimum. 16 | 17 | ## Disconnecting Clients 18 | 19 | ```php 20 | return (new Aerys\Host)->use(function(Aerys\Request $req, Aerys\Response $res) { 21 | $handle = \Amp\File\open("largefile.txt", "r"); 22 | while (null !== $chunk = yield $handle->read(8192)) { 23 | yield $response->write($chunk); # it will just abort here, when the client disconnects 24 | } 25 | }); 26 | ``` 27 | 28 | `Response::write()` and `Websocket\Endpoint::send()` return a `Promise` which is fulfilled at the first moment where the buffers aren't full. That `Promise` may also fail with a `ClientException` if the clients write stream has been closed. 29 | 30 | This allows to avoid spending too much processing time when fetching and returning large data incrementally as well as having too big buffers. 31 | 32 | Thus, this isn't relevant for most handlers, except the ones possibly generating very much data (on the order of more than a few hundred kilobytes - the lowest size of the buffer typically is at least 64 KiB). 33 | 34 | ## Body 35 | 36 | ```php 37 | use Amp\File; 38 | 39 | return (new Aerys\Host)->use(function (Aerys\Request $req, Aerys\Response $res) { 40 | try { 41 | $path = "test.txt"; 42 | $handle = yield File\open($path, "w+"); 43 | $body = $res->getBody(10 * 1024 ** 2); // 10 MB 44 | 45 | while (null !== $data = yield $body->read()) { 46 | yield $handle->write($data); 47 | } 48 | 49 | $res->end("Data successfully saved"); 50 | } catch (Aerys\ClientException $e) { 51 | # Writes may still arrive, even though reading stopped 52 | if ($e instanceof Aerys\ClientSizeException) { 53 | $res->end("Sent data too big, aborting"); 54 | } else { 55 | $res->end("Data has not been recevied completely."); 56 | } 57 | 58 | yield $handle->close(); // explicit close to avoid issues when unlink()'ing 59 | yield File\unlink($path); 60 | 61 | throw $e; 62 | } 63 | }); 64 | ``` 65 | 66 | `Amp\ByteStream\Message` (and the equivalent `Aerys\Websocket\Message`) also provide incremental access to messages, which is particularly important if you have larger message limits (like tens of megabytes) and don't want to buffer it all in memory. If multiple people are uploading large bodies concurrently, the memory might quickly get exhausted. 67 | 68 | Hence, incremental handling is important, accessible via [the `read()` API of `Amp\ByteStream\Message`](http://amphp.org/byte-stream/message). 69 | 70 | In case a client disconnects, the `Message` instance fails with an `Aerys\ClientException`. This exception is thrown for both the `read()` API and when `yield`ing the `Message`. If the size limits are exceeded, it's a `ClientSizeException` which is a child class of `ClientException`. 71 | 72 | {:.note} 73 | > `ClientException`s do not *need* to be caught. You may catch them if you want to continue, but don't have to. The Server will silently end the request cycle and discard that exception then. 74 | 75 | {:.note} 76 | > This describes only the direct return value of `getBody($size = -1)` respectively the `Aerys\Websocket\Message` usage; there is [similar handling for parsed bodies](classes/bodyparser.md). 77 | 78 | ## BodyParser 79 | 80 | ```php 81 | (new Aerys\Host)->use(function(Aerys\Request $req, Aerys\Response $res) { 82 | try { 83 | $body = Aerys\parseBody($req); 84 | $field = $body->stream("field", 10 * 1024 ** 2); // 10 MB 85 | $name = (yield $field->getMetadata())["name"] ?? ""; 86 | $size = 0; 87 | while (null !== ($data = yield $field->valid())) { 88 | $size += \strlen($data)); 89 | } 90 | $res->end("Received $size bytes for file $name"); 91 | } catch (Aerys\ClientException $e) { 92 | # Writes may still arrive, even though reading stopped 93 | $res->end("Upload failed ...") 94 | throw $e; 95 | } 96 | }); 97 | ``` 98 | 99 | Apart from implementing `Amp\Promise` (to be able to return `Aerys\ParsedBody` upon `yield`), the `Aerys\BodyParser` class (an instance of which is returned by the `Aerys\parseBody()` function) exposes one additional method: 100 | 101 | `stream($field, $size = 0): Aerys\FieldBody` with `$size` being the maximum size of the field (the size is added to the general size passed to `Aerys\parseBody()`). 102 | 103 | This returned `Aerys\FieldBody` instance extends `\Amp\ByteStream\Message` and thus has [the same semantics](http://amphp.org/byte-stream/message). 104 | 105 | Additionally, to provide the metadata information, the `Aerys\FieldBody` class has a `getMetadata()` function to return [the metadata array](http.md#request-bodies). 106 | 107 | The `Aerys\BodyParser::stream()` function can be called multiple times on the same field name in order to fetch all the fields with the same name: 108 | 109 | ```php 110 | # $body being an instance of Aerys\BodyParser 111 | while (null !== $data = yield ($field = $body->stream("field"))->read()) { 112 | # init next entry of that name "field" 113 | do { 114 | # work on $data 115 | } while (null !== $data = yield $field->read()); 116 | } 117 | ``` 118 | -------------------------------------------------------------------------------- /lib/Host.php: -------------------------------------------------------------------------------- 1 | 65535) { 41 | throw new \Error( 42 | "Invalid port number {$port}; integer in the range 1..65535 required" 43 | ); 44 | } 45 | 46 | if ($address === "*") { 47 | if (self::separateIPv4Binding()) { 48 | $this->interfaces[] = ["0.0.0.0", $port]; 49 | } 50 | 51 | $address = "::"; 52 | } 53 | 54 | if (!$isPath && !@inet_pton($address)) { 55 | throw new \Error( 56 | "Invalid IP address or unix domain socket path" 57 | ); 58 | } 59 | 60 | $this->interfaces[] = [$address, $port]; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Assign a domain name (e.g. localhost or mysite.com or subdomain.mysite.com). 67 | * 68 | * An explicit host name is only required if a server exposes more than one host on a given 69 | * interface. If a name is not defined (or "*") the server will allow any hostname. 70 | * 71 | * By default the port must match with the interface. It is possible to explicitly require 72 | * a specific port in the hostname by appending ":port" (e.g. "localhost:8080"). It is also 73 | * possible to specify a wildcard with "*" (e.g. "*:*" to accept any hostname from any port). 74 | * 75 | * @param string $name 76 | * @return self 77 | */ 78 | public function name(string $name): Host { 79 | $this->name = $name === "" ? "*" : $name; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * Use a callable request action or Middleware. 86 | * 87 | * Host actions are invoked to service requests in the order in which they are added. 88 | * 89 | * @param callable|Middleware|Bootable|Monitor $action 90 | * @throws \Error on invalid $action parameter 91 | * @return self 92 | */ 93 | public function use($action): Host { 94 | $isAction = is_callable($action) || $action instanceof Middleware || $action instanceof Bootable || $action instanceof Monitor; 95 | $isDriver = $action instanceof HttpDriver; 96 | 97 | if (!$isAction && !$isDriver) { 98 | throw new \Error( 99 | __METHOD__ . " requires a callable action or Bootable or Middleware or HttpDriver instance" 100 | ); 101 | } 102 | 103 | if ($isAction) { 104 | $this->actions[] = $action; 105 | } 106 | if ($isDriver) { 107 | if ($this->httpDriver) { 108 | throw new \Error( 109 | "Impossible to define two HttpDriver instances for one same Host; an instance of " . get_class($this->httpDriver) . " has already been defined as driver" 110 | ); 111 | } 112 | $this->httpDriver = $action; 113 | } 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Define TLS encryption settings for this host. 120 | * 121 | * @param string $certificate A string path pointing to your SSL/TLS certificate 122 | * @param string|null $key A string path pointing to your SSL/TLS key file (null if the certificate file is containing the key already) 123 | * @param array $options An optional array mapping additional SSL/TLS settings 124 | * @return self 125 | */ 126 | public function encrypt(string $certificate, string $key = null, array $options = []): Host { 127 | unset($options["SNI_server_certs"]); 128 | $options["local_cert"] = $certificate; 129 | if (isset($key)) { 130 | $options["local_pk"] = $key; 131 | } 132 | $this->crypto = $options; 133 | 134 | return $this; 135 | } 136 | 137 | public static function separateIPv4Binding(): bool { 138 | static $separateIPv6 = null; 139 | 140 | if ($separateIPv6 === null) { 141 | // PHP 7.0.0 doesn't have ipv6_v6only socket option yet 142 | if (PHP_VERSION_ID < 70001) { 143 | $separateIPv6 = !file_exists("/proc/sys/net/ipv6/bindv6only") || trim(file_get_contents("/proc/sys/net/ipv6/bindv6only")); 144 | } else { 145 | $separateIPv6 = true; 146 | } 147 | } 148 | 149 | return $separateIPv6; 150 | } 151 | 152 | /** 153 | * Retrieve an associative array summarizing the host definition. 154 | * 155 | * @return array 156 | */ 157 | public function export(): array { 158 | $defaultPort = $this->crypto ? 443 : 80; 159 | 160 | if (isset($this->interfaces)) { 161 | $interfaces = array_unique($this->interfaces, SORT_REGULAR); 162 | } else { 163 | $interfaces = [["::", $defaultPort]]; 164 | if (self::separateIPv4Binding()) { 165 | $interfaces[] = ["0.0.0.0", $defaultPort]; 166 | } 167 | } 168 | 169 | return [ 170 | "interfaces" => $interfaces, 171 | "name" => $this->name, 172 | "crypto" => $this->crypto, 173 | "actions" => $this->actions, 174 | "httpdriver" => $this->httpDriver, 175 | ]; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /lib/VhostContainer.php: -------------------------------------------------------------------------------- 1 | defaultHttpDriver = $driver; 15 | } 16 | 17 | /** 18 | * Add a virtual host to the collection. 19 | * 20 | * @param \Aerys\Vhost $vhost 21 | * @return void 22 | */ 23 | public function use(Vhost $vhost) { 24 | $vhost = clone $vhost; // do not allow change of state after use() 25 | $this->preventCryptoSocketConflict($vhost); 26 | foreach ($vhost->getIds() as $id) { 27 | if (isset($this->vhosts[$id])) { 28 | list($host, $port, $interfaceAddr, $interfacePort) = explode(":", $id); 29 | throw new \LogicException( 30 | $host === "*" 31 | ? "Cannot have two default hosts " . ($interfacePort == $port ? "" : "on port $port ") . "on the same interface ($interfaceAddr:$interfacePort)" 32 | : "Cannot have two hosts with the same name ($host" . ($interfacePort == $port ? "" : ":$port") . ") on the same interface ($interfaceAddr:$interfacePort)" 33 | ); 34 | } 35 | 36 | $this->vhosts[$id] = $vhost; 37 | } 38 | $this->addHttpDriver($vhost); 39 | $this->cachedVhostCount++; 40 | } 41 | 42 | // TLS is inherently bound to a specific interface. Unencrypted wildcard hosts will not work on encrypted interfaces and vice versa. 43 | private function preventCryptoSocketConflict(Vhost $new) { 44 | foreach ($this->vhosts as $old) { 45 | // If both hosts are encrypted or both unencrypted there is no conflict 46 | if ($new->isEncrypted() == $old->isEncrypted()) { 47 | continue; 48 | } 49 | foreach ($old->getInterfaces() as list($address, $port)) { 50 | if (in_array($port, $new->getPorts($address))) { 51 | throw new \Error( 52 | sprintf( 53 | "Cannot register encrypted host `%s`; unencrypted " . 54 | "host `%s` registered on conflicting interface `%s`", 55 | $new->IsEncrypted() ? $new->getName() : $old->getName(), 56 | $new->IsEncrypted() ? $old->getName() : $new->getName(), 57 | "$address:$port" 58 | ) 59 | ); 60 | } 61 | } 62 | } 63 | } 64 | 65 | private function addHttpDriver(Vhost $vhost) { 66 | $driver = $vhost->getHttpDriver() ?? $this->defaultHttpDriver; 67 | foreach ($vhost->getInterfaces() as list($address, $port)) { 68 | $defaultDriver = $this->httpDrivers[$port][$address[0] == "/" ? "" : \strlen(inet_pton($address)) === 4 ? "0.0.0.0" : "::"] ?? $driver; 69 | if (($this->httpDrivers[$port][$address] ?? $defaultDriver) !== $driver) { 70 | throw new \Error( 71 | "Cannot use two different HttpDriver instances on an equivalent address-port pair" 72 | ); 73 | } 74 | if ($address == "0.0.0.0" || $address == "::") { 75 | foreach ($this->httpDrivers[$port] ?? [] as $oldAddr => $oldDriver) { 76 | if ($oldDriver !== $driver && (\strlen(inet_pton($address)) === 4) == ($address == "0.0.0.0")) { 77 | throw new \Error( 78 | "Cannot use two different HttpDriver instances on an equivalent address-port pair" 79 | ); 80 | } 81 | } 82 | } 83 | $this->httpDrivers[$port][$address] = $driver; 84 | } 85 | $hash = spl_object_hash($driver); 86 | if ($this->setupArgs && $this->setupHttpDrivers[$hash] ?? false) { 87 | $driver->setup(...$this->setupArgs); 88 | $this->setupHttpDrivers[$hash] = true; 89 | } 90 | } 91 | 92 | public function setupHttpDrivers(...$args) { 93 | if ($this->setupHttpDrivers) { 94 | throw new \Error("Can setup http drivers only once"); 95 | } 96 | $this->setupArgs = $args; 97 | foreach ($this->httpDrivers as $drivers) { 98 | foreach ($drivers as $driver) { 99 | $hash = spl_object_hash($driver); 100 | if ($this->setupHttpDrivers[$hash] ?? false) { 101 | continue; 102 | } 103 | $this->setupHttpDrivers[$hash] = true; 104 | $driver->setup(...$args); 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Select the suited HttpDriver instance, filtered by address and port pair. 111 | */ 112 | public function selectHttpDriver($address, $port) { 113 | return $this->httpDrivers[$port][$address] ?? 114 | $this->httpDrivers[$port][\strpos($address, ":") === false ? "0.0.0.0" : "::"]; 115 | } 116 | 117 | /** 118 | * Select a virtual host match for the specified request according to RFC 7230 criteria. 119 | * 120 | * Note: For HTTP/1.0 requests (aka omitting a Host header), a proper Vhost will only ever be returned 121 | * if there is a matching wildcard host. 122 | * 123 | * @param \Aerys\InternalRequest $ireq 124 | * @return Vhost|null Returns a Vhost object and boolean TRUE if a valid host selected, FALSE otherwise 125 | * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.2 126 | * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.6.1.1 127 | */ 128 | public function selectHost(InternalRequest $ireq) { 129 | $client = $ireq->client; 130 | $serverId = ":{$client->serverAddr}:{$client->serverPort}"; 131 | 132 | $explicitHostId = "{$ireq->uriHost}:{$ireq->uriPort}{$serverId}"; 133 | if (isset($this->vhosts[$explicitHostId])) { 134 | return $this->vhosts[$explicitHostId]; 135 | } 136 | 137 | $addressWildcardHost = "*:{$ireq->uriPort}{$serverId}"; 138 | if (isset($this->vhosts[$addressWildcardHost])) { 139 | return $this->vhosts[$addressWildcardHost]; 140 | } 141 | 142 | $portWildcardHostId = "{$ireq->uriHost}:0{$serverId}"; 143 | if (isset($this->vhosts[$portWildcardHostId])) { 144 | return $this->vhosts[$portWildcardHostId]; 145 | } 146 | 147 | $addressPortWildcardHost = "*:0{$serverId}"; 148 | if (isset($this->vhosts[$addressPortWildcardHost])) { 149 | return $this->vhosts[$addressPortWildcardHost]; 150 | } 151 | 152 | if ($client->serverAddr[0] == "/") { // unix domain socket 153 | // there is no such thing like interface wildcards for unix domain sockets 154 | return null; // nothing found 155 | } 156 | 157 | $wildcardIP = \strpos($client->serverAddr, ":") === false ? "0.0.0.0" : "[::]"; 158 | $serverId = ":$wildcardIP:{$client->serverPort}"; 159 | 160 | $explicitHostId = "{$ireq->uriHost}:{$ireq->uriPort}{$serverId}"; 161 | if (isset($this->vhosts[$explicitHostId])) { 162 | return $this->vhosts[$explicitHostId]; 163 | } 164 | 165 | $addressWildcardHost = "*:{$ireq->uriPort}{$serverId}"; 166 | if (isset($this->vhosts[$addressWildcardHost])) { 167 | return $this->vhosts[$addressWildcardHost]; 168 | } 169 | 170 | $portWildcardHostId = "{$ireq->uriHost}:0{$serverId}"; 171 | if (isset($this->vhosts[$portWildcardHostId])) { 172 | return $this->vhosts[$portWildcardHostId]; 173 | } 174 | 175 | $addressPortWildcardHost = "*:0{$serverId}"; 176 | if (isset($this->vhosts[$addressPortWildcardHost])) { 177 | return $this->vhosts[$addressPortWildcardHost]; 178 | } 179 | 180 | return null; // nothing found 181 | } 182 | 183 | /** 184 | * Retrieve an array of unique socket addresses on which hosts should listen. 185 | * 186 | * @return array Returns an array of unique host addresses in the form: tcp://ip:port 187 | */ 188 | public function getBindableAddresses(): array { 189 | return array_unique(array_merge(...array_values(array_map(function ($vhost) { 190 | return $vhost->getBindableAddresses(); 191 | }, $this->vhosts)))); 192 | } 193 | 194 | /** 195 | * Retrieve stream encryption settings by bind address. 196 | * 197 | * @return array 198 | */ 199 | public function getTlsBindingsByAddress(): array { 200 | $bindMap = []; 201 | $sniNameMap = []; 202 | foreach ($this->vhosts as $vhost) { 203 | if (!$vhost->isEncrypted()) { 204 | continue; 205 | } 206 | 207 | foreach ($vhost->getBindableAddresses() as $bindAddress) { 208 | $contextArr = $vhost->getTlsContextArr(); 209 | $bindMap[$bindAddress] = $contextArr; 210 | 211 | if ($vhost->hasName()) { 212 | $sniNameMap[$bindAddress][$vhost->getName()] = $contextArr["local_cert"]; 213 | } 214 | } 215 | } 216 | 217 | // If we have multiple different TLS certs on the same bind address we need to assign 218 | // the "SNI_server_name" key to enable the SNI extension. 219 | foreach (array_keys($bindMap) as $bindAddress) { 220 | if (isset($sniNameMap[$bindAddress]) && count($sniNameMap[$bindAddress]) > 1) { 221 | $bindMap[$bindAddress]["SNI_server_name"] = $sniNameMap[$bindAddress]; 222 | } 223 | } 224 | 225 | return $bindMap; 226 | } 227 | 228 | public function count() { 229 | return $this->cachedVhostCount; 230 | } 231 | 232 | public function __debugInfo() { 233 | return [ 234 | "vhosts" => $this->vhosts, 235 | ]; 236 | } 237 | 238 | public function monitor(): array { 239 | return array_map(function ($vhost) { return $vhost->monitor(); }, $this->vhosts); 240 | } 241 | } 242 | --------------------------------------------------------------------------------