Add simple echo CGI script.
[newgopher.git] / ngo
blob9dd22dec56c744c2b3084975f2fc34122e1eebf7
1 #!/usr/bin/perl
3 use strict;
4 use Curses::UI; # CPAN
5 use IO::Socket::SSL;
6 use File::Temp qw/ tempfile tempdir /;
7 use File::stat;
8 use File::Basename;
10 use TLSGopher;
12 my $argo = 0;
13 my $dl_only = 0;
14 my $secure = 0;
15 if (!$ARGV[0]) {
16 # show_banner();
17 show_usage();
18 exit 1;
20 while ($ARGV[$argo] =~ /^-/) {
21 $dl_only = 1 if ($ARGV[$argo] =~ /o/);
22 $secure = 1 if ($ARGV[$argo] =~ /s/);
23 $secure = 0 if ($ARGV[$argo] =~ /S/);
24 $argo++;
27 my @history;
29 my ($host, $port, $selector) = ng_parse_uri($ARGV[$argo]);
31 if ($dl_only) {
32 my $shortname = fileparse($selector);
33 download_aux($host, $port, $selector, $shortname);
34 print "\nSaved (?) file $shortname\n";
35 exit 0;
38 my $cui = create_ui();
40 #if (!$selector) {
41 goto_dialog();
43 $cui->mainloop();
45 sub ngo_next_selector()
47 my $last_id = length(@history) - 1;
48 my $last = $history[ $last_id ];
49 my %item = %$last;
50 ng_advance($item{'host'}, $item{'port'}, $item{'selector'}, $item{'data'});
52 sub ngo_queue_selector
54 my ($host, $port, $selector, $post) = @_;
55 push @history, {
56 'uri' => 'gopher://'.$host.':'.$port.'/'.$selector,
57 'selector' => $selector,
58 'host' => $host,
59 'port' => $port,
60 'data' => $post,
64 sub create_ui()
66 my $cui = new Curses::UI( -color_support => 1 );
68 my @menu = (
69 { -label => 'File',
70 -submenu => [
71 { -label => 'Go To... g', -value => \&goto_dialog },
72 { -label => 'Exit ^C', -value => \&exit_dialog },
75 { -label => 'Link',
76 -submenu => [
77 { -label => 'Download... d', -value => \&download_dialog },
78 { -label => 'Follow f', -value => \&follow_cb },
79 #{ -label => 'Go Back b', -value => 0 },
84 my $menu = $cui->add(
85 'menu', 'Menubar',
86 -menu => \@menu,
87 -fg => "white",
90 $cui->set_binding(sub {$menu->focus}, "\cX");
91 $cui->set_binding(sub {$cui->layout}, "\cR");
92 $cui->set_binding( \&exit_dialog , "\cC");
93 $cui->set_binding( \&exit_dialog , "q");
94 $cui->set_binding( \&goto_dialog , "g");
95 $cui->set_binding( \&follow_cb , "f");
96 $cui->set_binding( \&download_dialog , "d");
98 my $win1 = $cui->add(
99 'win1', 'Window',
100 -border => 1,
101 -y => 1,
102 -padbottom => 1,
103 -bfg => 'red',
106 my $win2 = $cui->add(
107 'win2', 'Window',
108 -border => 0,
109 -y => -1,
110 -height => 1,
111 -bfg => 'blue',
114 my $statusbar = $win2->add(
115 'label', 'Label',
116 -width => -1,
117 -paddingspaces => 1,
118 -text => 'Ready. Use ^X to access Menu.',
119 -bg => "white",
120 -fg => "black",
123 $win1->focus;
125 return $cui;
128 sub titlebar
130 my $win = $cui->getobj('win1');
131 $win->title($_[0]);
132 #my $curlen = $label->width - length($_[0]) - 2;
133 #if ($curlen < 0) { $curlen = 0; }
134 #$label->text($_[0].''.sprintf('%'.$curlen.'s', ''));
135 #$label->draw;
138 sub statusbar
140 if ($cui) {
141 my $win = $cui->getobj('win2');
142 my $label = $win->getobj('label');
143 my $curlen = $label->width - length($_[0]) - 2;
144 if ($curlen < 0) { $curlen = 0; }
145 $label->text($_[0].''.sprintf('%'.$curlen.'s', ''));
146 $label->draw;
147 } else {
148 $| = 1;
149 print STDOUT "\r";
150 print STDOUT $_[0];
151 $| = 1;
153 #$label->intellidraw;
156 sub exit_dialog()
158 my $return = $cui->dialog(
159 -message => "Do you really want to quit?",
160 -title => "Quit",
161 -buttons => ['yes', 'no'],
163 exit(0) if $return;
166 sub goto_dialog()
168 my $return = $cui->question(
169 -question => "'New Gopher' URI:",
170 -answer => $host.':'.$port.'/'.$selector,
171 #-title => "Are you sure???",
173 my ($host, $port, $selector) = ng_parse_uri($return);
175 ng_advance($host, $port, $selector);
176 #ngo_queue_selector($host, $port, $selector);
177 #$cui->schedule_event( \&ngo_next_selector );
181 sub ng_advance
183 my ($host, $port, $selector, $post) = @_;
185 statusbar("Getting $host:$port/$selector");
187 my $resp = gopher_grab($host, $port, $selector, $post);
189 if (!$resp) { return; }
191 my ($size, $mimetype, $data) = ng_parse_response($resp);
193 #Hack
194 statusbar("$selector [$mimetype] $size b");
196 if ($mimetype eq 'text/x-menu')
198 titlebar($selector);
199 gophmenu_dialog($size, $mimetype, $data);
201 elsif ($mimetype eq 'application/x-interactive')
203 handle_interactive($host, $port, $selector, $data);
205 elsif ($mimetype =~ /^text\//)
207 if (!$data) { $data = '<no data>'; }
208 my $title = $selector;
209 $title =~ s/\t.*//; # hide metadata
210 $cui->dialog(-message => $data, -title => $title, -bfg => "red");
212 else
214 $cui->dialog('File type ['.$mimetype.'] size '.$size.' should be downloaded'."\n".substr($data, 0, 1024));
216 #Hack
217 statusbar("$selector [$mimetype] $size b");
221 sub gophmenu_dialog {
222 my ($size, $type, $data) = @_;
224 my @menu = ng_parse_menu($data, $host, $port);
226 # Prepare ListBox
227 my (@values, %labels, $i);
228 $i = 0;
229 foreach (@menu) {
230 my %item = %$_;
231 $values[$i] = $i;
232 $labels{$i} = gophmenu_pretty($item{'type'}, $item{'mimetype'}, $item{'name'}, $item{'selector'});
233 $i++;
236 my $win = $cui->getobj("win1");
237 my $lb = $win->getobj("mylistbox");
238 if ($lb) {
239 $lb->values( \@values );
240 $lb->labels( \%labels );
241 $lb->userdata( \@menu );
242 #$lb->focus();
243 return;
245 my $listbox = $win->add(
246 'mylistbox', 'Listbox',
247 -values => \@values,
248 -labels => \%labels,
249 -userdata => \@menu,
250 -onchange => \&gophmenu_select,
251 -onselchange=> \&gophmenu_hover,
253 #$listbox->focus();
256 sub gophmenu_getitem
258 my $listbox = $_[0];
259 my @udata = @{$listbox->userdata()};
260 my %item = %{$udata[ $listbox->get_active_id() ]};
261 return %item;
264 sub gophmenu_hover
266 my %item = gophmenu_getitem @_;
268 statusbar($item{'host'}.':'.$item{'port'}.'/'.$item{'selector'});
271 sub gophmenu_select
273 my $listbox = $_[0];
274 my %item = gophmenu_getitem $listbox;
276 $listbox->clear_selection();
278 ng_advance($item{'host'}, $item{'port'}, $item{'selector'});
279 #ngo_queue_selector($item{'host'}, $item{'port'}, $item{'selector'});
280 #$cui->schedule_event( \&ngo_next_selector );
283 sub follow_cb() {
284 my $list = $cui->getobj("win1")->getobj("mylistbox");
285 gophmenu_select($list);
288 sub download_dialog() {
289 my $list = $cui->getobj("win1")->getobj("mylistbox");
290 my %item = gophmenu_getitem $list;
292 if ($item{'type'} ne 'b') {
293 statusbar("Can't download non-binary selector ".$item{'selector'});
294 return;
297 my $shortname = fileparse($item{'selector'});
298 my $file = $cui->savefilebrowser(-file => $shortname);
300 download_aux($item{'host'}, $item{'port'}, $item{'selector'}, $file);
302 statusbar("Saved file ".$file);
305 sub gophmenu_pretty
307 my ($type, $mimetype, $name, $selector) = @_;
308 #if ($type eq 'i') {
309 # return sprintf(" -- %s", $name);
311 my $hint = '';
312 #$hint = $mimetype if $mimetype;
313 $hint = 'TXT' if $mimetype =~ /text/;
314 $hint = 'IMG' if $mimetype =~ /image/;
315 $hint = 'DIR' if $type eq 'm';
316 $hint = '==>' if $type eq 's';
318 return sprintf("%s %4s %s", $type, $hint, $name);
321 sub gopher_grab {
322 my ($host, $port, $selector, $post) = @_;
323 statusbar("Connecting to $host : $port\n");
324 my $sock = $secure ? new IO::Socket::SSL(PeerAddr => $host, PeerPort => $port, Proto => 'tcp', )
325 : new IO::Socket::INET(PeerAddr => $host, PeerPort => $port, Proto => 'tcp', Timeout => 15, );
326 if (!$sock) { show_error("Can't connect to $host : $port"); return ""; }
327 #return "Could not create socket: $!\n" unless $sock;
328 statusbar("Requesting '$selector'\n");
329 print $sock $selector."\r\n";
330 if ($post) {
331 print $sock length($post) . "\t\r\n";
332 print $sock $post;
334 my $data = '';
335 my $header = <$sock>;
336 # TODO: Must add some kind of time-out here
337 if (!($header =~ /(-1|\d+)\t(.*)/)) {
338 show_error("Can't parse header: (".length($header).")\n".$header."\n");
339 return '';
341 my ($size, $mimetype) = ($1, $2);
342 my ($len, $n, $need) = (0, 0, 1024);
343 statusbar("Receiving selector '$selector' size $size, $mimetype\n");
344 binmode($sock);
345 #$sock->blocking(0);
346 while(1) {
347 if ($size != -1 && $size - $len < $need) { $need = $size - $len; }
348 statusbar("Waiting for $need more");
349 $n = read($sock, $data, $need);
350 statusbar("+$n Read $len of $size");
351 if (!defined $n) { next; }
352 $len += $n;
353 if ($len >= $size) { last; }
354 if (eof $sock) { last; }
355 # TODO : timeout ?
357 close($sock);
358 statusbar("\n");
359 # $cui->dialog("GOT:\n".$data);
360 return $header . $data;
363 sub handle_interactive() {
365 my ($host, $port, $selector, $data) = @_;
366 my ($long_target, $rest) = split /\n/, $data, 2;
367 my $target = substr $long_target, 1;
368 undef $long_target;
370 my ($fh, $filename) = tempfile();
371 print $fh $rest;
372 close($fh);
374 my $sb = stat($filename);
375 my $mtime = $sb->mtime;
376 undef $sb;
378 system('$EDITOR '.$filename);
379 $cui->layout();
381 my $sb = stat($filename);
382 if ($mtime != $sb->mtime) {
383 my $fh; open $fh, $filename;
384 my $buf = '';
385 binmode($fh);
386 while (<$fh>) { $buf .= $_; }
387 close($fh);
389 ng_advance($host, $port, $target, $buf);
390 #ngo_queue_selector($host, $port, $target, $buf);
391 #$cui->schedule_event( \&ngo_next_selector );
392 } else {
393 statusbar("Interactive input aborted");
397 sub show_error {
398 if ($cui) {
399 $cui->dialog(-message => $_[0], -title => "Error", -fg => "blue");
400 } else {
401 print STDERR $_[0];
405 sub show_banner() {
408 sub show_usage() {
409 print "Usage: \n";
410 print " ./ngo [OPTIONS] HOST[:PORT][/SELECTOR] \n";
411 print " Options: \n";
412 print " -o Download file in non-interactive mode.\n";
413 print " -S Use plaintext connection istead of TLS.\n";
414 print " -s Use secure TLS connection.\n";
415 #print " -p Post a Form file FILE.\n";
418 sub download_aux() {
419 my ($host, $port, $selector, $filename) = @_;
421 my $resp = gopher_grab($host, $port, $selector);
422 if (!$resp) { return; }
424 my ($size, $mimetype, $data) = ng_parse_response($resp);
426 open my $fh, '>'.$filename
427 || die "Unable to open $filename for writing";
428 binmode $fh;
429 select $fh;
430 print $data;
431 close $fh;
432 select stdout;