diff --git a/.gitignore b/.gitignore index 45cef44..9429944 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,8 @@ *.code-workspace # Local History for Visual Studio Code -.history/ \ No newline at end of file +.history/ + +# Other Runtime-Generated Files +*.crt +*.key diff --git a/README.md b/README.md index 5eca354..94a8da4 100644 --- a/README.md +++ b/README.md @@ -2,55 +2,37 @@ RVPN Web Portal Proxy Adapter (based on MITM) -尝试将[浙江大学 RVPN 网页版](https://rvpn.zju.edu.cn)模拟成一个 HTTP 代理。 +因为不想在电脑上安装 EasyConnect,所以做了一个访问 ZJU 校内网站的轻量级方案——将 [浙江大学 RVPN 网页版](https://rvpn.zju.edu.cn) 模拟为本地 HTTP 代理。 -提出这个想法的讨论:[有没有将 CGI Proxy 转化为普通 HTTP Proxy 的工具呢? - V2EX](https://www.v2ex.com/t/670356) +## 使用 + +1. 前往 [Releases](https://github.com/CoolSpring8/rwppa/releases/) 下载并放于合适位置; +2. 运行,输入上网服务账号、密码(即用于校内拨号 VPN 的账号)以及监听端口(如“127.0.0.1:8080”代表监听 8080 端口,仅接受本机流量)。账号密码仅会被发送至 rvpn.zju.edu.cn,用于登录认证。 +3. 点击登录后别着急,先将程序同目录下生成的 rootCA.crt 安装至“受信任的根证书颁发机构”存储区。(之后运行不需要这一步) +4. 用 SwitchyOmega 等设置好代理,start browsing! + +注意不要删除 rootCA.crt 和 rootCA.key,否则需要重新添加信任。 ## 特性及局限 ### 特性: -- 配置完毕并登录后,可访问校内校外 HTTP / HTTPS 网站(例如 CC98,正版软件服务与管理平台等),原理基于模拟 RVPN 网页版。 -- 支持 HTTP 的 GET,POST,PUT,DELETE,HEAD 方法,一定程度上支持 OPTIONS 方法(模拟)。 +- 配置完毕并登录后,可访问校内校外 HTTP / HTTPS 网站(例如 CC98,正版软件服务与管理平台等)。 +- 支持 HTTP 的 GET,POST,PUT,DELETE,HEAD 方法,一定程度上支持 OPTIONS 方法(仅为模拟)。 +- 正确处理大部分网页及网页资源。如果碰到问题,欢迎提 Issue/PR ! ### 局限: -- **不支持 WebSocket,FTP,SSH等非 HTTP 协议。** +- **不支持 WebSocket,FTP,SSH等非 HTTP 协议。**你可能会想了解 [Hagb/docker-easyconnect](https://github.com/Hagb/docker-easyconnect) 项目。 - 不支持 HTTP 的 PATCH 等方法。 - OPTIONS 方法由于 RVPN 网页版不支持,所以实际不会向服务器发送请求,而是由 rwppa 直接返回一个针对 CORS 预检而配置的较为宽松的结果,安全性有所降低。 -- 不支持 TLS 1.3 Early Data(0-RTT)。若原网站启用过 TLS 1.3 Early Data且 session ticket 还没过期则会[无法访问](https://golang.org/src/crypto/tls/handshake_server_tls13.go)(如 V2EX)。(但是短期内大概不会有开启这个特性的校内网站吧。) - -## 使用 - -1. ```bash - openssl genrsa -out rootCA.key 4096 - ``` - -2. ```bash - openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt - ``` - -3. 把 rootCA.crt 安装到受信任证书存储区; - -4. 将 rootCA.key 和 rootCA.crt 的内容填入 cert.go; - -5. ``` - go build - ``` - -6. 在浏览器登录 rvpn.zju.edu.cn,从 Cookies 中找到 TWFID 字段的值; - -7. ``` - ./rwppa - ``` - -1~4 是可选的。你也可以信任并使用 cert.go 自带的证书,但出于安全性考虑不推荐这样做。 - -由于使用了 Fyne GUI,现在编译还需要 GCC 等额外依赖。 +- 不支持 TLS 1.3 Early Data(0-RTT)。若原网站启用过 TLS 1.3 Early Data 且 session ticket 还没过期则会[无法访问](https://golang.org/src/crypto/tls/handshake_server_tls13.go)(如 V2EX)。(但是短期内大概不会有开启这个特性的校内网站吧。) ## 其他 -欢迎 PR 改进! +提出这个想法的最初讨论:[有没有将 CGI Proxy 转化为普通 HTTP Proxy 的工具呢? - V2EX](https://www.v2ex.com/t/670356) + +欢迎提出 Issue 或 PR! ## LICENSE diff --git a/cert.go b/cert.go index 128028d..0df0bcd 100644 --- a/cert.go +++ b/cert.go @@ -31,95 +31,84 @@ package main import ( + "crypto/rand" + "crypto/rsa" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io/ioutil" + "math/big" + "os" + "time" "github.com/elazarl/goproxy" ) -var caCert = []byte(`-----BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIUdWndr4j2nJH3C+DfjL0sqG8PIMQwDQYJKoZIhvcNAQEL -BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDA2MTgwNzQ1NDBaFw0yMzA0 -MDgwNzQ1NDBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw -HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQDHiQ7JbOCvmEw+VYQY+4iLwtTHe9GyH3zXYujeeqC5 -rcBU5MBD8NyqBjSDi/JPXEiCpBkYzmDtmqiCEmuoi8r6LADoJUdo2QUzu+S9UVt7 -FqgXRNzZC5Enh1F9m/pkyz7+GtuQB4PIQGf74FNlza5Bd+9NmJrDYKxj6ydXoXTS -hTQrMEx3Wy8PzqpJw1006/So8Nh4jeu41DJr43bcO2J09/MpOd9pzB2GiDgrHu5M -5EAUJONWut62oWT42PYXzP2MRZPRmcd70/u5IX3LsV5Pi6XVqsFjjmIYnvKOa9rw -XICiCVdv5374CLwlTh4AnsstIjKpGOfWNx2cPXEDHtUQUXW+aaoNTR8gZOEs68qr -EpKz87xbl4pLo33nOFOSs0JU24nRGfLwOF3vN/usPqFEUyqquSWSOWfm3XPgKZlm -JrpmI/MgJ6YZunEujdvogT479RZ/IGq/Z/MhZaJj4iOcdq0iZqPUaKtV7v1lrl8S -1Z2O5fiVdoc/95n1D2ZQW7M5/+d9RiGuGSfVD95VA5WmQdGhRlV+MdeoGvvC+/F3 -1pMl61YBvGc7jpPM6GA535Cmb9yXZFiKsutTHuDWEMngjfvlFV2qrxNy9PX+AQ+W -XbidvCZKQqxE6fgnFIQxsI82ZCkdddI9d9ifMt4qkFNeJDbyeNxPuea0qb4752er -5wIDAQABo1MwUTAdBgNVHQ4EFgQUYB+DRN17B+CMNJCRMRlvrR26HbAwHwYDVR0j -BBgwFoAUYB+DRN17B+CMNJCRMRlvrR26HbAwDwYDVR0TAQH/BAUwAwEB/zANBgkq -hkiG9w0BAQsFAAOCAgEAuWn+Vea4esUk5HCZH5KVd43jhoN5qZ0EOVB0jNQ+JayW -/n3FO+o6N+CTqT1gU11uUp1H54N24s6oj+a/VuFzy38Delu34qKA8Y+rSnM/IgxW -Uo6M1exgPQ0KqSZvbiJ0JamdIaO14OL5hS7ZzNWo3ZnvDaG04tfOAUPPAIqRt1cq -ilRemHc4WmJ/U/7Vf1HaJ+QWEMQUVg0WbDHfsVP2EY+L1oQD7ZEoPKkCLcd/pGeP -1eoGxV6vKJL7JD8Sdl1UoHPksp/1+LIB4s6JE68NrksY3TqF5vC8jKHiG67h0XJ8 -WoWriNK/T6tSLj/ApAL1qeyz1nG1iccGJ4gaBOFH0kDl1vkU2qTbMQJv59cLUHWv -boevsBp356BiCBMYYkTvLAS4LDA27tvFcuzHtnaxQi22BnwZap9dvQJuxxtvfgCj -zeqmrCxwuuQpPvhHO4DJQxMxxeado6IyQmuVU6VJAnobBI2KIiGRDjldRqGe+aFe -6sTqW+NTs5ehkf0O12JTE9V2/yxALBd8BWEFhfaZfjU4g22TSF+1oPgd2r8qvmg6 -fICwA9tna6N36NyDnic1V+xF+93wBxIfhl+ZtzMISzxYlh+zyfipvz4qJb0Hhk0S -u8FUK+ksijxQTFP/ZFXNw/r8ufC1mHsWbD0bCqRhb3eVblU1f9e2XLmW4LUgtII= ------END CERTIFICATE-----`) - -var caKey = []byte(`-----BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY-----`) +func getCA() ([]byte, []byte, error) { + if fileExists("rootCA.crt") && fileExists("rootCA.key") { + caCert, err := ioutil.ReadFile("rootCA.crt") + if err != nil { + return nil, nil, err + } + caKey, err := ioutil.ReadFile("rootCA.key") + if err != nil { + return nil, nil, err + } + return caCert, caKey, err + } + + // files do not exist, make new ones instead + // source: https://golang.org/src/crypto/tls/generate_cert.go + priv, err := rsa.GenerateKey(rand.Reader, 2048) + panicOnErr(err) + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + notBefore := time.Now() + notAfter := notBefore.Add(3 * 365 * 24 * time.Hour) // 3 years + panicOnErr(err) + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"github.com/coolspring8/rwppa"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + panicOnErr(err) + certOut, err := os.Create("rootCA.crt") + panicOnErr(err) + err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + panicOnErr(err) + err = certOut.Close() + panicOnErr(err) + + keyOut, err := os.OpenFile("rootCA.key", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + panicOnErr(err) + privBytes, err := x509.MarshalPKCS8PrivateKey(priv) + panicOnErr(err) + err = pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) + panicOnErr(err) + err = keyOut.Close() + panicOnErr(err) + + caCert, err := ioutil.ReadFile("rootCA.crt") + if err != nil { + return nil, nil, err + } + caKey, err := ioutil.ReadFile("rootCA.key") + if err != nil { + return nil, nil, err + } + return caCert, caKey, err +} func setCA(caCert, caKey []byte) error { goproxyCa, err := tls.X509KeyPair(caCert, caKey) @@ -136,3 +125,17 @@ func setCA(caCert, caKey []byte) error { goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)} return nil } + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func panicOnErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/proxy.go b/proxy.go index b5be89b..de9625e 100644 --- a/proxy.go +++ b/proxy.go @@ -56,7 +56,14 @@ type reqData struct { } func startProxyServer(listenAddr string, twfid string) { - setCA(caCert, caKey) + caCert, caKey, err := getCA() + if err != nil { + panic(err) + } + err = setCA(caCert, caKey) + if err != nil { + panic(err) + } proxy := goproxy.NewProxyHttpServer() proxy.Verbose = true