#!/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;
}