/* * Copyright 2017 JBoss Inc * * 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 io.apiman.test.common.echo; import io.apiman.common.util.SimpleStringUtils; import io.apiman.gateway.engine.beans.EngineErrorResponse; import io.apiman.test.common.mock.EchoResponse; import io.vertx.core.AbstractVerticle; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.streams.WriteStream; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import org.apache.commons.lang3.math.NumberUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; /** * <p> * Vert.x edition of Echo servlet, with view to being more amenable * to performance testing. * </p> * <p> * Can be run directly with: <tt>vertx run EchoServerVertx.java</tt> * </p> * <p> * To set port, use the property -Dio.apiman.test.common.echo.port=1234 * </p> * * @author Marc Savy {@literal <marc@rhymewithgravy.com>} */ @SuppressWarnings("nls") public class EchoServerVertx extends AbstractVerticle { private static ObjectMapper jsonMapper = new ObjectMapper(); static { jsonMapper.enable(SerializationFeature.INDENT_OUTPUT); } private static JAXBContext jaxbContext; static { try { jaxbContext = JAXBContext.newInstance(EngineErrorResponse.class, EchoResponse.class); } catch (JAXBException e) { throw new RuntimeException(e); } } private long counter = 0L; @Override public void start(Future<Void> startFuture) { int port = NumberUtils.toInt(System.getProperty("io.apiman.test.common.echo.port"), 9999); vertx.createHttpServer() .requestHandler(new EchoHandler()) .listen(port, result -> { if (result.succeeded()) { startFuture.complete(); } else { startFuture.fail(result.cause()); } }); System.out.println("*** Starting EchoServerVertx on: " + port); } // Writes buffered chunks directly to the response and then calls #end. private static void writeXmlAndEnd(HttpServerResponse rep, EchoResponse echo) { try (BufferOutputStream bufferOutputStream = new BufferOutputStream(500, rep)) { Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); jaxbMarshaller.marshal(echo, bufferOutputStream); } catch (JAXBException e) { e.printStackTrace(); throw new RuntimeException(e); } } // Writes buffered chunks directly to the response and then calls #end. private static void writeJsonAndEnd(HttpServerResponse rep, EchoResponse echo) { try (BufferOutputStream bufferOutputStream = new BufferOutputStream(500, rep)) { jsonMapper.writeValue(bufferOutputStream, echo); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } private final class EchoHandler implements Handler<HttpServerRequest> { private long bodyLength = 0L; private MessageDigest sha1; public EchoHandler() { try { sha1 = MessageDigest.getInstance("SHA1"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } @Override public void handle(HttpServerRequest req) { try { _handle(req); } catch (Exception e) { handleError(req.response(), e); } } private void _handle(HttpServerRequest req) { HttpServerResponse rep = req.response(); if (req.headers().contains("X-Echo-ErrorCode")) { // Check if number, if not then set to 400. int errorCode = Optional.of(req.getHeader("X-Echo-ErrorCode")) .filter(NumberUtils::isNumber) .map(Integer::valueOf) .orElse(400); // Get error message, else set to "" to avoid NPE. String statusMsg = Optional.ofNullable(req.getHeader("X-Echo-ErrorMessage")) .orElse(""); // #end writes and flushes the response. rep.setStatusCode(errorCode) .setStatusMessage(statusMsg) .end(); return; } // If redirect query param set, do a 302. String query = req.query(); if (query != null && query.startsWith("redirectTo=")) { String redirectTo = query.substring(11); rep.putHeader("Location", redirectTo) .setStatusCode(302) .end(); return; } // Determine if explicitly needs XML (else, use JSON). boolean isXml = Optional.of(req.getHeader("Accept")) .filter(accept -> accept.contains("application/xml")) .map(accept -> !(accept.contains("application/json"))) .orElse(false); // Build response EchoResponse echo = new EchoResponse(); echo.setMethod(req.method().toString()); echo.setResource(normaliseResource(req)); echo.setUri(req.path()); req.handler(body -> { sha1.update(body.getBytes()); bodyLength += body.length(); }).endHandler(end -> { // If any body was present, encode digest as Base64. if (bodyLength > 0) { echo.setBodyLength(bodyLength); echo.setBodySha1(Base64.getEncoder().encodeToString(sha1.digest())); } echo.setCounter(++counter); echo.setHeaders(multimapToMap(req.headers())); rep.putHeader("Response-Counter", echo.getCounter().toString()); rep.setChunked(true); if (isXml) { // XML rep.putHeader("Content-Type", "application/xml"); writeXmlAndEnd(rep, echo); } else { // JSON rep.putHeader("Content-Type", "application/json"); writeJsonAndEnd(rep, echo); } }); } private void handleError(HttpServerResponse rep, Exception e) { e.printStackTrace(); if (!rep.ended()) { rep.setStatusCode(500); rep.end(); } } // IMPORTANT: This is lossy private Map<String, String> multimapToMap(MultiMap headers) { LinkedHashMap<String, String> out = new LinkedHashMap<>(); headers.forEach(pair -> out.put(pair.getKey(), pair.getValue())); return out; } private String normaliseResource(HttpServerRequest req) { if (req.query() != null) { String[] normalisedQueryString = req.query().split("&"); Arrays.sort(normalisedQueryString); return req.path() + "?" + SimpleStringUtils.join("&", normalisedQueryString); } else { return req.path(); } } } private static final class BufferOutputStream extends java.io.OutputStream { private Buffer vxBuffer; private int sizeHint; private WriteStream<Buffer> writeStream; private boolean ended = false; public BufferOutputStream(int sizeHint, WriteStream<Buffer> writeStream) { this.sizeHint = sizeHint; this.writeStream = writeStream; vxBuffer = Buffer.buffer(sizeHint); } @Override public void write(int b) throws IOException { checkFlush(1); vxBuffer.appendByte((byte) b); } @Override public void write(byte b[]) throws IOException { checkFlush(b.length); vxBuffer.appendBytes(b); } @Override public void write(byte b[], int off, int len) throws IOException { checkFlush(len); vxBuffer.appendBytes(b, off, len); } private void checkFlush(int len) { if (vxBuffer.length() + len >= sizeHint) { flush(); } } @Override public void flush() { writeStream.write(vxBuffer); vxBuffer.getByteBuf().clear(); } @Override public void close() { if (!ended) { if (vxBuffer.length() > 0) { writeStream.end(vxBuffer); } else { writeStream.end(); } ended = true; } } } }