哈工大计算机网络实验1 实现HTTP代理

哈工大计算机网络实验1 实现HTTP代理

需要代码请前往:哈工大计算机网络实验1 实现HTTP代理

实验要求

此部分来自于实验指导书

实验目的

  • 熟悉并掌握 Socket 网络编程的过程与技术;
  • 深入理解 HTTP 协议,掌握HTTP代理服务器的基本工作原理;
  • 掌握 HTTP 代理服务器设计与编程实现的基本技能。

实验内容

  1. 设计并实现一个基本 HTTP 代理服务器。要求在指定端口(例如8080)接收来自客户的HTTP 请求并且根据其中的 URL 地址访问该地址所指向的 HTTP 服务器(原服务器),接收 HTTP 服务器的响应报文,并将响应报文转发给对应的客户进行浏览。
  2. 设计并实现一个支持 Cache 功能的 HTTP 代理服务器。要求能缓 存原服务器响应的对象,并能够通过修改请求报文(添加 if-modified-since头行),向原服务器确认缓存对象是否是最新版本。(选作内容,加分项目,可以当堂完成或课下完成)
  3. 扩展 HTTP 代理服务器,支持如下功能:(选作内容,加分项目,
    可以当堂完成或课下完成)
    1. 网站过滤:允许/不允许访问某些网站;
    2. 用户过滤:支持/不支持某些用户访问外部网站;
    3. 网站引导:将用户对某个网站的访问引导至一个模拟网站(钓鱼)。

下面使用Java实现上述要求

1. 能够转发报文的简单HTTP代理服务器

  • 创建SocketServer.java文件,直接在main方法中执行后续操作。

  • 因为是本地代理,所以代理服务器的IP地址就是127.0.0.1,只需要另外指定监听的端口即可,然后创建一个ServerSocket的实例server等待连接:

    // 监听指定的端口
    int port = 808;
    ServerSocket server = new ServerSocket(port);
    
  • while(true)中等待连接,使用server.accept()获取这个连接并实例化一个新的Socket对象:

    Socket socket = server.accept();
    System.out.println("获取到一个连接!来自 " + socket.getInetAddress().getHostAddress());
    
  • 创建新线程,处理来自浏览器的报文。首先接收:

    // 解析header
    InputStreamReader r = new InputStreamReader(socket.getInputStream());
    BufferedReader br = new BufferedReader(r);
    String readLine = br.readLine();
    String host;
    
    StringBuilder header = new StringBuilder();
    
    while (readLine != null && !readLine.equals("")) {
        header.append(readLine).append("\n");
        readLine = br.readLine();
    }
    
  • 接下来有两个选择,第一个是把接收到的报文直接再发出去,第二是做一些处理,如果直接转发可以跳过下面两步(解析和构建)

  • 需要解析这个报文获取必要的东西就行,我只用了效率非常低的暴力办法,而且正确性不一定足够。代码稍微长点就不多贴一次了,具体见最后源码中的parse()方法。

  • 根据刚才解析到的报文头部重新构建一个发送报文:

    requestBuffer.append(method).append(" ").append(visitAddr)
        .append(" HTTP/1.1").append("\r\n")
        .append("HOST: ").append(host).append("\n")
        .append("Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\n")
        .append("Accept-Encoding:gzip, deflate, sdch\n")
        .append("Accept-Language:zh-CN,zh;q=0.8\n")
        .append("If-Modified-Since: ").append(lastModified).append("\n")
        .append("User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15\n")
        .append("Encoding:UTF-8\n")
        .append("Connection:keep-alive" + "\n")
        .append("\n");
    
  • 现在有了报文,创建一个新的Socket作为连接远程服务器的socket:

    // 创建新的socket连接远程服务器
    Socket connectRemoteSocket = new Socket(host, visitPort);
    // 这个是连接远程服务器的socket的stream
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connectRemoteSocket.getOutputStream()));
    // 发送报文
    writer.write(requestBuffer.toString()); 
    writer.flush();
    
  • 现在理一理思路,我们目前到底创建了多少个socket:

    1. 本地服务器socket,即ServerSocket的对象
    2. 用于连接这个程序和浏览器的socket,记作socket1
    3. 程序用来连接远程服务器的socket,记作socket2,和2一样都是Socket的对象。
  • 也就是说,浏览器本来应该直接跟远程服务器通信的,但是被我们拦截了下来,创建了这些socket来衔接。期间的输入输出流都是些什么鬼呢?

    1. 第一个InputStream是用于接收浏览器发送的报文,对于程序来说是一段输入,所以打开了第一个(socket1)的输入流来接收。
    2. 随后打开了连接远程服务器的socket1的输出流,因为我们要发送报文,对于程序来说是一段输出。
    3. 马上我们要打开socket2输入流用于接收服务器的应答,而且还要打开socket1的输出流,把服务器发送的报文再向浏览器输出。
  • 回到程序,刚才向服务器发送了一段报文,服务器也会应答我们,所以下面打开socket2的输入流和socket1的输出流完成这段输入输出,就算是完成了一次代理访问:

2. 加入缓存

这个思路很简单,代码见最后。

第一,要在向浏览器输出来自远程服务器的应答的同时,还要向一个文件里面输出相同的内容,这就是写缓存文件的过程;

第二,在重构报文的时候加入If-Modified-Since字段,后面跟的是GMT时间,我也用了粗暴的方式解决(不要学我,最好改进下)。这里使用缓存文件的最后修改时间作为这个时间。这个字段有什么用呢?发送这段报文给服务器之后,服务器会判断在你发送的时间之后,请求的资源是否被修改了,如果被修改,会返回状态码200 OK,并且包括最新的资源,如果返回 304 Not Modified,则意味着没有修改,后面不会跟随多余的内容,这时就要从我们创建好的缓存文件中读取内容了。

因此第三步,在服务器应答的时候多加一步,判断返回状态码:判断是200还是304来确定是否使用缓存。

3. 屏蔽访问和钓鱼请求

我使用几个Map和Set标记钓鱼网站和禁止访问的人或者资源,这个实现起来思路比缓存更简单。

遇到的问题

传输图片资源的时候会花掉,但是如果把buffer大小改成1个字节,就不会花了,原因还不太清楚……

源码

因为实验时间紧张,加上前期划水,以及个人水平所限,代码质量不佳,仅供参考

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author zyq
 * 简易的面向socket编程实现的HTTP代理。
 */
public class SocketServer {

    /**
     * 重定向主机map。
     */
    private static Map<String, String> redirectHostMap = new HashMap<>();

    /**
     * 重定向访问网址map。
     */
    private static Map<String, String> redirectAddrMap = new HashMap<>();

    /**
     * 禁止访问的网址。
     */
    private static Set<String> forbidSet = new HashSet<>();

    /**
     * 禁止访问的用户。
     */
    private static Set<String> forbidUser = new HashSet<>();

    static {
        // 更改这些内容达到屏蔽访问或钓鱼的目的

        // 这二者要配合使用
        // redirectAddrMap.put("http://jwts.hit.edu.cn/loginLdapQian/", "http://today.hit.edu.cn/");
        // redirectAddrMap.put("http://jwts.hit.edu.cn/loginLdapQian", "http://today.hit.edu.cn/");
        // redirectHostMap.put("jwts.hit.edu.cn", "today.hit.edu.cn");

//        forbidSet.add("http://jwes.hit.edu.cn/");
//        forbidSet.add("http://jwes.hit.edu.cn");
//        forbidSet.add("jwes.hit.edu.cn/");
//        forbidSet.add("jwes.hit.edu.cn");

//        forbidUser.add("127.0.0.1");

    }

    /**
     * 判断site是否被禁止访问.
     *
     * @param site 网址/主机/访问资源
     * @return true表示禁止访问,否则为false
     */
    private static boolean isForbidden(String site) {
        return forbidSet.contains(site);
    }

    /**
     * 重定向主机.
     *
     * @param oriHost 原始主机
     * @return 根据redirectHostMap重定向后的主机
     */
    private static String redirectHost(String oriHost) {
        Set<String> keywordSet = redirectHostMap.keySet();
        for (String keyword : keywordSet) {
            if (oriHost.contains(keyword)) {
                System.out.println("originHost: " + oriHost);
                String redHost = redirectHostMap.get(oriHost);  // 直接修改方案
                System.out.println("redirectHost: " + redHost);
                return redHost;
            }
        }
        return oriHost;
    }

    /**
     * 重定向访问资源.
     * 通常是图片等
     *
     * @param oriAddr 想访问的资源
     * @return 根据redirectAddrMap重定向后的访问资源
     */
    private static String redirectAddr(String oriAddr) {
        Set<String> keywordSet = redirectAddrMap.keySet();
        for (String keyword : keywordSet) {
            if (oriAddr != null && oriAddr.contains(keyword)) {
                System.out.println("originAddr: " + oriAddr);
                String redAddr = redirectAddrMap.get(oriAddr);  // 直接修改方案
                System.out.println("redirectAddr: " + redAddr);
                return redAddr;
            }
        }
        return oriAddr;
    }

    /**
     * 通过header解析各个参数.
     *
     * @param header HTTP报文头部
     * @return map,包含HTTP版本,method,访问内容,主机,端口
     */
    private static Map<String, String> parse(String header) {
        if (header.length() == 0) {
            return new HashMap<>();
        }
        String[] lines = header.split("\\n");
        String method = null;
        String visitAddr = null;
        String httpVersion = null;
        String hostName = null;
        String portString = null;
        for (String line : lines) {
            if ((line.contains("GET") || line.contains("POST") || line.contains("CONNECT")) && method == null) {
                // 这一行包括get xxx httpVersion
                String[] temp = line.split("\\s");  // 按空格分割
                method = temp[0];
                visitAddr = temp[1];
                httpVersion = temp[2];
                // 对addr再获得端口号
                // 端口也在这里
                // 先判断是否包含http://关键字
                if (visitAddr.contains("http://") || visitAddr.contains("https://")) {
                    // 包含
                    // 再判断是否包含端口号
                    String[] temp1 = visitAddr.split(":");
                    // 因为有http://带来的冒号,所以如果长度>=3则有端口号
                    // 且temp[1]是host
                    if (temp1.length >= 3) {
                        portString = temp1[2];
                    }
                } else {
                    // 不包含http
                    String[] temp1 = visitAddr.split(":");
                    // 长度>=2则有端口号
                    if (temp1.length >= 2) {
                        // 有端口号,最后没有斜杠
                        portString = temp1[1];
                    }
                }

            } else if (line.contains("Host: ") && hostName == null) {
                String[] temp = line.split("\\s");
                hostName = temp[1];
                int maohaoIndex = hostName.indexOf(':');
                if (maohaoIndex != -1) {
                    hostName = hostName.substring(0, maohaoIndex);
                }
            }
        }

        Map<String, String> map = new HashMap<>();
        // 构造参数map
        map.put("method", method);
        map.put("visitAddr", visitAddr);
        map.put("httpVersion", httpVersion);
        map.put("host", hostName);
        if (portString == null) {
            map.put("port", "80");
        } else {
            map.put("port", portString);
        }
        return map;
    }


    public static void main(String[] args) throws IOException {
        // 监听指定的端口
        int port = 808;
        ServerSocket server = new ServerSocket(port);
        // server将一直等待连接的到来
        System.out.println("server将一直等待连接的到来");

        // 如果使用多线程,那就需要线程池,防止并发过高时创建过多线程耗尽资源
        // 这是网上推荐的,但是本地访问量不大,没必要使用线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(100);

        while (true) {
            Socket socket = server.accept();
            System.out.println("获取到一个连接!来自 " + socket.getInetAddress().getHostAddress());
            boolean pass = true;
            if (forbidUser.contains(socket.getInetAddress().getHostAddress())) {
                pass = false;
            }
            boolean finalPass = pass;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 解析header
                        InputStreamReader r = new InputStreamReader(socket.getInputStream());
                        BufferedReader br = new BufferedReader(r);
                        String readLine = br.readLine();
                        String host;

                        StringBuilder header = new StringBuilder();

                        while (readLine != null && !readLine.equals("")) {
                            header.append(readLine).append("\n");
                            readLine = br.readLine();
                        }

                        // 在输入流结束之后判断
                        // 判断用户是否被屏蔽
                        if (!finalPass) {
                            System.out.println("From a forbidden user.");
                            PrintWriter pw = new PrintWriter(socket.getOutputStream());
                            pw.println("You are a forbidden user!");
                            pw.close();

                            socket.close();
                            return;
                        }

                        // 打印参数表
                        Map<String, String> map = parse(header.toString());

                        System.out.println("-------------------");
                        System.out.println(map);
                        System.out.println("-------------------");

                        host = map.get("host"); // host
                        // 端口
                        String portString = map.getOrDefault("port", "80");
                        // 端口
                        int visitPort = Integer.parseInt(portString);
                        // 访问的网站
                        String visitAddr = map.get("visitAddr");
                        // method
                        String method = map.getOrDefault("method", "GET");

                        // 判断是否屏蔽掉这个网站
                        if (visitAddr != null && isForbidden(visitAddr)) {
                            // 被屏蔽,不允许访问
                            System.out.println("Visiting a forbidden site.");
                            PrintWriter pw = new PrintWriter(socket.getOutputStream());
                            pw.println("You can not visit " + visitAddr + "!");
                            pw.close();
                        } else {
                            // 获得跳转主机和资源
                            String tempRedAddr = redirectAddr(visitAddr);
                            if (!tempRedAddr.equals(visitAddr)) {
                                visitAddr = tempRedAddr;
                                host = redirectHost(host);
                            }

                            // 看看在不在缓存中
                            // 获得一下文件
                            File cacheFile = new File(visitAddr.replace('/', 'g') + ".mycache");
                            boolean useCache = false;   // 标记是否用cache

                            // 默认的最后修改时间,用于文件不存在的时候
                            String lastModified = "Thu, 01 Jul 1970 20:00:00 GMT";

                            if (cacheFile.exists() && cacheFile.length() != 0) {
                                // 文件存在且大小不为0,说明访问内容被缓存过
                                System.out.println(visitAddr + " 有缓存");
                                // 获得修改时间
                                Calendar cal = Calendar.getInstance();
                                long time = cacheFile.lastModified();
                                SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH);
                                cal.setTimeInMillis(time);
                                cal.set(Calendar.HOUR, -7);
                                cal.setTimeZone(TimeZone.getTimeZone("GMT"));
                                lastModified = formatter.format(cal.getTime());
                                System.out.println(cal.getTime());
                            }

                            // 已经知道时间了,发报文
                            // 向远程服务器发送请求报文,加入if modified since
                            // 创建新的socket连接远程服务器
                            Socket connectRemoteSocket = new Socket(host, visitPort);

                            // 这个是连接远程服务器的socket的stream
                            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connectRemoteSocket.getOutputStream()));

                            StringBuffer requestBuffer = new StringBuffer();
                            requestBuffer.append(method).append(" ").append(visitAddr)
                                    .append(" HTTP/1.1").append("\r\n")
                                    .append("HOST: ").append(host).append("\n")
                                    .append("Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\n")
                                    .append("Accept-Encoding:gzip, deflate, sdch\n")
                                    .append("Accept-Language:zh-CN,zh;q=0.8\n")
                                    .append("If-Modified-Since: ").append(lastModified).append("\n")
                                    .append("User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15\n")
                                    .append("Encoding:UTF-8\n")
                                    .append("Connection:keep-alive" + "\n")
                                    .append("\n");
                            writer.write(requestBuffer.toString()); // 发送报文
                            // 打印看看
                            System.out.println(requestBuffer.toString());
                            // 发送报文
                            writer.flush();

                            // 这是向浏览器输出的输出流
                            OutputStream outToBrowser = socket.getOutputStream();

                            // 文件输出流
                            FileOutputStream fileOutputStream =
                                    new FileOutputStream(
                                            new File(visitAddr.replace('/', 'g') + ".mycache"));

                            // 从远程服务器获得输入的输入流
                            BufferedInputStream remoteInputStream =
                                    new BufferedInputStream(connectRemoteSocket.getInputStream());

                            // 先使用一个小字节缓存获得头部,包含第一行就够了
                            byte[] tempBytes = new byte[20];
                            int len = remoteInputStream.read(tempBytes);
                            String res = new String(tempBytes, 0, len);
                            System.out.println(res);
                            // 判断是否包含304,如果是包含,标记为使用缓存
                            if (res.contains("304")) {
                                // 远程服务器没有更新这个资源,可以直接使用缓存
                                System.out.println(visitAddr + " 服务器内容未变更,使用缓存");
                                // 刚才的小字节也不要了,后续的报文读完不用,然后直接从文件读
                                useCache = true;    // 用缓存
                            } else {
                                System.out.println(visitAddr + " 服务器内容可能变更,不使用缓存");

                                // 没有缓存,刚才临时读入的要用上。并且要接着读报文并向浏览器输出
                                outToBrowser.write(tempBytes);

                                // 临时字节写入缓存文件
                                fileOutputStream.write(tempBytes);
                            }
                            if (useCache) {
                                // 用缓存
                                // 这是向浏览器输出的输出流
                                System.out.println(visitAddr + " 正在使用缓存加载");
                                // 建立文件读写
                                FileInputStream fileInputStream = new FileInputStream(cacheFile);
                                int bufferLength = 1;
                                byte[] buffer = new byte[bufferLength];
                                int count;

                                while (true) {
                                    count = fileInputStream.read(buffer);
                                    System.out.println("Reading>.... From file>..." + count);
                                    if (count == -1) {
                                        break;
                                    }
                                    outToBrowser.write(buffer);
                                }
                                outToBrowser.flush();
                            }
                            // 不管用不用缓存都要接着读完来自服务器的数据
                            int bufferLength = 1;
                            byte[] buffer = new byte[bufferLength];
                            int count;
                            System.out.println("Start reading!>.....From > " + visitAddr);
                            while (true) {
                                count = remoteInputStream.read(buffer);
                                if (count == -1) {
                                    break;
                                }
                                if (!useCache) {
                                    // 不用缓存才写这些
                                    outToBrowser.write(buffer);
                                    fileOutputStream.write(buffer);
                                }
                            }
                            fileOutputStream.flush();   // 输出到文件
                            fileOutputStream.close();   // 关闭文件流
                            System.out.println("finish");

                            outToBrowser.flush();   // 输出到浏览器
                            connectRemoteSocket.close();    // 关闭连接远程服务器的socket

                        }
                        socket.close();// 关闭浏览器与程序的socket
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }


}

  • 20
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值