/
api_func.go
499 lines (458 loc) · 14 KB
/
api_func.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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
package web
import (
"encoding/base64"
"fmt"
"io"
"mime"
"net"
"net/http"
"strings"
"time"
_api "github.com/go-apibox/api"
"github.com/go-apibox/apiclient"
"github.com/go-apibox/session"
"github.com/go-apibox/utils"
"github.com/dchest/captcha"
"github.com/gorilla/securecookie"
"github.com/gorilla/websocket"
)
// 修复HTTP头部中的Content-Type
// 支付宝Notify通知结果头部如:
// Content-Type: application/x-www-form-urlencoded; text/html; charset=utf-8
// 为造成ParseForm()报错:mime: invalid media parameter
func fixHeader(h http.Header) {
if ct, has := h["Content-Type"]; has && len(ct) > 0 {
fields := strings.Split(ct[0], ";")
okFields := []string{}
if len(fields) > 1 {
okFields = append(okFields, fields[0])
for _, field := range fields[1:] {
// 必须带有=,而且不能包含/
if strings.IndexByte(field, '=') < 0 {
continue
}
if strings.IndexByte(field, '/') >= 0 {
continue
}
okFields = append(okFields, field)
}
h["Content-Type"][0] = strings.Join(okFields, ";")
}
}
}
func (web *Web) NewAPIFunc(api *API) http.HandlerFunc {
// 每个API后端都使用独立的sessionstore
web.sessionStoreMutex.Lock()
for _, localCookieName := range api.proxySessionMap {
if _, exists := web.sessionStoreMap[localCookieName]; exists {
continue
}
store, err := session.NewCookieStore(false, "")
if err != nil {
continue
}
web.sessionStoreMap[localCookieName] = store
}
web.sessionStoreMutex.Unlock()
if api.proxySessionEnabled {
// 每天更新一次 cookie key
go web.updateCookieKeyLoop(api)
}
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "POST" {
http.Error(w, "Unsupported request method!", http.StatusMethodNotAllowed)
return
}
if web.debug {
logger.Debug("%s %s", r.Method, r.RequestURI)
}
fixHeader(r.Header)
if err := r.ParseForm(); err != nil {
http.Error(w, "Request parse failed: "+err.Error(), http.StatusInternalServerError)
return
}
action := r.Form.Get("api_action")
// 判断是否要进行验证码检测
isCaptchaRequiredAction := false
if api.captchaMatcher.Match(action) {
isCaptchaRequiredAction = true
// 检测启动条件
needCaptcha := false
captchaConfig := api.captchaConfigs[action]
if captchaConfig.maxFailCount == 0 {
needCaptcha = true
} else {
// 检查是否超过最大失败次数
idVal := r.Form.Get(captchaConfig.identifier)
key := fmt.Sprintf("%s|%s", action, idVal)
item, has := web.captchaCache.Get(key)
if has && item.MustInt(0) > captchaConfig.maxFailCount {
needCaptcha = true
}
}
if needCaptcha {
id := r.Form.Get("captcha_id")
code := r.Form.Get("captcha_code")
if id == "" || code == "" {
var msg string
if r.Form.Get("api_lang") == "en_us" {
msg = "Missing captcha info!"
} else {
msg = "缺少验证码信息!"
}
errRes := _api.NewError("MissingCaptcha", msg)
_api.WriteData(w, r, errRes, action, "json", "", r.Form.Get("api_debug"))
return
}
if !captcha.VerifyString(id, code) {
// 验证失败,刷新验证码
captcha.Reload(id)
var msg string
if r.Form.Get("api_lang") == "en_us" {
msg = "Captcha verified failed!"
} else {
msg = "验证码错误!"
}
errRes := _api.NewError("InvalidCaptcha", msg)
_api.WriteData(w, r, errRes, action, "json", "", r.Form.Get("api_debug"))
return
} else {
// 验证成功,也一样刷新,防止重复利用
captcha.Reload(id)
}
}
}
// 代理到后端API
client := apiclient.NewClient(api.server)
if api.addr != "" {
client.GWADDR = api.addr
}
if api.signKey != "" {
client.SignKey = api.signKey
}
if api.nonceLength > 0 {
client.NonceEnabled = true
client.NonceLength = api.nonceLength
}
for k, v := range api.params {
client.SetDefaultParam(k, v)
}
// 判断是否websocket请求
if strings.ToLower(r.Header.Get("Upgrade")) != "websocket" {
var resp *apiclient.Response
var err error
// 删除头部cookie,重新构造
cookies := r.Cookies()
r.Header.Del("Cookie")
for _, cookie := range cookies {
// 删除未在配置中的cookie
remoteCookieName, exists := api.proxySessionRMap[cookie.Name]
if !exists {
continue
}
// 名称不同,则需要解密后重新加密
if remoteCookieName != cookie.Name {
localSessionStore := web.GetSessionStore(remoteCookieName)
// 解密cookie
dst := make(map[interface{}]interface{})
web.sessionStoreMutex.RLock()
err := securecookie.DecodeMulti(cookie.Name, cookie.Value, &dst, localSessionStore.Codecs...)
web.sessionStoreMutex.RUnlock()
if err != nil {
// 无法识别,丢弃
continue
}
// 改名后重新加密
cookie.Name = remoteCookieName
web.sessionStoreMutex.RLock()
cookie.Value, err = securecookie.EncodeMulti(cookie.Name, dst, localSessionStore.Codecs...)
web.sessionStoreMutex.RUnlock()
if err != nil {
// 加密失败,丢弃
continue
}
}
r.Header.Add("Cookie", cookie.String())
}
var remoteIp string
if r.RemoteAddr != "@" {
remoteIp, _, _ = net.SplitHostPort(r.RemoteAddr)
} else {
// unix domain socket
remoteIp = "@"
}
r.Header.Set("X-Real-IP", remoteIp)
switch r.Method {
case "GET":
resp, err = client.Get(action, r.Form, r.Header)
case "POST":
// 判断是否上传文件
isMultiPart := false
contentType := r.Header.Get("Content-Type")
if contentType != "" {
mediaType, _, err := mime.ParseMediaType(contentType)
if err == nil && strings.HasPrefix(mediaType, "multipart/") {
isMultiPart = true
}
}
if !isMultiPart {
resp, err = client.Post(action, r.Form, r.Header)
} else {
resp, err = client.Upload(action, r.Form, r.Header, r.Body)
}
default:
return
}
if err != nil {
if resp != nil && resp.Header.Get("X-Allow-Error-Response") == "on" {
// 返回原始响应内容
// return resp, nil
} else {
logger.Error("Request to api gateway failed: %s", err.Error())
http.Error(w, "Request to api gateway failed!", http.StatusBadGateway)
return
}
}
// 解析Cookie,管理session过期
cookies = resp.Cookies()
resp.Header.Del("Set-Cookie") // 清除头部重新构造
for _, cookie := range cookies {
// 删除未在配置中的cookie
localCookieName, exists := api.proxySessionMap[cookie.Name]
if !exists {
logger.Warning("Cookie name '%s' in api response is not defined, ignored.", cookie.Name)
continue
}
localSessionStore := web.GetSessionStore(localCookieName)
dst := make(map[interface{}]interface{})
web.sessionStoreMutex.RLock()
err := securecookie.DecodeMulti(cookie.Name, cookie.Value, &dst, localSessionStore.Codecs...)
web.sessionStoreMutex.RUnlock()
if err != nil {
if web.debug {
// 调试模式下,自动重新获取keypair
logger.Error(
"Cookie '%s' in api response decode failed: '%s', try update cookie key.",
cookie.Name, err.Error(),
)
web.updateCookieKey(api)
web.sessionStoreMutex.RLock()
err := securecookie.DecodeMulti(cookie.Name, cookie.Value, &dst, localSessionStore.Codecs...)
web.sessionStoreMutex.RUnlock()
if err != nil {
// 仍然无法识别,丢弃
logger.Error(
"Cookie '%s' in api response decode failed: '%s', ignored.",
cookie.Name, err.Error(),
)
continue
}
} else {
// 无法识别,丢弃
logger.Error(
"Cookie '%s' in api response decode failed: '%s', ignored.",
cookie.Name, err.Error(),
)
continue
}
}
// 取出session_id
sessionId := ""
for k, v := range dst {
kk, ok := k.(string)
if !ok {
continue
}
if kk == "session_id" {
vv, ok := v.(string)
if !ok {
continue
}
sessionId = vv
}
}
if sessionId != "" {
// 更新cache
web.sessionStoreMutex.RLock()
localSessionStore.SessionCache.SetIfNotExist(sessionId, true)
web.sessionStoreMutex.RUnlock()
}
// 名称不同,则需要重新加密
if localCookieName != cookie.Name {
cookie.Name = localCookieName
web.sessionStoreMutex.RLock()
cookie.Value, err = securecookie.EncodeMulti(cookie.Name, dst, localSessionStore.Codecs...)
web.sessionStoreMutex.RUnlock()
if err != nil {
// 加密失败,丢弃
logger.Error(
"Cookie '%s' in api response encode failed: '%s', ignored.",
cookie.Name, err.Error(),
)
continue
}
}
resp.Header.Add("Set-Cookie", cookie.String())
}
// 原样输出
header := w.Header()
for k, v := range resp.Header {
header[k] = v
}
// 操作失败次数计数,用于验证码开启检测
if isCaptchaRequiredAction {
if apiResult, err := resp.Result(); err == nil {
captchaConfig := api.captchaConfigs[action]
if captchaConfig.maxFailCount > 0 {
idVal := r.Form.Get(captchaConfig.identifier)
key := fmt.Sprintf("%s|%s", action, idVal)
if apiResult.CODE != "ok" {
// 登录失败,计数加1
item, has := web.captchaCache.Get(key)
if !has {
web.captchaCache.Set(key, 1)
} else {
web.captchaCache.Set(key, item.MustInt(0)+1)
}
} else {
// 登录成功,清空计数
if web.captchaCache.Has(key) {
web.captchaCache.Set(key, 0)
}
}
}
}
}
w.WriteHeader(resp.StatusCode)
if isCaptchaRequiredAction {
// body已被读取
body, err := resp.String()
if err != nil {
logger.Error("API gateway response write failed: %s", err.Error())
http.Error(w, "API gateway response get failed!", http.StatusInternalServerError)
return
}
_, err = w.Write([]byte(body))
if err != nil {
logger.Error("API gateway response write failed: %s", err.Error())
http.Error(w, "API gateway response write failed!", http.StatusInternalServerError)
return
}
} else {
// body未被读取
defer resp.Body.Close()
_, err = io.Copy(w, resp.Body)
if err != nil {
logger.Error("API gateway response write failed: %s", err.Error())
http.Error(w, "API gateway response write failed!", http.StatusInternalServerError)
return
}
}
} else {
// websocket
_, err := client.Websocket(action, r.Form, r.Header, w, r)
if err != nil {
// 非关闭连接错误,均打印日志,连接错误示例:
// websocket failed: websocket: close 1005
isCloseError := false
if _, ok := err.(*websocket.CloseError); ok {
isCloseError = true
} else if strings.Contains(err.Error(), "use of closed network connection") {
// go库net/net.go中获取到的网络错误,如:
// websocket failed: read tcp 192.168.1.140:8888->192.168.1.52:51058: use of closed network connection
isCloseError = true
} else if strings.Contains(err.Error(), "unexpected EOF") {
// websocket: close 1006 unexpected EOF
isCloseError = true
}
if !isCloseError {
logger.Error("websocket failed: %s", err.Error())
}
return
}
}
}
}
func (web *Web) updateCookieKeyLoop(api *API) {
for {
web.updateCookieKey(api)
time.Sleep(time.Duration(24) * time.Hour)
}
}
// 更新Cookie密钥,如果失败则不断间隔重试
func (web *Web) updateCookieKey(api *API) {
for {
if strings.HasPrefix(api.addr, "/") {
// unix domain socket
if !utils.FileExists(api.addr) {
time.Sleep(time.Second * time.Duration(1))
continue
}
}
client := apiclient.NewClient(api.server)
if api.addr != "" {
client.GWADDR = api.addr
}
if api.proxySessionSignKey != "" {
client.SignKey = api.proxySessionSignKey
}
if api.proxySessionNonceLength > 0 {
client.NonceEnabled = true
client.NonceLength = api.proxySessionNonceLength
}
client.SetOverrideParam("api_format", "json")
resp, err := client.Get(api.proxySessionAction, nil, nil)
if err != nil {
// 不显示该日志,因为要重试的可能性比较大
// logger.Error("Fetch session encrypt key failed: %s", err.Error())
time.Sleep(time.Second * time.Duration(1))
continue
}
j, err := resp.Json()
if err != nil {
logger.Error("Unrecognized result when fetch session encrypt key!")
time.Sleep(time.Second * time.Duration(1))
continue
}
rsCode := j.Get("CODE").MustString()
if rsCode != "ok" {
logger.Error("API return code '%s' when fetch session encrypt key!", rsCode)
time.Sleep(time.Second * time.Duration(1))
continue
}
data := j.Get("DATA").MustArray()
keyPairs := make([][]byte, 0, len(data))
hasErr := false
for _, v := range data {
s, ok := v.(string)
if !ok {
logger.Error("API return code '%s' when fetch session encrypt key!", rsCode)
hasErr = true
break
}
key, err := base64.StdEncoding.DecodeString(s)
if err != nil {
logger.Error("Unrecognized result of session encrypt key!")
hasErr = true
break
}
keyPairs = append(keyPairs, key)
}
if hasErr {
time.Sleep(time.Second * time.Duration(1))
continue
}
// 更新当前 key
web.sessionStoreMutex.Lock()
for _, local := range api.proxySessionMap {
api.proxySessionKeyPairs = keyPairs
web.GetSessionStore(local).LoadKeyPairs(keyPairs)
}
web.sessionStoreMutex.Unlock()
if web.debug {
logger.Debug("Successfully update cookie encrypt key from: %s", api.server)
}
// 更新成功,退出
break
}
}