Passed
Push — master ( 46729e...9da378 )
by Benjamin
05:02 queued 03:07
created

HttpTransport   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Test Coverage

Coverage 98.88%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 90
c 4
b 1
f 0
dl 0
loc 276
ccs 88
cts 89
cp 0.9888
rs 10
wmc 29

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getScheme() 0 3 2
A getConnectTimeout() 0 3 1
A setAuthentication() 0 3 1
A setProxy() 0 6 1
A setConnectTimeout() 0 3 1
A getContext() 0 16 3
A readResponseHeaders() 0 14 3
B fromUrl() 0 34 8
A __construct() 0 21 3
B send() 0 46 6
1
<?php
2
3
/*
4
 * This file is part of the php-gelf package.
5
 *
6
 * (c) Benjamin Zikarsky <http://benjamin-zikarsky.de>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Gelf\Transport;
13
14
use Gelf\MessageInterface;
15
use Gelf\Encoder\CompressedJsonEncoder;
16
use Gelf\Encoder\JsonEncoder as DefaultEncoder;
17
use RuntimeException;
18
19
/**
20
 * HttpTransport allows the transfer of GELF-messages to an compatible
21
 * GELF-HTTP-backend as described in
22
 * http://www.graylog2.org/resources/documentation/sending/gelfhttp
23
 *
24
 * It can also act as a direct publisher
25
 *
26
 * @author Benjamin Zikarsky <[email protected]>
27
 */
28
class HttpTransport extends AbstractTransport
29
{
30
    const DEFAULT_HOST = "127.0.0.1";
31
    const DEFAULT_PORT = 12202;
32
    const DEFAULT_PATH = "/gelf";
33
    
34
    const AUTO_SSL_PORT = 443;
35
    
36
    /**
37
     * @var string
38
     */
39
    protected $host;
40
41
    /**
42
     * @var int
43
     */
44
    protected $port;
45
46
    /**
47
     * @var string
48
     */
49
    protected $path;
50
51
    /**
52
     * @var StreamSocketClient
53
     */
54
    protected $socketClient;
55
56
    /**
57
     * @var SslOptions|null
58
     */
59
    protected $sslOptions = null;
60
61
    /**
62
     * @var string|null
63
     */
64
    protected $authentication = null;
65
66
    /**
67
     * @var string|null
68
     */
69
    protected $proxyUri = null;
70
71
    /**
72
     * @var bool
73
     */
74
    protected $requestFullUri = false;
75
76
    /**
77
     * Class constructor
78
     *
79
     * @param string|null     $host       when NULL or empty default-host is used
80
     * @param int|null        $port       when NULL or empty default-port is used
81
     * @param string|null     $path       when NULL or empty default-path is used
82
     * @param SslOptions|null $sslOptions when null not SSL is used
83
     */
84 16
    public function __construct(
85
        $host = self::DEFAULT_HOST,
86
        $port = self::DEFAULT_PORT,
87
        $path = self::DEFAULT_PATH,
88
        SslOptions $sslOptions = null
89
    ) {
90 16
        $this->host = $host;
91 16
        $this->port = $port;
92 16
        $this->path = $path;
93
94 16
        if ($port == self::AUTO_SSL_PORT && $sslOptions == null) {
95 1
            $sslOptions = new SslOptions();
96
        }
97
98 16
        $this->sslOptions = $sslOptions;
99 16
        $this->messageEncoder = new DefaultEncoder();
100 16
        $this->socketClient = new StreamSocketClient(
101 16
            $this->getScheme(),
102 16
            $this->host,
103 16
            $this->port,
104 16
            $this->getContext()
105
        );
106 16
    }
107
108
    /**
109
     * Creates a HttpTransport from a URI
110
     *
111
     * Supports http and https schemes, port-, path- and auth-definitions
112
     * If the port is omitted 80 and 443 are used respectively.
113
     * If a username but no password is given, and empty password is used.
114
     * If a https URI is given, the provided SslOptions (with a fallback to
115
     * the default SslOptions) are used.
116
     *
117
     * @param  string          $url
118
     * @param  SslOptions|null $sslOptions
119
     *
120
     * @return HttpTransport
121
     */
122 3
    public static function fromUrl($url, SslOptions $sslOptions = null)
123
    {
124 3
        $parsed = parse_url($url);
125
        
126
        // check it's a valid URL
127 3
        if (false === $parsed || !isset($parsed['host']) || !isset($parsed['scheme'])) {
128 1
            throw new \InvalidArgumentException("$url is not a valid URL");
129
        }
130
        
131
        // check it's http or https
132 2
        $scheme = strtolower($parsed['scheme']);
133 2
        if (!in_array($scheme, array('http', 'https'))) {
134 1
            throw new \InvalidArgumentException("$url is not a valid http/https URL");
135
        }
136
137
        // setup defaults
138 1
        $defaults = array('port' => 80, 'path' => '', 'user' => null, 'pass' => '');
139
140
        // change some defaults for https
141 1
        if ($scheme == 'https') {
142 1
            $sslOptions = $sslOptions ?: new SslOptions();
143 1
            $defaults['port'] = 443;
144
        }
145
         
146
        // merge defaults and real data and build transport
147 1
        $parsed = array_merge($defaults, $parsed);
148 1
        $transport = new static($parsed['host'], $parsed['port'], $parsed['path'], $sslOptions);
149
150
        // add optional authentication
151 1
        if ($parsed['user']) {
152 1
            $transport->setAuthentication($parsed['user'], $parsed['pass']);
153
        }
154
155 1
        return $transport;
156
    }
157
158
    /**
159
     * Sets HTTP basic authentication
160
     *
161
     * @param string $username
162
     * @param string $password
163
     */
164 2
    public function setAuthentication($username, $password)
165
    {
166 2
        $this->authentication = $username . ":" . $password;
167 2
    }
168
169
    /**
170
     * Enables HTTP proxy
171
     *
172
     * @param $proxyUri
173
     * @param bool $requestFullUri
174
     */
175 1
    public function setProxy($proxyUri, $requestFullUri = false)
176
    {
177 1
        $this->proxyUri = $proxyUri;
178 1
        $this->requestFullUri = $requestFullUri;
179
180 1
        $this->socketClient->setContext($this->getContext());
181 1
    }
182
183
    /**
184
     * Sends a Message over this transport
185
     *
186
     * @param MessageInterface $message
187
     *
188
     * @return int the number of bytes sent
189
     */
190 6
    public function send(MessageInterface $message)
191
    {
192 6
        $messageEncoder = $this->getMessageEncoder();
193 6
        $rawMessage = $messageEncoder->encode($message);
194
195
        $request = array(
196 6
            sprintf("POST %s HTTP/1.1", $this->path),
197 6
            sprintf("Host: %s:%d", $this->host, $this->port),
198 6
            sprintf("Content-Length: %d", strlen($rawMessage)),
199 6
            "Content-Type: application/json",
200 6
            "Connection: Keep-Alive",
201 6
            "Accept: */*"
202
        );
203
204 6
        if (null !== $this->authentication) {
205 1
            $request[] = "Authorization: Basic " . base64_encode($this->authentication);
206
        }
207
208 6
        if ($messageEncoder instanceof CompressedJsonEncoder) {
209 1
            $request[] = "Content-Encoding: gzip";
210
        }
211
212 6
        $request[] = ""; // blank line to separate headers from body
213 6
        $request[] = $rawMessage;
214
215 6
        $request = implode("\r\n", $request);
216
217 6
        $byteCount = $this->socketClient->write($request);
218 6
        $headers = $this->readResponseHeaders();
219
220
        // if we don't have a HTTP/1.1 connection, or the server decided to close the connection
221
        // we should do so as well. next read/write-attempt will open a new socket in this case.
222 6
        if (strpos($headers, "HTTP/1.1") !== 0 || preg_match("!Connection:\s*Close!i", $headers)) {
223 3
            $this->socketClient->close();
224
        }
225
226 6
        if (!preg_match("!^HTTP/1.\d 202 Accepted!i", $headers)) {
227 1
            throw new RuntimeException(
228
                sprintf(
229 1
                    "Graylog-Server didn't answer properly, expected 'HTTP/1.x 202 Accepted', response is '%s'",
230 1
                    trim($headers)
231
                )
232
            );
233
        }
234
235 5
        return $byteCount;
236
    }
237
238
    /**
239
     * @return string
240
     */
241 6
    private function readResponseHeaders()
242
    {
243 6
        $chunkSize = 1024; // number of bytes to read at once
244 6
        $delimiter = "\r\n\r\n"; // delimiter between headers and response
245 6
        $response = "";
246
247
        do {
248 6
            $chunk = $this->socketClient->read($chunkSize);
249 6
            $response .= $chunk;
250 6
        } while (false === strpos($chunk, $delimiter) && strlen($chunk) > 0);
251
252 6
        $elements = explode($delimiter, $response, 2);
253
254 6
        return $elements[0];
255
    }
256
257
    /**
258
     * @return string
259
     */
260 16
    private function getScheme()
261
    {
262 16
        return null === $this->sslOptions ? 'tcp' : 'ssl';
263
    }
264
265
    /**
266
     * @return array
267
     */
268 16
    private function getContext()
269
    {
270 16
        $options = array();
271
272 16
        if (null !== $this->sslOptions) {
273 3
            $options = array_merge($options, $this->sslOptions->toStreamContext($this->host));
274
        }
275
276 16
        if (null !== $this->proxyUri) {
277 1
            $options['http'] = array(
278 1
                'proxy' => $this->proxyUri,
279 1
                'request_fulluri' => $this->requestFullUri
280
            );
281
        }
282
283 16
        return $options;
284
    }
285
286
    /**
287
     * Sets the connect-timeout
288
     *
289
     * @param int $timeout
290
     */
291 1
    public function setConnectTimeout($timeout)
292
    {
293 1
        $this->socketClient->setConnectTimeout($timeout);
294 1
    }
295
296
    /**
297
     * Returns the connect-timeout
298
     *
299
     * @return int
300
     */
301 1
    public function getConnectTimeout()
302
    {
303 1
        return $this->socketClient->getConnectTimeout();
304
    }
305
}
306