File Coverage

blib/lib/Plack/App/GitHub/WebHook.pm
Criterion Covered Total %
statement 119 127 93.7
branch 36 40 90.0
condition 27 31 87.1
subroutine 29 33 87.8
pod 1 4 25.0
total 212 235 90.2


line stmt bran cond sub pod time code
1             package Plack::App::GitHub::WebHook;
2 7     7   599294 use strict;
  7         16  
  7         361  
3 7     7   81 use warnings;
  7         13  
  7         248  
4 7     7   92 use v5.10;
  7         29  
5              
6 7     7   40 use parent 'Plack::Component';
  7         12  
  7         64  
7 7     7   60711 use Plack::Util::Accessor qw(hook events secret access safe logger);
  7         2237  
  7         56  
8 7     7   5746 use Plack::Request;
  7         552191  
  7         322  
9 7     7   5240 use Plack::Middleware::HTTPExceptions;
  7         46385  
  7         316  
10 7     7   5897 use Plack::Middleware::Access;
  7         367980  
  7         415  
11 7     7   94 use Carp qw(croak);
  7         19  
  7         611  
12 7     7   8097 use JSON qw(decode_json);
  7         87226  
  7         49  
13 7     7   1509 use Scalar::Util qw(blessed);
  7         15  
  7         8952  
14              
15             our $VERSION = '0.9';
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 24 my $hook = shift;
24 16 100 66     189 if ( !ref $hook ) {
    100 50        
    100          
    50          
25 4         19 my $class = Plack::Util::load_class($hook, 'GitHub::WebHook');
26 3         536 $class = $class->new;
27 3     3   37 return sub { $class->call(@_) };
  3         7  
28             } elsif ( ref $hook eq 'HASH' ) {
29 2         6 my ($class, $args) = each %$hook;
30 2         8 $class = Plack::Util::load_class($class, 'GitHub::WebHook');
31 2 50       46 $class = $class->new( ref $args eq 'HASH' ? %$args : @$args );
32 2     2   24 return sub { $class->call(@_) };
  2         7  
33             } elsif ( blessed $hook and $hook->can('call') ) {
34 1     2   7 return sub { $hook->call(@_) };
  2         7  
35             } elsif ( (ref $hook // '') ne 'CODE') {
36 0         0 croak "hook must be a CODE or ARRAY of CODEs";
37             }
38 9         56 $hook;
39             }
40              
41             sub to_app {
42 29     29 1 95978 my $self = shift;
43              
44 29 100 50     118 my $hook = (ref $self->hook // '') eq 'ARRAY'
      100        
45             ? $self->hook : [ $self->hook // () ];
46 29         567 $self->hook([ map { github_webhook($_) } @$hook ]);
  16         48  
47              
48             my $app = Plack::Middleware::HTTPExceptions->wrap(
49 25     25   24934 sub { $self->call_granted($_[0]) }
50 28         432 );
51              
52 28 50       1054 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       207 $self->access('github') unless $self->access;
60 28 100       189 $self->access([]) if $self->access eq 'all';
61 28         276 my @rules = (@GITHUB_IPS, 'deny' => 'all');
62 28 100       82 if ( $self->access !~ /^github$/i ) {
63 24         214 @rules = ();
64 24         33 foreach (@{$self->access}) {
  24         125  
65 26 100 100     219 if (@rules and $rules[0] eq 'allow' and $_ =~ /^github$/i) {
      100        
66 3         20 push @rules, @GITHUB_IPS[1 .. $#GITHUB_IPS];
67             } else {
68 23         57 push @rules, $_;
69             }
70             }
71             }
72 28         296 $app = Plack::Middleware::Access->wrap( $app, rules => \@rules );
73              
74 28         16708 $app;
75             }
76              
77             sub call_granted {
78 25     25 0 41 my ($self, $env) = @_;
79              
80 25 100       98 if ( $env->{REQUEST_METHOD} ne 'POST' ) {
81 1         11 return [405,['Content-Type'=>'text/plain','Content-Length'=>18],['Method Not Allowed']];
82             }
83              
84 24         179 my $req = Plack::Request->new($env);
85 24   100     321 my $event = $env->{'HTTP_X_GITHUB_EVENT'} // '';
86 24   100     111 my $delivery = $env->{'HTTP_X_GITHUB_DELIVERY'} // '';
87 24         30 my $payload;
88 24         32 my ($status, $message);
89            
90 24 100 100     76 if ( !$self->events or grep { $event eq $_ } @{$self->events} ) {
  2         14  
  2         12  
91 23   100     207 $payload = $req->param('payload') || $req->content;
92 23         8677 $payload = eval { decode_json $payload };
  23         198  
93             }
94              
95 24 100       74 if (!$payload) {
96 2         31 return [400,['Content-Type'=>'text/plain','Content-Length'=>11],['Bad Request']];
97             }
98            
99             my $logger = Plack::App::GitHub::WebHook::Logger->new(
100       11     $self->logger || $env->{'psgix.logger'} || sub { }
101 22   100     87 );
102              
103 22 100       114 if ( $self->receive( [ $payload, $event, $delivery, $logger ], $env->{'psgi.errors'} ) ) {
104 9         19 ($status, $message) = (200,"OK");
105             } else {
106 12         23 ($status, $message) = (202,"Accepted");
107             }
108              
109 21 100       85 $message = ucfirst($event)." $message" if $self->events;
110              
111             return [
112 21         303 $status,
113             [ 'Content-Type' => 'text/plain', 'Content-Length' => length $message ],
114             [ $message ]
115             ];
116             }
117              
118             sub receive {
119 22     22 0 57 my ($self, $args, $error) = @_;
120              
121 22         25 foreach my $hook (@{$self->{hook}}) {
  22         74  
122 14 100 66     16 if ( !eval { $hook->(@$args) } || $@ ) {
  14         40  
123 5 100       55 if ( $@ ) {
124 2 100       7 if ($self->safe) {
125 1         14 $error->print($@);
126             } else {
127 1         11 die Plack::App::GitHub::WebHook::Exception->new( 500, $@ );
128             }
129             }
130 4         26 return;
131             }
132             }
133              
134 17         76 return scalar @{$self->{hook}};
  17         65  
135             }
136              
137             {
138             package Plack::App::GitHub::WebHook::Logger;
139 7     7   55 use Scalar::Util qw(blessed);
  7         14  
  7         3028  
140             sub new {
141 22     22   436 my $self = bless { logger => $_[1] }, $_[0];
142 22         56 foreach my $level (qw(debug info warn error fatal)) {
143 10     10   38 $self->{$level} = sub { $self->log( $level => $_[0] ) }
144 110         453 }
145 22         45 $self;
146             }
147             sub log {
148 22     22   39 my ($self, $level, $message) = @_;
149 22         19 chomp $message;
150 22 50       37 if (blessed $self->{logger}) {
151 0         0 $self->{logger}->log( level => $level, message => $message );
152             } else {
153 22         50 $self->{logger}->({ level => $level, message => $message });
154             }
155 22         91 1;
156             }
157 0     0   0 sub debug { $_[0]->log(debug => $_[1]) }
158 0     0   0 sub info { $_[0]->log(info => $_[1]) }
159 0     0   0 sub warn { $_[0]->log(warn => $_[1]) }
160 0     0   0 sub error { $_[0]->log(error => $_[1]) }
161 2     2   10 sub fatal { $_[0]->log(fatal => $_[1]) }
162             }
163              
164             {
165             package Plack::App::GitHub::WebHook::Exception;
166 7     7   57 use overload '""' => sub { $_[0]->{message} };
  7     1   14  
  7         83  
  1         12574  
167 1     1   9 sub new { bless { code => $_[1], message => $_[2] }, $_[0]; }
168 1     1   60 sub code { $_[0]->{code} }
169             }
170              
171             1;
172             __END__