Search code examples
communicationsampletls1.3smtpssmtp-server

Communication sample for SMTP over TLS (known as SSL/TLS)


I am trying to implement SSL/TLS im my SMTP server (PHP). Securing the connection with TLSv1.3 works, and the certificate (LetsEncrypt) is valid. I tested this with https://www.checktls.com/TestReceiver where it only works when I activate 'Direct TLS':

 seconds        test stage and result
[000.000]       Trying TLS on mrs.dzir.org[212.58.86.63:465] (-1)
[000.100]       Server answered
[000.707]       Connection converted to SSL
                SSLVersion in use: TLSv1_3
                Cipher in use: TLS_AES_256_GCM_SHA384
                Perfect Forward Secrecy: yes
                Session Algorithm in use: Curve X25519 DHE(253 bits)
[001.185]       TLS successfully started on this server
[001.185]   <~~ 220 MailRelayServer ESMTP server ready
[001.185]       We are allowed to connect
[001.185]   ~~> EHLO www12-do.checktls.com
[001.284]   <~~ 250-Hello [142.93.73.156]
                250-DATA
                250-AUTH LOGIN PLAIN CRAM-MD5
                250-AUTH=CRAM-MD5
                250 OK
[001.285]       We can use this server
[001.285]   ~~> AUTH PLAIN ********
[001.387]   <~~ 235 Authentication successful
[001.387]       AUTH successful
[001.387]   ~~> MAIL FROM:<[email protected]>
[001.490]   <~~ 550 [email protected] ... Sender not accepted
[001.490]       Cannot proof email address (reason: MAIL FROM rejected)
[001.490]       Note: This does not affect the CheckTLS Confidence Factor
[001.490]   ~~> QUIT
[001.589]   <~~ 221 Bye

When I try to update the connection details in my GMail app on my phone, it keeps saying

Email security not guaranteed  
There was a problem setting up security for this account

My SMTP server log says

2022-08-17 15:40:12 New Client Connected (46.114.140.164 [telefonica.de] -> AbuseIPDB Score: 0)
2022-08-17 15:40:12 SSL connection established for 46.114.140.164
2022-08-17 15:40:12 --> 220 MailRelayServer ESMTP server ready
2022-08-17 15:40:12 Client 0 from 46.114.140.164 Disconnecting
2022-08-17 15:40:13 New Client Connected (46.114.140.164 [telefonica.de] -> AbuseIPDB Score: 0)
2022-08-17 15:40:13 SSL connection established for 46.114.140.164
2022-08-17 15:40:13 --> 220 MailRelayServer ESMTP server ready
2022-08-17 15:40:13 Client 0 from 46.114.140.164 Disconnecting

So GMail tries 2 times, establishes a secure connection, and then doesn't react anymore (doesn't even send a 'QUIT' command).

Now to the question: As there's obviously something missing in the communication, I need a communication sample (something like my SMTP server protocol will do fine). Does anybody know where to get it? I searched the web, but can only find simple samples for STARTTLS, which is not what I need now.
Thanks in advance!

Output of the openssl tool:

# openssl s_client -connect mrs.dzir.org:465
CONNECTED(00000003)
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = R3
verify return:1
depth=0 CN = mrs.dzir.org
verify return:1
---
Certificate chain
 0 s:CN = mrs.dzir.org
   i:C = US, O = Let's Encrypt, CN = R3
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Aug 16 09:27:26 2022 GMT; NotAfter: Nov 14 09:27:25 2022 GMT
 1 s:C = US, O = Let's Encrypt, CN = R3
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Sep  4 00:00:00 2020 GMT; NotAfter: Sep 15 16:00:00 2025 GMT
 2 s:C = US, O = Internet Security Research Group, CN = ISRG Root X1
   i:O = Digital Signature Trust Co., CN = DST Root CA X3
   a:PKEY: rsaEncryption, 4096 (bit); sigalg: RSA-SHA256
   v:NotBefore: Jan 20 19:14:03 2021 GMT; NotAfter: Sep 30 18:14:03 2024 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIFHjCCBAagAwIBAgISBDQeLzaBb9+bUfozRluuUoZ9MA0GCSqGSIb3DQEBCwUA
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
EwJSMzAeFw0yMjA4MTYwOTI3MjZaFw0yMjExMTQwOTI3MjVaMBcxFTATBgNVBAMT
DG1ycy5kemlyLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMCK
AiOqNuaIj6pyt2HeQR/RQtxrXJL8uKppf05YFHhFUfJwOXxSDWioxlqn0igps7cO
NDoti4QC30BhsrAWXzewVtrROHStQYBmSOuGbtrLZ/FKMyXw/fH3ev1ObBgKBD3o
e9D1QC36kiqm34WQtCQ5rizcevKerkeuLAlj81SuyRONarWzG44GWjqtN0g9v6Vz
9QbvAqVLWyfgbJqYNpsbju9Sbc1QFpYL7JZ7nwhf+g6F8zWdSqMNXQCoYWMwf3WE
Qrj5lE8QLEbcszmtfYVvl40hOO9qPDdaz+PCXvWmHkktxt+GYi76HJxWYn7jMd0/
Te+zW7aBZ3s2+AxJScMCAwEAAaOCAkcwggJDMA4GA1UdDwEB/wQEAwIFoDAdBgNV
HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E
FgQUG2PkYn93TKVeB3bXcQejuT/KBmkwHwYDVR0jBBgwFoAUFC6zF7dYVsuuUAlA
5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRwOi8vcjMu
by5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5pLmxlbmNyLm9yZy8w
FwYDVR0RBBAwDoIMbXJzLmR6aXIub3JnMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcG
CysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5
cHQub3JnMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDwAHYA36Veq2iCTx9sre64X04+
WurNohKkal6OOxLAIERcKnMAAAGCpjGinAAABAMARzBFAiBOeG4tx7GFAeL65wk4
934DXgNrUbkRMalQD27PwPnN3AIhANdIcwJRjJAydZHsin1Wb2QINAcwiay6JOAz
R6HNOnD3AHYARqVV63X6kSAwtaKJafTzfREsQXS+/Um4havy/HD+bUcAAAGCpjGi
xQAABAMARzBFAiBYh3PL5r/CsD+ZUhQ7xUyFuHzV7wqyy/sL1J2vgbn1sQIhAIIV
VF8Th0lP42R0kiUA657yhofIPlBYSHA2umyWQHQYMA0GCSqGSIb3DQEBCwUAA4IB
AQCxdAHo8/vqiKdV1tw9ErjdY1xR0UgDAOPY2w9RgGZBIjQmfpQU0aqHvTBJetrK
Gjli9++Mg9cLKwsOLBe3r0gOLqwitdbcB9hh1sbUqPjhfjoa7uGBciU6XuRPtzRX
X0p7kQ6QrJvs8+QaZKiliQfMfiG8mzBJTOiTrWyfha4FvfC0BgvP5dnZHa8xY5dM
XJSdg0wQzxA8vI+dCyncqDlIo6ngwaoXqELF90fBT2WsvJfBDf5W8iqM/Iujw0DS
tY1d+cq1QM+7bVNrYrQkPRol0hhCzz7eSlvIp6Bx7jc0UcJq/EvkegoCZ7J4qX+/
EkydQTh73ho5D/28Ny0PgMfE
-----END CERTIFICATE-----
subject=CN = mrs.dzir.org
issuer=C = US, O = Let's Encrypt, CN = R3
---
No client certificate CA names sent
Requested Signature Algorithms: ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:Ed25519:Ed448:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA224:RSA+SHA224
Shared Requested Signature Algorithms: ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:Ed25519:Ed448:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 4633 bytes and written 424 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: B8346466CC912BE31A603A30F52C3289464D8964107FC8CCCBAEF6B21E6B5FA2
    Session-ID-ctx:
    Resumption PSK: 7C325FBD4945DEA9F2E6C0236B94CB968580167BAD18BDA3034A2075BF894E40A1FD35E03D3D82E170BF09C5CFC1BC23
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 7200 (seconds)
    TLS session ticket:
    0000 - 53 5c 5c 8f 9c 0a ee c8-59 2d 61 ac df c2 61 d1   S\\.....Y-a...a.
    0010 - fb 91 6c 20 44 df 9f 05-93 86 ce b4 29 eb da 65   ..l D.......)..e
    0020 - de f3 97 04 4a 68 72 61-88 a3 7d 7f 13 26 5d 41   ....Jhra..}..&]A
    0030 - 89 15 99 4f ab 6c 86 4a-23 b5 52 cc f6 0b 1c 85   ...O.l.J#.R.....
    0040 - 63 3d c9 98 36 08 ad 58-fe fb d2 9c 74 f0 ca 52   c=..6..X....t..R
    0050 - 73 36 ce d4 41 6d aa 86-53 af 22 ac 42 a7 f6 a9   s6..Am..S.".B...
    0060 - 6d 19 3f ca 2a ec 5a c9-fd 26 6d 88 4e 3d 4c 9b   m.?.*.Z..&m.N=L.
    0070 - 7f d8 ee a0 ad f3 f2 eb-d0 5a d4 76 25 4f 7f 01   .........Z.v%O..
    0080 - ca 2d 50 77 44 fb 62 f3-4d 67 2c dc 00 45 28 74   .-PwD.b.Mg,..E(t
    0090 - 88 10 30 c8 b6 7c 8d bb-bc 24 a7 70 3a 00 26 00   ..0..|...$.p:.&.
    00a0 - da 85 24 04 c2 2a de b4-59 90 ee d8 b9 e7 81 e7   ..$..*..Y.......
    00b0 - ab 9a 06 4c 4b 7b 4e 1d-13 e3 bc a4 13 07 c9 c7   ...LK{N.........
    00c0 - 35 26 8a 45 59 e9 fc a0-ff 7d 30 d6 62 8b 51 21   5&.EY....}0.b.Q!

    Start Time: 1660805781
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: 2291062CB8DDF654332636FE85D4A9BB9833B1BE052CA12C26CBBA790D542B35
    Session-ID-ctx:
    Resumption PSK: 5E7ED4B63AF3D4F1703055F06DCEB9F5B730F1FBC1F738C8AAFABDE702578D0C2F9F6D51D822C59B8C5EA6A1A481C0C1
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 7200 (seconds)
    TLS session ticket:
    0000 - 53 5c 5c 8f 9c 0a ee c8-59 2d 61 ac df c2 61 d1   S\\.....Y-a...a.
    0010 - 1e 26 ab 4d c7 7f 14 ae-f8 0c 29 c9 2f 2a e4 c5   .&.M......)./*..
    0020 - 0b d5 61 8a 80 cd 5c 0a-ef 25 17 52 69 6e c0 0c   ..a...\..%.Rin..
    0030 - d6 73 16 2d 70 90 d7 9d-bd ac dc 35 62 f3 9a 33   .s.-p......5b..3
    0040 - ce 7e 33 e2 f7 56 b7 84-de f6 f8 ff 82 fe 7a 9c   .~3..V........z.
    0050 - 4c 68 27 3a 7c 6b 02 44-90 6d 88 d1 97 5d 13 98   Lh':|k.D.m...]..
    0060 - a8 41 f5 3c d2 14 84 62-30 94 f2 fd 1c 1b 42 80   .A.<...b0.....B.
    0070 - 6c c9 10 ce 60 ff 4b 76-c8 e3 7d 49 d0 fe 0b a3   l...`.Kv..}I....
    0080 - 5b 31 c2 77 52 8c 87 17-c3 1b 3d 83 51 2a 12 ed   [1.wR.....=.Q*..
    0090 - c6 7c 0e 07 ba b3 bf ec-ee c3 ee b6 41 6d 0b b5   .|..........Am..
    00a0 - bf 2c fd 1e 05 e4 c3 76-3b 9d 1d 52 a1 2b f2 5e   .,.....v;..R.+.^
    00b0 - 35 f8 a4 56 d1 4c 8f c1-c6 cb 8c 2a 4f a3 fe ad   5..V.L.....*O...
    00c0 - 83 f9 9c dd 31 6f 5a e0-fe d4 c0 70 b0 c7 7b 49   ....1oZ....p..{I

    Start Time: 1660805781
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK
220 MailRelayServer ESMTP server ready

I added blocking and unblocking the socket before and after enabling crypto, and added a check for the crypto to have been successful. When connection with the openssl tool I can communicate with the server perfectly, but when I try to connect with the GMail app it still says 'email security not guaranteed' and my server's communication log says New Client Rejected (79.238.153.195 [telekom.de] -> AbuseIPDB Score: 0, TLS could not be established) (I check the return value of stream_socket_enable_crypto() for this:

stream_set_blocking($this->socket, true);
$bOK = stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLSv1_3_SERVER);
$aServer['port']);
stream_set_blocking($this->socket, false);
if (!$bOK) {
    SocketServer::debug('New Client Rejected (' . $ip . ' [' . $ret['data']['domain'] . '] -> AbuseIPDB Score: ' . $ret['data']['abuseConfidenceScore'] . ', TLS could not be established)', '');
    SocketServer::socket_write_smart($this->socket, '554 TLS needed', true);
    return false;
}

The server's connection log then says:

2022-08-18 08:06:04 New Client Rejected (79.238.153.195 [telekom.de] -> AbuseIPDB Score: 0, TLS could not be established)
2022-08-18 08:06:04 --> 554 TLS needed

Following ist the class file for the SocketServer and SocketServerClient classes which holds everything to build the connection (simplified as much as possible but still functional):

<?php
/**
 * class SocketServer
 * 
 * @author Navarr Barnier
 * @abstract A Framework for creating a multi-client server using the PHP language.
 */
class SocketServer {

    /**
     * @var run
     * @abstract Bool - a boolean to signalize to stop
     */
    protected $run = true;

    /**
     * @var config
     * @abstract Array - an array of configuration information used by the server.
     */
    protected array $config = array();

    /**
     * @var hooks
     * @abstract Array - a dictionary of hooks and the callbacks attached to them.
     */
    protected array $hooks = array();

    /**
     * @var master_socket
     * @abstract resource - The master socket used by the server.
     */
    protected $master_socket;

    /**
     * @var max_clients
     * @abstract unsigned int - The maximum number of clients allowed to connect.
     */
    public int $max_clients = 10;

    /**
     * @var max_read
     * @abstract unsigned int - The maximum number of bytes to read from a socket at a single time.
     */
    public int $max_read = 1024;

    /**
     * @var clients
     * @abstract Array - an array of connected clients.
     */
    public array $clients;

    /**
     * function __construct
     * 
     * @abstract Creates the socket and starts listening to it.
     * @param string - IP Address to bind to, NULL for default.
     * @param int - Port to bind to
     * @return void
     */
    public function __construct($bind_ip, $port, $domain = '') {
        set_time_limit(0);

        $this->config['ip'] = $bind_ip;
        $this->config['port'] = $port;

        $errno = 0;
        $errmsg = '';

        $set = ['ssl' => [
            'local_cert' => '/etc/letsencrypt/live/' . $domain . '/fullchain.pem',
            'local_pk' => '/etc/letsencrypt/live/' . $domain . '/privkey.pem',
            'disable_compression' => false,
            'ssltransport' => 'tlsv1.3'
        ]];
        $context = stream_context_create($set);
        $this->master_socket = stream_socket_server("tcp://{$bind_ip}:{$port}", $errno, $errmsg, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $context);
        if ($this->master_socket === false) {
            SocketServer::debug("Could not start listening for connections on {$bind_ip}:{$port}", '');
            die("Issue Binding\r\n{$errmsg} ({$errno})\r\n");
        }
        stream_socket_enable_crypto($this->master_socket, false);
        SocketServer::debug("Listening for connections on {$bind_ip}:{$port} with TLS enabled", '');
        echo "Listenting for connections on {$bind_ip}:{$port} with TLS enabled\r\n";

        pcntl_async_signals(true);
        pcntl_signal(SIGINT, [$this, 'shutdown']); // Call $this->shutdown() on SIGINT
        pcntl_signal(SIGTERM, [$this, 'shutdown']); // Call $this->shutdown() on SIGTERM
    }

    /**
     * function __destruct
     */
    public function __destruct() {
        fclose($this->master_socket);
        foreach ($this->clients as $client) {
            fclose($client->socket);
        }
    }

    /**
     * function hook
     * 
     * @abstract Adds a function to be called whenever a certain action happens. Can be extended in your implementation.
     * @param string - Command
     * @param callback- Function to Call.
     * @see unhook
     * @see trigger_hooks
     * @return void
     */
    public function hook($command, $function) {
        $command = strtoupper($command);
        if (!isset($this->hooks[$command])) {
            $this->hooks[$command] = array();
        }
        $k = array_search($function, $this->hooks[$command]);
        if ($k === FALSE) {
            $this->hooks[$command][] = $function;
        }
    }

    /**
     * function unhook
     * 
     * @abstract Deletes a function from the call list for a certain action. Can be extended in your implementation.
     * @param string - Command
     * @param callback- Function to Delete from Call List
     * @see hook
     * @see trigger_hooks
     * @return void
     */
    public function unhook($command = NULL, $function) {
        $command = strtoupper($command);
        if ($command !== NULL) {
            $k = array_search($function, $this->hooks[$command]);
            if ($k !== FALSE) {
                unset($this->hooks[$command][$k]);
            }
        } else {
            $k = array_search($this->user_funcs, $function);
            if ($k !== FALSE) {
                unset($this->user_funcs[$k]);
            }
        }
    }

    /**
     * function loop_once
     * 
     * @abstract Runs the class's actions once.
     * @discussion Should only be used if you want to run additional checks during server operation. Otherwise, use infinite_loop()
     * @param void
     * @see infinite_loop
     * @return bool - True
     */
    public function loop_once() {
        $bOK = true;

        // Setup Clients Listen Socket For Reading
        $read[0] = $this->master_socket;
        for ($i = 0; $i < $this->max_clients; $i ++) {
            if (isset($this->clients[$i])) {
                $read[$i + 1] = $this->clients[$i]->socket;
            }
        }

        // Set up a blocking call to socket_select
        $tv_sec = 5;
        $write = $except = null;
        if (@stream_select($read, $write, $except, $tv_sec) < 1) {
            $bOK = false;
        }

        if ($bOK) {
            // Handle new Connections
            if (in_array($this->master_socket, $read)) {
                for ($i = 0; $i < $this->max_clients; $i ++) {
                    if (empty($this->clients[$i])) {
                        $this->clients[$i] = new SocketServerClient($this->config['ssl']);
                        if ($this->clients[$i]->connect($this->master_socket, $i)) {
                            $this->trigger_hooks("CONNECT", $this->clients[$i], "");
                        }
                        else {
                            unset($this->clients[$i]);
                        }
                        break;
                    }
                    elseif ($i == ($this->max_clients - 1)) {
                        SocketServer::debug("Too many clients... :( ");
                    }
                }
            }

            // Handle Input
            for($i = 0; $i < $this->max_clients; $i ++) { // for each client
                if (isset($this->clients[$i])) {
                    if (in_array($this->clients[$i]->socket, $read)) {
                        if ($this->config['ssl']) {
                            $input = fread($this->clients[$i]->socket, $this->max_read);
                        }
                        else {
                            $input = socket_read($this->clients[$i]->socket, $this->max_read);
                        }
                        if ($input == null) {
                            $this->disconnect($i);
                        }
                        else {
                            $this->trigger_hooks("INPUT", $this->clients[$i], $input);
                        }
                    }
                }
            }
        }

        return $this->run;
    }

    /**
     * function disconnect
     * 
     * @abstract Disconnects a client from the server.
     * @param int - Index of the client to disconnect.
     * @param string - Message to send to the hooks
     * @return void
     */
    public function disconnect($client_index, $message = "") {
        $i = $client_index;
        SocketServer::debug("Client {$i} from {$this->clients[$i]->ip} Disconnecting", '');
        $this->trigger_hooks("DISCONNECT", $this->clients[$i], $message);
        unset($this->clients[$i]);
    }

    /**
     * function trigger_hooks
     * 
     * @abstract Triggers Hooks for a certain command.
     * @param string - Command who's hooks you want to trigger.
     * @param object - The client who activated this command.
     * @param string - The input from the client, or a message to be sent to the hooks.
     * @return void
     */
    public function trigger_hooks($command, &$client, $input) {
        if (isset($this->hooks[$command])) {
            foreach($this->hooks[$command] as $function) {
                $continue = call_user_func($function, $this, $client, $input);
                if ($continue === FALSE) {
                    break;
                }
            }
        }
    }

    /**
     * function trigger_hook
     * 
     * @abstract Triggers Hook for a certain command.
     * @param string - Command who's hooks you want to trigger.
     * @param string - The input from the client, or a message to be sent to the hooks.
     * @return void
     */
    public function trigger_hook($command, $input) {
        if (isset($this->hooks[$command])) {
            foreach($this->hooks[$command] as $function) {
                $continue = call_user_func($function, $this, $input);
                if ($continue === FALSE) {
                    break;
                }
            }
        }
    }

    /**
     * function infinite_loop
     * 
     * @abstract Runs the server code until the server is shut down.
     * @see loop_once
     * @param void
     * @return void
     */
    public function infinite_loop() {
        $test = true;
        do {
            $test = $this->loop_once();
            if (file_exists(realpath(dirname(__FILE__) . '/../restart.cmd'))) {
                unlink(realpath(dirname(__FILE__) . '/../restart.cmd'));
                $test = false;
                $this->shutdown();
            }
        } while ($test);
    }

    /**
     * function debug
     * 
     * @static
     * @abstract Outputs Text directly.
     * @discussion Yeah, should probably make a way to turn this off.
     * @param string - Text to Output
     * @return void
     */
    public static function debug($text, $dir = '<--', $log = 'communication') {
        global $debug;

        if ($debug) {
            $fp = fopen(dirname(__FILE__) . '/../' . $log . '.log', 'a');
            if (is_array($text)) {
                foreach ($text as $line) {
                    fwrite($fp, date('Y-m-d H:i:s') . ' ' . ((strlen($dir)) ? $dir . ' ' : '') . $line . "\r\n");
                }
            }
            else {
                $lines = explode("\n", $text);
                foreach ($lines as $line) {
                    fwrite($fp, date('Y-m-d H:i:s') . ' ' . ((strlen($dir)) ? $dir . ' ' : '') . str_replace("\r", '', $line) . "\r\n");
                }
            }
            fclose($fp);
        }
    }

    /**
     * function socket_write_smart
     * 
     * @static
     * @abstract Writes data to the socket, including the length of the data, and ends it with a CRLF unless specified.
     * @discussion It is perfectly valid for socket_write_smart to return zero which means no bytes have been written. Be sure to use the === operator to check * for FALSE in case of an error.
     * @param resource- Socket Instance
     * @param string - Data to write to the socket.
     * @param string - Data to end the line with. Specify a "" if you don't want a line end sent.
     * @return mixed - Returns the number of bytes successfully written to the socket or FALSE on failure. The error code can be retrieved with socket_last_error(). This code may be passed to socket_strerror() to get a textual explanation of the error.
     */
    public static function socket_write_smart(&$sock, $string, $crlf = "\r\n") {
        SocketServer::debug($string, '-->');
        $ret = fwrite($sock, $string . $crlf);
        return $ret;
    }

    /**
     * function __get
     * 
     * @abstract Magic Method used for allowing the reading of protected variables.
     * @discussion You never need to use this method, simply calling $server->variable works because of this method's existence.
     * @param string - Variable to retrieve
     * @return mixed - Returns the reference to the variable called.
     */
    public function &__get($name) {
        return $this->{$name};
    }

    public function shutdown() {
        $this->run = false;
        SocketServer::debug('Shutting down', '');
        echo "\nShutting down\n";
        $this->__destruct();
        exit(0);
    }
}

/**
 * class SocketServerClient
 * 
 * @author Navarr Barnier
 * @abstract A Client Instance for use with SocketServer
 */
class SocketServerClient {
    /**
     * var socket
     * @abstract resource - The client's socket resource, for sending and receiving data with.
     */
    protected $socket;

    /**
     * var ip
     * @abstract string - The client's IP address, as seen by the server.
     */
    protected $ip;

    /**
     * var hostname
     * @abstract string - The client's hostname, as seen by the server.
     * @discussion This variable is only set after calling lookup_hostname, as hostname lookups can take up a decent amount of time.
     * @see lookup_hostname
     */
    protected $hostname;

    /**
     * var server_clients_index
     * @abstract int - The index of this client in the SocketServer's client array.
     */
    protected $server_clients_index;

    /**
     * function connect
     * 
     * @param resource- The resource of the socket the client is connecting by, generally the master socket.
     * @param int - The Index in the Server's client array.
     * @return bool Connection accepted or not
     */
    public function connect(&$socket, $i) {
        $this->server_clients_index = $i;
        $this->socket = stream_socket_accept($socket) or die("Failed to Accept\n");
        $ip = '';
        $name = stream_socket_get_name($this->socket, true);
        list($ip, $port) = explode(':', $name);
        $this->ip = $ip;

        # Block the connection until the secure connection is established
        stream_set_blocking($this->socket, true);
        $bOK = stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLSv1_3_SERVER);
        stream_set_blocking($this->socket, false);
        if (!$bOK) {
            SocketServer::debug('New Client Rejected (' . $ip . ', TLS could not be established)', '');
            SocketServer::socket_write_smart($this->socket, '554 TLS needed', true);
            return false;
        }
        SocketServer::debug('New Client Connected (' . $ip . ')', '');
        SocketServer::debug('SSL connection established for ' . $ip, '');

        return true;
    }

    /**
     * function lookup_hostname
     * 
     * @abstract Searches for the user's hostname and stores the result to hostname.
     * @see hostname
     * @param void
     * @return string - The hostname on success or the IP address on failure.
     */
    public function lookup_hostname() {
        $this->hostname = gethostbyaddr($this->ip);
        return $this->hostname;
    }

    /**
     * function __destruct
     * 
     * @abstract Closes the socket. Thats pretty much it.
     * @param void
     * @return void
     */
    public function __destruct() {
        fclose($this->socket);
    }

    function &__get($name) {
        return $this->{$name};
    }

    function __isset($name) {
        return isset($this->{$name});
    }
}
?>

Use it like this ($bind_ip must be the local IP of the machine it runs on):

# Create a Server binding to the given ip address and listen to the given port for connections
$sDomain = str_replace(array('http://', 'https://'), '', $sMainUrl);
$p1 = strpos($sDomain, '/');
if ($p1 !== false) {
    $sDomain = substr($sDomain, 0, $p1);
}
$server = new SocketServer($aServer['ip'], $aServer['port'], $sDomain);

# Run handleConnect every time someone connects
$server->hook('CONNECT', 'handleConnect');

# Run handleInput whenever text is sent to the server
$server->hook('INPUT', 'handleInput');

# Run Server Code Until Process is terminated
$server->infinite_loop();

# Manage communication classes
$aCommunications = array();

/**
 * function handleConnect
 * 
 * @param $server object Server instance (ignored)
 * @param $client object Client instance
 * @param $input string Input from the connecting client (ignored)
 * @return void
 */
function handleConnect($server, $client, $input) {
    global $aCommunications, $aServer;

    $aCommunications[$client->server_clients_index] = new Communication($aServer['ssl']);
    SocketServer::socket_write_smart($client->socket, $aCommunications[$client->server_clients_index]->welcome(), $aServer['ssl']);
}

/**
 * function handleInput
 * 
 * @param $server object Server instance
 * @param $client object Client instance
 * @param $input string Input from the connecting client
 * @return void
 */
function handleInput($server, $client, $input) {
    global $aCommunications;

    if (!$aCommunications[$client->server_clients_index]->handleInput($client, $input)) {
        $server->disconnect($client->server_clients_index);
        unset($aCommunications[$client->server_clients_index]);
    }
}

The communication after building up the connection is in another file and not needed for this question.

I just tested with Thunderbird (Portable on Windows 10) and it works, but not in GMail on Android. Is this a GMail specific problem? Couldn't find anything useful on that in the web.


Solution

  • The above code, including the last changes, works fine if just one setting is changed.

    In the class SocketServerClient in the function connect() change the line

    $bOK = stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLSv1_3_SERVER);
    

    to

    $bOK = stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_SERVER);
    

    Both GMail (Android) and Outlook 2019 don't seem to be able to use TLS 1.3.
    Outlook 2019 can natively use up to TLS 1.1 and with IISCrypto it can learn TLS 1.2.