├── .gitignore
├── .idea
├── misc.xml
├── modules.xml
├── sogo.iml
└── vcs.xml
├── README.md
├── init.go
├── main.go
├── mio
├── io.go
└── prefix.go
├── sogo.json
└── utils
└── tool.go
/.gitignore:
--------------------------------------------------------------------------------
1 | sogo*
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/sogo.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sogo
2 |
3 | 一个使用http流量进行混淆的socks5代理。
4 |
5 | 服务器端:[sogo-server](https://github.com/arloor/sogo-server)
6 |
7 | 之前写了一个http代理,用起来也是十分地舒服,但是有几个点还是有些遗憾的:
8 |
9 | - http代理只能代理http协议,相比socks5代理不够通用。。
10 | - netty是个好框架,但是java占用内存是真的多。。
11 |
12 | 所以,我又写了一个socks5代理,起名叫[sogo](https://github.com/arloor/sogo)。
13 |
14 | sogo本身包含sogo(client)和sogo-server。如果把sogo和sogo-server看成一个整体,一个黑盒,这个整体就是一个socks5代理。sogo(client)与本地电脑交互;sogo-server与目标网站交互;sogo(client)和sogo-server之间的交互就是http协议包裹payload进行通信。
15 |
16 |
17 | ## 电报讨论组
18 |
19 | 电报讨论组 https://t.me/popstary
20 |
21 | ## 运行日志
22 |
23 | 以下是观看一个视频的日志:
24 |
25 | ```shell
26 | 2019/04/10 00:14:28 main.go:58: 与客户端握手成功
27 | 2019/04/10 00:14:28 main.go:207: 目的地址类型:3 域名长度:15 目标域名:www.youtube.com 目标端口:443
28 | 2019/04/10 00:14:28 main.go:58: 与客户端握手成功
29 | 2019/04/10 00:14:28 main.go:207: 目的地址类型:3 域名长度:11 目标域名:i.ytimg.com 目标端口:443
30 | 2019/04/10 00:14:28 main.go:58: 与客户端握手成功
31 | 2019/04/10 00:14:28 main.go:207: 目的地址类型:3 域名长度:13 目标域名:yt3.ggpht.com 目标端口:443
32 | 2019/04/10 00:14:35 main.go:58: 与客户端握手成功
33 | 2019/04/10 00:14:35 main.go:207: 目的地址类型:3 域名长度:32 目标域名:r2---sn-i3belnel.googlevideo.com 目标端口:443
34 | 2019/04/10 00:14:35 main.go:58: 与客户端握手成功
35 | 2019/04/10 00:14:35 main.go:207: 目的地址类型:3 域名长度:32 目标域名:r2---sn-i3belnel.googlevideo.com 目标端口:443
36 | 2019/04/10 00:14:35 main.go:58: 与客户端握手成功
37 | 2019/04/10 00:14:35 main.go:207: 目的地址类型:3 域名长度:32 目标域名:r2---sn-i3belnel.googlevideo.com 目标端口:443
38 | ```
39 |
40 | 以下是访问github的日志:
41 | ```shell
42 | 2019/04/10 00:15:57 main.go:58: 与客户端握手成功
43 | 2019/04/10 00:15:57 main.go:207: 目的地址类型:3 域名长度:10 目标域名:github.com 目标端口:443
44 | 2019/04/10 00:15:57 main.go:58: 与客户端握手成功
45 | 2019/04/10 00:15:57 main.go:207: 目的地址类型:3 域名长度:10 目标域名:github.com 目标端口:443
46 | 2019/04/10 00:15:59 main.go:58: 与客户端握手成功
47 | 2019/04/10 00:15:59 main.go:207: 目的地址类型:3 域名长度:15 目标域名:live.github.com 目标端口:443
48 | 2019/04/10 00:16:00 main.go:58: 与客户端握手成功
49 | 2019/04/10 00:16:00 main.go:207: 目的地址类型:3 域名长度:14 目标域名:api.github.com 目标端口:443
50 | ```
51 |
52 |
53 |
54 | ## 特性
55 |
56 | sogo项目最好的两个特性如下:
57 |
58 | 1. 使用http包裹payload(有意义的数据)。
59 | 2. 将sogo-server所在的ip:端口伪装成一个http网站。
60 |
61 | 效用、坚固、美观——对软件产品的三个要求。上面两个特性,既可以说是坚固,也可以说是美观,至于效用就不用说了,在这里谈坚固和美观的前提就是效用被完整地实现。用通俗地话来说,这个代理的坚固和美观就是:伪装、防止被识别。
62 |
63 | ## 处理socks5握手——对socks5协议的实现
64 |
65 | sogo(client)与本地电脑交互,因此需要实现socks5协议,与本地用户(比如chrome)握手协商。
66 |
67 | 一个典型的sock5握手的顺序:
68 |
69 | 1. client:0x05 0x01 0x00
70 | 2. server: 0x05 0x00
71 | 3. client: 0x05 0x01 0x00 0x01 ip1 ip2 ip3 ip4 0x00 0x50
72 | 4. server: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x10 0x10
73 | 5. client与server开始盲转发
74 |
75 | 这一部分代码见如下两个函数:
76 |
77 | ```java
78 | //file: sogo/main.go
79 |
80 | //读 5 1 0 写回 5 0
81 | func handshake(clientCon net.Conn) error {
82 | var buf = make([]byte, 300)
83 | numRead, err := clientCon.Read(buf)
84 | if err != nil {
85 | return err
86 | } else if numRead == 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 {
87 | return mio.WriteAll(clientCon, []byte{0x05, 0x00})
88 | } else {
89 | log.Printf("%d", buf[:numRead])
90 | return mio.WriteAll(clientCon, []byte{0x05, 0x00})
91 | }
92 | }
93 |
94 | func getTargetAddr(clientCon net.Conn) (string, error) {
95 | var buf = make([]byte, 1024)
96 | numRead, err := clientCon.Read(buf)
97 | if err != nil {
98 | return "", err
99 | } else if numRead > 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 {
100 | if buf[3] == 3 {
101 | log.Printf("目的地址类型:%d 域名长度:%d 目标域名:%s 目标端口:%s", buf[3], buf[4], buf[5:5+buf[4]], strconv.Itoa(int(binary.BigEndian.Uint16(buf[5+buf[4]:7+buf[4]]))))
102 | writeErr := mio.WriteAll(clientCon, []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10})
103 | return string(buf[5:5+buf[4]]) + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[5+buf[4]:7+buf[4]]))), writeErr
104 | } else if buf[3] == 1 {
105 | log.Printf("目的地址类型:%d 目标域名:%s 目标端口:%s", buf[3], net.IPv4(buf[4], buf[5], buf[6], buf[7]).String(), strconv.Itoa(int(binary.BigEndian.Uint16(buf[8:10]))))
106 | writeErr := mio.WriteAll(clientCon, []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10})
107 | return net.IPv4(buf[4], buf[5], buf[6], buf[7]).String() + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[8:10]))), writeErr
108 | } else {
109 | return "", errors.New("不能处理ipv6")
110 | }
111 |
112 | } else {
113 | return "", errors.New("不能处理非CONNECT请求")
114 | }
115 | }
116 | ```
117 |
118 | 完成handshake, getTargetAddr 之后,chrome就会发送真实的http请求了,sogo(client) 要做的就是将这部分http请求进行加密,然后加上http请求的头,发送到sogo-server。
119 |
120 | ## 使用http包裹payload
121 |
122 | 第一部分:如何将真实的http请求,再进行加密,最后加上假的http请求头,变成伪装好的http请求,发送给sogo-server。
123 |
124 |
125 | ```java
126 | //file: sogo/mio/prefix.go
127 | var fakeHost = "qtgwuehaoisdhuaishdaisuhdasiuhlassjd.com" //虚假host
128 |
129 | func AppendHttpRequestPrefix(buf []byte, addr string) []byte {
130 | Simple(&buf, len(buf))//对真实的http请求的简单加密
131 | // 演示base64编码
132 | addrBase64 := base64.NewEncoding("abcdefghijpqrzABCKLMNOkDEFGHIJl345678mnoPQRSTUVstuvwxyWXYZ0129+/").EncodeToString([]byte(addr))
133 | buf = append([]byte("POST /target?at="+addrBase64+" HTTP/1.1\r\nHost: "+fakeHost+"\r\nAccept: */*\r\nContent-Type: text/plain\r\naccept-encoding: gzip, deflate\r\ncontent-length: "+strconv.Itoa(len(buf))+"\r\n\r\n"), buf...)
134 | return buf
135 | }
136 | ```
137 |
138 | 包裹完毕之后返回的[]byte就可以发送给sogo-server了。
139 |
140 | 第二部分:将sogo-server从目标网站获得的真实响应进行简单加密,包裹http响应头,发送给sogo(client)。
141 |
142 |
143 |
144 |
145 | ```java
146 | //file: sogo-server/mio/prefix.go
147 | func AppendHttpResponsePrefix(buf []byte) []byte {
148 | Simple(&buf, len(buf))
149 | buf = append([]byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: "+strconv.Itoa(len(buf))+"\r\n\r\n"), buf...)
150 | return buf
151 | }
152 | ```
153 |
154 | 包裹完毕之后返回的[]byte就可以发送给sogo(client)了。
155 |
156 | ## 解包伪装好的请求、响应
157 |
158 | 先看以下伪装好的请求的样子:
159 |
160 | ```shell
161 | POST /target?at={targetAddrBase64} HTTP/1.1
162 | Host: {fakehost}
163 | Accept: */*
164 | Content-Type: text/plain
165 | accept-encoding: gzip, deflate
166 | content-length: {content-length}
167 |
168 | {payload-after-crypto}
169 | ```
170 |
171 | sogo-server拿到这个伪装好的请求,要做的事有:
172 |
173 | 1. 获取{targetAddrBase64},拿到真实的目标网站地址
174 | 2. 获取请求头的Host字段,如果不是定义好的fakehost,则说明是直接访问sogo-server,这时sogo-server就是个到混淆网站的反向代理(这就是之前提到的第二个特性。下面将会详细解释如何实现
175 | 3. 获取{content-length},根据这个content-length确定payload部分的长度。
176 | 4. 读取指定长度的payload,解密,并创建到targetAddr的连接,转发至targetAddr
177 |
178 | 这些步骤很明确吧。其实有一些细节,挺麻烦的。
179 |
180 | tcp是面向流的协议,也就是会有很多个连续的上面的片段,要合理划分出这些片段。有些人称这个为解决“tcp粘包”,谷歌tcp粘包就能搜到如何实现这个需求。但是注意,不要称这个为“tcp粘包”,别人会说tcp是面向流的协议,哪来什么包,你知识体系有问题,你看过tcp协议没有。这些话都是知乎上某一问题的答案说的。所以,别说“tcp粘包”,但是可以用这个关键词去搜索如何解决这个问题。
181 |
182 | 如果,现在你看了如何解决这个问题,其实就是一句话,在tcp上层定义自己的应用层协议:也就是tcp报文的格式。http这个应用层协议就是一种tcp报文的一种定义。
183 |
184 | 我们的伪装好的报文就是http协议,所以要做的就是实现自己的http请求解析器,获取我们关心的信息。
185 |
186 | sogo的http请求解析器,在:
187 |
188 | ```java
189 | //file sogo-server/server.go
190 | func read(clientConn net.Conn, redundancy []byte) (payload, redundancyRetain []byte, target string, readErr error)
191 | ```
192 |
193 | 这一部分有点繁杂。。不多解释,自己看代码吧。
194 |
195 | ## 伪装sogo-server:80为其他http网站
196 |
197 | 这一部分就是第二特性:将sogo-server所在的ip:端口伪装成一个http网站。
198 |
199 | 上一节,我们提到 {fakehost}。我们故意将{fakehost}定义为一个复杂、很长的域名。我们伪装的请求,都会带有如下请求头
200 |
201 | ```shell
202 | Host: {fakehost}
203 | ```
204 |
205 | 如果,http请求的Host不是这个{fakehost}则说明这不是一个经sogo(client)的请求,而是直接请求了sogo-server。也就是,有人来嗅探啦!
206 |
207 | 对这种,我们就会将该请求,原封不动地转到伪装站。(其实还是有点修改的,但这是细节,看代码吧)所以,直接访问sogo-server-ip:80 就是访问伪装站:80。
208 |
209 |
210 | ## linux上服务端部署
211 |
212 | ```shell
213 | wget https://github.com/arloor/sogo/releases/download/v1.0/sogo-server
214 | wget https://github.com/arloor/sogo/releases/download/v1.0/sogo-server.json
215 |
216 | chmod +x sogo-server
217 | mv -f sogo-server /usr/local/bin/
218 | mv -f sogo-server.json /usr/local/bin/
219 | kill -9 $(lsof -i:80|tail -1|awk '$1!=""{print $2}') #关闭80端口应用
220 | ulimit -n 65536 #设置进程最多打开文件数量,防止 too many openfiles错误(太多连接
221 | (sogo-server &)
222 | ```
223 |
224 | ## linux上客户端安装
225 |
226 | ```shell
227 | # 国内机器下面两个wget会很慢,考虑本地下载再上传到服务器吧
228 | wget https://github.com/arloor/sogo/releases/download/v1.0/sogo.json
229 | wget https://github.com/arloor/sogo/releases/download/v1.0/sogo
230 |
231 | chmod +x sogo
232 | mv -f sogo /usr/local/bin/
233 | mv -f sogo.json /usr/local/bin/
234 | ulimit -n 65536 #设置进程最多打开文件数量,防止 too many openfiles错误(太多连接
235 | # 运行前,先修改/usr/local/bin/sogo.json
236 | (sogo &) #以 /usr/local/bin/sogo.json 为配置文件 该配置下,服务端地址被设置为proxy
237 | #(sogo -c path &) #以path指向的文件为配置文件
238 | ```
239 |
240 | ## windows客户端安装
241 |
242 | 到[Release](https://github.com/arloor/sogo/releases/tag/v1.0)下载`sogo.exe`和`sogo.json`。
243 |
244 | sogo.json内容如下:
245 |
246 | ```json
247 | {
248 | "ClientPort": 8888,
249 | "Use": 0,
250 | "Servers": [
251 | {
252 | "ProxyAddr": "proxy",
253 | "ProxyPort": 80,
254 | "UserName": "a",
255 | "Password": "b"
256 | }
257 | ],
258 | "Dev":false
259 | }
260 | ```
261 | 先修改`ProxyAddr`为服务端安装的地址即可。其他配置项是高级功能,例如多服务器管理,多用户管理(用户认证)等等。
262 |
263 | >shadowsocks是没有多用户管理的,ss每个端口对应一个用户。sogo则使用用户名+密码认证,使多个用户使用同一个服务器端口。
264 |
265 | 修改好之后,双击`sogo.exe`,这时会发现该目录下多了一个 sogo_8888.log 的文件,这就说明,在本地的8888端口启动好了这个sock5代理。(没有界面哦。
266 |
267 | ## 结束
268 |
269 | 这篇博客梳理了一下sogo的实现原理,总之,sogo是一个优雅的代理。sogo代码不多,对go语言、网络编程感兴趣的人可以看看。
270 |
271 |
--------------------------------------------------------------------------------
/init.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/arloor/sogo/utils"
8 | "log"
9 | "os"
10 | "strconv"
11 | "sync"
12 | )
13 |
14 | var configFilePath string = utils.GetWorkDir() + "sogo.json" //绝对路径或相对路径
15 |
16 | var localAddr string
17 | var proxyAddr string
18 | var authorization string
19 | var prefix1 string = "POST /target?at="
20 | var prefix2 string
21 | var prefix3 = "\r\n\r\n"
22 |
23 | //prefix= prefix1+target+prefix2+length+prefix
24 |
25 | var pool = &sync.Pool{
26 | New: func() interface{} {
27 | log.Println("new 1")
28 | return make([]byte, 9192)
29 | },
30 | }
31 |
32 | var pool2 = &sync.Pool{
33 | New: func() interface{} {
34 | log.Println("new 22222222")
35 | return make([]byte, 9192)
36 | },
37 | }
38 |
39 | const fakeHost string = "qtgwuehaoisdhuaishdaisuhdasiuhlassjd.com"
40 |
41 | func printUsage() {
42 | fmt.Println("运行方式: sogo [-c configFilePath ] 若不使用 -c指定配置文件,则默认使用" + configFilePath)
43 | }
44 |
45 | func init() {
46 |
47 | printUsage()
48 |
49 | if len(os.Args) == 3 && os.Args[1] == "-c" {
50 | configFilePath = os.Args[2]
51 | }
52 |
53 | log.SetOutput(os.Stdout)
54 | log.SetFlags(log.Lshortfile | log.Flags())
55 | configinit()
56 | log.Println("配置信息为:", Config)
57 | server := Config.Servers[Config.Use]
58 | setServerConfig(server)
59 | if !Config.Dev {
60 | log.Println("已启动sogo客户端,请在sogo_" + strconv.Itoa(Config.ClientPort) + ".log查看详细日志")
61 | f, _ := os.OpenFile("sogo_"+strconv.Itoa(Config.ClientPort)+".log", os.O_WRONLY|os.O_CREATE|os.O_SYNC|os.O_APPEND, 0755)
62 | log.SetOutput(f)
63 | }
64 |
65 | localAddr = ":" + strconv.Itoa(Config.ClientPort)
66 |
67 | }
68 |
69 | //设置三个参数
70 | func setServerConfig(server Server) {
71 | proxyAddr = server.ProxyAddr + ":" + strconv.Itoa(server.ProxyPort)
72 | authorization = base64.StdEncoding.EncodeToString([]byte(server.UserName + ":" + server.Password))
73 | prefix2 = " HTTP/1.1\r\nHost: " + fakeHost + "\r\nAuthorization: Basic " + authorization + "\r\nAccept: */*\r\nContent-Type: text/plain\r\naccept-encoding: gzip, deflate\r\ncontent-length: "
74 | log.Println("服务器配置:", proxyAddr, "认证信息:", "Basic "+authorization)
75 | }
76 |
77 | type Server struct {
78 | ProxyAddr string
79 | ProxyPort int
80 | UserName string
81 | Password string
82 | }
83 |
84 | type Info struct {
85 | Dev bool
86 | ClientPort int
87 | Use int
88 | Servers []Server //8081,请不要修改
89 | }
90 |
91 | var Config = Info{
92 | true,
93 | 7777,
94 | 0,
95 | []Server{
96 | Server{"proxy", 80, "a", "b"},
97 | },
98 | }
99 |
100 | func (configInfo Info) String() string {
101 | str, _ := configInfo.ToJSONString()
102 | return str
103 | }
104 |
105 | //implement JSONObject
106 | func (configInfo Info) ToJSONString() (str string, error error) {
107 | b, err := json.Marshal(configInfo)
108 | if err != nil {
109 | return "", err
110 | } else {
111 | return string(b), nil
112 | }
113 | }
114 |
115 | func configinit() {
116 | configFile, err := os.Open(configFilePath)
117 | defer configFile.Close()
118 | if err != nil {
119 | log.Println("Error", "打开"+configFilePath+"失败,使用默认配置", err)
120 | return
121 | }
122 | bufSize := 1024
123 | buf := make([]byte, bufSize)
124 | for {
125 | total := 0
126 | n, err := configFile.Read(buf)
127 | total += n
128 | if err != nil {
129 | log.Println("Error", "读取"+configFilePath+"失败,使用默认配置", err)
130 | return
131 | } else if n < bufSize {
132 | log.Println("OK", "读取"+configFilePath+"成功")
133 | buf = buf[:total]
134 | break
135 | }
136 |
137 | }
138 | err = json.Unmarshal(buf, &Config)
139 | if err != nil {
140 | log.Println("Error", "读取"+configFilePath+"失败,使用默认配置", err)
141 | return
142 | }
143 |
144 | }
145 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "encoding/binary"
6 | "errors"
7 | "fmt"
8 | "github.com/arloor/sogo/mio"
9 | "log"
10 | "net"
11 | "net/http"
12 | "strings"
13 |
14 | "strconv"
15 | )
16 |
17 | var hand = []byte{0x05, 0x00}
18 | var ack = []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10}
19 |
20 | func handler(w http.ResponseWriter, r *http.Request) {
21 | //写回请求体本身
22 | bufio.NewReader(r.Body).WriteTo(w)
23 |
24 | }
25 | func server8080() {
26 | http.HandleFunc("/", handler)
27 | if err := http.ListenAndServe(":8080", nil); err != nil {
28 | log.Println("serve过程中出错", err)
29 | }
30 | }
31 |
32 | func main() {
33 | //go server8080()
34 |
35 | //==================
36 |
37 | ln, err := net.Listen("tcp", localAddr)
38 | if err != nil {
39 | fmt.Println("监听", localAddr, "失败 ", err)
40 | return
41 | }
42 | defer ln.Close()
43 | fmt.Println("成功监听 ", ln.Addr())
44 | for {
45 | c, err := ln.Accept()
46 | if err != nil {
47 | log.Println("接受连接失败 ", err)
48 | } else {
49 | go handleClientConnnection(c)
50 | }
51 | }
52 | }
53 |
54 | func ban(addr string) bool {
55 | if strings.Contains(addr, "imap") {
56 | return true
57 | }
58 | return false
59 | }
60 |
61 | func handleClientConnnection(clientCon net.Conn) {
62 | defer clientCon.Close()
63 | handshakeErr := handshake(clientCon)
64 | if handshakeErr != nil {
65 | log.Println("handshakeErr ", handshakeErr)
66 | return
67 | } else {
68 | log.Println("与客户端握手成功")
69 | addr, getTargetErr := getTargetAddr(clientCon)
70 | if getTargetErr != nil {
71 | log.Println("getTargetErr ", getTargetErr)
72 | return
73 | } else if ban(addr) {
74 | log.Println("ban", addr)
75 | return
76 | } else {
77 | //开始连接到服务器,并传输
78 | var serverConn, dialErr = net.Dial("tcp", proxyAddr)
79 | //var serverConn, dialErr = net.Dial("tcp", addr)
80 | if dialErr != nil {
81 | log.Println("dialErr ", dialErr)
82 | clientCon.Close()
83 | return
84 | }
85 | go handleServerConn(serverConn, clientCon)
86 |
87 | var buf = pool.Get().([]byte)
88 | defer pool.Put(buf)
89 | for {
90 |
91 | num, readErr := clientCon.Read(buf[1000:])
92 | if readErr != nil {
93 | log.Print("readErr ", readErr, clientCon.RemoteAddr())
94 | clientCon.Close()
95 | serverConn.Close()
96 | return
97 | }
98 | writeErr := mio.WriteAll(serverConn, mio.AppendHttpRequestPrefix(buf[:num+1000], addr, prefix1, prefix2, prefix3))
99 | //writeErr := mio.WriteAll(serverConn, buf[:num])
100 | if writeErr != nil {
101 | log.Print("writeErr ", writeErr)
102 | clientCon.Close()
103 | serverConn.Close()
104 | return
105 | }
106 | buf = buf[0:]
107 | }
108 | }
109 | }
110 |
111 | }
112 |
113 | //HTTP/1.1 200 OK
114 | //Content-Type: text/plain; charset=utf-8
115 | //Content-Length: 181
116 | //
117 | //HTTP/1.1 304 Not Modified
118 | //Date: Tue, 09 Apr 2019 08:46:15 GMT
119 | //Server: Apache/2.4.6 (CentOS)
120 | //Connection: Keep-Alive
121 | //Keep-Alive: timeout=5, max=100
122 | //ETag: "37cb-58613717be980"
123 |
124 | func handleServerConn(serverConn, clientCon net.Conn) {
125 | defer serverConn.Close()
126 | readBuf := pool2.Get().([]byte)
127 | payload := pool2.Get().([]byte)[:0]
128 | prefixBuf := pool2.Get().([]byte)[:0]
129 | redundencyBuf := pool2.Get().([]byte)[:0]
130 | defer func() {
131 | pool2.Put(readBuf)
132 | pool2.Put(payload)
133 | pool2.Put(prefixBuf)
134 | pool2.Put(redundencyBuf)
135 | }()
136 |
137 | state := "preflix"
138 | contentlength := -1
139 | for {
140 | var buf []byte = nil //待处理的字符
141 | if len(redundencyBuf) != 0 {
142 | buf = redundencyBuf
143 | redundencyBuf = redundencyBuf[:0]
144 | } else {
145 | readNum, readErr := serverConn.Read(readBuf)
146 | if readErr != nil {
147 | log.Println("readErr ", readErr)
148 | clientCon.Close()
149 | break
150 | }
151 | buf = readBuf[:readNum]
152 | }
153 |
154 | switch state {
155 | case "preflix":
156 | //获取响应的prefix
157 | //HTTP/1.1 200 OK
158 | //Content-Type: text/plain; charset=utf-8
159 | //Content-Length: 3717
160 | for i := 0; i+4 <= len(buf); i++ { //0 1 2 3 4 5
161 | if buf[i] == '\r' && buf[i+2] == '\r' && buf[i+1] == '\n' && buf[i+3] == '\n' {
162 | prefixBuf = append(prefixBuf, buf[:i]...)
163 | state = "payload"
164 | if i+4 < len(buf) {
165 | redundencyBuf = append(redundencyBuf, buf[i+4:]...)
166 | }
167 | break
168 | }
169 | }
170 | if state == "payload" {
171 | //分析头部,获取响应头的contentlength
172 | headrs := strings.Split(string(prefixBuf), "\r\n")
173 | requestline := headrs[0]
174 | parts := strings.Split(requestline, " ")
175 | if len(parts) < 3 {
176 | fmt.Println(requestline)
177 | log.Println(errors.New("不是以HTTP/1.1 200 OK这种开头,说明上个响应有问题。"))
178 | clientCon.Close()
179 | return
180 | }
181 | //version := parts[0]
182 | //code := parts[1]
183 | //msg := parts[2]
184 | var headmap = make(map[string]string)
185 | for i := 1; i < len(headrs); i++ {
186 | headsplit := strings.Split(headrs[i], ": ")
187 | if len(headsplit) == 2 {
188 | headmap[headsplit[0]] = headsplit[1]
189 | }
190 | }
191 | if headmap["Content-Length"] == "" {
192 | contentlength = 0
193 | } else {
194 | contentlength, _ = strconv.Atoi(headmap["Content-Length"])
195 | }
196 | //log.Println(contentlength)
197 | } else if state == "preflix" {
198 | prefixBuf = append(prefixBuf, buf[:len(buf)]...)
199 | }
200 | case "payload":
201 | toAppend := contentlength - len(payload)
202 | hasAll := true
203 | if toAppend > len(buf) {
204 | hasAll = false
205 | }
206 | if hasAll {
207 | payload = append(payload, buf[:toAppend]...)
208 | state = "ready"
209 | if len(buf) > toAppend {
210 | redundencyBuf = append(redundencyBuf, buf[toAppend:]...)
211 | }
212 |
213 | //下面开始传输payload
214 | mio.Simple(&payload, len(payload))
215 | mio.WriteAll(clientCon, payload)
216 | payload = payload[:0]
217 | state = "preflix"
218 | } else {
219 | payload = append(payload, buf[:len(buf)]...)
220 | }
221 |
222 | default:
223 |
224 | }
225 | }
226 | }
227 |
228 | //func handleServerConn(serverConn, clientCon net.Conn) {
229 | // defer serverConn.Close()
230 | // redundancy := make([]byte, 0)
231 | // for {
232 | // redundancyRetain, readerr := read(serverConn, clientCon, redundancy)
233 | // redundancy = redundancyRetain
234 | // if readerr != nil {
235 | // log.Println("readerr", readerr)
236 | // break
237 | // }
238 | // }
239 | //}
240 |
241 | func read(serverConn, clientConn net.Conn, redundancy []byte) (redundancyRetain []byte, readErr error) {
242 | buf := pool2.Get().([]byte)
243 |
244 | num := 0
245 | contentlength := -1
246 | prefixAll := false
247 | prefix := pool2.Get().([]byte)[0:0]
248 | //redundancy:=make([]byte,0)
249 | payload := pool2.Get().([]byte)[0:0]
250 |
251 | defer func() {
252 | pool2.Put(buf)
253 | pool2.Put(prefix[:cap(prefix)])
254 | pool2.Put(payload[:cap(payload)])
255 | }()
256 |
257 | for {
258 | if len(redundancy) != 0 {
259 | buf = redundancy
260 | num = len(redundancy)
261 | redundancy = redundancy[0:0]
262 | } else {
263 | num, readErr = serverConn.Read(buf)
264 | if readErr != nil {
265 | return redundancy, readErr
266 | }
267 | }
268 |
269 | if num <= 0 {
270 | return nil, errors.New("读到<=0字节,未预期地情况")
271 | } else {
272 | if !prefixAll { //追加到前缀
273 | prefix = append(prefix, buf[:num]...)
274 | //todo
275 | // string(prefix)需要优化,只是为了找到\r\n\r\n别这样,别转成string
276 | if index := strings.Index(string(prefix), "\r\n\r\n"); index >= 0 {
277 | if index+4 < len(prefix) {
278 | payload = append(payload, prefix[index+4:]...)
279 | }
280 | prefix = prefix[:index]
281 | prefixAll = true
282 | //分析头部
283 | headrs := strings.Split(string(prefix), "\r\n")
284 |
285 | requestline := headrs[0]
286 | parts := strings.Split(requestline, " ")
287 | if len(parts) < 3 {
288 | fmt.Println(requestline)
289 | return nil, errors.New("不是以HTTP/1.1 200 OK这种开头,说明上个响应有问题。")
290 | }
291 | //version := parts[0]
292 | //code := parts[1]
293 | //msg := parts[2]
294 |
295 | var headmap = make(map[string]string)
296 | for i := 1; i < len(headrs); i++ {
297 | headsplit := strings.Split(headrs[i], ": ")
298 | headmap[headsplit[0]] = headsplit[1]
299 | }
300 | if headmap["Content-Length"] == "" {
301 | contentlength = 0
302 | } else {
303 | contentlength, _ = strconv.Atoi(headmap["Content-Length"])
304 | }
305 | }
306 | } else { //追加到payload
307 | payload = append(payload, buf[:num]...)
308 | }
309 | }
310 | buf = buf[0:]
311 | if contentlength != -1 && contentlength < len(payload) { //这说明读多了,要把多的放到redundancy
312 | redundancy = append(redundancy, payload[contentlength:]...)
313 | payload = payload[:contentlength]
314 | }
315 | if contentlength == len(payload) {
316 | //写会
317 | mio.Simple(&payload, len(payload))
318 | writeErr := mio.WriteAll(clientConn, payload)
319 | if writeErr != nil {
320 | clientConn.Close()
321 | }
322 | return redundancy, writeErr
323 | }
324 | }
325 | }
326 |
327 | func getTargetAddr(clientCon net.Conn) (string, error) {
328 | var buf = pool.Get().([]byte)
329 | defer pool.Put(buf)
330 | numRead, err := clientCon.Read(buf)
331 | if err != nil {
332 | return "", err
333 | } else if numRead > 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 {
334 | if buf[3] == 3 {
335 | log.Printf("目的地址类型:%d 域名长度:%d 目标域名:%s 目标端口:%s", buf[3], buf[4], buf[5:5+buf[4]], strconv.Itoa(int(binary.BigEndian.Uint16(buf[5+buf[4]:7+buf[4]]))))
336 | writeErr := mio.WriteAll(clientCon, ack)
337 | return string(buf[5:5+buf[4]]) + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[5+buf[4]:7+buf[4]]))), writeErr
338 | } else if buf[3] == 1 {
339 | log.Printf("目的地址类型:%d 目标域名:%s 目标端口:%s", buf[3], net.IPv4(buf[4], buf[5], buf[6], buf[7]).String(), strconv.Itoa(int(binary.BigEndian.Uint16(buf[8:10]))))
340 | writeErr := mio.WriteAll(clientCon, ack)
341 | return net.IPv4(buf[4], buf[5], buf[6], buf[7]).String() + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[8:10]))), writeErr
342 | } else {
343 | return "", errors.New("不能处理ipv6")
344 | }
345 |
346 | } else {
347 | return "", errors.New("不能处理非CONNECT请求")
348 | }
349 | }
350 |
351 | //读 5 1 0 写回 5 0
352 | func handshake(clientCon net.Conn) error {
353 | //var buf = make([]byte,100)
354 | var buf = pool.Get().([]byte)
355 | defer pool.Put(buf)
356 | numRead, err := clientCon.Read(buf)
357 | if err != nil {
358 | return err
359 | } else if numRead == 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 {
360 | return mio.WriteAll(clientCon, hand)
361 | } else {
362 | log.Printf("%d", buf[:numRead])
363 | return mio.WriteAll(clientCon, hand)
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/mio/io.go:
--------------------------------------------------------------------------------
1 | package mio
2 |
3 | import (
4 | "net"
5 | )
6 |
7 | func WriteAll(conn net.Conn, buf []byte) error {
8 | //log.Print("写会浏览器")
9 | for writtenNum := 0; writtenNum != len(buf); {
10 | tempNum, err := conn.Write(buf[writtenNum:])
11 | if err != nil {
12 | return err
13 | }
14 | writtenNum += tempNum
15 | }
16 | return nil
17 | }
18 |
--------------------------------------------------------------------------------
/mio/prefix.go:
--------------------------------------------------------------------------------
1 | package mio
2 |
3 | import (
4 | "encoding/base64"
5 | "strconv"
6 | )
7 |
8 | //POST / HTTP/1.1
9 | //cache-control: no-cache
10 | //Postman-Token: 6b859da0-0a7e-4c4e-a0e2-2544aeef732f
11 | //Content-Type: text/plain
12 | //User-Agent: PostmanRuntime/7.6.1
13 | //Accept: */*
14 | //Host: localhost:8080
15 | //accept-encoding: gzip, deflate
16 | //content-length: 5
17 | //Connection: keep-alive
18 | //
19 | //aaaaa
20 | //
21 | //HTTP/1.1 200 OK
22 | //Date: Tue, 09 Apr 2019 03:26:07 GMT
23 | //Content-Length: 5
24 | //Content-Type: text/plain; charset=utf-8
25 | //Connection: keep-alive
26 | //
27 | //aaaaa
28 |
29 | func AppendHttpRequestPrefix(buf []byte, targetAddr string, prefix1 string, prefix2 string, prefix3 string) []byte {
30 | content := buf[1000:]
31 | prefix := buf[:1000]
32 | Simple(&content, len(content))
33 | contentLength := strconv.Itoa(len(content))
34 | // 演示base64编码
35 | addrBase64 := base64.NewEncoding("abcdefghijpqrzABCKLMNOkDEFGHIJl345678mnoPQRSTUVstuvwxyWXYZ0129+/").EncodeToString([]byte(targetAddr))
36 | prefixEnd := 1000
37 | prefixLength := len(prefix1) + len(addrBase64) + len(prefix2) + len(contentLength) + len(prefix3)
38 | prefixStart := prefixEnd - prefixLength
39 | prefixIndex := prefixStart
40 | for _, x := range prefix1 {
41 | prefix[prefixIndex] = byte(x)
42 | prefixIndex++
43 | }
44 | for _, x := range addrBase64 {
45 | prefix[prefixIndex] = byte(x)
46 | prefixIndex++
47 | }
48 | for _, x := range prefix2 {
49 | prefix[prefixIndex] = byte(x)
50 | prefixIndex++
51 | }
52 | for _, x := range contentLength {
53 | prefix[prefixIndex] = byte(x)
54 | prefixIndex++
55 | }
56 | for _, x := range prefix3 {
57 | prefix[prefixIndex] = byte(x)
58 | prefixIndex++
59 | }
60 | buf = buf[prefixStart:]
61 | //println(len(buf)) lenbuf就是实际上传字节数
62 | return buf
63 | }
64 |
65 | //取反
66 | func Simple(bufPtr *[]byte, num int) {
67 | buf := *bufPtr
68 | for i := 0; i < num; i++ {
69 | buf[i] = ^buf[i]
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/sogo.json:
--------------------------------------------------------------------------------
1 | {
2 | "ClientPort": 8888,
3 | "Use": 0,
4 | "Servers": [
5 | {
6 | "ProxyAddr": "proxy",
7 | "ProxyPort": 80,
8 | "UserName": "a",
9 | "Password": "b"
10 | }
11 | ],
12 | "Dev":true
13 | }
--------------------------------------------------------------------------------
/utils/tool.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 | "path/filepath"
7 | "strings"
8 | )
9 |
10 | func GetWorkDir() string {
11 | file, err := exec.LookPath(os.Args[0])
12 | if err != nil {
13 | //
14 | return ""
15 | }
16 | path, err := filepath.Abs(file)
17 | if err != nil {
18 | return ""
19 | }
20 | i := strings.LastIndex(path, "/")
21 | if i < 0 {
22 | i = strings.LastIndex(path, "\\")
23 | }
24 | if i < 0 {
25 | return ""
26 | }
27 | return string(path[:i+1])
28 | }
29 |
--------------------------------------------------------------------------------