package org.rakam.ui;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.util.AttributeKey;
import io.netty.util.CharsetUtil;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.parser.Parser;
import org.rakam.server.http.HttpService;
import org.rakam.server.http.RakamHttpRequest;
import org.rakam.server.http.annotations.QueryParam;
import javax.annotation.PostConstruct;
import javax.net.ssl.SSLException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.UriBuilder;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.List;
@Path("/ui/proxy")
public class ProxyWebService extends HttpService {
private final AttributeKey<RakamHttpRequest> CONNECTION_ATTR = AttributeKey.newInstance("CONNECTION_ATTR");
private Bootstrap bootstrap;
private Bootstrap sslBootstrap;
@PostConstruct
public void startClient() throws SSLException {
NioEventLoopGroup group = new NioEventLoopGroup(4);
SslContext sslCtx = SslContextBuilder.forClient()
.clientAuth(ClientAuth.NONE)
.trustManager(InsecureTrustManagerFactory.INSTANCE).build();
sslBootstrap = new Bootstrap().channel(NioSocketChannel.class)
.group(group).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
InetSocketAddress addr = ch.remoteAddress();
SslHandler sslHandler;
// for some hosts the hostname and port required, jdk ssl throws handshake_failure
if(addr != null) {
sslHandler = sslCtx.newHandler(ch.alloc(), addr.getHostName(), addr.getPort());
} else {
sslHandler = sslCtx.newHandler(ch.alloc());
}
ch.pipeline().addLast(sslHandler)
.addLast(new HttpClientCodec())
.addLast(new HttpContentDecompressor())
.addLast(new HttpObjectAggregator(10048576))
.addLast(new ProxyChannelInboundHandler());
}
});
bootstrap = new Bootstrap().channel(NioSocketChannel.class)
.group(group).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new HttpClientCodec())
.addLast(new HttpContentDecompressor())
.addLast(new HttpObjectAggregator(10048576))
.addLast(new ProxyChannelInboundHandler());
}
});
}
@Path("/")
@GET
public void proxy(RakamHttpRequest request, @QueryParam("u") String uri) throws InterruptedException {
URI url = UriBuilder.fromUri(uri).build();
int port;
if(url.getPort() != -1) {
port = url.getPort();
} else
if(url.getScheme().equals("http")) {
port = 80;
} else
if(url.getScheme().equals("https")) {
port = 443;
} else {
request.response("invalid scheme").end();
return;
}
Channel ch = (port == 443 ? sslBootstrap : bootstrap).connect(url.getHost(), port)
.sync().channel();
ch.attr(CONNECTION_ATTR).set(request);
HttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, url.getRawPath());
req.headers().set(HttpHeaders.Names.HOST, url.getHost());
req.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE);
req.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP);
req.headers().set(HttpHeaders.Names.CACHE_CONTROL, HttpHeaders.Values.NO_CACHE);
req.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE);
req.headers().set(HttpHeaders.Names.PRAGMA, HttpHeaders.Values.NO_CACHE);
req.headers().set(HttpHeaders.Names.USER_AGENT, "rakam-ab-test-tool 0.1");
ch.writeAndFlush(req);
}
private class ProxyChannelInboundHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
RakamHttpRequest rakamHttpRequest = ctx.channel().attr(CONNECTION_ATTR).get();
Channel channel = rakamHttpRequest.context().channel();
FullHttpResponse resp = (FullHttpResponse) msg;
String contentType = resp.headers().get(HttpHeaders.Names.CONTENT_TYPE);
Charset charset = null;
if(contentType != null) {
Iterable<String> split = Splitter.on(";").trimResults().split(contentType);
for (String item : split) {
List<String> charsetStr = new QueryStringDecoder("?" + item).parameters().get("charset");
if(charsetStr != null) {
charset = Charset.forName(charsetStr.get(0));
break;
}
}
}
if(charset == null) {
charset = CharsetUtil.UTF_8;
}
String content = resp.content().toString(charset);
Document parse = Jsoup.parse(content, "", Parser.htmlParser());
String url = rakamHttpRequest.params().get("u").get(0);
parse.head().prepend(String.format("<base href='%s'>", url));
parse.head().prepend("<link href=\"/static/components/codemirror/lib/codemirror.css\" media=\"screen\" rel=\"stylesheet\" />");
parse.head().prepend("<link href=\"/static/embed/jquery-ui-theme.css\" media=\"screen\" rel=\"stylesheet\" />");
parse.head().prepend("<link href=\"/static/components/bootstrap-colorpicker/css/colorpicker.css\" media=\"screen\" rel=\"stylesheet\" />");
parse.head().prepend("<link href=\"/static/embed/rakam-inline-editor.css\" media=\"screen\" rel=\"stylesheet\" />");
parse.head().prepend("<script src=\"/static/embed/rakam-inline-editor.js\"></script>");
parse.head().prepend("<script src=\"/static/components/bootstrap-colorpicker/js/bootstrap-colorpicker.js\"></script>");
parse.head().prepend("<script src=\"/static/components/codemirror/mode/xml/xml.js\"></script>");
parse.head().prepend("<script src=\"/static/components/codemirror/mode/javascript/javascript.js\"></script>");
parse.head().prepend("<script src=\"/static/components/codemirror/mode/css/css.js\"></script>");
parse.head().prepend("<script src=\"/static/components/codemirror/mode/vbscript/vbscript.js\"></script>");
parse.head().prepend("<script src=\"/static/components/codemirror/mode/htmlmixed/htmlmixed.js\"></script>");
parse.head().prepend("<script src=\"/static/components/codemirror/lib/codemirror.js\"></script>");
parse.head().prepend("<script src=\"/static/components/jquery-ui/jquery-ui.min.js\"></script>");
parse.head().prepend("<script src=\"/static/components/jquery/dist/jquery.min.js\"></script>");
byte[] bytes = parse.outerHtml().getBytes(charset);
DefaultFullHttpResponse copy = new DefaultFullHttpResponse(
resp.getProtocolVersion(), resp.getStatus(), Unpooled.wrappedBuffer(bytes));
copy.headers().set(resp.headers());
copy.trailingHeaders().set(resp.trailingHeaders());
copy.headers().set(HttpHeaders.Names.CONTENT_LENGTH, bytes.length);
String location = resp.headers().get(HttpHeaders.Names.LOCATION);
copy.headers().set("X-Frame-Options", "ALLOWALL");
if(location != null && (location.startsWith("/") || (resp.getStatus().code() == 301 || resp.getStatus().code() == 302))) {
if(location.startsWith("/")) {
location = url.trim()+location;
}
copy.headers().set(HttpHeaders.Names.LOCATION, CharMatcher.is('/').trimTrailingFrom("/ui/proxy?u="+location)+'/');
}
channel.writeAndFlush(copy).addListener(ChannelFutureListener.CLOSE);
}
}
}