欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java netty开发一个http/https代理

程序员文章站 2022-03-02 15:29:48
...

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代理服务器开发完成

相关标签: netty