Java实现Http代理服务器&通过http代理进行内网安装yum软件
本文通过Java进行http代理服务器实现,并在这个Java版http代理服务器的基础上,再描述如何通过http代理进行内网系统的yum软件安装
1.Http代理服务器简介
HTTP代理服务器是一种特殊的网络服务,允许一个网络终端(一般为客户端)通过这个服务与另一个网络终端进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击。
HTTP代理:
http请求经过代理服务器,代理服务器只要负责转发相应的http响应体就可以
HTTPS代理:https请求经过代理服务器,会发送一个CONNECT报文,用于和代理服务器建立隧道,如果代理服务器返回HTTP 200,则建立成功,后续代理服务器只要负责转发数据。
2.Http代理服务器Java实现
常见的http代理服务器实现有ccproxy,haproxy,nginx等,这里通过Java实现简单的http代理服务器
通过Java进行http代理服务器实例,主要是解析http协议头里面的消息,解析出此请求是http还是https请求,如果是http请求,就解析出真实的请求的host,然后进行数据转发,如果是https请求,就进行CONNECT的解析,然后返回给客户端,再继续进行数据的转发。
2.1 Java源码
整个实现代码,采用一个Java类实现如下,将内容复制到你的idea或eclipse中编绎就可,无任何jar包依赖。
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Base64;
import java.util.Date;
/**
* http代理服务器,隧道模式
* @author liujh
*/
public class ProxyHttpsServer {
private final int bufferSize = 8092;
private int defaultPort = 1080; //默认端口
private int localPort ;
private ServerSocket localServerSocket;
private boolean socksNeekLogin = true;//是否需要登录
private String username = "admin";
private String password = "admin123";
public static void main(String[] args) {
Integer port = args.length == 1 ? Integer.parseInt(args[0]) : null;
new ProxyHttpsServer(port).startService();
}
public ProxyHttpsServer(Integer port){
this.localPort = port == null ? defaultPort : port;
}
public void startService() {
try {
//开启一个ServerSocket服务器,监听请求的到来.
localServerSocket = new ServerSocket(localPort);
log("httpproxy server started , listen on " +localServerSocket.getInetAddress().getHostAddress()+":"+ localPort );
// 一直监听,接收到新连接,则开启新线程去处理
while (true) {
Socket localSocket = localServerSocket.accept();
new SocketThread(localSocket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//格式化打印方法,用来打印信息
private final void log(Object message, Object... args) {
Date dat = new Date();
String msg = String.format("%1$tF %1$tT %2$-5s %3$s%n", dat, Thread.currentThread().getId(), String.format(message.toString(),args));
System.out.print(msg);
}
/**
* IO操作中共同的关闭方法
* @param socket
*/
protected final void closeIo(Socket closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (IOException e) {
}
}
}
/**
* IO操作中共同的关闭方法
* @param socket
*/
protected final void closeIo(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (IOException e) {
}
}
}
private class SocketThread extends Thread {
private Socket localSocket;
private Socket remoteSocket;
private InputStream lin;
private InputStream rin;
private OutputStream lout;
private OutputStream rout;
public SocketThread(Socket socket) {
this.localSocket = socket;
}
public void run() {
//获取远程socket的地址,然后进行打印
String addr = localSocket.getRemoteSocketAddress().toString();
log("process one socket : %s", addr);
try {
lin = localSocket.getInputStream();
lout = localSocket.getOutputStream();
StringBuilder headStr = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(lin));
//读取HTTP请求头,并拿到HOST请求头和method
String line;
String host = "";
String proxy_Authorization = "";
while ((line = br.readLine()) != null) {
//打印http协议头
log(line);
headStr.append(line + "\r\n");
if (line.length() == 0) {
break;
} else {
String[] temp = line.split(" ");
if (temp[0].contains("Host")) {
host = temp[1];
}
//如果配置了需要登陆,就解析消息头里面的Proxy-Authorization字段,认证的账号和密码信息是通过base64加密传过来的。
if(socksNeekLogin && (temp[0].contains("Proxy-Authorization"))){//获取认证信息
proxy_Authorization = temp[2];
}
}
}
String type = headStr.substring(0, headStr.indexOf(" "));
//根据host头解析出目标服务器的host和port
String[] hostTemp = host.split(":");
host = hostTemp[0];
int port = 80; //先设置成默认的Http端口80
//hostTemp的长度大于1表示用户指定了非80或443端口,解析出对应的端口出来
if (hostTemp.length > 1) {
port = Integer.valueOf(hostTemp[1]);
}else{
//端口如果没有指定,有可能是443,也有可能是80,因此尝试根据HTTP method来判断是https还是http请求,有CONNECT的是HTTPS请求,采用443端口
if ("CONNECT".equalsIgnoreCase(type)){
port = 443;
}
}
//
boolean isLogin = false;
//如果需要登录,校验登录是否通过
if(socksNeekLogin){
//通过username和password进行base64加密得到一个串,然后和请求里面传过来的Proxy-Authorization比较,一致的话就认证成功。
String authenticationEncoding = Base64.getEncoder().encodeToString(new String(username + ":" + password).getBytes());
if(proxy_Authorization.equals(authenticationEncoding)){
isLogin = true;//登录通过
//log("login success, basic: %s", proxy_Authorization);
}else {
log("httpproxy server need login,but login failed .");
}
}
//不需要登录或已被校验登录成功,才进入代理,否则直接程序结束,关闭连接
if(!socksNeekLogin || isLogin){
//连接到目标服务器
remoteSocket = new Socket(host, port);//进行远程连接
rin = remoteSocket.getInputStream();
rout = remoteSocket.getOutputStream();
//根据HTTP method来判断是https还是http请求
if ("CONNECT".equalsIgnoreCase(type)) {//https先建立隧道
lout.write("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes());
lout.flush();
}else {//http直接将请求头转发
rout.write(headStr.toString().getBytes());
rout.flush();
}
new ReadThread().start();
//设置超时,超过时间未收到客户端请求,关闭资源
//remoteSocket.setSoTimeout(10000);
//写数据,负责读取客户端发送过来的数据,转发给远程
byte[] data = new byte[bufferSize];
int len = 0;
while((len = lin.read(data)) > 0){
if(len == bufferSize) {//读到了缓存大小一致的数据,不需要拷贝,直接使用
rout.write(data);
rout.flush();
}else {//读到了比缓存大小的数据,需要拷贝到新数组然后再使用
byte[] dest = new byte[len];
System.arraycopy(data, 0, dest, 0, len);
rout.write(dest);
rout.flush();
}
}
}
} catch (Exception e) {
log("exception : %s %s", e.getClass(), e.getLocalizedMessage());
//e.printStackTrace();
} finally {
log("close socket, system cleanning ... %s ", addr);
closeIo(lin);
closeIo(rin);
closeIo(lout);
closeIo(rout);
closeIo(localSocket);
closeIo(remoteSocket);
}
}
//读数据线程负责读取远程数据后回写到客户端
class ReadThread extends Thread {
@Override
public void run() {
try {
byte[] data = new byte[bufferSize];
int len = 0;
while((len = rin.read(data)) > 0){
if(len == bufferSize) {//读到了缓存大小一致的数据,不需要拷贝,直接使用
lout.write(data);
lout.flush();
}else {//读到了比缓存大小的数据,需要拷贝到新数组然后再使用
byte[] dest = new byte[len];
System.arraycopy(data, 0, dest, 0, len);
lout.write(dest);
lout.flush();
}
}
} catch (IOException e) {
//log(remoteSocket.getLocalAddress() + ":"+ remoteSocket.getPort() + " remoteSocket InputStream disconnected.");
} finally {
}
}
}
}
}
2.2 代码分析说明
- ##可支持http代理,认证或不认证功能通过socksNeekLogin参数来支持,如果socksNeekLogin设置为true,说明需要进行认证,并相应的username和password为认证的账号和密码。
private boolean socksNeekLogin = true;//是否需要登录
private String username = "admin";
private String password = "admin123";
- ##首先定义一个ServerSocket服务器,监听端口1080,端口可以通过命令行参数进行修改。
//开启一个ServerSocket服务器,监听请求的到来.
localServerSocket = new ServerSocket(localPort);
log("httpproxy server started , listen on " +localServerSocket.getInetAddress().getHostAddress()+":"+ localPort );
- ##阻塞循环等待请求进来,收到一个请求后,开启一个线程进行处理
// 一直监听,接收到新连接,则开启新线程去处理
while (true) {
Socket localSocket = localServerSocket.accept();
new SocketThread(localSocket).start();
}
- ##在新开启的线程中进行主要逻辑的处理。先进行简单的打印远程的连接
//获取远程socket的地址,然后进行打印
String addr = localSocket.getRemoteSocketAddress().toString();
log("process one socket : %s", addr);
- ##接着通过BufferedReader包装好inputstream流,然后一行一行的读。如果某一行含有Host,就解析出真实目标服务器的ip和端口
- ##如何设置了需要登陆,就解析消息头中的Proxy-Authorization字段,然后进行账号密码判断是否一致,决定是否认证成功
//如果配置了需要登陆,就解析消息头里面的Proxy-Authorization字段,认证的账号和密码信息是通过base64加密传过来的。
if(socksNeekLogin && temp[0].contains("Proxy-Authorization")){//获取认证信息
proxy_Authorization = temp[2];
}
boolean isLogin = false;
//如果需要登录,校验登录是否通过
if(socksNeekLogin){
//通过username和password进行base64加密得到一个串,然后和请求里面传过来的Proxy-Authorization比较,一致的话就认证成功。
String authenticationEncoding = Base64.getEncoder().encodeToString(new String(username + ":" + password).getBytes());
if(proxy_Authorization.equals(authenticationEncoding)){
isLogin = true;//登录通过
//log("login success, basic: %s", proxy_Authorization);
}else {
log("httpproxy server need login,but login failed .");
}
}
- ##如果不需要登陆,或认证成功,进一步进行是http还是https的判断,分别作不同的处理
//根据HTTP method来判断是https还是http请求
if ("CONNECT".equalsIgnoreCase(type)) {//https先建立隧道
lout.write("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes());
lout.flush();
}else {//http直接将请求头转发
rout.write(headStr.toString().getBytes());
rout.flush();
}
- ##最后作真正的数据转发即可
3.通过http代理进行内网安装yum软件
生产环境,常常是不给服务器有互联网访问的权限的,要进行yum安装一些软件,就有点麻烦,常用的做法有,在内网搭建一个私库,然后通过私库进行安装,见另一篇nexus3私库环境搭建(maven,yum,apt,nodejs), 或者进行离线rpm安装等,本篇
采用http代理服务器+securt远程透传的方式
进行,还是较简单的。
3.1 步骤2实现的代理服务器启动
一般我本地笔记本肯定是有外网访问权限的,连本地个人电脑都没有外网访问,就没办法了,先进行代理服务器的启动如下:
##这里指定了以1080端口启动,并默认是开了认证的,账号admin密码admin123
java -server -Xmx256m -Xms256m -Xmn128m ProxyHttpsServer 1080
##代理服务器启动成功截图:
3.2通过securecrt进行远程内网服务器的连接
一般可以通过vpn或其他方式能在个人笔记本电脑上直接连进远程的内网服务器,
连接远程服务器后,通过securecrt自带的远程端口透传功能进行远程端口到本地端口的反向映射
。
如下:通过crt连接我的内网服务器192.168.56.101,然后通过crt作一个远程到本地的端口转发
,(其实这里我的192.168.56.101是能直连我的本地电脑192.168.56.1,但为了模拟只能单向从本地到远程,因此这里通过crt作一个远程到本地的反向转发)
经过以上的端口转发配置后,通过在服务器192.168.56.101上进行访问127.0.0.1:11080就相当于访问到我的本地电脑的192.168.56.1的1080了
,测试如下:
因为我没有装telnet,因此采用curl测试端口情况,在192.168.56.101上执行curl http://127.0.0.1:11080, 显示Empty reply from server,其实是成功的。看本地的程序日志也能看出来有请求到达。说明端口从远程到本地是成功的。
3.3 进行export配置通过http代理访问
在远程服务器上进行export三条命令的执行,告诉服务器,通过代理访问相关资源。
注:export命令只对当前session生效,即关了crt再进入服务器时,要重新执行
export http_proxy=http://admin:admin123@127.0.0.1:11080
export https_proxy=http://admin:admin123@127.0.0.1:11080
export no_proxy='127.0.0.1'
上面的no_proxy是告诉服务器127.0.0.1相关的地址不需要走代理
以访问www.baidu.com为例,执行命令行,访问不了:
执行export后再执行curl http://www.baidu.com测试
3.4 通过代理进行yum软件的安装
由于3.3配置后,能在远程内网访问直接访问外面的网站了,因此,接下来正常直接按yum操作执行软件安装即可了,如:
yum install -y telnet
安装日志正常,软件能正常安装,超级方便,相当于有外网状态下进行相关的yum软件安装
本地的代理程序,也显示已通过代理进行远程的yum.repo网址的访问,说明是真的通过代理服务器走资源的访问并转发。
4. 总结
本文讲述了如何通过Java实现一个http代理服务器,并进行简单的应用。如yum软件的安装等,另外,sock5代理服务器的原理与实现,以及应用也类似,后面再写一个Java版的Sock5代理服务器的实现及应用,如对这篇文章有什么疑问,请指正或回复我。