HttpServer.java
/*
* Copyright (c) 2016-2023 AsyncHttpClient Project. All rights reserved.
*
* 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.asynchttpclient.testserver;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import java.io.Closeable;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentLinkedQueue;
import static io.netty.handler.codec.http.HttpHeaderNames.LOCATION;
import static org.asynchttpclient.test.TestUtils.TEXT_HTML_CONTENT_TYPE_WITH_ISO_8859_1_CHARSET;
import static org.asynchttpclient.test.TestUtils.TEXT_HTML_CONTENT_TYPE_WITH_UTF_8_CHARSET;
import static org.asynchttpclient.test.TestUtils.addHttpConnector;
import static org.asynchttpclient.test.TestUtils.addHttpsConnector;
public class HttpServer implements Closeable {
private final ConcurrentLinkedQueue<Handler> handlers = new ConcurrentLinkedQueue<>();
private int httpPort;
private int httpsPort;
private Server server;
public HttpServer() {
}
public HttpServer(int httpPort, int httpsPort) {
this.httpPort = httpPort;
this.httpsPort = httpsPort;
}
public void start() throws Exception {
server = new Server();
ServerConnector httpConnector = addHttpConnector(server);
if (httpPort != 0) {
httpConnector.setPort(httpPort);
}
server.setHandler(new QueueHandler());
ServerConnector httpsConnector = addHttpsConnector(server);
if (httpsPort != 0) {
httpsConnector.setPort(httpsPort);
}
server.start();
httpPort = httpConnector.getLocalPort();
httpsPort = httpsConnector.getLocalPort();
}
public void enqueue(Handler handler) {
handlers.offer(handler);
}
public void enqueueOk() {
enqueueResponse(response -> response.setStatus(200));
}
public void enqueueResponse(HttpServletResponseConsumer c) {
handlers.offer(new ConsumerHandler(c));
}
public void enqueueEcho() {
handlers.offer(new EchoHandler());
}
public void enqueueRedirect(int status, String location) {
enqueueResponse(response -> {
response.setStatus(status);
response.setHeader(LOCATION.toString(), location);
});
}
public int getHttpPort() {
return httpPort;
}
public int getsHttpPort() {
return httpsPort;
}
public String getHttpUrl() {
return "http://localhost:" + httpPort;
}
public String getHttpsUrl() {
return "https://localhost:" + httpsPort;
}
public void reset() {
handlers.clear();
}
@Override
public void close() throws IOException {
if (server != null) {
try {
server.stop();
} catch (Exception e) {
throw new IOException(e);
}
}
}
@FunctionalInterface
public interface HttpServletResponseConsumer {
void apply(HttpServletResponse response) throws IOException, ServletException;
}
public abstract static class AutoFlushHandler extends AbstractHandler {
private final boolean closeAfterResponse;
AutoFlushHandler() {
this(false);
}
AutoFlushHandler(boolean closeAfterResponse) {
this.closeAfterResponse = closeAfterResponse;
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
handle0(target, baseRequest, request, response);
response.getOutputStream().flush();
if (closeAfterResponse) {
response.getOutputStream().close();
}
}
protected abstract void handle0(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException;
}
private static class ConsumerHandler extends AutoFlushHandler {
private final HttpServletResponseConsumer c;
ConsumerHandler(HttpServletResponseConsumer c) {
this(c, false);
}
ConsumerHandler(HttpServletResponseConsumer c, boolean closeAfterResponse) {
super(closeAfterResponse);
this.c = c;
}
@Override
protected void handle0(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
c.apply(response);
}
}
public static class EchoHandler extends AutoFlushHandler {
@Override
protected void handle0(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String delay = request.getHeader("X-Delay");
if (delay != null) {
try {
Thread.sleep(Long.parseLong(delay));
} catch (NumberFormatException | InterruptedException e1) {
throw new ServletException(e1);
}
}
response.setStatus(200);
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.addHeader("Allow", "GET,HEAD,POST,OPTIONS,TRACE");
}
response.setContentType(request.getHeader("X-IsoCharset") != null ? TEXT_HTML_CONTENT_TYPE_WITH_ISO_8859_1_CHARSET : TEXT_HTML_CONTENT_TYPE_WITH_UTF_8_CHARSET);
response.addHeader("X-ClientPort", String.valueOf(request.getRemotePort()));
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
response.addHeader("X-PathInfo", pathInfo);
}
String queryString = request.getQueryString();
if (queryString != null) {
response.addHeader("X-QueryString", queryString);
}
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
response.addHeader("X-" + headerName, request.getHeader(headerName));
}
StringBuilder requestBody = new StringBuilder();
for (Entry<String, String[]> e : baseRequest.getParameterMap().entrySet()) {
response.addHeader("X-" + e.getKey(), URLEncoder.encode(e.getValue()[0], StandardCharsets.UTF_8));
}
Cookie[] cs = request.getCookies();
if (cs != null) {
for (Cookie c : cs) {
response.addCookie(c);
}
}
if (requestBody.length() > 0) {
response.getOutputStream().write(requestBody.toString().getBytes());
}
int size = 16384;
if (request.getContentLength() > 0) {
size = request.getContentLength();
}
if (size > 0) {
int read = 0;
while (read > -1) {
byte[] bytes = new byte[size];
read = request.getInputStream().read(bytes);
if (read > 0) {
response.getOutputStream().write(bytes, 0, read);
}
}
}
}
}
private class QueueHandler extends AbstractHandler {
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
Handler handler = handlers.poll();
if (handler == null) {
response.sendError(500, "No handler enqueued");
response.getOutputStream().flush();
response.getOutputStream().close();
} else {
handler.handle(target, baseRequest, request, response);
}
}
}
}