/* * Copyright (c) 2014 The APN-PROXY Project * * The APN-PROXY Project licenses this file to you 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.xx_dev.apn.proxy; import com.xx_dev.apn.proxy.ApnProxyRemoteForwardHandler.RemoteChannelInactiveCallback; import com.xx_dev.apn.proxy.remotechooser.ApnProxyRemote; import com.xx_dev.apn.proxy.utils.Base64; import com.xx_dev.apn.proxy.utils.HttpErrorUtil; import com.xx_dev.apn.proxy.utils.LoggerUtil; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.PooledByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.*; import io.netty.util.ReferenceCountUtil; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.util.*; /** * @author xmx * @version $Id: com.xx_dev.apn.proxy.ApnProxyUserAgentForwardHandler 14-1-8 16:13 (xmx) Exp $ */ public class ApnProxyUserAgentForwardHandler extends ChannelInboundHandlerAdapter implements RemoteChannelInactiveCallback { private static final Logger logger = Logger.getLogger(ApnProxyUserAgentForwardHandler.class); public static final String HANDLER_NAME = "apnproxy.useragent.forward"; private Map<String, Channel> remoteChannelMap = new HashMap<String, Channel>(); private List<HttpContent> httpContentBuffer = new ArrayList<HttpContent>(); @Override public void channelRead(final ChannelHandlerContext uaChannelCtx, final Object msg) throws Exception { final Channel uaChannel = uaChannelCtx.channel(); final ApnProxyRemote apnProxyRemote = uaChannel .attr(ApnProxyConnectionAttribute.ATTRIBUTE_KEY).get().getRemote(); if (msg instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) msg; Channel remoteChannel = remoteChannelMap.get(apnProxyRemote.getRemoteAddr()); if (remoteChannel != null && remoteChannel.isActive()) { LoggerUtil.debug(logger, uaChannel.attr(ApnProxyConnectionAttribute.ATTRIBUTE_KEY), "Use old remote channel"); HttpRequest request = constructRequestForProxy(httpRequest, apnProxyRemote); remoteChannel.writeAndFlush(request); } else { LoggerUtil.debug(logger, uaChannel.attr(ApnProxyConnectionAttribute.ATTRIBUTE_KEY), "Create new remote channel"); Bootstrap bootstrap = new Bootstrap(); bootstrap .group(uaChannel.eventLoop()) .channel(NioSocketChannel.class) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .option(ChannelOption.AUTO_READ, false) .handler( new ApnProxyRemoteForwardChannelInitializer(uaChannel, this)); // set local address if (StringUtils.isNotBlank(ApnProxyLocalAddressChooser.choose(apnProxyRemote .getRemoteHost()))) { bootstrap.localAddress(new InetSocketAddress((ApnProxyLocalAddressChooser .choose(apnProxyRemote.getRemoteHost())), 0)); } ChannelFuture remoteConnectFuture = bootstrap.connect( apnProxyRemote.getRemoteHost(), apnProxyRemote.getRemotePort()); remoteChannel = remoteConnectFuture.channel(); remoteChannelMap.put(apnProxyRemote.getRemoteAddr(), remoteChannel); remoteConnectFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { future.channel().write( constructRequestForProxy((HttpRequest) msg, apnProxyRemote)); for (HttpContent hc : httpContentBuffer) { future.channel().writeAndFlush(hc); if (hc instanceof LastHttpContent) { future.channel().writeAndFlush(Unpooled.EMPTY_BUFFER) .addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { future.channel().read(); } } }); } } httpContentBuffer.clear(); } else { LoggerUtil.error(logger, uaChannel.attr(ApnProxyConnectionAttribute.ATTRIBUTE_KEY), "Remote channel create fail"); // send error response String errorMsg = "remote connect to " + uaChannel.attr(ApnProxyConnectionAttribute.ATTRIBUTE_KEY).get().getRemote().getRemoteAddr() + " fail"; HttpMessage errorResponseMsg = HttpErrorUtil.buildHttpErrorMessage( HttpResponseStatus.INTERNAL_SERVER_ERROR, errorMsg); uaChannel.writeAndFlush(errorResponseMsg); httpContentBuffer.clear(); future.channel().close(); } } }); } ReferenceCountUtil.release(msg); } else { Channel remoteChannel = remoteChannelMap.get(apnProxyRemote.getRemoteAddr()); HttpContent hc = ((HttpContent) msg); //hc.retain(); //HttpContent _hc = hc.copy(); if (remoteChannel != null && remoteChannel.isActive()) { remoteChannel.writeAndFlush(hc); if (hc instanceof LastHttpContent) { remoteChannel.writeAndFlush(Unpooled.EMPTY_BUFFER) .addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { future.channel().read(); } } }); } } else { httpContentBuffer.add(hc); } } } @Override public void channelInactive(ChannelHandlerContext uaChannelCtx) throws Exception { if (logger.isDebugEnabled()) { logger.debug("UA channel: inactive" + uaChannelCtx.attr(ApnProxyConnectionAttribute.ATTRIBUTE_KEY)); } LoggerUtil.debug(logger, uaChannelCtx.attr(ApnProxyConnectionAttribute.ATTRIBUTE_KEY), "UA channel inactive"); for (Map.Entry<String, Channel> entry : remoteChannelMap.entrySet()) { final Channel remoteChannel = entry.getValue(); remoteChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener( new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { remoteChannel.close(); } }); } } @Override public void exceptionCaught(ChannelHandlerContext uaChannelCtx, Throwable cause) throws Exception { logger.error(cause.getMessage() + " " + uaChannelCtx.attr(ApnProxyConnectionAttribute.ATTRIBUTE_KEY), cause); uaChannelCtx.close(); } @Override public void remoteChannelInactive(final Channel uaChannel, String inactiveRemoteAddr) throws Exception { LoggerUtil.debug(logger, uaChannel.attr(ApnProxyConnectionAttribute.ATTRIBUTE_KEY), "Remote channel inactive, and flush end"); remoteChannelMap.remove(inactiveRemoteAddr); if (uaChannel.isActive()) { uaChannel.writeAndFlush(Unpooled.EMPTY_BUFFER); } } private HttpRequest constructRequestForProxy(HttpRequest httpRequest, ApnProxyRemote apnProxyRemote) { String uri = httpRequest.getUri(); if (!apnProxyRemote.isAppleyRemoteRule()) { uri = this.getPartialUrl(uri); } HttpRequest _httpRequest = new DefaultHttpRequest(httpRequest.getProtocolVersion(), httpRequest.getMethod(), uri); Set<String> headerNames = httpRequest.headers().names(); for (String headerName : headerNames) { if (StringUtils.equalsIgnoreCase(headerName, "Proxy-Connection")) { continue; } if (StringUtils.equalsIgnoreCase(headerName, "Pragma")) { continue; } // if (StringUtils.equalsIgnoreCase(headerName, HttpHeaders.Names.CONNECTION)) { // continue; // } _httpRequest.headers().add(headerName, httpRequest.headers().getAll(headerName)); } _httpRequest.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE); // _httpRequest.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.IDENTITY); if (StringUtils.isNotBlank(apnProxyRemote.getProxyUserName()) && StringUtils.isNotBlank(apnProxyRemote.getProxyPassword())) { String proxyAuthorization = apnProxyRemote.getProxyUserName() + ":" + apnProxyRemote.getProxyPassword(); try { _httpRequest.headers().set("Proxy-Authorization", "Basic " + Base64.encodeBase64String(proxyAuthorization.getBytes("UTF-8"))); } catch (UnsupportedEncodingException e) { } } return _httpRequest; } private String getPartialUrl(String fullUrl) { if (StringUtils.startsWith(fullUrl, "http")) { int idx = StringUtils.indexOf(fullUrl, "/", 7); return idx == -1 ? "/" : StringUtils.substring(fullUrl, idx); } return fullUrl; } }