#!/usr/bin/perl # This is a gemini client. # Copyright (C) 2022 Jake Thoughts # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # jake@jakes-mail.top, jjakke@member.fsf.org (email & xmpp) # I sure hope that this is its own branch hahah use strict; use warnings; #use diagnostics; #use utf8; #use bytes; use feature qw(refaliasing); no warnings qw(experimental::refaliasing); our $VERSION = 'v1.0.1Tk'; # Modules use IO::Socket::SSL; # CPAN use URL::XS qw(parse_url split_url_path parse_url_query); # CPAN use IO::Pager; # CPAN require Text::Wrapper; # CPAN #use Term::ReadKey; # CPAN use Term::ANSIColor; # Core use Path::Naive qw(normalize_path); # CPAN #use Text::ParseWords; # Core use Term::ReadLine; # CPAN use Smart::Comments; # CPAN use URI::Encode qw(uri_encode); # CPAN use IO::Socket::SSL::Utils; # CPAN use File::Slurper qw(read_dir read_text); # CPAN use Data::Dumper; # Core use File::BaseDir qw(xdg_config_home xdg_data_home xdg_cache_home); # CPAN use TOML qw(from_toml to_toml); # CPAN use Tk; use Tk::Text; use Tk::TextANSIColor; # sudo cpanm IO::Socket::SSL URL::XS IO::Pager Text::Wraper Term::ReadKey Path::Naive Text::ParseWords Term::ReadLine Smart::Comments URI::Encode IO::Socket::SSL::Utils File::Slurper File::BaseDir TOML Tk my $top = MainWindow->new( -title=>'Gmi Main Window' ); my(@bold, @normal, $tag); if ($top->depth > 1) { @bold = (-background => '#43ce80', qw/-relief raised -borderwidth 1/); @normal = (-background => undef, qw/-relief flat/); } else { @bold = (qw/-foreground white -background black/); @normal = (-foreground => undef, -background => undef); } my $control_frame = $top->Frame() ->pack( -fill=>'x', -expand=>0, ); my $back_botton = $control_frame->Button( -text=>'Back', -command=>\&back, )->pack( -side=>'left', ); my $label = $control_frame->Label( -text=>'URL:', )->pack( -side=>'left' ); my $entry = $control_frame->Entry( )->pack( -side=>'left', -expand=>1, -fill=>'x', ); my $button = $control_frame->Button( -text=>'Go!', -command=>\&go_to_url, )->pack( -side=>'left', ); my $body_frame = $top->Frame() ->pack( #-side=>'top', -fill=>'both', -expand=>1, ); my $t = $body_frame->Scrolled( 'TextANSIColor', -relief=>'groove', -borderwidth=>2, -setgrid=>'true', -wrap=>'word', -font=>'arimo 12', #-height=>30, -scrollbars=>'e', #-font => 'fixed', )->pack( -expand=>'yes', -fill=>'both', ); $t->insert('0.0', "Type in a valid domain name then press 'Go!'"); my %config = ( 'use_pager' => 1, 'pager_text_wrap_auto' => 1, 'textwrap' => 70, # not used if pager_text_warp_auto is 1 'timeout' => 10, 'pretty_links' => 0, 'pretty_headers' => 1, 'auto_redirect' => 3, 'pager' => 'less -R', 'doc_out' => 1, 'keep_history' => 0, ); my @doc; my @links; my $current_url = ""; my @history; my @bookmarks; my $prompt = ""; my $pre_block = 0; my @status_code; my $status_code1; my $status_code2; my $auto_redirect_count = 0; my $sleep; my $use_cert = 0; my $cert; my $key; my %known_hosts; my $program_name = 'jakes-gemini-client-tk'; my $bd = File::BaseDir->new; my $xdg_config = $bd->xdg_config_home . "/$program_name/"; my $xdg_data = $bd->xdg_data_home . "/$program_name/"; my $xdg_cache = $bd->xdg_cache_home . "/$program_name/"; if (! -e $xdg_config) { mkdir($xdg_config) } if (! -e $xdg_data) { mkdir($xdg_data) } if (! -e $xdg_cache) { mkdir($xdg_cache) } ### $xdg_config ### $xdg_data ### $xdg_cache my @cache; load_config(); MainLoop; #my $wrapper = Text::Wrapper->new(columns=>$config{textwrap}, body_start=>''); #$ENV{PAGER} = $config{pager}; sub go_to_url { url($entry->get()); } sub show_to_user { my ($text) = @_; my $window=$top->Toplevel( -title=>'Gmi' ); my $label=$window->Label( -text=>"$text", )->pack(); } sub update_entry_Tk_url { $entry->delete(0,'end'); $entry->insert(0.0,$_[0]); } sub url { my ($url) = @_; ### $url if (!$url) { show_to_user("Example: [gemini://]capsule.com/\n"); return 1;} if (! has_scheme($url) ) { $url = "gemini://$url"; } eval { $url = parse_url($url); }; if ($@) { show_to_user( "$@\n"); return 0; } ### $url if (! $url->{port} or $url->{port} eq 0) { $url->{port} = '1965'; } my $cl; if ($use_cert and $cert and $key) { $cl = IO::Socket::SSL->new( PeerHost => "$url->{host}", PeerPort => "$url->{port}", SSL_verify_mode => SSL_VERIFY_NONE, SSL_verifycn_name => "$url->{host}", SSL_version => '!SSLv2:!SSLv3:!TLSv1:!TLSv1_1', SSL_fast_shutdown => 1, SSL_cert_file => "$cert", SSL_key_file => "$key", Timeout => 2, ); } else { $cl = IO::Socket::SSL->new( PeerHost => "$url->{host}", PeerPort => "$url->{port}", SSL_verify_mode => SSL_VERIFY_NONE, SSL_verifycn_name => "$url->{host}", SSL_version => '!SSLv2:!SSLv3:!TLSv1:!TLSv1_1', SSL_fast_shutdown => 1, Timeout => 2, ); } if ($cl) { $current_url = return_url($url); ### $current_url ### $url update_entry_Tk_url($current_url); if (@doc) { @cache = @doc; } undef(@doc); if (peer_cert($cl, $url->{host})) { eval { local $SIG{ALRM} = sub {close ($cl); unshift(@doc,""); die "TIMEOUT";}; alarm $config{timeout}; # gemini spec: # is an absolute path print $cl "$current_url\r\n"; @doc = <$cl>; close($cl); # if this isnt here, the kernel will have to clean up unused ports alarm 0; }; if ($@) { show_to_user( "Timed out after $config{timeout} seconds - server is taking too long.\n"); update_history( ["$current_url", "timed out"] ); ### @doc } else { follow_status_code(); } } else { show_to_user( "Not connecting\n"); } } else { show_to_user( "error=$!, ssl_error=$SSL_ERROR\n"); } } sub peer_cert { # ## peer_cert # 1. compare figureprint if possible # 2. is it expired? # 3. if error: ask user for imput my ($cl, $hostname) = @_; # ## before CERT_asHash my @peer_certs = $cl->peer_certificates; #show_to_user( Dumper(\@peer_certs)); # # if the server sends us more than ONE cert then I am baffled as to why but it happens my $peer_cert = CERT_asHash($peer_certs[0]); # ## IO Socket SSL Utils already has done fingerprinting so we do not need to. my $peer_fingerprint = $peer_cert->{fingerprint_sha256}; my $peer_not_before = $peer_cert->{not_before}; my $peer_not_after = $peer_cert->{not_after}; my $verify_hostname = $cl->verify_hostname($hostname); ### $peer_cert ### $peer_fingerprint ### $peer_not_before ### $peer_not_after ### $verify_hostname if (! $verify_hostname) { show_to_user( "The server offers a cert that doesn't match their domain name.\n"); return 0; } # compare figureprint... if ($known_hosts{$hostname}) { # user HAS visited this domain before if ($known_hosts{$hostname} eq $peer_fingerprint) { # fingerprint same ; } else { # fingerprint NOT same show_to_user( colored("!!! THE HOST'S FINGERPRINT IS NOT THE SAME !!!\n", 'bright_red')); show_to_user( "Do you still wish to connect to it? y/N "); chomp(my $yORn = ); if (lc $yORn eq 'y') { show_to_user( "Would you like to replace the known_hosts fingerprint with this one? y/N "); chomp(my $yORn = ); if (lc $yORn eq 'y') { $known_hosts{$hostname} = $peer_fingerprint; } } else { return 0; } } } else { # user has NOT visited this domain before TOFU $known_hosts{$hostname} = $peer_fingerprint; } # check expiratory if ($peer_not_before < time() and time() < $peer_not_after) { ### not expired ; } else { ### expired! show_to_user( "The host has an expired certificate. Connect anyway? y/N "); chomp(my $yORn = ); if (lc $yORn eq 'y') { ; } else { return 0; } } return 1; } sub follow_status_code { return 1 if (!@doc); @status_code = split(' ', $doc[0]); $status_code1 = substr($doc[0],0,1); $status_code2 = substr($doc[0],1,1); update_history( ["$current_url", "$status_code1$status_code2"] ); my $bad_code = "The server sent an invalid status code, not defined by gemini specifications.\n@doc"; if ($status_code1 == 2) { # 2 will probably be the most common server reply if ($status_code2 == 0) { shift(@doc); # user probably doesn't want to see the 20 */* every time # at some point deal with image/* and other media get_links(); pretty_links() if ($config{pretty_links}); pretty_headers() if ($config{pretty_headers}); if ($config{doc_out}) { display(); } } else { show_to_user( $bad_code); } } elsif ($status_code1 == 1) { if ($status_code2 == 0) { show_to_user( "Server wants you to type something:\n@doc"); } elsif ($status_code2 == 1) { show_to_user( "Server wants you to type something secretly (do note: not secure):\n@doc"); #ReadMode 2; } else { show_to_user("$bad_code"); show_to_user( "\nPresumably, the server wants you to type something:\n"); } chomp(my $input = ); $input = uri_encode($input); #ReadMode 1; url("$current_url?$input"); clear_query(); } elsif ($status_code1 == 3) { show_to_user($bad_code) if ($status_code2 !~ m/^0|1$/); # not worrying about permanent/temporary yet show_to_user( "Redirecting anyway.\n") if ($status_code2 !~ m/^0|1$/); if ($config{auto_redirect}) { $auto_redirect_count++; if ($auto_redirect_count < $config{auto_redirect}) { show_to_user( "Redirection...\n"); ### @status_code access_resource($status_code[1]); # redirection can be './hello.gmi' } else { show_to_user( "Reached limit for auto redirection.\n@doc"); } } else { show_to_user( "The server wants to redirect us, however, autoredirect has turned off.\n@doc"); } $auto_redirect_count = 0; } elsif ($status_code1 == 4) { if ($status_code2 == 0) { show_to_user( "Temporary failure.\n@doc"); } elsif ($status_code2 == 1) { show_to_user( "Server is unavailable (yet somehow sent a reply to us).\n@doc"); } elsif ($status_code2 == 2) { show_to_user( "Server's CGI script is broken.\n@doc"); } elsif ($status_code2 == 3) { show_to_user( "Server was unable to proxy content.\n@doc"); } elsif ($status_code2 == 4) { if (!$sleep) { $sleep = 5; show_to_user("Server wants us to slow down.\n" . "Reconnecting after $sleep seconds\n" . "Press 'ctrl-C' to cancel sleep\n" . "@doc\n"); eval { local $SIG{INT} = sub { die "Cancelling Sleep!\n" }; sleep ($sleep); # give the server an extra second url($current_url); } } else { show_to_user("The server is asking us to slow down and try again.\n" . "We have already done this once: not doing it again.\n"); } undef($sleep); } else { show_to_user("$bad_code"); } } elsif ($status_code1 == 5) { if ($status_code2 == 0) { show_to_user( "Permanent failure. Requests to this URI will reliably fail.\n@doc"); } elsif ($status_code2 == 1) { show_to_user( "Not found. Not available but *may* be in the future\n@doc"); } elsif ($status_code2 == 2) { show_to_user( "Gone. This resource will not be available again.\n@doc"); } elsif ($status_code2 == 3) { show_to_user("Proxy Request Refused. Resource not served by this server and this\n" . "server does not accept proxy requests.\n@doc"); } elsif ($status_code2 == 9) { show_to_user( "Bad Request. The server is unable to parse our request.\n@doc"); } else { show_to_user($bad_code); } } elsif ($status_code1 == 6) { if ($status_code2 == 0) { show_to_user( "The server is asking you for a certificate. (cert-use to load a cert)\n@doc"); } elsif ($status_code2 == 1) { show_to_user( "The certificate you supplied is not authorized to access this resource.\n@doc"); } elsif ($status_code2 == 2) { show_to_user( "The certificate you supplied is malformed. There is something wrong with it.\n@doc"); } else { show_to_user( $bad_code); } } else { show_to_user( $bad_code); } ### @status_code; } #sub urlrelative { # my ($urlr) = @_; # if (! $urlr) { $urlr = ""} # if (! $current_url) {show_to_user( "Relative movement impossible: no current URL.\n"); return 1;} # my $end_with_slash = 1 if ($urlr =~ m|/$|); # my $dot_only = 1 if ($urlr =~ m|^\.$|); # my $dot_2_only = 1 if ($urlr =~ m|^\.\.$|); # since we are going up into a dir # my $c_url_directory = 1 if ($current_url =~ m|/$|); # my $remove_slash = 1 if ($urlr =~ m|^\\$|); # back slash \ not a forward slash / # # ### is urlr a slash only ? # if ($urlr =~ m|^/$|) { # if slash only # $current_url .= "/"; # url("$current_url"); # return 0; # } # # ### remove slash ? # if ($remove_slash) { # substr($current_url,-1,1,''); # ### $current_url # url($current_url); # return 0; # } # # ### is the current url a directory? # if ($c_url_directory) { # $current_url .= "$urlr"; # my $c_p_url = parse_url($current_url); # ### $c_p_url # # if $urlr has '..' then normalize_path will not go beyond the last element # # which is why prefixing a '/' and then taking it away (parse_url doesnt have # # the forward / ) will result in expected behavior as host always end with '/' # $c_p_url->{path} = normalize_path("/" . $c_p_url->{path}); # if ($end_with_slash or $dot_only or $dot_2_only) { # $c_p_url->{path} .= "/"; # normalize path will remove it # } # substr($c_p_url->{path},0,1,''); # if ($c_p_url->{path} eq '/') { # delete $c_p_url->{path}; # dont want to ask for 'gemini://host.com//' # } # url(return_url($c_p_url)); # return 0; # } # # ### current url is not a directory, so should be fine to remove last item on path # # ^ some servers serve index page on 'dir' rather than 'dir/' :v annoying # # authors using servers like that will notice that the links on index page need to be # # carefully managed, because ALL clients (that handle links correctly) *will* mess up. # if (! $c_url_directory) { # my $c_p_url = parse_url($current_url); # ### $c_p_url # my $c_paths_url = split_url_path($c_p_url->{path}, 256); # ### $c_paths_url # pop(@$c_paths_url); # ### $c_paths_url # delete ($c_p_url->{path}); # for (@$c_paths_url) { # $c_p_url->{path} .= "$_/"; # } # ### $c_p_url # $c_p_url->{path} .= "$urlr"; # $c_p_url->{path} = normalize_path("/" . "$c_p_url->{path}"); # if ($end_with_slash or $dot_only or $dot_2_only) { # $c_p_url->{path} .= "/"; # normalize path will remove it # } # substr($c_p_url->{path},0,1,''); # if ($c_p_url->{path} eq '/') { # delete $c_p_url->{path}; # dont want to ask for 'gemini://host.com//' # } # ### $c_p_url # url(return_url($c_p_url)); # return 0; # } #} sub return_url { my ($url) = @_; my $url_string; # ## @_ if (ref($url) ne 'HASH') { # assuming string if ($url !~ m|.*://|) { $url =~ s|(.*)|gemini://$1|; } $url = parse_url($url); } my $scheme = $url->{scheme}; my $username = $url->{username}; my $password = $url->{password}; my $host = $url->{host}; my $port = $url->{port}; my $path = $url->{path}; my $query = $url->{query}; my $fragment = $url->{fragment}; # 'https://peter:bro@localhost:8989/some/path/to/resource?q1=yes&q2=no&q3=maybe#frag=1'; # a valid url ^^ if (! $scheme) { $url_string .= "gemini://"; } else { $url_string .= "$scheme://"; } if ($username and $password) { $url_string .= "$username:$password@"; } elsif ($username) { $url_string .= "$username@"; } $url_string .= "$host" if ($host); if ($port and $port ne 1965) { $url_string .= ":$port"; } $url_string .= "/"; $url_string .= "$path" if ($path); $url_string .= "?$query" if ($query); $url_string .= "#$fragment" if ($fragment); return $url_string; } sub get_links { undef(@links); #for my $line (split('\n', $doc)) { for (@doc) { #if ($line =~ m/^=>[\s]*([\w\d\-\\\/\.\:\~\?\=\#]+)[\s]*(.+)?$/gm) { _pre_block($_); if ($_ =~ m/^=>[\s]*([\w\d\-\\\/\.\:\~\?\=\#]+)[\s]*(.+)?$/gm and not $pre_block) { if ($2) { push(@links, ["$1", "$2"]); } else { push(@links, ["$1", "$1"]); } } } } #sub hist { # my $hist; # my $counter = 1; # ### @history # for my $item (@history) { # ### $item # $hist .= "[$counter] ". colored("$$item[0]", 'underline') ." $$item[1]\n"; # $counter++; # } # if ($hist) { # show_to_user( $hist); # } # else { # show_to_user( "\n"); # } #} sub update_history { my ($history) = @_; push(@history, $history); } sub links { my $d; my $counter = 1; for my $item (@links) { $d .= "[$counter] ". colored($$item[0], 'underline') . " $$item[1]\n"; $counter++; } if ($d) { show_to_user( $d); } else { show_to_user( "No links.\n"); } } sub nav { # Setting 0: from links # Setting 1: from history # Setting 2: from bookmarks my ($setting,$n) = @_; ### $n ### $setting ### @_ if (! $n or $n !~ /\d+/) { #print $OUT "Try 'links', 'hist', or 'bookmarks' then 'navY X', where x is the number and Y. " . #"is nav mode.\n"; return 1; } $n -= 1; my $link; if ($setting == 0 and @links and exists $links[$n][0]) { $link = $links[$n][0]; } elsif ($setting == 1 and @history and exists $history[$n][0]) { $link = $history[$n][0]; } elsif ($setting == 2 and @bookmarks and exists $bookmarks[$n]) { $link = $bookmarks[$n]; } else { show_to_user( "'" . ($n+1) ."' isn't available.\n"); return 1; } access_resource($link); } sub access_resource { my ($link) = @_; # This implies it is valid fqdn if (has_scheme($link)) { url("$link"); } elsif (substr($link,0,2) eq '//') { # technically valid url("gemini:$link"); } else { my $c_url = parse_url($current_url); my $end_with_slash = 1 if ($link =~ m|.*/$|); my $begin_with_slash = 1 if ($link =~ m|^/.*|); ### absolute location ? if ($begin_with_slash) { delete($c_url->{path}); $c_url->{path} = $link; #if ($end_with_slash){ # $c_url->{path} .= "/"; #} substr($c_url->{path},0,1,''); url(return_url($c_url)); return 0; } ### current url is a directory ? if ($current_url =~ m|.*/$|) { $current_url .= $link; #if ($end_with_slash) { # $current_url .= "/"; #} url(return_url($current_url)); return 0; } else { ### current url must NOT be a directory my $d; if ($c_url->{path}) { my $cp_url = split_url_path($c_url->{path}, 256); pop(@$cp_url); for (@$cp_url) { $d .= "$_/"; } } $d .= $link; $c_url->{path} = normalize_path($d); if ($end_with_slash) { $c_url->{path} .= "/"; } url(return_url($c_url)); return 0; } } } sub has_scheme { return ($_[0] =~ m|^.*(://)|); } sub root { my $new_url = parse_url($current_url); url("$new_url->{scheme}://$new_url->{host}/"); } sub back { my ($display) = @_; ### @_ ### $display #my $dok_out = $config{doc_out}; #$config{doc_out} = 0 if ($display == 1); if (@history >= 2) { $current_url="$history[-2][0]"; update_entry_Tk_url($current_url); if (@cache) { my @array = @doc; @doc = @cache; @cache = @array; get_links(); update_history( ["$current_url", "back"] ); display() if (! $display); } else { # when the user just started the program and types 'back' and has history url("$history[-2][0]"); } } else { show_to_user( "No such history there."); } ### @history #$config{doc_out} = $dok_out if ($display == 1); } sub display { $t->delete('0.0', 'end'); my $counter = 0; foreach my $line (@doc) { _pre_block($line); if ((substr($line,0,2)) eq '=>' and not $pre_block) { $line =~ m/^=>[\s]*([\w\d\-\\\/\.\:\~\?\=\#]+)[\s]*(.+)?$/; my $_line; if ($2) { $_line = colored("[". ($counter+1) . _is_not_scheme_gemini($1) . "]",'underline') . " $2\n"; } else { $_line = colored("[" . ($counter+1) . _is_not_scheme_gemini($1) . "]",'underline') . " $1\n"; } $t->insert('end', "$_line", "URL$counter"); $counter++; } else { $t->insert('end', "$line"); } } #print Dumper($t->tagNames()); for my $item ($t->tagNames()) { if (substr($item,0,3) eq 'URL') { $t->tagBind("$item", ''=> sub {shift->tagConfigure("$item",@bold)}); $t->tagBind("$item", ''=> sub {shift->tagConfigure("$item",@normal)}); $item =~ m/^URL(\d+)+$/; my $number = $1; $t->tagBind("$item", '<1>', sub {nav(0,($number+1))}); } } # Author's note: ^ do not touch unless you know why I did what I did. # Absolutely maddening. ... You'd think a while loop and incrementing # $counter (after setting to 0) and doing "URL$counter" would work # but actually that doesn't work at all, and I don't know why. } sub toggle { # refaliasing \my $t = \$_[0]; if ($t == 1) { $t = 0; } else { $t = 1; } if (! $_[1]) { show_to_user( "$t\n"); } } sub pretty_headers { for (@doc) { _pre_block($_); if ((substr($_,0,3)) eq '###' and not $pre_block) { $_ =~ s/^###[\s]*(.*)$/colored("$1",'underline')/e; } if ((substr($_,0,2)) eq '##' and not $pre_block) { $_ =~ s/^##[\s]*(.*)$/colored("$1",'bold')/e; } if ((substr($_,0,1)) eq '#' and not $pre_block) { $_ =~ s/^#[\s]*(.*)$/colored("$1",'bold', 'underline')/e; } } } sub _is_not_scheme_gemini { ### @_ my ($thing) = $_[0]; if (has_scheme($thing)) { $_[0] =~ m|(.*)://.*|; if ($1 ne 'gemini') { return " $1"; } } return ''; } sub _pre_block { if (substr($_[0],0,3) eq '```') { toggle($pre_block,1); return 1; } } sub value_number { # refaliasing \my $v = \$_[0]; my $numb = $_[1]; my $k = $_[2]; $v = $numb if ($numb =~ m/^\d*$/); show_to_user( "$k is now: $v\n"); ### @_ } sub clear_query { return 1 if (!$current_url); my @url = split('\?', $current_url); $current_url = $url[0]; } sub cert_create { my $old_umask = umask(077); if (! -e "$xdg_data/certs") { mkdir("$xdg_data/certs"); } show_to_user( "What should the profile name be? "); chomp(my $input = ); $input =~ tr|/|.|; if (-e "$xdg_data/certs/$input") { show_to_user( "Sorry, but that already exists.\n"); } else { mkdir("$xdg_data/certs/$input"); show_to_user( "What should the commonName be? (it will be used to identify you easily) "); chomp(my $common_name = ); my $expire; while (1) { show_to_user( "When should this expire? (1s, 1h, 1d, 1m, 1y, 0s (cancel)) "); chomp($expire = ); if ($expire =~ m/^(\d+)+(\w)$/) { if (lc $2 eq 's') { if ($1 == 0) { show_to_user( "Cancelling.\n"); umask($old_umask); return 1; } else { $expire = $1 + time(); } } elsif (lc $2 eq 'h') { $expire = ($1*3600) + time(); # 3600 seconds in an hour } elsif (lc $2 eq 'd') { $expire = ($1*86400) + time(); # 86400 seconds in a day } elsif (lc $2 eq 'm') { $expire = ($1*2592000) + time(); # 2592000 seconds in 30 days (month) } elsif (lc $2 eq 'y') { $expire = ($1*31536000) + time(); # 31536000 seconds in 365 days (year) } else { next; } } else { next; } last; } my $not_before = time(); ### $expire ### $not_before my %cert = ( 'subject' => { 'commonName' => "$common_name", }, 'not_before' => $not_before, 'not_after' => $expire, ); my ($cert, $key) = CERT_create(%cert); PEM_cert2file($cert,"$xdg_data/certs/$input/cert.pem"); PEM_key2file($key,"$xdg_data/certs/$input/key.pem"); CERT_free($cert); KEY_free($key); } umask($old_umask); } sub cert_use { if (! $use_cert) { my $counter = 1; my @profiles; if (! -e "$xdg_data/certs") { show_to_user( "You need to create a cert first: 'cert-create'.\n"); return 1; } for (read_dir("$xdg_data/certs")) { show_to_user( "[$counter] $_\n"); push(@profiles, "$_"); $counter++; } if (! @profiles) { show_to_user( "You need to create a cert first: 'cert-create'.\n"); return 1; } show_to_user( "Which profile? "); chomp(my $input = ); if ($input) { eval { $cert = PEM_file2cert("$xdg_data/certs/$profiles[$input-1]/cert.pem"); $key = PEM_file2key("$xdg_data/certs/$profiles[$input-1]/key.pem"); CERT_free($cert); KEY_free($key); $cert = "$xdg_data/certs/$profiles[$input-1]/cert.pem"; $key = "$xdg_data/certs/$profiles[$input-1]/key.pem"; }; if ($@) { show_to_user( "Something is wrong with the certificate/key. Will not use.\n$@"); $cert = ""; $key = ""; } else { show_to_user( "Cert and Key loaded.\n"); toggle($use_cert); } } else { show_to_user( "Did not load anything.\n"); } } else { show_to_user( "Unloading cert and key.\n"); toggle($use_cert); } } sub cert_del { if (! -e "$xdg_data/certs") { show_to_user( "You need to create a cert first: 'cert-create'.\n"); return 0; } my $counter = 1; my @profiles; for (read_dir("$xdg_data/certs")) { show_to_user( "[$counter] $_\n"); push(@profiles, "$_"); $counter++; } if (! @profiles) { show_to_user( "You need to create a cert first: 'cert-create'.\n"); return 0; } show_to_user( "Delete which profile? "); chomp(my $input = ); if ($input) { show_to_user( "Are you sure you want to delete $profiles[$input-1]/? y/N"); chomp(my $yORn = ); if (lc $yORn eq 'y') { unlink("$xdg_data/certs/$profiles[$input-1]/cert.pem", "$xdg_data/certs/$profiles[$input-1]/key.pem"); rmdir("$xdg_data/certs/$profiles[$input-1]"); if ($!) { show_to_user( "Unable to delete profile.\n$!\n"); } else { show_to_user( "Profile '$profiles[$input-1]' succesfully deleted.\n"); } } else { show_to_user( "Nothing is deleted.\n"); } } else { show_to_user( "Nothing is deleted.\n"); } } sub bookmarks { my $counter = 1; for (@bookmarks) { show_to_user( "[$counter] $_\n"); $counter++; } } sub bookmark_del { my ($b) = @_; if ($b and $b =~ m/^\d+$/) { if ($bookmarks[$b-1]) { show_to_user( "Removing $bookmarks[$b-1]"); splice(@bookmarks,$b-1,1); } ### @bookmarks } else { show_to_user( "Doing nothing."); } } sub bookmark_add { my ($b) = @_; if ($b) { push(@bookmarks, $b); } else { push(@bookmarks, $current_url); } } sub save_config { my $toml = to_toml(\%config); open(my $FH, '>', "$xdg_config/config.toml"); print $FH $toml; close $FH; } sub load_config { my ($config, $err); if (-e "$xdg_config/config.toml") { ($config, $err) = from_toml(read_text("$xdg_config/config.toml")); unless ($config) { show_to_user( "Error parsing toml: $err\n"); } } ### $config for (keys %$config) { $config{$_} = %$config{$_}; } if (-e "$xdg_data/history") { open(my $FH, '<', "$xdg_data/history"); while (<$FH>) { my @h = split(' ',$_); push(@history, [@h]); } close $FH; } ### @history if (-e "$xdg_data/bookmarks") { open(my $FH, '<', "$xdg_data/bookmarks"); while (<$FH>) { chomp($_); push(@bookmarks, $_); } close $FH; } ### @bookmarks if (-e "$xdg_data/known_hosts") { open(my $FH, '<', "$xdg_data/known_hosts"); while (<$FH>) { my ($a, $b) = split(' ', $_); $known_hosts{$a} = $b; } } ### %known_hosts } sub begin_exit { my $old_umask = umask(077); if ($config{keep_history}) { # over writes the history with old and new open(my $FH, '>', "$xdg_data/history"); for my $item (@history) { print $FH "$$item[0] $$item[1]\n"; } close $FH; } # over writes the bookmarks with old and new open(my $FH, '>', "$xdg_data/bookmarks"); for my $bookmark (@bookmarks) { print $FH "$bookmark\n"; } close $FH; # over writes the known_hosts with old and new open($FH, '>', "$xdg_data/known_hosts"); for my $known_host (sort keys %known_hosts) { print $FH "$known_host $known_hosts{$known_host}\n"; } close $FH; umask($old_umask); exit 0; }