]> iEval git - app-fonbot-daemon.git/blame_incremental - lib/App/FonBot/Plugin/HTTPD.pm
Fix resources in Makefile.PL
[app-fonbot-daemon.git] / lib / App / FonBot / Plugin / HTTPD.pm
... / ...
CommitLineData
1package App::FonBot::Plugin::HTTPD;
2
3our $VERSION = '0.000_5';
4
5use v5.14;
6use strict;
7use warnings;
8
9use Apache2::Authen::Passphrase qw/pwcheck/;
10use HTTP::Status qw/HTTP_BAD_REQUEST HTTP_OK HTTP_NO_CONTENT HTTP_FORBIDDEN HTTP_UNAUTHORIZED/;
11use JSON qw/encode_json/;
12use Log::Log4perl;
13use POE::Component::Server::HTTP qw/RC_OK RC_DENY RC_WAIT/;
14
15use DB_File;
16use MIME::Base64 qw/decode_base64/;
17use Storable qw/freeze thaw/;
18use Text::ParseWords qw/shellwords/;
19
20use App::FonBot::Plugin::Config qw/$httpd_port/;
21use App::FonBot::Plugin::Common;
22
23##################################################
24
25my $log=Log::Log4perl->get_logger(__PACKAGE__);
26
27my $httpd;
28my %waiting_userrequests;
29my %responses;
30
31sub init{
32 $log->info('initializing '.__PACKAGE__);
33 %waiting_requests = ();
34 %waiting_userrequests = ();
35 $httpd = POE::Component::Server::HTTP->new(
36 Port => 8888,
37 PreHandler => { '/' => [\&pre_auth, \&pre_get, \&pre_userget], },
38 ContentHandler =>{ '/send' => \&on_send, '/get' => \&on_get, '/ok' => \&on_ok, '/userget' => \&on_userget, '/usersend' => \&on_usersend },
39 ErrorHandler => { '/' => sub { RC_OK }},
40 Headers => { 'Cache-Control' => 'no-cache' },
41 );
42}
43
44sub fini{
45 $log->info('finishing '.__PACKAGE__);
46 POE::Kernel->call($httpd, 'shutdown');
47}
48
49##################################################
50
51sub httpdie (\$$;$){
52 my ($response,$errstr,$errcode)=@_;
53
54 $$response->code($errcode // HTTP_BAD_REQUEST);
55 $$response->header(Content_Type => 'text/plain');
56 $$response->message($errstr);
57
58 die 'Bad Request';
59}
60
61sub pre_auth{
62 my ($request, $response)=@_;
63
64 eval {
65 my $authorization=$request->header('Authorization') // die 'No Authorization header';
66 $authorization =~ /^Basic (.+)$/ or die 'Invalid Authorization header';
67 my ($user, $password) = decode_base64($1) =~ /^(.+):(.*)$/ or die 'Invalid Authorization header';
68 eval { pwcheck $user, $password; 1 } or die 'Invalid user/password combination';
69 $request->header(Username => $user);
70 $log->debug("HTTP request from $user to url ".$request->url);
71 };
72 if (my $error = $@) {
73 $response->code(HTTP_UNAUTHORIZED);
74 $response->message('Bad username or password');
75 $response->header(Content_Type => 'text/plain');
76 $response->header(WWW_Authenticate => 'Basic realm="fonbotd"');
77 $response->content('Unauthorized');
78 $log->debug("Request denied: $error");
79 return RC_DENY
80 }
81
82 $response->content('');
83 RC_OK
84}
85
86sub pre_get{
87 my ($request, $response)=@_;
88 my $user=$request->header('Username');
89 return RC_OK if $response->code;
90 return RC_OK unless $user;
91 return RC_OK unless $request->uri =~ m,/get,;
92
93 unless (exists $commands{$user}) {
94 $log->debug("No pending commands for $user, entering RC_WAIT");
95 $waiting_requests{$user}->continue if exists $waiting_requests{$user};
96 $waiting_requests{$user}=$response;
97 return RC_WAIT
98 }
99
100 RC_OK
101}
102
103sub pre_userget{
104 my ($request, $response)=@_;
105 my $user=$request->header('Username');
106 return RC_OK if $response->code;
107 return RC_OK unless $user;
108 return RC_OK unless $request->uri =~ m,/userget,;
109
110 unless (exists $responses{$user}) {
111 $log->debug("No pending responses for $user, entering RC_WAIT");
112 $waiting_userrequests{$user}->continue if exists $waiting_userrequests{$user};
113 $waiting_userrequests{$user}=$response;
114 return RC_WAIT
115 }
116
117 RC_OK
118}
119
120sub on_ok{
121 my ($request, $response)=@_;
122 return RC_OK if $response->code;
123
124 $response->code(HTTP_OK);
125 RC_OK
126}
127
128sub on_get{
129 my ($request, $response)=@_;
130 return RC_OK if $response->code;
131
132 eval {
133 my $user=$request->header('Username');
134 $log->debug("on_get from user $user");
135
136 if (exists $commands{$user}) {
137 my $json=encode_json thaw $commands{$user};
138 $log->debug("Sending JSON: $json to $user");
139 $response->content($json);
140 $response->code(HTTP_OK);
141 $response->message('Commands sent');
142 } else {
143 $log->debug("Sending back 204 No Content");
144 $response->code(HTTP_NO_CONTENT);
145 $response->message('No pending commands');
146 }
147
148 delete $commands{$user}
149 };
150
151 $log->error("ERROR: $@") if $@ && $@ !~ /^Bad Request /;
152
153 RC_OK
154}
155
156sub on_userget{
157 my ($request, $response)=@_;
158 return RC_OK if $response->code;
159
160 eval {
161 my $user=$request->header('Username');
162 $log->debug("on_userget from user $user");
163
164 if (exists $responses{$user}) {
165 my $json=encode_json $responses{$user};
166 $log->debug("Sending JSON: $json to $user");
167 $response->content($json);
168 $response->code(HTTP_OK);
169 $response->message('Responses sent');
170 } else {
171 $log->debug("Sending back 204 No Content");
172 $response->code(HTTP_NO_CONTENT);
173 $response->message('No pending responses');
174 }
175
176 delete $responses{$user}
177 };
178
179 $log->error("ERROR: $@") if $@ && $@ !~ /^Bad Request /;
180
181 RC_OK
182}
183
184sub on_send{
185 my ($request, $response)=@_;
186 return RC_OK if $response->code;
187
188 eval {
189 httpdie $response, 'All requests must use the POST http method' unless $request->method eq 'POST';
190 my $user=$request->header('Username');
191
192 my $destination=$request->header('X-Destination') // httpdie $response, 'Missing destination address';
193 my ($driver, $address)=shellwords $destination;
194
195 my $content=$request->content // httpdie $response, 'Content is undef';
196
197 if ($driver eq 'HTTP') {
198 $responses{$user}//=[];
199 push @{$responses{$user}}, $content;
200 if (exists $waiting_userrequests{$user}) {
201 $waiting_userrequests{$user}->continue;
202 delete $waiting_userrequests{$user}
203 }
204 } else {
205 unless ($ok_user_addresses{"$user $driver $address"}) {
206 $response->code(HTTP_FORBIDDEN);
207 $response->message("$user is not allowed to send messages to $address");
208 return
209 }
210
211 POE::Kernel->post($driver, 'send_message', $address, $content) or $log->error("Driver not found: $driver");
212 }
213
214 $response->code(HTTP_NO_CONTENT);
215 $response->message('Message sent');
216 };
217
218 $log->error("ERROR: $@") if $@ && $@ !~ /^Bad Request /;
219 $log->debug('Responding to send from $user with '.$response->code.' '.$response->message);
220 RC_OK
221}
222
223sub on_usersend{
224 my ($request, $response)=@_;
225 $log->debug("asdasd asd");
226 return RC_OK if $response->code;
227
228 eval{
229 httpdie $response, 'All requests must use the POST http method' unless $request->method eq 'POST';
230 my $user=$request->header('Username');
231
232 my $content=$request->content // httpdie $response, 'Content is undef';
233
234 sendmsg $user, $request->header('X-Requestid'), HTTP => shellwords $_ for split '\n', $content;
235
236 $response->code(HTTP_NO_CONTENT);
237 $response->message('Command sent');
238 };
239
240 $log->error("ERROR: $@") if $@ && $@ !~ /^Bad Request /;
241 $log->debug('Responding to usersend from $user with '.$response->code.' '.$response->message);
242 RC_OK
243}
244
2451;
246__END__
247
248=encoding utf-8
249
250=head1 NAME
251
252App::FonBot::Plugin::HTTPD - FonBot webserver plugin, used for communication with phones
253
254=head1 SYNOPSIS
255
256 use App::FonBot::Plugin::HTTPD;
257 App::FonBot::Plugin::HTTPD->init;
258 ...
259 App::FonBot::Plugin::HTTPD->fini;
260
261=head1 DESCRIPTION
262
263This FonBot plugin provides a webserver for interacting with fonbotd. All requests use Basic access authentication.
264
265The available calls are:
266
267=over
268
269=item GET C</get>
270
271Returns a JSON array of pending commands for the current user. Uses long polling — the server does not respond immediately if there are no pending commands.
272
273=item GET C</ok>
274
275Returns a 200 OK.
276
277=item POST C</send>
278
279Sends a message to an address. The address is given in the C<X-Destination> header. The message is in the POST data.
280
281=item GET C</userget>
282
283Returns a JSON array of pending messages for the current user. Uses long polling — the server does not respond immediately if there are no pending commands.
284
285=item POST C</usersend>
286
287Sends a command to the sender's phone. The optional C<X-Requestid> header sets the request ID. The command is in the POST data
288
289=back
290
291=head1 CONFIGURATION VARIABLES
292
293=over
294
295=item C<$httpd_port>
296
297The HTTPD listens on this port.
298
299=back
300
301=head1 AUTHOR
302
303Marius Gavrilescu C<< marius@ieval.ro >>
304
305=head1 COPYRIGHT AND LICENSE
306
307Copyright 2013 Marius Gavrilescu
308
309This file is part of fonbotd.
310
311fonbotd is free software: you can redistribute it and/or modify
312it under the terms of the GNU Affero General Public License as published by
313the Free Software Foundation, either version 3 of the License, or
314(at your option) any later version.
315
316fonbotd is distributed in the hope that it will be useful,
317but WITHOUT ANY WARRANTY; without even the implied warranty of
318MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
319GNU Affero General Public License for more details.
320
321You should have received a copy of the GNU Affero General Public License
322along with fonbotd. If not, see <http://www.gnu.org/licenses/>
323
324
325=cut
This page took 0.023529 seconds and 4 git commands to generate.