1919
2020import java .io .IOException ;
2121import java .net .URI ;
22- import java .net .URISyntaxException ;
2322import java .nio .charset .StandardCharsets ;
2423import java .security .InvalidKeyException ;
2524import java .security .KeyManagementException ;
4039import org .apache .cloudstack .storage .command .CommandResult ;
4140import org .apache .commons .codec .DecoderException ;
4241import org .apache .commons .codec .binary .Base64 ;
43- import org .apache .commons .httpclient .HttpStatus ;
4442import org .apache .commons .lang3 .StringUtils ;
4543import org .apache .http .Header ;
4644import org .apache .http .HttpEntity ;
4745import org .apache .http .HttpHeaders ;
46+ import org .apache .http .HttpStatus ;
4847import org .apache .http .client .config .RequestConfig ;
4948import org .apache .http .client .methods .CloseableHttpResponse ;
5049import org .apache .http .client .methods .HttpPost ;
6362import org .json .JSONException ;
6463import org .json .JSONObject ;
6564
65+ import com .cloud .exception .InvalidParameterValueException ;
66+
6667public class WebhookDeliveryThread implements Runnable {
6768 protected static Logger LOGGER = LogManager .getLogger (WebhookDeliveryThread .class );
6869
6970 private static final String HEADER_X_CS_EVENT_ID = "X-CS-Event-ID" ;
7071 private static final String HEADER_X_CS_EVENT = "X-CS-Event" ;
7172 private static final String HEADER_X_CS_SIGNATURE = "X-CS-Signature" ;
7273 private static final String PREFIX_HEADER_USER_AGENT = "CS-Hookshot/" ;
74+ private static final int MAX_REDIRECT_HOPS = 5 ;
7375 private final Webhook webhook ;
7476 private final Event event ;
7577 private CloseableHttpClient httpClient ;
@@ -79,6 +81,11 @@ public class WebhookDeliveryThread implements Runnable {
7981 private Date startTime ;
8082 private int deliveryTries = 3 ;
8183 private int deliveryTimeout = 10 ;
84+ private String destinationBlocklist ;
85+ private boolean blockLocalAddresses = true ;
86+ private boolean allowRedirects ;
87+ private boolean allowHttp ;
88+ private URI payloadUri ;
8289
8390 AsyncCompletionCallback <WebhookDeliveryResult > callback ;
8491
@@ -97,19 +104,19 @@ protected boolean isValidJson(String json) {
97104
98105 protected void setHttpClient () throws NoSuchAlgorithmException , KeyStoreException , KeyManagementException {
99106 if (webhook .isSslVerification ()) {
100- httpClient = HttpClients .createDefault ();
107+ httpClient = HttpClients .custom (). disableRedirectHandling (). build ();
101108 return ;
102109 }
103110 httpClient = HttpClients
104111 .custom ()
112+ .disableRedirectHandling ()
105113 .setSSLContext (new SSLContextBuilder ().loadTrustMaterial (null ,
106114 TrustAllStrategy .INSTANCE ).build ())
107115 .setSSLHostnameVerifier (NoopHostnameVerifier .INSTANCE )
108116 .build ();
109117 }
110118
111- protected HttpPost getBasicHttpPostRequest () throws URISyntaxException {
112- final URI uri = new URI (webhook .getPayloadUrl ());
119+ protected HttpPost getBasicHttpPostRequest (final URI uri ) {
113120 HttpPost request = new HttpPost ();
114121 RequestConfig .Builder requestConfig = RequestConfig .custom ();
115122 requestConfig .setConnectTimeout (deliveryTimeout * 1000 );
@@ -143,13 +150,19 @@ protected void updateRequestHeaders(HttpPost request) throws DecoderException, N
143150 this .headers = StringUtils .join (headers , "\n " );
144151 }
145152
146- public WebhookDeliveryThread (Webhook webhook , Event event ,
153+ public WebhookDeliveryThread (Webhook webhook , Event event , URI uri ,
147154 AsyncCompletionCallback <WebhookDeliveryResult > callback ) {
148155 this .webhook = webhook ;
149156 this .event = event ;
157+ this .payloadUri = uri ;
150158 this .callback = callback ;
151159 }
152160
161+ public WebhookDeliveryThread (Webhook webhook , Event event ,
162+ AsyncCompletionCallback <WebhookDeliveryResult > callback ) {
163+ this (webhook , event , null , callback );
164+ }
165+
153166 public void setDeliveryTries (int deliveryTries ) {
154167 this .deliveryTries = deliveryTries ;
155168 }
@@ -158,13 +171,29 @@ public void setDeliveryTimeout(int deliveryTimeout) {
158171 this .deliveryTimeout = deliveryTimeout ;
159172 }
160173
174+ public void setDestinationBlocklist (String destinationBlocklist ) {
175+ this .destinationBlocklist = destinationBlocklist ;
176+ }
177+
178+ public void setBlockLocalAddresses (boolean blockLocalAddresses ) {
179+ this .blockLocalAddresses = blockLocalAddresses ;
180+ }
181+
182+ public void setAllowRedirects (boolean allowRedirects ) {
183+ this .allowRedirects = allowRedirects ;
184+ }
185+
186+ public void setAllowHttp (boolean allowHttp ) {
187+ this .allowHttp = allowHttp ;
188+ }
189+
161190 @ Override
162191 public void run () {
163- LOGGER .debug ("Delivering event: {} for {}" , event .getEventType (), webhook );
164192 if (event == null ) {
165193 LOGGER .warn ("Invalid event received for delivering to {}" , webhook );
166194 return ;
167195 }
196+ LOGGER .debug ("Delivering event: {} for {}" , event .getEventType (), webhook );
168197 payload = event .getDescription ();
169198 LOGGER .trace ("Payload: {}" , payload );
170199 int attempt = 0 ;
@@ -176,6 +205,10 @@ public void run() {
176205 callback .complete (new WebhookDeliveryResult (headers , payload , success , response , new Date ()));
177206 return ;
178207 }
208+ if (payloadUri == null ) {
209+ payloadUri = WebhookUrlValidator .validateWebhookDestinationUrl (webhook .getPayloadUrl (), allowHttp ,
210+ destinationBlocklist , blockLocalAddresses );
211+ }
179212 while (attempt < deliveryTries ) {
180213 attempt ++;
181214 if (delivery (attempt )) {
@@ -188,7 +221,7 @@ public void run() {
188221
189222 protected void updateResponseFromRequest (HttpEntity entity ) {
190223 try {
191- this .response = EntityUtils .toString (entity , StandardCharsets .UTF_8 );
224+ this .response = EntityUtils .toString (entity , StandardCharsets .UTF_8 );
192225 } catch (IOException e ) {
193226 LOGGER .error ("Failed to parse response for event: {} for {}" ,
194227 event .getEventType (), webhook );
@@ -199,30 +232,66 @@ protected void updateResponseFromRequest(HttpEntity entity) {
199232 protected boolean delivery (int attempt ) {
200233 startTime = new Date ();
201234 try {
202- HttpPost request = getBasicHttpPostRequest ();
203- StringEntity input = new StringEntity (payload ,
204- isValidJson (payload ) ? ContentType .APPLICATION_JSON : ContentType .TEXT_PLAIN );
205- request .setEntity (input );
206- updateRequestHeaders (request );
207- LOGGER .trace ("Delivering event: {} for {} with timeout: {}, " +
208- "attempt #{}" , event .getEventType (), webhook ,
209- deliveryTimeout , attempt );
210- final CloseableHttpResponse response = httpClient .execute (request );
211- updateResponseFromRequest (response .getEntity ());
212- if (response .getStatusLine ().getStatusCode () == HttpStatus .SC_OK ) {
213- LOGGER .trace ("Successfully delivered event: {} for {}" ,
214- event .getEventType (), webhook );
215- return true ;
235+ int redirectsFollowed = 0 ;
236+ URI currentUri = payloadUri ;
237+ while (true ) {
238+ HttpPost request = getBasicHttpPostRequest (currentUri );
239+ StringEntity input = new StringEntity (payload ,
240+ isValidJson (payload ) ? ContentType .APPLICATION_JSON : ContentType .TEXT_PLAIN );
241+ request .setEntity (input );
242+ updateRequestHeaders (request );
243+ LOGGER .trace ("Delivering event: {} for {} with timeout: {}, attempt #{}, uri={}" ,
244+ event .getEventType (), webhook , deliveryTimeout , attempt , currentUri );
245+ try (CloseableHttpResponse response = httpClient .execute (request )) {
246+ updateResponseFromRequest (response .getEntity ());
247+ int statusCode = response .getStatusLine ().getStatusCode ();
248+ if (statusCode == HttpStatus .SC_OK ) {
249+ LOGGER .trace ("Successfully delivered event: {} for {}" ,
250+ event .getEventType (), webhook );
251+ return true ;
252+ }
253+ if (!isRedirectStatus (statusCode )) {
254+ return false ;
255+ }
256+ if (!allowRedirects ) {
257+ this .response = String .format (
258+ "Delivery failed due to redirect status code %s while redirects are disabled" ,
259+ statusCode );
260+ return false ;
261+ }
262+ Header locationHeader = response .getFirstHeader (HttpHeaders .LOCATION );
263+ if (locationHeader == null || StringUtils .isBlank (locationHeader .getValue ())) {
264+ this .response = "Delivery failed due to redirect response without location header" ;
265+ return false ;
266+ }
267+ redirectsFollowed ++;
268+ if (redirectsFollowed > MAX_REDIRECT_HOPS ) {
269+ this .response = String .format (
270+ "Delivery failed due to too many redirect hops (>%s)" ,
271+ MAX_REDIRECT_HOPS );
272+ return false ;
273+ }
274+ currentUri = WebhookUrlValidator .validateWebhookDestinationURI (
275+ currentUri .resolve (locationHeader .getValue ()),
276+ allowHttp ,destinationBlocklist , blockLocalAddresses );
277+ }
216278 }
217- } catch (URISyntaxException | IOException | DecoderException | NoSuchAlgorithmException |
218- InvalidKeyException e ) {
279+ } catch (IOException | DecoderException | NoSuchAlgorithmException | InvalidKeyException e ) {
219280 LOGGER .warn ("Failed to deliver {}, in attempt #{} due to: {}" ,
220281 webhook , attempt , e .getMessage ());
221282 response = String .format ("Failed due to : %s" , e .getMessage ());
283+ } catch (InvalidParameterValueException e ) {
284+ LOGGER .warn ("Failed to deliver {}, in attempt #{} due to security policy: {}" ,
285+ webhook , attempt , e .getMessage ());
286+ response = String .format ("Failed due to : %s" , e .getMessage ());
222287 }
223288 return false ;
224289 }
225290
291+ protected boolean isRedirectStatus (final int statusCode ) {
292+ return statusCode >= 300 && statusCode < 400 ;
293+ }
294+
226295 public static String generateHMACSignature (String data , String key )
227296 throws InvalidKeyException , NoSuchAlgorithmException , DecoderException {
228297 Mac mac = Mac .getInstance ("HMACSHA256" );
0 commit comments