-
Notifications
You must be signed in to change notification settings - Fork 1
/
authpass.go
314 lines (275 loc) · 7.1 KB
/
authpass.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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
package main
import (
"bytes"
"context"
"crypto/tls"
"embed"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
)
const (
maxLength = 512
trait = "w6XDp+KIqw=="
)
var (
version string
commit string
date string
)
// The request info and password is stored in embed file to be modified dynamically
//
//go:embed data.bin
var content embed.FS
var (
u string
p string
totp bool
output string
nopersist bool
re *regexp.Regexp
notice *regexp.Regexp
redir *regexp.Regexp
)
func init() {
re = regexp.MustCompile(`(?s)name="csrf" value="(?P<csrf>.*?)".*name="ip" value="(?P<ip>.*?)"`)
notice = regexp.MustCompile(`(?s)<div class="notice">(.*?)</div>`)
redir = regexp.MustCompile(`(?s)window.location = "(.*?)"`)
flag.StringVar(&u, "u", "", "开启了访问验证的隧道地址, e.g. https://something:12345")
flag.StringVar(&p, "p", "", "访问验证密码")
flag.BoolVar(&totp, "totp", false, "访问验证密码")
flag.BoolVar(&nopersist, "nopersist", false, "不记住认证(将于auth_time后失效)")
help := flag.Bool("h", false, "显示此帮助信息")
if runtime.GOOS == "windows" {
flag.StringVar(&output, "o", "authpass_generated.exe", "生成专用客户端的存放路径")
} else {
flag.StringVar(&output, "o", "authpass_generated", "生成专用客户端的存放路径")
}
flag.Parse()
if *help {
flag.PrintDefaults()
os.Exit(0)
}
}
func interactParam() {
fmt.Printf("您未提供生成参数,请提供下面的参数: \n > 隧道访问地址(如 https://something:12345): ")
fmt.Scanln(&u)
u = strings.TrimSpace(u)
if pu, err := url.Parse(u); err != nil || pu.Scheme != "https" {
u = "https://" + u
if _, err = url.Parse(u); err != nil {
fmt.Println("您提供的隧道访问地址无法解析,请检查输入")
return
}
}
fmt.Printf(" > 访问验证密码(如只使用 TOTP 功能,请留空): ")
fmt.Scanln(&p)
p = strings.TrimSpace(p)
var s string
if p != "" {
fmt.Printf(" > 是否使用 TOTP(Y/N, 默认为N): ")
fmt.Scanln(&s)
totp = strings.ToLower(strings.TrimSpace(s)) == "y"
} else {
totp = true
}
fmt.Printf(" > 是否记住认证(Y/N, 默认为Y): ")
fmt.Scanln(&s)
nopersist = strings.ToLower(strings.TrimSpace(s)) == "n"
}
type data struct {
Url string `json:"url"`
Pass string `json:"pass"`
Totp bool `json:"totp"`
Persist bool `json:"persist"`
}
func parseEmbed() {
c, _ := content.Open("data.bin")
buf, _ := io.ReadAll(c)
t, _ := base64.StdEncoding.DecodeString(trait)
buf = bytes.TrimPrefix(buf, t)
buf = buf[:bytes.IndexByte(buf, 0x18)]
d := data{}
if err := json.Unmarshal(buf, &d); err != nil {
fatal("执行失败: 程序已损坏")
}
u = d.Url
p = d.Pass
totp = d.Totp
nopersist = !d.Persist
}
func genExe() {
selfPath, err := os.Executable()
if err != nil {
fatal("载入程序失败:", err)
}
self, err := os.OpenFile(selfPath, os.O_RDONLY, os.ModePerm)
if err != nil {
fatal("载入程序失败:", err)
}
c, err := io.ReadAll(self)
if err != nil {
fatal("载入程序失败:", err)
}
t, _ := base64.StdEncoding.DecodeString(trait)
index := bytes.Index(c, t)
if index == -1 {
fatal("处理失败: 程序已损坏")
}
index += len(t)
d := data{
Url: u,
Pass: p,
Totp: totp,
Persist: !nopersist,
}
j, err := json.Marshal(d)
if err != nil {
fatal("序列化失败:", err)
}
if len(j) > maxLength {
fatal("数据过长,请缩短密码再试")
}
copy(c[index:index+maxLength], j)
c[index+maxLength] = 0x18
out, err := os.Create(output)
if err != nil {
fatal("创建文件失败:", err)
}
n, err := out.Write(c)
if err != nil {
fatal("写入可执行文件失败:", err)
}
out.Close()
fmt.Printf("文件生成成功,%d 字节已写入\n", n)
pressKey()
}
func main() {
fmt.Println("===== SakuraFrp AuthPanel GuestTool =====")
fmt.Printf("version %s @ %s, %s\n", version, commit, date)
if u == "" || (p == "" && !totp) {
parseEmbed()
if u == "" || (p == "" && !totp) {
interactParam()
genExe()
return
}
} else {
genExe()
return
}
uri, _ := url.Parse(u)
// Set skip tls verify
tlsConfig := &tls.Config{InsecureSkipVerify: true, MaxVersion: tls.VersionTLS12}
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = tlsConfig
client := http.Client{Transport: customTransport}
// GET authpanel
resp, err := client.Get(u)
if err != nil {
if uri.Port() == "443" || net.ParseIP(uri.Hostname()) != nil {
fatal("请求", u, "时发生错误:", err)
}
// retry ip as sni
fmt.Println("直接请求失败,尝试替代 SNI 请求")
ips, err2 := net.DefaultResolver.LookupIP(context.Background(), "ip4", uri.Hostname())
if err2 != nil || len(ips) == 0 {
fatal("请求", u, "时发生错误:", err, ",并且无法解析域名:", err2)
}
tlsConfig.ServerName = ips[0].To4().String()
resp, err = client.Get(u)
if err != nil {
fatal("请求", u, "时发生错误:", err)
}
}
res, err := io.ReadAll(resp.Body)
if err != nil {
fatal("请求", u, "时发生错误:", err)
}
// parse to get csrf and ip
groups := re.FindStringSubmatch(string(res))
if len(groups) != 3 {
fatal("解析服务器返回内容时发生错误,原始内容:\n", string(res))
}
// POST authpanel
form := url.Values{}
form.Set("csrf", groups[1])
form.Set("ip", groups[2])
form.Set("pw", p)
if totp {
var s string
fmt.Printf(" > 请输入 TOTP 一次性密码: ")
fmt.Scanln(&s)
form.Set("totp", s)
}
if !nopersist {
form.Set("persist_auth", "on")
}
resp, err = client.PostForm(u, form)
if err != nil {
fatal("提交", u, "时发生错误:", err)
}
res, err = io.ReadAll(resp.Body)
if err != nil {
fatal("提交", u, "时发生错误:", err)
}
// parse result
groups = notice.FindStringSubmatch(string(res))
if len(groups) != 2 {
fatal("解析服务器返回内容时发生错误,原始内容:\n", string(res))
}
result := strings.TrimSpace(groups[1])
switch {
case strings.HasPrefix(result, "认证成功, 正在为您跳转到后续链接"):
redirs := redir.FindStringSubmatch(string(res))
fmt.Println(redirs)
if len(redirs) == 2 {
if err := open(redirs[1]); err == nil {
fmt.Println("认证成功, 已为您打开后续链接")
} else {
fmt.Println("认证成功, 未能为您打开后续链接:", err)
}
pressKey()
break
}
fallthrough
case result == "认证成功, 现在可以关闭页面并正常连接隧道了":
fmt.Println("认证成功, 现在可以正常连接了")
pressKey()
default:
fatal("认证失败,原因:", result)
}
}
func open(url string) error {
switch runtime.GOOS {
case "windows":
return exec.Command("explorer", url).Start()
case "darwin":
return exec.Command("open", url).Start()
case "linux":
return exec.Command("xdg-open", url).Start()
default:
return errors.New("os not supported")
}
}
func fatal(things ...interface{}) {
fmt.Println(things...)
pressKey()
os.Exit(1)
}
func pressKey() {
fmt.Println("===== 按任意键继续 =====")
b := make([]byte, 1)
os.Stdin.Read(b)
}