f7b2bc0ddf1102520da25559887bd38456cda20a
[spider.git] / perl / AsyncMsg.pm
1 #
2 # This class is the internal subclass that does various Async connects and
3 # retreivals of info. Typical uses (and specific support) include http get and
4 # post.
5
6 # This merely starts up a Msg handler (and no DXChannel) ($conn in other words)
7 # does the GET, parses out the result and the data and then (assuming a positive
8 # result and that the originating callsign is still online) punts out the data
9 # to the caller.
10 #
11 # It isn't designed to be very clever.
12 #
13 # Copyright (c) 2013 - Dirk Koopman G1TLH
14 #
15
16 package AsyncMsg;
17
18 use Msg;
19 use DXDebug;
20 use DXUtil;
21 use DXChannel;
22
23 use vars qw(@ISA $deftimeout);
24
25 @ISA = qw(Msg);
26 $deftimeout = 15;
27
28 my %outstanding;
29
30 #
31 # standard http get handler
32 #
33 sub handle_get
34 {
35         my $conn = shift;
36         my $msg = shift;
37
38         my $state = $conn->{state};
39         
40         dbg("asyncmsg: $msg") if isdbg('async');
41
42         # no point in going on if there is no-one wanting the output anymore
43         my $dxchan = DXChannel::get($conn->{caller});
44         unless ($dxchan) {
45                 $conn->disconnect;
46                 return;
47         }
48         
49         if ($state eq 'waitreply') {
50                 # look at the reply code and decide whether it is a success
51                 my ($http, $code, $ascii) = $msg =~ m|(HTTP/\d\.\d)\s+(\d+)\s+(.*)|;
52                 if ($code == 200) {
53                         # success
54                         $conn->{state} = 'waitblank';
55                 } elsif ($code == 302) {
56                         # redirect
57                         $conn->{state} = 'waitlocation';
58                 } else {
59                         $dxchan->send("$code $ascii");
60                         $conn->disconnect;
61                 } 
62         } elsif ($state  eq 'waitlocation') {
63                 my ($path) = $msg =~ m|Location:\s*(.*)|;
64                 if ($path) {
65                         my @uri = split m|/+|, $path;
66                         if ($uri[0] eq 'http:') {
67                                 shift @uri;
68                                 my $host = shift @uri;
69                                 my $newpath = '/' . join('/', @uri);
70                                 $newpath .= '/' if $path =~ m|/$|;
71                                 _getpost(ref $conn, $conn->{asyncsort}, $conn->{caller}, $host, 80, $newpath, @{$conn->{asyncargs}});
72                         } elsif ($path =~ m|^/|) {
73                                 _getpost(ref $conn, $conn->{asyncsort}, $conn->{caller}, $conn->{peerhost}, $conn->{peerport}, $path,
74                                                  @{$conn->{asyncargs}});
75                         }
76                         delete $conn->{on_disconnect};
77                         $conn->disconnect;
78                 }
79         } elsif ($state eq 'waitblank') {
80                 unless ($msg) {
81                         $conn->{state} = 'indata';
82                 }
83         } elsif ($conn->{state} eq 'indata') {
84                 if (my $filter = $conn->{filter}) {
85                         no strict 'refs';
86                         # this will crash if the command has been redefined and the filter is a
87                         # function defined there whilst the request is in flight,
88                         # but this isn't exactly likely in a production environment.
89                         $filter->($conn, $msg, $dxchan);
90                 } else {
91                         my $prefix = $conn->{prefix} || '';
92                         $dxchan->send("$prefix$msg");
93                 }
94         }
95 }
96
97
98 # simple raw handler
99 #
100 # Just outputs everything
101 #
102 sub handle_raw
103 {
104         my $conn = shift;
105         my $msg = shift;
106
107         # no point in going on if there is no-one wanting the output anymore
108         my $dxchan = DXChannel::get($conn->{caller});
109         unless ($dxchan) {
110                 $conn->disconnect;
111                 return;
112         }
113
114         # send out the data
115         my $prefix = $conn->{prefix} || '';
116         $dxchan->send("$prefix$msg");
117 }
118
119 sub new 
120 {
121         my $pkg = shift;
122         my $call = shift;
123         my $handler = shift;
124         
125         my $conn = $pkg->SUPER::new($handler);
126         $conn->{caller} = ref $call ? $call->call : $call;
127
128         # make it persistent
129         $outstanding{$conn} = $conn;
130         
131         return $conn;
132 }
133
134 # This does a http get on a path on a host and
135 # returns the result (through an optional filter)
136 #
137 # expects to be called something like from a cmd.pl file:
138 #
139 # AsyncMsg->get($self, <host>, <port>, <path>, [<key=>value>...]
140
141 # Standard key => value pairs are:
142 #
143 # filter => CODE ref (e.g. sub { ... })
144 # prefix => <string>                 prefix output with this string
145 #
146 # Anything else is taken and sent as (extra) http header stuff e.g:
147 #
148 # 'User-Agent' => qq{DXSpider;$main::version;$main::build;$^O}
149 # 'Content-Type' => q{text/xml; charset=utf-8}
150 # 'Content-Length' => $lth
151 #
152 # Host: is always set to the name of the host (unless overridden)
153 # User-Agent: is set to default above (unless overridden)
154 #
155 sub _getpost
156 {
157         my $pkg = shift;
158         my $sort = shift;
159         my $call = shift;
160         my $host = shift;
161         my $port = shift;
162         my $path = shift;
163         my %args = @_;
164         
165
166         my $conn = $pkg->new($call, \&handle_get);
167         $conn->{asyncargs} = [@_];
168         $conn->{state} = 'waitreply';
169         $conn->{filter} = delete $args{filter} if exists $args{filter};
170         $conn->{prefix} = delete $args{prefix} if exists $args{prefix};
171         $conn->{on_disconnect} = delete $args{on_disc} || delete $args{on_disconnect};
172         $conn->{path} = $path;
173         $conn->{asyncsort} = $sort;
174         
175         $r = $conn->connect($host, $port);
176         if ($r) {
177                 dbg("Sending '$sort $path HTTP/1.0'") if isdbg('async');
178                 $conn->send_later("$sort $path HTTP/1.0\n");
179
180                 my $h = delete $args{Host} || $host;
181                 my $u = delete $args{'User-Agent'} || "DxSpider;$main::version;$main::build;$^O;$main::mycall"; 
182                 my $d = delete $args{data};
183                 
184             $conn->send_later("Host: $h\n");
185                 $conn->send_later("User-Agent: $u\n");
186                 while (my ($k,$v) = each %args) {
187                         $conn->send_later("$k: $v\n");
188                 }
189                 $conn->send_later("\n$d") if defined $d;
190                 $conn->send_later("\n");
191         }
192         
193         return $r ? $conn : undef;
194 }
195
196 sub get
197 {
198         my $pkg = shift;
199         _getpost($pkg, "GET", @_);
200 }
201
202 sub post
203 {
204         my $pkg = shift;
205         _getpost($pkg, "POST", @_);
206 }
207
208 # do a raw connection
209 #
210 # Async->raw($self, <host>, <port>, [handler => CODE ref], [prefix => <string>]);
211 #
212 # With no handler defined, everything sent by the connection will be sent to
213 # the caller.
214 #
215 # One can send stuff out on the connection by doing a standard "$conn->send_later(...)" 
216 # inside the (custom) handler.
217
218 sub raw
219 {
220         my $pkg = shift;
221         my $call = shift;
222         my $host = shift;
223         my $port = shift;
224
225         my %args = @_;
226
227         my $handler = delete $args{handler} || \&handle_raw;
228         my $conn = $pkg->new($call, $handler);
229         $conn->{prefix} = delete $args{prefix} if exists $args{prefix};
230         $r = $conn->connect($host, $port);
231         return $r ? $conn : undef;
232 }
233
234 sub connect
235 {
236         my $conn = shift;
237         my $host = shift;
238         my $port = shift;
239         
240         # start a connection
241         my $r = $conn->SUPER::connect($host, $port);
242         if ($r) {
243                 dbg("AsyncMsg: Connected $conn->{cnum} to $host $port") if isdbg('async');
244         } else {
245                 dbg("AsyncMsg: ***Connect $conn->{cnum} Failed to $host $port $!") if isdbg('async');
246         }
247         
248         return $r;
249 }
250
251 sub disconnect
252 {
253         my $conn = shift;
254
255         if (my $ondisc = $conn->{on_disconnect}) {
256                 my $dxchan = DXChannel::get($conn->{caller});
257                 if ($dxchan) {
258                         no strict 'refs';
259                         $ondisc->($conn, $dxchan)
260                 }
261         }
262         delete $outstanding{$conn};
263         $conn->SUPER::disconnect;
264 }
265
266 sub DESTROY
267 {
268         my $conn = shift;
269         delete $outstanding{$conn};
270         $conn->SUPER::DESTROY;
271 }
272
273 1;
274