> ## Content Index
> Fetch the complete content index at: https://blog.hacking.pub/llms.txt
> Use this file to discover other available public pages before exploring further.

# golang proxy
- URL: https://blog.hacking.pub/2016/09/06/hello-world/
- Published: 2016-09-06T13:23:51.000Z
- Updated: 2016-10-29T16:25:41.000Z
- Author: chainhelen

先介绍几个概念

### 文件尾

linux系统有个重要的思想就是一切皆文件。

1. 普通文件:打开文件文件时程序能拿到文件操作符，当已经读取的数据等于文件本身大小时，返回一个EOF常量。
2. 标准文件:特殊字符判断文件尾，例如linux终端Ctrl+D。
3. socket流:被动关闭方接收主动接受方发送的FIN数据包时，被动接受方的数据会从系统那拿到一个ZERO值。

### http协议

介绍http，网上太多了，就讲一个我遇到的坑。

```golang
package main
    import (
	"fmt"
	"log"
	"net"
)
func readFromConn(conn net.Conn) []byte {
	const HTTP_LEN = 20480
	const BUF_LEN = 256

	buf := make([]byte, HTTP_LEN)
	buflen := 0
	tmp := make([]byte, BUF_LEN)

	for {
		n, err := conn.Read(tmp)
		if err != nil {
			if err.Error() != "EOF" {
				log.Fatal("io.error  ", err)
				return nil
			}
			break
		}

		if buflen+n > HTTP_LEN {
			log.Fatal("HTTP is too langer")
			return nil
		}
		if 0 != n {
			copy(buf[buflen:], tmp)
			buflen += n
		} else {
			break
		}
	}
	return buf[:buflen]
}
func main() {

	conn, err := net.Dial("tcp", "www.baidu.com:80")
	defer conn.close()
	if err != nil {
		fmt.Printf("%v\n", err)
		return
	}
	fmt.Printf("use localAddr to connec %v\n", conn.LocalAddr().String())
	//fmt.Fprintf(conn, "GET / HTTP/1.1\r\n\r\n")
	n, errW := conn.Write([]byte("GET / HTTP/1.0\r\n\r\n"))
	fmt.Printf("n       is %v\n", n)
	if errW != nil {
           fmt.Printf("errW    is %v\n", errW)
           return
	}
	buf := readFromConn(conn)
	if nil != buf {
		fmt.Printf("%s\n", buf)
	} else {
		fmt.Printf("nil")
	}
}

```

重点是注释的那行和它下面几行，百度或者其他几个网站，对于HTTP 1.1版本GET请求头中如果只含有 “GET / HTTP/1.1\\r\\n\\r\\n“ ，那么不会返回完整页面，但是1.0版本的就可以

### 文件描述符

**fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体，也就是说，file结构体的引用计数要增加。**  
假设 fd1 和 fd2 两个文件描述符都指向同一个 file 结构体，当使用 close 系统调用关闭 fd1后， fd2 仍可以使用。这是因为在 file 结构体中还维护者一个引用计数 count，当 close(fd1) 后，count 减 1，只有在 count 为 0 时，内核才会真正的释放 file 结构体真正关闭文件。可以做一下实验：

```
#include <stdio.h>
#include <unistd.h>

int main() {
    FILE * fp = fopen("1.txt", "a");

    if(0 == fork()) {
        printf("the child process , the pid is %d\n", getpid());
        fputs("the child process write ok\n", fp);
        fclose(fp);
        return 0;
    }

    fputs("the parent process write ok\n", fp);
    fclose(fp);

    return 0;
}

```

可以看到代码中只有一处fopen，但是有两处fclose。最终结果：

```
ch@ch-pc:~/workspace/main_test_file$ cat 1.txt 
the parent process write ok
the child process write ok

```

[相关文章](https://my.oschina.net/u/660063/blog/201187?ref=blog.hacking.pub)

## http代理

写http代理分为**两种情况**

- 一种是普通的http报文，代理服务器直接转发就行了。
- 一种是https报文，需要做要验证。

验证的时候，首先客户端（web浏览器）发送CONNECT请求

```
CONNECT / HTTP/1.1
Host: www.baidu.com:80
Proxy-Connection: Keep-Alive
Proxy-Authorization: Basic *
Content-Length: 0

```

其中\*号 是 用户名:密码 的base64编码  
验证成功，代理服务器就返回

```
HTTP/1.1 200 Connection Established

```

不成功，就返回

```
HTTP/1.1 407 Unauthorized

```

如此，当验证成功过后，后面的流程跟普通的http请求一样转发

###### connection 和 proxy-connection

[区别在这](https://imququ.com/post/the-proxy-connection-header-in-http-request.html?ref=blog.hacking.pub)，“如果浏览器对这样的代理发送了 Connection: Keep-Alive，那么结果会变得很复杂。这个 Header 会被不理解它的代理原封不动的转给服务端，如果服务器也不能理解就还好，能理解就彻底杯具了。服务器并不知道 Keep-Alive 是由代理错误地转发而来，它会认为代理希望建立持久连接，服务端同意之后也返回一个 Keep-Alive。同样，响应中的 Keep-Alive 也会被代理原样返给浏览器，同时代理还会傻等服务器关闭连接——实际上，服务端已经按照 Keep-Alive 指示保持了连接，即使数据回传完成，也不会关闭连接。另一方面，浏览器收到 Keep-Alive 之后，会复用之前的连接发送剩下的请求，但代理不认为这个连接上还会有其他请求，请求被忽略。这样，浏览器会一直处于挂起状态，直到连接超时。"

"这个问题最根本的原因是代理服务器转发了禁止转发的 Header。但是要升级所有老旧的代理也不是件简单的事，所以浏览器厂商和代理实现者协商了一个变通的方案：首先，显式给浏览器设置代理后，浏览器会把请求头中的 Connection 替换为 Proxy-Connetion。这样，对于老旧的代理，它不认识这个 Header，会继续发给服务器，服务器也不认识，代理和服务器之间不会建立持久连接（不能正确处理 Connection 的都是 HTTP/1.0 代理），服务器不返回 Keep-Alive，代理和浏览器之间也不会建立持久连接。而对于新代理，它可以理解 Proxy-Connetion，会用 Connection 取代无意义的 Proxy-Connection，并将其发送给服务器，以收到预期的效果。”

###### 简易的实现

我自己写的很简单的实现 [代码传送门](https://github.com/chainhelen/go%5Fproxy?ref=blog.hacking.pub)

## sock代理

sock代理的[详细流程](https://my.oschina.net/u/660063/blog/201187?ref=blog.hacking.pub)  
sock又分sock4，sock5，tcp和udp  
这里只提一下sock5的tcp，实现了其中简单的部分。

- 客户端(web浏览器)可以发送三种请求代理

> 05 01 00 3个字节 匿名代理  
> 05 01 02 3个字节 用户名密码方式验证  
> 05 02 00 02 4个字节 匿名或用户名密码

- 代理服务器 返回

> 05 00 2个字节 允许匿名  
> 05 02 2个字节 邀请验证

- 如果邀请验证，就会多一步交互，客户端发送 01 06 6C 61 6F 74 73 65 06 36 36 36 38 38 38

> 01 固定，不可改  
> 06 指用户名长度，后面的是用户名  
> 6C 61 6F 74 73 65 用户名的accii码  
> 06 指密码的长度  
> 36 36 36 38 38 38密码的ascii码

- 代理服务器如果验证失败，直接关闭连接即可。如果成功，返回01 00
- 下面只看tcp的访问域名，客户端发送 05 01 00 03 13 77 65 62 2E 73 6F 75 72 63 65 66 6F 72 67 65 2E 6E 65 74 00 16

> **1.** 05固定  
> **2.** 01是tcp  
> **3.** 03说明是域名  
> **4.** 13是域名长度 0x13 = 19  
> **5.** 77 65 62 2E 73 6F 75 72 63 65 66 6F 72 67 65 2E 6E 65 74 代表域名ascii码  
> **6.** 00 16 代表端口

- 上诉客户端报文后面的部分都会被代理服务器转发至远程服务器
- 对于tcp的IP访问 是这种形式 : 发送 05 01 00 01 + 目的地址(4字节） +  
目的端口（2字节）

###### 注意事项

1. 从上叙描述来看，可以知道，dns的解析既可以放在客户端，也可以放在代理服务器上。
2. 在chrome浏览器里面设置好代理后，访问120.24.180.89，发现报文的前四个是05 01 00 03。从这点可以看出浏览器并不认为搜索框里面的120.24.180.89是个域名。

[代码戳这里](https://github.com/chainhelen/go%5Fproxy?ref=blog.hacking.pub)