This repository has been archived by the owner on Feb 29, 2024. It is now read-only.
/
proxy.go
145 lines (125 loc) · 5.35 KB
/
proxy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Copyright (C) 2020 CoolSpring8
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/elazarl/goproxy"
goproxy_html "github.com/elazarl/goproxy/ext/html"
)
var (
// rvpnURLMatcher detects RVPN-modified URLs.
rvpnURLMatcher *regexp.Regexp = regexp.MustCompile(`/web/[0-3]/(https?)/[0-2]/`)
// movedLocationURLMatcher detects the URL in the 3xx response's Location header.
movedLocationURLMatcher *regexp.Regexp = regexp.MustCompile(`https://.*:443/web/[0-3]/(https?)/[0-2]/`)
// isOPTIONSRequest checks whether the request's method is OPTIONS.
isOPTIONSRequest goproxy.ReqCondition = goproxy.ReqConditionFunc(
func(req *http.Request, ctx *goproxy.ProxyCtx) bool {
return req.Method == "OPTIONS"
})
// hasMovedLocationHeader checks whether the request has a Location header.
hasMovedLocationHeader goproxy.RespCondition = goproxy.RespConditionFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) bool {
return resp.Header.Get("Location") != ""
})
// isWebRelatedText checks whether the response's content type is one of html, js, css, xml and json.
isWebRelatedText goproxy.RespCondition = goproxy.ContentTypeIs(
"text/html",
"text/css",
"text/javascript", "application/javascript", "application/x-javascript",
"text/xml",
"text/json")
)
// reqData stores a request's raw URL, and a port-stripped one.
// rawURLwithoutPort is for URLs like https://example.com:443/ ,
// where in href-s, port 443 does not show up but need to be replaced.
type reqData struct {
rawURLWithPort string
rawURLWithoutPort string
}
// startProxyServer starts a proxy server listening at the given port with the given twfid.
func startProxyServer(listenAddr, twfid string) {
caCert, caKey, err := getCA()
if err != nil {
panic(err)
}
err = setCA(caCert, caKey)
if err != nil {
panic(err)
}
proxy := goproxy.NewProxyHttpServer()
proxy.Verbose = true
proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
proxy.OnRequest().DoFunc(
func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
// store rawURL for later use
rawURLWithPort := req.URL.String()
hostWithoutPort, _, err := net.SplitHostPort(req.URL.Host)
if err != nil {
hostWithoutPort = req.URL.Host // http at port 80 causes "missing port in address"
}
rawURLWithoutPort := req.URL.Scheme + "://" + hostWithoutPort + req.URL.Path // Query isn't here, but it doesn't matter
ctx.UserData = reqData{rawURLWithPort, rawURLWithoutPort}
// new request target
// TODO: add other missed fields in "type URL struct" if necessary, like "User"
// port has been included in "Host"
if req.URL.RawQuery != "" {
newURL, err := url.Parse("https://rvpn.zju.edu.cn/web/2/" + req.URL.Scheme + "/0/" + req.URL.Host + req.URL.Path + "?" + req.URL.RawQuery)
if err != nil {
return req, nil // this rarely happens?
}
req.URL = newURL
} else {
newURL, err := url.Parse("https://rvpn.zju.edu.cn/web/2/" + req.URL.Scheme + "/0/" + req.URL.Host + req.URL.Path)
if err != nil {
return req, nil
}
req.URL = newURL
}
// add cookie for web portal verification
req.AddCookie(&http.Cookie{Name: "TWFID", Value: twfid})
return req, nil
})
proxy.OnRequest(isOPTIONSRequest).DoFunc(
func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
resp := goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusOK, "")
resp.Header.Add("Access-Control-Allow-Credentials", "true")
resp.Header.Add("Access-Control-Allow-Headers", "authorization, content-type")
resp.Header.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, HEAD, OPTIONS")
resp.Header.Add("Access-Control-Allow-Origin", "*")
return req, resp
})
proxy.OnResponse(hasMovedLocationHeader).DoFunc(
func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
respLocation := resp.Header.Get("Location")
newLocation := movedLocationURLMatcher.ReplaceAllString(respLocation, "$1://")
resp.Header.Set("Location", newLocation)
return resp
})
proxy.OnResponse(isWebRelatedText).Do(goproxy_html.HandleString(
func(s string, ctx *goproxy.ProxyCtx) string {
// fix link in page, and fix "src" issues in javascript files
c := rvpnURLMatcher.ReplaceAllString(s, "$1://")
rawURLWithPort := ctx.UserData.(reqData).rawURLWithPort
rawURLWithoutPort := ctx.UserData.(reqData).rawURLWithoutPort
c = strings.ReplaceAll(c, rawURLWithPort[:strings.LastIndex(rawURLWithPort, "/")+1], "") // possible out of bounds?
c = strings.ReplaceAll(c, rawURLWithoutPort[:strings.LastIndex(rawURLWithoutPort, "/")+1], "")
return c
}))
fmt.Println("Current TWFID:" + twfid)
fmt.Println("Listen Address:" + listenAddr)
log.Fatal(http.ListenAndServe(listenAddr, proxy))
}