File Coverage

lib/OAuthomatic/Internal/MicroWeb.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package OAuthomatic::Internal::MicroWeb;
2             # ABSTRACT: temporary embedded web server used internally
3              
4              
5 1     1   1239 use namespace::sweep;
  1         20399  
  1         8  
6 1     1   236 use Moose;
  0            
  0            
7             use MooseX::AttributeShortcuts;
8             use MooseX::Types::Path::Tiny qw/AbsDir AbsPath/;
9             use Path::Tiny qw/path/;
10             use threads;
11             use Thread::Queue;
12             use HTTP::Server::Brick;
13             use HTTP::Status;
14             use IO::Null;
15             use URI;
16             use URI::QueryParam;
17             use Template;
18             use Carp;
19             use Const::Fast;
20             use OAuthomatic::Internal::Util;
21             use OAuthomatic::Error;
22              
23             const my $THREAD_START_TIMEOUT => 10;
24             const my $FORM_FILL_WARNING_TIMEOUT => 1 * 60;
25              
26              
27             has 'config' => (
28             is => 'ro', isa => 'OAuthomatic::Config', required => 1,
29             handles=>[ 'app_name', 'html_dir', 'debug' ]);
30              
31              
32             has 'server' => (
33             is => 'ro', isa => 'OAuthomatic::Server', required => 1,
34             handles => [
35             'site_name',
36             'site_client_creation_page',
37             'site_client_creation_desc',
38             'site_client_creation_help',
39             ]);
40              
41              
42             has 'port' => (
43             is=>'lazy', isa=>'Int', required=>1, default => sub {
44             require Net::EmptyPort;
45             return Net::EmptyPort::empty_port();
46             });
47              
48              
49             has 'template_dir' => (
50             is=>'lazy', isa=>AbsDir, required=>1, coerce=>1, default=>sub {
51             return $_[0]->html_dir->child("templates");
52             });
53              
54              
55             has 'static_dir' => (
56             is=>'lazy', isa=>AbsDir, required=>1, coerce=>1, default=>sub {
57             return $_[0]->html_dir->child("static");
58             });
59              
60              
61             has 'verbose' => (is=>'ro', isa=>'Bool');
62              
63              
64             has 'callback_path' => (is=>'ro', isa=>'Str', default=>sub {"/oauth_granted"});
65              
66              
67             has 'client_key_path' => (is=>'ro', isa=>'Str', default=>sub {"/client_key"});
68              
69             has 'root_url' => (is=>'lazy', default=>sub{ "http://localhost:". $_[0]->port });
70             has 'callback_url' => (is=>'lazy', default=>sub{ $_[0]->root_url . $_[0]->callback_path });
71             has 'client_key_url' => (is=>'lazy', default=>sub{ $_[0]->root_url . $_[0]->client_key_path });
72              
73             has '_oauth_queue' => (is=>'ro', builder=>sub{Thread::Queue->new()});
74             has '_client_key_queue' => (is=>'ro', builder=>sub{Thread::Queue->new()});
75              
76             has '_brick' => (is=>'lazy');
77              
78             has '_template' => (is=>'lazy');
79              
80             has 'is_running' => (is => 'rwp', isa => 'Bool');
81              
82              
83             sub start {
84             my $self = shift;
85              
86             OAuthomatic::Error::Generic->throw(
87             ident => "Server is already running")
88             if $self->is_running;
89              
90             my $brick = $self->_brick;
91             my $static_dir = $self->static_dir;
92             my $callback_path = $self->callback_path
93             or OAuthomatic::Error::Generic->throw(
94             ident => "No callback_path");
95             my $client_key_path = $self->client_key_path
96             or OAuthomatic::Error::Generic->throw(
97             ident => "No client_key_path");
98             my $tt = $self->_template;
99             my $oauth_queue = $self->_oauth_queue;
100             my $client_key_queue = $self->_client_key_queue;
101              
102             print "[OAuthomatic] Spawning embedded web server thread\n";
103              
104             $self->{thread} = threads->create(
105             sub {
106             my ($brick, $static_dir, $callback_path, $client_key_path, $tt,
107             $oauth_queue, $client_key_queue) = @_;
108              
109             $brick->mount($callback_path => {
110             handler => sub {
111             _handle_oauth_request($oauth_queue, $tt, @_);
112             return RC_OK;
113             },
114             wildcard => 1, # let's treat longer urls as erroneous replies
115             });
116             $brick->mount($client_key_path => {
117             handler => sub {
118             _handle_client_key_request($client_key_queue, $tt, @_);
119             return RC_OK;
120             },
121             });
122             $brick->mount("/favicon.ico" => {
123             handler => sub { return RC_NOT_FOUND; },
124             });
125             $brick->mount("/static" => {
126             path => $static_dir,
127             });
128             $brick->mount( '/' => {
129             handler => sub {
130             _handle_generic_request($tt, @_);
131             return RC_NOT_FOUND;
132             },
133             wildcard => 1,
134             });
135              
136             print "[OAuthomatic] Embedded web server listens to requests\n";
137              
138             # Signalling we started. This queue is as good as any
139             $oauth_queue->enqueue({"started" => 1});
140              
141             $brick->start();
142             },
143             $brick, $static_dir, $callback_path, $client_key_path, $tt,
144             $oauth_queue, $client_key_queue);
145              
146             # Reading start signal
147             $oauth_queue->dequeue_timed($THREAD_START_TIMEOUT)
148             or OAuthomatic::Error::Generic->throw(
149             ident => "Failed to start embedded web",
150             extra => "Failed to receive completion info in $THREAD_START_TIMEOUT seconds. Is system heavily overloaded?");
151              
152             $self->_set_is_running(1);
153             }
154              
155              
156             sub stop {
157             my $self = shift;
158              
159             print "[OAuthomatic] Shutting down embedded web server\n";
160              
161             $self->{thread}->kill('HUP');
162             $self->{thread}->join;
163              
164             $self->_set_is_running(0);
165             }
166              
167             has '_usage_counter' => (is=>'rw', isa=>'Int', default=>0);
168              
169              
170             sub start_using {
171             my $self = shift;
172             $self->start unless $self->is_running;
173             $self->_usage_counter($self->_usage_counter + 1);
174             }
175              
176              
177             sub finish_using {
178             my $self = shift;
179             my $counter = $self->_usage_counter - 1;
180             $self->_usage_counter($counter);
181             if($counter <= 0 && $self->is_running) {
182             $self->stop;
183             }
184             }
185              
186              
187              
188             sub wait_for_oauth_grant {
189             my $self = shift;
190             my $reply;
191             while(1) {
192             $reply = $self->_oauth_queue->dequeue_timed($FORM_FILL_WARNING_TIMEOUT);
193             last if $reply;
194             print "Callback still not received. Please, accept the authorization in the browser (or Ctrl-C me if you changed your mind)\n";
195             }
196              
197             unless($reply->{verifier}) {
198              
199             # FIXME: provide http request
200              
201             if($reply->{oauth_problem}) {
202             OAuthomatic::Error::Generic->throw(
203             ident => "OAuth access rejected",
204             extra => "Attempt to get OAuth authorization was rejected. Error code: $reply->{oauth_problem}",
205             );
206             } else {
207             OAuthomatic::Error::Generic->throw(
208             ident => "Invalid OAuth callback",
209             extra => "Failed to read verifier. Most likely this means some error/omission in OAuthomatic code.\nI am so sorry...\n");
210             }
211             }
212              
213             return unless %$reply;
214             return OAuthomatic::Types::Verifier->new($reply);
215             }
216              
217              
218             sub wait_for_client_cred {
219             my $self = shift;
220             my $reply;
221             while(1) {
222             $reply = $self->_client_key_queue->dequeue_timed($FORM_FILL_WARNING_TIMEOUT);
223             last if $reply;
224             print "Form still not filled. Please, fill the form shown (or Ctrl-C me if you changed your mind)\n";
225             }
226             return unless %$reply;
227             return return OAuthomatic::Types::ClientCred->new(
228             data => $reply,
229             remap => {"client_key" => "key", "client_secret" => "secret"});
230             }
231              
232             sub _build__brick {
233             my $self = shift;
234             my @args = (port => $self->port);
235             unless($self->verbose) {
236             my $null = IO::Null->new;
237             push @args, (error_log => $null, access_log => $null);
238             }
239             my $brick = HTTP::Server::Brick->new(@args);
240             # URLs are mounted in start, to make it clear which thread references which
241             # variables.
242             return $brick;
243             }
244              
245             sub _build__template {
246             my $self = shift;
247              
248             my $tt_vars = {
249             app_name => $self->app_name,
250             site_name => $self->site_name,
251             site_client_creation_page => $self->site_client_creation_page,
252             site_client_creation_desc => $self->site_client_creation_desc,
253             site_client_creation_help => $self->site_client_creation_help,
254             static_dir => $self->static_dir,
255             };
256              
257             my $tt = Template->new({
258             INCLUDE_PATH=>[$self->template_dir, $self->static_dir],
259             VARIABLES=>$tt_vars,
260             ($self->debug ? (CACHE_SIZE => 0) : ()), # Disable caching during tests
261             # STRICT=>1,
262             }) or die "Failed to setup templates: $Template::ERROR\n";
263              
264             return $tt;
265             }
266              
267             ###########################################################################
268             # Methods below are called in worker thread
269             ###########################################################################
270              
271             sub _render_template {
272             my ($resp, $tt, $template_name, $tt_par) = @_;
273              
274             unless( $tt->process($template_name,
275             $tt_par,
276             sub { $resp->add_content(@_); }) ) {
277             my $err = $tt->error();
278             # use Data::Dumper; print Dumper($err); print Dumper($err->info); print Dumper($err->type); print $err->as_string();
279             use Data::Dumper; print Dumper($err->info);
280             OAuthomatic::Error::Generic->throw(
281             ident => "Template error",
282             extra => $err->as_string());
283             }
284             }
285              
286             sub _handle_oauth_request {
287             my ($queue, $tt, $req, $resp) = @_;
288              
289             my $params = $req->uri->query_form_hash(); # URI::QueryParam
290              
291             #if($self->debug) {
292             # use Data::Dumper;
293             # print "Parameters obtained in callback: ", Dumper($params);
294             #}
295              
296             my $verifier = $params->{'oauth_verifier'};
297             my $token = $params->{'oauth_token'};
298              
299             my $reply = {};
300             my $template_name;
301              
302              
303             if ($verifier && $token) {
304             $reply = {
305             verifier => $verifier,
306             token => $token,
307             };
308             $template_name = "oauth_granted.thtml";
309             } else {
310             my $oauth_problem = $params->{'oauth_problem'} || '';
311             $reply->{oauth_problem} = $oauth_problem if $oauth_problem;
312             if($oauth_problem eq 'user_refused') {
313             $template_name = "oauth_rejected.thtml";
314             } else {
315             $template_name = "oauth_bad_request.thtml";
316             }
317             }
318              
319             $resp->code(200);
320             _render_template($resp, $tt, $template_name, $reply);
321              
322             $queue->enqueue($reply);
323             }
324              
325             sub _handle_client_key_request {
326             my ($queue, $tt, $req, $resp) = @_;
327              
328             $resp->code(200);
329              
330             unless($req->method eq 'POST') {
331             # Just show input form
332             _render_template($resp, $tt, "client_key_entry.thtml", {});
333             } else {
334             my $params = parse_httpmsg_form($req) || {};
335              
336             my %values;
337             my %errors;
338             # Validation
339             foreach my $pname (qw(client_key client_secret)) {
340             my $value = $params->{$pname};
341             # Strip leading and final spaces (possible copy&paste)
342             $value =~ s/^[\s\r\n]+//x;
343             $value =~ s/[\s\r\n]+$//x;
344             unless($value) {
345             $errors{$pname} = "Missing value.";
346             } elsif ($value !~ /^\S{10,1000}$/x) {
347             $errors{$pname} = "Invalid value (suspiciously short, too long, or contaning invalid characters)";
348             }
349             $values{$pname} = $value;
350             }
351              
352             unless(%errors) {
353             _render_template($resp, $tt, "client_key_submitted.thtml", {});
354             $queue->enqueue(\%values);
355             } else {
356             # Redisplay
357             _render_template($resp, $tt, "client_key_entry.thtml", {
358             errors_found => 1,
359             error => \%errors,
360             value => \%values });
361             }
362             }
363              
364             }
365              
366             sub _handle_generic_request {
367             my ($tt, $req, $resp) = @_;
368              
369             $resp->code(200);
370             _render_template($resp, $tt, "default.thtml", {});
371             }
372              
373             1;
374              
375             __END__
376              
377             =pod
378              
379             =encoding UTF-8
380              
381             =head1 NAME
382              
383             OAuthomatic::Internal::MicroWeb - temporary embedded web server used internally
384              
385             =head1 VERSION
386              
387             version 0.01
388              
389             =head1 DESCRIPTION
390              
391             Utility class used internally by OAuthomatic: temporary web server
392             spawned in separate thread, used to receive final redirect of OAuth
393             sequence, and to present additional pages to the user.
394              
395             This module provides both implementation of this server, and methods
396             to communicate with it.
397              
398             =head1 PARAMETERS
399              
400             =head2 port
401              
402             Port the helper runs at. By default allocated randomly.
403              
404             =head2 template_dir
405              
406             Directory containing page templates. By default, use templates
407             provided with OAuthomatic (according to C<html_dir> param).
408              
409             =head2 static_dir
410              
411             Directory containing static files referenced by templates. By default,
412             use templates provided with OAuthomatic (according to C<html_dir>
413             param).
414              
415             =head2 verbose
416              
417             Enable console logging of web server interactions.
418              
419             =head2 callback_path
420              
421             URL path used in OAuth callback (/oauth_granted by default).
422              
423             =head2 callback_path
424              
425             URL path used in user interactions (/client_key by default).
426              
427             =head1 METHODS
428              
429             =head2 start
430              
431             Start embedded web server. To be called (from main thread) before any ineractions begin.
432              
433             =head2 stop
434              
435             Stop embedded web server. To be called (from main thread) after OAuth is properly configured.
436              
437             =head2 start_using
438              
439             Starts if not yet running. Increases usage counter.
440              
441             =head2 finish_using
442              
443             Decreass usage counter. Stops if it tropped to 0.
444              
445             =head2 wait_for_oauth_grant
446              
447             Wait until OAuth post-rights-grant callback arrives and return tokens it provided.
448             Blocks until then. Throws proper error if failed.
449              
450             To be called from the main thread.
451              
452             =head2 wait_for_client_cred
453              
454             Wait until user entered application tokens. Blocks until then.
455              
456             To be called from the main thread.
457              
458             =head1 ATTRIBUTES
459              
460             =head2 config
461              
462             L<OAuthomatic::Config> object used to bundle various configuration params.
463              
464             =head2 server
465              
466             L<OAuthomatic::Server> object used to bundle server-related configuration params.
467              
468             =head1 AUTHOR
469              
470             Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
471              
472             =head1 COPYRIGHT AND LICENSE
473              
474             This software is copyright (c) 2015 by Marcin Kasperski.
475              
476             This is free software; you can redistribute it and/or modify it under
477             the same terms as the Perl 5 programming language system itself.
478              
479             =cut