1 /*
2  * Zed Attack Proxy (ZAP) and its related class files.
3  *
4  * ZAP is an HTTP/HTTPS proxy for assessing web application security.
5  *
6  * Copyright 2017 The ZAP Development Team
7  *
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *     http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  */
20 package org.zaproxy.zap.extension.api;
21 
22 import static org.hamcrest.CoreMatchers.notNullValue;
23 import static org.hamcrest.CoreMatchers.nullValue;
24 import static org.hamcrest.MatcherAssert.assertThat;
25 import static org.hamcrest.Matchers.containsString;
26 import static org.hamcrest.Matchers.endsWith;
27 import static org.hamcrest.Matchers.equalTo;
28 import static org.hamcrest.Matchers.is;
29 import static org.hamcrest.Matchers.startsWith;
30 import static org.junit.jupiter.api.Assertions.assertThrows;
31 import static org.mockito.Mockito.when;
32 import static org.mockito.Mockito.withSettings;
33 
34 import java.net.Inet4Address;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.concurrent.TimeUnit;
38 import net.sf.json.JSON;
39 import net.sf.json.JSONObject;
40 import org.apache.commons.httpclient.URI;
41 import org.junit.jupiter.api.Test;
42 import org.junit.jupiter.api.extension.ExtendWith;
43 import org.mockito.Mockito;
44 import org.mockito.junit.jupiter.MockitoExtension;
45 import org.parosproxy.paros.core.proxy.ProxyParam;
46 import org.parosproxy.paros.network.HttpHeader;
47 import org.parosproxy.paros.network.HttpInputStream;
48 import org.parosproxy.paros.network.HttpMessage;
49 import org.parosproxy.paros.network.HttpOutputStream;
50 import org.parosproxy.paros.network.HttpRequestHeader;
51 import org.parosproxy.paros.view.View;
52 import org.w3c.dom.Document;
53 import org.w3c.dom.Element;
54 import org.zaproxy.zap.network.DomainMatcher;
55 import org.zaproxy.zap.utils.ZapXmlConfiguration;
56 
57 /** Unit test for {@link API}. */
58 @ExtendWith(MockitoExtension.class)
59 class APIUnitTest {
60 
61     private static final String CUSTOM_API_PATH = "/custom/api/";
62 
63     @Test
shouldBeEnabledWhenInDaemonMode()64     void shouldBeEnabledWhenInDaemonMode() {
65         // Given
66         API api = new API();
67         // When
68         View.setDaemon(true);
69         // Then
70         assertThat(api.isEnabled(), is(equalTo(true)));
71     }
72 
73     @Test
shouldAcceptCallbackIfNoAddressesSet()74     void shouldAcceptCallbackIfNoAddressesSet() throws Exception {
75         // Given
76         API api = new API();
77         api.setOptionsParamApi(createOptionsParamApi());
78         TestApiImplementor apiImplementor = new TestApiImplementor();
79         String requestUri = api.getCallBackUrl(apiImplementor, "http://example.com");
80         HttpMessage requestHandled =
81                 api.handleApiRequest(
82                         createApiRequest(new byte[] {127, 0, 0, 1}, "example.com", requestUri),
83                         createMockedHttpInputStream(),
84                         createMockedHttpOutputStream());
85         // Then
86         assertThat(requestHandled, is(notNullValue()));
87         assertThat(apiImplementor.wasUsed(), is(equalTo(true)));
88     }
89 
90     @Test
shouldAcceptCallbackEvenIfAddressNotSet()91     void shouldAcceptCallbackEvenIfAddressNotSet() throws Exception {
92         // Given
93         API api = new API();
94         OptionsParamApi apiOptions = createOptionsParamApi();
95         apiOptions.setPermittedAddresses(createPermittedAddresses("127.0.0.1"));
96         api.setOptionsParamApi(apiOptions);
97         TestApiImplementor apiImplementor = new TestApiImplementor();
98         String requestUri = api.getCallBackUrl(apiImplementor, "http://example.com");
99         HttpMessage requestHandled =
100                 api.handleApiRequest(
101                         createApiRequest(new byte[] {10, 0, 0, 2}, "example.com", requestUri),
102                         createMockedHttpInputStream(),
103                         createMockedHttpOutputStream());
104         // Then
105         assertThat(requestHandled, is(notNullValue()));
106         assertThat(apiImplementor.wasUsed(), is(equalTo(true)));
107     }
108 
109     @Test
shouldAcceptCallbackEvenIfHostnameNotSet()110     void shouldAcceptCallbackEvenIfHostnameNotSet() throws Exception {
111         // Given
112         API api = new API();
113         OptionsParamApi apiOptions = createOptionsParamApi();
114         apiOptions.setPermittedAddresses(createPermittedAddresses("127.0.0.1", "localhost"));
115         api.setOptionsParamApi(apiOptions);
116         TestApiImplementor apiImplementor = new TestApiImplementor();
117         String requestUri = api.getCallBackUrl(apiImplementor, "http://example.com");
118         HttpMessage requestHandled =
119                 api.handleApiRequest(
120                         createApiRequest(new byte[] {127, 0, 0, 1}, "example.com", requestUri),
121                         createMockedHttpInputStream(),
122                         createMockedHttpOutputStream());
123         // Then
124         assertThat(requestHandled, is(notNullValue()));
125         assertThat(apiImplementor.wasUsed(), is(equalTo(true)));
126     }
127 
128     @Test
shouldAcceptAddressAndHostnameSet()129     void shouldAcceptAddressAndHostnameSet() throws Exception {
130         // Given
131         API api = new API();
132         OptionsParamApi apiOptions = createOptionsParamApi();
133         apiOptions.setPermittedAddresses(createPermittedAddresses("10.0.0.8", "example.com"));
134         api.setOptionsParamApi(apiOptions);
135         TestApiImplementor apiImplementor = new TestApiImplementor();
136         String requestUri = api.getCallBackUrl(apiImplementor, "http://example.com");
137         HttpMessage requestHandled =
138                 api.handleApiRequest(
139                         createApiRequest(new byte[] {10, 0, 0, 8}, "example.com", requestUri),
140                         createMockedHttpInputStream(),
141                         createMockedHttpOutputStream());
142         // Then
143         assertThat(requestHandled, is(notNullValue()));
144         assertThat(apiImplementor.wasUsed(), is(equalTo(true)));
145     }
146 
147     @Test
shouldCreateBaseUrlWithHttpSchemeAndZapAddressIfProxyingAndNotSecure()148     void shouldCreateBaseUrlWithHttpSchemeAndZapAddressIfProxyingAndNotSecure() {
149         // Given
150         boolean proxying = true;
151         boolean secure = false;
152         API api = new API();
153         OptionsParamApi apiOptions = createOptionsParamApi();
154         apiOptions.setSecureOnly(secure);
155         api.setOptionsParamApi(apiOptions);
156         // When
157         String baseUrl = api.getBaseURL(proxying);
158         // Then
159         assertThat(baseUrl, is(equalTo("http://zap/")));
160     }
161 
162     @Test
shouldCreateBaseUrlWithHttpsSchemeAndZapAddressIfProxyingAndSecure()163     void shouldCreateBaseUrlWithHttpsSchemeAndZapAddressIfProxyingAndSecure() {
164         // Given
165         boolean proxying = true;
166         boolean secure = true;
167         API api = new API();
168         OptionsParamApi apiOptions = createOptionsParamApi();
169         apiOptions.setSecureOnly(secure);
170         api.setOptionsParamApi(apiOptions);
171         // When
172         String baseUrl = api.getBaseURL(proxying);
173         // Then
174         assertThat(baseUrl, is(equalTo("https://zap/")));
175     }
176 
177     @Test
shouldCreateBaseUrlWithHttpsSchemeAndProxyAddressIfNotProxyingAndNotSecure()178     void shouldCreateBaseUrlWithHttpsSchemeAndProxyAddressIfNotProxyingAndNotSecure() {
179         // Given
180         boolean proxying = false;
181         boolean secure = false;
182         API api = new API();
183         OptionsParamApi apiOptions = createOptionsParamApi();
184         apiOptions.setSecureOnly(secure);
185         api.setOptionsParamApi(apiOptions);
186         api.setProxyParam(createProxyParam("127.0.0.1", 8080));
187         // When
188         String baseUrl = api.getBaseURL(proxying);
189         // Then
190         assertThat(baseUrl, is(equalTo("http://127.0.0.1:8080/")));
191     }
192 
193     @Test
shouldCreateBaseUrlWithHttpsSchemeAndProxyAddressIfNotProxyingAndSecure()194     void shouldCreateBaseUrlWithHttpsSchemeAndProxyAddressIfNotProxyingAndSecure() {
195         // Given
196         boolean proxying = false;
197         boolean secure = true;
198         API api = new API();
199         OptionsParamApi apiOptions = createOptionsParamApi();
200         apiOptions.setSecureOnly(secure);
201         api.setOptionsParamApi(apiOptions);
202         api.setProxyParam(createProxyParam("127.0.0.1", 8080));
203         // When
204         String baseUrl = api.getBaseURL(proxying);
205         // Then
206         assertThat(baseUrl, is(equalTo("https://127.0.0.1:8080/")));
207     }
208 
209     @Test
210     void
shouldCreateBaseUrlWithApiRequestWithHttpSchemeZapAddressAndApiNonceIfProxyingNotSecureAndNotView()211             shouldCreateBaseUrlWithApiRequestWithHttpSchemeZapAddressAndApiNonceIfProxyingNotSecureAndNotView() {
212         // Given
213         boolean proxying = true;
214         boolean secure = false;
215         API api = new API();
216         OptionsParamApi apiOptions = createOptionsParamApi();
217         apiOptions.setSecureOnly(secure);
218         api.setOptionsParamApi(apiOptions);
219         // When
220         String baseUrl =
221                 api.getBaseURL(API.Format.JSON, "test", API.RequestType.action, "test", proxying);
222         // Then
223         assertThat(baseUrl, startsWith("http://zap/JSON/test/action/test/?apinonce="));
224         assertThat(baseUrl, endsWith("&"));
225         assertApiNonceMatch(api, baseUrl);
226     }
227 
228     @Test
229     void
shouldCreateBaseUrlWithApiRequestWithHttpsSchemeZapAddressAndApiNonceIfProxyingIsSecureAndNotView()230             shouldCreateBaseUrlWithApiRequestWithHttpsSchemeZapAddressAndApiNonceIfProxyingIsSecureAndNotView() {
231         // Given
232         boolean proxying = true;
233         boolean secure = true;
234         API api = new API();
235         OptionsParamApi apiOptions = createOptionsParamApi();
236         apiOptions.setSecureOnly(secure);
237         api.setOptionsParamApi(apiOptions);
238         // When
239         String baseUrl =
240                 api.getBaseURL(API.Format.JSON, "test", API.RequestType.action, "test", proxying);
241         // Then
242         assertThat(baseUrl, startsWith("https://zap/JSON/test/action/test/?apinonce="));
243         assertThat(baseUrl, endsWith("&"));
244         assertApiNonceMatch(api, baseUrl);
245     }
246 
247     @Test
248     void
shouldCreateBaseUrlWithApiRequestWithHttpSchemeProxyAddressAndApiNonceIfNotProxyingNotSecureAndNotView()249             shouldCreateBaseUrlWithApiRequestWithHttpSchemeProxyAddressAndApiNonceIfNotProxyingNotSecureAndNotView() {
250         // Given
251         boolean proxying = false;
252         boolean secure = false;
253         API api = new API();
254         OptionsParamApi apiOptions = createOptionsParamApi();
255         apiOptions.setSecureOnly(secure);
256         api.setOptionsParamApi(apiOptions);
257         api.setProxyParam(createProxyParam("127.0.0.1", 8080));
258         // When
259         String baseUrl =
260                 api.getBaseURL(API.Format.JSON, "test", API.RequestType.action, "test", proxying);
261         // Then
262         assertThat(baseUrl, startsWith("http://127.0.0.1:8080/JSON/test/action/test/?apinonce="));
263         assertThat(baseUrl, endsWith("&"));
264         assertApiNonceMatch(api, baseUrl);
265     }
266 
267     @Test
268     void
shouldCreateBaseUrlWithApiRequestWithHttpsSchemeProxyAddressAndApiNonceIfNotProxyingIsSecureAndNotView()269             shouldCreateBaseUrlWithApiRequestWithHttpsSchemeProxyAddressAndApiNonceIfNotProxyingIsSecureAndNotView() {
270         // Given
271         boolean proxying = false;
272         boolean secure = true;
273         API api = new API();
274         OptionsParamApi apiOptions = createOptionsParamApi();
275         apiOptions.setSecureOnly(secure);
276         api.setOptionsParamApi(apiOptions);
277         api.setProxyParam(createProxyParam("127.0.0.1", 8080));
278         // When
279         String baseUrl =
280                 api.getBaseURL(API.Format.JSON, "test", API.RequestType.action, "test", proxying);
281         // Then
282         assertThat(baseUrl, startsWith("https://127.0.0.1:8080/JSON/test/action/test/?apinonce="));
283         assertThat(baseUrl, endsWith("&"));
284         assertApiNonceMatch(api, baseUrl);
285     }
286 
287     @Test
288     void
shouldCreateBaseUrlWithApiRequestWithHttpSchemeZapAddressAndNoApiNonceIfProxyingNotSecureAndView()289             shouldCreateBaseUrlWithApiRequestWithHttpSchemeZapAddressAndNoApiNonceIfProxyingNotSecureAndView() {
290         // Given
291         boolean proxying = true;
292         boolean secure = false;
293         API api = new API();
294         OptionsParamApi apiOptions = createOptionsParamApi();
295         apiOptions.setSecureOnly(secure);
296         api.setOptionsParamApi(apiOptions);
297         // When
298         String baseUrl =
299                 api.getBaseURL(API.Format.JSON, "test", API.RequestType.view, "test", proxying);
300         // Then
301         assertThat(baseUrl, is(equalTo("http://zap/JSON/test/view/test/")));
302     }
303 
304     @Test
305     void
shouldCreateBaseUrlWithApiRequestWithHttpsSchemeZapAddressAndNoApiNonceIfProxyingIsSecureAndView()306             shouldCreateBaseUrlWithApiRequestWithHttpsSchemeZapAddressAndNoApiNonceIfProxyingIsSecureAndView() {
307         // Given
308         boolean proxying = true;
309         boolean secure = true;
310         API api = new API();
311         OptionsParamApi apiOptions = createOptionsParamApi();
312         apiOptions.setSecureOnly(secure);
313         api.setOptionsParamApi(apiOptions);
314         // When
315         String baseUrl =
316                 api.getBaseURL(API.Format.JSON, "test", API.RequestType.view, "test", proxying);
317         // Then
318         assertThat(baseUrl, is(equalTo("https://zap/JSON/test/view/test/")));
319     }
320 
321     @Test
322     void
shouldCreateBaseUrlWithApiRequestWithHttpSchemeProxyAddressAndNoApiNonceIfNotProxyingNotSecureAndView()323             shouldCreateBaseUrlWithApiRequestWithHttpSchemeProxyAddressAndNoApiNonceIfNotProxyingNotSecureAndView() {
324         // Given
325         boolean proxying = false;
326         boolean secure = false;
327         API api = new API();
328         OptionsParamApi apiOptions = createOptionsParamApi();
329         apiOptions.setSecureOnly(secure);
330         api.setOptionsParamApi(apiOptions);
331         api.setProxyParam(createProxyParam("127.0.0.1", 8080));
332         // When
333         String baseUrl =
334                 api.getBaseURL(API.Format.JSON, "test", API.RequestType.view, "test", proxying);
335         // Then
336         assertThat(baseUrl, is(equalTo("http://127.0.0.1:8080/JSON/test/view/test/")));
337     }
338 
339     @Test
340     void
shouldCreateBaseUrlWithApiRequestWithHttpsSchemeProxyAddressAndNoApiNonceIfNotProxyingIsSecureAndView()341             shouldCreateBaseUrlWithApiRequestWithHttpsSchemeProxyAddressAndNoApiNonceIfNotProxyingIsSecureAndView() {
342         // Given
343         boolean proxying = false;
344         boolean secure = true;
345         API api = new API();
346         OptionsParamApi apiOptions = createOptionsParamApi();
347         apiOptions.setSecureOnly(secure);
348         api.setOptionsParamApi(apiOptions);
349         api.setProxyParam(createProxyParam("127.0.0.1", 8080));
350         // When
351         String baseUrl =
352                 api.getBaseURL(API.Format.JSON, "test", API.RequestType.view, "test", proxying);
353         // Then
354         assertThat(baseUrl, is(equalTo("https://127.0.0.1:8080/JSON/test/view/test/")));
355     }
356 
357     @Test
shouldGenerateOneTimeApiNonce()358     void shouldGenerateOneTimeApiNonce() throws Exception {
359         // Given
360         API api = new API();
361         api.setOptionsParamApi(createOptionsParamApi());
362         // When
363         String nonce = api.getOneTimeNonce(CUSTOM_API_PATH);
364         // Then
365         assertThat(nonce, is(notNullValue()));
366         assertThat(isApiNonceValid(api, CUSTOM_API_PATH, nonce), is(equalTo(true)));
367         // Should not be valid more than once
368         assertThat(isApiNonceValid(api, CUSTOM_API_PATH, nonce), is(equalTo(false)));
369     }
370 
371     @Test
shouldConsumeButNotAcceptOneTimeApiNonceIfPathMismatch()372     void shouldConsumeButNotAcceptOneTimeApiNonceIfPathMismatch() throws Exception {
373         // Given
374         API api = new API();
375         api.setOptionsParamApi(createOptionsParamApi());
376         // When
377         String nonce = api.getOneTimeNonce(CUSTOM_API_PATH);
378         // Then
379         assertThat(nonce, is(notNullValue()));
380         assertThat(isApiNonceValid(api, "/not/same/path/", nonce), is(equalTo(false)));
381         assertThat(isApiNonceValid(api, CUSTOM_API_PATH, nonce), is(equalTo(false)));
382     }
383 
384     @Test
shouldNotAcceptOneTimeApiNonceIfExpired()385     void shouldNotAcceptOneTimeApiNonceIfExpired() throws Exception {
386         // Given
387         int ttlInSecs = 1;
388         OptionsParamApi optionsParamApi = Mockito.mock(OptionsParamApi.class);
389         when(optionsParamApi.getNonceTimeToLiveInSecs()).thenReturn(ttlInSecs);
390         API api = new API();
391         api.setOptionsParamApi(optionsParamApi);
392         String nonce = api.getOneTimeNonce(CUSTOM_API_PATH);
393         // When
394         Thread.sleep(TimeUnit.SECONDS.toMillis(ttlInSecs + 1));
395         // Then
396         assertThat(nonce, is(notNullValue()));
397         assertThat(isApiNonceValid(api, CUSTOM_API_PATH, nonce), is(equalTo(false)));
398     }
399 
400     @Test
shouldExpireExistingOneTimeApiNonces()401     void shouldExpireExistingOneTimeApiNonces() throws Exception {
402         // Given
403         int ttlInSecs = 1;
404         OptionsParamApi optionsParamApi = Mockito.mock(OptionsParamApi.class);
405         when(optionsParamApi.getNonceTimeToLiveInSecs()).thenReturn(ttlInSecs);
406         API api = new API();
407         api.setOptionsParamApi(optionsParamApi);
408         String nonce1 = api.getOneTimeNonce(CUSTOM_API_PATH + "1");
409         String nonce2 = api.getOneTimeNonce(CUSTOM_API_PATH + "2");
410         String nonce3 = api.getOneTimeNonce(CUSTOM_API_PATH + "2");
411         // When
412         Thread.sleep(TimeUnit.SECONDS.toMillis(ttlInSecs + 1));
413         // Then
414         assertThat(isApiNonceValid(api, CUSTOM_API_PATH + "1", nonce1), is(equalTo(false)));
415         assertThat(isApiNonceValid(api, CUSTOM_API_PATH + "2", nonce2), is(equalTo(false)));
416         assertThat(isApiNonceValid(api, CUSTOM_API_PATH + "3", nonce3), is(equalTo(false)));
417     }
418 
419     @Test
shouldGenerateLongLivedApiNonce()420     void shouldGenerateLongLivedApiNonce() {
421         // Given
422         API api = new API();
423         api.setOptionsParamApi(createOptionsParamApi());
424         // When
425         String nonce = api.getLongLivedNonce(CUSTOM_API_PATH);
426         // Then
427         assertThat(nonce, is(notNullValue()));
428         assertThat(isApiNonceValid(api, CUSTOM_API_PATH, nonce), is(equalTo(true)));
429         // Should be valid more than once
430         assertThat(isApiNonceValid(api, CUSTOM_API_PATH, nonce), is(equalTo(true)));
431     }
432 
433     @Test
shouldNotExpireLongLivedApiNonce()434     void shouldNotExpireLongLivedApiNonce() throws Exception {
435         // Given
436         int ttlInSecs = 1;
437         OptionsParamApi optionsParamApi = Mockito.mock(OptionsParamApi.class);
438         when(optionsParamApi.getNonceTimeToLiveInSecs()).thenReturn(ttlInSecs);
439         API api = new API();
440         api.setOptionsParamApi(optionsParamApi);
441         String nonce = api.getLongLivedNonce(CUSTOM_API_PATH);
442         // When
443         Thread.sleep(TimeUnit.SECONDS.toMillis(ttlInSecs + 1));
444         // Then
445         assertThat(isApiNonceValid(api, CUSTOM_API_PATH, nonce), is(equalTo(true)));
446     }
447 
448     @Test
shouldNotAcceptLongLivedApiNonceIfPathMismatch()449     void shouldNotAcceptLongLivedApiNonceIfPathMismatch() throws Exception {
450         // Given
451         API api = new API();
452         api.setOptionsParamApi(createOptionsParamApi());
453         // When
454         String nonce = api.getLongLivedNonce(CUSTOM_API_PATH);
455         // Then
456         assertThat(nonce, is(notNullValue()));
457         assertThat(isApiNonceValid(api, "/not/same/path/", nonce), is(equalTo(false)));
458         assertThat(isApiNonceValid(api, CUSTOM_API_PATH, nonce), is(equalTo(true)));
459     }
460 
461     @Test
shouldNotHandleShortcutIfNotForcedRequest()462     void shouldNotHandleShortcutIfNotForcedRequest() throws Exception {
463         // Given
464         API api = createApiWithoutKey();
465         TestApiImplementor apiImplementor = new TestApiImplementor();
466         String shortcut = "shortcut";
467         apiImplementor.addApiShortcut(shortcut);
468         api.registerApiImplementor(apiImplementor);
469         String requestUri = "/" + shortcut;
470         boolean forced = false;
471         // When
472         HttpMessage requestHandled =
473                 api.handleApiRequest(
474                         createApiRequest(new byte[] {127, 0, 0, 1}, "localhost", requestUri),
475                         createMockedHttpInputStream(),
476                         createMockedHttpOutputStream(),
477                         forced);
478         // Then
479         assertThat(requestHandled, is(nullValue()));
480         assertThat(apiImplementor.wasUsed(), is(equalTo(false)));
481     }
482 
483     @Test
shouldHandleShortcutIfForcedRequest()484     void shouldHandleShortcutIfForcedRequest() throws Exception {
485         // Given
486         API api = createApiWithoutKey();
487         TestApiImplementor apiImplementor = new TestApiImplementor();
488         String shortcut = "shortcut";
489         apiImplementor.addApiShortcut(shortcut);
490         api.registerApiImplementor(apiImplementor);
491         String requestUri = "/" + shortcut;
492         boolean forced = true;
493         // When
494         HttpMessage requestHandled =
495                 api.handleApiRequest(
496                         createApiRequest(new byte[] {127, 0, 0, 1}, "localhost", requestUri),
497                         createMockedHttpInputStream(),
498                         createMockedHttpOutputStream(),
499                         forced);
500         // Then
501         assertThat(requestHandled, is(notNullValue()));
502         assertThat(apiImplementor.wasUsed(), is(equalTo(true)));
503     }
504 
505     @Test
shouldHandleShortcutIfZapRequest()506     void shouldHandleShortcutIfZapRequest() throws Exception {
507         // Given
508         API api = createApiWithoutKey();
509         TestApiImplementor apiImplementor = new TestApiImplementor();
510         String shortcut = "shortcut";
511         apiImplementor.addApiShortcut(shortcut);
512         api.registerApiImplementor(apiImplementor);
513         String requestUri = "/" + shortcut;
514         // When
515         HttpMessage requestHandled =
516                 api.handleApiRequest(
517                         createApiRequest(new byte[] {127, 0, 0, 1}, API.API_DOMAIN, requestUri),
518                         createMockedHttpInputStream(),
519                         createMockedHttpOutputStream());
520         // Then
521         assertThat(requestHandled, is(notNullValue()));
522         assertThat(apiImplementor.wasUsed(), is(equalTo(true)));
523     }
524 
525     @Test
shouldHandleShortcutIfSecureZapRequest()526     void shouldHandleShortcutIfSecureZapRequest() throws Exception {
527         // Given
528         API api = createApiWithoutKey();
529         TestApiImplementor apiImplementor = new TestApiImplementor();
530         String shortcut = "shortcut";
531         apiImplementor.addApiShortcut(shortcut);
532         api.registerApiImplementor(apiImplementor);
533         String requestUri = "/" + shortcut;
534         HttpRequestHeader apiRequest =
535                 createApiRequest(new byte[] {127, 0, 0, 1}, API.API_DOMAIN, requestUri);
536         apiRequest.setSecure(true);
537         // When
538         HttpMessage requestHandled =
539                 api.handleApiRequest(
540                         apiRequest, createMockedHttpInputStream(), createMockedHttpOutputStream());
541         // Then
542         assertThat(requestHandled, is(notNullValue()));
543         assertThat(apiImplementor.wasUsed(), is(equalTo(true)));
544     }
545 
546     @Test
shouldFailToGetXmlFromResponseWithNullEndpointName()547     void shouldFailToGetXmlFromResponseWithNullEndpointName() throws ApiException {
548         // Given
549         String endpointName = null;
550         ApiResponse response = ApiResponseTest.INSTANCE;
551         // When
552         ApiException e =
553                 assertThrows(ApiException.class, () -> API.responseToXml(endpointName, response));
554         // Then
555         assertThat(e.getMessage(), containsString("internal_error"));
556     }
557 
558     @Test
shouldFailToGetXmlFromResponseWithEmptyEndpointName()559     void shouldFailToGetXmlFromResponseWithEmptyEndpointName() throws ApiException {
560         // Given
561         String endpointName = "";
562         ApiResponse response = ApiResponseTest.INSTANCE;
563         // When
564         ApiException e =
565                 assertThrows(ApiException.class, () -> API.responseToXml(endpointName, response));
566         // Then
567         assertThat(e.getMessage(), containsString("internal_error"));
568     }
569 
570     @Test
shouldFailToGetXmlFromResponseWithInvalidXmlEndpointName()571     void shouldFailToGetXmlFromResponseWithInvalidXmlEndpointName() throws ApiException {
572         // Given
573         String endpointName = "<";
574         ApiResponse response = ApiResponseTest.INSTANCE;
575         // When
576         ApiException e =
577                 assertThrows(ApiException.class, () -> API.responseToXml(endpointName, response));
578         // Then
579         assertThat(e.getMessage(), containsString("internal_error"));
580     }
581 
582     @Test
shouldFailToGetXmlFromNullResponse()583     void shouldFailToGetXmlFromNullResponse() throws ApiException {
584         // Given
585         String endpointName = "Name";
586         ApiResponse response = null;
587         // When
588         ApiException e =
589                 assertThrows(ApiException.class, () -> API.responseToXml(endpointName, response));
590         // Then
591         assertThat(e.getMessage(), containsString("internal_error"));
592     }
593 
594     @Test
shouldGetXmlFromResponse()595     void shouldGetXmlFromResponse() throws ApiException {
596         // Given
597         String endpointName = "Name";
598         ApiResponse response = ApiResponseTest.INSTANCE;
599         // When
600         String xmlResponse = API.responseToXml(endpointName, response);
601         // Then
602         assertThat(
603                 xmlResponse,
604                 is(
605                         equalTo(
606                                 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><Name>XML</Name>")));
607     }
608 
609     @Test
shouldGetHtmlFromResponse()610     void shouldGetHtmlFromResponse() {
611         // Given
612         ApiResponse response = ApiResponseTest.INSTANCE;
613         // When
614         String htmlResponse = API.responseToHtml(response);
615         // Then
616         assertThat(htmlResponse, is(equalTo("<head>\n</head>\n<body>\nHTML</body>\n")));
617     }
618 
619     private static class ApiResponseTest extends ApiResponse {
620 
621         private static final ApiResponseTest INSTANCE = new ApiResponseTest("");
622 
ApiResponseTest(String name)623         ApiResponseTest(String name) {
624             super(name);
625         }
626 
627         @Override
toJSON()628         public JSON toJSON() {
629             return null;
630         }
631 
632         @Override
toXML(Document doc, Element rootElement)633         public void toXML(Document doc, Element rootElement) {
634             rootElement.appendChild(doc.createTextNode("XML"));
635         }
636 
637         @Override
toHTML(StringBuilder sb)638         public void toHTML(StringBuilder sb) {
639             sb.append("HTML");
640         }
641 
642         @Override
toString(int indent)643         public String toString(int indent) {
644             return null;
645         }
646     }
647 
assertApiNonceMatch(API api, String baseUrl)648     private static void assertApiNonceMatch(API api, String baseUrl) {
649         try {
650             // Given
651             URI uri = new URI(baseUrl, true);
652             String hostHeader = uri.getHost() + (uri.getPort() != -1 ? ":" + uri.getPort() : "");
653             TestApiImplementor apiImplementor = new TestApiImplementor();
654             api.registerApiImplementor(apiImplementor);
655             // When
656             HttpMessage requestHandled =
657                     api.handleApiRequest(
658                             createApiRequest(new byte[] {127, 0, 0, 1}, hostHeader, baseUrl),
659                             createMockedHttpInputStream(),
660                             createMockedHttpOutputStream(),
661                             true);
662             // Then
663             assertThat(requestHandled, is(notNullValue()));
664             assertThat(apiImplementor.wasUsed(), is(equalTo(true)));
665         } catch (Exception e) {
666             throw new RuntimeException(e);
667         }
668     }
669 
isApiNonceValid(API api, String requestPath, String nonce)670     private static boolean isApiNonceValid(API api, String requestPath, String nonce) {
671         try {
672             URI requestUri = Mockito.mock(URI.class);
673             when(requestUri.getPath()).thenReturn(requestPath);
674             HttpRequestHeader request =
675                     Mockito.mock(HttpRequestHeader.class, withSettings().lenient());
676             when(request.getURI()).thenReturn(requestUri);
677             when(request.getHeader(HttpHeader.X_ZAP_API_NONCE)).thenReturn(nonce);
678             when(request.getSenderAddress())
679                     .thenReturn(Inet4Address.getByAddress(new byte[] {127, 0, 0, 1}));
680             return api.hasValidKey(request, new JSONObject());
681         } catch (Exception e) {
682             throw new RuntimeException(e);
683         }
684     }
685 
createPermittedAddresses(String... addresses)686     private static List<DomainMatcher> createPermittedAddresses(String... addresses) {
687         if (addresses == null || addresses.length == 0) {
688             return new ArrayList<>();
689         }
690 
691         List<DomainMatcher> permittedAddresses = new ArrayList<>();
692         for (String address : addresses) {
693             permittedAddresses.add(new DomainMatcher(address));
694         }
695         return permittedAddresses;
696     }
697 
createApiRequest( byte[] remoteAddress, String hostname, String requestUri)698     private static HttpRequestHeader createApiRequest(
699             byte[] remoteAddress, String hostname, String requestUri) throws Exception {
700         HttpRequestHeader httpRequestHeader =
701                 new HttpRequestHeader(
702                         "GET " + requestUri + " HTTP/1.1\r\n" + "Host: " + hostname + "\r\n");
703         httpRequestHeader.setSenderAddress(Inet4Address.getByAddress(remoteAddress));
704         return httpRequestHeader;
705     }
706 
createOptionsParamApi()707     private static OptionsParamApi createOptionsParamApi() {
708         OptionsParamApi optionsParamApi = new OptionsParamApi();
709         optionsParamApi.load(new ZapXmlConfiguration());
710         return optionsParamApi;
711     }
712 
createProxyParam(String proxyAddress, int proxyPort)713     private static ProxyParam createProxyParam(String proxyAddress, int proxyPort) {
714         ProxyParam proxyParam = new ProxyParam();
715         proxyParam.load(new ZapXmlConfiguration());
716         proxyParam.setProxyIp(proxyAddress);
717         proxyParam.setProxyPort(proxyPort);
718         return proxyParam;
719     }
720 
createMockedHttpInputStream()721     private static HttpInputStream createMockedHttpInputStream() {
722         return Mockito.mock(HttpInputStream.class);
723     }
724 
createMockedHttpOutputStream()725     private static HttpOutputStream createMockedHttpOutputStream() {
726         return Mockito.mock(HttpOutputStream.class);
727     }
728 
createApiWithoutKey()729     private static API createApiWithoutKey() {
730         OptionsParamApi options = createOptionsParamApi();
731         options.setDisableKey(true);
732         API api = new API();
733         api.setOptionsParamApi(options);
734         return api;
735     }
736 
737     private static class TestApiImplementor extends ApiImplementor {
738 
739         static final String PREFIX = "test";
740 
741         private boolean used;
742 
743         @Override
getPrefix()744         public String getPrefix() {
745             return PREFIX;
746         }
747 
748         @Override
handleApiView(String name, JSONObject params)749         public ApiResponse handleApiView(String name, JSONObject params) throws ApiException {
750             used = true;
751             return new ApiResponseElement(name, "value");
752         }
753 
754         @Override
handleApiAction(String name, JSONObject params)755         public ApiResponse handleApiAction(String name, JSONObject params) throws ApiException {
756             used = true;
757             return new ApiResponseElement(name, "value");
758         }
759 
760         @Override
handleApiOther(HttpMessage msg, String name, JSONObject params)761         public HttpMessage handleApiOther(HttpMessage msg, String name, JSONObject params)
762                 throws ApiException {
763             used = true;
764             return new HttpMessage();
765         }
766 
767         @Override
handleCallBack(HttpMessage msg)768         public String handleCallBack(HttpMessage msg) throws ApiException {
769             used = true;
770             return "response";
771         }
772 
773         @Override
handleApiOptionView(String name, JSONObject params)774         public ApiResponse handleApiOptionView(String name, JSONObject params) throws ApiException {
775             used = true;
776             return new ApiResponseElement(name, "value");
777         }
778 
779         @Override
handleApiOptionAction(String name, JSONObject params)780         public ApiResponse handleApiOptionAction(String name, JSONObject params)
781                 throws ApiException {
782             used = true;
783             return new ApiResponseElement(name, "value");
784         }
785 
786         @Override
handleShortcut(HttpMessage msg)787         public HttpMessage handleShortcut(HttpMessage msg) throws ApiException {
788             used = true;
789             return new HttpMessage();
790         }
791 
wasUsed()792         boolean wasUsed() {
793             return used;
794         }
795     }
796 }
797