File Coverage

blib/lib/Plack/App/GitHub/WebHook.pm
Criterion Covered Total %
statement 117 124 94.3
branch 35 38 92.1
condition 27 31 87.1
subroutine 28 32 87.5
pod 1 4 25.0
total 208 229 90.8


line stmt bran cond sub pod time code
1             package Plack::App::GitHub::WebHook;
2 7     7   368000 use strict;
  7         14  
  7         188  
3 7     7   44 use warnings;
  7         9  
  7         154  
4 7     7   60 use v5.10;
  7         18  
  7         223  
5              
6 7     7   25 use parent 'Plack::Component';
  7         9  
  7         36  
7 7     7   32347 use Plack::Util::Accessor qw(hook events secret access safe);
  7         1378  
  7         45  
8 7     7   3367 use Plack::Request;
  7         186407  
  7         199  
9 7     7   3327 use Plack::Middleware::HTTPExceptions;
  7         26032  
  7         191  
10 7     7   2996 use Plack::Middleware::Access;
  7         232084  
  7         246  
11 7     7   64 use Carp qw(croak);
  7         14  
  7         384  
12 7     7   4812 use JSON qw(decode_json);
  7         59138  
  7         32  
13 7     7   971 use Scalar::Util qw(blessed);
  7         15  
  7         6087  
14              
15             our $VERSION = '0.8';
16              
17             our @GITHUB_IPS = (
18             allow => "204.232.175.64/27",
19             allow => "192.30.252.0/22",
20             );
21              
22             sub github_webhook {
23 16     16 0 21 my $hook = shift;
24 16 100 66     138 if ( !ref $hook ) {
    100 50        
    100          
    50          
25 4         16 my $class = Plack::Util::load_class($hook, 'GitHub::WebHook');
26 3         404 $class = $class->new;
27 3     3   30 return sub { $class->call(@_) };
  3         8  
28             } elsif ( ref $hook eq 'HASH' ) {
29 2         6 my ($class, $args) = each %$hook;
30 2         7 $class = Plack::Util::load_class($class, 'GitHub::WebHook');
31 2 50       36 $class = $class->new( ref $args eq 'HASH' ? %$args : @$args );
32 2     2   18 return sub { $class->call(@_) };
  2         5  
33             } elsif ( blessed $hook and $hook->can('call') ) {
34 1     2   5 return sub { $hook->call(@_) };
  2         5  
35             } elsif ( (ref $hook // '') ne 'CODE') {
36 0         0 croak "hook must be a CODE or ARRAY of CODEs";
37             }
38 9         36 $hook;
39             }
40              
41             sub to_app {
42 29     29 1 69962 my $self = shift;
43              
44 29 100 50     94 my $hook = (ref $self->hook // '') eq 'ARRAY'
      100        
45             ? $self->hook : [ $self->hook // () ];
46 29         494 $self->hook([ map { github_webhook($_) } @$hook ]);
  16         33  
47              
48             my $app = Plack::Middleware::HTTPExceptions->wrap(
49 25     25   17904 sub { $self->call_granted($_[0]) }
50 28         464 );
51              
52 28 50       780 if ($self->secret) {
53 0         0 require Plack::Middleware::HubSignature;
54 0         0 $app = Plack::Middleware::HubSignature->wrap($app,
55             secret => $self->secret
56             );
57             }
58              
59 28 100       158 $self->access('github') unless $self->access;
60 28 100       128 $self->access([]) if $self->access eq 'all';
61 28         180 my @rules = (@GITHUB_IPS, 'deny' => 'all');
62 28 100       54 if ( $self->access !~ /^github$/i ) {
63 24         185 @rules = ();
64 24         19 foreach (@{$self->access}) {
  24         45  
65 26 100 100     137 if (@rules and $rules[0] eq 'allow' and $_ =~ /^github$/i) {
      100        
66 3         7 push @rules, @GITHUB_IPS[1 .. $#GITHUB_IPS];
67             } else {
68 23         33 push @rules, $_;
69             }
70             }
71             }
72 28         203 $app = Plack::Middleware::Access->wrap( $app, rules => \@rules );
73              
74 28         13208 $app;
75             }
76              
77             sub call_granted {
78 25     25 0 30 my ($self, $env) = @_;
79              
80 25 100       72 if ( $env->{REQUEST_METHOD} ne 'POST' ) {
81 1         10 return [405,['Content-Type'=>'text/plain','Content-Length'=>18],['Method Not Allowed']];
82             }
83              
84 24         104 my $req = Plack::Request->new($env);
85 24   100     220 my $event = $env->{'HTTP_X_GITHUB_EVENT'} // '';
86 24   100     78 my $delivery = $env->{'HTTP_X_GITHUB_DELIVERY'} // '';
87 24         21 my $payload;
88 24         23 my ($status, $message);
89            
90 24 100 100     54 if ( !$self->events or grep { $event eq $_ } @{$self->events} ) {
  2         10  
  2         10  
91 23   100     143 $payload = $req->param('payload') || $req->content;
92 23         8081 $payload = eval { decode_json $payload };
  23         158  
93             }
94              
95 24 100       48 if (!$payload) {
96 2         18 return [400,['Content-Type'=>'text/plain','Content-Length'=>11],['Bad Request']];
97             }
98            
99             my $logger = Plack::App::GitHub::WebHook::Logger->new(
100 11     11   6 $env->{'psgix.logger'} || sub { }
101 22   100     219 );
102              
103 22 100       82 if ( $self->receive( [ $payload, $event, $delivery, $logger ], $env->{'psgi.errors'} ) ) {
104 9         19 ($status, $message) = (200,"OK");
105             } else {
106 12         18 ($status, $message) = (202,"Accepted");
107             }
108              
109 21 100       54 $message = ucfirst($event)." $message" if $self->events;
110              
111             return [
112 21         193 $status,
113             [ 'Content-Type' => 'text/plain', 'Content-Length' => length $message ],
114             [ $message ]
115             ];
116             }
117              
118             sub receive {
119 22     22 0 54 my ($self, $args, $error) = @_;
120              
121 22         27 foreach my $hook (@{$self->{hook}}) {
  22         52  
122 14 100 66     14 if ( !eval { $hook->(@$args) } || $@ ) {
  14         32  
123 5 100       37 if ( $@ ) {
124 2 100       4 if ($self->safe) {
125 1         9 $error->print($@);
126             } else {
127 1         7 die Plack::App::GitHub::WebHook::Exception->new( 500, $@ );
128             }
129             }
130 4         17 return;
131             }
132             }
133              
134 17         59 return scalar @{$self->{hook}};
  17         49  
135             }
136              
137             {
138             package Plack::App::GitHub::WebHook::Logger;
139             sub new {
140 22     22   71 my $self = bless { logger => $_[1] }, $_[0];
141 22         49 foreach my $level (qw(debug info warn error fatal)) {
142 10     10   31 $self->{$level} = sub { $self->log( $level => $_[0] ) }
143 110         335 }
144 22         39 $self;
145             }
146             sub log {
147 22     22   29 my ($self, $level, $message) = @_;
148 22         13 chomp $message;
149 22         37 $self->{logger}->({ level => $level, message => $message });
150 22         55 1;
151             }
152 0     0   0 sub debug { $_[0]->log(debug => $_[1]) }
153 0     0   0 sub info { $_[0]->log(info => $_[1]) }
154 0     0   0 sub warn { $_[0]->log(warn => $_[1]) }
155 0     0   0 sub error { $_[0]->log(error => $_[1]) }
156 2     2   10 sub fatal { $_[0]->log(fatal => $_[1]) }
157             }
158              
159             {
160             package Plack::App::GitHub::WebHook::Exception;
161 7     7   36 use overload '""' => sub { $_[0]->{message} };
  7     1   9  
  7         59  
  1         792  
162 1     1   7 sub new { bless { code => $_[1], message => $_[2] }, $_[0]; }
163 1     1   43 sub code { $_[0]->{code} }
164             }
165              
166             1;
167             __END__