java netty开发一个http/https代理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/woaiqianzhige/article/details/84850960

http代理数据传播路径:

  1. 客户端将请求发送到代理,代理解析出消息目的地再去请求服务器
  2. 服务器将完整结果返回给代理,代理再将结果返回给客户端
  3. 代理就在两者之间进行中转数据

https消息传播模式:

  1. 客户端将请求的目的地端口明文发送到代理,
  2. 代理解析出服务器host 端口,并连接成功,返回客户端连接成功的标识
  3. 客户端知道代理已经连接成功了,开始将ssl握手之类的加密数据发送给代理
  4. 代理就在服务器客户端之间进行转发数据,他并不知道传输的数据到底是什么,因为是加密的

实现方式:

编程语言:java
框架选择:netty

首先创建一个标准的netty启动


		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workGroup)
					.channel(NioServerSocketChannel.class)
					.option(ChannelOption.SO_BACKLOG, 128)
					.childOption(ChannelOption.SO_KEEPALIVE, true)
					.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 6000)
					.childHandler(new ProxyServiceInit());

			ChannelFuture f = b.bind(PropertiesUtil.getIntProp("start.port")).sync();
			f.channel().closeFuture().sync();
		} finally {
			workGroup.shutdownGracefully();
			bossGroup.shutdownGracefully();
		}



向其中添加这么两个handler,其中HttpServerCodec是netty自带的,HttpService是自己实现的


	@Override
	protected void initChannel(Channel channel) throws Exception {
		ChannelPipeline p = channel.pipeline();
	
		p.addLast("httpcode", new HttpServerCodec());
		p.addLast("httpservice", new HttpService());
	}

HttpServerCodec会将客户端传进来的消息转成httpobject对象,并且是已经被聚合了的http消息,我们在自己写的HttpService中使用


	自定义httpservice 集成simpleinbondhandler

	public class HttpService extends SimpleChannelInboundHandler<HttpObject> {
		
		//保留全局ctx
		private ChannelHandlerContext ctx;
		//创建一会用于连接web服务器的	Bootstrap	
		private Bootstrap b = new Bootstrap();
		
		//channelActive方法中将ctx保留为全局变量
		@Override
		public void channelActive(ChannelHandlerContext ctx) throws Exception {
			super.channelActive(ctx);
			this.ctx = ctx;
		}
		
		//Complete方法中刷新数据
		@Override
		public void channelReadComplete(ChannelHandlerContext ctx) {
			ctx.flush();
		}


		@Override
		protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject msg) throws Exception {
		if (msg instanceof HttpRequest) {
			//转成 HttpRequest
			HttpRequest req = (HttpRequest) msg;
			if (PasswordChecker.digestLogin(req)) { //检测密码,后面讲
				HttpMethod method = req.method();	//获取请求方式,http的有get post ..., https的是 CONNECT
				String headerHost = req.headers().get("Host");	//获取请求头中的Host字段
				String host = "";
				int port = 80;									//端口默认80
				String[] split = headerHost.split(":");			//可能有请求是 host:port的情况,
				host = split[0];					
				if (split.length > 1) {	
					port = Integer.valueOf(split[1]);
				}
				Promise<Channel> promise = createPromise(host, port);	//根据host和port创建连接到服务器的连接

				/*
				根据是http还是http的不同,为promise添加不同的监听器
				*/
				if (method.equals(HttpMethod.CONNECT)) {
					//如果是https的连接
					promise.addListener(new FutureListener<Channel>() {
						@Override
						public void operationComplete(Future<Channel> channelFuture) throws Exception {
							//首先向浏览器发送一个200的响应,证明已经连接成功了,可以发送数据了
							FullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(200, "OK"));
							//向浏览器发送同意连接的响应,并在发送完成后移除httpcode和httpservice两个handler
							ctx.writeAndFlush(resp).addListener(new ChannelFutureListener() {
								@Override
								public void operationComplete(ChannelFuture channelFuture) throws Exception {
									ChannelPipeline p = ctx.pipeline();
									p.remove("httpcode");
									p.remove("httpservice");
								}
							});
							ChannelPipeline p = ctx.pipeline();
							//将客户端channel添加到转换数据的channel,(这个NoneHandler是自己写的)
							p.addLast(new NoneHandler(channelFuture.getNow()));
						}
					});
				} else {
					//如果是http连接,首先将接受的请求转换成原始字节数据
					EmbeddedChannel em = new EmbeddedChannel(new HttpRequestEncoder());
					em.writeOutbound(req);
					final Object o = em.readOutbound();
					em.close();
					promise.addListener(new FutureListener<Channel>() {
						@Override
						public void operationComplete(Future<Channel> channelFuture) throws Exception {
							//移除	httpcode	httpservice 并添加	NoneHandler,并向服务器发送请求的byte数据				
							ChannelPipeline p = ctx.pipeline();
							p.remove("httpcode");
							p.remove("httpservice");
							//添加handler
							p.addLast(new NoneHandler(channelFuture.getNow()));
							channelFuture.get().writeAndFlush(o);
						}
					});
				}
			} else {
				ctx.writeAndFlush(PasswordChecker.getDigest());
			}
		} else {
			ReferenceCountUtil.release(msg);
		}
		}
	

	//根据host和端口,创建一个连接web的连接
	private Promise<Channel> createPromise(String host, int port) {
		final Promise<Channel> promise = ctx.executor().newPromise();

		b.group(ctx.channel().eventLoop())
				.channel(NioSocketChannel.class)
				.remoteAddress(host, port)
				.handler(new NoneHandler(ctx.channel()))
				.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
				.connect()
				.addListener(new ChannelFutureListener() {
					@Override
					public void operationComplete(ChannelFuture channelFuture) throws Exception {
						if (channelFuture.isSuccess()) {
							promise.setSuccess(channelFuture.channel());
						} else {
							ctx.close();
							channelFuture.cancel(true);
						}
					}
				});
		return promise;
	}
		

	
	}


noneHandler里面只做数据的转发,没有任何逻辑


	public class NoneHandler extends ChannelInboundHandlerAdapter {

	private Channel outChannel;

	public NoneHandler(Channel outChannel) {
		this.outChannel = outChannel;
	}


	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		//System.out.println("交换数据");
		outChannel.write(msg);
	}

	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
		outChannel.flush();
	}
}





上面我们添加了密码的验证,去掉密码验证部分就能直接使用了,可以先去掉测试一下,添加密码验证放在服务器上更安全
密码验证的逻辑就是,

  1. http(s)请求头中包含我们约定好的密码,如果没有我们就发送一个407响应,这样浏览器(chromr,firefox,ie)就知道代理服务器要验证密码,就会弹出窗口要我们输入密码,

  2. 如果验证成功过一次,下次的请求浏览器会自动带上密码,不用再重新输入

  3. 密码的方式有两种,一种是简单的Basic,密码是明文传输的,一种是digest方式

方式一 basic 方式


	如果没有输入密码的情况下,代理返回的响应是

		FullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED);
		resp.headers().add("Proxy-Authenticate", "Basic realm=\"Text\"");
		resp.headers().setInt("Content-Length", resp.content().readableBytes());
		return resp;

	发送一个 PROXY_AUTHENTICATION_REQUIRED响应,响应头不包含  "Basic realm=\"Text\"",其中 `Text`可以随便起,这样浏览器就能弹窗出来

	报文可能长这样
	
	HTTP/1.0 407 PROXY_AUTHENTICATION_REQUIRED
	Server: SokEvo/1.0
	WWW-Authenticate: Basic realm="Text"
	Content-Type: text/html
	Content-Length: xxx



代理如何验证密码呢?就是将头部的帐号密码解析出来,和我们的帐号密码进行比对
	用户输入帐号密码后,服务器可能收到这样的报文

	Get /index.html HTTP/1.0
	Host:www.google.com
	Proxy-Authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxx	


	//basic方式登录
	public static boolean basicLogin(HttpRequest req) {
		//获取请求头中的 Proxy-Authorization
		String s = req.headers().get("Proxy-Authorization");
		if (s == null) {
			return false;
		}

		//密码的形式是   `Basic 帐号:密码`用冒号拼接在一起,在取base64
		try {
			String[] split = s.split(" ");
			byte[] decode = Base64.decodeBase64(split[1]); //去数组中的第二个,第一个是一个Basic固定的字符
			String userNamePassWord = new String(decode);
			String[] split1 = userNamePassWord.split(":", 2);
			PasswordChecker.basicCheck(split1[0], split1[1]); //比较帐号密码是不是我们自己的帐号密码
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}


方式二 digest方式,

这种方式是,服务器发送一个随机数给浏览器,浏览器用密码和一堆东西混合(用户名,uri之类的)在一起,进行md5加密,将混合的东西传给服务器,
那么服务器以同样的方式进行操作一遍,如果得到的结果与浏览器传上来的相同,那么就证明密码正确,传输阶段,不会暴露帐号密码

这个参考维基百科的 http digest 摘要加密方式页面

ok一个带帐号密码控制的http代理服务器开发完成

windows这样配置就可以用proxy上网了,也可以放在服务器上跑

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/woaiqianzhige/article/details/84850960