- Device-Id/Session-Id 仅当请求头未携带时才写入应答头 - 静态文件和 WebSocket 升级应答仅通过 Cookie 维护身份 - Client App 头改为 App-Name/App-Version(破折号命名) - NoLogHeaders → NoLogRequestHeaders,NoLogOutputFields → NoLogResponseFields,新增 NoLogResponseHeaders - 默认排除列表动态构建,用户只需追加自定义字段 - Cookie 头智能过滤:不再整体排除,仅剔除匹配排除列表的 key Co-Authored-By: deepseek-v4-pro[1m] <deepseek-ai@claude-code-best.win>
165 lines
4.1 KiB
Go
165 lines
4.1 KiB
Go
package service
|
||
|
||
import (
|
||
"apigo.cc/go/file"
|
||
"apigo.cc/go/log"
|
||
"mime"
|
||
"net/http"
|
||
"net/url"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// Static 注册静态文件目录
|
||
func (hc *HostContext) Static(path, rootPath string) *HostContext {
|
||
host := hc.host
|
||
if host == "*" {
|
||
host = ""
|
||
}
|
||
hc.ws.StaticByHost(path, rootPath, host)
|
||
return hc
|
||
}
|
||
|
||
// Static 注册静态文件目录 (使用默认 Host "*")
|
||
func Static(path, rootPath string) {
|
||
DefaultServer.Static(path, rootPath)
|
||
}
|
||
|
||
func (ws *WebServer) Static(path, rootPath string) {
|
||
ws.Host("*").Static(path, rootPath)
|
||
}
|
||
|
||
// StaticByHost 为指定域名注册静态文件目录
|
||
func StaticByHost(path, rootPath, host string) {
|
||
DefaultServer.StaticByHost(path, rootPath, host)
|
||
}
|
||
|
||
func (ws *WebServer) StaticByHost(path, rootPath, host string) {
|
||
if !filepath.IsAbs(rootPath) {
|
||
if absPath, err := filepath.Abs(rootPath); err == nil {
|
||
rootPath = absPath
|
||
}
|
||
}
|
||
|
||
ws.staticsByHostLock.Lock()
|
||
defer ws.staticsByHostLock.Unlock()
|
||
|
||
if ws.codeStatics[host] == nil {
|
||
ws.codeStatics[host] = make(map[string]*string)
|
||
}
|
||
ws.codeStatics[host][path] = &rootPath
|
||
ws.rebuildStaticsUnderLock(host)
|
||
}
|
||
|
||
// ReplaceStatics 使用 Copy-on-Write 机制原子地替换指定 host 下的动态静态目录规则
|
||
func ReplaceStatics(host string, config map[string]string) {
|
||
DefaultServer.ReplaceStatics(host, config)
|
||
}
|
||
|
||
func (ws *WebServer) ReplaceStatics(host string, config map[string]string) {
|
||
newStatics := make(map[string]*string, len(config))
|
||
for path, rootPath := range config {
|
||
rp := rootPath
|
||
if !filepath.IsAbs(rp) {
|
||
if absPath, err := filepath.Abs(rp); err == nil {
|
||
rp = absPath
|
||
}
|
||
}
|
||
newStatics[path] = &rp
|
||
}
|
||
|
||
ws.staticsByHostLock.Lock()
|
||
defer ws.staticsByHostLock.Unlock()
|
||
|
||
ws.dynamicStatics[host] = newStatics
|
||
ws.rebuildStaticsUnderLock(host)
|
||
}
|
||
|
||
func (ws *WebServer) getStaticFilePath(requestPath, host string) string {
|
||
requestPath, _ = url.PathUnescape(requestPath)
|
||
ws.staticsByHostLock.RLock()
|
||
defer ws.staticsByHostLock.RUnlock()
|
||
|
||
// 优先匹配指定域名的配置
|
||
if filePath := ws.findMatchedPathSorted(ws.hostStatics[host], requestPath); filePath != "" {
|
||
return filePath
|
||
}
|
||
|
||
// 匹配全局配置
|
||
return ws.findMatchedPathSorted(ws.hostStatics[""], requestPath)
|
||
}
|
||
|
||
func (ws *WebServer) findMatchedPathSorted(config []*staticType, requestPath string) string {
|
||
for _, rule := range config {
|
||
if strings.HasPrefix(requestPath, rule.path) {
|
||
return filepath.Join(*rule.rootPath, requestPath[len(rule.path):])
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func (ws *WebServer) processStatic(requestPath string, request *Request, response *Response, logger *log.Logger) bool {
|
||
filePath := ws.getStaticFilePath(requestPath, request.Host)
|
||
if filePath == "" {
|
||
return false
|
||
}
|
||
|
||
info := file.GetFileInfo(filePath)
|
||
if info == nil {
|
||
return false
|
||
}
|
||
|
||
if info.IsDir {
|
||
// 自动查找索引文件
|
||
indexFiles := ws.Config.IndexFiles
|
||
if len(indexFiles) == 0 {
|
||
indexFiles = []string{"index.html", "index.htm"}
|
||
}
|
||
|
||
for _, indexFile := range indexFiles {
|
||
f := filepath.Join(filePath, indexFile)
|
||
if i := file.GetFileInfo(f); i != nil && !i.IsDir {
|
||
filePath = f
|
||
info = i
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if info.IsDir {
|
||
return false
|
||
}
|
||
|
||
// 静态文件通过 Cookie 维护 ID,不应答 Device-Id / Session-Id 头
|
||
response.Header().Del(ws.usedDeviceIdKey)
|
||
response.Header().Del(ws.usedSessionIdKey)
|
||
|
||
// 检查 304
|
||
if ifModifiedSince := request.Header().Get("If-Modified-Since"); ifModifiedSince != "" {
|
||
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {
|
||
if time.Unix(info.ModTime, 0).Truncate(time.Second).Before(t.Truncate(time.Second)) ||
|
||
time.Unix(info.ModTime, 0).Truncate(time.Second).Equal(t.Truncate(time.Second)) {
|
||
response.WriteHeader(http.StatusNotModified)
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
// 发送文件
|
||
contentType := mime.TypeByExtension(filepath.Ext(filePath))
|
||
if contentType == "" {
|
||
contentType = "application/octet-stream"
|
||
}
|
||
response.Header().Set("Content-Type", contentType)
|
||
response.Header().Set("Last-Modified", time.Unix(info.ModTime, 0).UTC().Format(http.TimeFormat))
|
||
|
||
data, err := file.ReadBytes(filePath)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
_, _ = response.Write(data)
|
||
return true
|
||
}
|