💾 Archived View for thfr.info › gemini › cgi-and-client-cert › index.gmi captured on 2024-03-21 at 15:08:22. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-01-29)

-=-=-=-=-=-=-

First Steps with CGI and Client Certificates for Gemini

I've been dipping my toes into making something other than just static gemini pages. That is, I've started doing some CGI.

First of all, after some initial experiments with Vger, the limitations soon became clear and I switched to using Omar Polo's excellent gmid gemini server.

Initial Experiments with Vger

gmid

The following will show some interesting things I learned about Gemini server stuff and implemented in Perl. I will take them from my little application "popcat.cgi" that is taken from a web example.

popcat.cgi

popcat.cgi Source

Popcat - the original inspiration

Of note, based on the Wikipedia entry on Popcat, you can conclude that one hobby gemini developer (me) can accomplish what takes three computer science students in HTML.

Popcat Wikipedia entry (english)

In addition, this is a spying-free version - you determine everything that the server will ever get with the use of the client certificate. While I thought about using the request IP address to determine the country to count the click for, I ultimately decided to just rely on the information in the client certificate (`/C=...`).

Note that popcat.cgi may continue evolving. The code quoted below reflects the how I implemented several key aspects at the time of writing (2022-02-18). Things may change in the future, but the code fragments here may still be of use to some.

Simple ASCII art with Perl here documents

https://perldoc.perl.org/perlglossary#here-document

https://perlmaven.com/here-documents

my $popcat_closed = <<"END_CLOSED";
    XX                           XX
   XXXX                         XXXX
  XXXXXX                       XXXXXX
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 
XXXX   XXXXXXXXXXXXXXXXXXXXXXX   XXXXXX
XXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXX XXX XXX XXXXXXXXXXXXXXXX 
 XXXXXXXXXXXXX       XXXXXXXXXXXXXXXX  
  XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
   XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
      XXXXXXXXXXXXXXXXXXXXXXXXX
END_CLOSED

Check for client certificate and prompt if none presented

It's as simple as 4 (or fewer) lines of Perl to have a complete implementation of checking for and if needed prompting for a client certificate:

# If no client certificate, prompt for one.
unless ( $ENV{'TLS_CLIENT_HASH'} ) {
        print "60 Enter country code when generating the client certificate to participate in country statistics\r\n";
        exit;
}

Note 60 is the status code 'CLIENT CERTIFICATE REQUIRED'. The text afterwards is the 'META' that is displayed at least by Lagrange with the requirement for a client certificate. `\r\n` is the carriage return + line feed (CRLF) at the end that is also required by gemini.

Official specification including status codes.

Use the client certificate identity (hash) to manage a persistent counter

Here the TLS_CLIENT_HASH works pretty much as a cookie in HTML space, with the notable difference of the user having control over the lifetime and cross-page/cross-domain use of the identifier.

These lines are modified from what popcat.cgi uses for readability.

my $file = '/cgi-data/popcat/users.txt';
my $key = $ENV{'TLS_CLIENT_HASH'};
open(my $fh, '+<', $file) or die "Open: $!";
my @array = <$fh>;
my $count;

# read count from $file or set to 1
if ( my ( $target_line ) = grep( /^\Q$key\E/, @array ) ) {
	my (undef, $old_count) = split( ' ', $target_line );
	$count = $old_count + 1;
	foreach (@array) {
		s/^\Q$key $old_count\E$/$key $count/;
	}
}
else {
	push( @array, $key . " 1\n" );     # new entry
	$count = 1;
}

# Write updated table to file and close
seek($fh, 0, 0)                 or die "Seeking: $!";
print $fh @array           or die "Printing: $!";
truncate($fh, tell($fh))        or die "Truncating: $!";
close $fh                       or die "Closing: $!";

The first block opens the filehandle and initializes the variables. The second block checks for a pre-existing entry for the identity (TLS_CLIENT_HASH). The associated count is incremented if found; otherwise a new entry is created. The last block writes the updated file and closes the filehandle.

Note the newline `\n` when a new entry is created. Without that, there is no separation of entries and the code won't work as intended.

Get and use the country code in the client certificate

This may be useful for other applications, too, but may be limited by the need to remind the users to enter the country code at the time of certificate creation.

This implementation allows for certificates without country code (stored as '<None>'). Alternatively, if you want to require it strictly, you could check for the country code at the time of checking for the certificate itself (see above).

Another barrier to this is that the user needs to know their 2-letter country code ('US', 'DE', 'IT', ...). To my knowledge, clients don't help with this at the moment (see screenshots below from Lagrange).

Lagrange only prompts for the Common Name when creating the certificate.

You need to click 'More...' to enter country information.

The country code needs to be typed in manually.

Note that there is no checking whether the country code is an accepted one. I was able to create a certificate with country 'XZ' which is not a code for any country.

List of accepted country codes.

This could be an area of improvement for gemini clients; for example by providing a menu or allowing to define a default certificate country in the settings.

# determine country
my ($cert_country) = ( $ENV{'TLS_CLIENT_ISSUER'} =~ m|/C=([A-Z]{2,})| );
unless ($cert_country) { $cert_country = '<None>' };
#...
print "Country: $cert_country\n";

The regex is probably too permissive, as all permissible country codes seem to be only 2 letters, never more than 2.

Bonus: get country from IP address

Note this approach is more intrusive and gives the user less control than the above.

use IP::Country::Fast;
my $reg = IP::Country::Fast->new();
my $remote_addr = $ENV{'REMOTE_ADDR'};
my $ip_country = $reg->inet_atocc( $remote_addr );

Use flock to account for concurrency

The webpage may be accessed simultaneously by different users which could lead to corruption if data isn't updated one at a time. Therefore, I added flock into the mix to hold a lock while reading and then writing the file.

use Fcntl qw( :flock );

# ...
open(my $fh, '+<', $file) or die "Open: $!";
flock($fh, LOCK_EX) or die "Locking: $!";

# ...
# <read file, update data, write updated file>

flock($fh, LOCK_UN)             or die "Cannot unlock file - $!";
close $fh                       or die "Closing: $!";

Conclusion

popcat.cgi is a simple example for CGI and client certificates in gemini. Client certificates can be used to gather country information, but gemini clients may need to improve the implementation of this data field to make it more usable.