Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
UDPTransport
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 3
132
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 newFromString
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 emit
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7use Wikimedia\IPUtils;
8
9/**
10 * A generic class to send a message over UDP
11 *
12 * If a message prefix is provided to the constructor or via
13 * UDPTransport::newFromString(), the payload of the UDP datagrams emitted
14 * will be formatted with the prefix and a single space at the start of each
15 * line. This is the payload format expected by the udp2log service.
16 *
17 * @since 1.25
18 */
19class UDPTransport {
20    // Limit to 64 KiB
21    public const MAX_PAYLOAD_SIZE = 65507;
22    private string $host;
23    private int $port;
24    /** @var bool|string */
25    private $prefix;
26    private int $domain;
27
28    /**
29     * @param string $host IP address to send to
30     * @param int $port port number
31     * @param int $domain AF_INET or AF_INET6 constant
32     * @param string|bool $prefix Prefix to use, false for no prefix
33     */
34    public function __construct( $host, $port, $domain, $prefix = false ) {
35        $this->host = $host;
36        $this->port = $port;
37        $this->domain = $domain;
38        $this->prefix = $prefix;
39    }
40
41    /**
42     * @param string $info In the format of "udp://host:port/prefix"
43     * @return UDPTransport
44     */
45    public static function newFromString( $info ) {
46        if ( preg_match( '!^udp:(?://)?\[([0-9a-fA-F:]+)\]:(\d+)(?:/(.*))?$!', $info, $m ) ) {
47            // IPv6 bracketed host
48            $host = $m[1];
49            $port = intval( $m[2] );
50            $prefix = $m[3] ?? false;
51            $domain = AF_INET6;
52        } elseif ( preg_match( '!^udp:(?://)?([a-zA-Z0-9.-]+):(\d+)(?:/(.*))?$!', $info, $m ) ) {
53            $host = $m[1];
54            if ( !IPUtils::isIPv4( $host ) ) {
55                $host = gethostbyname( $host );
56            }
57            $port = intval( $m[2] );
58            $prefix = $m[3] ?? false;
59            $domain = AF_INET;
60        } else {
61            throw new InvalidArgumentException( __METHOD__ . ': Invalid UDP specification' );
62        }
63
64        return new self( $host, $port, $domain, $prefix );
65    }
66
67    /**
68     * @param string $text
69     */
70    public function emit( $text ): void {
71        // Clean it up for the multiplexer
72        if ( $this->prefix !== false ) {
73            $text = preg_replace( '/^/m', $this->prefix . ' ', $text );
74
75            if ( strlen( $text ) > self::MAX_PAYLOAD_SIZE - 1 ) {
76                $text = substr( $text, 0, self::MAX_PAYLOAD_SIZE - 1 );
77            }
78
79            if ( !str_ends_with( $text, "\n" ) ) {
80                $text .= "\n";
81            }
82        } elseif ( strlen( $text ) > self::MAX_PAYLOAD_SIZE ) {
83            $text = substr( $text, 0, self::MAX_PAYLOAD_SIZE );
84        }
85
86        $sock = socket_create( $this->domain, SOCK_DGRAM, SOL_UDP );
87        if ( !$sock ) { // @todo should this throw an exception?
88            return;
89        }
90
91        socket_sendto( $sock, $text, strlen( $text ), 0, $this->host, $this->port );
92        socket_close( $sock );
93    }
94}