💾 Archived View for thfr.info › gemini › cgi-and-client-cert › index.gmi captured on 2023-11-04 at 11:36:43. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-01-29)
-=-=-=-=-=-=-
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.
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 - 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.
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
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.
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.
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.
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 );
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: $!";
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.