2021-11-08 TLS and Perl

I feel stretched thin. Deep down there is a fear gnawing at my innards. I try to remember the litany against fear, but it won’t come. I try to distract myself by spending time with the laptop.

The Gemini spec makes TLS close_notify mandatory. I don’t know how to do this. All I know is that Phoebe fails the test. On the mailing list, Stéphane said that his “agunua” tool would report it. It is simple to install (“pip3 install agunua”) and unfortunately he’s right, that’s exactly what it reports: “Warning, no TLS shutdown received from the server”.

But where is the problem? I use Mojo::IOLoop, which uses an IO::Socket::SSL socket, which uses the Net::SSLeay library, which speaks with OpenSSL. Somewhere along the lines, the information is lost.

I decided that I needed a simple use case. Here’s a simple Gemini server using Mojo::IOLoop:

use Mojo::IOLoop;
Mojo::IOLoop->server(
  {port => 1965, tls => 1} =>
  sub {
    my ($loop, $stream) = @_;
    $stream->on(
      read => sub {
	my ($stream, $bytes) = @_;
	# $stream->write("HTTP/1.1 200 OK\r\n\r\n");
	$stream->write("20 text/plain\r\n");
	$stream->write("Hello\n");
	$stream->close_gracefully();
      });
  });
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;

Run it, and use the client:

> agunua --force-ipv4 gemini://localhost/
Warning, no TLS shutdown received from the server
Hello

Sadly, I don’t know what to do, now. The $stream doesn’t provide more methods except for “close” and “close_gracefully”. When I use close, then no output gets printed.

OK. Let’s try to write a server using IO::Socket::SSL, without Mojo::IOLoop!

use strict;
use IO::Socket::SSL;
my $srv = IO::Socket::SSL->new(
  LocalAddr => '0.0.0.0:1965',
  Listen => 10,
  SSL_cert_file => 'cert.pem',
  SSL_key_file => 'key.pem',
    );
while (1) {
  my $cl = $srv->accept or die "failed to accept or ssl handshake: $!,$SSL_ERROR";
  my $req = <$cl>;
  print $cl "20 text/plain\r\nHello\n";
}

And test it:

> agunua --force-ipv4 gemini://localhost/
Warning, no TLS shutdown received from the server
Hello

All right. But I can call close on the socket!

use strict;
use IO::Socket::SSL;
my $srv = IO::Socket::SSL->new(
  LocalAddr => '0.0.0.0:1965',
  Listen => 10,
  SSL_cert_file => 'cert.pem',
  SSL_key_file => 'key.pem',
    );
while (1) {
  my $cl = $srv->accept or die "failed to accept or ssl handshake: $!,$SSL_ERROR";
  my $req = <$cl>;
  print $cl "20 text/plain\r\nHello\n";
  $cl->close; # ✨ new ✨
}

And test it:

> agunua --force-ipv4 gemini://localhost/
Hello

Wow! I have struggled so long to get this right, meddling with Mojo::IOLoop! Why did it take me so long to get the idea to try without it.

That’s how programming works, I guess. If you don’t have the right ideas, no amount of effort is going to help. How weird is that.

OK, so now I know that explicitly closing the SSL socket does what I want. But how to apply this insight to Mojo::IOLoop? After all, I don’t want to rewrite my code, if at all possible.

When I close the handle, it doesn’t work at all.

use Mojo::IOLoop;
Mojo::IOLoop->server(
  {port => 1965, tls => 1} =>
  sub {
    my ($loop, $stream) = @_;
    $stream->on(
      read => sub {
	my ($stream, $bytes) = @_;
	# $stream->write("HTTP/1.1 200 OK\r\n\r\n");
	$stream->write("20 text/plain\r\n");
	$stream->write("Hello\n");
	$stream->close_gracefully();
	$stream->{handle}->close(); # ✨ new ✨
      });
  });
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;

When I run it, I get endless repetitions of this warning:

Mojo::Reactor::Poll: I/O watcher failed: Mojo::IOLoop::Stream: Bad file descriptor at /home/alex/perl5/perlbrew/perls/perl-5.32.0/lib/site_perl/5.32.0/Mojo/EventEmitter.pm line 19.

So Mojo is in fact trying to use the handle. I guess I need to figure out what “close_gracefully” does for the $stream.

sub close_gracefully { $_[0]->is_writing ? $_[0]{graceful}++ : $_[0]->close }

OK. And when writing:

  # Clear the buffer to free the underlying SV* memory
  undef $self->{buffer}, $self->emit('drain') unless length $self->{buffer};
  return undef if $self->is_writing;
  return $self->close if $self->{graceful};
  $self->reactor->watch($handle, !$self->{paused}, 0) if $self->{handle};

Right, so when it finishes writing, it the $stream closes. But… what about the handle, the IO::Socket::SSL? Let’s see what “close” does.

sub close {
  my $self = shift;
  return unless my $reactor = $self->reactor;
  return unless my $handle  = delete $self->timeout(0)->{handle};
  $reactor->remove($handle);
  $self->emit('close');
}

I’m not so sure that this bookkeeping makes sense, here. Shouldn’t the handle be closed? Anyway. Perhaps we can do something with the emission of the “close” event…

use Mojo::IOLoop;
Mojo::IOLoop->server(
  {port => 1965, tls => 1} =>
  sub {
    my ($loop, $stream) = @_;
    $stream->on(
      read => sub {
	my ($stream, $bytes) = @_;
	# $stream->write("HTTP/1.1 200 OK\r\n\r\n");
	$stream->write("20 text/plain\r\n");
	$stream->write("Hello\n");
	$stream->close_gracefully();
      });
    # ✨ new ✨
    $stream->on(
      close => sub {
	my ($stream) = @_;
	$stream->handle->close();
      });
  });
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;

Apparently we cannot. When I try this, the client still reports “Warning, no TLS shutdown received from the server” and the server reports “Mojo::Reactor::Poll: I/O watcher failed: Can’t call method “close” on an undefined value at mojo-ioloop-server.pl line 19.”

OK. But perhaps… we can override stuff? This is Perl, after all. Let’s edit that library code.

sub close {
  my $self = shift;
  return unless my $reactor = $self->reactor;
  return unless my $handle  = delete $self->timeout(0)->{handle};
  $reactor->remove($handle);
  $handle->close; # ✨ new ✨
  $self->emit('close');
}

My problem disappears! Oh wow. Now to prepare a merge request for Mojolicious.

Close handle when closing the stream ​#1875

​#Perl

Comments

(Please contact me if you want to remove your comment.)

Unfortunately, I had to write a unit test for this.

– Alex 2023-06-01 13:17 UTC