HttpTransport::fromUrl()   B
last analyzed

Complexity

Conditions 8
Paths 6

Size

Total Lines 34
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 8
eloc 15
c 2
b 1
f 0
nc 6
nop 2
dl 0
loc 34
rs 8.4444
ccs 12
cts 12
cp 1
crap 8
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
    private const DEFAULT_HOST = "127.0.0.1";
31
    private const DEFAULT_PORT = 12202;
32
    private const DEFAULT_PATH = "/gelf";
33
    private const AUTO_SSL_PORT = 443;
34
35
    private StreamSocketClient $socketClient;
36
37
    private ?string $authentication = null;
38
    private ?string $proxyUri = null;
39
    private ?bool $requestFullUri = false;
40
41
    public function __construct(
42
        private string $host = self::DEFAULT_HOST,
43
        private int $port = self::DEFAULT_PORT,
44
        private string $path = self::DEFAULT_PATH,
45
        private ?SslOptions $sslOptions = null
46
    ) {
47
        parent::__construct();
48
49
        if ($port == self::AUTO_SSL_PORT && $sslOptions === null) {
50
            $this->sslOptions = new SslOptions();
51
        }
52
53
        $this->socketClient = new StreamSocketClient(
54
            $this->getScheme(),
55
            $this->host,
56
            $this->port,
57
            $this->getContext()
58
        );
59
    }
60
61
    /**
62
     * Creates a HttpTransport from a URI
63
     *
64
     * Supports http and https schemes, port-, path- and auth-definitions
65
     * If the port is omitted 80 and 443 are used respectively.
66
     * If a username but no password is given, and empty password is used.
67
     * If a https URI is given, the provided SslOptions (with a fallback to
68
     * the default SslOptions) are used.
69
     */
70
    public static function fromUrl(string $url, ?SslOptions $sslOptions = null): self
71
    {
72
        $parsed = parse_url($url);
73
        
74
        // check it's a valid URL
75
        if (false === $parsed || !isset($parsed['host']) || !isset($parsed['scheme'])) {
76
            throw new \InvalidArgumentException("$url is not a valid URL");
77
        }
78
        
79
        // check it's http or https
80
        $scheme = strtolower($parsed['scheme']);
81
        if (!in_array($scheme, ['http', 'https'])) {
82
            throw new \InvalidArgumentException("$url is not a valid http/https URL");
83
        }
84 16
85
        // setup defaults
86
        $defaults = ['port' => 80, 'path' => '', 'user' => null, 'pass' => ''];
87
88
        // change some defaults for https
89
        if ($scheme == 'https') {
90 16
            $sslOptions = $sslOptions ?: new SslOptions();
91 16
            $defaults['port'] = 443;
92 16
        }
93
         
94 16
        // merge defaults and real data and build transport
95 1
        $parsed = array_merge($defaults, $parsed);
96
        $transport = new self($parsed['host'], $parsed['port'], $parsed['path'], $sslOptions);
97
98 16
        // add optional authentication
99 16
        if ($parsed['user']) {
100 16
            $transport->setAuthentication($parsed['user'], $parsed['pass']);
101 16
        }
102 16
103 16
        return $transport;
104 16
    }
105
106 16
    /**
107
     * Sets HTTP basic authentication
108
     */
109
    public function setAuthentication(string $username, string $password): void
110
    {
111
        $this->authentication = $username . ":" . $password;
112
    }
113
114
    /**
115
     * Enables HTTP proxy
116
     */
117
    public function setProxy(string $proxyUri, bool $requestFullUri = false): void
118
    {
119
        $this->proxyUri = $proxyUri;
120
        $this->requestFullUri = $requestFullUri;
121
122 3
        $this->socketClient->setContext($this->getContext());
123
    }
124 3
125
    /**
126
     * @inheritDoc
127 3
     */
128 1
    public function send(MessageInterface $message): int
129
    {
130
        $messageEncoder = $this->getMessageEncoder();
131
        $rawMessage = $messageEncoder->encode($message);
132 2
133 2
        $request = [
134 1
            sprintf("POST %s HTTP/1.1", $this->path),
135
            sprintf("Host: %s:%d", $this->host, $this->port),
136
            sprintf("Content-Length: %d", strlen($rawMessage)),
137
            "Content-Type: application/json",
138 1
            "Connection: Keep-Alive",
139
            "Accept: */*"
140
        ];
141 1
142 1
        if (null !== $this->authentication) {
143 1
            $request[] = "Authorization: Basic " . base64_encode($this->authentication);
144
        }
145
146
        if ($messageEncoder instanceof CompressedJsonEncoder) {
147 1
            $request[] = "Content-Encoding: gzip";
148 1
        }
149
150
        $request[] = ""; // blank line to separate headers from body
151 1
        $request[] = $rawMessage;
152 1
153
        $request = implode("\r\n", $request);
154
155 1
        $byteCount = $this->socketClient->write($request);
156
        $headers = $this->readResponseHeaders();
157
158
        // if we don't have a HTTP/1.1 connection, or the server decided to close the connection
159
        // we should do so as well. next read/write-attempt will open a new socket in this case.
160
        if (!str_starts_with($headers, "HTTP/1.1") || preg_match("!Connection:\s*Close!i", $headers)) {
161
            $this->socketClient->close();
162
        }
163
164 2
        if (!preg_match("!^HTTP/1.\d 202 Accepted!i", $headers)) {
165
            throw new RuntimeException(
166 2
                sprintf(
167 2
                    "Graylog-Server didn't answer properly, expected 'HTTP/1.x 202 Accepted', response is '%s'",
168
                    trim($headers)
169
                )
170
            );
171
        }
172
173
        return $byteCount;
174
    }
175 1
176
    private function readResponseHeaders(): string
177 1
    {
178 1
        $chunkSize = 1024; // number of bytes to read at once
179
        $delimiter = "\r\n\r\n"; // delimiter between headers and response
180 1
        $response = "";
181 1
182
        do {
183
            $chunk = $this->socketClient->read($chunkSize);
184
            $response .= $chunk;
185
        } while (!str_contains($chunk, $delimiter) && strlen($chunk) > 0);
186
187
        $elements = explode($delimiter, $response, 2);
188
189
        return $elements[0];
190 6
    }
191
192 6
    private function getScheme(): string
193 6
    {
194
        return null === $this->sslOptions ? 'tcp' : 'ssl';
195
    }
196 6
197 6
    private function getContext(): array
198 6
    {
199 6
        $options = [];
200 6
201 6
        if (null !== $this->sslOptions) {
202
            $options = array_merge($options, $this->sslOptions->toStreamContext($this->host));
203
        }
204 6
205 1
        if (null !== $this->proxyUri) {
206
            $options['http'] = [
207
                'proxy' => $this->proxyUri,
208 6
                'request_fulluri' => $this->requestFullUri
209 1
            ];
210
        }
211
212 6
        return $options;
213 6
    }
214
215 6
    /**
216
     * Sets the connect-timeout
217 6
     */
218 6
    public function setConnectTimeout(int $timeout): void
219
    {
220
        $this->socketClient->setConnectTimeout($timeout);
221
    }
222 6
223 3
    /**
224
     * Returns the connect-timeout
225
     */
226 6
    public function getConnectTimeout(): int
227 1
    {
228
        return $this->socketClient->getConnectTimeout();
229 1
    }
230
}
231