File Coverage

blib/lib/Net/Gemini.pm
Criterion Covered Total %
statement 110 133 82.7
branch 35 58 60.3
condition 10 14 71.4
subroutine 22 24 91.6
pod 11 11 100.0
total 188 240 78.3


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # a Gemini protocol client
4             #
5             # "The conjunction of Jupiter with one of the stars of Gemini, which
6             # 'we ourselves have seen' (1.6.343b30) has been dated in recent years
7             # to December 337 BC."
8             # -- Malcolm Wilson. Structure and Method in Aristotle's Meteorologica.
9              
10             # NOTE this silently accepts URI with userinfo; those probably
11             # should be failed?
12             #
13             # KLUGE this may break if the URI module ever gets URI/gemini.pm
14             package URI::gemini {
15 4     4   934018 use URI;
  4         19124  
  4         134  
16 4     4   1766 use parent 'URI::_server';
  4         1121  
  4         21  
17 11     11   1124 sub default_port { 1965 }
18 0     0   0 sub userinfo { return undef } # gemini has no userinfo
19 0     0   0 sub secure { 1 }
20              
21             sub canonical {
22 11     11   23 my $self = shift;
23 11         39 my $other = $self->SUPER::canonical;
24 11         647 $self->SUPER::userinfo(undef); # gemini has no userinfo
25              
26 11   33     1012 my $slash_path =
27             defined( $other->authority )
28             && !length( $other->path )
29             && !defined( $other->query );
30              
31 11 50       605 if ($slash_path) {
32 0 0       0 $other = $other->clone if $other == $self;
33 0         0 $other->path("/");
34             }
35 11         25 $other;
36             }
37             }
38              
39             package Net::Gemini;
40             our $VERSION = '0.06';
41 4     4   25601 use strict;
  4         8  
  4         84  
42 4     4   29 use warnings;
  4         14  
  4         87  
43 4     4   20 use Encode ();
  4         38  
  4         101  
44 4     4   23 use IO::Socket::SSL;
  4         8  
  4         29  
45 4     4   655 use Net::SSLeay;
  4         9  
  4         5793  
46              
47             sub _DEFAULT_BUFSIZE () { 4096 }
48              
49 1     1 1 51 sub code { $_[0]{_code} } # 0..6 response code
50 1     1 1 15 sub content { $_[0]{_buf} }
51 1     1 1 11 sub error { $_[0]{_error} } # error message for 0 code
52 1     1 1 19 sub host { $_[0]{_host} }
53 1     1 1 18 sub meta { $_[0]{_meta} }
54 1     1 1 21 sub port { $_[0]{_port} }
55 1     1 1 21 sub socket { $_[0]{_socket} }
56 1     1 1 671 sub status { $_[0]{_status} } # two digit '1x', '2x', ... response code
57 1     1 1 4 sub uri { $_[0]{_uri} }
58              
59             # see VERIFICATION below; the caller should supply a custom callback.
60             # the default is thus "Trust On Almost Any Use" (TOAAU) or similar to
61             # what gg(1) of gmid does
62 5     5   35 sub _verify_ssl { 1 }
63              
64             # minimal method to get a resource (see also ->request)
65             sub get {
66 17     17 1 31920 my ( $class, $source, %param ) = @_;
67 17         110 my %obj;
68 17 100       75 unless ( defined $source ) {
69 3         54 @obj{qw(_code _error)} = ( 0, "source is not defined" );
70 3         30 goto BLESSING;
71             }
72              
73 14         199 $obj{_uri} = URI->new($source);
74 14 100       6070 unless ( $obj{_uri}->scheme eq 'gemini' ) {
75 3         321 @obj{qw(_code _error)} = ( 0, "could not parse '$source'" );
76 3         15 goto BLESSING;
77             }
78 11         329 @obj{qw/_host _port/} = ( $obj{_uri}->host, $obj{_uri}->port );
79              
80 11         1089 my $yuri = $obj{_uri}->canonical;
81 11 100       107 if ( length $yuri > 1024 ) {
82 3         33 @obj{qw(_code _error)} = ( 0, "URI is too long" );
83 3         15 goto BLESSING;
84             }
85              
86             # VERIFICATION is based on the following though much remains up to
87             # the caller to manage
88             # gemini://makeworld.space/gemlog/2020-07-03-tofu-rec.gmi
89             # gemini://alexschroeder.ch/page/2020-07-20%20Does%20a%20Gemini%20certificate%20need%20a%20Common%20Name%20matching%20the%20domain%3F
90             eval {
91             $obj{_socket} = IO::Socket::SSL->new(
92             SSL_hostname => $obj{_host}, # SNI
93             ( $param{tofu} ? ( SSL_verifycn_scheme => 'none' ) : () ),
94             SSL_verify_callback => sub {
95 5     5   119818 my ( $ok, $ctx_store, $certname, $error, $cert, $depth ) = @_;
96 5 50       48 if ( $depth != 0 ) {
97 0 0       0 return 1 if $param{tofu};
98 0         0 return $ok;
99             }
100             ( $param{verify_ssl} || \&_verify_ssl )->(
101 5   50     447 @obj{qw(_host _port)},
102             Net::SSLeay::X509_get_fingerprint( $cert, 'sha256' ),
103             Net::SSLeay::P_ASN1_TIME_get_isotime( Net::SSLeay::X509_get_notBefore($cert) ),
104             Net::SSLeay::P_ASN1_TIME_get_isotime( Net::SSLeay::X509_get_notAfter($cert) ),
105             $ok,
106             $cert
107             );
108             },
109 8         175 ( exists $param{ssl} ? %{ $param{ssl} } : () ),
110             PeerHost => $obj{_host},
111             PeerPort => $obj{_port},
112 8 50       155 ) or die $!;
    50          
    50          
113 5         8744 1;
114 8 100       73 } or do {
115 3         1209 @obj{qw(_code _error)} = ( 0, "IO::Socket::SSL failed: $@" );
116 3         30 goto BLESSING;
117             };
118              
119 5         58 binmode $obj{_socket}, ':raw';
120              
121 5         54 my $n = syswrite $obj{_socket}, "$yuri\r\n";
122 5 50       941 unless ( defined $n ) {
123 0         0 @obj{qw(_code _error)} = ( 0, "send URI failed: $!" );
124 0         0 goto BLESSING;
125             }
126             # KLUGE we're done with the connection as a writer at this point,
127             # but IO::Socket::SSL does not appear to offer a public means to
128             # only call shutdown and nothing else. using this is a bit risky
129             # should the IO::Socket::SSL internals change
130 0         0 Net::SSLeay::shutdown( ${ *{ $obj{_socket} } }{'_SSL_object'} )
  0         0  
131 5 50       45 if $param{early_shutdown};
132              
133             # get the STATUS SPACE header response (and, probably, more)
134 5         141 $obj{_buf} = '';
135 5         28 while (1) {
136 13   100     126 my $n = sysread $obj{_socket}, my $buf, $param{bufsize} || _DEFAULT_BUFSIZE;
137 13 50       4017091 unless ( defined $n ) {
138 0         0 @obj{qw(_code _error)} = ( 0, "recv response failed: $!" );
139 0         0 goto BLESSING;
140             }
141 13 50       53 if ( $n == 0 ) {
142 0         0 @obj{qw(_code _error)} = ( 0, "recv EOF" );
143 0         0 goto BLESSING;
144             }
145 13         48 $obj{_buf} .= $buf;
146 13 100       75 last if length $obj{_buf} >= 3;
147             }
148 5 100       156 if ( $obj{_buf} =~ m/^(([1-6])[0-9])[ ]/ ) {
149 3         86 @obj{qw(_status _code)} = ( $1, $2 );
150 3         25 substr $obj{_buf}, 0, 3, '';
151             } else {
152             @obj{qw(_code _error)} =
153 2         122 ( 0, "invalid response " . sprintf "%vx", substr $obj{_buf}, 0, 3 );
154 2         50 goto BLESSING;
155             }
156              
157             # META -- at most 1024 characters, followed by \r\n. the loop is in
158             # the event the server is being naughty and trickling bytes in one
159             # by one (probably you will want a timeout somewhere, or an async
160             # version of this code)
161 3   100     90 my $bufsize = $param{bufsize} || _DEFAULT_BUFSIZE;
162 3         22 while (1) {
163 2053 100       5778 if ( $obj{_buf} =~ m/^(.{0,1024}?)\r\n/ ) {
164 1         27 $obj{_meta} = $1;
165 1         10 my $len = length $obj{_meta};
166 1 50       17 if ( $len == 0 ) {
167             # special case mentioned in the specification
168 0         0 $obj{_meta} = 'text/gemini; charset=utf-8';
169             } else {
170             eval {
171 1         42 $obj{_meta} = Encode::decode( 'UTF-8', $obj{_meta}, Encode::FB_CROAK );
172 1         479 1;
173 1 50       3 } or do {
174 0         0 @obj{qw(_code _error)} = ( 0, "failed to decode meta: $@" );
175 0         0 goto BLESSING;
176             };
177 1         10 substr $obj{_buf}, 0, $len + 2, ''; # +2 for the \r\n
178             }
179 1         13 last;
180             } else {
181 2052         2620 my $len = length $obj{_buf};
182 2052 100       3430 if ( $len > 1024 ) {
183 2         62 @obj{qw(_code _error)} = ( 0, "meta is too long" );
184 2         46 goto BLESSING;
185             }
186 2050         2366 my $buf;
187 2050         6112 my $n = sysread $obj{_socket}, $buf, $bufsize;
188 2050 50       415014 unless ( defined $n ) {
189 0         0 @obj{qw(_code _error)} = ( 0, "recv response failed: $!" );
190 0         0 goto BLESSING;
191             }
192 2050 50       4102 if ( $n == 0 ) {
193 0         0 @obj{qw(_code _error)} = ( 0, "recv EOF" );
194 0         0 goto BLESSING;
195             }
196 2050         4384 $obj{_buf} .= $buf;
197             }
198             }
199              
200             BLESSING:
201 17 100 100     331 close $obj{_socket} if defined $obj{_socket} and $obj{_code} != 2;
202 17         2759 bless( \%obj, $class ), $obj{_code};
203             }
204              
205             # drain what remains (if anything) via a callback interface. assumes
206             # that a ->get call has been made
207             sub getmore {
208 1     1 1 3 my ( $self, $callback, %param ) = @_;
209              
210 1         7 my $len = length $self->{_buf};
211 1 50       6 if ($len) {
212 1 50       10 $callback->( $self->{_buf}, $len ) or return;
213             }
214              
215 1   50     19 my $bufsize = $param{bufsize} || 4096;
216 1         3 while (1) {
217 1         2 my $buf;
218 1         7 $len = sysread $self->{_socket}, $buf, $bufsize;
219 1 50       104 if ( !defined $len ) {
    50          
220 0         0 die "sysread failed: $!\n";
221             } elsif ( $len == 0 ) {
222 1         4 last;
223             }
224 0 0       0 $callback->( $buf, $len ) or return;
225             }
226 1         6 close $self->{_socket};
227             }
228              
229             1;
230             __END__