Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/herald/worker/HeraldWebhookWorker.php
12256 views
1
<?php
2
3
final class HeraldWebhookWorker
4
extends PhabricatorWorker {
5
6
protected function doWork() {
7
$viewer = PhabricatorUser::getOmnipotentUser();
8
9
$data = $this->getTaskData();
10
$request_phid = idx($data, 'webhookRequestPHID');
11
12
$request = id(new HeraldWebhookRequestQuery())
13
->setViewer($viewer)
14
->withPHIDs(array($request_phid))
15
->executeOne();
16
if (!$request) {
17
throw new PhabricatorWorkerPermanentFailureException(
18
pht(
19
'Unable to load webhook request ("%s"). It may have been '.
20
'garbage collected.',
21
$request_phid));
22
}
23
24
$status = $request->getStatus();
25
if ($status !== HeraldWebhookRequest::STATUS_QUEUED) {
26
throw new PhabricatorWorkerPermanentFailureException(
27
pht(
28
'Webhook request ("%s") is not in "%s" status (actual '.
29
'status is "%s"). Declining call to hook.',
30
$request_phid,
31
HeraldWebhookRequest::STATUS_QUEUED,
32
$status));
33
}
34
35
// If we're in silent mode, permanently fail the webhook request and then
36
// return to complete this task.
37
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
38
$this->failRequest(
39
$request,
40
HeraldWebhookRequest::ERRORTYPE_HOOK,
41
HeraldWebhookRequest::ERROR_SILENT);
42
return;
43
}
44
45
$hook = $request->getWebhook();
46
47
if ($hook->isDisabled()) {
48
$this->failRequest(
49
$request,
50
HeraldWebhookRequest::ERRORTYPE_HOOK,
51
HeraldWebhookRequest::ERROR_DISABLED);
52
throw new PhabricatorWorkerPermanentFailureException(
53
pht(
54
'Associated hook ("%s") for webhook request ("%s") is disabled.',
55
$hook->getPHID(),
56
$request_phid));
57
}
58
59
$uri = $hook->getWebhookURI();
60
try {
61
PhabricatorEnv::requireValidRemoteURIForFetch(
62
$uri,
63
array(
64
'http',
65
'https',
66
));
67
} catch (Exception $ex) {
68
$this->failRequest(
69
$request,
70
HeraldWebhookRequest::ERRORTYPE_HOOK,
71
HeraldWebhookRequest::ERROR_URI);
72
throw new PhabricatorWorkerPermanentFailureException(
73
pht(
74
'Associated hook ("%s") for webhook request ("%s") has invalid '.
75
'fetch URI: %s',
76
$hook->getPHID(),
77
$request_phid,
78
$ex->getMessage()));
79
}
80
81
$object_phid = $request->getObjectPHID();
82
83
$object = id(new PhabricatorObjectQuery())
84
->setViewer($viewer)
85
->withPHIDs(array($object_phid))
86
->executeOne();
87
if (!$object) {
88
$this->failRequest(
89
$request,
90
HeraldWebhookRequest::ERRORTYPE_HOOK,
91
HeraldWebhookRequest::ERROR_OBJECT);
92
93
throw new PhabricatorWorkerPermanentFailureException(
94
pht(
95
'Unable to load object ("%s") for webhook request ("%s").',
96
$object_phid,
97
$request_phid));
98
}
99
100
$xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(
101
$object);
102
$xaction_phids = $request->getTransactionPHIDs();
103
if ($xaction_phids) {
104
$xactions = $xaction_query
105
->setViewer($viewer)
106
->withObjectPHIDs(array($object_phid))
107
->withPHIDs($xaction_phids)
108
->execute();
109
$xactions = mpull($xactions, null, 'getPHID');
110
} else {
111
$xactions = array();
112
}
113
114
// To prevent thundering herd issues for high volume webhooks (where
115
// a large number of workers might try to work through a request backlog
116
// simultaneously, before the error backoff can catch up), we never
117
// parallelize requests to a particular webhook.
118
119
$lock_key = 'webhook('.$hook->getPHID().')';
120
$lock = PhabricatorGlobalLock::newLock($lock_key);
121
122
try {
123
$lock->lock();
124
} catch (Exception $ex) {
125
phlog($ex);
126
throw new PhabricatorWorkerYieldException(15);
127
}
128
129
$caught = null;
130
try {
131
$this->callWebhookWithLock($hook, $request, $object, $xactions);
132
} catch (Exception $ex) {
133
$caught = $ex;
134
}
135
136
$lock->unlock();
137
138
if ($caught) {
139
throw $caught;
140
}
141
}
142
143
private function callWebhookWithLock(
144
HeraldWebhook $hook,
145
HeraldWebhookRequest $request,
146
$object,
147
array $xactions) {
148
$viewer = PhabricatorUser::getOmnipotentUser();
149
150
if ($hook->isInErrorBackoff($viewer)) {
151
throw new PhabricatorWorkerYieldException($hook->getErrorBackoffWindow());
152
}
153
154
$xaction_data = array();
155
foreach ($xactions as $xaction) {
156
$xaction_data[] = array(
157
'phid' => $xaction->getPHID(),
158
);
159
}
160
161
$trigger_data = array();
162
foreach ($request->getTriggerPHIDs() as $trigger_phid) {
163
$trigger_data[] = array(
164
'phid' => $trigger_phid,
165
);
166
}
167
168
$payload = array(
169
'object' => array(
170
'type' => phid_get_type($object->getPHID()),
171
'phid' => $object->getPHID(),
172
),
173
'triggers' => $trigger_data,
174
'action' => array(
175
'test' => $request->getIsTestAction(),
176
'silent' => $request->getIsSilentAction(),
177
'secure' => $request->getIsSecureAction(),
178
'epoch' => (int)$request->getDateCreated(),
179
),
180
'transactions' => $xaction_data,
181
);
182
183
$payload = id(new PhutilJSON())->encodeFormatted($payload);
184
$key = $hook->getHmacKey();
185
$signature = PhabricatorHash::digestHMACSHA256($payload, $key);
186
$uri = $hook->getWebhookURI();
187
188
$future = id(new HTTPSFuture($uri))
189
->setMethod('POST')
190
->addHeader('Content-Type', 'application/json')
191
->addHeader('X-Phabricator-Webhook-Signature', $signature)
192
->setTimeout(15)
193
->setData($payload);
194
195
list($status) = $future->resolve();
196
197
if ($status->isTimeout()) {
198
$error_type = HeraldWebhookRequest::ERRORTYPE_TIMEOUT;
199
} else {
200
$error_type = HeraldWebhookRequest::ERRORTYPE_HTTP;
201
}
202
$error_code = $status->getStatusCode();
203
204
$request
205
->setErrorType($error_type)
206
->setErrorCode($error_code)
207
->setLastRequestEpoch(PhabricatorTime::getNow());
208
209
$retry_forever = HeraldWebhookRequest::RETRY_FOREVER;
210
if ($status->isTimeout() || $status->isError()) {
211
$should_retry = ($request->getRetryMode() === $retry_forever);
212
213
$request
214
->setLastRequestResult(HeraldWebhookRequest::RESULT_FAIL);
215
216
if ($should_retry) {
217
$request->save();
218
219
throw new Exception(
220
pht(
221
'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
222
'will be retried.',
223
$request->getPHID(),
224
$uri,
225
$error_type,
226
$error_code));
227
} else {
228
$request
229
->setStatus(HeraldWebhookRequest::STATUS_FAILED)
230
->save();
231
232
throw new PhabricatorWorkerPermanentFailureException(
233
pht(
234
'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
235
'will not be retried.',
236
$request->getPHID(),
237
$uri,
238
$error_type,
239
$error_code));
240
}
241
} else {
242
$request
243
->setLastRequestResult(HeraldWebhookRequest::RESULT_OKAY)
244
->setStatus(HeraldWebhookRequest::STATUS_SENT)
245
->save();
246
}
247
}
248
249
private function failRequest(
250
HeraldWebhookRequest $request,
251
$error_type,
252
$error_code) {
253
254
$request
255
->setStatus(HeraldWebhookRequest::STATUS_FAILED)
256
->setErrorType($error_type)
257
->setErrorCode($error_code)
258
->setLastRequestResult(HeraldWebhookRequest::RESULT_NONE)
259
->setLastRequestEpoch(0)
260
->save();
261
}
262
263
}
264
265