Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-22 14:47:17 +07:00
co-authored by Claude Opus 5.5
commit 3639d2184a
574 changed files with 61690 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
package http
import "strings"
// ApplyClientForwardHeaders 向上游透传客户端 IP 与浏览器 UA。
func ApplyClientForwardHeaders(header map[string]string, clientIP, userAgent string) {
if header == nil {
return
}
if ip := strings.TrimSpace(clientIP); ip != "" {
header["X-Real-IP"] = ip
}
if ua := strings.TrimSpace(userAgent); ua != "" {
header["User-Agent"] = ua
}
}
// ApplyClientForwardHeadersWithXUA 透传 IP、浏览器 UA,以及可选的 X-User-Agent。
func ApplyClientForwardHeadersWithXUA(header map[string]string, clientIP, userAgent, xUserAgent string) {
ApplyClientForwardHeaders(header, clientIP, userAgent)
if header == nil {
return
}
if xua := strings.TrimSpace(xUserAgent); xua != "" {
header["X-User-Agent"] = xua
}
}
+24
View File
@@ -0,0 +1,24 @@
package http
// ForwardExcludedHeaders 是本服务转发请求到上游 serverAddr 时必须剔除的请求头集合。
// key 使用 net/http.Header 的规范形式 (textproto.CanonicalMIMEHeaderKey),
// 调用方可直接用 http.Header 的 key 做查找。
//
// 包含两类:
// 1. RFC 7230 §6.1 定义的 hop-by-hop 头,不允许跨代理转发。
// 2. 本进程内部注入的控制标志,只对本进程的中间件有意义,不能让上游看见。
var ForwardExcludedHeaders = map[string]bool{
// hop-by-hop
"Connection": true,
"Keep-Alive": true,
"Proxy-Authenticate": true,
"Proxy-Authorization": true,
"Te": true,
"Trailers": true,
"Transfer-Encoding": true,
"Upgrade": true,
// 内部 DoS 防护标志,由 server.headToGet 注入,
// 仅供 templatecache / filecache 判断是否回填缓存。
"X-Origin-Head": true,
}
+242
View File
@@ -0,0 +1,242 @@
package http
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"reflect"
"time"
)
var (
client = http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConnsPerHost: 5,
MaxConnsPerHost: 100,
},
}
)
type ByteSize int64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
)
var (
outOfBodySizeErr = errors.New("out of body size limit")
)
type ResponseBody struct {
StatusCode int
Data []byte
}
type ResponseBody2 struct {
StatusCode int
Data []byte
Header http.Header
}
type RequestFormData map[string]string
// Post
// Note: 只会读取小于1M的数据
func Post(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) {
fr, err = do(http.MethodPost, url, header, body, int64(1024*MB))
return
}
func PostJson(url string, body io.Reader) (fr *ResponseBody, err error) {
header := map[string]string{}
header["Content-Type"] = "application/json"
fr, err = do(http.MethodPost, url, header, body, int64(1024*KB))
return
}
// PostFormData 提交标准的form表单
func PostFormData(url string, fromdata RequestFormData) (fr *ResponseBody, err error) {
var b bytes.Buffer
writer := multipart.NewWriter(&b)
for k, v := range fromdata {
if err = writer.WriteField(k, v); err != nil {
return
}
}
if err = writer.Close(); err != nil {
return
}
header := map[string]string{"Content-Type": writer.FormDataContentType()}
fr, err = do(http.MethodPost, url, header, &b, 2*int64(KB))
return
}
func DoHost(method string, url string, header map[string]string, body io.Reader, host string) (fr *ResponseBody2, err error) {
fr, err = doHost(method, url, header, body, int64(2*MB), host)
return
}
// Get
// Note: 只会读取小于1M的数据
func Get(url string, header map[string]string) (fr *ResponseBody, err error) {
fr, err = do(http.MethodGet, url, header, nil, int64(KB))
return
}
// GetLargeFile 获取大文件。2M
func GetLargeFile(url string, header map[string]string) (fr *ResponseBody, err error) {
fr, err = do(http.MethodGet, url, header, nil, int64(20*MB))
return
}
func doHost(method string, url string, header map[string]string, body io.Reader, size int64, host string) (fr *ResponseBody2, err error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return
}
for k, v := range header {
req.Header.Set(k, v)
}
req.Host = host
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.ContentLength > size {
err = outOfBodySizeErr
return
}
data, err := ioutil.ReadAll(io.LimitReader(resp.Body, size))
if err != nil {
return
}
fr = &ResponseBody2{
StatusCode: resp.StatusCode,
Data: data,
Header: resp.Header,
}
return
}
func do(method string, url string, header map[string]string, body io.Reader, size int64) (fr *ResponseBody, err error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return
}
for k, v := range header {
req.Header.Set(k, v)
}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.ContentLength > size {
err = outOfBodySizeErr
return
}
data, err := ioutil.ReadAll(io.LimitReader(resp.Body, size))
if err != nil {
return
}
fr = &ResponseBody{
StatusCode: resp.StatusCode,
Data: data,
}
return
}
// 推送对象到文件服务器
func PostObject(ctx context.Context, url string, header map[string]string, body []byte, objName string) (string, error) {
base64data := base64.StdEncoding.EncodeToString(body)
req := struct {
Name string `json:"fileName"`
Data string `json:"fileData"`
}{
Name: objName,
Data: base64data,
}
jsonBytes, _ := json.Marshal(req)
resp, err := Post(url, header, bytes.NewReader(jsonBytes))
if err != nil {
return "", err
}
type Obj struct {
Domain string `json:"domain"`
Name string `json:"fileName"`
}
fsResp := struct {
Code int `json:"code"`
Object Obj `json:"data"`
Msg string `json:"msg"`
}{}
if err = json.Unmarshal(resp.Data, &fsResp); err != nil {
return "", err
}
if fsResp.Code != 200 || fsResp.Object.Name == "" {
return "", fmt.Errorf("上传fs失败")
}
return fsResp.Object.Name, nil
}
func PostWithBind(ctx context.Context, bind interface{}, url string, body io.Reader) error {
if err := verifyBind(bind); err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("response status code:%d", resp.StatusCode)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(data, bind)
}
func verifyBind(bind interface{}) error {
bType := reflect.TypeOf(bind)
if bType.Kind() != reflect.Ptr {
return errors.New("bind must be a Ptr")
}
return nil
}
func GetWithBody(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) {
fr, err = do(http.MethodGet, url, header, body, int64(20*MB))
return
}
+16
View File
@@ -0,0 +1,16 @@
package http
import "fmt"
func ExampleGet() {
urlstr := "https://palce.hzbeisheng.com/api/sms"
data, err := Get(urlstr, nil)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(data.Data))
// Output:
}