Skip to content

Proxy Lab

Posted on:2022.02.06

TOC

Open TOC

Proxy Lab

preparation

tar

vgalaxy@vgalaxy-VirtualBox:~/Downloads$ tar xf proxylab-handout.tar
vgalaxy@vgalaxy-VirtualBox:~/Downloads$ tar xvf proxylab-handout.tar

RFC 1945

https://datatracker.ietf.org/doc/html/rfc1945

part

  1. 实现一个基础的 Web proxy
  2. 处理并发问题
  3. 向 Web proxy 加入缓存机制

proxy

server ⇔ proxy ⇔ browser (client)

tiny

make 得到可执行文件后

使用 port-for-user.pl 得到端口号

vgalaxy@vgalaxy-VirtualBox:~/Desktop/csapp/lab/proxylab-handout$ ./port-for-user.pl
vgalaxy: 29718

然后运行 ./tiny 29718

并在浏览器中进入 http://localhost:29718/

可以得到如下的信息

Accepted connection from (localhost, 54094)
GET / HTTP/1.1
Host: localhost:29718
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1

注意这里的 29718 是服务器的端口号,54094 是浏览器的临时端口号

第一行是服务器 accept 成功后打印的信息

下面的内容则是来自浏览器的 HTTP 请求,包括请求行和请求报头

我们也可以使用 telnet 访问服务器

vgalaxy@vgalaxy-VirtualBox:~$ telnet localhost 29718
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

这里的 localhost 为域名,数字地址为 127.0.0.1,即为 host

键入

GET / HTTP/1.1

服务器会打印如下信息

Accepted connection from (localhost, 54220)
GET / HTTP/1.1

并返回 telnet 如下信息

HTTP/1.0 200 OK
Server: Tiny Web Server
Content-length: 120
Content-type: text/html
<html>
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O'Hallaron
</body>
</html>
Connection closed by foreign host.

由于我们没有设置请求报头 Connection,服务器终止了连接

上述的浏览器会处理这些信息,从而显示出了网页

tools

tiny is listening on port 15213
proxy is listening on port 15214

you can request a page from Tiny via your proxy using the following curl command:

curl -v --proxy http://localhost:15214 http://localhost:15213/home.html

一方面可以当做 telnet 使用

另一方面可以充当服务器

nc -l 12345

服务器只会打印 HTTP 请求,不会发送客户端任何信息

Network Settings -> Proxy

当测试 cache 时,需要关掉 browser 自带的 cache

note

Part Ⅰ Basic

requirement

Web proxy 需要响应客户端的请求,并能够正确处理 GET 请求,即 Web proxy 需要向服务器发送 HTTP/1.0 GET 请求,并接受来自服务器的响应,最终将结果发送给客户端

需要参考 RFC 1945,以应对客户端的不同请求

当 Web proxy 向服务器发送 HTTP 请求时,需要包含如下的请求报头:

除了 proxy 的端口号,客户端的 HTTP 请求中也可能包含端口号,如在浏览器中键入

http://www.cmu.edu:8080/hub/index.html

Web proxy should connect to the host www.cmu.edu on port 8080 instead of the default HTTP port, which is port 80

也就是说 Web proxy 应当向 www.cmu.edu 的 8080 端口发送如下的 HTTP 请求

GET /hub/index.html HTTP/1.0

driver

启动 tiny 和 proxy

依次对列表中的文件进行 download_proxy 和 download_noproxy

最后对比文件是否一致

process

首先确认客户端的 HTTP 请求,从而进行解析

手动启动 proxy 和 tiny 后

注意需要在 tiny 文件夹内启动服务器,否则会 404

键入

curl -v --proxy http://localhost:29718 http://localhost:29719/home.html
curl --max-time 5 --silent --proxy http://localhost:29718 --output home.html http://localhost:29719/home.html
curl --max-time 5 --silent --output home.html http://localhost:29719/home.html

发现客户端的 HTTP 请求有固定的形式

vgalaxy@vgalaxy-VirtualBox:~/Desktop/csapp/lab/proxylab-handout$ ./proxy 29718
Accept connection from (localhost, 37434)
HTTP request from client:
GET http://localhost:29719/home.html HTTP/1.1

从中解析出端口号和文件名后,生成 Web proxy 向服务器发送的 HTTP 请求:

GET /home.html HTTP/1.0
Host: localhost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3
Connection: close
Proxy-Connection: close

Web proxy 通过端口号连接服务器,并发送上述 HTTP 请求

观察到测试文件的最大大小约为 39KB,设置 MAXFILESIZE 为 81920B,使用无缓冲的 RIO 函数一次性读写

Web proxy 在得到服务器的 HTTP 响应后,不加修改直接返回给客户端

于是 Basic 就通过了……

*** Basic ***
Starting tiny on 3411
Starting proxy on 12859
1: home.html
Fetching ./tiny/home.html into ./.proxy using the proxy
Fetching ./tiny/home.html into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
2: csapp.c
Fetching ./tiny/csapp.c into ./.proxy using the proxy
Fetching ./tiny/csapp.c into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
3: tiny.c
Fetching ./tiny/tiny.c into ./.proxy using the proxy
Fetching ./tiny/tiny.c into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
4: godzilla.jpg
Fetching ./tiny/godzilla.jpg into ./.proxy using the proxy
Fetching ./tiny/godzilla.jpg into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
5: tiny
Fetching ./tiny/tiny into ./.proxy using the proxy
Fetching ./tiny/tiny into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
Killing tiny and proxy
basicScore: 40/40

note

由于在测试中客户端请求的服务器始终为 tiny 服务器,HTTP 请求格式也已知

所以并未实现 writeup 中的全部要求

理论上客户端可以向 proxy 请求任意服务器,HTTP 请求格式也无法预先得知

不过从测试的角度来说,这就足够了

Part Ⅱ Concurrency

有如下几种做法:

考虑到第三阶段有缓存这个共享变量,我们使用基于线程的方法

requirement

proxy 需要能够应对客户端的并发请求

driver

启动 tiny 和 proxy 后,启动另一个服务器 nop-server,该服务器会阻塞客户端的请求,不作任何响应

然后依次对列表中的文件进行 download_proxy 和 download_noproxy

最后对比文件是否一致

process

简单的照着课本写一遍就可以通过

*** Concurrency ***
Starting tiny on port 30072
Starting proxy on port 21031
Starting the blocking NOP server on port 16346
Trying to fetch a file from the blocking nop-server
Fetching ./tiny/home.html into ./.noproxy directly from Tiny
Fetching ./tiny/home.html into ./.proxy using the proxy
Checking whether the proxy fetch succeeded
Success: Was able to fetch tiny/home.html from the proxy.
Killing tiny, proxy, and nop-server
concurrencyScore: 15/15

目前由于 doit 是线程安全的函数,所以不需要进行同步

Part Ⅲ Caching

requirement

向 Web proxy 加入缓存机制

proxy 的缓存总大小为 1MB,只缓存 ≤ 100KB 的文件

注:这里的大小不包含元数据,如响应行和响应报头

驱逐策略为 LRU,同时,需要考虑客户端对缓存的并发读写需求,即 Readers-Writers Problem

driver

启动 tiny 和 proxy 后

依次对列表中的文件进行 download_proxy

然后关闭 tiny 服务器

再对某个列表中特定文件进行 download_proxy

最后对比文件是否一致

process

设计如下的 Cache

typedef struct {
char filename[MAXLINE];
char contents[MAX_OBJECT_SIZE];
ssize_t bytes;
time_t time_tag;
} file_t;
#define CACHE_SETS 10
static file_t file_cache[CACHE_SETS];

Cache 通过文件名索引,全相联映射

contents 中存放了 HTTP 响应的全部信息,包括一些元数据

使用 time_tag,配合线程安全的 time 函数实现 LRU 驱逐策略

由于读 file_cache 时会更新 time_tag,所以不考虑 Readers-Writers Problem,读写 file_cache 时均使用信号量同步

由于 proxy 在读服务器的响应时,需要动态的判断响应内容的大小是否足以缓存

所以修改 proxy 每次读 MAXBUF 个字节,立刻返回给客户端,并逐步拷贝到 cache_buf 中,当不够缓存时,则放弃拷贝

void server_reply(int fd, char *host, char *port, char *request,
char *filename) {
int clientfd = Open_clientfd(host, port);
char *buf = (char *)Calloc(MAXBUF, sizeof *buf);
char *cache_buf = (char *)Calloc(MAX_OBJECT_SIZE, sizeof *cache_buf);
ssize_t byte_cnt = 0;
int is_cached = 1;
Rio_writen(clientfd, request, MAXLINE);
while (1) {
ssize_t state = Rio_readn(clientfd, buf, MAXBUF);
if (state == 0) { // EOF
break;
} else if (state == -1) {
clienterror(fd, "GET", "500", "Internal Server Error",
"Web proxy encounters an error while reading from server");
Close(clientfd);
Free(buf);
Free(cache_buf);
return;
} else {
Rio_writen(fd, buf, state);
if (byte_cnt + state < MAX_OBJECT_SIZE) {
memcpy(cache_buf + byte_cnt, buf, state);
byte_cnt += state;
} else {
is_cached = 0;
}
}
}
Close(clientfd);
Free(buf);
if (is_cached)
update_cache(cache_buf, filename, byte_cnt);
Free(cache_buf);
}

然后测试就可以通过了

*** Cache ***
Starting tiny on port 32087
Starting proxy on port 31810
Fetching ./tiny/tiny.c into ./.proxy using the proxy
Fetching ./tiny/home.html into ./.proxy using the proxy
Fetching ./tiny/csapp.c into ./.proxy using the proxy
Killing tiny
Fetching a cached copy of ./tiny/home.html into ./.noproxy
Success: Was able to fetch tiny/home.html from the cache.
Killing proxy
cacheScore: 15/15

Part Ⅳ Web browsers

由于测试实在太弱,我们考虑让 proxy 处理实际环境

使用 Firefox

设置代理服务器

ddadad8937c3405fa6a7468cac1250dc.png

禁用缓存

04ac6d81496f42689c33abe005c5cf0a.png

同时,我们需要修改解析 uri 和发送 HTTP 请求的地方,使其通用化

使用 POSIX 的正则表达式库解析 uri

目前只支持如下两种格式:

http://csapp.cs.cmu.edu/3e/labs.html
http://www.cmu.edu:8080/hub/index.html

由于 POSIX 的正则表达式库总是 greedy 的,所以还要进行一些修正

配置好后,我们访问 http://csapp.cs.cmu.edu/3e/labs.html

根据 proxy 的调试信息,可以拦截到如下的 HTTP 请求

GET http://csapp.cs.cmu.edu/3e/labs.html HTTP/1.1
CONNECT incoming.telemetry.mozilla.org:443 HTTP/1.1
GET http://csapp.cs.cmu.edu/3e/css/csapp.css HTTP/1.1
GET http://feeds.feedburner.com/csapp?format=sigpro&nItems=15&displayExcerpts=true&excerptFormat=plain&excerptLength=25&displayDate=true&dateLocation=below HTTP/1.1
GET http://csapp.cs.cmu.edu/3e/images/csapp3e-cover.jpg HTTP/1.1
GET http://csapp.cs.cmu.edu/3e/images/new.gif HTTP/1.1
GET http://feedburner.google.com/fb/i/icn/feed-icon-10x10.gif HTTP/1.1
GET http://feedburner.google.com/fb/images/buzzboost-pwrd.gif HTTP/1.1
GET http://www.google-analytics.com/ga.js HTTP/1.1
GET http://csapp.cs.cmu.edu/favicon.ico HTTP/1.1
CONNECT incoming.telemetry.mozilla.org:443 HTTP/1.1
GET http://detectportal.firefox.com/success.txt HTTP/1.1
GET http://detectportal.firefox.com/success.txt?ipv4 HTTP/1.1
GET http://detectportal.firefox.com/success.txt?ipv6 HTTP/1.1
CONNECT push.services.mozilla.com:443 HTTP/1.1

再次访问该网站时,只有如下的 HTTP 请求

GET http://feeds.feedburner.com/csapp?format=sigpro&nItems=15&displayExcerpts=true&excerptFormat=plain&excerptLength=25&displayDate=true&dateLocation=below HTTP/1.1
GET http://feedburner.google.com/fb/i/icn/feed-icon-10x10.gif HTTP/1.1
GET http://feedburner.google.com/fb/images/buzzboost-pwrd.gif HTTP/1.1

从中可以发现一些现象:

[proxy.c, 76, main] Accept connection from (localhost, 33770)
[proxy.c, 76, main] Accept connection from (localhost, 33772)
[proxy.c, 76, main] Accept connection from (localhost, 33774)
[proxy.c, 76, main] Accept connection from (localhost, 33776)

其他大部分网站均无法访问……

Summary

这个 lab 涉及到了相当多的内容:

虽然可以用不到 500 行的代码实现一个简易的 Web Proxy,但这背后依赖了很多已有的库:

另外,Web Proxy 在错误处理方面也不够完善,代码中只出现了三处 clienterror:

这里随便使用了 500 这个 status code

还可以再完善的地方: