/*
* Copyright 2013 BiasedBit
*
* 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 com.biasedbit.http.server;
import com.biasedbit.http.client.ssl.BogusSslContextFactory;
import lombok.*;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.*;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.http.*;
import org.jboss.netty.handler.ssl.SslHandler;
import org.jboss.netty.util.CharsetUtil;
import javax.net.ssl.SSLEngine;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.jboss.netty.handler.codec.http.HttpHeaders.*;
/**
* A simple HttpServer with configurable error introduction.
*
* @author <a href="http://biasedbit.com/">Bruno de Carvalho</a>
*/
@RequiredArgsConstructor
public class DummyHttpServer {
// configuration defaults -----------------------------------------------------------------------------------------
public static final boolean USE_SSL = false;
public static final int COMPRESSION_LEVEL = 0;
public static final float FAILURE_PROBABILITY = 0.0f;
public static final long RESPONSE_LATENCY = 0;
public static final boolean USE_OLD_IO = false;
// Taken from http://www.w3schools.com/XML/xml_examples.asp
public static final String CONTENT =
"<breakfast_menu> \n" +
"\t<food> \n" +
"\t\t<name>Belgian Waffles</name> \n" +
"\t\t<price>$5.95</price> \n" +
"\t\t<description>two of our famous Belgian Waffles with plenty of real maple syrup</description> \n" +
"\t\t<calories>650</calories> \n" +
"\t</food> \n" +
"\t<food> \n" +
"\t\t<name>Strawberry Belgian Waffles</name> \n" +
"\t\t<price>$7.95</price> \n" +
"\t\t<description>light Belgian waffles covered with strawberries and whipped cream</description> \n" +
"\t\t<calories>900</calories> \n" +
"\t</food> \n" +
"\t<food> \n" +
"\t\t<name>Berry-Berry Belgian Waffles</name> \n" +
"\t\t<price>$8.95</price> \n" +
"\t\t<description>light Belgian waffles covered with an assortment of fresh berries and " +
"whipped cream</description> \n" +
"\t\t<calories>900</calories> \n" +
"\t</food> \n" +
"\t<food> \n" +
"\t\t<name>French Toast</name> \n" +
"\t\t<price>$4.50</price> \n" +
"\t\t<description>thick slices made from our homemade sourdough bread</description> \n" +
"\t\t<calories>600</calories> \n" +
"\t</food> \n" +
"\t<food> \n" +
"\t\t<name>Homestyle Breakfast</name> \n" +
"\t\t<price>$6.95</price> \n" +
"\t\t<description>two eggs, bacon or sausage, toast, and our ever-popular hash browns</description> \n" +
"\t\t<calories>950</calories> \n" +
"\t</food> \n" +
"</breakfast_menu> ";
// properties -----------------------------------------------------------------------------------------------------
@Getter private final String host;
@Getter private final int port;
@Getter @Setter private boolean verbose;
@Getter @Setter private boolean useSsl = USE_SSL;
@Getter @Setter private int compressionLevel = COMPRESSION_LEVEL;
@Getter @Setter private long responseLatency = RESPONSE_LATENCY;
@Getter @Setter private boolean useOldIo = USE_OLD_IO;
@Getter @Setter private String content = CONTENT;
@Getter private float failureProbability = FAILURE_PROBABILITY;
// internal vars --------------------------------------------------------------------------------------------------
private ServerBootstrap bootstrap;
private DefaultChannelGroup channelGroup;
private boolean running;
private final AtomicInteger errors = new AtomicInteger();
// constructors ---------------------------------------------------------------------------------------------------
public DummyHttpServer(int port) { this(null, port); }
// interface ------------------------------------------------------------------------------------------------------
public boolean init() {
Executor bossExecutor = Executors.newCachedThreadPool();
Executor workerExecutor = Executors.newCachedThreadPool();
ChannelFactory factory;
if (useOldIo) factory = new OioServerSocketChannelFactory(bossExecutor, workerExecutor);
else factory = new NioServerSocketChannelFactory(bossExecutor, workerExecutor);
bootstrap = new ServerBootstrap(factory);
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override public ChannelPipeline getPipeline()
throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
if (useSsl) {
SSLEngine engine = BogusSslContextFactory.getInstance().getServerContext().createSSLEngine();
engine.setUseClientMode(false);
pipeline.addLast("ssl", new SslHandler(engine));
}
pipeline.addLast("encoder", new HttpResponseEncoder());
pipeline.addLast("decoder", new HttpRequestDecoder());
if (compressionLevel > 0) pipeline.addLast("compressor", new HttpContentCompressor(compressionLevel));
pipeline.addLast("aggregator", new HttpChunkAggregator(5242880)); // 5MB
pipeline.addLast("handler", new RequestHandler());
return pipeline;
}
});
channelGroup = new DefaultChannelGroup("hotpotato-dummy-server-" + Integer.toHexString(hashCode()));
SocketAddress bindAddress = (host != null) ? new InetSocketAddress(host, port) : new InetSocketAddress(port);
Channel serverChannel = bootstrap.bind(bindAddress);
channelGroup.add(serverChannel);
return (running = serverChannel.isBound());
}
public void terminate() {
if (!running) return;
running = false;
channelGroup.close().awaitUninterruptibly();
bootstrap.releaseExternalResources();
}
// getters & setters ----------------------------------------------------------------------------------------------
public void setFailureProbability(float failureProbability) {
if (failureProbability < 0) this.failureProbability = 0;
else if (failureProbability > 1.0) this.failureProbability = 1;
else this.failureProbability = failureProbability;
}
// private classes ------------------------------------------------------------------------------------------------
private final class RequestHandler
extends SimpleChannelUpstreamHandler {
private final ChannelBuffer contentBuffer = ChannelBuffers.copiedBuffer(content, CharsetUtil.UTF_8);
// SimpleChannelUpstreamHandler -------------------------------------------------------------------------------
@Override public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e)
throws Exception {
channelGroup.add(e.getChannel());
}
@Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
throws Exception {
if (Math.random() <= failureProbability) {
errors.incrementAndGet();
e.getChannel().close();
return;
}
if (responseLatency > 0) try { Thread.sleep(responseLatency); } catch (InterruptedException ignored) { }
HttpRequest request = (HttpRequest) e.getMessage();
if (verbose) System.err.println(request);
if ((request.getContent().readableBytes() > 0) && verbose) {
System.err.println("-------------");
System.err.println("Body has " + request.getContent().readableBytes() + " readable bytes.");
System.err.println("--- BODY START ----------");
System.err.println(request.getContent().toString(CharsetUtil.UTF_8));
System.err.println("--- BODY END ------------\n");
}
// Build the response object.
HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), HttpResponseStatus.ACCEPTED);
response.setContent(contentBuffer);
setHeader(response, Names.CONTENT_TYPE, "text/xml; charset=UTF-8");
boolean keepAlive = isKeepAlive(request);
setHeader(response, Names.CONTENT_LENGTH, contentBuffer.readableBytes());
ChannelFuture f = e.getChannel().write(response);
// Write the response & close the connection after the write operation.
if (!keepAlive) f.addListener(ChannelFutureListener.CLOSE);
}
@Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
throws Exception {
if (e.getChannel().isConnected()) e.getChannel().close();
}
}
// main -----------------------------------------------------------------------------------------------------------
public static void main(String[] args) {
String host = null;
int port = 80;
float failureProbability = 0.0f;
boolean useOio = false;
boolean verbose = false;
if (args.length >= 1) host = args[0];
if (args.length >= 2) port = Integer.parseInt(args[1]);
if (args.length >= 3) failureProbability = Float.parseFloat(args[2]);
if (args.length == 4) useOio = ("useOio".equals(args[3]));
if (args.length == 5) verbose = ("verbose".equals(args[4]));
final DummyHttpServer server = new DummyHttpServer(host, port);
server.setVerbose(verbose);
server.setFailureProbability(failureProbability);
server.setUseOldIo(useOio);
//server.setResponseLatency(50L);
if (!server.init()) {
System.err.println("Failed to bind server to " + (host == null ? '*' : host) + ":" + port +
(useOio ? " (Oio)" : " (Nio)") + " (FP: " + failureProbability + ")");
} else {
System.out.println("Server bound to " + (host == null ? '*' : host) + ":" + port +
(useOio ? " (Oio)" : " (Nio)") + " (FP: " + failureProbability + ")");
}
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override public void run() { server.terminate(); }
});
}
}