forked from varspool/Wrench
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathProtocol.php
More file actions
754 lines (644 loc) · 22 KB
/
Copy pathProtocol.php
File metadata and controls
754 lines (644 loc) · 22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
<?php
namespace Wrench\Protocol;
use Exception;
use InvalidArgumentException;
use Wrench\Exception\BadRequestException;
use Wrench\Exception\HandshakeException;
use Wrench\Payload\Payload;
/**
* Definitions and implementation helpers for the Wrenchs protocol.
*
* @see https://datatracker.ietf.org/doc/html/rfc6455
*/
abstract class Protocol
{
/**
* Relevant schemes.
*/
public const SCHEME_WEBSOCKET = 'ws';
public const SCHEME_WEBSOCKET_SECURE = 'wss';
public const SCHEME_UNDERLYING = 'tcp';
public const SCHEME_UNDERLYING_SECURE = 'tls';
/**
* HTTP headers.
*/
public const HEADER_HOST = 'host';
public const HEADER_KEY = 'sec-websocket-key';
public const HEADER_PROTOCOL = 'sec-websocket-protocol';
public const HEADER_VERSION = 'sec-websocket-version';
public const HEADER_ACCEPT = 'sec-websocket-accept';
public const HEADER_EXTENSIONS = 'sec-websocket-extensions';
public const HEADER_ORIGIN = 'origin';
public const HEADER_CONNECTION = 'connection';
public const HEADER_UPGRADE = 'upgrade';
/**
* HTTP error statuses.
*/
public const HTTP_SWITCHING_PROTOCOLS = 101;
public const HTTP_BAD_REQUEST = 400;
public const HTTP_UNAUTHORIZED = 401;
public const HTTP_FORBIDDEN = 403;
public const HTTP_NOT_FOUND = 404;
public const HTTP_RATE_LIMITED = 420;
public const HTTP_SERVER_ERROR = 500;
public const HTTP_NOT_IMPLEMENTED = 501;
/**
* Close statuses.
*
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.4
*/
public const CLOSE_NORMAL = 1000;
public const CLOSE_GOING_AWAY = 1001;
public const CLOSE_PROTOCOL_ERROR = 1002;
public const CLOSE_DATA_INVALID = 1003;
public const CLOSE_RESERVED = 1004;
public const CLOSE_RESERVED_NONE = 1005;
public const CLOSE_RESERVED_ABNORM = 1006;
public const CLOSE_DATA_INCONSISTENT = 1007;
public const CLOSE_POLICY_VIOLATION = 1008;
public const CLOSE_MESSAGE_TOO_BIG = 1009;
public const CLOSE_EXTENSION_NEEDED = 1010;
public const CLOSE_UNEXPECTED = 1011;
public const CLOSE_RESERVED_TLS = 1015;
/**
* Frame types
* %x0 denotes a continuation frame
* %x1 denotes a text frame
* %x2 denotes a binary frame
* %x3-7 are reserved for further non-control frames
* %x8 denotes a connection close
* %x9 denotes a ping
* %xA denotes a pong
* %xB-F are reserved for further control frames.
*/
public const TYPE_CONTINUATION = 0;
public const TYPE_TEXT = 1;
public const TYPE_BINARY = 2;
public const TYPE_RESERVED_3 = 3;
public const TYPE_RESERVED_4 = 4;
public const TYPE_RESERVED_5 = 5;
public const TYPE_RESERVED_6 = 6;
public const TYPE_RESERVED_7 = 7;
public const TYPE_CLOSE = 8;
public const TYPE_PING = 9;
public const TYPE_PONG = 10;
public const TYPE_RESERVED_11 = 11;
public const TYPE_RESERVED_12 = 12;
public const TYPE_RESERVED_13 = 13;
public const TYPE_RESERVED_14 = 14;
public const TYPE_RESERVED_15 = 15;
/**
* Used in the WebSocket accept header.
*/
public const MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
/**
* The request MUST contain an |Upgrade| header field whose value
* MUST include the "websocket" keyword.
*/
public const UPGRADE_VALUE = 'websocket';
/**
* The request MUST contain a |Connection| header field whose value
* MUST include the "Upgrade" token.
*/
public const CONNECTION_VALUE = 'Upgrade';
/**
* printf compatible, passed request path string.
*/
public const REQUEST_LINE_FORMAT = 'GET %s HTTP/1.1';
/**
* Used for parsing requested path. preg_* compatible.
*/
public const REQUEST_LINE_REGEX = '/^GET (\S+) HTTP\/1\.1$/D';
/**
* printf compatible.
*/
public const RESPONSE_LINE_FORMAT = 'HTTP/1.1 %d %s';
/**
* printf compatible, passed header name and value.
*/
public const HEADER_LINE_FORMAT = '%s: %s';
public const CLOSE_REASONS = [
self::CLOSE_NORMAL => 'normal close',
self::CLOSE_GOING_AWAY => 'going away',
self::CLOSE_PROTOCOL_ERROR => 'protocol error',
self::CLOSE_DATA_INVALID => 'data invalid',
self::CLOSE_DATA_INCONSISTENT => 'data inconsistent',
self::CLOSE_POLICY_VIOLATION => 'policy violation',
self::CLOSE_MESSAGE_TOO_BIG => 'message too big',
self::CLOSE_EXTENSION_NEEDED => 'extension needed',
self::CLOSE_UNEXPECTED => 'unexpected error',
self::CLOSE_RESERVED => null, // Don't use these!
self::CLOSE_RESERVED_NONE => null,
self::CLOSE_RESERVED_ABNORM => null,
self::CLOSE_RESERVED_TLS => null,
];
public const FRAME_TYPES = [
'continuation' => self::TYPE_CONTINUATION,
'text' => self::TYPE_TEXT,
'binary' => self::TYPE_BINARY,
'close' => self::TYPE_CLOSE,
'ping' => self::TYPE_PING,
'pong' => self::TYPE_PONG,
];
public const HTTP_RESPONSES = [
self::HTTP_SWITCHING_PROTOCOLS => 'Switching Protocols',
self::HTTP_BAD_REQUEST => 'Bad Request',
self::HTTP_UNAUTHORIZED => 'Unauthorized',
self::HTTP_FORBIDDEN => 'Forbidden',
self::HTTP_NOT_FOUND => 'Not Found',
self::HTTP_NOT_IMPLEMENTED => 'Not Implemented',
self::HTTP_RATE_LIMITED => 'Enhance Your Calm',
];
/**
* Valid schemes.
*
* @var list<string>
*/
protected static $schemes = [
self::SCHEME_WEBSOCKET,
self::SCHEME_WEBSOCKET_SECURE,
self::SCHEME_UNDERLYING,
self::SCHEME_UNDERLYING_SECURE,
];
/**
* Generates a key suitable for use in the protocol.
*
* This base implementation returns a 16-byte (128 bit) random key as a
* binary string.
*/
public function generateKey(): string
{
return \base64_encode(\random_bytes(16));
}
/**
* Gets request handshake string
* The leading line from the client follows the Request-Line format.
* The leading line from the server follows the Status-Line format. The
* Request-Line and Status-Line productions are defined in [RFC2616].
* An unordered set of header fields comes after the leading line in
* both cases. The meaning of these header fields is specified in
* Section 4 of this document. Additional header fields may also be
* present, such as cookies [RFC6265]. The format and parsing of
* headers is as defined in [RFC2616].
*
* @param string $uri WebSocket URI, e.g. ws://example.org:8000/chat
* @param string $key 16 byte binary string key
* @param string $origin Origin of the request
*/
public function getRequestHandshake(
string $uri,
string $key,
string $origin,
array $headers = []
): string {
if (!$uri || !$key || !$origin) {
throw new InvalidArgumentException('You must supply a URI, key and origin');
}
[$scheme, $host, $port, $path, $query] = self::validateUri($uri);
if ($query) {
$path .= '?'.$query;
}
if (self::SCHEME_WEBSOCKET == $scheme && 80 == $port) {
// do nothing
} elseif (self::SCHEME_WEBSOCKET_SECURE == $scheme && 443 == $port) {
// do nothing
} else {
$host = $host.':'.$port;
}
$handshake = [
\sprintf(self::REQUEST_LINE_FORMAT, $path),
];
$headers = \array_merge(
$this->getDefaultRequestHeaders(
$host,
$key,
$origin
),
$headers
);
foreach ($headers as $name => $value) {
$handshake[] = \sprintf(self::HEADER_LINE_FORMAT, $name, $value);
}
return \implode("\r\n", $handshake)."\r\n\r\n";
}
/**
* Validates a WebSocket URI.
*
* @throws InvalidArgumentException
*/
public function validateUri(string $uri): array
{
if (!$uri) {
throw new InvalidArgumentException('Invalid URI');
}
$scheme = \parse_url($uri, \PHP_URL_SCHEME);
$this->validateScheme($scheme ?: '');
$host = \parse_url($uri, \PHP_URL_HOST);
if (!$host) {
throw new InvalidArgumentException('Invalid host');
}
$port = \parse_url($uri, \PHP_URL_PORT);
if (!$port) {
$port = $this->getPort($scheme);
}
$path = \parse_url($uri, \PHP_URL_PATH);
if (!$path) {
throw new InvalidArgumentException('Invalid path');
}
$query = \parse_url($uri, \PHP_URL_QUERY);
return [$scheme, $host, $port, $path, $query ?: ''];
}
/**
* Validates a scheme.
*
* @throws InvalidArgumentException
*/
protected function validateScheme(string $scheme): string
{
if (!$scheme) {
throw new InvalidArgumentException('No scheme specified');
}
if (!\in_array($scheme, self::$schemes)) {
throw new InvalidArgumentException('Unknown socket scheme: '.$scheme);
}
if (self::SCHEME_WEBSOCKET_SECURE == $scheme) {
return self::SCHEME_UNDERLYING_SECURE;
}
return self::SCHEME_UNDERLYING;
}
/**
* Gets the default port for a scheme
* By default, the WebSocket Protocol uses port 80 for regular WebSocket
* connections and port 443 for WebSocket connections tunneled over TLS.
*/
protected function getPort(string $scheme): int
{
if (self::SCHEME_WEBSOCKET == $scheme) {
return 80;
}
if (self::SCHEME_WEBSOCKET_SECURE == $scheme) {
return 443;
}
if (self::SCHEME_UNDERLYING == $scheme) {
return 80;
}
if (self::SCHEME_UNDERLYING_SECURE == $scheme) {
return 443;
}
throw new InvalidArgumentException('Unknown websocket scheme');
}
/**
* Gets the default request headers.
*
* @return array<string, string>
*/
protected function getDefaultRequestHeaders(
string $host,
string $key,
string $origin
): array {
return [
self::HEADER_HOST => $host,
self::HEADER_UPGRADE => self::UPGRADE_VALUE,
self::HEADER_CONNECTION => self::CONNECTION_VALUE,
self::HEADER_KEY => $key,
self::HEADER_ORIGIN => $origin,
self::HEADER_VERSION => (string) $this->getVersion(),
];
}
/**
* Gets the version number.
*/
abstract public function getVersion(): int;
/**
* Gets a handshake response body.
*/
public function getResponseHandshake(string $key, array $headers = []): string
{
$headers = \array_merge(
$this->getSuccessResponseHeaders($key),
$headers
);
return $this->getHttpResponse(self::HTTP_SWITCHING_PROTOCOLS, $headers);
}
/**
* Gets the default response headers.
*
* @param string $key
*
* @return string[]
*/
protected function getSuccessResponseHeaders(string $key): array
{
return [
self::HEADER_UPGRADE => self::UPGRADE_VALUE,
self::HEADER_CONNECTION => self::CONNECTION_VALUE,
self::HEADER_ACCEPT => $this->getAcceptValue($key),
];
}
/**
* Gets the expected accept value for a handshake response
* Note that the protocol calls for the base64 encoded value to be hashed,
* not the original 16 byte random key.
*
* @see https://datatracker.ietf.org/doc/html/rfc6455#section-4.2.2
*
* @param string $key
*
* @return string
*/
protected function getAcceptValue(string $key): string
{
return \base64_encode(\sha1($key.self::MAGIC_GUID, true));
}
/**
* Gets an HTTP response.
*/
protected function getHttpResponse(int $status, array $headers = []): string
{
if (\array_key_exists($status, self::HTTP_RESPONSES)) {
$response = self::HTTP_RESPONSES[$status];
} else {
$response = self::HTTP_RESPONSES[self::HTTP_NOT_IMPLEMENTED];
}
$handshake = [
\sprintf(self::RESPONSE_LINE_FORMAT, $status, $response),
];
foreach ($headers as $name => $value) {
$handshake[] = \sprintf(self::HEADER_LINE_FORMAT, $name, $value);
}
return \implode("\r\n", $handshake)."\r\n\r\n";
}
/**
* Gets a response to an error in the handshake.
*
* @param int|Exception $e Exception or HTTP error
* @param array $headers
*
* @return string
*/
public function getResponseError($e, array $headers = []): string
{
$code = false;
if ($e instanceof Exception) {
$code = $e->getCode();
} elseif (\is_numeric($e)) {
$code = (int) $e;
}
if (!$code || $code < 400 || $code > 599) {
$code = self::HTTP_SERVER_ERROR;
}
return $this->getHttpResponse($code, $headers);
}
/**
* @throws HandshakeException
*/
public function validateResponseHandshake(string $response, string $key): bool
{
if (!$response) {
return false;
}
$statusCode = $this->getStatusCode($response);
if (self::HTTP_SWITCHING_PROTOCOLS !== $statusCode) {
$errorMessage = \explode("\n", \trim($this->getBody($response), " \n\r\t\0\x0B"), 2)[0];
throw new HandshakeException(\trim(\sprintf('Expected handshake response status code %d, but received %d. %s', self::HTTP_SWITCHING_PROTOCOLS, $statusCode, $errorMessage), " \n\r\t\0\x0B"));
}
$acceptHeaderValue = $this->getHeaders($response)[self::HEADER_ACCEPT] ?? '';
if ('' === $acceptHeaderValue) {
throw new HandshakeException('No accept header received on handshake response');
}
return $this->getEncodedHash($key) === $acceptHeaderValue;
}
/**
* Gets the status code from a full response.
*
* If there is no status line, we return 0.
*
* @return int
*/
protected function getStatusCode(string $response): int
{
[$statusLine] = \explode("\r\n", $response, 2);
[$protocol, $statusCode] = \explode(' ', $response, 2);
return (int) $statusCode;
}
/**
* Gets the headers from a full response.
*
* @return array<string, array>
*/
protected function getHeaders(string $response): array
{
$parts = \explode("\r\n\r\n", $response, 2);
if (\count($parts) < 2) {
$parts[] = '';
}
[$headers, $body] = $parts;
$return = [];
foreach (\explode("\r\n", $headers) as $header) {
$parts = \explode(':', $header, 2);
if (2 == \count($parts)) {
[$name, $value] = $parts;
if (!isset($return[$name])) {
$return[$name] = \trim($value, " \n\r\t\0\x0B");
} else {
if (\is_array($return[$name])) {
$return[$name][] = \trim($value, " \n\r\t\0\x0B");
} else {
$return[$name] = [$return[$name], \trim($value, " \n\r\t\0\x0B")];
}
}
}
}
return \array_change_key_case($return);
}
/**
* Gets the body from a full response.
*
* @return string
*/
protected function getBody(string $response): string
{
return \explode("\r\n\r\n", $response, 2)[1] ?? '';
}
/**
* Gets an encoded hash for a key.
*
* @param string $key
*
* @return string
*/
public function getEncodedHash(string $key): string
{
return \base64_encode(\pack('H*', \sha1($key.self::MAGIC_GUID)));
}
/**
* Validates a request handshake.
*
* @param string $request
*
* @throws BadRequestException
*/
public function validateRequestHandshake(string $request): array
{
[$request, $headers] = $this->getRequestHeaders($request);
// make a copy of the headers array to store all extra headers
$extraHeaders = $headers;
// parse the resulting url to separate query parameters from the path
$url = \parse_url($this->validateRequestLine($request));
$path = $url['path'] ?? null;
$urlParams = [];
if (isset($url['query'])) {
\parse_str($url['query'], $urlParams);
}
if (empty($headers[self::HEADER_ORIGIN])) {
throw new BadRequestException('No origin header');
}
unset($extraHeaders[self::HEADER_ORIGIN]);
$origin = $headers[self::HEADER_ORIGIN];
if (!isset($headers[self::HEADER_UPGRADE])
|| self::UPGRADE_VALUE != \strtolower($headers[self::HEADER_UPGRADE])
) {
throw new BadRequestException('Invalid upgrade header');
}
unset($extraHeaders[self::HEADER_UPGRADE]);
if (!isset($headers[self::HEADER_CONNECTION])
|| false === \stripos($headers[self::HEADER_CONNECTION], self::CONNECTION_VALUE)
) {
throw new BadRequestException('Invalid connection header');
}
unset($extraHeaders[self::HEADER_CONNECTION]);
if (!isset($headers[self::HEADER_HOST])) {
// @todo Validate host == listening socket? Or would that break
// TCP proxies?
throw new BadRequestException('No host header');
}
unset($extraHeaders[self::HEADER_HOST]);
if (!isset($headers[self::HEADER_VERSION])) {
throw new BadRequestException('No version header received on handshake request');
}
if (!$this->acceptsVersion((int) $headers[self::HEADER_VERSION])) {
throw new BadRequestException('Unsupported version: '.$headers[self::HEADER_VERSION]);
}
unset($extraHeaders[self::HEADER_VERSION]);
if (!isset($headers[self::HEADER_KEY])) {
throw new BadRequestException('No key header received');
}
$key = \trim($headers[self::HEADER_KEY], " \n\r\t\0\x0B");
if (!$key) {
throw new BadRequestException('Invalid key');
}
unset($extraHeaders[self::HEADER_KEY]);
// Optional
$protocol = null;
if (isset($headers[self::HEADER_PROTOCOL])) {
$protocol = $headers[self::HEADER_PROTOCOL];
unset($extraHeaders[self::HEADER_PROTOCOL]);
}
$extensions = [];
if (!empty($headers[self::HEADER_EXTENSIONS])) {
$extensions = $headers[self::HEADER_EXTENSIONS];
if (\is_scalar($extensions)) {
$extensions = [$extensions];
}
}
unset($extraHeaders[self::HEADER_EXTENSIONS]);
return [$path, $origin, $key, $extensions, $protocol, $extraHeaders, $urlParams];
}
/**
* Gets request headers.
*
* @throws InvalidArgumentException
*/
protected function getRequestHeaders(string $response): array
{
$eol = \stripos($response, "\r\n");
if (false === $eol) {
throw new InvalidArgumentException('Invalid request line');
}
$request = \substr($response, 0, $eol);
$headers = $this->getHeaders(\substr($response, $eol + 2));
return [$request, $headers];
}
/**
* Validates a request line.
*
* @throws BadRequestException
*/
protected function validateRequestLine(string $line): string
{
\preg_match(self::REQUEST_LINE_REGEX, $line, $matches);
if (!($matches[1] ?? false)) {
throw new BadRequestException('Invalid request line', 400);
}
/** @var string */
return $matches[1];
}
/**
* Subclasses should implement this method and return a boolean to the given
* version string, as to whether they would like to accept requests from
* user agents that specify that version.
*/
abstract public function acceptsVersion(int $version): bool;
/**
* Gets a suitable WebSocket close frame.
*
* Set `masked` to false if you send a close frame from server side.
*/
public function getClosePayload(int $code, bool $masked = true): Payload
{
if (!\array_key_exists($code, self::CLOSE_REASONS)) {
$code = self::CLOSE_UNEXPECTED;
}
$body = \pack('n', $code).self::CLOSE_REASONS[$code];
$payload = $this->getPayload();
return $payload->encode($body, self::TYPE_CLOSE, $masked);
}
/**
* Gets a payload instance, suitable for use in decoding/encoding protocol
* frames.
*/
abstract public function getPayload(): Payload;
/**
* Validates a socket URI.
*
* @throws InvalidArgumentException
*/
public function validateSocketUri(string $uri): array
{
if (!$uri) {
throw new InvalidArgumentException('Invalid URI');
}
$scheme = $this->validateScheme(\parse_url($uri, \PHP_URL_SCHEME) ?: '');
$host = \parse_url($uri, \PHP_URL_HOST);
if (!$host) {
throw new InvalidArgumentException('Invalid host');
}
$port = \parse_url($uri, \PHP_URL_PORT);
if (!$port) {
$port = $this->getPort($scheme);
}
return [$scheme, $host, $port];
}
/**
* Validates an origin URI.
*
* @throws InvalidArgumentException
*/
public function validateOriginUri(string $origin): string
{
$origin = (string) $origin;
if (!$origin) {
throw new InvalidArgumentException('Invalid URI');
}
$scheme = \parse_url($origin, \PHP_URL_SCHEME);
if (!$scheme) {
throw new InvalidArgumentException('Invalid scheme');
}
$host = \parse_url($origin, \PHP_URL_HOST);
if (!$host) {
throw new InvalidArgumentException('Invalid host');
}
return $origin;
}
}