golang proxy

先介绍几个概念

文件尾

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

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

http协议

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

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  

相关文章

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

区别在这,“如果浏览器对这样的代理发送了 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,并将其发送给服务器,以收到预期的效果。”

简易的实现

我自己写的很简单的实现 代码传送门

sock代理

sock代理的详细流程

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是个域名。

代码戳这里