Completed
Push — master ( 057455...91afe8 )
by Benjamin
09:02
created

HttpTransport::__construct()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 12
cts 12
cp 1
rs 9.536
c 0
b 0
f 0
cc 3
nc 2
nop 4
crap 3
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
     * Class constructor
68 15
     *
69
     * @param string|null     $host       when NULL or empty default-host is used
70 15
     * @param int|null        $port       when NULL or empty default-port is used
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $port a bit more specific; maybe use integer.
Loading history...
71 15
     * @param string|null     $path       when NULL or empty default-path is used
72 15
     * @param SslOptions|null $sslOptions when null not SSL is used
73
     */
74 15
    public function __construct(
75 1
        $host = self::DEFAULT_HOST,
76 1
        $port = self::DEFAULT_PORT,
77
        $path = self::DEFAULT_PATH,
78 15
        SslOptions $sslOptions = null
79
    ) {
80 15
        $this->host = $host;
81 15
        $this->port = $port;
82 15
        $this->path = $path;
83 15
84 15
        if ($port == self::AUTO_SSL_PORT && $sslOptions == null) {
85 15
            $sslOptions = new SslOptions();
86 15
        }
87 15
88
        $this->sslOptions = $sslOptions;
89
90
        $this->messageEncoder = new DefaultEncoder();
91
        $this->socketClient = new StreamSocketClient(
92
            $this->getScheme(),
93
            $this->host,
94
            $this->port,
95
            $this->getContext()
96
        );
97
    }
98
99
    /**
100
     * Creates a HttpTransport from a URI
101
     *
102
     * Supports http and https schemes, port-, path- and auth-definitions
103 3
     * If the port is ommitted 80 and 443 are used respectively.
104
     * If a username but no password is given, and empty password is used.
105 3
     * If a https URI is given, the provided SslOptions (with a fallback to
106
     * the default SslOptions) are used.
107
     *
108 3
     * @param  string          $url
109 1
     * @param  SslOptions|null $sslOptions
110
     *
111
     * @return HttpTransport
112
     */
113 2
    public static function fromUrl($url, SslOptions $sslOptions = null)
114 2
    {
115 1
        $parsed = parse_url($url);
116
        
117
        // check it's a valid URL
118
        if (false === $parsed || !isset($parsed['host']) || !isset($parsed['scheme'])) {
119 1
            throw new \InvalidArgumentException("$url is not a valid URL");
120
        }
121
        
122 1
        // check it's http or https
123 1
        $scheme = strtolower($parsed['scheme']);
124 1
        if (!in_array($scheme, array('http', 'https'))) {
125 1
            throw new \InvalidArgumentException("$url is not a valid http/https URL");
126
        }
127
128 1
        // setup defaults
129 1
        $defaults = array('port' => 80, 'path' => '', 'user' => null, 'pass' => '');
130
131
        // change some defaults for https
132 1
        if ($scheme == 'https') {
133 1
            $sslOptions = $sslOptions ?: new SslOptions();
134 1
            $defaults['port'] = 443;
135
        }
136 1
         
137
        // merge defaults and real data and build transport
138
        $parsed = array_merge($defaults, $parsed);
139
        $transport = new static($parsed['host'], $parsed['port'], $parsed['path'], $sslOptions);
140
141
        // add optional authentication
142
        if ($parsed['user']) {
143
            $transport->setAuthentication($parsed['user'], $parsed['pass']);
144
        }
145 2
146
        return $transport;
147 2
    }
148 2
149
    /**
150
     * Sets HTTP basic authentication
151
     *
152
     * @param string $username
153
     * @param string $password
154
     */
155
    public function setAuthentication($username, $password)
156
    {
157 6
        $this->authentication = $username . ":" . $password;
158
    }
159 6
160 6
    /**
161
     * Sends a Message over this transport
162
     *
163 6
     * @param MessageInterface $message
164 6
     *
165 6
     * @return int the number of bytes sent
166 6
     */
167 6
    public function send(MessageInterface $message)
168
    {
169 6
        $messageEncoder = $this->getMessageEncoder();
170
        $rawMessage = $messageEncoder->encode($message);
171 6
172 1
        $request = array(
173 1
            sprintf("POST %s HTTP/1.1", $this->path),
174
            sprintf("Host: %s:%d", $this->host, $this->port),
175 6
            sprintf("Content-Length: %d", strlen($rawMessage)),
176 1
            "Content-Type: application/json",
177 1
            "Connection: Keep-Alive",
178
            "Accept: */*"
179 6
        );
180 6
181
        if (null !== $this->authentication) {
182 6
            $request[] = "Authorization: Basic " . base64_encode($this->authentication);
183
        }
184 6
185 6
        if ($messageEncoder instanceof CompressedJsonEncoder) {
186
            $request[] = "Content-Encoding: gzip";
187
        }
188
189 6
        $request[] = ""; // blank line to separate headers from body
190 3
        $request[] = $rawMessage;
191 3
192
        $request = implode($request, "\r\n");
193 6
194 1
        $byteCount = $this->socketClient->write($request);
195 1
        $headers = $this->readResponseHeaders();
196 1
197 1
        // if we don't have a HTTP/1.1 connection, or the server decided to close the connection
198 1
        // we should do so as well. next read/write-attempt will open a new socket in this case.
199 1
        if (strpos($headers, "HTTP/1.1") !== 0 || preg_match("!Connection:\s*Close!i", $headers)) {
200
            $this->socketClient->close();
201
        }
202 5
203
        if (!preg_match("!^HTTP/1.\d 202 Accepted!i", $headers)) {
204
            throw new RuntimeException(
205
                sprintf(
206
                    "Graylog-Server didn't answer properly, expected 'HTTP/1.x 202 Accepted', response is '%s'",
207
                    trim($headers)
208 6
                )
209
            );
210 6
        }
211 6
212 6
        return $byteCount;
213
    }
214
215 6
    /**
216 6
     * @return string
217 6
     */
218
    private function readResponseHeaders()
219 6
    {
220
        $chunkSize = 1024; // number of bytes to read at once
221 6
        $delimiter = "\r\n\r\n"; // delimiter between headers and rsponse
222
        $response = "";
223
224
        do {
225
            $chunk = $this->socketClient->read($chunkSize);
226
            $response .= $chunk;
227 15
        } while (false === strpos($chunk, $delimiter) && strlen($chunk) > 0);
228
229 15
        $elements = explode($delimiter, $response, 2);
230
231
        return $elements[0];
232
    }
233
234
    /**
235 15
     * @return string
236
     */
237 15
    private function getScheme()
238 15
    {
239
        return null === $this->sslOptions ? 'tcp' : 'ssl';
240
    }
241 3
242
    /**
243
     * @return array
244
     */
245
    private function getContext()
246
    {
247
        if (null === $this->sslOptions) {
248
            return array();
249 1
        }
250
251 1
        return $this->sslOptions->toStreamContext($this->host);
252 1
    }
253
254
    /**
255
     * Sets the connect-timeout
256
     *
257
     * @param int $timeout
258
     */
259 1
    public function setConnectTimeout($timeout)
260
    {
261 1
        $this->socketClient->setConnectTimeout($timeout);
262
    }
263
264
    /**
265
     * Returns the connect-timeout
266
     *
267
     * @return int
268
     */
269
    public function getConnectTimeout()
270
    {
271
        return $this->socketClient->getConnectTimeout();
272
    }
273
}
274