Skip to content

Commit

Permalink
Merge pull request #4926 from thc202/network/api-custom-servers
Browse files Browse the repository at this point in the history
Change AJAX Spider to access the ZAP API
  • Loading branch information
psiinon authored Sep 25, 2023
2 parents 9de742a + 9c28732 commit c77d562
Show file tree
Hide file tree
Showing 10 changed files with 416 additions and 27 deletions.
3 changes: 3 additions & 0 deletions addOns/network/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- Allow to create custom servers with the ZAP API.

### Changed
- Maintenance changes.
- Update names of generated root CA certificate and issued server certificates.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
Expand Down Expand Up @@ -114,9 +115,11 @@
import org.zaproxy.addon.network.internal.server.http.handlers.LegacyNoCacheRequestHandler;
import org.zaproxy.addon.network.internal.server.http.handlers.LegacyProxyListenerHandler;
import org.zaproxy.addon.network.internal.server.http.handlers.RemoveAcceptEncodingHandler;
import org.zaproxy.addon.network.internal.server.http.handlers.ZapApiHandler;
import org.zaproxy.addon.network.internal.ui.LocalServerInfoLabel;
import org.zaproxy.addon.network.internal.ui.PromptHttpProxyPasswordDialog;
import org.zaproxy.addon.network.server.HttpMessageHandler;
import org.zaproxy.addon.network.server.HttpServerConfig;
import org.zaproxy.addon.network.server.Server;
import org.zaproxy.addon.network.server.ServerInfo;
import org.zaproxy.zap.ZAP;
Expand Down Expand Up @@ -387,20 +390,11 @@ private void shutdownEventGroups() {
* @return the server.
* @throws NullPointerException if the given handler is {@code null}.
* @since 0.1.0
* @see #createHttpServer(HttpServerConfig)
*/
public Server createHttpServer(HttpMessageHandler handler) {
Objects.requireNonNull(handler);
List<HttpMessageHandler> handlers =
Arrays.asList(ConnectReceivedHandler.getSetAndOverrideInstance(), handler);
return createHttpServer(() -> new MainServerHandler(blockingServerExecutor, handlers));
}

private Server createHttpServer(Supplier<MainServerHandler> handler) {
return new HttpServer(
getMainEventLoopGroup(),
getMainEventExecutorGroup(),
serverCertificateService,
handler);
return createHttpServer(HttpServerConfig.builder().setHttpMessageHandler(handler).build());
}

/**
Expand All @@ -424,11 +418,15 @@ public ServerInfo getMainProxyServerInfo() {
* @return the server.
* @throws NullPointerException if the given handler is {@code null}.
* @since 0.1.0
* @see #createHttpServer(HttpServerConfig)
*/
public Server createHttpProxy(int initiator, HttpMessageHandler handler) {
Objects.requireNonNull(handler);
HttpSender httpSender = new HttpSender(initiator);
return createHttpProxy(httpSender, handler);
return createHttpServer(
HttpServerConfig.builder()
.setHttpSender(new HttpSender(initiator))
.setHttpMessageHandler(handler)
.build());
}

/**
Expand All @@ -442,22 +440,67 @@ public Server createHttpProxy(int initiator, HttpMessageHandler handler) {
* @return the server.
* @throws NullPointerException if the HTTP sender and given handler are {@code null}.
* @since 0.1.0
* @see #createHttpServer(HttpServerConfig)
*/
public Server createHttpProxy(HttpSender httpSender, HttpMessageHandler handler) {
Objects.requireNonNull(handler);
Objects.requireNonNull(httpSender);
List<HttpMessageHandler> handlers =
Arrays.asList(
ConnectReceivedHandler.getSetAndOverrideInstance(),
RemoveAcceptEncodingHandler.getEnabledInstance(),
DecodeResponseHandler.getEnabledInstance(),
handler,
CloseOnRecursiveRequestHandler.getInstance(),
new HttpSenderHandler(httpSender));
return createHttpServer(
() ->
new MainProxyHandler(
blockingServerExecutor, legacyProxyListenerHandler, handlers));
HttpServerConfig.builder()
.setHttpSender(httpSender)
.setHttpMessageHandler(handler)
.build());
}

/**
* Creates an HTTP server with the given configuration.
*
* <p>The CONNECT requests are automatically handled as is the possible TLS upgrade.
*
* <p>A configuration with an {@link HttpSender} creates a proxy. The connection is
* automatically closed on recursive requests.
*
* @param config the server configuration.
* @return the server.
* @throws NullPointerException if the given config is {@code null}.
* @since 0.11.0
*/
public Server createHttpServer(HttpServerConfig config) {
Objects.requireNonNull(config);

Supplier<MainServerHandler> mainServerHandler;
boolean addApiHandler = config.isServeZapApi();
HttpSender httpSender = config.getHttpSender();
if (httpSender != null) {
List<HttpMessageHandler> handlers = new ArrayList<>(addApiHandler ? 7 : 6);
handlers.add(ConnectReceivedHandler.getSetAndOverrideInstance());
handlers.add(RemoveAcceptEncodingHandler.getEnabledInstance());
handlers.add(DecodeResponseHandler.getEnabledInstance());
if (addApiHandler) {
handlers.add(ZapApiHandler.getEnabledInstance());
}
handlers.add(config.getHttpMessageHandler());
handlers.add(CloseOnRecursiveRequestHandler.getInstance());
handlers.add(new HttpSenderHandler(httpSender));
mainServerHandler =
() ->
new MainProxyHandler(
blockingServerExecutor, legacyProxyListenerHandler, handlers);
} else {
List<HttpMessageHandler> handlers = new ArrayList<>(addApiHandler ? 3 : 2);
handlers.add(ConnectReceivedHandler.getSetAndOverrideInstance());
if (addApiHandler) {
handlers.add(ZapApiHandler.getEnabledInstance());
}
handlers.add(config.getHttpMessageHandler());
mainServerHandler = () -> new MainServerHandler(blockingServerExecutor, handlers);
}

return new HttpServer(
getMainEventLoopGroup(),
getMainEventExecutorGroup(),
serverCertificateService,
mainServerHandler);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ public class ZapApiHandler implements HttpMessageHandler {

private static final Logger LOGGER = LogManager.getLogger(ZapApiHandler.class);

private static final ZapApiHandler ALWAYS_ENABLED = new ZapApiHandler(() -> true);

/**
* Gets the handler that always handles API requests.
*
* @return the handler, never {@code null}.
*/
public static ZapApiHandler getEnabledInstance() {
return ALWAYS_ENABLED;
}

private HandlerState state;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2023 The ZAP Development Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.zaproxy.addon.network.server;

import java.util.Objects;
import org.parosproxy.paros.network.HttpSender;

/**
* The configuration for an HTTP server.
*
* @since 0.11.0
* @see #builder()
*/
public class HttpServerConfig {

private final HttpMessageHandler httpMessageHandler;
private final HttpSender httpSender;
private final boolean serveZapApi;

private HttpServerConfig(
HttpMessageHandler httpMessageHandler, HttpSender httpSender, boolean serveZapApi) {
this.httpMessageHandler = httpMessageHandler;
this.httpSender = httpSender;
this.serveZapApi = serveZapApi;
}

public HttpMessageHandler getHttpMessageHandler() {
return httpMessageHandler;
}

public HttpSender getHttpSender() {
return httpSender;
}

public boolean isServeZapApi() {
return serveZapApi;
}

/**
* Creates a builder of {@link HttpServerConfig}.
*
* @return a new builder.
*/
public static Builder builder() {
return new Builder();
}

/**
* A builder of {@link HttpServerConfig}.
*
* @see #build()
*/
public static class Builder {

private HttpMessageHandler httpMessageHandler;

private HttpSender httpSender;

private boolean serveZapApi;

/**
* Sets the HTTP message handler.
*
* @param httpMessageHandler the HTTP message handler, must not be {@code null}.
* @throws NullPointerException if the given {@code httpMessageHandler} is {@code null}.
* @return the builder for chaining.
*/
public Builder setHttpMessageHandler(HttpMessageHandler httpMessageHandler) {
this.httpMessageHandler = Objects.requireNonNull(httpMessageHandler);
return this;
}

/**
* Sets the HTTP sender, which will make the server act as a proxy.
*
* @param httpSender the HTTP sender.
* @return the builder for chaining.
*/
public Builder setHttpSender(HttpSender httpSender) {
this.httpSender = httpSender;
return this;
}

/**
* Sets whether or not the API should be served.
*
* @param serveZapApi {@code true} if the API should be served, {@code false} otherwise.
* @return the builder for chaining.
*/
public Builder setServeZapApi(boolean serveZapApi) {
this.serveZapApi = serveZapApi;
return this;
}

/**
* Builds the {@link HttpServerConfig} with properties set.
*
* @return the configuration.
* @throws IllegalStateException if any of the required properties were not set.
*/
public HttpServerConfig build() {
if (httpMessageHandler == null) {
throw new IllegalStateException("The httpMessageHandler was not set.");
}

return new HttpServerConfig(httpMessageHandler, httpSender, serveZapApi);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.apache.commons.httpclient.HttpState;
Expand Down Expand Up @@ -104,6 +105,7 @@
import org.zaproxy.addon.network.internal.server.http.PassThrough;
import org.zaproxy.addon.network.internal.server.http.handlers.LegacyProxyListenerHandler;
import org.zaproxy.addon.network.server.HttpMessageHandler;
import org.zaproxy.addon.network.server.HttpServerConfig;
import org.zaproxy.addon.network.server.Server;
import org.zaproxy.addon.network.server.ServerInfo;
import org.zaproxy.addon.network.testutils.TestClient;
Expand All @@ -112,6 +114,7 @@
import org.zaproxy.zap.extension.api.API;
import org.zaproxy.zap.extension.api.ApiElement;
import org.zaproxy.zap.extension.api.CoreAPI;
import org.zaproxy.zap.extension.api.OptionsParamApi;
import org.zaproxy.zap.utils.ZapXmlConfiguration;

/** Unit test for {@link ExtensionNetwork}. */
Expand Down Expand Up @@ -1089,6 +1092,76 @@ void shouldThrowIfCreatingHttpServerWithNullHandler() throws Exception {
assertThrows(NullPointerException.class, () -> extension.createHttpServer(handler));
}

@Test
void shouldThrowIfCreatingHttpServerWithNullConfig() throws Exception {
// Given
extension.hook(mock(ExtensionHook.class));
HttpServerConfig config = null;
// When / Then
assertThrows(NullPointerException.class, () -> extension.createHttpServer(config));
}

@Test
void shouldCreateHttpServerWithZapApi() throws Exception {
// Given
AtomicBoolean requestReachedHandler = new AtomicBoolean();
HttpMessageHandler handler = (ctx, msg) -> requestReachedHandler.set(true);
extension.hook(mock(ExtensionHook.class));
HttpMessage msg = new HttpMessage(new HttpRequestHeader("GET http://zap/ HTTP/1.1"));
TestClient client =
new TextTestClient(
"127.0.0.1",
ch -> ch.pipeline().addFirst("http.client", new HttpClientCodec()));
HttpServerConfig config =
HttpServerConfig.builder()
.setHttpMessageHandler(handler)
.setServeZapApi(true)
.build();
given(optionsParam.getApiParam()).willReturn(mock(OptionsParamApi.class));
// When
try (Server server = extension.createHttpServer(config)) {
int port = server.start(Server.ANY_PORT);
Channel channel = client.connect(port, null);
channel.writeAndFlush(msg).sync();
TextTestClient.waitForResponse(channel);
// Then
assertThat(requestReachedHandler.get(), is(equalTo(false)));
} finally {
client.close();
}
}

@Test
void shouldCreateHttpProxyWithZapApi() throws Exception {
// Given
AtomicBoolean requestReachedHandler = new AtomicBoolean();
HttpMessageHandler handler = (ctx, msg) -> requestReachedHandler.set(true);
extension.hook(mock(ExtensionHook.class));
HttpMessage msg = new HttpMessage(new HttpRequestHeader("GET http://zap/ HTTP/1.1"));
TestClient client =
new TextTestClient(
"127.0.0.1",
ch -> ch.pipeline().addFirst("http.client", new HttpClientCodec()));
HttpServerConfig config =
HttpServerConfig.builder()
.setHttpMessageHandler(handler)
.setHttpSender(new HttpSender(1))
.setServeZapApi(true)
.build();
given(optionsParam.getApiParam()).willReturn(mock(OptionsParamApi.class));
// When
try (Server server = extension.createHttpServer(config)) {
int port = server.start(Server.ANY_PORT);
Channel channel = client.connect(port, null);
channel.writeAndFlush(msg).sync();
TextTestClient.waitForResponse(channel);
// Then
assertThat(requestReachedHandler.get(), is(equalTo(false)));
} finally {
client.close();
}
}

@Test
void shouldGetProxyPacContent() {
// Given
Expand Down
Loading

0 comments on commit c77d562

Please sign in to comment.