feat: 添加云端函数HTTP请求、SMTP邮件、数据库操作能力
- 添加HTTP客户端工具支持GET/POST请求 - 添加SMTP邮件发送功能 - 添加数据库操作(db.getRecords, db.deleteRecord, db.deleteRecords) - 添加订单管理云端函数(读取、接收、筛选、成功、失败) - 添加测试推送和邮件通知云端函数 - 修复云端函数编辑页面预览代码换行显示 - 云端变量数据模型重构(合并数据类型)
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 10 * time.Second
|
||||
|
||||
type HTTPResponse struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Status string `json:"status"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
JSON map[string]interface{} `json:"json,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPClient struct {
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewHTTPClient(timeout time.Duration) *HTTPClient {
|
||||
if timeout <= 0 {
|
||||
timeout = DefaultTimeout
|
||||
}
|
||||
|
||||
return &HTTPClient{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Get(urlStr string, headers map[string]string) *HTTPResponse {
|
||||
return c.doRequest("GET", urlStr, headers, nil)
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Post(urlStr string, headers map[string]string, body interface{}) *HTTPResponse {
|
||||
return c.doRequest("POST", urlStr, headers, body)
|
||||
}
|
||||
|
||||
func (c *HTTPClient) doRequest(method, urlStr string, headers map[string]string, body interface{}) *HTTPResponse {
|
||||
resp := &HTTPResponse{
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
|
||||
if err := validateURL(urlStr); err != nil {
|
||||
resp.Error = err.Error()
|
||||
return resp
|
||||
}
|
||||
|
||||
var reqBody []byte
|
||||
var err error
|
||||
contentType := "application/json"
|
||||
|
||||
if body != nil {
|
||||
switch v := body.(type) {
|
||||
case string:
|
||||
reqBody = []byte(v)
|
||||
contentType = "text/plain"
|
||||
case map[string]interface{}:
|
||||
reqBody, err = json.Marshal(v)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("failed to marshal body: %v", err)
|
||||
return resp
|
||||
}
|
||||
default:
|
||||
reqBody, err = json.Marshal(v)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("failed to marshal body: %v", err)
|
||||
return resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
if reqBody != nil {
|
||||
req, err = http.NewRequest(method, urlStr, bytes.NewReader(reqBody))
|
||||
} else {
|
||||
req, err = http.NewRequest(method, urlStr, nil)
|
||||
}
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("failed to create request: %v", err)
|
||||
return resp
|
||||
}
|
||||
|
||||
if headers != nil {
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
if body != nil && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
httpResp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("request failed: %v", err)
|
||||
return resp
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
resp.StatusCode = httpResp.StatusCode
|
||||
resp.Status = httpResp.Status
|
||||
|
||||
for k, v := range httpResp.Header {
|
||||
if len(v) > 0 {
|
||||
resp.Headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(httpResp.Body); err != nil {
|
||||
resp.Error = fmt.Sprintf("failed to read response body: %v", err)
|
||||
return resp
|
||||
}
|
||||
resp.Body = buf.String()
|
||||
|
||||
if strings.Contains(httpResp.Header.Get("Content-Type"), "application/json") {
|
||||
var jsonData map[string]interface{}
|
||||
if err := json.Unmarshal(buf.Bytes(), &jsonData); err == nil {
|
||||
resp.JSON = jsonData
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func validateURL(urlStr string) error {
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid URL: %v", err)
|
||||
}
|
||||
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return fmt.Errorf("only http and https protocols are allowed")
|
||||
}
|
||||
|
||||
host := parsedURL.Hostname()
|
||||
if host == "" {
|
||||
return fmt.Errorf("empty host")
|
||||
}
|
||||
|
||||
if isPrivateIP(host) {
|
||||
return fmt.Errorf("access to private IP addresses is not allowed")
|
||||
}
|
||||
|
||||
if isLocalhost(host) {
|
||||
return fmt.Errorf("access to localhost is not allowed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLocalhost(host string) bool {
|
||||
lowerHost := strings.ToLower(host)
|
||||
return lowerHost == "localhost" || lowerHost == "127.0.0.1" || lowerHost == "::1"
|
||||
}
|
||||
|
||||
func isPrivateIP(host string) bool {
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||
return true
|
||||
}
|
||||
|
||||
privateBlocks := []string{
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"169.254.0.0/16",
|
||||
"127.0.0.0/8",
|
||||
"::1/128",
|
||||
"fc00::/7",
|
||||
"fe80::/10",
|
||||
}
|
||||
|
||||
for _, block := range privateBlocks {
|
||||
_, cidr, _ := net.ParseCIDR(block)
|
||||
if cidr != nil && cidr.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user