ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-FCP/FCP.pm
Revision: 1.12
Committed: Wed Sep 10 04:50:44 2003 UTC (20 years, 8 months ago) by root
Branch: MAIN
Changes since 1.11: +175 -63 lines
Log Message:
*** empty log message ***

File Contents

# Content
1 =head1 NAME
2
3 Net::FCP - http://freenet.sf.net client protocol
4
5 =head1 SYNOPSIS
6
7 use Net::FCP;
8
9 my $fcp = new Net::FCP;
10
11 my $ni = $fcp->txn_node_info->result;
12 my $ni = $fcp->node_info;
13
14 =head1 DESCRIPTION
15
16 See L<http://freenet.sourceforge.net/index.php?page=fcp> for a description
17 of what the messages do. I am too lazy to document all this here.
18
19 =head1 WARNING
20
21 This module is alpha. While it probably won't destroy (much :) of your
22 data, it currently falls short of what it should provide (intelligent uri
23 following, splitfile downloads, healing...)
24
25 =head2 IMPORT TAGS
26
27 Nothing much can be "imported" from this module right now. There are,
28 however, certain "import tags" that can be used to select the event model
29 to be used.
30
31 Event models are implemented as modules under the C<Net::FCP::Event::xyz>
32 class, where C<xyz> is the event model to use. The default is C<Event> (or
33 later C<Auto>).
34
35 The import tag to use is named C<event=xyz>, e.g. C<event=Event>,
36 C<event=Glib> etc.
37
38 You should specify the event module to use only in the main program.
39
40 =head2 THE Net::FCP CLASS
41
42 =over 4
43
44 =cut
45
46 package Net::FCP;
47
48 use Carp;
49
50 $VERSION = 0.05;
51
52 no warnings;
53
54 our $EVENT = Net::FCP::Event::Auto::;
55 $EVENT = Net::FCP::Event::Event;#d#
56
57 sub import {
58 shift;
59
60 for (@_) {
61 if (/^event=(\w+)$/) {
62 $EVENT = "Net::FCP::Event::$1";
63 }
64 }
65 eval "require $EVENT";
66 die $@ if $@;
67 }
68
69 sub touc($) {
70 local $_ = shift;
71 1 while s/((?:^|_)(?:svk|chk|uri)(?:_|$))/\U$1/;
72 s/(?:^|_)(.)/\U$1/g;
73 $_;
74 }
75
76 sub tolc($) {
77 local $_ = shift;
78 s/(?<=[a-z])(?=[A-Z])/_/g;
79 lc $_;
80 }
81
82 =item $meta = Net::FCP::parse_metadata $string
83
84 Parse a metadata string and return it.
85
86 The metadata will be a hashref with key C<version> (containing
87 the mandatory version header entries).
88
89 All other headers are represented by arrayrefs (they can be repeated).
90
91 Since this is confusing, here is a rather verbose example of a parsed
92 manifest:
93
94 (
95 version => { revision => 1 },
96 document => [
97 {
98 "info.format" => "image/jpeg",
99 name => "background.jpg",
100 "redirect.target" => "freenet:CHK\@ZcagI,ra726bSw"
101 },
102 {
103 "info.format" => "text/html",
104 name => ".next",
105 "redirect.target" => "freenet:SSK\@ilUPAgM/TFEE/3"
106 },
107 {
108 "info.format" => "text/html",
109 "redirect.target" => "freenet:CHK\@8M8Po8ucwI,8xA"
110 }
111 ]
112 )
113
114 =cut
115
116 sub parse_metadata {
117 my $meta;
118
119 my $data = shift;
120 if ($data =~ /^Version\015?\012/gc) {
121 my $hdr = $meta->{version} = {};
122
123 for (;;) {
124 while ($data =~ /\G([^=\015\012]+)=([^\015\012]*)\015?\012/gc) {
125 my ($k, $v) = ($1, $2);
126 my @p = split /\./, tolc $k, 3;
127
128 $hdr->{$p[0]} = $v if @p == 1; # lamest code I ever wrote
129 $hdr->{$p[0]}{$p[1]} = $v if @p == 2;
130 $hdr->{$p[0]}{$p[1]}{$p[3]} = $v if @p == 3;
131 die "FATAL: 4+ dot metadata" if @p >= 4;
132 }
133
134 if ($data =~ /\GEndPart\015?\012/gc) {
135 # nop
136 } elsif ($data =~ /\GEnd\015?\012/gc) {
137 last;
138 } elsif ($data =~ /\G([A-Za-z0-9.\-]+)\015?\012/gcs) {
139 push @{$meta->{tolc $1}}, $hdr = {};
140 } elsif ($data =~ /\G(.*)/gcs) {
141 die "metadata format error ($1)";
142 }
143 }
144 }
145
146 #$meta->{tail} = substr $data, pos $data;
147
148 $meta;
149 }
150
151 =item $fcp = new Net::FCP [host => $host][, port => $port]
152
153 Create a new virtual FCP connection to the given host and port (default
154 127.0.0.1:8481, or the environment variables C<FREDHOST> and C<FREDPORT>).
155
156 Connections are virtual because no persistent physical connection is
157 established. However, the existance of the node is checked by executing a
158 C<ClientHello> transaction.
159
160 =cut
161
162 sub new {
163 my $class = shift;
164 my $self = bless { @_ }, $class;
165
166 $self->{host} ||= $ENV{FREDHOST} || "127.0.0.1";
167 $self->{port} ||= $ENV{FREDPORT} || 8481;
168
169 #$self->{nodehello} = $self->client_hello
170 # or croak "unable to get nodehello from node\n";
171
172 $self;
173 }
174
175 sub progress {
176 my ($self, $txn, $type, $attr) = @_;
177 warn "progress<$txn,$type," . (join ":", %$attr) . ">\n";
178 }
179
180 =item $txn = $fcp->txn(type => attr => val,...)
181
182 The low-level interface to transactions. Don't use it.
183
184 Here are some examples of using transactions:
185
186 The blocking case, no (visible) transactions involved:
187
188 my $nodehello = $fcp->client_hello;
189
190 A transaction used in a blocking fashion:
191
192 my $txn = $fcp->txn_client_hello;
193 ...
194 my $nodehello = $txn->result;
195
196 Or shorter:
197
198 my $nodehello = $fcp->txn_client_hello->result;
199
200 Setting callbacks:
201
202 $fcp->txn_client_hello->cb(
203 sub { my $nodehello => $_[0]->result }
204 );
205
206 =cut
207
208 sub txn {
209 my ($self, $type, %attr) = @_;
210
211 $type = touc $type;
212
213 my $txn = "Net::FCP::Txn::$type"->new(fcp => $self, type => tolc $type, attr => \%attr);
214
215 $txn;
216 }
217
218 sub _txn($&) {
219 my ($name, $sub) = @_;
220 *{"$name\_txn"} = $sub;
221 *{$name} = sub { $sub->(@_)->result };
222 }
223
224 =item $txn = $fcp->txn_client_hello
225
226 =item $nodehello = $fcp->client_hello
227
228 Executes a ClientHello request and returns it's results.
229
230 {
231 max_file_size => "5f5e100",
232 node => "Fred,0.6,1.46,7050"
233 protocol => "1.2",
234 }
235
236 =cut
237
238 _txn client_hello => sub {
239 my ($self) = @_;
240
241 $self->txn ("client_hello");
242 };
243
244 =item $txn = $fcp->txn_client_info
245
246 =item $nodeinfo = $fcp->client_info
247
248 Executes a ClientInfo request and returns it's results.
249
250 {
251 active_jobs => "1f",
252 allocated_memory => "bde0000",
253 architecture => "i386",
254 available_threads => 17,
255 datastore_free => "5ce03400",
256 datastore_max => "2540be400",
257 datastore_used => "1f72bb000",
258 estimated_load => 52,
259 free_memory => "5cc0148",
260 is_transient => "false",
261 java_name => "Java HotSpot(_T_M) Server VM",
262 java_vendor => "http://www.blackdown.org/",
263 java_version => "Blackdown-1.4.1-01",
264 least_recent_timestamp => "f41538b878",
265 max_file_size => "5f5e100",
266 most_recent_timestamp => "f77e2cc520"
267 node_address => "1.2.3.4",
268 node_port => 369,
269 operating_system => "Linux",
270 operating_system_version => "2.4.20",
271 routing_time => "a5",
272 }
273
274 =cut
275
276 _txn client_info => sub {
277 my ($self) = @_;
278
279 $self->txn ("client_info");
280 };
281
282 =item $txn = $fcp->txn_generate_chk ($metadata, $data)
283
284 =item $uri = $fcp->generate_chk ($metadata, $data)
285
286 Creates a new CHK, given the metadata and data. UNTESTED.
287
288 =cut
289
290 _txn generate_chk => sub {
291 my ($self, $metadata, $data) = @_;
292
293 $self->txn (generate_chk => data => "$data$metadata", metadata_length => length $metadata);
294 };
295
296 =item $txn = $fcp->txn_generate_svk_pair
297
298 =item ($public, $private) = @{ $fcp->generate_svk_pair }
299
300 Creates a new SVK pair. Returns an arrayref.
301
302 [
303 "hKs0-WDQA4pVZyMPKNFsK1zapWY",
304 "ZnmvMITaTXBMFGl4~jrjuyWxOWg"
305 ]
306
307 =cut
308
309 _txn generate_svk_pair => sub {
310 my ($self) = @_;
311
312 $self->txn ("generate_svk_pair");
313 };
314
315 =item $txn = $fcp->txn_insert_private_key ($private)
316
317 =item $uri = $fcp->insert_private_key ($private)
318
319 Inserts a private key. $private can be either an insert URI (must start
320 with freenet:SSK@) or a raw private key (i.e. the private value you get back
321 from C<generate_svk_pair>).
322
323 Returns the public key.
324
325 UNTESTED.
326
327 =cut
328
329 _txn insert_private_key => sub {
330 my ($self, $privkey) = @_;
331
332 $self->txn (invert_private_key => private => $privkey);
333 };
334
335 =item $txn = $fcp->txn_get_size ($uri)
336
337 =item $length = $fcp->get_size ($uri)
338
339 Finds and returns the size (rounded up to the nearest power of two) of the
340 given document.
341
342 UNTESTED.
343
344 =cut
345
346 _txn get_size => sub {
347 my ($self, $uri) = @_;
348
349 $self->txn (get_size => URI => $uri);
350 };
351
352 =item $txn = $fcp->txn_client_get ($uri [, $htl = 15 [, $removelocal = 0]])
353
354 =item ($metadata, $data) = @{ $fcp->client_get ($uri, $htl, $removelocal)
355
356 Fetches a (small, as it should fit into memory) file from
357 freenet. C<$meta> is the metadata (as returned by C<parse_metadata> or
358 C<undef>).
359
360 Due to the overhead, a better method to download big files should be used.
361
362 my ($meta, $data) = @{
363 $fcp->client_get (
364 "freenet:CHK@hdXaxkwZ9rA8-SidT0AN-bniQlgPAwI,XdCDmBuGsd-ulqbLnZ8v~w"
365 )
366 };
367
368 =cut
369
370 _txn client_get => sub {
371 my ($self, $uri, $htl, $removelocal) = @_;
372
373 $self->txn (client_get => URI => $uri, hops_to_live => ($htl || 15), remove_local_key => $removelocal ? "true" : "false");
374 };
375
376 =item MISSING: ClientPut
377
378 =back
379
380 =head2 THE Net::FCP::Txn CLASS
381
382 All requests (or transactions) are executed in a asynchroneous way (LIE:
383 uploads are blocking). For each request, a C<Net::FCP::Txn> object is
384 created (worse: a tcp connection is created, too).
385
386 For each request there is actually a different subclass (and it's possible
387 to subclass these, although of course not documented).
388
389 The most interesting method is C<result>.
390
391 =over 4
392
393 =cut
394
395 package Net::FCP::Txn;
396
397 use Fcntl;
398 use Socket;
399
400 =item new arg => val,...
401
402 Creates a new C<Net::FCP::Txn> object. Not normally used.
403
404 =cut
405
406 sub new {
407 my $class = shift;
408 my $self = bless { @_ }, $class;
409
410 $self->{signal} = $EVENT->new_signal;
411
412 $self->{fcp}{txn}{$self} = $self;
413
414 my $attr = "";
415 my $data = delete $self->{attr}{data};
416
417 while (my ($k, $v) = each %{$self->{attr}}) {
418 $attr .= (Net::FCP::touc $k) . "=$v\012"
419 }
420
421 if (defined $data) {
422 $attr .= "DataLength=" . (length $data) . "\012";
423 $data = "Data\012$data";
424 } else {
425 $data = "EndMessage\012";
426 }
427
428 socket my $fh, PF_INET, SOCK_STREAM, 0
429 or Carp::croak "unable to create new tcp socket: $!";
430 binmode $fh, ":raw";
431 fcntl $fh, F_SETFL, O_NONBLOCK;
432 connect $fh, (sockaddr_in $self->{fcp}{port}, inet_aton $self->{fcp}{host})
433 and !$!{EWOULDBLOCK}
434 and !$!{EINPROGRESS}
435 and Carp::croak "FCP::txn: unable to connect to $self->{fcp}{host}:$self->{fcp}{port}: $!\n";
436
437 $self->{sbuf} =
438 "\x00\x00\x00\x02"
439 . Net::FCP::touc $self->{type}
440 . "\012$attr$data";
441
442 #$fh->shutdown (1); # freenet buggy?, well, it's java...
443
444 $self->{fh} = $fh;
445
446 $self->{w} = $EVENT->new_from_fh ($fh)->cb(sub { $self->fh_ready_w })->poll(0, 1, 1);
447
448 $self;
449 }
450
451 =item $txn = $txn->cb ($coderef)
452
453 Sets a callback to be called when the request is finished. The coderef
454 will be called with the txn as it's sole argument, so it has to call
455 C<result> itself.
456
457 Returns the txn object, useful for chaining.
458
459 Example:
460
461 $fcp->txn_client_get ("freenet:CHK....")
462 ->userdata ("ehrm")
463 ->cb(sub {
464 my $data = shift->result;
465 });
466
467 =cut
468
469 sub cb($$) {
470 my ($self, $cb) = @_;
471 $self->{cb} = $cb;
472 $self;
473 }
474
475 =item $txn = $txn->userdata ([$userdata])
476
477 Set user-specific data. This is useful in progress callbacks. The data can be accessed
478 using C<< $txn->{userdata} >>.
479
480 Returns the txn object, useful for chaining.
481
482 =cut
483
484 sub userdata($$) {
485 my ($self, $data) = @_;
486 $self->{userdata} = $data;
487 $self;
488 }
489
490 sub fh_ready_w {
491 my ($self) = @_;
492
493 my $len = syswrite $self->{fh}, $self->{sbuf};
494
495 if ($len > 0) {
496 substr $self->{sbuf}, 0, $len, "";
497 unless (length $self->{sbuf}) {
498 fcntl $self->{fh}, F_SETFL, 0;
499 $self->{w}->cb(sub { $self->fh_ready_r })->poll (1, 0, 1);
500 }
501 } elsif (defined $len) {
502 $self->throw (Net::FCP::Exception->new (network_error => { reason => "unexpected end of file while writing" }));
503 } else {
504 $self->throw (Net::FCP::Exception->new (network_error => { reason => "$!" }));
505 }
506 }
507
508 sub fh_ready_r {
509 my ($self) = @_;
510
511 if (sysread $self->{fh}, $self->{buf}, 65536, length $self->{buf}) {
512 for (;;) {
513 if ($self->{datalen}) {
514 warn "expecting new datachunk $self->{datalen}, got ".(length $self->{buf})."\n";#d#
515 if (length $self->{buf} >= $self->{datalen}) {
516 $self->rcv_data (substr $self->{buf}, 0, delete $self->{datalen}, "");
517 } else {
518 last;
519 }
520 } elsif ($self->{buf} =~ s/^DataChunk\015?\012Length=([0-9a-fA-F]+)\015?\012Data\015?\012//) {
521 $self->{datalen} = hex $1;
522 warn "expecting new datachunk $self->{datalen}\n";#d#
523 } elsif ($self->{buf} =~ s/^([a-zA-Z]+)\015?\012(?:(.+?)\015?\012)?EndMessage\015?\012//s) {
524 $self->rcv ($1, {
525 map { my ($a, $b) = split /=/, $_, 2; ((Net::FCP::tolc $a), $b) }
526 split /\015?\012/, $2
527 });
528 } else {
529 last;
530 }
531 }
532 } else {
533 $self->eof;
534 }
535 }
536
537 sub rcv_data {
538 my ($self, $chunk) = @_;
539
540 $self->{data} .= $chunk;
541
542 $self->progress ("data", { chunk => length $chunk, total => length $self->{data}, end => $self->{datalength} });
543 }
544
545 sub rcv {
546 my ($self, $type, $attr) = @_;
547
548 $type = Net::FCP::tolc $type;
549
550 #use PApp::Util; warn PApp::Util::dumpval [$type, $attr];
551
552 if (my $method = $self->can("rcv_$type")) {
553 $method->($self, $attr, $type);
554 } else {
555 warn "received unexpected reply type '$type' for '$self->{type}', ignoring\n";
556 }
557 }
558
559 # used as a default exception thrower
560 sub rcv_throw_exception {
561 my ($self, $attr, $type) = @_;
562 $self->throw (new Net::FCP::Exception $type, $attr);
563 }
564
565 *rcv_failed = \&Net::FCP::Txn::rcv_throw_exception;
566 *rcv_format_error = \&Net::FCP::Txn::rcv_throw_exception;
567
568 sub throw {
569 my ($self, $exc) = @_;
570
571 $self->{exception} = $exc;
572 $self->set_result (1);
573 $self->eof; # must be last to avoid loops
574 }
575
576 sub set_result {
577 my ($self, $result) = @_;
578
579 unless (exists $self->{result}) {
580 $self->{result} = $result;
581 $self->{cb}->($self) if exists $self->{cb};
582 $self->{signal}->send;
583 }
584 }
585
586 sub eof {
587 my ($self) = @_;
588
589 delete $self->{w};
590 delete $self->{fh};
591
592 delete $self->{fcp}{txn}{$self};
593
594 $self->set_result; # just in case
595 }
596
597 sub progress {
598 my ($self, $type, $attr) = @_;
599 $self->{fcp}->progress ($self, $type, $attr);
600 }
601
602 =item $result = $txn->result
603
604 Waits until a result is available and then returns it.
605
606 This waiting is (depending on your event model) not very efficient, as it
607 is done outside the "mainloop".
608
609 =cut
610
611 sub result {
612 my ($self) = @_;
613
614 $self->{signal}->wait while !exists $self->{result};
615
616 die $self->{exception} if $self->{exception};
617
618 return $self->{result};
619 }
620
621 package Net::FCP::Txn::ClientHello;
622
623 use base Net::FCP::Txn;
624
625 sub rcv_node_hello {
626 my ($self, $attr) = @_;
627
628 $self->set_result ($attr);
629 }
630
631 package Net::FCP::Txn::ClientInfo;
632
633 use base Net::FCP::Txn;
634
635 sub rcv_node_info {
636 my ($self, $attr) = @_;
637
638 $self->set_result ($attr);
639 }
640
641 package Net::FCP::Txn::GenerateCHK;
642
643 use base Net::FCP::Txn;
644
645 sub rcv_success {
646 my ($self, $attr) = @_;
647
648 $self->set_result ($attr);
649 }
650
651 package Net::FCP::Txn::GenerateSVKPair;
652
653 use base Net::FCP::Txn;
654
655 sub rcv_success {
656 my ($self, $attr) = @_;
657
658 $self->set_result ([$attr->{PublicKey}, $attr->{PrivateKey}]);
659 }
660
661 package Net::FCP::Txn::InvertPrivateKey;
662
663 use base Net::FCP::Txn;
664
665 sub rcv_success {
666 my ($self, $attr) = @_;
667
668 $self->set_result ($attr->{PublicKey});
669 }
670
671 package Net::FCP::Txn::GetSize;
672
673 use base Net::FCP::Txn;
674
675 sub rcv_success {
676 my ($self, $attr) = @_;
677
678 $self->set_result ($attr->{Length});
679 }
680
681 package Net::FCP::Txn::GetPut;
682
683 # base class for get and put
684
685 use base Net::FCP::Txn;
686
687 *rcv_uri_error = \&Net::FCP::Txn::rcv_throw_exception;
688 *rcv_route_not_found = \&Net::FCP::Txn::rcv_throw_exception;
689
690 sub rcv_restarted {
691 my ($self, $attr, $type) = @_;
692
693 delete $self->{datalength};
694 delete $self->{metalength};
695 delete $self->{data};
696
697 $self->progress ($type, $attr);
698 }
699
700 package Net::FCP::Txn::ClientGet;
701
702 use base Net::FCP::Txn::GetPut;
703
704 *rcv_data_not_found = \&Net::FCP::Txn::rcv_throw_exception;
705
706 sub rcv_data_found {
707 my ($self, $attr, $type) = @_;
708
709 $self->progress ($type, $attr);
710
711 $self->{datalength} = hex $attr->{data_length};
712 $self->{metalength} = hex $attr->{metadata_length};
713 }
714
715 sub eof {
716 my ($self) = @_;
717
718 if ($self->{datalength} == length $self->{data}) {
719 my $data = delete $self->{data};
720 my $meta = Net::FCP::parse_metadata substr $data, 0, $self->{metalength}, "";
721
722 $self->set_result ([$meta, $data]);
723 } elsif (!exists $self->{result}) {
724 $self->throw (Net::FCP::Exception->new (short_data => {
725 reason => "unexpected eof or internal node error",
726 received => length $self->{data},
727 expected => $self->{datalength},
728 }));
729 }
730 }
731
732 package Net::FCP::Txn::ClientPut;
733
734 use base Net::FCP::Txn::GetPut;
735
736 *rcv_size_error = \&Net::FCP::Txn::rcv_throw_exception;
737 *rcv_key_collision = \&Net::FCP::Txn::rcv_throw_exception;
738
739 sub rcv_pending {
740 my ($self, $attr, $type) = @_;
741 $self->progress ($type, $attr);
742 }
743
744 sub rcv_success {
745 my ($self, $attr, $type) = @_;
746 $self->set_result ($attr);
747 }
748
749 package Net::FCP::Exception;
750
751 use overload
752 '""' => sub {
753 "Net::FCP::Exception<<$_[0][0]," . (join ":", %{$_[0][1]}) . ">>\n";
754 };
755
756 sub new {
757 my ($class, $type, $attr) = @_;
758
759 bless [Net::FCP::tolc $type, { %$attr }], $class;
760 }
761
762 =back
763
764 =head1 SEE ALSO
765
766 L<http://freenet.sf.net>.
767
768 =head1 BUGS
769
770 =head1 AUTHOR
771
772 Marc Lehmann <pcg@goof.com>
773 http://www.goof.com/pcg/marc/
774
775 =cut
776
777 1;
778