374 lines
9.2 KiB
Go
374 lines
9.2 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/perfect-panel/server/internal/svc"
|
|
pkgaes "github.com/perfect-panel/server/pkg/aes"
|
|
"github.com/perfect-panel/server/pkg/constant"
|
|
"github.com/perfect-panel/server/pkg/result"
|
|
"github.com/perfect-panel/server/pkg/xerr"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
noWritten = -1
|
|
defaultStatus = http.StatusOK
|
|
|
|
ctxDeviceDecryptStatusKey = "device_decrypt_status"
|
|
ctxDeviceDecryptReasonKey = "device_decrypt_reason"
|
|
ctxEncryptedQueryKey = "encrypted_query"
|
|
ctxDecryptedQueryKey = "decrypted_query"
|
|
ctxEncryptedBodyKey = "encrypted_request_body"
|
|
ctxDecryptedBodyKey = "decrypted_request_body"
|
|
|
|
deviceDecryptSkipPathPublicFileUpload = "/v1/public/file/upload"
|
|
)
|
|
|
|
func DeviceMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
return func(c *gin.Context) {
|
|
|
|
if !srvCtx.Config.Device.Enable {
|
|
c.Set(ctxDeviceDecryptStatusKey, "skipped")
|
|
c.Set(ctxDeviceDecryptReasonKey, "device_encryption_disabled")
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
if srvCtx.Config.Device.SecuritySecret == "" {
|
|
c.Set(ctxDeviceDecryptStatusKey, "failed")
|
|
c.Set(ctxDeviceDecryptReasonKey, "device_secret_empty")
|
|
result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.SecretIsEmpty), "Secret is empty"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
if ctx.Value(constant.CtxKeyUser) == nil && c.GetHeader("Login-Type") != "" {
|
|
ctx = context.WithValue(ctx, constant.CtxLoginType, c.GetHeader("Login-Type"))
|
|
c.Request = c.Request.WithContext(ctx)
|
|
}
|
|
|
|
loginType, ok := ctx.Value(constant.CtxLoginType).(string)
|
|
if !ok || loginType != "device" {
|
|
c.Set(ctxDeviceDecryptStatusKey, "skipped")
|
|
if ok {
|
|
c.Set(ctxDeviceDecryptReasonKey, fmt.Sprintf("login_type_%s_not_device", loginType))
|
|
} else {
|
|
c.Set(ctxDeviceDecryptReasonKey, "login_type_not_found")
|
|
}
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
rw := NewResponseWriter(c, srvCtx)
|
|
if shouldSkipDeviceRequestDecrypt(c) {
|
|
c.Set(ctxDeviceDecryptStatusKey, "skipped")
|
|
c.Set(ctxDeviceDecryptReasonKey, "multipart_upload_passthrough")
|
|
c.Writer = rw
|
|
c.Next()
|
|
rw.FlushAbort()
|
|
return
|
|
}
|
|
if !rw.Decrypt() {
|
|
c.Set(ctxDeviceDecryptStatusKey, "failed")
|
|
if _, exists := c.Get(ctxDeviceDecryptReasonKey); !exists {
|
|
c.Set(ctxDeviceDecryptReasonKey, "decrypt_failed")
|
|
}
|
|
result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidCiphertext), "Invalid ciphertext"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
c.Writer = rw
|
|
c.Next()
|
|
rw.FlushAbort()
|
|
}
|
|
}
|
|
|
|
func shouldSkipDeviceRequestDecrypt(c *gin.Context) bool {
|
|
if c.Request.URL.Path != deviceDecryptSkipPathPublicFileUpload {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(c.GetHeader("Content-Type"))), "multipart/form-data")
|
|
}
|
|
|
|
func NewResponseWriter(c *gin.Context, srvCtx *svc.ServiceContext) (rw *ResponseWriter) {
|
|
rw = &ResponseWriter{
|
|
c: c,
|
|
body: new(bytes.Buffer),
|
|
size: noWritten,
|
|
status: defaultStatus,
|
|
ResponseWriter: c.Writer,
|
|
}
|
|
rw.encryptionKey = srvCtx.Config.Device.SecuritySecret
|
|
rw.encryptionMethod = "AES"
|
|
rw.encryption = true
|
|
return rw
|
|
}
|
|
|
|
func (rw *ResponseWriter) Encrypt() {
|
|
if !rw.encryption {
|
|
return
|
|
}
|
|
buf := rw.body.Bytes()
|
|
params := map[string]interface{}{}
|
|
err := json.Unmarshal(buf, ¶ms)
|
|
if err != nil {
|
|
return
|
|
}
|
|
data := params["data"]
|
|
if data != nil {
|
|
var jsonData []byte
|
|
str, ok := data.(string)
|
|
if ok {
|
|
jsonData = []byte(str)
|
|
} else {
|
|
jsonData, _ = json.Marshal(data)
|
|
}
|
|
encrypt, iv, err := pkgaes.Encrypt(jsonData, rw.encryptionKey)
|
|
if err != nil {
|
|
return
|
|
}
|
|
params["data"] = map[string]interface{}{
|
|
"data": encrypt,
|
|
"time": iv,
|
|
}
|
|
|
|
}
|
|
marshal, _ := json.Marshal(params)
|
|
rw.body.Reset()
|
|
rw.body.Write(marshal)
|
|
}
|
|
|
|
func (rw *ResponseWriter) Decrypt() bool {
|
|
if !rw.encryption {
|
|
rw.c.Set(ctxDeviceDecryptStatusKey, "skipped")
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, "response_encryption_disabled")
|
|
return true
|
|
}
|
|
|
|
//判断url链接中是否存在data和iv数据,存在就进行解密并设置回去
|
|
query := rw.c.Request.URL.Query()
|
|
originalRawQuery := rw.c.Request.URL.RawQuery
|
|
dataStr := query.Get("data")
|
|
timeStr := query.Get("time")
|
|
hasEncryptedQuery := dataStr != "" && timeStr != ""
|
|
queryDecrypted := false
|
|
if hasEncryptedQuery {
|
|
rw.c.Set(ctxEncryptedQueryKey, originalRawQuery)
|
|
}
|
|
if dataStr != "" && timeStr != "" {
|
|
decrypt, err := pkgaes.Decrypt(dataStr, rw.encryptionKey, timeStr)
|
|
if err == nil {
|
|
params := map[string]interface{}{}
|
|
err = json.Unmarshal([]byte(decrypt), ¶ms)
|
|
if err == nil {
|
|
for k, v := range params {
|
|
query.Set(k, fmt.Sprintf("%v", v))
|
|
}
|
|
query.Del("data")
|
|
query.Del("time")
|
|
rw.c.Request.RequestURI = fmt.Sprintf("%s?%s", rw.c.Request.RequestURI[:strings.Index(rw.c.Request.RequestURI, "?")], query.Encode())
|
|
rw.c.Request.URL.RawQuery = query.Encode()
|
|
rw.c.Set(ctxDecryptedQueryKey, query.Encode())
|
|
queryDecrypted = true
|
|
}
|
|
} else {
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, fmt.Sprintf("query_decrypt_failed:%v", err))
|
|
}
|
|
}
|
|
|
|
//判断body是否存在数据,存在就尝试解密,并设置回去
|
|
body, err := io.ReadAll(rw.c.Request.Body)
|
|
if err != nil {
|
|
if queryDecrypted {
|
|
rw.c.Set(ctxDeviceDecryptStatusKey, "success")
|
|
} else {
|
|
rw.c.Set(ctxDeviceDecryptStatusKey, "skipped")
|
|
}
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, fmt.Sprintf("read_body_failed:%v", err))
|
|
return true
|
|
}
|
|
|
|
if len(body) == 0 {
|
|
if queryDecrypted {
|
|
rw.c.Set(ctxDeviceDecryptStatusKey, "success")
|
|
} else {
|
|
rw.c.Set(ctxDeviceDecryptStatusKey, "skipped")
|
|
if hasEncryptedQuery {
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, "query_decrypt_failed")
|
|
} else {
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, "empty_body")
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
rw.c.Set(ctxEncryptedBodyKey, string(body))
|
|
|
|
params := map[string]interface{}{}
|
|
err = json.Unmarshal(body, ¶ms)
|
|
data := params["data"]
|
|
nonce := params["time"]
|
|
if err != nil || data == nil {
|
|
if err != nil {
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, fmt.Sprintf("body_unmarshal_failed:%v", err))
|
|
} else {
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, "body_data_field_missing")
|
|
}
|
|
return false
|
|
}
|
|
|
|
str, ok := data.(string)
|
|
if !ok {
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, "body_data_not_string")
|
|
return false
|
|
}
|
|
iv, ok := nonce.(string)
|
|
if !ok {
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, "body_time_not_string")
|
|
return false
|
|
}
|
|
|
|
decrypt, err := pkgaes.Decrypt(str, rw.encryptionKey, iv)
|
|
if err != nil {
|
|
rw.c.Set(ctxDeviceDecryptReasonKey, fmt.Sprintf("body_decrypt_failed:%v", err))
|
|
return false
|
|
}
|
|
rw.c.Request.Body = io.NopCloser(bytes.NewBuffer([]byte(decrypt)))
|
|
rw.c.Request.Header.Set("Content-Type", "application/json")
|
|
rw.c.Set(ctxDecryptedBodyKey, decrypt)
|
|
rw.c.Set(ctxDeviceDecryptStatusKey, "success")
|
|
return true
|
|
}
|
|
|
|
func (rw *ResponseWriter) FlushAbort() {
|
|
defer rw.c.Abort()
|
|
rw.flush = true
|
|
if rw.encryption {
|
|
rw.Encrypt()
|
|
}
|
|
_, err := rw.Write(rw.body.Bytes())
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
type ResponseWriter struct {
|
|
http.ResponseWriter
|
|
size int
|
|
status int
|
|
flush bool
|
|
body *bytes.Buffer
|
|
c *gin.Context
|
|
encryption bool
|
|
encryptionKey string
|
|
encryptionMethod string
|
|
}
|
|
|
|
func (rw *ResponseWriter) Unwrap() http.ResponseWriter {
|
|
return rw.ResponseWriter
|
|
}
|
|
|
|
//nolint:unused
|
|
func (rw *ResponseWriter) reset(writer http.ResponseWriter) {
|
|
rw.ResponseWriter = writer
|
|
rw.size = noWritten
|
|
rw.status = defaultStatus
|
|
}
|
|
|
|
func (rw *ResponseWriter) WriteHeader(code int) {
|
|
if code > 0 && rw.status != code {
|
|
if rw.Written() {
|
|
return
|
|
}
|
|
rw.status = code
|
|
}
|
|
}
|
|
|
|
func (rw *ResponseWriter) WriteHeaderNow() {
|
|
if !rw.Written() {
|
|
rw.size = 0
|
|
rw.ResponseWriter.WriteHeader(rw.status)
|
|
}
|
|
}
|
|
|
|
func (rw *ResponseWriter) Write(data []byte) (n int, err error) {
|
|
if rw.flush {
|
|
rw.WriteHeaderNow()
|
|
n, err = rw.ResponseWriter.Write(data)
|
|
rw.size += n
|
|
} else {
|
|
rw.body.Write(data)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (rw *ResponseWriter) WriteString(s string) (n int, err error) {
|
|
if rw.flush {
|
|
rw.WriteHeaderNow()
|
|
n, err = rw.ResponseWriter.Write([]byte(s))
|
|
rw.size += n
|
|
} else {
|
|
rw.body.Write([]byte(s))
|
|
}
|
|
return
|
|
}
|
|
|
|
func (rw *ResponseWriter) Status() int {
|
|
return rw.status
|
|
}
|
|
|
|
func (rw *ResponseWriter) Size() int {
|
|
return rw.size
|
|
}
|
|
|
|
func (rw *ResponseWriter) Written() bool {
|
|
return rw.size != noWritten
|
|
}
|
|
|
|
// Hijack implements the http.Hijacker interface.
|
|
func (rw *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
if rw.size < 0 {
|
|
rw.size = 0
|
|
}
|
|
return rw.ResponseWriter.(http.Hijacker).Hijack()
|
|
}
|
|
|
|
// CloseNotify implements the http.CloseNotifier interface.
|
|
func (rw *ResponseWriter) CloseNotify() <-chan bool {
|
|
// 通过 r.Context().Done() 来监听请求的取消
|
|
done := rw.c.Request.Context().Done()
|
|
closed := make(chan bool)
|
|
|
|
// 当上下文被取消时,通过 closed channel 发送通知
|
|
go func() {
|
|
<-done
|
|
closed <- true
|
|
}()
|
|
|
|
return closed
|
|
}
|
|
|
|
// Flush implements the http.Flusher interface.
|
|
func (rw *ResponseWriter) Flush() {
|
|
rw.WriteHeaderNow()
|
|
rw.ResponseWriter.(http.Flusher).Flush()
|
|
}
|
|
|
|
func (rw *ResponseWriter) Pusher() (pusher http.Pusher) {
|
|
if pusher, ok := rw.ResponseWriter.(http.Pusher); ok {
|
|
return pusher
|
|
}
|
|
return nil
|
|
}
|