1<?php
2namespace Ratchet\Server;
3use Ratchet\MessageComponentInterface;
4use React\EventLoop\LoopInterface;
5use React\Socket\ServerInterface;
6use React\EventLoop\Factory as LoopFactory;
7use React\Socket\Server as Reactor;
8use React\Socket\SecureServer as SecureReactor;
9
10/**
11 * Creates an open-ended socket to listen on a port for incoming connections.
12 * Events are delegated through this to attached applications
13 */
14class IoServer {
15    /**
16     * @var \React\EventLoop\LoopInterface
17     */
18    public $loop;
19
20    /**
21     * @var \Ratchet\MessageComponentInterface
22     */
23    public $app;
24
25    /**
26     * The socket server the Ratchet Application is run off of
27     * @var \React\Socket\ServerInterface
28     */
29    public $socket;
30
31    /**
32     * @param \Ratchet\MessageComponentInterface  $app      The Ratchet application stack to host
33     * @param \React\Socket\ServerInterface       $socket   The React socket server to run the Ratchet application off of
34     * @param \React\EventLoop\LoopInterface|null $loop     The React looper to run the Ratchet application off of
35     */
36    public function __construct(MessageComponentInterface $app, ServerInterface $socket, LoopInterface $loop = null) {
37        if (false === strpos(PHP_VERSION, "hiphop")) {
38            gc_enable();
39        }
40
41        set_time_limit(0);
42        ob_implicit_flush();
43
44        $this->loop = $loop;
45        $this->app  = $app;
46        $this->socket = $socket;
47
48        $socket->on('connection', array($this, 'handleConnect'));
49    }
50
51    /**
52     * @param  \Ratchet\MessageComponentInterface $component  The application that I/O will call when events are received
53     * @param  int                                $port       The port to server sockets on
54     * @param  string                             $address    The address to receive sockets on (0.0.0.0 means receive connections from any)
55     * @return IoServer
56     */
57    public static function factory(MessageComponentInterface $component, $port = 80, $address = '0.0.0.0') {
58        $loop   = LoopFactory::create();
59        $socket = new Reactor($address . ':' . $port, $loop);
60
61        return new static($component, $socket, $loop);
62    }
63
64    /**
65     * Run the application by entering the event loop
66     * @throws \RuntimeException If a loop was not previously specified
67     */
68    public function run() {
69        if (null === $this->loop) {
70            throw new \RuntimeException("A React Loop was not provided during instantiation");
71        }
72
73        // @codeCoverageIgnoreStart
74        $this->loop->run();
75        // @codeCoverageIgnoreEnd
76    }
77
78    /**
79     * Triggered when a new connection is received from React
80     * @param \React\Socket\ConnectionInterface $conn
81     */
82    public function handleConnect($conn) {
83        $conn->decor = new IoConnection($conn);
84        $conn->decor->resourceId = (int)$conn->stream;
85
86        $uri = $conn->getRemoteAddress();
87        $conn->decor->remoteAddress = trim(
88            parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri, PHP_URL_HOST),
89            '[]'
90        );
91
92        $this->app->onOpen($conn->decor);
93
94        $conn->on('data', function ($data) use ($conn) {
95            $this->handleData($data, $conn);
96        });
97        $conn->on('close', function () use ($conn) {
98            $this->handleEnd($conn);
99        });
100        $conn->on('error', function (\Exception $e) use ($conn) {
101            $this->handleError($e, $conn);
102        });
103    }
104
105    /**
106     * Data has been received from React
107     * @param string                            $data
108     * @param \React\Socket\ConnectionInterface $conn
109     */
110    public function handleData($data, $conn) {
111        try {
112            $this->app->onMessage($conn->decor, $data);
113        } catch (\Exception $e) {
114            $this->handleError($e, $conn);
115        }
116    }
117
118    /**
119     * A connection has been closed by React
120     * @param \React\Socket\ConnectionInterface $conn
121     */
122    public function handleEnd($conn) {
123        try {
124            $this->app->onClose($conn->decor);
125        } catch (\Exception $e) {
126            $this->handleError($e, $conn);
127        }
128
129        unset($conn->decor);
130    }
131
132    /**
133     * An error has occurred, let the listening application know
134     * @param \Exception                        $e
135     * @param \React\Socket\ConnectionInterface $conn
136     */
137    public function handleError(\Exception $e, $conn) {
138        $this->app->onError($conn->decor, $e);
139    }
140}
141