Webhook::getEventClassName()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Smalot\Bitbucket\Webhook;
4
5
use Smalot\Bitbucket\Webhook\Event\EventBase;
6
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
7
use Symfony\Component\HttpFoundation\Request;
8
9
/**
10
 * Class Webhook
11
 * @package Smalot\Bitbucket\Webhook
12
 */
13
class Webhook
14
{
15
    /**
16
     * @var EventDispatcherInterface
17
     */
18
    protected $eventDispatcher;
19
20
    /**
21
     * @var string
22
     */
23
    protected $eventName;
24
25
    /**
26
     * @var string
27
     */
28
    protected $payload;
29
30
    /**
31
     * @var int
32
     */
33
    protected $attemptCount;
34
35
    /**
36
     * @var string
37
     */
38
    protected $requestUuid;
39
40
    /**
41
     * @var array
42
     */
43
    protected $eventMap;
44
45
    /**
46
     * Webhook constructor.
47
     * @param EventDispatcherInterface $eventDispatcher
48
     */
49
    public function __construct(EventDispatcherInterface $eventDispatcher = null)
50
    {
51
        $this->eventDispatcher = $eventDispatcher;
52
        $this->eventMap = array();
53
    }
54
55
    /**
56
     * @param string $event
57
     * @return string
58
     */
59
    public function getEventClassName($event)
60
    {
61
        $map = $this->getEventMap();
62
63
        return $map[$event];
64
    }
65
66
    /**
67
     * @return array
68
     */
69
    public function getDefaultEventNames()
70
    {
71
        return array(
72
            'repo:push',
73
            'repo:fork',
74
            'repo:commit_comment_created',
75
            'repo:commit_status_created',
76
            'repo:commit_status_updated',
77
            'issue:created',
78
            'issue:updated',
79
            'issue:comment_created',
80
            'pullrequest:created',
81
            'pullrequest:updated',
82
            'pullrequest:approved',
83
            'pullrequest:unapproved',
84
            'pullrequest:fulfilled',
85
            'pullrequest:rejected',
86
            'pullrequest:comment_created',
87
            'pullrequest:comment_updated',
88
            'pullrequest:comment_deleted',
89
        );
90
    }
91
92
    /**
93
     * @return array
94
     */
95
    public function getEventMap()
96
    {
97
        if (empty($this->eventMap)) {
98
            $this->eventMap = array();
99
            $namespace = '\\Smalot\\Bitbucket\\Webhook\\Event\\';
100
            $eventNames = $this->getDefaultEventNames();
101
102
            $classNames = array_map(
103
                function($event) use ($namespace) {
104
                    $className = str_replace(' ', '', ucwords(str_replace(array('_', ':'), ' ', $event)));
105
106
                    return $namespace . $className . 'Event';
107
                },
108
                $eventNames
109
            );
110
111
            $this->eventMap = array_combine($eventNames, $classNames);
112
        }
113
114
        return $this->eventMap;
115
    }
116
117
    /**
118
     * @param Request $request
119
     * @return bool
120
     */
121
    public function isValidRequest(Request $request)
122
    {
123
        try {
124
            $valid = $this->checkSecurity($request);
125
        } catch (\Exception $e) {
126
            return false;
127
        }
128
129
        return $valid;
130
    }
131
132
    /**
133
     * @param Request $request
134
     * @param bool $dispatch
135
     * @return EventBase
136
     *
137
     * @throws \InvalidArgumentException
138
     */
139
    public function parseRequest(Request $request, $dispatch = true)
140
    {
141
        if (!$this->checkSecurity($request)) {
142
            throw new \InvalidArgumentException('Invalid security, check remote IP.');
143
        }
144
145
        if ($className = $this->getEventClassName($this->eventName)) {
146
            $event = new $className(
147
                $this->eventName,
148
                $this->payload,
149
                $this->attemptCount,
150
                $this->requestUuid
151
            );
152
        } else {
153
            throw new \InvalidArgumentException('Unknown event type: ' . $this->eventName . '.');
154
        }
155
156
        if (null !== $this->eventDispatcher && $dispatch) {
157
            $this->eventDispatcher->dispatch(Events::WEBHOOK_REQUEST, $event);
158
        }
159
160
        return $event;
161
    }
162
163
    /**
164
     * @param Request $request
165
     * @return bool
166
     *
167
     * @throws \InvalidArgumentException
168
     */
169
    protected function checkSecurity(Request $request)
170
    {
171
        $trustedIpRanges = array(
172
            '131.103.20.160/27',
173
            '165.254.145.0/26',
174
            '104.192.143.0/24',
175
        );
176
177
        // Reset any previously payload set.
178
        $this->requestUuid = $this->eventName = $this->payload = $this->attemptCount = null;
179
180
        // Extract Bitbucket headers from request.
181
        $requestUuid = (string)$request->headers->get('X-Request-Uuid');
182
        $event = (string)$request->headers->get('X-Event-Key');
183
        $count = (int)$request->headers->get('X-Attempt-Number');
184
        $payload = (string)$request->getContent();
185
186
        if (empty($requestUuid) || empty($event) || empty($count)) {
187
            throw new \InvalidArgumentException('Missing Bitbucket headers.');
188
        }
189
190
        if ($this->ipInRanges($request->getClientIp(), $trustedIpRanges)) {
191
            $this->requestUuid = $requestUuid;
192
            $this->eventName = $event;
193
            $this->attemptCount = $count;
194
            $this->payload = $payload;
195
196
            return true;
197
        }
198
199
        return false;
200
    }
201
202
    /**
203
     * @param string $ip
204
     * @param array $ranges
205
     * @return bool
206
     */
207
    protected function ipInRanges($ip, $ranges)
208
    {
209
        foreach ($ranges as $range) {
210
            if ($this->ipInRange($ip, $range)) {
211
                return true;
212
            }
213
        }
214
215
        return false;
216
    }
217
218
    /**
219
     * Check if a given ip is in a network
220
     * @link https://gist.github.com/tott/7684443
221
     *
222
     * @param  string $ip IP to check in IPV4 format eg. 127.0.0.1
223
     * @param  string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
224
     * @return boolean true if the ip is in this range / false if not.
225
     */
226
    protected function ipInRange($ip, $range)
227
    {
228
        // $range is in IP/CIDR format eg 127.0.0.1/24
229
        list($range, $netmask) = explode('/', $range, 2);
230
        $range_decimal = ip2long($range);
231
        $ip_decimal = ip2long($ip);
232
        $wildcard_decimal = pow(2, (32 - $netmask)) - 1;
233
        $netmask_decimal = ~$wildcard_decimal;
234
235
        return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal));
236
    }
237
}
238