Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
package cors
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
type Options struct {
Origin *[]string //传入空指针,表示允许"*", 传入空数组表示禁止跨域
Methods []string //Methods
AllowedHeaders []string //AllowHeaders
ExposedHeaders []string //exposeHeader
Credentials bool //cookie
MaxAge int64 //缓存MaxAge
PreflightContinue bool //在遇到Options的时候,继续,而不是返回请求
OptionsSuccessStatus int //Options时,返回的状态码,默认204
}
func configureOrigin(o *Options, c *gin.Context) map[string]string {
reqOrigin := c.GetHeader("Origin")
if o.Origin == nil {
return map[string]string{"Access-Control-Allow-Origin": "*"}
}
headers := make(map[string]string)
isAllowed := false
for _, allowed := range *o.Origin {
if allowed == reqOrigin {
isAllowed = true
break
}
}
if isAllowed {
headers["Access-Control-Allow-Origin"] = reqOrigin
} else {
headers["Access-Control-Allow-Origin"] = strconv.FormatBool(false)
}
headers["Vary"] = "Origin"
return headers
}
func configureMethods(o *Options) map[string]string {
headers := make(map[string]string)
if len(o.Methods) > 0 {
headers["Access-Control-Allow-Methods"] = strings.ToUpper(strings.Join(o.Methods, ","))
}
return headers
}
func configureCredentials(o *Options) map[string]string {
headers := make(map[string]string)
if o.Credentials {
headers["Access-Control-Allow-Credentials"] = strconv.FormatBool(true)
}
return headers
}
func configureAllowedHeaders(o *Options, c *gin.Context) map[string]string {
headers := make(map[string]string)
if len(o.AllowedHeaders) > 0 {
headers["Access-Control-Allow-Headers"] = strings.Join(o.AllowedHeaders, ",")
}
return headers
}
func configureExposedHeaders(o *Options) map[string]string {
var headers = make(map[string]string)
if len(o.ExposedHeaders) > 0 {
headers["Access-Control-Expose-Headers"] = strings.Join(o.ExposedHeaders, ",")
}
return headers
}
func configureMaxAge(o *Options) map[string]string {
var headers = make(map[string]string)
if o.MaxAge >= 0 {
headers["Access-Control-Max-Age"] = strconv.FormatInt(o.MaxAge, 10)
}
return headers
}
func mergeMap(m ...map[string]string) map[string]string {
r := make(map[string]string)
for _, t := range m {
for k, v := range t {
r[k] = v
}
}
return r
}
func applyHeader(c *gin.Context, h map[string]string) {
for k, v := range h {
c.Header(k, v)
}
}
// Cors 跨域处理
func Cors(o *Options) gin.HandlerFunc {
if o.OptionsSuccessStatus == 0 {
o.OptionsSuccessStatus = http.StatusNoContent
}
return func(c *gin.Context) {
method := c.Request.Method
//c.Header("Access-Control-Allow-Origin", "*")
//c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,Content-Length,Etag,Content-Range,Accept-Ranges,User-Agent,Range,Bucket,Content-Disposition,Signature,X-Forwarded-For,X-Real-Ip")
if method == "OPTIONS" {
h := mergeMap(
configureOrigin(o, c),
configureCredentials(o),
configureMethods(o),
configureAllowedHeaders(o, c),
configureMaxAge(o),
configureExposedHeaders(o),
)
applyHeader(c, h)
if o.PreflightContinue {
return
}
c.Header("Content-Length", "0")
c.AbortWithStatus(o.OptionsSuccessStatus)
return
}
h := mergeMap(
configureOrigin(o, c),
configureCredentials(o),
configureExposedHeaders(o),
)
applyHeader(c, h)
}
}
+100
View File
@@ -0,0 +1,100 @@
package ginzap
import (
"net"
"net/http"
"os"
"runtime/debug"
"strings"
"time"
"91porn-server/common/hevcpull"
"91porn-server/common/log"
"github.com/gin-gonic/gin"
)
func Logger(ctxKeys []string) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := hevcpull.RedactText(c.Request.URL.RawQuery)
c.Next()
end := time.Now()
latency := end.Sub(start)
if len(c.Errors) > 0 {
for _, e := range c.Errors.Errors() {
log.Error(e, log.R(c))
}
} else {
l := []log.Field{
log.R(c),
log.Any("status", c.Writer.Status()),
log.Any("method", c.Request.Method),
log.Any("path", path),
log.Any("query", query),
log.Any("user-agent", c.Request.UserAgent()),
log.Any("time", end.Format("2006-01-02 15:04:05")),
log.Any("latency", latency),
}
for _, k := range ctxKeys {
v, e := c.Get(k)
if e {
l = append(l, log.Any(k, v))
}
}
log.Info(path, l...)
}
}
}
// RecoveryWithZap returns a gin.HandlerFunc (middleware)
// that recovers from any panics and logs requests using uber-go/zap.
// All errors are logged using zap.Error().
// stack means whether output the stack info.
// The stack info is easy to find where the error occurs but the stack info is too large.
func Recovery(stack bool, do func(err interface{})) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
if do != nil {
do(err)
}
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
// Record request identity without exposing authentication headers or body.
requestFields := []log.Field{
log.R(c),
log.Any("method", c.Request.Method),
log.Any("path", c.Request.URL.Path),
log.Any("panic", err),
}
if brokenPipe {
log.Error(c.Request.URL.Path, requestFields...)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
if stack {
requestFields = append(requestFields, log.Any("stack", string(debug.Stack())))
}
log.Error("[Recovery from panic]", requestFields...)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"result": false,
"error": http.StatusText(http.StatusInternalServerError),
})
}
}()
c.Next()
}
}
+17
View File
@@ -0,0 +1,17 @@
package ginzap
import (
"strings"
"testing"
"91porn-server/common/hevcpull"
)
func TestSignedPullQueryIsRedactedBeforeLogging(t *testing.T) {
const signature = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
query := "hevc_exp=1&hevc_sig=" + signature
safe := hevcpull.RedactText(query)
if strings.Contains(safe, signature) || !strings.Contains(safe, "[REDACTED]") {
t.Fatalf("signed pull query was not redacted: %s", safe)
}
}
+37
View File
@@ -0,0 +1,37 @@
package ip
import (
"net/http"
"91porn-server/common/constant"
"91porn-server/common/stderr"
"91porn-server/models/v/ipwhitemod"
"91porn-server/web/webg"
"github.com/gin-gonic/gin"
)
// RealIP 获取真实IP
func RealIP(c *gin.Context) {
ip := c.ClientIP()
c.Set(constant.CtxIP, ip)
}
var accessForbidMsg = gin.H{
"code": stderr.ErrAccessForbid,
"msg": stderr.ErrAccessForbid.Msg(),
}
// RealIP 获取真实IP
func CheckWhiteIP(ctx *gin.Context) {
//检查ip是否在白名单中
// ipFlag, err := webg.Redis.SISMember(constant.IPWhiteRedisKey, constant.CtxIP)
// if webg.Conf.EnableIPWhite.IsEnable && (err != nil || !ipFlag) {
if webg.Conf.EnableIPWhite.IsEnable {
iPWhite, err := ipwhitemod.FindOneByIp(ctx.ClientIP())
if err != nil || iPWhite.IP == "" {
ctx.AbortWithStatusJSON(http.StatusOK, accessForbidMsg)
return
}
}
}
+39
View File
@@ -0,0 +1,39 @@
package requestid
import (
"strings"
"github.com/gin-gonic/gin"
)
func New(opts ...Option) gin.HandlerFunc {
var cfg config = config{
generator: DefaultFenerator,
}
for _, opt := range opts {
opt(&cfg)
}
return func(c *gin.Context) {
rid := strings.TrimSpace(c.GetHeader(HeaderKey))
clientProvided := rid != "" && len(rid) <= 128
if !clientProvided {
rid = cfg.generator()
}
c.Request.Header.Set(HeaderKey, rid)
c.Header(HeaderKey, rid)
c.Set(ContextKey, rid)
c.Set(ClientProvidedContextKey, clientProvided)
}
}
// FromClient 仅返回客户端显式提供且长度合规的请求ID。
func FromClient(c *gin.Context) (string, bool) {
if c == nil {
return "", false
}
provided, ok := c.Get(ClientProvidedContextKey)
if !ok || provided != true {
return "", false
}
return c.GetHeader(HeaderKey), true
}
+82
View File
@@ -0,0 +1,82 @@
package requestid
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func TestMiddlewareClientProvidedRequestID(t *testing.T) {
gin.SetMode(gin.TestMode)
var (
gotID string
gotProvided bool
)
router := gin.New()
router.Use(New(WithGenerator(func() string { return "generated-id" })))
router.GET("/", func(c *gin.Context) {
gotID, gotProvided = FromClient(c)
c.Status(http.StatusNoContent)
})
request := httptest.NewRequest(http.MethodGet, "/", nil)
request.Header.Set(HeaderKey, " client-id ")
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if gotID != "client-id" || !gotProvided {
t.Fatalf("FromClient() = (%q, %v), want (client-id, true)", gotID, gotProvided)
}
if got := response.Header().Get(HeaderKey); got != "client-id" {
t.Fatalf("response %s = %q, want client-id", HeaderKey, got)
}
}
func TestMiddlewareGeneratedRequestIDIsTracingOnly(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
requestID string
}{
{name: "missing"},
{name: "blank", requestID: " "},
{name: "too long", requestID: strings.Repeat("x", 129)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
gotID string
gotProvided bool
)
router := gin.New()
router.Use(New(WithGenerator(func() string { return "generated-id" })))
router.GET("/", func(c *gin.Context) {
gotID, gotProvided = FromClient(c)
c.Status(http.StatusNoContent)
})
request := httptest.NewRequest(http.MethodGet, "/", nil)
if tt.requestID != "" {
request.Header.Set(HeaderKey, tt.requestID)
}
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if gotID != "" || gotProvided {
t.Fatalf("FromClient() = (%q, %v), want empty and false", gotID, gotProvided)
}
if got := request.Header.Get(HeaderKey); got != "generated-id" {
t.Fatalf("request %s = %q, want generated-id", HeaderKey, got)
}
if got := response.Header().Get(HeaderKey); got != "generated-id" {
t.Fatalf("response %s = %q, want generated-id", HeaderKey, got)
}
})
}
}
+29
View File
@@ -0,0 +1,29 @@
package requestid
import uuid "github.com/satori/go.uuid"
const (
HeaderKey = "X-Request-ID"
ContextKey = "Ctx-Request-ID"
// ClientProvidedContextKey 标记请求ID是否由客户端显式提供;
// 服务端自动生成的ID仅用于链路追踪,不应自动开启业务幂等缓存。
ClientProvidedContextKey = "Ctx-Request-ID-Client-Provided"
)
type RequestIDGenerator func() string
type config struct {
generator RequestIDGenerator
}
type Option func(*config)
func WithGenerator(g RequestIDGenerator) Option {
return func(c *config) {
c.generator = g
}
}
var DefaultFenerator = func() string {
return uuid.NewV4().String()
}
+101
View File
@@ -0,0 +1,101 @@
package ua
import (
"net/url"
"strings"
"91porn-server/common/constant"
"91porn-server/common/log"
"github.com/gin-gonic/gin"
)
type UA struct {
UserAgent string
DevID string //设备id 唯一标识
DevType string //设备类型 华为nova7
SysType string //pc
Ver string //1.0
BuildID string //
Mac string //
GlobalDevID string //全局唯一表示
Terminal string //终端 0-客户端;1-h5(ios)端;2-web端
IsH5 string
SystemVersion string
SystemName string
DeviceBrand string
DeviceModel string
SID string
}
func UAer(c *gin.Context) {
userAgent := c.GetHeader("X-User-Agent") //兼容H5 UserAgent
if userAgent == "" {
userAgent = c.Request.UserAgent()
}
if userAgent == "" {
log.Warn("user-agent empty")
return
}
ua := Parse(userAgent)
if sid := strings.TrimSpace(c.GetHeader("sid")); sid != "" {
ua.SID = sid
}
c.Set(constant.CtxUA, ua)
}
func Parse(userAgent string) UA {
var uastr string
ua := UA{}
decodeUa, err := url.QueryUnescape(userAgent)
if err != nil {
log.Warn("URLDecode user-agent error", log.Any("ua", userAgent))
uastr = userAgent
} else {
uastr = decodeUa
}
uas := strings.Split(uastr, ";")
for _, v := range uas {
vss := strings.SplitN(strings.TrimSpace(v), "=", 2)
if len(vss) < 2 {
log.Warn("user-agent miss", log.Any("ua", userAgent))
continue
}
key := strings.TrimSpace(vss[0])
value := strings.TrimSpace(vss[1])
switch key {
case "DevID":
ua.DevID = value
case "DevType":
ua.DevType = value
case "SysType":
ua.SysType = value
case "Ver":
ua.Ver = value
case "BuildID":
ua.BuildID = value
case "Mac":
ua.Mac = value
case "GlobalDevID":
ua.GlobalDevID = value
case "Terminal":
ua.Terminal = value
case "IsH5":
ua.IsH5 = value
case "SystemVersion":
ua.SystemVersion = value
case "SystemName":
ua.SystemName = value
case "DeviceBrand":
ua.DeviceBrand = value
case "DeviceModel", "device_model":
ua.DeviceModel = value
case "SID", "Sid", "sid":
ua.SID = value
default:
log.Warn("user-agent unknown", log.Any("key", key), log.Any("ua", userAgent))
}
}
ua.UserAgent = uastr
return ua
}
+89
View File
@@ -0,0 +1,89 @@
package ua
import (
"net/http/httptest"
"strings"
"testing"
"91porn-server/common/constant"
"github.com/gin-gonic/gin"
)
func TestUAerParsesSIDFromHeaderAndUserAgent(t *testing.T) {
gin.SetMode(gin.TestMode)
body := `{"devID":"91porn_google_3695e94c7226d787","qrCnt":"","devType":"lynx:36:id=CP1A.260305.018","sysType":"android","ver":"1.0.6","buildID":"com.noporn.newchatone_zero_six","devToken":"Qp6IluBGdByscQvXczvffmfC7YQxO6zm3Rr/A52FBpk=","cutInfos":""}`
req := httptest.NewRequest("POST", "/api/app/mine/login", strings.NewReader(body))
req.Header.Set("sid", "3a73a39a03cf450893f89909e31d4bea")
req.Header.Set("user-agent", "DevID%3D91porn_google_3695e94c7226d787%3BDevType%3Dlynx%3A36%3Aid%3DCP1A.260305.018%3BSysType%3Dandroid%3BVer%3D1.0.6%3BBuildID%3Dcom.noporn.newchatone_zero_six%3BDeviceBrand%3Dgoogle%3BSystemName%3DAndroid%3BSystemVersion%3D16")
req.Header.Set("x-api-key", "timestamp=1776140330;sign=f8c0476aa413e3e21ae6c8e2ad02b0c0f6af6d25;nonce=d8b1f060-6c3b-458b-b059-7ef87d2e9f79")
req.Header.Set("accept-encoding", "*")
req.Header.Set("device", "android")
req.Header.Set("host", "91pornht.remoces.com")
req.Header.Set("authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzYxNDAyNTc3ODk4NjUyMDAsInR5cGUiOjAsInVpZCI6MzAzNzQzfQ.1tXpLY-FsDu9HzkT5SVVbMaOMtzkgLU5PBZzvHm-X0I")
req.Header.Set("api_version", "1.0.0")
req.Header.Set("content-type", "application/json;charset=UTF-8")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
UAer(c)
value, ok := c.Get(constant.CtxUA)
if !ok {
t.Fatal("UAer did not set UA in context")
}
got, ok := value.(UA)
if !ok {
t.Fatalf("context UA type = %T, want ua.UA", value)
}
if got.SID != "3a73a39a03cf450893f89909e31d4bea" {
t.Fatalf("SID = %q, want header sid", got.SID)
}
if got.DevID != "91porn_google_3695e94c7226d787" {
t.Fatalf("DevID = %q", got.DevID)
}
if got.DevType != "lynx:36:id=CP1A.260305.018" {
t.Fatalf("DevType = %q", got.DevType)
}
if got.SysType != "android" {
t.Fatalf("SysType = %q", got.SysType)
}
if got.Ver != "1.0.6" {
t.Fatalf("Ver = %q", got.Ver)
}
if got.BuildID != "com.noporn.newchatone_zero_six" {
t.Fatalf("BuildID = %q", got.BuildID)
}
if got.DeviceBrand != "google" {
t.Fatalf("DeviceBrand = %q", got.DeviceBrand)
}
if got.SystemName != "Android" {
t.Fatalf("SystemName = %q", got.SystemName)
}
if got.SystemVersion != "16" {
t.Fatalf("SystemVersion = %q", got.SystemVersion)
}
}
func TestParseAcceptsSIDKeyVariants(t *testing.T) {
for _, tc := range []struct {
name string
userAgent string
want string
}{
{name: "sid", userAgent: "DevID=device;sid=lower", want: "lower"},
{name: "Sid", userAgent: "DevID=device;Sid=title", want: "title"},
{name: "SID", userAgent: "DevID=device;SID=upper", want: "upper"},
} {
t.Run(tc.name, func(t *testing.T) {
got := Parse(tc.userAgent)
if got.SID != tc.want {
t.Fatalf("SID = %q, want %q", got.SID, tc.want)
}
})
}
}