Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2e6aaaa18 | ||
|
|
8e68da7725 | ||
|
|
7abb84c880 | ||
|
|
a17878f5b2 | ||
|
|
8a8881ac47 | ||
|
|
c567404b7a | ||
|
|
b220b0f48e | ||
|
|
9609c90d75 | ||
|
|
2c3c32af5b | ||
|
|
b4a4b2e9b1 | ||
|
|
c42ff1e1e9 | ||
|
|
9fed1141c2 | ||
|
|
e87f031293 | ||
|
|
c4bac7f43c | ||
|
|
47818f972e | ||
|
|
218a0300c5 | ||
|
|
63f6c4177f | ||
|
|
1830c22a31 | ||
|
|
18611e8a7c | ||
|
|
c45f7adf04 | ||
|
|
1a200918a8 | ||
|
|
b942bb776e | ||
|
|
5cf84efccd | ||
|
|
ebb6ebd10c | ||
|
|
42d0d63cf4 | ||
|
|
96f8f7e925 | ||
|
|
e7e7214d58 | ||
|
|
ade979a725 | ||
|
|
60a8de13e7 | ||
|
|
9fa24bed0a | ||
|
|
87bc1a7e03 | ||
|
|
1a05f56149 | ||
|
|
f88db576e1 | ||
|
|
dc3f26ea1a | ||
|
|
6fc30144f7 | ||
|
|
25b0b98bd4 | ||
|
|
27b5817d5e | ||
|
|
dcb61dfd33 | ||
|
|
bbcfdbbf5e | ||
|
|
b2a1bef08f | ||
|
|
2b18b5c2ca | ||
|
|
6ff030dbd8 | ||
|
|
0ddeef6986 | ||
|
|
976bd3600b | ||
|
|
a64047a934 | ||
|
|
e82f207935 | ||
|
|
61b5316a1f | ||
|
|
82329aa8b0 | ||
|
|
7dabd9c19c | ||
|
|
9437acd18c | ||
|
|
9da7a34edf | ||
|
|
b6a5491dcc | ||
|
|
bcee658567 | ||
|
|
afc8f7b703 | ||
|
|
7a4b89d2fb | ||
|
|
c6299a2fb0 | ||
|
|
8b5d74af9b | ||
|
|
a194360a56 | ||
|
|
b12f7f69ba | ||
|
|
06ec4d3fba | ||
|
|
c209ab912f | ||
|
|
32720d772d | ||
|
|
a89c02fd10 | ||
|
|
37ef86b92f | ||
|
|
4c19c37f49 | ||
|
|
1bb818b5b0 | ||
|
|
825e46458f | ||
|
|
a42737bd28 | ||
|
|
5f76be2cfd | ||
|
|
dbddf8a91a | ||
|
|
6c457f41f6 | ||
|
|
e4b2a650f0 | ||
|
|
913ba95801 | ||
|
|
a9f8e39703 | ||
|
|
534f013f59 | ||
|
|
258380f75c | ||
|
|
8c0e51ec46 | ||
|
|
4c37c7ab84 | ||
|
|
f005da1d5f | ||
|
|
e99acc4694 | ||
|
|
408357dfcf | ||
|
|
0109a27c06 | ||
|
|
e6e2dccc42 |
@@ -50,6 +50,7 @@ function build() {
|
||||
fi
|
||||
|
||||
cp "$ROOT"/configs/api.template.yaml "$DIST"/configs
|
||||
cp "$ROOT"/configs/cluster.template.yaml "$DIST"/configs
|
||||
cp -R "$ROOT"/www "$DIST"/
|
||||
cp -R "$ROOT"/pages "$DIST"/
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ func main() {
|
||||
Product(teaconst.ProductName).
|
||||
Usage(teaconst.ProcessName + " [-v|start|stop|restart|status|quit|test|reload|service|daemon|pprof|accesslog]").
|
||||
Usage(teaconst.ProcessName + " [trackers|goman|conns|gc]").
|
||||
Usage(teaconst.ProcessName + " [ip.drop|ip.reject|ip.remove] IP")
|
||||
Usage(teaconst.ProcessName + " [ip.drop|ip.reject|ip.remove|ip.close] IP")
|
||||
|
||||
app.On("test", func() {
|
||||
err := nodes.NewNode().Test()
|
||||
@@ -241,6 +241,38 @@ func main() {
|
||||
}
|
||||
}
|
||||
})
|
||||
app.On("ip.close", func() {
|
||||
var args = os.Args[2:]
|
||||
if len(args) == 0 {
|
||||
fmt.Println("Usage: edge-node ip.close IP")
|
||||
return
|
||||
}
|
||||
var ip = args[0]
|
||||
if len(net.ParseIP(ip)) == 0 {
|
||||
fmt.Println("IP '" + ip + "' is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("close ip '" + ip)
|
||||
|
||||
var sock = gosock.NewTmpSock(teaconst.ProcessName)
|
||||
reply, err := sock.Send(&gosock.Command{
|
||||
Code: "closeIP",
|
||||
Params: map[string]any{
|
||||
"ip": ip,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("[ERROR]" + err.Error())
|
||||
} else {
|
||||
var errString = maps.NewMap(reply.Params).GetString("error")
|
||||
if len(errString) > 0 {
|
||||
fmt.Println("[ERROR]" + errString)
|
||||
} else {
|
||||
fmt.Println("ok")
|
||||
}
|
||||
}
|
||||
})
|
||||
app.On("ip.remove", func() {
|
||||
var args = os.Args[2:]
|
||||
if len(args) == 0 {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package caches
|
||||
|
||||
const (
|
||||
SuffixAll = "@GOEDGE_" // 通用后缀
|
||||
SuffixWebP = "@GOEDGE_WEBP" // WebP后缀
|
||||
SuffixCompression = "@GOEDGE_" // 压缩后缀 SuffixCompression + Encoding
|
||||
SuffixMethod = "@GOEDGE_" // 请求方法后缀 SuffixMethod + RequestMethod
|
||||
|
||||
11
internal/caches/file_dir.go
Normal file
11
internal/caches/file_dir.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package caches
|
||||
|
||||
import "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
|
||||
|
||||
type FileDir struct {
|
||||
Path string
|
||||
Capacity *shared.SizeCapacity
|
||||
IsFull bool
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package caches
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -59,3 +60,17 @@ func (this *Item) IncreaseHit(week int32) {
|
||||
this.Week = week
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Item) RequestURI() string {
|
||||
var schemeIndex = strings.Index(this.Key, "://")
|
||||
if schemeIndex <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var firstSlashIndex = strings.Index(this.Key[schemeIndex+3:], "/")
|
||||
if firstSlashIndex <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return this.Key[schemeIndex+3+firstSlashIndex:]
|
||||
}
|
||||
|
||||
@@ -81,3 +81,14 @@ func TestItems_Memory2(t *testing.T) {
|
||||
t.Log(w, len(i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestItem_RequestURI(t *testing.T) {
|
||||
for _, u := range []string{
|
||||
"https://goedge.cn/hello/world",
|
||||
"https://goedge.cn:8080/hello/world",
|
||||
"https://goedge.cn/hello/world?v=1&t=123",
|
||||
} {
|
||||
var item = &Item{Key: u}
|
||||
t.Log(u, "=>", item.RequestURI())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ func (this *FileList) CleanPrefix(prefix string) error {
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// TODO 需要优化
|
||||
this.memoryCache.Clean()
|
||||
}()
|
||||
|
||||
@@ -172,6 +173,46 @@ func (this *FileList) CleanPrefix(prefix string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanMatchKey 清理通配符匹配的缓存数据,类似于 https://*.example.com/hello
|
||||
func (this *FileList) CleanMatchKey(key string) error {
|
||||
if len(key) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// TODO 需要优化
|
||||
this.memoryCache.Clean()
|
||||
}()
|
||||
|
||||
for _, db := range this.dbList {
|
||||
err := db.CleanMatchKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanMatchPrefix 清理通配符匹配的缓存数据,类似于 https://*.example.com/prefix/
|
||||
func (this *FileList) CleanMatchPrefix(prefix string) error {
|
||||
if len(prefix) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// TODO 需要优化
|
||||
this.memoryCache.Clean()
|
||||
}()
|
||||
|
||||
for _, db := range this.dbList {
|
||||
err := db.CleanMatchPrefix(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *FileList) Remove(hash string) error {
|
||||
_, err := this.remove(hash)
|
||||
return err
|
||||
|
||||
@@ -13,7 +13,10 @@ import (
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -108,7 +111,7 @@ func (this *FileListDB) Open(dbPath string) error {
|
||||
|
||||
this.writeBatch = dbs.NewBatch(writeDB, 4)
|
||||
this.writeBatch.OnFail(func(err error) {
|
||||
remotelogs.Warn("LIST_FILE_DB", "run batch failed: "+err.Error())
|
||||
remotelogs.Warn("LIST_FILE_DB", "run batch failed: "+err.Error()+" ("+filepath.Base(this.dbPath)+")")
|
||||
})
|
||||
|
||||
goman.New(func() {
|
||||
@@ -177,6 +180,9 @@ func (this *FileListDB) Init() error {
|
||||
}
|
||||
|
||||
this.selectHashListStmt, err = this.readDB.Prepare(`SELECT "id", "hash" FROM "` + this.itemsTableName + `" WHERE id>:id ORDER BY id ASC LIMIT 2000`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
this.deleteByHashSQL = `DELETE FROM "` + this.itemsTableName + `" WHERE "hash"=?`
|
||||
this.deleteByHashStmt, err = this.writeDB.Prepare(this.deleteByHashSQL)
|
||||
@@ -388,6 +394,85 @@ func (this *FileListDB) CleanPrefix(prefix string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (this *FileListDB) CleanMatchKey(key string) error {
|
||||
if !this.isReady {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 忽略 @GOEDGE_
|
||||
if strings.Contains(key, SuffixAll) {
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var host = u.Host
|
||||
hostPart, _, err := net.SplitHostPort(host)
|
||||
if err == nil && len(hostPart) > 0 {
|
||||
host = hostPart
|
||||
}
|
||||
if len(host) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转义
|
||||
var queryKey = strings.ReplaceAll(key, "%", "\\%")
|
||||
queryKey = strings.ReplaceAll(queryKey, "_", "\\_")
|
||||
queryKey = strings.Replace(queryKey, "*", "%", 1)
|
||||
|
||||
// TODO 检查大批量数据下的操作性能
|
||||
var staleLife = 600 // TODO 需要可以设置
|
||||
var unixTime = utils.UnixTime() // 只删除当前的,不删除新的
|
||||
|
||||
_, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+int64(staleLife), host, "*."+host, queryKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+int64(staleLife), host, "*."+host, queryKey+SuffixAll+"%")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *FileListDB) CleanMatchPrefix(prefix string) error {
|
||||
if !this.isReady {
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(prefix)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var host = u.Host
|
||||
hostPart, _, err := net.SplitHostPort(host)
|
||||
if err == nil && len(hostPart) > 0 {
|
||||
host = hostPart
|
||||
}
|
||||
if len(host) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转义
|
||||
var queryPrefix = strings.ReplaceAll(prefix, "%", "\\%")
|
||||
queryPrefix = strings.ReplaceAll(queryPrefix, "_", "\\_")
|
||||
queryPrefix = strings.Replace(queryPrefix, "*", "%", 1)
|
||||
queryPrefix += "%"
|
||||
|
||||
// TODO 检查大批量数据下的操作性能
|
||||
var staleLife = 600 // TODO 需要可以设置
|
||||
var unixTime = utils.UnixTime() // 只删除当前的,不删除新的
|
||||
|
||||
_, err = this.writeDB.Exec(`UPDATE "`+this.itemsTableName+`" SET "expiredAt"=0, "staleAt"=? WHERE "host" GLOB ? AND "host" NOT GLOB ? AND "key" LIKE ? ESCAPE '\'`, unixTime+int64(staleLife), host, "*."+host, queryPrefix)
|
||||
return err
|
||||
}
|
||||
|
||||
func (this *FileListDB) CleanAll() error {
|
||||
if !this.isReady {
|
||||
return nil
|
||||
|
||||
@@ -47,3 +47,41 @@ func TestFileListDB_IncreaseHitAsync(t *testing.T) {
|
||||
// wait transaction
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
func TestFileListDB_CleanMatchKey(t *testing.T) {
|
||||
var db = caches.NewFileListDB()
|
||||
err := db.Open(Tea.Root + "/data/cache-db-large.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = db.Init()
|
||||
|
||||
err = db.CleanMatchKey("https://*.goedge.cn/large-text")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.CleanMatchKey("https://*.goedge.cn:1234/large-text?%2B____")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileListDB_CleanMatchPrefix(t *testing.T) {
|
||||
var db = caches.NewFileListDB()
|
||||
err := db.Open(Tea.Root + "/data/cache-db-large.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = db.Init()
|
||||
|
||||
err = db.CleanMatchPrefix("https://*.goedge.cn/large-text")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.CleanMatchPrefix("https://*.goedge.cn:1234/large-text?%2B____")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ type ListInterface interface {
|
||||
// CleanPrefix 清除某个前缀的缓存
|
||||
CleanPrefix(prefix string) error
|
||||
|
||||
// CleanMatchKey 清除通配符匹配的Key
|
||||
CleanMatchKey(key string) error
|
||||
|
||||
// CleanMatchPrefix 清除通配符匹配的前缀
|
||||
CleanMatchPrefix(prefix string) error
|
||||
|
||||
// Remove 删除内容
|
||||
Remove(hash string) error
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package caches
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/zero"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -146,6 +149,82 @@ func (this *MemoryList) CleanPrefix(prefix string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanMatchKey 清理通配符匹配的缓存数据,类似于 https://*.example.com/hello
|
||||
func (this *MemoryList) CleanMatchKey(key string) error {
|
||||
if strings.Contains(key, SuffixAll) {
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var host = u.Host
|
||||
hostPart, _, err := net.SplitHostPort(host)
|
||||
if err == nil && len(hostPart) > 0 {
|
||||
host = hostPart
|
||||
}
|
||||
|
||||
if len(host) == 0 {
|
||||
return nil
|
||||
}
|
||||
var requestURI = u.RequestURI()
|
||||
|
||||
this.locker.RLock()
|
||||
defer this.locker.RUnlock()
|
||||
|
||||
// TODO 需要优化性能,支持千万级数据低于1s的处理速度
|
||||
for _, itemMap := range this.itemMaps {
|
||||
for _, item := range itemMap {
|
||||
if configutils.MatchDomain(host, item.Host) {
|
||||
var itemRequestURI = item.RequestURI()
|
||||
if itemRequestURI == requestURI || strings.HasPrefix(itemRequestURI, requestURI+SuffixAll) {
|
||||
item.ExpiredAt = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanMatchPrefix 清理通配符匹配的缓存数据,类似于 https://*.example.com/prefix/
|
||||
func (this *MemoryList) CleanMatchPrefix(prefix string) error {
|
||||
u, err := url.Parse(prefix)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var host = u.Host
|
||||
hostPart, _, err := net.SplitHostPort(host)
|
||||
if err == nil && len(hostPart) > 0 {
|
||||
host = hostPart
|
||||
}
|
||||
if len(host) == 0 {
|
||||
return nil
|
||||
}
|
||||
var requestURI = u.RequestURI()
|
||||
var isRootPath = requestURI == "/"
|
||||
|
||||
this.locker.RLock()
|
||||
defer this.locker.RUnlock()
|
||||
|
||||
// TODO 需要优化性能,支持千万级数据低于1s的处理速度
|
||||
for _, itemMap := range this.itemMaps {
|
||||
for _, item := range itemMap {
|
||||
if configutils.MatchDomain(host, item.Host) {
|
||||
var itemRequestURI = item.RequestURI()
|
||||
if isRootPath || strings.HasPrefix(itemRequestURI, requestURI) {
|
||||
item.ExpiredAt = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *MemoryList) Remove(hash string) error {
|
||||
this.locker.Lock()
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ func init() {
|
||||
type Manager struct {
|
||||
// 全局配置
|
||||
MaxDiskCapacity *shared.SizeCapacity
|
||||
DiskDir string
|
||||
MainDiskDir string
|
||||
SubDiskDirs []*serverconfigs.CacheDir
|
||||
MaxMemoryCapacity *shared.SizeCapacity
|
||||
|
||||
policyMap map[int64]*serverconfigs.HTTPCachePolicy // policyId => []*Policy
|
||||
@@ -47,12 +48,10 @@ func (this *Manager) UpdatePolicies(newPolicies []*serverconfigs.HTTPCachePolicy
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
newPolicyIds := []int64{}
|
||||
var newPolicyIds = []int64{}
|
||||
for _, policy := range newPolicies {
|
||||
// 使用节点单独的缓存目录
|
||||
if len(this.DiskDir) > 0 {
|
||||
policy.UpdateDiskDir(this.DiskDir)
|
||||
}
|
||||
policy.UpdateDiskDir(this.MainDiskDir, this.SubDiskDirs)
|
||||
|
||||
newPolicyIds = append(newPolicyIds, policy.Id)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type OpenFileCache struct {
|
||||
poolList *linkedlist.List
|
||||
watcher *fsnotify.Watcher
|
||||
|
||||
locker sync.Mutex
|
||||
locker sync.RWMutex
|
||||
|
||||
maxSize int
|
||||
count int
|
||||
@@ -54,13 +54,18 @@ func NewOpenFileCache(maxSize int) (*OpenFileCache, error) {
|
||||
}
|
||||
|
||||
func (this *OpenFileCache) Get(filename string) *OpenFile {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
this.locker.RLock()
|
||||
pool, ok := this.poolMap[filename]
|
||||
this.locker.RUnlock()
|
||||
if ok {
|
||||
file, consumed := pool.Get()
|
||||
if consumed {
|
||||
this.locker.Lock()
|
||||
this.count--
|
||||
|
||||
// pool如果为空,也不需要从列表中删除,避免put时需要重新创建
|
||||
|
||||
this.locker.Unlock()
|
||||
}
|
||||
return file
|
||||
}
|
||||
@@ -124,6 +129,9 @@ func (this *OpenFileCache) Close(filename string) {
|
||||
|
||||
pool, ok := this.poolMap[filename]
|
||||
if ok {
|
||||
// 设置关闭状态
|
||||
pool.SetClosing()
|
||||
|
||||
delete(this.poolMap, filename)
|
||||
this.poolList.Remove(pool.linkItem)
|
||||
_ = this.watcher.Remove(filename)
|
||||
@@ -146,6 +154,7 @@ func (this *OpenFileCache) CloseAll() {
|
||||
this.poolMap = map[string]*OpenFilePool{}
|
||||
this.poolList.Reset()
|
||||
_ = this.watcher.Close()
|
||||
this.count = 0
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
|
||||
43
internal/caches/open_file_cache_test.go
Normal file
43
internal/caches/open_file_cache_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package caches_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/caches"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewOpenFileCache_Close(t *testing.T) {
|
||||
cache, err := caches.NewOpenFileCache(1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cache.Debug()
|
||||
cache.Put("a.txt", caches.NewOpenFile(nil, nil, nil, 0))
|
||||
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0))
|
||||
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0))
|
||||
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0))
|
||||
cache.Put("c.txt", caches.NewOpenFile(nil, nil, nil, 0))
|
||||
cache.Get("b.txt")
|
||||
cache.Get("d.txt")
|
||||
cache.Close("a.txt")
|
||||
|
||||
time.Sleep(100 * time.Second)
|
||||
}
|
||||
|
||||
func TestNewOpenFileCache_CloseAll(t *testing.T) {
|
||||
cache, err := caches.NewOpenFileCache(1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cache.Debug()
|
||||
cache.Put("a.txt", caches.NewOpenFile(nil, nil, nil, 0))
|
||||
cache.Put("b.txt", caches.NewOpenFile(nil, nil, nil, 0))
|
||||
cache.Put("c.txt", caches.NewOpenFile(nil, nil, nil, 0))
|
||||
cache.Get("b.txt")
|
||||
cache.Get("d.txt")
|
||||
cache.CloseAll()
|
||||
|
||||
time.Sleep(6 * time.Second)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ type OpenFilePool struct {
|
||||
linkItem *linkedlist.Item
|
||||
filename string
|
||||
version int64
|
||||
isClosed bool
|
||||
}
|
||||
|
||||
func NewOpenFilePool(filename string) *OpenFilePool {
|
||||
@@ -29,26 +30,43 @@ func (this *OpenFilePool) Filename() string {
|
||||
}
|
||||
|
||||
func (this *OpenFilePool) Get() (*OpenFile, bool) {
|
||||
// 如果已经关闭,直接返回
|
||||
if this.isClosed {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
select {
|
||||
case file := <-this.c:
|
||||
err := file.SeekStart()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, true
|
||||
}
|
||||
file.version = this.version
|
||||
if file != nil {
|
||||
err := file.SeekStart()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, true
|
||||
}
|
||||
file.version = this.version
|
||||
|
||||
return file, true
|
||||
return file, true
|
||||
}
|
||||
return nil, false
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (this *OpenFilePool) Put(file *OpenFile) bool {
|
||||
// 如果已关闭,则不接受新的文件
|
||||
if this.isClosed {
|
||||
_ = file.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件版本号
|
||||
if this.version > 0 && file.version > 0 && file.version != this.version {
|
||||
_ = file.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
// 加入Pool
|
||||
select {
|
||||
case this.c <- file:
|
||||
return true
|
||||
@@ -63,14 +81,18 @@ func (this *OpenFilePool) Len() int {
|
||||
return len(this.c)
|
||||
}
|
||||
|
||||
func (this *OpenFilePool) SetClosing() {
|
||||
this.isClosed = true
|
||||
}
|
||||
|
||||
func (this *OpenFilePool) Close() {
|
||||
Loop:
|
||||
this.isClosed = true
|
||||
for {
|
||||
select {
|
||||
case file := <-this.c:
|
||||
_ = file.Close()
|
||||
default:
|
||||
break Loop
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ package caches_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/caches"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -15,3 +17,30 @@ func TestOpenFilePool_Get(t *testing.T) {
|
||||
t.Log(pool.Get())
|
||||
t.Log(pool.Get())
|
||||
}
|
||||
|
||||
func TestOpenFilePool_Close(t *testing.T) {
|
||||
var pool = caches.NewOpenFilePool("a")
|
||||
pool.Put(caches.NewOpenFile(nil, nil, nil, 0))
|
||||
pool.Put(caches.NewOpenFile(nil, nil, nil, 0))
|
||||
pool.Close()
|
||||
}
|
||||
|
||||
func TestOpenFilePool_Concurrent(t *testing.T) {
|
||||
var pool = caches.NewOpenFilePool("a")
|
||||
var concurrent = 1000
|
||||
var wg = &sync.WaitGroup{}
|
||||
wg.Add(concurrent)
|
||||
for i := 0; i < concurrent; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if rands.Int(0, 1) == 1 {
|
||||
pool.Put(caches.NewOpenFile(nil, nil, nil, 0))
|
||||
}
|
||||
if rands.Int(0, 1) == 0 {
|
||||
pool.Get()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -3,38 +3,88 @@
|
||||
package caches
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// PartialRanges 内容分区范围定义
|
||||
type PartialRanges struct {
|
||||
Ranges [][2]int64 `json:"ranges"`
|
||||
Version int `json:"version"` // 版本号
|
||||
Ranges [][2]int64 `json:"ranges"` // 范围
|
||||
BodySize int64 `json:"bodySize"` // 总长度
|
||||
}
|
||||
|
||||
// NewPartialRanges 获取新对象
|
||||
func NewPartialRanges() *PartialRanges {
|
||||
return &PartialRanges{Ranges: [][2]int64{}}
|
||||
func NewPartialRanges(expiresAt int64) *PartialRanges {
|
||||
return &PartialRanges{
|
||||
Ranges: [][2]int64{},
|
||||
Version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPartialRangesFromData 从数据中解析范围
|
||||
func NewPartialRangesFromData(data []byte) (*PartialRanges, error) {
|
||||
var rs = NewPartialRanges(0)
|
||||
for {
|
||||
var index = bytes.IndexRune(data, '\n')
|
||||
if index < 0 {
|
||||
break
|
||||
}
|
||||
var line = data[:index]
|
||||
var colonIndex = bytes.IndexRune(line, ':')
|
||||
if colonIndex > 0 {
|
||||
switch string(line[:colonIndex]) {
|
||||
case "v": // 版本号
|
||||
rs.Version = types.Int(line[colonIndex+1:])
|
||||
case "b": // 总长度
|
||||
rs.BodySize = types.Int64(line[colonIndex+1:])
|
||||
case "r": // 范围信息
|
||||
var commaIndex = bytes.IndexRune(line, ',')
|
||||
if commaIndex > 0 {
|
||||
rs.Ranges = append(rs.Ranges, [2]int64{types.Int64(line[colonIndex+1 : commaIndex]), types.Int64(line[commaIndex+1:])})
|
||||
}
|
||||
}
|
||||
}
|
||||
data = data[index+1:]
|
||||
if len(data) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// NewPartialRangesFromJSON 从JSON中解析范围
|
||||
func NewPartialRangesFromJSON(data []byte) (*PartialRanges, error) {
|
||||
var rs = NewPartialRanges()
|
||||
var rs = NewPartialRanges(0)
|
||||
err := json.Unmarshal(data, &rs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs.Version = 0
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// NewPartialRangesFromFile 从文件中加载范围信息
|
||||
func NewPartialRangesFromFile(path string) (*PartialRanges, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewPartialRangesFromJSON(data)
|
||||
if len(data) == 0 {
|
||||
return NewPartialRanges(0), nil
|
||||
}
|
||||
|
||||
// 兼容老的JSON格式
|
||||
if data[0] == '{' {
|
||||
return NewPartialRangesFromJSON(data)
|
||||
}
|
||||
|
||||
// 新的格式
|
||||
return NewPartialRangesFromData(data)
|
||||
}
|
||||
|
||||
// Add 添加新范围
|
||||
@@ -105,29 +155,27 @@ func (this *PartialRanges) Nearest(begin int64, end int64) (r [2]int64, ok bool)
|
||||
return
|
||||
}
|
||||
|
||||
// AsJSON 转换为JSON
|
||||
func (this *PartialRanges) AsJSON() ([]byte, error) {
|
||||
return json.Marshal(this)
|
||||
// 转换为字符串
|
||||
func (this *PartialRanges) String() string {
|
||||
var s = "v:" + strconv.Itoa(this.Version) + "\n" + // version
|
||||
"b:" + this.formatInt64(this.BodySize) + "\n" // bodySize
|
||||
for _, r := range this.Ranges {
|
||||
s += "r:" + this.formatInt64(r[0]) + "," + this.formatInt64(r[1]) + "\n" // range
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Bytes 将内容转换为字节
|
||||
func (this *PartialRanges) Bytes() []byte {
|
||||
return []byte(this.String())
|
||||
}
|
||||
|
||||
// WriteToFile 写入到文件中
|
||||
func (this *PartialRanges) WriteToFile(path string) error {
|
||||
data, err := this.AsJSON()
|
||||
if err != nil {
|
||||
return errors.New("convert to json failed: " + err.Error())
|
||||
}
|
||||
return os.WriteFile(path, data, 0666)
|
||||
}
|
||||
|
||||
// ReadFromFile 从文件中读取
|
||||
func (this *PartialRanges) ReadFromFile(path string) (*PartialRanges, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewPartialRangesFromJSON(data)
|
||||
return os.WriteFile(path, this.Bytes(), 0666)
|
||||
}
|
||||
|
||||
// Max 获取最大位置
|
||||
func (this *PartialRanges) Max() int64 {
|
||||
if len(this.Ranges) > 0 {
|
||||
return this.Ranges[len(this.Ranges)-1][1]
|
||||
@@ -135,6 +183,11 @@ func (this *PartialRanges) Max() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Reset 重置范围信息
|
||||
func (this *PartialRanges) Reset() {
|
||||
this.Ranges = [][2]int64{}
|
||||
}
|
||||
|
||||
func (this *PartialRanges) merge(index int) {
|
||||
// forward
|
||||
var lastIndex = index
|
||||
@@ -187,3 +240,7 @@ func (this *PartialRanges) max(n1 int64, n2 int64) int64 {
|
||||
}
|
||||
return n2
|
||||
}
|
||||
|
||||
func (this *PartialRanges) formatInt64(i int64) string {
|
||||
return strconv.FormatInt(i, 10)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
package caches_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/caches"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewPartialRanges(t *testing.T) {
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.Add(1, 100)
|
||||
r.Add(50, 300)
|
||||
|
||||
@@ -28,7 +30,7 @@ func TestNewPartialRanges(t *testing.T) {
|
||||
func TestNewPartialRanges1(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.Add(1, 100)
|
||||
r.Add(1, 101)
|
||||
r.Add(1, 102)
|
||||
@@ -47,7 +49,7 @@ func TestNewPartialRanges1(t *testing.T) {
|
||||
|
||||
func TestNewPartialRanges2(t *testing.T) {
|
||||
// low -> high
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.Add(1, 100)
|
||||
r.Add(1, 101)
|
||||
r.Add(1, 102)
|
||||
@@ -63,7 +65,7 @@ func TestNewPartialRanges2(t *testing.T) {
|
||||
|
||||
func TestNewPartialRanges3(t *testing.T) {
|
||||
// high -> low
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.Add(301, 302)
|
||||
r.Add(303, 304)
|
||||
r.Add(200, 300)
|
||||
@@ -75,7 +77,7 @@ func TestNewPartialRanges3(t *testing.T) {
|
||||
|
||||
func TestNewPartialRanges4(t *testing.T) {
|
||||
// nearby
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.Add(301, 302)
|
||||
r.Add(303, 304)
|
||||
r.Add(305, 306)
|
||||
@@ -90,7 +92,7 @@ func TestNewPartialRanges4(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewPartialRanges5(t *testing.T) {
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
for j := 0; j < 1000; j++ {
|
||||
r.Add(int64(j), int64(j+100))
|
||||
}
|
||||
@@ -100,7 +102,7 @@ func TestNewPartialRanges5(t *testing.T) {
|
||||
func TestNewPartialRanges_Nearest(t *testing.T) {
|
||||
{
|
||||
// nearby
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.Add(301, 400)
|
||||
r.Add(401, 500)
|
||||
r.Add(501, 600)
|
||||
@@ -112,7 +114,7 @@ func TestNewPartialRanges_Nearest(t *testing.T) {
|
||||
|
||||
{
|
||||
// nearby
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.Add(301, 400)
|
||||
r.Add(450, 500)
|
||||
r.Add(550, 600)
|
||||
@@ -131,45 +133,100 @@ func TestNewPartialRanges_Large_Range(t *testing.T) {
|
||||
var largeSize int64 = 10000000000000
|
||||
t.Log(largeSize/1024/1024/1024, "G")
|
||||
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.Add(1, largeSize)
|
||||
jsonData, err := r.AsJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(jsonData))
|
||||
var s = r.String()
|
||||
t.Log(s)
|
||||
|
||||
r2, err := caches.NewPartialRangesFromJSON(jsonData)
|
||||
r2, err := caches.NewPartialRangesFromData([]byte(s))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
a.IsTrue(largeSize == r2.Ranges[0][1])
|
||||
logs.PrintAsJSON(r, t)
|
||||
}
|
||||
|
||||
func TestNewPartialRanges_AsJSON(t *testing.T) {
|
||||
var r = caches.NewPartialRanges()
|
||||
for j := 0; j < 1000; j++ {
|
||||
r.Add(int64(j), int64(j+100))
|
||||
func TestPartialRanges_Encode_JSON(t *testing.T) {
|
||||
var r = caches.NewPartialRanges(0)
|
||||
for i := 0; i < 10; i++ {
|
||||
r.Ranges = append(r.Ranges, [2]int64{int64(i * 100), int64(i*100 + 100)})
|
||||
}
|
||||
data, err := r.AsJSON()
|
||||
var before = time.Now()
|
||||
data, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(data))
|
||||
t.Log(time.Since(before).Seconds()*1000, "ms")
|
||||
t.Log(len(data))
|
||||
}
|
||||
|
||||
r2, err := caches.NewPartialRangesFromJSON(data)
|
||||
func TestPartialRanges_Encode_String(t *testing.T) {
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.BodySize = 1024
|
||||
for i := 0; i < 10; i++ {
|
||||
r.Ranges = append(r.Ranges, [2]int64{int64(i * 100), int64(i*100 + 100)})
|
||||
}
|
||||
var before = time.Now()
|
||||
var data = r.String()
|
||||
t.Log(time.Since(before).Seconds()*1000, "ms")
|
||||
t.Log(len(data))
|
||||
|
||||
r2, err := caches.NewPartialRangesFromData([]byte(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(r2.Ranges)
|
||||
logs.PrintAsJSON(r2, t)
|
||||
}
|
||||
|
||||
func TestPartialRanges_Version(t *testing.T) {
|
||||
{
|
||||
ranges, err := caches.NewPartialRangesFromData([]byte(`e:1668928495
|
||||
r:0,1048576
|
||||
r:1140260864,1140295164`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("version:", ranges.Version)
|
||||
}
|
||||
{
|
||||
ranges, err := caches.NewPartialRangesFromData([]byte(`e:1668928495
|
||||
r:0,1048576
|
||||
r:1140260864,1140295164
|
||||
v:0
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("version:", ranges.Version)
|
||||
}
|
||||
{
|
||||
ranges, err := caches.NewPartialRangesFromJSON([]byte(`{}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("version:", ranges.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewPartialRanges(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var r = caches.NewPartialRanges()
|
||||
var r = caches.NewPartialRanges(0)
|
||||
for j := 0; j < 1000; j++ {
|
||||
r.Add(int64(j), int64(j+100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPartialRanges_String(b *testing.B) {
|
||||
var r = caches.NewPartialRanges(0)
|
||||
r.BodySize = 1024
|
||||
for i := 0; i < 10; i++ {
|
||||
r.Ranges = append(r.Ranges, [2]int64{int64(i * 100), int64(i*100 + 100)})
|
||||
}
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = r.String()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (this *FileReader) InitAutoDiscard(autoDiscard bool) error {
|
||||
this.header = this.openFile.header
|
||||
}
|
||||
|
||||
isOk := false
|
||||
var isOk = false
|
||||
|
||||
if autoDiscard {
|
||||
defer func() {
|
||||
@@ -67,17 +67,17 @@ func (this *FileReader) InitAutoDiscard(autoDiscard bool) error {
|
||||
|
||||
this.expiresAt = int64(binary.BigEndian.Uint32(buf[:SizeExpiresAt]))
|
||||
|
||||
status := types.Int(string(buf[OffsetStatus : OffsetStatus+SizeStatus]))
|
||||
var status = types.Int(string(buf[OffsetStatus : OffsetStatus+SizeStatus]))
|
||||
if status < 100 || status > 999 {
|
||||
return errors.New("invalid status")
|
||||
}
|
||||
this.status = status
|
||||
|
||||
// URL
|
||||
urlLength := binary.BigEndian.Uint32(buf[OffsetURLLength : OffsetURLLength+SizeURLLength])
|
||||
var urlLength = binary.BigEndian.Uint32(buf[OffsetURLLength : OffsetURLLength+SizeURLLength])
|
||||
|
||||
// header
|
||||
headerSize := int(binary.BigEndian.Uint32(buf[OffsetHeaderLength : OffsetHeaderLength+SizeHeaderLength]))
|
||||
var headerSize = int(binary.BigEndian.Uint32(buf[OffsetHeaderLength : OffsetHeaderLength+SizeHeaderLength]))
|
||||
if headerSize == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func (this *FileReader) InitAutoDiscard(autoDiscard bool) error {
|
||||
|
||||
// body
|
||||
this.bodyOffset = this.headerOffset + int64(headerSize)
|
||||
bodySize := int(binary.BigEndian.Uint64(buf[OffsetBodyLength : OffsetBodyLength+SizeBodyLength]))
|
||||
var bodySize = int(binary.BigEndian.Uint64(buf[OffsetBodyLength : OffsetBodyLength+SizeBodyLength]))
|
||||
if bodySize == 0 {
|
||||
isOk = true
|
||||
return nil
|
||||
@@ -158,7 +158,7 @@ func (this *FileReader) ReadHeader(buf []byte, callback ReaderFunc) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
isOk := false
|
||||
var isOk = false
|
||||
|
||||
defer func() {
|
||||
if !isOk {
|
||||
@@ -171,7 +171,7 @@ func (this *FileReader) ReadHeader(buf []byte, callback ReaderFunc) error {
|
||||
return err
|
||||
}
|
||||
|
||||
headerSize := this.headerSize
|
||||
var headerSize = this.headerSize
|
||||
|
||||
for {
|
||||
n, err := this.fp.Read(buf)
|
||||
@@ -215,7 +215,11 @@ func (this *FileReader) ReadHeader(buf []byte, callback ReaderFunc) error {
|
||||
}
|
||||
|
||||
func (this *FileReader) ReadBody(buf []byte, callback ReaderFunc) error {
|
||||
isOk := false
|
||||
if this.bodySize == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var isOk = false
|
||||
|
||||
defer func() {
|
||||
if !isOk {
|
||||
@@ -257,15 +261,22 @@ func (this *FileReader) ReadBody(buf []byte, callback ReaderFunc) error {
|
||||
}
|
||||
|
||||
func (this *FileReader) Read(buf []byte) (n int, err error) {
|
||||
if this.bodySize == 0 {
|
||||
n = 0
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
|
||||
n, err = this.fp.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
_ = this.discard()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (this *FileReader) ReadBodyRange(buf []byte, start int64, end int64, callback ReaderFunc) error {
|
||||
isOk := false
|
||||
var isOk = false
|
||||
|
||||
defer func() {
|
||||
if !isOk {
|
||||
@@ -273,7 +284,7 @@ func (this *FileReader) ReadBodyRange(buf []byte, start int64, end int64, callba
|
||||
}
|
||||
}()
|
||||
|
||||
offset := start
|
||||
var offset = start
|
||||
if start < 0 {
|
||||
offset = this.bodyOffset + this.bodySize + end
|
||||
end = this.bodyOffset + this.bodySize - 1
|
||||
@@ -296,7 +307,7 @@ func (this *FileReader) ReadBodyRange(buf []byte, start int64, end int64, callba
|
||||
for {
|
||||
n, err := this.fp.Read(buf)
|
||||
if n > 0 {
|
||||
n2 := int(end-offset) + 1
|
||||
var n2 = int(end-offset) + 1
|
||||
if n2 <= n {
|
||||
_, e := callback(n2)
|
||||
if e != nil {
|
||||
@@ -344,12 +355,12 @@ func (this *FileReader) FP() *os.File {
|
||||
}
|
||||
|
||||
func (this *FileReader) Close() error {
|
||||
if this.openFileCache != nil {
|
||||
if this.isClosed {
|
||||
return nil
|
||||
}
|
||||
this.isClosed = true
|
||||
if this.isClosed {
|
||||
return nil
|
||||
}
|
||||
this.isClosed = true
|
||||
|
||||
if this.openFileCache != nil {
|
||||
if this.openFile != nil {
|
||||
this.openFileCache.Put(this.fp.Name(), this.openFile)
|
||||
} else {
|
||||
@@ -359,6 +370,7 @@ func (this *FileReader) Close() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return this.fp.Close()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestFileReader(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, path := storage.keyPath("my-key")
|
||||
_, path, _ := storage.keyPath("my-key")
|
||||
|
||||
fp, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -105,7 +105,7 @@ func TestFileReader_Range(t *testing.T) {
|
||||
}
|
||||
_ = writer.Close()**/
|
||||
|
||||
_, path := storage.keyPath("my-number")
|
||||
_, path, _ := storage.keyPath("my-number")
|
||||
|
||||
fp, err := os.Open(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -117,13 +117,10 @@ func (this *PartialFileReader) ContainsRange(r rangeutils.Range) (r2 rangeutils.
|
||||
r2, ok = this.ranges.Nearest(r.Start(), r.End())
|
||||
if ok && this.bodySize > 0 {
|
||||
// 考虑可配置
|
||||
var span int64 = 512 * 1024
|
||||
if this.bodySize > 1<<30 {
|
||||
span = 1 << 20
|
||||
}
|
||||
const minSpan = 128 << 10
|
||||
|
||||
// 这里限制返回的最小缓存,防止因为返回的内容过小而导致请求过多
|
||||
if r2.Length() < r.Length() && r2.Length() < span {
|
||||
if r2.Length() < r.Length() && r2.Length() < minSpan {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
@@ -138,6 +135,10 @@ func (this *PartialFileReader) MaxLength() int64 {
|
||||
return this.ranges.Max() + 1
|
||||
}
|
||||
|
||||
func (this *PartialFileReader) Ranges() *PartialRanges {
|
||||
return this.ranges
|
||||
}
|
||||
|
||||
func (this *PartialFileReader) discard() error {
|
||||
_ = os.Remove(this.rangePath)
|
||||
return this.FileReader.discard()
|
||||
|
||||
@@ -49,8 +49,7 @@ const (
|
||||
SizeBodyLength = 8
|
||||
OffsetBodyLength = OffsetHeaderLength + SizeHeaderLength
|
||||
|
||||
SizeMeta = SizeExpiresAt + SizeStatus + SizeURLLength + SizeHeaderLength + SizeBodyLength
|
||||
OffsetKey = SizeMeta
|
||||
SizeMeta = SizeExpiresAt + SizeStatus + SizeURLLength + SizeHeaderLength + SizeBodyLength
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -93,7 +92,10 @@ type FileStorage struct {
|
||||
|
||||
openFileCache *OpenFileCache
|
||||
|
||||
diskIsFull bool
|
||||
mainDir string
|
||||
mainDiskIsFull bool
|
||||
|
||||
subDirs []*FileDir
|
||||
}
|
||||
|
||||
func NewFileStorage(policy *serverconfigs.HTTPCachePolicy) *FileStorage {
|
||||
@@ -157,6 +159,16 @@ func (this *FileStorage) UpdatePolicy(newPolicy *serverconfigs.HTTPCachePolicy)
|
||||
return
|
||||
}
|
||||
|
||||
var subDirs = []*FileDir{}
|
||||
for _, subDir := range newOptions.SubDirs {
|
||||
subDirs = append(subDirs, &FileDir{
|
||||
Path: subDir.Path,
|
||||
Capacity: subDir.Capacity,
|
||||
IsFull: false,
|
||||
})
|
||||
}
|
||||
this.checkDiskSpace()
|
||||
|
||||
err = newOptions.Init()
|
||||
if err != nil {
|
||||
remotelogs.Error("CACHE", "update policy '"+types.String(this.policy.Id)+"' failed: init options failed: "+err.Error())
|
||||
@@ -219,6 +231,19 @@ func (this *FileStorage) Init() error {
|
||||
this.options.Dir = filepath.Clean(this.options.Dir)
|
||||
var dir = this.options.Dir
|
||||
|
||||
var subDirs = []*FileDir{}
|
||||
for _, subDir := range this.options.SubDirs {
|
||||
subDirs = append(subDirs, &FileDir{
|
||||
Path: subDir.Path,
|
||||
Capacity: subDir.Capacity,
|
||||
IsFull: false,
|
||||
})
|
||||
}
|
||||
this.subDirs = subDirs
|
||||
if len(subDirs) > 0 {
|
||||
this.checkDiskSpace()
|
||||
}
|
||||
|
||||
if len(dir) == 0 {
|
||||
return errors.New("[CACHE]cache storage dir can not be empty")
|
||||
}
|
||||
@@ -321,7 +346,7 @@ func (this *FileStorage) openReader(key string, allowMemory bool, useStale bool,
|
||||
}
|
||||
}
|
||||
|
||||
hash, path := this.keyPath(key)
|
||||
hash, path, _ := this.keyPath(key)
|
||||
|
||||
// 检查文件记录是否已过期
|
||||
if !useStale {
|
||||
@@ -389,26 +414,21 @@ func (this *FileStorage) openReader(key string, allowMemory bool, useStale bool,
|
||||
}
|
||||
|
||||
// OpenWriter 打开缓存文件等待写入
|
||||
func (this *FileStorage) OpenWriter(key string, expiresAt int64, status int, size int64, maxSize int64, isPartial bool) (Writer, error) {
|
||||
return this.openWriter(key, expiresAt, status, size, maxSize, isPartial, false)
|
||||
func (this *FileStorage) OpenWriter(key string, expiresAt int64, status int, headerSize int, bodySize int64, maxSize int64, isPartial bool) (Writer, error) {
|
||||
return this.openWriter(key, expiresAt, status, headerSize, bodySize, maxSize, isPartial, false)
|
||||
}
|
||||
|
||||
// OpenFlushWriter 打开从其他媒介直接刷入的写入器
|
||||
func (this *FileStorage) OpenFlushWriter(key string, expiresAt int64, status int) (Writer, error) {
|
||||
return this.openWriter(key, expiresAt, status, -1, -1, false, true)
|
||||
func (this *FileStorage) OpenFlushWriter(key string, expiresAt int64, status int, headerSize int, bodySize int64) (Writer, error) {
|
||||
return this.openWriter(key, expiresAt, status, headerSize, bodySize, -1, false, true)
|
||||
}
|
||||
|
||||
func (this *FileStorage) openWriter(key string, expiredAt int64, status int, size int64, maxSize int64, isPartial bool, isFlushing bool) (Writer, error) {
|
||||
func (this *FileStorage) openWriter(key string, expiredAt int64, status int, headerSize int, bodySize int64, maxSize int64, isPartial bool, isFlushing bool) (Writer, error) {
|
||||
// 是否正在退出
|
||||
if teaconst.IsQuiting {
|
||||
return nil, ErrWritingUnavailable
|
||||
}
|
||||
|
||||
// 当前磁盘可用容量是否严重不足
|
||||
if this.diskIsFull {
|
||||
return nil, NewCapacityError("the disk is full")
|
||||
}
|
||||
|
||||
// 是否已忽略
|
||||
if this.ignoreKeys.Has(key) {
|
||||
return nil, ErrEntityTooLarge
|
||||
@@ -421,8 +441,8 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, siz
|
||||
maxMemorySize = maxSize
|
||||
}
|
||||
var memoryStorage = this.memoryStorage
|
||||
if !isFlushing && !isPartial && memoryStorage != nil && ((size > 0 && size < maxMemorySize) || size < 0) {
|
||||
writer, err := memoryStorage.OpenWriter(key, expiredAt, status, size, maxMemorySize, false)
|
||||
if !isFlushing && !isPartial && memoryStorage != nil && ((bodySize > 0 && bodySize < maxMemorySize) || bodySize < 0) {
|
||||
writer, err := memoryStorage.OpenWriter(key, expiredAt, status, headerSize, bodySize, maxMemorySize, false)
|
||||
if err == nil {
|
||||
return writer, nil
|
||||
}
|
||||
@@ -475,17 +495,9 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, siz
|
||||
|
||||
var hash = stringutil.Md5(key)
|
||||
|
||||
// TODO 可以只stat一次
|
||||
var dir = this.options.Dir + "/p" + strconv.FormatInt(this.policy.Id, 10) + "/" + hash[:2] + "/" + hash[2:4]
|
||||
_, err = os.Stat(dir)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
err = os.MkdirAll(dir, 0777)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir, diskIsFull := this.subDir(hash)
|
||||
if diskIsFull {
|
||||
return nil, NewCapacityError("the disk is full")
|
||||
}
|
||||
|
||||
// 检查缓存是否已经生成
|
||||
@@ -532,19 +544,38 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, siz
|
||||
// 从已经存储的内容中读取信息
|
||||
var isNewCreated = true
|
||||
var partialBodyOffset int64
|
||||
var partialRanges *PartialRanges
|
||||
if isPartial {
|
||||
readerFp, err := os.OpenFile(tmpPath, os.O_RDONLY, 0444)
|
||||
if err == nil {
|
||||
var partialReader = NewPartialFileReader(readerFp)
|
||||
err = partialReader.Init()
|
||||
_ = partialReader.Close()
|
||||
if err == nil && partialReader.bodyOffset > 0 {
|
||||
isNewCreated = false
|
||||
partialBodyOffset = partialReader.bodyOffset
|
||||
} else {
|
||||
_ = this.removeCacheFile(tmpPath)
|
||||
// 数据库中是否存在
|
||||
existsCacheItem, _ := this.list.Exist(hash)
|
||||
if existsCacheItem {
|
||||
readerFp, err := os.OpenFile(tmpPath, os.O_RDONLY, 0444)
|
||||
if err == nil {
|
||||
var partialReader = NewPartialFileReader(readerFp)
|
||||
err = partialReader.Init()
|
||||
_ = partialReader.Close()
|
||||
if err == nil && partialReader.bodyOffset > 0 {
|
||||
partialRanges = partialReader.Ranges()
|
||||
if bodySize > 0 && partialRanges != nil && partialRanges.BodySize > 0 && bodySize != partialRanges.BodySize {
|
||||
_ = this.removeCacheFile(tmpPath)
|
||||
} else {
|
||||
isNewCreated = false
|
||||
partialBodyOffset = partialReader.bodyOffset
|
||||
}
|
||||
} else {
|
||||
_ = this.removeCacheFile(tmpPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if isNewCreated {
|
||||
err = this.list.Remove(hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if partialRanges == nil {
|
||||
partialRanges = NewPartialRanges(expiredAt)
|
||||
}
|
||||
}
|
||||
|
||||
var flags = os.O_CREATE | os.O_WRONLY
|
||||
@@ -554,7 +585,16 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, siz
|
||||
var before = time.Now()
|
||||
writer, err := os.OpenFile(tmpPath, flags, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// TODO 检查在各个系统中的稳定性
|
||||
if os.IsNotExist(err) {
|
||||
_ = os.MkdirAll(dir, 0777)
|
||||
|
||||
// open file again
|
||||
writer, err = os.OpenFile(tmpPath, flags, 0666)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !isFlushing {
|
||||
if time.Since(before) >= maxOpenFilesSlowCost {
|
||||
@@ -586,9 +626,12 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, siz
|
||||
return nil, ErrFileIsWriting
|
||||
}
|
||||
|
||||
var metaBodySize int64 = -1
|
||||
var metaHeaderSize = -1
|
||||
if isNewCreated {
|
||||
// 写入过期时间
|
||||
var metaBytes = make([]byte, SizeMeta+len(key))
|
||||
// 写入meta
|
||||
// 从v0.5.8开始不再在meta中写入Key
|
||||
var metaBytes = make([]byte, SizeMeta)
|
||||
binary.BigEndian.PutUint32(metaBytes[OffsetExpiresAt:], uint32(expiredAt))
|
||||
|
||||
// 写入状态码
|
||||
@@ -597,17 +640,17 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, siz
|
||||
}
|
||||
copy(metaBytes[OffsetStatus:], strconv.Itoa(status))
|
||||
|
||||
// 写入URL长度
|
||||
binary.BigEndian.PutUint32(metaBytes[OffsetURLLength:], uint32(len(key)))
|
||||
|
||||
// 写入Header Length
|
||||
binary.BigEndian.PutUint32(metaBytes[OffsetHeaderLength:], uint32(0))
|
||||
if headerSize > 0 {
|
||||
binary.BigEndian.PutUint32(metaBytes[OffsetHeaderLength:], uint32(headerSize))
|
||||
metaHeaderSize = headerSize
|
||||
}
|
||||
|
||||
// 写入Body Length
|
||||
binary.BigEndian.PutUint64(metaBytes[OffsetBodyLength:], uint64(0))
|
||||
|
||||
// 写入URL
|
||||
copy(metaBytes[OffsetKey:], key)
|
||||
if bodySize > 0 {
|
||||
binary.BigEndian.PutUint64(metaBytes[OffsetBodyLength:], uint64(bodySize))
|
||||
metaBodySize = bodySize
|
||||
}
|
||||
|
||||
_, err = writer.Write(metaBytes)
|
||||
if err != nil {
|
||||
@@ -617,12 +660,7 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, siz
|
||||
|
||||
isOk = true
|
||||
if isPartial {
|
||||
ranges, err := NewPartialRangesFromFile(cachePathName + "@ranges.cache")
|
||||
if err != nil {
|
||||
ranges = NewPartialRanges()
|
||||
}
|
||||
|
||||
return NewPartialFileWriter(writer, key, expiredAt, isNewCreated, isPartial, partialBodyOffset, ranges, func() {
|
||||
return NewPartialFileWriter(writer, key, expiredAt, metaHeaderSize, metaBodySize, isNewCreated, isPartial, partialBodyOffset, partialRanges, func() {
|
||||
sharedWritingFileKeyLocker.Lock()
|
||||
delete(sharedWritingFileKeyMap, key)
|
||||
if len(sharedWritingFileKeyMap) == 0 {
|
||||
@@ -631,7 +669,7 @@ func (this *FileStorage) openWriter(key string, expiredAt int64, status int, siz
|
||||
sharedWritingFileKeyLocker.Unlock()
|
||||
}), nil
|
||||
} else {
|
||||
return NewFileWriter(this, writer, key, expiredAt, -1, func() {
|
||||
return NewFileWriter(this, writer, key, expiredAt, metaHeaderSize, metaBodySize, -1, func() {
|
||||
sharedWritingFileKeyLocker.Lock()
|
||||
delete(sharedWritingFileKeyMap, key)
|
||||
if len(sharedWritingFileKeyMap) == 0 {
|
||||
@@ -658,7 +696,7 @@ func (this *FileStorage) AddToList(item *Item) {
|
||||
}
|
||||
|
||||
item.MetaSize = SizeMeta + 128
|
||||
hash := stringutil.Md5(item.Key)
|
||||
var hash = stringutil.Md5(item.Key)
|
||||
err := this.list.Add(hash, item)
|
||||
if err != nil && !strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
remotelogs.Error("CACHE", "add to list failed: "+err.Error())
|
||||
@@ -672,15 +710,12 @@ func (this *FileStorage) Delete(key string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
// 先尝试内存缓存
|
||||
this.runMemoryStorageSafety(func(memoryStorage *MemoryStorage) {
|
||||
_ = memoryStorage.Delete(key)
|
||||
})
|
||||
|
||||
hash, path := this.keyPath(key)
|
||||
hash, path, _ := this.keyPath(key)
|
||||
err := this.list.Remove(hash)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -695,9 +730,6 @@ func (this *FileStorage) Delete(key string) error {
|
||||
|
||||
// Stat 统计
|
||||
func (this *FileStorage) Stat() (*Stat, error) {
|
||||
this.locker.RLock()
|
||||
defer this.locker.RUnlock()
|
||||
|
||||
return this.list.Stat(func(hash string) bool {
|
||||
return true
|
||||
})
|
||||
@@ -720,57 +752,72 @@ func (this *FileStorage) CleanAll() error {
|
||||
|
||||
// 删除缓存和目录
|
||||
// 不能直接删除子目录,比较危险
|
||||
dir := this.dir()
|
||||
fp, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
|
||||
stat, err := fp.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !stat.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 改成待删除
|
||||
subDirs, err := fp.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, info := range subDirs {
|
||||
subDir := info.Name()
|
||||
|
||||
// 检查目录名
|
||||
ok, err := regexp.MatchString(`^[0-9a-f]{2}$`, subDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
var rootDirs = []string{this.options.Dir}
|
||||
var subDirs = this.subDirs // copy slice
|
||||
if len(subDirs) > 0 {
|
||||
for _, subDir := range subDirs {
|
||||
rootDirs = append(rootDirs, subDir.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改目录名
|
||||
tmpDir := dir + "/" + subDir + "-deleted"
|
||||
err = os.Rename(dir+"/"+subDir, tmpDir)
|
||||
var dirNameReg = regexp.MustCompile(`^[0-9a-f]{2}$`)
|
||||
for _, rootDir := range rootDirs {
|
||||
var dir = rootDir + "/p" + types.String(this.policy.Id)
|
||||
err = func(dir string) error {
|
||||
fp, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
|
||||
stat, err := fp.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !stat.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 改成待删除
|
||||
subDirs, err := fp.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, info := range subDirs {
|
||||
subDir := info.Name()
|
||||
|
||||
// 检查目录名
|
||||
if !dirNameReg.MatchString(subDir) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 修改目录名
|
||||
tmpDir := dir + "/" + subDir + "-deleted"
|
||||
err = os.Rename(dir+"/"+subDir, tmpDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 重新遍历待删除
|
||||
goman.New(func() {
|
||||
err = this.cleanDeletedDirs(dir)
|
||||
if err != nil {
|
||||
remotelogs.Warn("CACHE", "delete '*-deleted' dirs failed: "+err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 重新遍历待删除
|
||||
goman.New(func() {
|
||||
err = this.cleanDeletedDirs(dir)
|
||||
if err != nil {
|
||||
remotelogs.Warn("CACHE", "delete '*-deleted' dirs failed: "+err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -781,9 +828,6 @@ func (this *FileStorage) Purge(keys []string, urlType string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
// 先尝试内存缓存
|
||||
this.runMemoryStorageSafety(func(memoryStorage *MemoryStorage) {
|
||||
_ = memoryStorage.Purge(keys, urlType)
|
||||
@@ -792,6 +836,19 @@ func (this *FileStorage) Purge(keys []string, urlType string) error {
|
||||
// 目录
|
||||
if urlType == "dir" {
|
||||
for _, key := range keys {
|
||||
// 检查是否有通配符 http(s)://*.example.com
|
||||
var schemeIndex = strings.Index(key, "://")
|
||||
if schemeIndex > 0 {
|
||||
var keyRight = key[schemeIndex+3:]
|
||||
if strings.HasPrefix(keyRight, "*.") {
|
||||
err := this.list.CleanMatchPrefix(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err := this.list.CleanPrefix(key)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -802,7 +859,21 @@ func (this *FileStorage) Purge(keys []string, urlType string) error {
|
||||
|
||||
// URL
|
||||
for _, key := range keys {
|
||||
hash, path := this.keyPath(key)
|
||||
// 检查是否有通配符 http(s)://*.example.com
|
||||
var schemeIndex = strings.Index(key, "://")
|
||||
if schemeIndex > 0 {
|
||||
var keyRight = key[schemeIndex+3:]
|
||||
if strings.HasPrefix(keyRight, "*.") {
|
||||
err := this.list.CleanMatchKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 普通的Key
|
||||
hash, path, _ := this.keyPath(key)
|
||||
err := this.removeCacheFile(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
@@ -873,25 +944,22 @@ func (this *FileStorage) CanSendfile() bool {
|
||||
return this.options.EnableSendfile
|
||||
}
|
||||
|
||||
// 绝对路径
|
||||
func (this *FileStorage) dir() string {
|
||||
return this.options.Dir + "/p" + strconv.FormatInt(this.policy.Id, 10) + "/"
|
||||
}
|
||||
|
||||
// 获取Key对应的文件路径
|
||||
func (this *FileStorage) keyPath(key string) (hash string, path string) {
|
||||
func (this *FileStorage) keyPath(key string) (hash string, path string, diskIsFull bool) {
|
||||
hash = stringutil.Md5(key)
|
||||
dir := this.options.Dir + "/p" + strconv.FormatInt(this.policy.Id, 10) + "/" + hash[:2] + "/" + hash[2:4]
|
||||
var dir string
|
||||
dir, diskIsFull = this.subDir(hash)
|
||||
path = dir + "/" + hash + ".cache"
|
||||
return
|
||||
}
|
||||
|
||||
// 获取Hash对应的文件路径
|
||||
func (this *FileStorage) hashPath(hash string) (path string) {
|
||||
func (this *FileStorage) hashPath(hash string) (path string, diskIsFull bool) {
|
||||
if len(hash) != 32 {
|
||||
return ""
|
||||
return "", false
|
||||
}
|
||||
dir := this.options.Dir + "/p" + strconv.FormatInt(this.policy.Id, 10) + "/" + hash[:2] + "/" + hash[2:4]
|
||||
var dir string
|
||||
dir, diskIsFull = this.subDir(hash)
|
||||
path = dir + "/" + hash + ".cache"
|
||||
return
|
||||
}
|
||||
@@ -949,18 +1017,31 @@ func (this *FileStorage) initList() error {
|
||||
}
|
||||
|
||||
// 清理任务
|
||||
// TODO purge每个分区
|
||||
func (this *FileStorage) purgeLoop() {
|
||||
// 检查磁盘剩余空间
|
||||
this.checkDiskSpace()
|
||||
|
||||
// 计算是否应该开启LFU清理
|
||||
var capacityBytes = this.policy.CapacityBytes()
|
||||
var capacityBytes = this.diskCapacityBytes()
|
||||
var startLFU = false
|
||||
var lfuFreePercent = this.policy.PersistenceLFUFreePercent
|
||||
if lfuFreePercent <= 0 {
|
||||
lfuFreePercent = 5
|
||||
}
|
||||
if this.diskIsFull {
|
||||
|
||||
var hasFullDisk = this.mainDiskIsFull
|
||||
if !hasFullDisk {
|
||||
var subDirs = this.subDirs // copy slice
|
||||
for _, subDir := range subDirs {
|
||||
if subDir.IsFull {
|
||||
hasFullDisk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasFullDisk {
|
||||
startLFU = true
|
||||
} else {
|
||||
var usedPercent = float32(this.TotalDiskSize()*100) / float32(capacityBytes)
|
||||
@@ -993,7 +1074,7 @@ func (this *FileStorage) purgeLoop() {
|
||||
}
|
||||
for i := 0; i < times; i++ {
|
||||
countFound, err := this.list.Purge(purgeCount, func(hash string) error {
|
||||
path := this.hashPath(hash)
|
||||
path, _ := this.hashPath(hash)
|
||||
err := this.removeCacheFile(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
remotelogs.Error("CACHE", "purge '"+path+"' error: "+err.Error())
|
||||
@@ -1027,7 +1108,7 @@ func (this *FileStorage) purgeLoop() {
|
||||
|
||||
remotelogs.Println("CACHE", "LFU purge policy '"+this.policy.Name+"' id: "+types.String(this.policy.Id)+", count: "+types.String(count))
|
||||
err := this.list.PurgeLFU(count, func(hash string) error {
|
||||
path := this.hashPath(hash)
|
||||
path, _ := this.hashPath(hash)
|
||||
err := this.removeCacheFile(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
remotelogs.Error("CACHE", "purge '"+path+"' error: "+err.Error())
|
||||
@@ -1108,7 +1189,7 @@ func (this *FileStorage) hotLoop() {
|
||||
expiresAt = bestExpiresAt
|
||||
}
|
||||
|
||||
writer, err := memoryStorage.openWriter(item.Key, expiresAt, reader.Status(), reader.BodySize(), -1, false)
|
||||
writer, err := memoryStorage.openWriter(item.Key, expiresAt, reader.Status(), types.Int(reader.HeaderSize()), reader.BodySize(), -1, false)
|
||||
if err != nil {
|
||||
if !CanIgnoreErr(err) {
|
||||
remotelogs.Error("CACHE", "transfer hot item failed: "+err.Error())
|
||||
@@ -1132,9 +1213,12 @@ func (this *FileStorage) hotLoop() {
|
||||
}
|
||||
|
||||
err = reader.ReadBody(buf, func(n int) (goNext bool, err error) {
|
||||
_, err = writer.Write(buf[:n])
|
||||
if err == nil {
|
||||
goNext = true
|
||||
goNext = true
|
||||
if n > 0 {
|
||||
_, err = writer.Write(buf[:n])
|
||||
if err != nil {
|
||||
goNext = false
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
@@ -1147,6 +1231,7 @@ func (this *FileStorage) hotLoop() {
|
||||
memoryStorage.AddToList(&Item{
|
||||
Type: writer.ItemType(),
|
||||
Key: item.Key,
|
||||
Host: ParseHost(item.Key),
|
||||
ExpiredAt: expiresAt,
|
||||
HeaderSize: writer.HeaderSize(),
|
||||
BodySize: writer.BodySize(),
|
||||
@@ -1354,7 +1439,55 @@ func (this *FileStorage) checkDiskSpace() {
|
||||
err := unix.Statfs(this.options.Dir, &stat)
|
||||
if err == nil {
|
||||
var availableBytes = stat.Bavail * uint64(stat.Bsize)
|
||||
this.diskIsFull = availableBytes < MinDiskSpace
|
||||
this.mainDiskIsFull = availableBytes < MinDiskSpace
|
||||
}
|
||||
}
|
||||
var subDirs = this.subDirs // copy slice
|
||||
for _, subDir := range subDirs {
|
||||
var stat unix.Statfs_t
|
||||
err := unix.Statfs(subDir.Path, &stat)
|
||||
if err == nil {
|
||||
var availableBytes = stat.Bavail * uint64(stat.Bsize)
|
||||
subDir.IsFull = availableBytes < MinDiskSpace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取目录
|
||||
func (this *FileStorage) subDir(hash string) (dirPath string, dirIsFull bool) {
|
||||
var suffix = "/p" + types.String(this.policy.Id) + "/" + hash[:2] + "/" + hash[2:4]
|
||||
|
||||
if len(hash) < 4 {
|
||||
return this.options.Dir + suffix, this.mainDiskIsFull
|
||||
}
|
||||
|
||||
var subDirs = this.subDirs // copy slice
|
||||
var countSubDirs = len(subDirs)
|
||||
if countSubDirs == 0 {
|
||||
return this.options.Dir + suffix, this.mainDiskIsFull
|
||||
}
|
||||
|
||||
countSubDirs++ // add main dir
|
||||
|
||||
// 最多只支持16个目录
|
||||
if countSubDirs > 16 {
|
||||
countSubDirs = 16
|
||||
}
|
||||
|
||||
var dirIndex = this.charCode(hash[0]) % uint8(countSubDirs)
|
||||
if dirIndex == 0 {
|
||||
return this.options.Dir + suffix, this.mainDiskIsFull
|
||||
}
|
||||
var subDir = subDirs[dirIndex-1]
|
||||
return subDir.Path + suffix, subDir.IsFull
|
||||
}
|
||||
|
||||
func (this *FileStorage) charCode(r byte) uint8 {
|
||||
if r >= '0' && r <= '9' {
|
||||
return r - '0'
|
||||
}
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return r - 'a' + 10
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestFileStorage_OpenWriter(t *testing.T) {
|
||||
|
||||
header := []byte("Header")
|
||||
body := []byte("This is Body")
|
||||
writer, err := storage.OpenWriter("my-key", time.Now().Unix()+86400, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("my-key", time.Now().Unix()+86400, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func TestFileStorage_OpenWriter_Partial(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
writer, err := storage.OpenWriter("my-key", time.Now().Unix()+86400, 200, -1, -1, true)
|
||||
writer, err := storage.OpenWriter("my-key", time.Now().Unix()+86400, 200, -1, -1, -1, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func TestFileStorage_OpenWriter_HTTP(t *testing.T) {
|
||||
t.Log(time.Since(now).Seconds()*1000, "ms")
|
||||
}()
|
||||
|
||||
writer, err := storage.OpenWriter("my-http-response", time.Now().Unix()+86400, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("my-http-response", time.Now().Unix()+86400, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -212,7 +212,7 @@ func TestFileStorage_Concurrent_Open_DifferentFile(t *testing.T) {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
writer, err := storage.OpenWriter("abc"+strconv.Itoa(i), time.Now().Unix()+3600, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc"+strconv.Itoa(i), time.Now().Unix()+3600, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
if err != ErrFileIsWriting {
|
||||
t.Error(err)
|
||||
@@ -267,7 +267,7 @@ func TestFileStorage_Concurrent_Open_SameFile(t *testing.T) {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
writer, err := storage.OpenWriter("abc"+strconv.Itoa(0), time.Now().Unix()+3600, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc"+strconv.Itoa(0), time.Now().Unix()+3600, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
if err != ErrFileIsWriting {
|
||||
t.Error(err)
|
||||
@@ -522,7 +522,7 @@ func TestFileStorage_DecodeFile(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, path := storage.keyPath("my-key")
|
||||
_, path, _ := storage.keyPath("my-key")
|
||||
t.Log(path)
|
||||
}
|
||||
|
||||
@@ -569,6 +569,6 @@ func BenchmarkFileStorage_KeyPath(b *testing.B) {
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = storage.keyPath(strconv.Itoa(i))
|
||||
_, _, _ = storage.keyPath(strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ type StorageInterface interface {
|
||||
|
||||
// OpenWriter 打开缓存写入器等待写入
|
||||
// size 和 maxSize 可能为-1
|
||||
OpenWriter(key string, expiresAt int64, status int, size int64, maxSize int64, isPartial bool) (Writer, error)
|
||||
OpenWriter(key string, expiresAt int64, status int, headerSize int, bodySize int64, maxSize int64, isPartial bool) (Writer, error)
|
||||
|
||||
// OpenFlushWriter 打开从其他媒介直接刷入的写入器
|
||||
OpenFlushWriter(key string, expiresAt int64, status int) (Writer, error)
|
||||
OpenFlushWriter(key string, expiresAt int64, status int, headerSize int, bodySize int64) (Writer, error)
|
||||
|
||||
// Delete 删除某个键值对应的缓存
|
||||
Delete(key string) error
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package caches
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"math"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -149,7 +149,7 @@ func (this *MemoryStorage) OpenReader(key string, useStale bool, isPartial bool)
|
||||
}
|
||||
|
||||
// OpenWriter 打开缓存写入器等待写入
|
||||
func (this *MemoryStorage) OpenWriter(key string, expiredAt int64, status int, size int64, maxSize int64, isPartial bool) (Writer, error) {
|
||||
func (this *MemoryStorage) OpenWriter(key string, expiredAt int64, status int, headerSize int, bodySize int64, maxSize int64, isPartial bool) (Writer, error) {
|
||||
if this.ignoreKeys.Has(key) {
|
||||
return nil, ErrEntityTooLarge
|
||||
}
|
||||
@@ -158,15 +158,15 @@ func (this *MemoryStorage) OpenWriter(key string, expiredAt int64, status int, s
|
||||
if isPartial {
|
||||
return nil, ErrFileIsWriting
|
||||
}
|
||||
return this.openWriter(key, expiredAt, status, size, maxSize, true)
|
||||
return this.openWriter(key, expiredAt, status, headerSize, bodySize, maxSize, true)
|
||||
}
|
||||
|
||||
// OpenFlushWriter 打开从其他媒介直接刷入的写入器
|
||||
func (this *MemoryStorage) OpenFlushWriter(key string, expiresAt int64, status int) (Writer, error) {
|
||||
return this.openWriter(key, expiresAt, status, -1, -1, true)
|
||||
func (this *MemoryStorage) OpenFlushWriter(key string, expiresAt int64, status int, headerSize int, bodySize int64) (Writer, error) {
|
||||
return this.openWriter(key, expiresAt, status, headerSize, bodySize, -1, true)
|
||||
}
|
||||
|
||||
func (this *MemoryStorage) openWriter(key string, expiresAt int64, status int, size int64, maxSize int64, isDirty bool) (Writer, error) {
|
||||
func (this *MemoryStorage) openWriter(key string, expiresAt int64, status int, headerSize int, bodySize int64, maxSize int64, isDirty bool) (Writer, error) {
|
||||
// 待写入队列是否已满
|
||||
if isDirty &&
|
||||
this.parentStorage != nil &&
|
||||
@@ -207,10 +207,10 @@ func (this *MemoryStorage) openWriter(key string, expiresAt int64, status int, s
|
||||
return nil, NewCapacityError("write memory cache failed: too many keys in cache storage")
|
||||
}
|
||||
capacityBytes := this.memoryCapacityBytes()
|
||||
if size < 0 {
|
||||
size = 0
|
||||
if bodySize < 0 {
|
||||
bodySize = 0
|
||||
}
|
||||
if capacityBytes > 0 && capacityBytes <= this.totalSize+size {
|
||||
if capacityBytes > 0 && capacityBytes <= this.totalSize+bodySize {
|
||||
return nil, NewCapacityError("write memory cache failed: over memory size: " + strconv.FormatInt(capacityBytes, 10) + ", current size: " + strconv.FormatInt(this.totalSize, 10) + " bytes")
|
||||
}
|
||||
|
||||
@@ -230,10 +230,10 @@ func (this *MemoryStorage) openWriter(key string, expiresAt int64, status int, s
|
||||
|
||||
// Delete 删除某个键值对应的缓存
|
||||
func (this *MemoryStorage) Delete(key string) error {
|
||||
hash := this.hash(key)
|
||||
var hash = this.hash(key)
|
||||
this.locker.Lock()
|
||||
delete(this.valuesMap, hash)
|
||||
_ = this.list.Remove(fmt.Sprintf("%d", hash))
|
||||
_ = this.list.Remove(types.String(hash))
|
||||
this.locker.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -263,6 +263,19 @@ func (this *MemoryStorage) Purge(keys []string, urlType string) error {
|
||||
// 目录
|
||||
if urlType == "dir" {
|
||||
for _, key := range keys {
|
||||
// 检查是否有通配符 http(s)://*.example.com
|
||||
var schemeIndex = strings.Index(key, "://")
|
||||
if schemeIndex > 0 {
|
||||
var keyRight = key[schemeIndex+3:]
|
||||
if strings.HasPrefix(keyRight, "*.") {
|
||||
err := this.list.CleanMatchPrefix(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err := this.list.CleanPrefix(key)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -273,6 +286,19 @@ func (this *MemoryStorage) Purge(keys []string, urlType string) error {
|
||||
|
||||
// URL
|
||||
for _, key := range keys {
|
||||
// 检查是否有通配符 http(s)://*.example.com
|
||||
var schemeIndex = strings.Index(key, "://")
|
||||
if schemeIndex > 0 {
|
||||
var keyRight = key[schemeIndex+3:]
|
||||
if strings.HasPrefix(keyRight, "*.") {
|
||||
err := this.list.CleanMatchKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err := this.Delete(key)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -336,7 +362,12 @@ func (this *MemoryStorage) CanUpdatePolicy(newPolicy *serverconfigs.HTTPCachePol
|
||||
// AddToList 将缓存添加到列表
|
||||
func (this *MemoryStorage) AddToList(item *Item) {
|
||||
item.MetaSize = int64(len(item.Key)) + 128 /** 128是我们评估的数据结构的长度 **/
|
||||
hash := fmt.Sprintf("%d", this.hash(item.Key))
|
||||
var hash = types.String(this.hash(item.Key))
|
||||
|
||||
if len(item.Host) == 0 {
|
||||
item.Host = ParseHost(item.Key)
|
||||
}
|
||||
|
||||
_ = this.list.Add(hash, item)
|
||||
}
|
||||
|
||||
@@ -433,7 +464,7 @@ func (this *MemoryStorage) startFlush() {
|
||||
var statCount = 0
|
||||
var writeDelayMS float64 = 0
|
||||
|
||||
for hash := range this.dirtyChan {
|
||||
for key := range this.dirtyChan {
|
||||
statCount++
|
||||
|
||||
if statCount == 100 {
|
||||
@@ -455,7 +486,7 @@ func (this *MemoryStorage) startFlush() {
|
||||
}
|
||||
}
|
||||
|
||||
this.flushItem(hash)
|
||||
this.flushItem(key)
|
||||
|
||||
if writeDelayMS > 0 {
|
||||
time.Sleep(time.Duration(writeDelayMS) * time.Millisecond)
|
||||
@@ -481,7 +512,7 @@ func (this *MemoryStorage) flushItem(key string) {
|
||||
return
|
||||
}
|
||||
|
||||
writer, err := this.parentStorage.OpenFlushWriter(key, item.ExpiresAt, item.Status)
|
||||
writer, err := this.parentStorage.OpenFlushWriter(key, item.ExpiresAt, item.Status, len(item.HeaderValue), int64(len(item.BodyValue)))
|
||||
if err != nil {
|
||||
if !CanIgnoreErr(err) {
|
||||
remotelogs.Error("CACHE", "flush items failed: open writer failed: "+err.Error())
|
||||
@@ -513,6 +544,7 @@ func (this *MemoryStorage) flushItem(key string) {
|
||||
this.parentStorage.AddToList(&Item{
|
||||
Type: writer.ItemType(),
|
||||
Key: key,
|
||||
Host: ParseHost(key),
|
||||
ExpiredAt: item.ExpiresAt,
|
||||
HeaderSize: writer.HeaderSize(),
|
||||
BodySize: writer.BodySize(),
|
||||
@@ -542,7 +574,7 @@ func (this *MemoryStorage) memoryCapacityBytes() int64 {
|
||||
func (this *MemoryStorage) deleteWithoutLocker(key string) error {
|
||||
hash := this.hash(key)
|
||||
delete(this.valuesMap, hash)
|
||||
_ = this.list.Remove(fmt.Sprintf("%d", hash))
|
||||
_ = this.list.Remove(types.String(hash))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,22 @@ import (
|
||||
)
|
||||
|
||||
func TestMemoryStorage_OpenWriter(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
var storage = NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
|
||||
writer, err := storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.WriteHeader([]byte("Header"))
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
_, _ = writer.Write([]byte(", World"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(storage.valuesMap)
|
||||
|
||||
{
|
||||
@@ -30,6 +37,7 @@ func TestMemoryStorage_OpenWriter(t *testing.T) {
|
||||
if err != nil {
|
||||
if err == ErrNotFound {
|
||||
t.Log("not found: abc")
|
||||
return
|
||||
} else {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -63,7 +71,7 @@ func TestMemoryStorage_OpenWriter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
writer, err = storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1, -1, false)
|
||||
writer, err = storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -102,21 +110,29 @@ func TestMemoryStorage_OpenReaderLock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoryStorage_Delete(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
var storage = NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(len(storage.valuesMap))
|
||||
}
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc1", time.Now().Unix()+60, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc1", time.Now().Unix()+60, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(len(storage.valuesMap))
|
||||
}
|
||||
_ = storage.Delete("abc1")
|
||||
@@ -124,14 +140,18 @@ func TestMemoryStorage_Delete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoryStorage_Stat(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
var storage = NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
expiredAt := time.Now().Unix() + 60
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(len(storage.valuesMap))
|
||||
storage.AddToList(&Item{
|
||||
Key: "abc",
|
||||
@@ -140,11 +160,15 @@ func TestMemoryStorage_Stat(t *testing.T) {
|
||||
})
|
||||
}
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(len(storage.valuesMap))
|
||||
storage.AddToList(&Item{
|
||||
Key: "abc1",
|
||||
@@ -161,14 +185,18 @@ func TestMemoryStorage_Stat(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoryStorage_CleanAll(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
expiredAt := time.Now().Unix() + 60
|
||||
var storage = NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
var expiredAt = time.Now().Unix() + 60
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
storage.AddToList(&Item{
|
||||
Key: "abc",
|
||||
BodySize: 5,
|
||||
@@ -176,11 +204,15 @@ func TestMemoryStorage_CleanAll(t *testing.T) {
|
||||
})
|
||||
}
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
storage.AddToList(&Item{
|
||||
Key: "abc1",
|
||||
BodySize: 5,
|
||||
@@ -199,11 +231,15 @@ func TestMemoryStorage_Purge(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
expiredAt := time.Now().Unix() + 60
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
storage.AddToList(&Item{
|
||||
Key: "abc",
|
||||
BodySize: 5,
|
||||
@@ -211,11 +247,15 @@ func TestMemoryStorage_Purge(t *testing.T) {
|
||||
})
|
||||
}
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
storage.AddToList(&Item{
|
||||
Key: "abc1",
|
||||
BodySize: 5,
|
||||
@@ -231,7 +271,7 @@ func TestMemoryStorage_Purge(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoryStorage_Expire(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{
|
||||
var storage = NewMemoryStorage(&serverconfigs.HTTPCachePolicy{
|
||||
MemoryAutoPurgeInterval: 5,
|
||||
}, nil)
|
||||
err := storage.Init()
|
||||
@@ -242,11 +282,15 @@ func TestMemoryStorage_Expire(t *testing.T) {
|
||||
for i := 0; i < 1000; i++ {
|
||||
expiredAt := time.Now().Unix() + int64(rands.Int(0, 60))
|
||||
key := "abc" + strconv.Itoa(i)
|
||||
writer, err := storage.OpenWriter(key, expiredAt, 200, -1, -1, false)
|
||||
writer, err := storage.OpenWriter(key, expiredAt, 200, -1, -1, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = writer.Write([]byte("Hello"))
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
storage.AddToList(&Item{
|
||||
Key: key,
|
||||
BodySize: 5,
|
||||
@@ -257,7 +301,7 @@ func TestMemoryStorage_Expire(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoryStorage_Locker(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
var storage = NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
err := storage.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
30
internal/caches/utils.go
Normal file
30
internal/caches/utils.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package caches
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseHost(key string) string {
|
||||
var schemeIndex = strings.Index(key, "://")
|
||||
if schemeIndex <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var firstSlashIndex = strings.Index(key[schemeIndex+3:], "/")
|
||||
if firstSlashIndex <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var host = key[schemeIndex+3 : schemeIndex+3+firstSlashIndex]
|
||||
|
||||
hostPart, _, err := net.SplitHostPort(host)
|
||||
if err == nil && len(hostPart) > 0 {
|
||||
host = configutils.QuoteIP(hostPart)
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
51
internal/caches/utils_test.go
Normal file
51
internal/caches/utils_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package caches_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/caches"
|
||||
"github.com/cespare/xxhash"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseHost(t *testing.T) {
|
||||
for _, u := range []string{
|
||||
"https://goedge.cn/hello/world",
|
||||
"https://goedge.cn:8080/hello/world",
|
||||
"https://goedge.cn/hello/world?v=1&t=123",
|
||||
"https://[::1]:1234/hello/world?v=1&t=123",
|
||||
"https://[::1]/hello/world?v=1&t=123",
|
||||
"https://127.0.0.1/hello/world?v=1&t=123",
|
||||
"https:/hello/world?v=1&t=123",
|
||||
"123456",
|
||||
} {
|
||||
t.Log(u, "=>", caches.ParseHost(u))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUintString(t *testing.T) {
|
||||
t.Log(strconv.FormatUint(xxhash.Sum64String("https://goedge.cn/"), 10))
|
||||
t.Log(strconv.FormatUint(123456789, 10))
|
||||
t.Log(fmt.Sprintf("%d", 1234567890123))
|
||||
}
|
||||
|
||||
func BenchmarkUint_String(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strconv.FormatUint(1234567890123, 10)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUint_String2(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = types.String(1234567890123)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUint_String3(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%d", 1234567890123)
|
||||
}
|
||||
}
|
||||
@@ -11,25 +11,32 @@ import (
|
||||
)
|
||||
|
||||
type FileWriter struct {
|
||||
storage StorageInterface
|
||||
rawWriter *os.File
|
||||
key string
|
||||
headerSize int64
|
||||
bodySize int64
|
||||
expiredAt int64
|
||||
maxSize int64
|
||||
endFunc func()
|
||||
once sync.Once
|
||||
storage StorageInterface
|
||||
rawWriter *os.File
|
||||
key string
|
||||
|
||||
metaHeaderSize int
|
||||
headerSize int64
|
||||
|
||||
metaBodySize int64 // 写入前的内容长度
|
||||
bodySize int64
|
||||
|
||||
expiredAt int64
|
||||
maxSize int64
|
||||
endFunc func()
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func NewFileWriter(storage StorageInterface, rawWriter *os.File, key string, expiredAt int64, maxSize int64, endFunc func()) *FileWriter {
|
||||
func NewFileWriter(storage StorageInterface, rawWriter *os.File, key string, expiredAt int64, metaHeaderSize int, metaBodySize int64, maxSize int64, endFunc func()) *FileWriter {
|
||||
return &FileWriter{
|
||||
storage: storage,
|
||||
key: key,
|
||||
rawWriter: rawWriter,
|
||||
expiredAt: expiredAt,
|
||||
maxSize: maxSize,
|
||||
endFunc: endFunc,
|
||||
storage: storage,
|
||||
key: key,
|
||||
rawWriter: rawWriter,
|
||||
expiredAt: expiredAt,
|
||||
maxSize: maxSize,
|
||||
endFunc: endFunc,
|
||||
metaHeaderSize: metaHeaderSize,
|
||||
metaBodySize: metaBodySize,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +52,10 @@ func (this *FileWriter) WriteHeader(data []byte) (n int, err error) {
|
||||
|
||||
// WriteHeaderLength 写入Header长度数据
|
||||
func (this *FileWriter) WriteHeaderLength(headerLength int) error {
|
||||
bytes4 := make([]byte, 4)
|
||||
if this.metaHeaderSize > 0 && this.metaHeaderSize == headerLength {
|
||||
return nil
|
||||
}
|
||||
var bytes4 = make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(bytes4, uint32(headerLength))
|
||||
_, err := this.rawWriter.Seek(SizeExpiresAt+SizeStatus+SizeURLLength, io.SeekStart)
|
||||
if err != nil {
|
||||
@@ -88,7 +98,10 @@ func (this *FileWriter) WriteAt(offset int64, data []byte) error {
|
||||
|
||||
// WriteBodyLength 写入Body长度数据
|
||||
func (this *FileWriter) WriteBodyLength(bodyLength int64) error {
|
||||
bytes8 := make([]byte, 8)
|
||||
if this.metaBodySize >= 0 && bodyLength == this.metaBodySize {
|
||||
return nil
|
||||
}
|
||||
var bytes8 = make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(bytes8, uint64(bodyLength))
|
||||
_, err := this.rawWriter.Seek(SizeExpiresAt+SizeStatus+SizeURLLength+SizeHeaderLength, io.SeekStart)
|
||||
if err != nil {
|
||||
@@ -109,7 +122,7 @@ func (this *FileWriter) Close() error {
|
||||
this.endFunc()
|
||||
})
|
||||
|
||||
path := this.rawWriter.Name()
|
||||
var path = this.rawWriter.Name()
|
||||
|
||||
err := this.WriteHeaderLength(types.Int(this.headerSize))
|
||||
if err != nil {
|
||||
|
||||
@@ -11,13 +11,18 @@ import (
|
||||
)
|
||||
|
||||
type PartialFileWriter struct {
|
||||
rawWriter *os.File
|
||||
key string
|
||||
headerSize int64
|
||||
bodySize int64
|
||||
expiredAt int64
|
||||
endFunc func()
|
||||
once sync.Once
|
||||
rawWriter *os.File
|
||||
key string
|
||||
|
||||
metaHeaderSize int
|
||||
headerSize int64
|
||||
|
||||
metaBodySize int64
|
||||
bodySize int64
|
||||
|
||||
expiredAt int64
|
||||
endFunc func()
|
||||
once sync.Once
|
||||
|
||||
isNew bool
|
||||
isPartial bool
|
||||
@@ -27,17 +32,19 @@ type PartialFileWriter struct {
|
||||
rangePath string
|
||||
}
|
||||
|
||||
func NewPartialFileWriter(rawWriter *os.File, key string, expiredAt int64, isNew bool, isPartial bool, bodyOffset int64, ranges *PartialRanges, endFunc func()) *PartialFileWriter {
|
||||
func NewPartialFileWriter(rawWriter *os.File, key string, expiredAt int64, metaHeaderSize int, metaBodySize int64, isNew bool, isPartial bool, bodyOffset int64, ranges *PartialRanges, endFunc func()) *PartialFileWriter {
|
||||
return &PartialFileWriter{
|
||||
key: key,
|
||||
rawWriter: rawWriter,
|
||||
expiredAt: expiredAt,
|
||||
endFunc: endFunc,
|
||||
isNew: isNew,
|
||||
isPartial: isPartial,
|
||||
bodyOffset: bodyOffset,
|
||||
ranges: ranges,
|
||||
rangePath: partialRangesFilePath(rawWriter.Name()),
|
||||
key: key,
|
||||
rawWriter: rawWriter,
|
||||
expiredAt: expiredAt,
|
||||
endFunc: endFunc,
|
||||
isNew: isNew,
|
||||
isPartial: isPartial,
|
||||
bodyOffset: bodyOffset,
|
||||
ranges: ranges,
|
||||
rangePath: partialRangesFilePath(rawWriter.Name()),
|
||||
metaHeaderSize: metaHeaderSize,
|
||||
metaBodySize: metaBodySize,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +78,11 @@ func (this *PartialFileWriter) AppendHeader(data []byte) error {
|
||||
|
||||
// WriteHeaderLength 写入Header长度数据
|
||||
func (this *PartialFileWriter) WriteHeaderLength(headerLength int) error {
|
||||
bytes4 := make([]byte, 4)
|
||||
if this.metaHeaderSize > 0 && this.metaHeaderSize == headerLength {
|
||||
return nil
|
||||
}
|
||||
|
||||
var bytes4 = make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(bytes4, uint32(headerLength))
|
||||
_, err := this.rawWriter.Seek(SizeExpiresAt+SizeStatus+SizeURLLength, io.SeekStart)
|
||||
if err != nil {
|
||||
@@ -110,8 +121,13 @@ func (this *PartialFileWriter) WriteAt(offset int64, data []byte) error {
|
||||
}
|
||||
|
||||
if this.bodyOffset == 0 {
|
||||
this.bodyOffset = SizeMeta + int64(len(this.key)) + this.headerSize
|
||||
var keyLength = 0
|
||||
if this.ranges.Version == 0 { // 以往的版本包含有Key
|
||||
keyLength = len(this.key)
|
||||
}
|
||||
this.bodyOffset = SizeMeta + int64(keyLength) + this.headerSize
|
||||
}
|
||||
|
||||
_, err := this.rawWriter.WriteAt(data, this.bodyOffset+offset)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -129,7 +145,10 @@ func (this *PartialFileWriter) SetBodyLength(bodyLength int64) {
|
||||
|
||||
// WriteBodyLength 写入Body长度数据
|
||||
func (this *PartialFileWriter) WriteBodyLength(bodyLength int64) error {
|
||||
bytes8 := make([]byte, 8)
|
||||
if this.metaBodySize > 0 && this.metaBodySize == bodyLength {
|
||||
return nil
|
||||
}
|
||||
var bytes8 = make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(bytes8, uint64(bodyLength))
|
||||
_, err := this.rawWriter.Seek(SizeExpiresAt+SizeStatus+SizeURLLength+SizeHeaderLength, io.SeekStart)
|
||||
if err != nil {
|
||||
@@ -150,8 +169,11 @@ func (this *PartialFileWriter) Close() error {
|
||||
this.endFunc()
|
||||
})
|
||||
|
||||
this.ranges.BodySize = this.bodySize
|
||||
err := this.ranges.WriteToFile(this.rangePath)
|
||||
if err != nil {
|
||||
_ = this.rawWriter.Close()
|
||||
this.remove()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ func TestPartialFileWriter_Write(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ranges = caches.NewPartialRanges()
|
||||
var writer = caches.NewPartialFileWriter(fp, "test", time.Now().Unix()+86500, true, true, 0, ranges, func() {
|
||||
var ranges = caches.NewPartialRanges(0)
|
||||
var writer = caches.NewPartialFileWriter(fp, "test", time.Now().Unix()+86500, -1, -1, true, true, 0, ranges, func() {
|
||||
t.Log("end")
|
||||
})
|
||||
_, err = writer.WriteHeader([]byte("header"))
|
||||
|
||||
@@ -9,11 +9,15 @@ import (
|
||||
// APIConfig 节点API配置
|
||||
type APIConfig struct {
|
||||
RPC struct {
|
||||
Endpoints []string `yaml:"endpoints"`
|
||||
DisableUpdate bool `yaml:"disableUpdate"`
|
||||
} `yaml:"rpc"`
|
||||
NodeId string `yaml:"nodeId"`
|
||||
Secret string `yaml:"secret"`
|
||||
Endpoints []string `yaml:"endpoints" json:"endpoints"`
|
||||
DisableUpdate bool `yaml:"disableUpdate" json:"disableUpdate"`
|
||||
} `yaml:"rpc" json:"rpc"`
|
||||
NodeId string `yaml:"nodeId" json:"nodeId"`
|
||||
Secret string `yaml:"secret" json:"secret"`
|
||||
}
|
||||
|
||||
func NewAPIConfig() *APIConfig {
|
||||
return &APIConfig{}
|
||||
}
|
||||
|
||||
func LoadAPIConfig() (*APIConfig, error) {
|
||||
|
||||
@@ -3,9 +3,9 @@ package configs
|
||||
// ClusterConfig 集群配置
|
||||
type ClusterConfig struct {
|
||||
RPC struct {
|
||||
Endpoints []string `yaml:"endpoints"`
|
||||
DisableUpdate bool `yaml:"disableUpdate"`
|
||||
} `yaml:"rpc"`
|
||||
ClusterId string `yaml:"clusterId"`
|
||||
Secret string `yaml:"secret"`
|
||||
Endpoints []string `yaml:"endpoints" json:"endpoints"`
|
||||
DisableUpdate bool `yaml:"disableUpdate" json:"disableUpdate"`
|
||||
} `yaml:"rpc" json:"rpc"`
|
||||
ClusterId string `yaml:"clusterId" json:"clusterId"`
|
||||
Secret string `yaml:"secret" json:"secret"`
|
||||
}
|
||||
|
||||
7
internal/conns/linger.go
Normal file
7
internal/conns/linger.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package conns
|
||||
|
||||
type LingerConn interface {
|
||||
SetLinger(sec int) error
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
var SharedMap = NewMap()
|
||||
|
||||
type Map struct {
|
||||
m map[string]map[int]net.Conn // ip => { port => Conn }
|
||||
m map[string]map[int]net.Conn // ip => { port => Conn }
|
||||
|
||||
locker sync.RWMutex
|
||||
}
|
||||
@@ -37,9 +37,7 @@ func (this *Map) Add(conn net.Conn) {
|
||||
defer this.locker.Unlock()
|
||||
connMap, ok := this.m[ip]
|
||||
if !ok {
|
||||
this.m[ip] = map[int]net.Conn{
|
||||
port: conn,
|
||||
}
|
||||
this.m[ip] = map[int]net.Conn{port: conn}
|
||||
} else {
|
||||
connMap[port] = conn
|
||||
}
|
||||
@@ -96,6 +94,13 @@ func (this *Map) CloseIPConns(ip string) {
|
||||
|
||||
if ok {
|
||||
for _, conn := range conns {
|
||||
// 设置Linger
|
||||
lingerConn, isLingerConn := conn.(LingerConn)
|
||||
if isLingerConn {
|
||||
_ = lingerConn.SetLinger(0)
|
||||
}
|
||||
|
||||
// 关闭
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
@@ -109,9 +114,10 @@ func (this *Map) AllConns() []net.Conn {
|
||||
|
||||
var result = []net.Conn{}
|
||||
for _, m := range this.m {
|
||||
for _, conn := range m {
|
||||
result = append(result, conn)
|
||||
for _, connInfo := range m {
|
||||
result = append(result, connInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
Version = "0.5.6"
|
||||
Version = "0.6.0"
|
||||
|
||||
ProductName = "Edge Node"
|
||||
ProcessName = "edge-node"
|
||||
|
||||
@@ -70,7 +70,7 @@ func (this *DDoSProtectionManager) Apply(config *ddosconfigs.ProtectionConfig) e
|
||||
nodeConfig, _ := nodeconfigs.SharedNodeConfig()
|
||||
if nodeConfig != nil {
|
||||
var allowIPList = nodeConfig.AllowedIPs
|
||||
if !utils.ContainsSameStrings(allowIPList, this.lastAllowIPList) {
|
||||
if !utils.EqualStrings(allowIPList, this.lastAllowIPList) {
|
||||
allowIPListChanged = true
|
||||
this.lastAllowIPList = allowIPList
|
||||
}
|
||||
@@ -91,6 +91,9 @@ func (this *DDoSProtectionManager) Apply(config *ddosconfigs.ProtectionConfig) e
|
||||
}
|
||||
|
||||
if nftablesInstance == nil {
|
||||
if config == nil || !config.IsOn() {
|
||||
return nil
|
||||
}
|
||||
return errors.New("nftables instance should not be nil")
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ func (this *APIStream) handleWriteCache(message *pb.NodeStreamMessage) error {
|
||||
}
|
||||
|
||||
expiredAt := time.Now().Unix() + msg.LifeSeconds
|
||||
writer, err := storage.OpenWriter(msg.Key, expiredAt, 200, int64(len(msg.Value)), -1, false)
|
||||
writer, err := storage.OpenWriter(msg.Key, expiredAt, 200, -1, int64(len(msg.Value)), -1, false)
|
||||
if err != nil {
|
||||
this.replyFail(message.RequestId, "prepare writing failed: "+err.Error())
|
||||
return err
|
||||
@@ -407,7 +407,7 @@ func (this *APIStream) handleCheckLocalFirewall(message *pb.NodeStreamMessage) e
|
||||
var protectionConfig = sharedNodeConfig.DDoSProtection
|
||||
err = firewalls.SharedDDoSProtectionManager.Apply(protectionConfig)
|
||||
if err != nil {
|
||||
this.replyFail(message.RequestId, dataMessage.Name+"was installed, but apply DDoS protection config failed: "+err.Error())
|
||||
this.replyFail(message.RequestId, dataMessage.Name+" was installed, but apply DDoS protection config failed: "+err.Error())
|
||||
} else {
|
||||
this.replyOk(message.RequestId, string(result.AsJSON()))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package nodes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/conns"
|
||||
@@ -25,17 +26,26 @@ import (
|
||||
type ClientConn struct {
|
||||
BaseClientConn
|
||||
|
||||
isTLS bool
|
||||
hasDeadline bool
|
||||
hasRead bool
|
||||
createdAt int64
|
||||
|
||||
isTLS bool
|
||||
isHTTP bool
|
||||
hasRead bool
|
||||
|
||||
isLO bool // 是否为环路
|
||||
isInAllowList bool
|
||||
|
||||
hasResetSYNFlood bool
|
||||
|
||||
lastReadAt int64
|
||||
lastWriteAt int64
|
||||
lastErr error
|
||||
|
||||
readDeadlineTime int64
|
||||
isShortReading bool // header or handshake
|
||||
}
|
||||
|
||||
func NewClientConn(rawConn net.Conn, isTLS bool, quickClose bool, isInAllowList bool) net.Conn {
|
||||
func NewClientConn(rawConn net.Conn, isHTTP bool, isTLS bool, isInAllowList bool) net.Conn {
|
||||
// 是否为环路
|
||||
var remoteAddr = rawConn.RemoteAddr().String()
|
||||
var isLO = strings.HasPrefix(remoteAddr, "127.0.0.1:") || strings.HasPrefix(remoteAddr, "[::1]:")
|
||||
@@ -43,11 +53,13 @@ func NewClientConn(rawConn net.Conn, isTLS bool, quickClose bool, isInAllowList
|
||||
var conn = &ClientConn{
|
||||
BaseClientConn: BaseClientConn{rawConn: rawConn},
|
||||
isTLS: isTLS,
|
||||
isHTTP: isHTTP,
|
||||
isLO: isLO,
|
||||
isInAllowList: isInAllowList,
|
||||
createdAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if quickClose {
|
||||
if isHTTP {
|
||||
// TODO 可以在配置中设置此值
|
||||
_ = conn.SetLinger(nodeconfigs.DefaultTCPLinger)
|
||||
}
|
||||
@@ -59,6 +71,18 @@ func NewClientConn(rawConn net.Conn, isTLS bool, quickClose bool, isInAllowList
|
||||
}
|
||||
|
||||
func (this *ClientConn) Read(b []byte) (n int, err error) {
|
||||
var globalServerConfig = sharedNodeConfig.GlobalServerConfig
|
||||
|
||||
if globalServerConfig != nil && globalServerConfig.Performance.Debug {
|
||||
this.lastReadAt = time.Now().Unix()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
this.lastErr = errors.New("read error: " + err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 环路直接读取
|
||||
if this.isLO {
|
||||
n, err = this.rawConn.Read(b)
|
||||
@@ -68,34 +92,30 @@ func (this *ClientConn) Read(b []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// TLS
|
||||
if this.isTLS {
|
||||
if !this.hasDeadline {
|
||||
_ = this.rawConn.SetReadDeadline(time.Now().Add(time.Duration(nodeconfigs.DefaultTLSHandshakeTimeout) * time.Second)) // TODO 握手超时时间可以设置
|
||||
this.hasDeadline = true
|
||||
defer func() {
|
||||
_ = this.rawConn.SetReadDeadline(time.Time{})
|
||||
}()
|
||||
}
|
||||
// 设置读超时时间
|
||||
var autoReadTimeout = globalServerConfig != nil && globalServerConfig.Performance.AutoReadTimeout
|
||||
if this.isHTTP && !this.isShortReading && autoReadTimeout {
|
||||
this.setHTTPReadTimeout()
|
||||
}
|
||||
|
||||
// 开始读取
|
||||
n, err = this.rawConn.Read(b)
|
||||
if n > 0 {
|
||||
atomic.AddUint64(&teaconst.InTrafficBytes, uint64(n))
|
||||
if !this.hasRead {
|
||||
this.hasRead = true
|
||||
}
|
||||
this.hasRead = true
|
||||
}
|
||||
|
||||
// 检测是否为握手错误
|
||||
var isHandshakeError = err != nil && os.IsTimeout(err) && !this.hasRead
|
||||
if isHandshakeError {
|
||||
// 检测是否为超时错误
|
||||
var isTimeout = err != nil && os.IsTimeout(err)
|
||||
var isHandshakeError = isTimeout && !this.hasRead
|
||||
if isTimeout {
|
||||
_ = this.SetLinger(0)
|
||||
} else {
|
||||
_ = this.SetLinger(nodeconfigs.DefaultTCPLinger)
|
||||
}
|
||||
|
||||
// 忽略白名单和局域网
|
||||
if !this.isInAllowList && !utils.IsLocalIP(this.RawIP()) {
|
||||
if this.isHTTP && !this.isInAllowList && !utils.IsLocalIP(this.RawIP()) {
|
||||
// SYN Flood检测
|
||||
if this.serverId == 0 || !this.hasResetSYNFlood {
|
||||
var synFloodConfig = sharedNodeConfig.SYNFloodConfig()
|
||||
@@ -114,6 +134,34 @@ func (this *ClientConn) Read(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (this *ClientConn) Write(b []byte) (n int, err error) {
|
||||
var globalServerConfig = sharedNodeConfig.GlobalServerConfig
|
||||
|
||||
if globalServerConfig != nil && globalServerConfig.Performance.Debug {
|
||||
this.lastWriteAt = time.Now().Unix()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
this.lastErr = errors.New("write error: " + err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 设置写超时时间
|
||||
if globalServerConfig != nil && globalServerConfig.Performance.AutoWriteTimeout {
|
||||
// TODO L2 -> L1 写入时不限制时间
|
||||
var timeoutSeconds = len(b) / 1024
|
||||
if timeoutSeconds < 3 {
|
||||
timeoutSeconds = 3
|
||||
}
|
||||
_ = this.rawConn.SetWriteDeadline(time.Now().Add(time.Duration(timeoutSeconds) * time.Second)) // TODO 时间可以设置
|
||||
}
|
||||
|
||||
// 延长读超时时间
|
||||
if this.isHTTP {
|
||||
this.setHTTPReadTimeout()
|
||||
}
|
||||
|
||||
// 开始写入
|
||||
n, err = this.rawConn.Write(b)
|
||||
if n > 0 {
|
||||
// 统计当前服务带宽
|
||||
@@ -125,6 +173,17 @@ func (this *ClientConn) Write(b []byte) (n int, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是写入超时,则立即关闭连接
|
||||
if err != nil && os.IsTimeout(err) {
|
||||
// TODO 考虑对多次慢连接的IP做出惩罚
|
||||
conn, ok := this.rawConn.(LingerConn)
|
||||
if ok {
|
||||
_ = conn.SetLinger(0)
|
||||
}
|
||||
|
||||
_ = this.Close()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,6 +215,27 @@ func (this *ClientConn) SetDeadline(t time.Time) error {
|
||||
}
|
||||
|
||||
func (this *ClientConn) SetReadDeadline(t time.Time) error {
|
||||
// 如果开启了HTTP自动读超时选项,则自动控制超时时间
|
||||
var globalServerConfig = sharedNodeConfig.GlobalServerConfig
|
||||
if this.isHTTP && globalServerConfig != nil && globalServerConfig.Performance.AutoReadTimeout {
|
||||
this.isShortReading = false
|
||||
|
||||
var unixTime = t.Unix()
|
||||
if unixTime < 10 {
|
||||
return nil
|
||||
}
|
||||
if unixTime == this.readDeadlineTime {
|
||||
return nil
|
||||
}
|
||||
this.readDeadlineTime = unixTime
|
||||
var seconds = -time.Since(t)
|
||||
if seconds <= 0 || seconds > HTTPIdleTimeout {
|
||||
return nil
|
||||
}
|
||||
if seconds < HTTPIdleTimeout-1*time.Second {
|
||||
this.isShortReading = true
|
||||
}
|
||||
}
|
||||
return this.rawConn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
@@ -163,6 +243,22 @@ func (this *ClientConn) SetWriteDeadline(t time.Time) error {
|
||||
return this.rawConn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (this *ClientConn) CreatedAt() int64 {
|
||||
return this.createdAt
|
||||
}
|
||||
|
||||
func (this *ClientConn) LastReadAt() int64 {
|
||||
return this.lastReadAt
|
||||
}
|
||||
|
||||
func (this *ClientConn) LastWriteAt() int64 {
|
||||
return this.lastWriteAt
|
||||
}
|
||||
|
||||
func (this *ClientConn) LastErr() error {
|
||||
return this.lastErr
|
||||
}
|
||||
|
||||
func (this *ClientConn) resetSYNFlood() {
|
||||
ttlcache.SharedCache.Delete("SYN_FLOOD:" + this.RawIP())
|
||||
}
|
||||
@@ -194,3 +290,8 @@ func (this *ClientConn) increaseSYNFlood(synFloodConfig *firewallconfigs.SYNFloo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置读超时时间
|
||||
func (this *ClientConn) setHTTPReadTimeout() {
|
||||
_ = this.SetReadDeadline(time.Now().Add(HTTPIdleTimeout))
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
// ClientListener 客户端网络监听
|
||||
type ClientListener struct {
|
||||
rawListener net.Listener
|
||||
isHTTP bool
|
||||
isTLS bool
|
||||
quickClose bool
|
||||
}
|
||||
|
||||
func NewClientListener(listener net.Listener, quickClose bool) *ClientListener {
|
||||
func NewClientListener(listener net.Listener, isHTTP bool) *ClientListener {
|
||||
return &ClientListener{
|
||||
rawListener: listener,
|
||||
quickClose: quickClose,
|
||||
isHTTP: isHTTP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func (this *ClientListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return NewClientConn(conn, this.isTLS, this.quickClose, isInAllowList), nil
|
||||
return NewClientConn(conn, this.isHTTP, this.isTLS, isInAllowList), nil
|
||||
}
|
||||
|
||||
func (this *ClientListener) Close() error {
|
||||
|
||||
@@ -43,7 +43,11 @@ func (this *HTTPAccessLogQueue) Start() {
|
||||
for range ticker.C {
|
||||
err := this.loop()
|
||||
if err != nil {
|
||||
remotelogs.Error("ACCESS_LOG_QUEUE", err.Error())
|
||||
if rpc.IsConnError(err) {
|
||||
remotelogs.Debug("ACCESS_LOG_QUEUE", err.Error())
|
||||
} else {
|
||||
remotelogs.Error("ACCESS_LOG_QUEUE", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,11 +95,11 @@ func (this *HTTPClientPool) Client(req *HTTPRequest,
|
||||
numberCPU = 8
|
||||
}
|
||||
if maxConnections <= 0 {
|
||||
maxConnections = numberCPU * 32
|
||||
maxConnections = numberCPU * 64
|
||||
}
|
||||
|
||||
if idleConns <= 0 {
|
||||
idleConns = numberCPU * 8
|
||||
idleConns = numberCPU * 16
|
||||
}
|
||||
|
||||
// 可以判断为Ln节点请求
|
||||
|
||||
@@ -237,6 +237,14 @@ func (this *HTTPRequest) Do() {
|
||||
}
|
||||
}
|
||||
|
||||
// UA名单
|
||||
if !this.isSubRequest && this.web.UserAgent != nil && this.web.UserAgent.IsOn {
|
||||
if this.doCheckUserAgent() {
|
||||
this.doEnd()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 访问控制
|
||||
if !this.isSubRequest && this.web.Auth != nil && this.web.Auth.IsOn {
|
||||
if this.doAuth() {
|
||||
@@ -526,6 +534,11 @@ func (this *HTTPRequest) configureWeb(web *serverconfigs.HTTPWebConfig, isTop bo
|
||||
this.web.Referers = web.Referers
|
||||
}
|
||||
|
||||
// user agent
|
||||
if web.UserAgent != nil && (web.UserAgent.IsPrior || isTop) {
|
||||
this.web.UserAgent = web.UserAgent
|
||||
}
|
||||
|
||||
// request limit
|
||||
if web.RequestLimit != nil && (web.RequestLimit.IsPrior || isTop) {
|
||||
this.web.RequestLimit = web.RequestLimit
|
||||
@@ -758,6 +771,8 @@ func (this *HTTPRequest) Format(source string) string {
|
||||
return strconv.FormatInt(this.requestFromTime.Unix(), 10)
|
||||
case "host":
|
||||
return this.ReqHost
|
||||
case "cname":
|
||||
return this.ReqServer.CNameDomain
|
||||
case "referer":
|
||||
return this.RawReq.Referer()
|
||||
case "referer.host":
|
||||
@@ -1131,6 +1146,8 @@ func (this *HTTPRequest) requestRemoteAddr(supportVar bool) string {
|
||||
|
||||
// 获取请求的客户端地址列表
|
||||
func (this *HTTPRequest) requestRemoteAddrs() (result []string) {
|
||||
result = append(result, this.requestRemoteAddr(true))
|
||||
|
||||
// X-Forwarded-For
|
||||
var forwardedFor = this.RawReq.Header.Get("X-Forwarded-For")
|
||||
if len(forwardedFor) > 0 {
|
||||
@@ -1552,7 +1569,7 @@ func (this *HTTPRequest) processRequestHeaders(reqHeader http.Header) {
|
||||
}
|
||||
|
||||
// 是否已删除
|
||||
if this.web.ResponseHeaderPolicy.ContainsDeletedHeader(header.Name) {
|
||||
if this.web.RequestHeaderPolicy.ContainsDeletedHeader(header.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1690,6 +1707,36 @@ func (this *HTTPRequest) processResponseHeaders(responseHeader http.Header, stat
|
||||
responseHeader[header.Name] = []string{headerValue}
|
||||
}
|
||||
}
|
||||
|
||||
// CORS
|
||||
if this.web.ResponseHeaderPolicy.CORS != nil && this.web.ResponseHeaderPolicy.CORS.IsOn {
|
||||
var corsConfig = this.web.ResponseHeaderPolicy.CORS
|
||||
|
||||
// Allow-Origin
|
||||
if len(corsConfig.AllowOrigin) == 0 {
|
||||
var origin = this.RawReq.Header.Get("Origin")
|
||||
if len(origin) > 0 {
|
||||
responseHeader.Set("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
} else {
|
||||
responseHeader.Set("Access-Control-Allow-Origin", corsConfig.AllowOrigin)
|
||||
}
|
||||
|
||||
// Allow-Methods
|
||||
if len(corsConfig.AllowMethods) == 0 {
|
||||
responseHeader.Set("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, HEAD, OPTIONS")
|
||||
} else {
|
||||
responseHeader.Set("Access-Control-Allow-Methods", strings.Join(corsConfig.AllowMethods, ", "))
|
||||
}
|
||||
|
||||
// Max-Age
|
||||
if corsConfig.MaxAge > 0 {
|
||||
responseHeader.Set("Access-Control-Max-Age", types.String(corsConfig.MaxAge))
|
||||
}
|
||||
|
||||
// Allow-Credentials
|
||||
responseHeader.Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
}
|
||||
|
||||
// HSTS
|
||||
@@ -1721,10 +1768,10 @@ func (this *HTTPRequest) bytePool(contentLength int64) *utils.BytePool {
|
||||
return utils.BytePool1k
|
||||
}
|
||||
if contentLength < 32768 { // 32K
|
||||
return utils.BytePool4k
|
||||
return utils.BytePool16k
|
||||
}
|
||||
if contentLength < 131072 { // 128K
|
||||
return utils.BytePool16k
|
||||
return utils.BytePool32k
|
||||
}
|
||||
return utils.BytePool32k
|
||||
}
|
||||
|
||||
@@ -295,34 +295,31 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
|
||||
}
|
||||
}
|
||||
|
||||
var pool = this.bytePool(fileSize)
|
||||
var buf = pool.Get()
|
||||
defer func() {
|
||||
pool.Put(buf)
|
||||
}()
|
||||
|
||||
// 读取Header
|
||||
var headerBuf = []byte{}
|
||||
var headerData = []byte{}
|
||||
this.writer.SetSentHeaderBytes(reader.HeaderSize())
|
||||
err = reader.ReadHeader(buf, func(n int) (goNext bool, err error) {
|
||||
headerBuf = append(headerBuf, buf[:n]...)
|
||||
var headerPool = this.bytePool(reader.HeaderSize())
|
||||
var headerBuf = headerPool.Get()
|
||||
err = reader.ReadHeader(headerBuf, func(n int) (goNext bool, err error) {
|
||||
headerData = append(headerData, headerBuf[:n]...)
|
||||
for {
|
||||
nIndex := bytes.Index(headerBuf, []byte{'\n'})
|
||||
nIndex := bytes.Index(headerData, []byte{'\n'})
|
||||
if nIndex >= 0 {
|
||||
row := headerBuf[:nIndex]
|
||||
row := headerData[:nIndex]
|
||||
spaceIndex := bytes.Index(row, []byte{':'})
|
||||
if spaceIndex <= 0 {
|
||||
return false, errors.New("invalid header '" + string(row) + "'")
|
||||
}
|
||||
|
||||
this.writer.Header().Set(string(row[:spaceIndex]), string(row[spaceIndex+1:]))
|
||||
headerBuf = headerBuf[nIndex+1:]
|
||||
headerData = headerData[nIndex+1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
headerPool.Put(headerBuf)
|
||||
if err != nil {
|
||||
if !this.canIgnore(err) {
|
||||
remotelogs.WarnServer("HTTP_REQUEST_CACHE", this.URL()+": read from cache failed: read header failed: "+err.Error())
|
||||
@@ -460,13 +457,16 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
|
||||
respHeader.Set("Content-Length", strconv.FormatInt(ranges[0].Length(), 10))
|
||||
this.writer.WriteHeader(http.StatusPartialContent)
|
||||
|
||||
err = reader.ReadBodyRange(buf, ranges[0].Start(), ranges[0].End(), func(n int) (goNext bool, err error) {
|
||||
_, err = this.writer.Write(buf[:n])
|
||||
var pool = this.bytePool(fileSize)
|
||||
var bodyBuf = pool.Get()
|
||||
err = reader.ReadBodyRange(bodyBuf, ranges[0].Start(), ranges[0].End(), func(n int) (goNext bool, err error) {
|
||||
_, err = this.writer.Write(bodyBuf[:n])
|
||||
if err != nil {
|
||||
return false, errWritingToClient
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
pool.Put(bodyBuf)
|
||||
if err != nil {
|
||||
this.varMapping["cache.status"] = "MISS"
|
||||
|
||||
@@ -513,13 +513,16 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
|
||||
}
|
||||
}
|
||||
|
||||
err := reader.ReadBodyRange(buf, r.Start(), r.End(), func(n int) (goNext bool, err error) {
|
||||
_, err = this.writer.Write(buf[:n])
|
||||
var pool = this.bytePool(fileSize)
|
||||
var bodyBuf = pool.Get()
|
||||
err := reader.ReadBodyRange(bodyBuf, r.Start(), r.End(), func(n int) (goNext bool, err error) {
|
||||
_, err = this.writer.Write(bodyBuf[:n])
|
||||
if err != nil {
|
||||
return false, errWritingToClient
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
pool.Put(bodyBuf)
|
||||
if err != nil {
|
||||
if !this.canIgnore(err) {
|
||||
remotelogs.WarnServer("HTTP_REQUEST_CACHE", this.URL()+": read from cache failed: "+err.Error())
|
||||
@@ -543,15 +546,18 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
|
||||
this.writer.Prepare(resp, fileSize, reader.Status(), false)
|
||||
this.writer.WriteHeader(reader.Status())
|
||||
|
||||
var pool = this.bytePool(fileSize)
|
||||
var bodyBuf = pool.Get()
|
||||
if storage.CanSendfile() {
|
||||
if fp, canSendFile := this.writer.canSendfile(); canSendFile {
|
||||
this.writer.sentBodyBytes, err = io.CopyBuffer(this.writer.rawWriter, fp, buf)
|
||||
this.writer.sentBodyBytes, err = io.CopyBuffer(this.writer.rawWriter, fp, bodyBuf)
|
||||
} else {
|
||||
_, err = io.CopyBuffer(this.writer, resp.Body, buf)
|
||||
_, err = io.CopyBuffer(this.writer, resp.Body, bodyBuf)
|
||||
}
|
||||
} else {
|
||||
_, err = io.CopyBuffer(this.writer, resp.Body, buf)
|
||||
_, err = io.CopyBuffer(this.writer, resp.Body, bodyBuf)
|
||||
}
|
||||
pool.Put(bodyBuf)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
@@ -622,7 +628,14 @@ func (this *HTTPRequest) tryPartialReader(storage caches.StorageInterface, key s
|
||||
}()
|
||||
|
||||
// 检查范围
|
||||
//const maxFirstSpan = 16 << 20 // TODO 可以在缓存策略中设置此值
|
||||
for index, r := range ranges {
|
||||
// 没有指定结束位置时,自动指定一个
|
||||
/**if r.Start() >= 0 && r.End() == -1 {
|
||||
if partialReader.MaxLength() > r.Start()+maxFirstSpan {
|
||||
r[1] = r.Start() + maxFirstSpan
|
||||
}
|
||||
}**/
|
||||
r1, ok := r.Convert(partialReader.MaxLength())
|
||||
if !ok {
|
||||
return nil, nil
|
||||
|
||||
@@ -117,8 +117,18 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
|
||||
continue
|
||||
}
|
||||
|
||||
var reqHost = this.ReqHost
|
||||
|
||||
// 忽略跳转前端口
|
||||
if u.DomainBeforeIgnorePorts {
|
||||
h, _, err := net.SplitHostPort(reqHost)
|
||||
if err == nil && len(h) > 0 {
|
||||
reqHost = h
|
||||
}
|
||||
}
|
||||
|
||||
// 如果跳转前后域名一致,则终止
|
||||
if u.DomainAfter == this.ReqHost {
|
||||
if u.DomainAfter == reqHost {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -126,7 +136,7 @@ func (this *HTTPRequest) doHostRedirect() (blocked bool) {
|
||||
if len(scheme) == 0 {
|
||||
scheme = this.requestScheme()
|
||||
}
|
||||
if u.DomainsAll || configutils.MatchDomains(u.DomainsBefore, this.ReqHost) {
|
||||
if u.DomainsAll || configutils.MatchDomains(u.DomainsBefore, reqHost) {
|
||||
var afterURL = scheme + "://" + u.DomainAfter + urlPath
|
||||
if fullURL == afterURL {
|
||||
// 终止匹配
|
||||
|
||||
@@ -12,6 +12,8 @@ func (this *HTTPRequest) doCheckReferers() (shouldStop bool) {
|
||||
return
|
||||
}
|
||||
|
||||
const cacheSeconds = "3600" // 时间不能过长,防止修改设置后长期无法生效
|
||||
|
||||
var refererURL = this.RawReq.Header.Get("Referer")
|
||||
if len(refererURL) == 0 {
|
||||
if this.web.Referers.MatchDomain(this.ReqHost, "") {
|
||||
@@ -19,6 +21,7 @@ func (this *HTTPRequest) doCheckReferers() (shouldStop bool) {
|
||||
}
|
||||
|
||||
this.tags = append(this.tags, "refererCheck")
|
||||
this.writer.Header().Set("Cache-Control", "max-age="+cacheSeconds)
|
||||
this.writeCode(http.StatusForbidden, "The referer has been blocked.", "当前访问已被防盗链系统拦截。")
|
||||
|
||||
return true
|
||||
@@ -31,6 +34,7 @@ func (this *HTTPRequest) doCheckReferers() (shouldStop bool) {
|
||||
}
|
||||
|
||||
this.tags = append(this.tags, "refererCheck")
|
||||
this.writer.Header().Set("Cache-Control", "max-age="+cacheSeconds)
|
||||
this.writeCode(http.StatusForbidden, "The referer has been blocked.", "当前访问已被防盗链系统拦截。")
|
||||
|
||||
return true
|
||||
@@ -38,6 +42,7 @@ func (this *HTTPRequest) doCheckReferers() (shouldStop bool) {
|
||||
|
||||
if !this.web.Referers.MatchDomain(this.ReqHost, u.Host) {
|
||||
this.tags = append(this.tags, "refererCheck")
|
||||
this.writer.Header().Set("Cache-Control", "max-age="+cacheSeconds)
|
||||
this.writeCode(http.StatusForbidden, "The referer has been blocked.", "当前访问已被防盗链系统拦截。")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@ func (this *HTTPRequest) doStat() {
|
||||
|
||||
// 内置的统计
|
||||
stats.SharedHTTPRequestStatManager.AddRemoteAddr(this.ReqServer.Id, this.requestRemoteAddr(true), this.writer.SentBodyBytes(), this.isAttack)
|
||||
stats.SharedHTTPRequestStatManager.AddUserAgent(this.ReqServer.Id, this.requestHeader("User-Agent"))
|
||||
stats.SharedHTTPRequestStatManager.AddUserAgent(this.ReqServer.Id, this.requestHeader("User-Agent"), this.remoteAddr)
|
||||
}
|
||||
|
||||
24
internal/nodes/http_request_user_agent.go
Normal file
24
internal/nodes/http_request_user_agent.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package nodes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (this *HTTPRequest) doCheckUserAgent() (shouldStop bool) {
|
||||
if this.web.UserAgent == nil {
|
||||
return
|
||||
}
|
||||
|
||||
const cacheSeconds = "3600" // 时间不能过长,防止修改设置后长期无法生效
|
||||
|
||||
if !this.web.UserAgent.AllowRequest(this.RawReq) {
|
||||
this.tags = append(this.tags, "userAgentCheck")
|
||||
this.writer.Header().Set("Cache-Control", "max-age="+cacheSeconds)
|
||||
this.writeCode(http.StatusForbidden, "The User-Agent has been blocked.", "当前访问已被UA名单拦截。")
|
||||
return true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func (this *HTTPWriter) Prepare(resp *http.Response, size int64, status int, ena
|
||||
this.req.web.RequestLimit != nil &&
|
||||
this.req.web.RequestLimit.IsOn &&
|
||||
this.req.web.RequestLimit.OutBandwidthPerConnBytes() > 0 {
|
||||
this.writer = writers.NewRateLimitWriter(this.writer, this.req.web.RequestLimit.OutBandwidthPerConnBytes())
|
||||
this.writer = writers.NewRateLimitWriter(this.req.RawReq.Context(), this.writer, this.req.web.RequestLimit.OutBandwidthPerConnBytes())
|
||||
}
|
||||
|
||||
return
|
||||
@@ -303,7 +303,19 @@ func (this *HTTPWriter) PrepareCache(resp *http.Response, size int64) {
|
||||
if this.isPartial {
|
||||
cacheKey += caches.SuffixPartial
|
||||
}
|
||||
cacheWriter, err := storage.OpenWriter(cacheKey, expiresAt, this.StatusCode(), size, cacheRef.MaxSizeBytes(), this.isPartial)
|
||||
|
||||
// 待写入尺寸
|
||||
var totalSize = size
|
||||
if totalSize < 0 && this.isPartial {
|
||||
var contentRange = resp.Header.Get("Content-Range")
|
||||
if len(contentRange) > 0 {
|
||||
_, partialTotalSize := httpRequestParseContentRangeHeader(contentRange)
|
||||
if partialTotalSize > 0 {
|
||||
totalSize = partialTotalSize
|
||||
}
|
||||
}
|
||||
}
|
||||
cacheWriter, err := storage.OpenWriter(cacheKey, expiresAt, this.StatusCode(), this.calculateHeaderLength(), totalSize, cacheRef.MaxSizeBytes(), this.isPartial)
|
||||
if err != nil {
|
||||
if err == caches.ErrEntityTooLarge && addStatusHeader {
|
||||
this.Header().Set("X-Cache", "BYPASS, entity too large")
|
||||
@@ -324,13 +336,19 @@ func (this *HTTPWriter) PrepareCache(resp *http.Response, size int64) {
|
||||
}
|
||||
|
||||
// 写入Header
|
||||
var headerBuf = utils.SharedBufferPool.Get()
|
||||
for k, v := range this.Header() {
|
||||
if k == "Set-Cookie" || (this.isPartial && k == "Content-Range") {
|
||||
continue
|
||||
}
|
||||
for _, v1 := range v {
|
||||
if this.isPartial && k == "Content-Type" && strings.Contains(v1, "multipart/byteranges") {
|
||||
continue
|
||||
}
|
||||
_, err = cacheWriter.WriteHeader([]byte(k + ":" + v1 + "\n"))
|
||||
_, err = headerBuf.Write([]byte(k + ":" + v1 + "\n"))
|
||||
if err != nil {
|
||||
utils.SharedBufferPool.Put(headerBuf)
|
||||
|
||||
remotelogs.Error("HTTP_WRITER", "write cache failed: "+err.Error())
|
||||
_ = this.cacheWriter.Discard()
|
||||
this.cacheWriter = nil
|
||||
@@ -338,6 +356,14 @@ func (this *HTTPWriter) PrepareCache(resp *http.Response, size int64) {
|
||||
}
|
||||
}
|
||||
}
|
||||
_, err = cacheWriter.WriteHeader(headerBuf.Bytes())
|
||||
utils.SharedBufferPool.Put(headerBuf)
|
||||
if err != nil {
|
||||
remotelogs.Error("HTTP_WRITER", "write cache failed: "+err.Error())
|
||||
_ = this.cacheWriter.Discard()
|
||||
this.cacheWriter = nil
|
||||
return
|
||||
}
|
||||
|
||||
if this.isPartial {
|
||||
// content-range
|
||||
@@ -558,6 +584,11 @@ func (this *HTTPWriter) PrepareCompression(resp *http.Response, size int64) {
|
||||
return
|
||||
}
|
||||
|
||||
// 分区内容不压缩,防止读取失败
|
||||
if !this.compressionConfig.EnablePartialContent && this.StatusCode() == http.StatusPartialContent {
|
||||
return
|
||||
}
|
||||
|
||||
if this.compressionConfig.Level <= 0 {
|
||||
return
|
||||
}
|
||||
@@ -627,16 +658,21 @@ func (this *HTTPWriter) PrepareCompression(resp *http.Response, size int64) {
|
||||
cacheKey += this.cacheReaderSuffix
|
||||
}
|
||||
|
||||
compressionCacheWriter, err := this.cacheStorage.OpenWriter(cacheKey+caches.SuffixCompression+compressionEncoding, expiredAt, this.StatusCode(), -1, cacheRef.MaxSizeBytes(), false)
|
||||
compressionCacheWriter, err := this.cacheStorage.OpenWriter(cacheKey+caches.SuffixCompression+compressionEncoding, expiredAt, this.StatusCode(), this.calculateHeaderLength(), -1, cacheRef.MaxSizeBytes(), false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 写入Header
|
||||
var headerBuffer = utils.SharedBufferPool.Get()
|
||||
for k, v := range this.Header() {
|
||||
if k == "Set-Cookie" || (this.isPartial && k == "Content-Range") {
|
||||
continue
|
||||
}
|
||||
for _, v1 := range v {
|
||||
_, err = compressionCacheWriter.WriteHeader([]byte(k + ":" + v1 + "\n"))
|
||||
_, err = headerBuffer.Write([]byte(k + ":" + v1 + "\n"))
|
||||
if err != nil {
|
||||
utils.SharedBufferPool.Put(headerBuffer)
|
||||
remotelogs.Error("HTTP_WRITER", "write compression cache failed: "+err.Error())
|
||||
_ = compressionCacheWriter.Discard()
|
||||
compressionCacheWriter = nil
|
||||
@@ -645,6 +681,15 @@ func (this *HTTPWriter) PrepareCompression(resp *http.Response, size int64) {
|
||||
}
|
||||
}
|
||||
|
||||
_, err = compressionCacheWriter.WriteHeader(headerBuffer.Bytes())
|
||||
utils.SharedBufferPool.Put(headerBuffer)
|
||||
if err != nil {
|
||||
remotelogs.Error("HTTP_WRITER", "write compression cache failed: "+err.Error())
|
||||
_ = compressionCacheWriter.Discard()
|
||||
compressionCacheWriter = nil
|
||||
return
|
||||
}
|
||||
|
||||
if compressionCacheWriter != nil {
|
||||
this.compressionCacheWriter = compressionCacheWriter
|
||||
var teeWriter = writers.NewTeeWriterCloser(this.writer, compressionCacheWriter)
|
||||
@@ -942,10 +987,14 @@ func (this *HTTPWriter) finishWebP() {
|
||||
expiredAt = this.cacheWriter.ExpiredAt()
|
||||
}
|
||||
|
||||
webpCacheWriter, _ = this.cacheStorage.OpenWriter(cacheKey, expiredAt, this.StatusCode(), -1, -1, false)
|
||||
webpCacheWriter, _ = this.cacheStorage.OpenWriter(cacheKey, expiredAt, this.StatusCode(), -1, -1, -1, false)
|
||||
if webpCacheWriter != nil {
|
||||
// 写入Header
|
||||
for k, v := range this.Header() {
|
||||
if k == "Set-Cookie" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 这里是原始的数据,不需要内容编码
|
||||
if k == "Content-Encoding" || k == "Transfer-Encoding" {
|
||||
continue
|
||||
@@ -1157,3 +1206,16 @@ func (this *HTTPWriter) finishRequest() {
|
||||
_ = this.rawReader.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 计算Header长度
|
||||
func (this *HTTPWriter) calculateHeaderLength() (result int) {
|
||||
for k, v := range this.Header() {
|
||||
if k == "Set-Cookie" || (this.isPartial && k == "Content-Range") {
|
||||
continue
|
||||
}
|
||||
for _, v1 := range v {
|
||||
result += len(k) + 1 /**:**/ + len(v1) + 1 /**\n**/
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (this *BaseListener) buildTLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
Certificates: nil,
|
||||
GetConfigForClient: func(clientInfo *tls.ClientHelloInfo) (config *tls.Config, e error) {
|
||||
tlsPolicy, _, err := this.matchSSL(clientInfo.ServerName)
|
||||
tlsPolicy, _, err := this.matchSSL(this.helloServerName(clientInfo))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (this *BaseListener) buildTLSConfig() *tls.Config {
|
||||
return tlsPolicy.TLSConfig(), nil
|
||||
},
|
||||
GetCertificate: func(clientInfo *tls.ClientHelloInfo) (certificate *tls.Certificate, e error) {
|
||||
tlsPolicy, cert, err := this.matchSSL(clientInfo.ServerName)
|
||||
tlsPolicy, cert, err := this.matchSSL(this.helloServerName(clientInfo))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -182,3 +182,18 @@ func (this *BaseListener) findNamedServerMatched(name string) (serverConfig *ser
|
||||
|
||||
return nil, name
|
||||
}
|
||||
|
||||
// 从Hello信息中获取服务名称
|
||||
func (this *BaseListener) helloServerName(clientInfo *tls.ClientHelloInfo) string {
|
||||
var serverName = clientInfo.ServerName
|
||||
if len(serverName) == 0 {
|
||||
var localAddr = clientInfo.Conn.LocalAddr()
|
||||
if localAddr != nil {
|
||||
tcpAddr, ok := localAddr.(*net.TCPAddr)
|
||||
if ok {
|
||||
serverName = tcpAddr.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return serverName
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestBaseListener_FindServer(t *testing.T) {
|
||||
sharedNodeConfig = &nodeconfigs.NodeConfig{}
|
||||
|
||||
var listener = &BaseListener{}
|
||||
listener.Group = &serverconfigs.ServerAddressGroup{}
|
||||
listener.Group = serverconfigs.NewServerAddressGroup("https://*:443")
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
var server = &serverconfigs.ServerConfig{
|
||||
IsOn: true,
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
|
||||
var httpErrorLogger = log.New(io.Discard, "", 0)
|
||||
|
||||
const HTTPIdleTimeout = 75 * time.Second
|
||||
|
||||
type contextKey struct {
|
||||
key string
|
||||
}
|
||||
@@ -43,16 +45,12 @@ func (this *HTTPListener) Serve() error {
|
||||
this.httpServer = &http.Server{
|
||||
Addr: this.addr,
|
||||
Handler: this,
|
||||
ReadTimeout: 1 * time.Hour, // TODO 改成可以配置
|
||||
ReadHeaderTimeout: 3 * time.Second, // TODO 改成可以配置
|
||||
WriteTimeout: 2 * time.Hour, // TODO 改成可以配置
|
||||
IdleTimeout: 75 * time.Second, // TODO 改成可以配置
|
||||
ReadHeaderTimeout: 3 * time.Second, // TODO 改成可以配置
|
||||
IdleTimeout: HTTPIdleTimeout, // TODO 改成可以配置
|
||||
ConnState: func(conn net.Conn, state http.ConnState) {
|
||||
switch state {
|
||||
case http.StateNew:
|
||||
atomic.AddInt64(&this.countActiveConnections, 1)
|
||||
case http.StateActive, http.StateIdle, http.StateHijacked:
|
||||
// Nothing to do
|
||||
case http.StateClosed:
|
||||
atomic.AddInt64(&this.countActiveConnections, -1)
|
||||
}
|
||||
@@ -116,8 +114,14 @@ func (this *HTTPListener) Reload(group *serverconfigs.ServerAddressGroup) {
|
||||
|
||||
// ServerHTTP 处理HTTP请求
|
||||
func (this *HTTPListener) ServeHTTP(rawWriter http.ResponseWriter, rawReq *http.Request) {
|
||||
// 不支持Connect
|
||||
if rawReq.Method == http.MethodConnect {
|
||||
http.Error(rawWriter, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 域名
|
||||
var reqHost = rawReq.Host
|
||||
var reqHost = strings.ToLower(strings.TrimRight(rawReq.Host, "."))
|
||||
|
||||
// TLS域名
|
||||
if this.isIP(reqHost) {
|
||||
|
||||
@@ -2,14 +2,15 @@ package nodes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
iplib "github.com/TeaOSLab/EdgeCommon/pkg/iplibrary"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ddosconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/caches"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/configs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/conns"
|
||||
@@ -24,12 +25,13 @@ import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/stats"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/trackers"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils"
|
||||
_ "github.com/TeaOSLab/EdgeNode/internal/utils/clock" // 触发时钟更新
|
||||
_ "github.com/TeaOSLab/EdgeNode/internal/utils/agents" // 引入Agent管理器
|
||||
_ "github.com/TeaOSLab/EdgeNode/internal/utils/clock" // 触发时钟更新
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/jsonutils"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf"
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"github.com/iwind/gosock/pkg/gosock"
|
||||
@@ -38,6 +40,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
@@ -59,18 +62,27 @@ type Node struct {
|
||||
sock *gosock.Sock
|
||||
locker sync.Mutex
|
||||
|
||||
maxCPU int32
|
||||
maxThreads int
|
||||
timezone string
|
||||
oldMaxCPU int32
|
||||
oldMaxThreads int
|
||||
oldTimezone string
|
||||
oldHTTPCachePolicies []*serverconfigs.HTTPCachePolicy
|
||||
oldHTTPFirewallPolicies []*firewallconfigs.HTTPFirewallPolicy
|
||||
oldFirewallActions []*firewallconfigs.FirewallActionConfig
|
||||
oldMetricItems []*serverconfigs.MetricItemConfig
|
||||
|
||||
updatingServerMap map[int64]*serverconfigs.ServerConfig
|
||||
|
||||
lastAPINodeVersion int64
|
||||
lastAPINodeAddrs []string // 以前的API节点地址
|
||||
|
||||
lastTaskVersion int64
|
||||
}
|
||||
|
||||
func NewNode() *Node {
|
||||
return &Node{
|
||||
sock: gosock.NewTmpSock(teaconst.ProcessName),
|
||||
maxThreads: -1,
|
||||
maxCPU: -1,
|
||||
oldMaxThreads: -1,
|
||||
oldMaxCPU: -1,
|
||||
updatingServerMap: map[int64]*serverconfigs.ServerConfig{},
|
||||
}
|
||||
}
|
||||
@@ -190,7 +202,7 @@ func (this *Node) Start() {
|
||||
}
|
||||
}
|
||||
sharedNodeConfig = nodeConfig
|
||||
this.onReload(nodeConfig)
|
||||
this.onReload(nodeConfig, true)
|
||||
|
||||
// 发送事件
|
||||
events.Notify(events.EventLoaded)
|
||||
@@ -311,8 +323,9 @@ func (this *Node) loop() error {
|
||||
return errors.New("create rpc client failed: " + err.Error())
|
||||
}
|
||||
|
||||
var nodeCtx = rpcClient.Context()
|
||||
tasksResp, err := rpcClient.NodeTaskRPC.FindNodeTasks(nodeCtx, &pb.FindNodeTasksRequest{})
|
||||
tasksResp, err := rpcClient.NodeTaskRPC.FindNodeTasks(rpcClient.Context(), &pb.FindNodeTasksRequest{
|
||||
Version: this.lastTaskVersion,
|
||||
})
|
||||
if err != nil {
|
||||
if rpc.IsConnError(err) && !Tea.IsTesting() {
|
||||
return nil
|
||||
@@ -320,15 +333,18 @@ func (this *Node) loop() error {
|
||||
return errors.New("read node tasks failed: " + err.Error())
|
||||
}
|
||||
for _, task := range tasksResp.NodeTasks {
|
||||
err := this.execTask(rpcClient, nodeCtx, task)
|
||||
this.finishTask(task.Id, err)
|
||||
err := this.execTask(rpcClient, task)
|
||||
if !this.finishTask(task.Id, task.Version, err) {
|
||||
// 防止失败的任务无法重试
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 执行任务
|
||||
func (this *Node) execTask(rpcClient *rpc.RPCClient, nodeCtx context.Context, task *pb.NodeTask) error {
|
||||
func (this *Node) execTask(rpcClient *rpc.RPCClient, task *pb.NodeTask) error {
|
||||
switch task.Type {
|
||||
case "ipItemChanged":
|
||||
// 防止阻塞
|
||||
@@ -358,7 +374,7 @@ func (this *Node) execTask(rpcClient *rpc.RPCClient, nodeCtx context.Context, ta
|
||||
return errors.New("reload common scripts failed: " + err.Error())
|
||||
}
|
||||
case "nodeLevelChanged":
|
||||
levelInfoResp, err := rpcClient.NodeRPC.FindNodeLevelInfo(nodeCtx, &pb.FindNodeLevelInfoRequest{})
|
||||
levelInfoResp, err := rpcClient.NodeRPC.FindNodeLevelInfo(rpcClient.Context(), &pb.FindNodeLevelInfoRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -379,7 +395,7 @@ func (this *Node) execTask(rpcClient *rpc.RPCClient, nodeCtx context.Context, ta
|
||||
sharedNodeConfig.ParentNodes = parentNodes
|
||||
}
|
||||
case "ddosProtectionChanged":
|
||||
resp, err := rpcClient.NodeRPC.FindNodeDDoSProtection(nodeCtx, &pb.FindNodeDDoSProtectionRequest{})
|
||||
resp, err := rpcClient.NodeRPC.FindNodeDDoSProtection(rpcClient.Context(), &pb.FindNodeDDoSProtectionRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -407,7 +423,7 @@ func (this *Node) execTask(rpcClient *rpc.RPCClient, nodeCtx context.Context, ta
|
||||
return nil
|
||||
}
|
||||
case "globalServerConfigChanged":
|
||||
resp, err := rpcClient.NodeRPC.FindNodeGlobalServerConfig(nodeCtx, &pb.FindNodeGlobalServerConfigRequest{})
|
||||
resp, err := rpcClient.NodeRPC.FindNodeGlobalServerConfig(rpcClient.Context(), &pb.FindNodeGlobalServerConfigRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -430,7 +446,7 @@ func (this *Node) execTask(rpcClient *rpc.RPCClient, nodeCtx context.Context, ta
|
||||
}
|
||||
case "userServersStateChanged":
|
||||
if task.UserId > 0 {
|
||||
resp, err := rpcClient.UserRPC.CheckUserServersState(nodeCtx, &pb.CheckUserServersStateRequest{UserId: task.UserId})
|
||||
resp, err := rpcClient.UserRPC.CheckUserServersState(rpcClient.Context(), &pb.CheckUserServersStateRequest{UserId: task.UserId})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -452,39 +468,44 @@ func (this *Node) execTask(rpcClient *rpc.RPCClient, nodeCtx context.Context, ta
|
||||
}
|
||||
|
||||
// 标记任务完成
|
||||
func (this *Node) finishTask(taskId int64, err error) {
|
||||
func (this *Node) finishTask(taskId int64, taskVersion int64, taskErr error) (success bool) {
|
||||
if taskId <= 0 {
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
logs.Println("[NODE]", "create rpc client failed: "+err.Error())
|
||||
return
|
||||
remotelogs.Debug("NODE", "create rpc client failed: "+err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
var nodeCtx = rpcClient.Context()
|
||||
var isOk = taskErr == nil
|
||||
if isOk && taskVersion > this.lastTaskVersion {
|
||||
this.lastTaskVersion = taskVersion
|
||||
}
|
||||
|
||||
var isOk = err == nil
|
||||
var errMsg = ""
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
if taskErr != nil {
|
||||
errMsg = taskErr.Error()
|
||||
}
|
||||
|
||||
_, err = rpcClient.NodeTaskRPC.ReportNodeTaskDone(nodeCtx, &pb.ReportNodeTaskDoneRequest{
|
||||
_, err = rpcClient.NodeTaskRPC.ReportNodeTaskDone(rpcClient.Context(), &pb.ReportNodeTaskDoneRequest{
|
||||
NodeTaskId: taskId,
|
||||
IsOk: isOk,
|
||||
Error: errMsg,
|
||||
})
|
||||
success = err == nil
|
||||
|
||||
if err != nil {
|
||||
// 不需要上报到服务中心
|
||||
// 连接错误不需要上报到服务中心
|
||||
if rpc.IsConnError(err) {
|
||||
logs.Println("[NODE]", "report task done failed: "+err.Error())
|
||||
remotelogs.Debug("NODE", "report task done failed: "+err.Error())
|
||||
} else {
|
||||
remotelogs.Error("NODE", "report task done failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
// 读取API配置
|
||||
@@ -515,10 +536,8 @@ func (this *Node) syncConfig(taskVersion int64) error {
|
||||
}
|
||||
|
||||
// 获取同步任务
|
||||
var nodeCtx = rpcClient.Context()
|
||||
|
||||
// TODO 这里考虑只同步版本号有变更的
|
||||
configResp, err := rpcClient.NodeRPC.FindCurrentNodeConfig(nodeCtx, &pb.FindCurrentNodeConfigRequest{
|
||||
configResp, err := rpcClient.NodeRPC.FindCurrentNodeConfig(rpcClient.Context(), &pb.FindCurrentNodeConfigRequest{
|
||||
Version: -1, // 更新所有版本
|
||||
Compress: true,
|
||||
NodeTaskVersion: taskVersion,
|
||||
@@ -589,7 +608,7 @@ func (this *Node) syncConfig(taskVersion int64) error {
|
||||
remotelogs.Println("NODE", "loading config ...")
|
||||
}
|
||||
|
||||
this.onReload(nodeConfig)
|
||||
this.onReload(nodeConfig, true)
|
||||
|
||||
// 发送事件
|
||||
events.Notify(events.EventReload)
|
||||
@@ -720,12 +739,12 @@ func (this *Node) checkClusterConfig() error {
|
||||
return err
|
||||
}
|
||||
|
||||
logs.Println("[NODE]registering node to cluster ...")
|
||||
remotelogs.Debug("NODE", "registering node to cluster ...")
|
||||
resp, err := rpcClient.NodeRPC.RegisterClusterNode(rpcClient.ClusterContext(config.ClusterId, config.Secret), &pb.RegisterClusterNodeRequest{Name: HOSTNAME})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs.Println("[NODE]registered successfully")
|
||||
remotelogs.Debug("NODE", "registered successfully")
|
||||
|
||||
// 写入到配置文件中
|
||||
if len(resp.Endpoints) == 0 {
|
||||
@@ -733,8 +752,8 @@ func (this *Node) checkClusterConfig() error {
|
||||
}
|
||||
var apiConfig = &configs.APIConfig{
|
||||
RPC: struct {
|
||||
Endpoints []string `yaml:"endpoints"`
|
||||
DisableUpdate bool `yaml:"disableUpdate"`
|
||||
Endpoints []string `yaml:"endpoints" json:"endpoints"`
|
||||
DisableUpdate bool `yaml:"disableUpdate" json:"disableUpdate"`
|
||||
}{
|
||||
Endpoints: resp.Endpoints,
|
||||
DisableUpdate: false,
|
||||
@@ -742,12 +761,12 @@ func (this *Node) checkClusterConfig() error {
|
||||
NodeId: resp.UniqueId,
|
||||
Secret: resp.Secret,
|
||||
}
|
||||
logs.Println("[NODE]writing 'configs/api.yaml' ...")
|
||||
remotelogs.Debug("NODE", "writing 'configs/api.yaml' ...")
|
||||
err = apiConfig.WriteFile(Tea.ConfigFile("api.yaml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs.Println("[NODE]wrote 'configs/api.yaml' successfully")
|
||||
remotelogs.Debug("NODE", "wrote 'configs/api.yaml' successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -861,16 +880,56 @@ func (this *Node) listenSock() error {
|
||||
},
|
||||
})
|
||||
case "conns":
|
||||
var addrs = []string{}
|
||||
var connMaps = []maps.Map{}
|
||||
var connMap = conns.SharedMap.AllConns()
|
||||
for _, conn := range connMap {
|
||||
addrs = append(addrs, conn.RemoteAddr().String())
|
||||
var createdAt int64
|
||||
var lastReadAt int64
|
||||
var lastWriteAt int64
|
||||
var lastErrString = ""
|
||||
clientConn, ok := conn.(*ClientConn)
|
||||
if ok {
|
||||
createdAt = clientConn.CreatedAt()
|
||||
lastReadAt = clientConn.LastReadAt()
|
||||
lastWriteAt = clientConn.LastWriteAt()
|
||||
|
||||
var lastErr = clientConn.LastErr()
|
||||
if lastErr != nil {
|
||||
lastErrString = lastErr.Error()
|
||||
}
|
||||
}
|
||||
var age int64 = -1
|
||||
var lastReadAge int64 = -1
|
||||
var lastWriteAge int64 = -1
|
||||
var currentTime = time.Now().Unix()
|
||||
if createdAt > 0 {
|
||||
age = currentTime - createdAt
|
||||
}
|
||||
if lastReadAt > 0 {
|
||||
lastReadAge = currentTime - lastReadAt
|
||||
}
|
||||
if lastWriteAt > 0 {
|
||||
lastWriteAge = currentTime - lastWriteAt
|
||||
}
|
||||
|
||||
connMaps = append(connMaps, maps.Map{
|
||||
"addr": conn.RemoteAddr().String(),
|
||||
"age": age,
|
||||
"readAge": lastReadAge,
|
||||
"writeAge": lastWriteAge,
|
||||
"lastErr": lastErrString,
|
||||
})
|
||||
}
|
||||
sort.Slice(connMaps, func(i, j int) bool {
|
||||
var m1 = connMaps[i]
|
||||
var m2 = connMaps[j]
|
||||
return m1.GetInt64("age") < m2.GetInt64("age")
|
||||
})
|
||||
|
||||
_ = cmd.Reply(&gosock.Command{
|
||||
Params: map[string]interface{}{
|
||||
"addrs": addrs,
|
||||
"total": len(addrs),
|
||||
"conns": connMaps,
|
||||
"total": len(connMaps),
|
||||
},
|
||||
})
|
||||
case "dropIP":
|
||||
@@ -902,6 +961,11 @@ func (this *Node) listenSock() error {
|
||||
} else {
|
||||
_ = cmd.ReplyOk()
|
||||
}
|
||||
case "closeIP":
|
||||
var m = maps.NewMap(cmd.Params)
|
||||
var ip = m.GetString("ip")
|
||||
conns.SharedMap.CloseIPConns(ip)
|
||||
_ = cmd.ReplyOk()
|
||||
case "removeIP":
|
||||
var m = maps.NewMap(cmd.Params)
|
||||
var ip = m.GetString("ip")
|
||||
@@ -952,12 +1016,12 @@ func (this *Node) listenSock() error {
|
||||
|
||||
err := this.sock.Listen()
|
||||
if err != nil {
|
||||
logs.Println("NODE", err.Error())
|
||||
remotelogs.Debug("NODE", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
events.OnKey(events.EventQuit, this, func() {
|
||||
remotelogs.Println("NODE", "quit unix sock")
|
||||
remotelogs.Debug("NODE", "quit unix sock")
|
||||
_ = this.sock.Close()
|
||||
})
|
||||
|
||||
@@ -965,97 +1029,165 @@ func (this *Node) listenSock() error {
|
||||
}
|
||||
|
||||
// 重载配置调用
|
||||
func (this *Node) onReload(config *nodeconfigs.NodeConfig) {
|
||||
func (this *Node) onReload(config *nodeconfigs.NodeConfig, reloadAll bool) {
|
||||
nodeconfigs.ResetNodeConfig(config)
|
||||
sharedNodeConfig = config
|
||||
|
||||
// 缓存策略
|
||||
caches.SharedManager.MaxDiskCapacity = config.MaxCacheDiskCapacity
|
||||
caches.SharedManager.MaxMemoryCapacity = config.MaxCacheMemoryCapacity
|
||||
caches.SharedManager.DiskDir = config.CacheDiskDir
|
||||
if len(config.HTTPCachePolicies) > 0 {
|
||||
caches.SharedManager.UpdatePolicies(config.HTTPCachePolicies)
|
||||
} else {
|
||||
caches.SharedManager.UpdatePolicies([]*serverconfigs.HTTPCachePolicy{})
|
||||
if reloadAll {
|
||||
// 缓存策略
|
||||
var subDirs = config.CacheDiskSubDirs
|
||||
for _, subDir := range subDirs {
|
||||
subDir.Path = filepath.Clean(subDir.Path)
|
||||
}
|
||||
if len(subDirs) > 0 {
|
||||
sort.Slice(subDirs, func(i, j int) bool {
|
||||
return subDirs[i].Path < subDirs[j].Path
|
||||
})
|
||||
}
|
||||
|
||||
var cachePoliciesChanged = !jsonutils.Equal(caches.SharedManager.MaxDiskCapacity, config.MaxCacheDiskCapacity) ||
|
||||
!jsonutils.Equal(caches.SharedManager.MaxMemoryCapacity, config.MaxCacheMemoryCapacity) ||
|
||||
!jsonutils.Equal(caches.SharedManager.MainDiskDir, config.CacheDiskDir) ||
|
||||
!jsonutils.Equal(caches.SharedManager.SubDiskDirs, subDirs) ||
|
||||
!jsonutils.Equal(this.oldHTTPCachePolicies, config.HTTPCachePolicies)
|
||||
|
||||
caches.SharedManager.MaxDiskCapacity = config.MaxCacheDiskCapacity
|
||||
caches.SharedManager.MaxMemoryCapacity = config.MaxCacheMemoryCapacity
|
||||
caches.SharedManager.MainDiskDir = config.CacheDiskDir
|
||||
caches.SharedManager.SubDiskDirs = subDirs
|
||||
|
||||
if cachePoliciesChanged {
|
||||
// copy
|
||||
this.oldHTTPCachePolicies = []*serverconfigs.HTTPCachePolicy{}
|
||||
err := jsonutils.Copy(&this.oldHTTPCachePolicies, config.HTTPCachePolicies)
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "onReload: copy HTTPCachePolicies failed: "+err.Error())
|
||||
}
|
||||
|
||||
// update
|
||||
if len(config.HTTPCachePolicies) > 0 {
|
||||
caches.SharedManager.UpdatePolicies(config.HTTPCachePolicies)
|
||||
} else {
|
||||
caches.SharedManager.UpdatePolicies([]*serverconfigs.HTTPCachePolicy{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WAF策略
|
||||
waf.SharedWAFManager.UpdatePolicies(config.FindAllFirewallPolicies())
|
||||
iplibrary.SharedActionManager.UpdateActions(config.FirewallActions)
|
||||
|
||||
// 统计指标
|
||||
metrics.SharedManager.Update(config.MetricItems)
|
||||
|
||||
// max cpu
|
||||
if config.MaxCPU != this.maxCPU {
|
||||
if config.MaxCPU > 0 && config.MaxCPU < int32(runtime.NumCPU()) {
|
||||
runtime.GOMAXPROCS(int(config.MaxCPU))
|
||||
remotelogs.Println("NODE", "[CPU]set max cpu to '"+types.String(config.MaxCPU)+"'")
|
||||
} else {
|
||||
var threads = runtime.NumCPU() * 4
|
||||
runtime.GOMAXPROCS(threads)
|
||||
remotelogs.Println("NODE", "[CPU]set max cpu to '"+types.String(threads)+"'")
|
||||
}
|
||||
|
||||
this.maxCPU = config.MaxCPU
|
||||
}
|
||||
|
||||
// max threads
|
||||
if config.MaxThreads != this.maxThreads {
|
||||
if config.MaxThreads > 0 {
|
||||
debug.SetMaxThreads(config.MaxThreads)
|
||||
remotelogs.Println("NODE", "[THREADS]set max threads to '"+types.String(config.MaxThreads)+"'")
|
||||
} else {
|
||||
debug.SetMaxThreads(nodeconfigs.DefaultMaxThreads)
|
||||
remotelogs.Println("NODE", "[THREADS]set max threads to '"+types.String(nodeconfigs.DefaultMaxThreads)+"'")
|
||||
}
|
||||
this.maxThreads = config.MaxThreads
|
||||
}
|
||||
|
||||
// timezone
|
||||
var timeZone = config.TimeZone
|
||||
if len(timeZone) == 0 {
|
||||
timeZone = "Asia/Shanghai"
|
||||
}
|
||||
|
||||
if this.timezone != timeZone {
|
||||
location, err := time.LoadLocation(timeZone)
|
||||
// 包含了服务里的WAF策略,所以需要整体更新
|
||||
var allFirewallPolicies = config.FindAllFirewallPolicies()
|
||||
if !jsonutils.Equal(allFirewallPolicies, this.oldHTTPFirewallPolicies) {
|
||||
// copy
|
||||
this.oldHTTPFirewallPolicies = []*firewallconfigs.HTTPFirewallPolicy{}
|
||||
err := jsonutils.Copy(&this.oldHTTPFirewallPolicies, allFirewallPolicies)
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "[TIMEZONE]change time zone failed: "+err.Error())
|
||||
return
|
||||
remotelogs.Error("NODE", "onReload: copy HTTPFirewallPolicies failed: "+err.Error())
|
||||
}
|
||||
|
||||
remotelogs.Println("NODE", "[TIMEZONE]change time zone to '"+timeZone+"'")
|
||||
time.Local = location
|
||||
this.timezone = timeZone
|
||||
// update
|
||||
waf.SharedWAFManager.UpdatePolicies(allFirewallPolicies)
|
||||
}
|
||||
|
||||
// product information
|
||||
if config.ProductConfig != nil {
|
||||
teaconst.GlobalProductName = config.ProductConfig.Name
|
||||
}
|
||||
if reloadAll {
|
||||
if !jsonutils.Equal(config.FirewallActions, this.oldFirewallActions) {
|
||||
// copy
|
||||
this.oldFirewallActions = []*firewallconfigs.FirewallActionConfig{}
|
||||
err := jsonutils.Copy(&this.oldFirewallActions, config.FirewallActions)
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "onReload: copy FirewallActionConfigs failed: "+err.Error())
|
||||
}
|
||||
|
||||
// DNS resolver
|
||||
if config.DNSResolver != nil {
|
||||
var err error
|
||||
switch config.DNSResolver.Type {
|
||||
case nodeconfigs.DNSResolverTypeGoNative:
|
||||
err = os.Setenv("GODEBUG", "netdns=go")
|
||||
case nodeconfigs.DNSResolverTypeCGO:
|
||||
err = os.Setenv("GODEBUG", "netdns=cgo")
|
||||
default:
|
||||
// update
|
||||
iplibrary.SharedActionManager.UpdateActions(config.FirewallActions)
|
||||
}
|
||||
|
||||
// 统计指标
|
||||
if !jsonutils.Equal(this.oldMetricItems, config.MetricItems) {
|
||||
// copy
|
||||
this.oldMetricItems = []*serverconfigs.MetricItemConfig{}
|
||||
err := jsonutils.Copy(&this.oldMetricItems, config.MetricItems)
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "onReload: copy MetricItemConfigs failed: "+err.Error())
|
||||
}
|
||||
|
||||
// update
|
||||
metrics.SharedManager.Update(config.MetricItems)
|
||||
}
|
||||
|
||||
// max cpu
|
||||
if config.MaxCPU != this.oldMaxCPU {
|
||||
if config.MaxCPU > 0 && config.MaxCPU < int32(runtime.NumCPU()) {
|
||||
runtime.GOMAXPROCS(int(config.MaxCPU))
|
||||
remotelogs.Println("NODE", "[CPU]set max cpu to '"+types.String(config.MaxCPU)+"'")
|
||||
} else {
|
||||
var threads = runtime.NumCPU() * 4
|
||||
runtime.GOMAXPROCS(threads)
|
||||
remotelogs.Println("NODE", "[CPU]set max cpu to '"+types.String(threads)+"'")
|
||||
}
|
||||
|
||||
this.oldMaxCPU = config.MaxCPU
|
||||
}
|
||||
|
||||
// max threads
|
||||
if config.MaxThreads != this.oldMaxThreads {
|
||||
if config.MaxThreads > 0 {
|
||||
debug.SetMaxThreads(config.MaxThreads)
|
||||
remotelogs.Println("NODE", "[THREADS]set max threads to '"+types.String(config.MaxThreads)+"'")
|
||||
} else {
|
||||
debug.SetMaxThreads(nodeconfigs.DefaultMaxThreads)
|
||||
remotelogs.Println("NODE", "[THREADS]set max threads to '"+types.String(nodeconfigs.DefaultMaxThreads)+"'")
|
||||
}
|
||||
this.oldMaxThreads = config.MaxThreads
|
||||
}
|
||||
|
||||
// timezone
|
||||
var timeZone = config.TimeZone
|
||||
if len(timeZone) == 0 {
|
||||
timeZone = "Asia/Shanghai"
|
||||
}
|
||||
|
||||
if this.oldTimezone != timeZone {
|
||||
location, err := time.LoadLocation(timeZone)
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "[TIMEZONE]change time zone failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
remotelogs.Println("NODE", "[TIMEZONE]change time zone to '"+timeZone+"'")
|
||||
time.Local = location
|
||||
this.oldTimezone = timeZone
|
||||
}
|
||||
|
||||
// product information
|
||||
if config.ProductConfig != nil {
|
||||
teaconst.GlobalProductName = config.ProductConfig.Name
|
||||
}
|
||||
|
||||
// DNS resolver
|
||||
if config.DNSResolver != nil {
|
||||
var err error
|
||||
switch config.DNSResolver.Type {
|
||||
case nodeconfigs.DNSResolverTypeGoNative:
|
||||
err = os.Setenv("GODEBUG", "netdns=go")
|
||||
case nodeconfigs.DNSResolverTypeCGO:
|
||||
err = os.Setenv("GODEBUG", "netdns=cgo")
|
||||
default:
|
||||
// 默认使用go原生
|
||||
err = os.Setenv("GODEBUG", "netdns=go")
|
||||
}
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "[DNS_RESOLVER]set env failed: "+err.Error())
|
||||
}
|
||||
} else {
|
||||
// 默认使用go原生
|
||||
err = os.Setenv("GODEBUG", "netdns=go")
|
||||
}
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "[DNS_RESOLVER]set env failed: "+err.Error())
|
||||
}
|
||||
} else {
|
||||
// 默认使用go原生
|
||||
err := os.Setenv("GODEBUG", "netdns=go")
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "[DNS_RESOLVER]set env failed: "+err.Error())
|
||||
err := os.Setenv("GODEBUG", "netdns=go")
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "[DNS_RESOLVER]set env failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// API Node地址,这里不限制是否为空,因为在为空时仍然要有对应的处理
|
||||
this.changeAPINodeAddrs(config.APINodeAddrs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1091,7 +1223,7 @@ func (this *Node) reloadServer() {
|
||||
}
|
||||
}
|
||||
|
||||
this.onReload(newNodeConfig)
|
||||
this.onReload(newNodeConfig, false)
|
||||
|
||||
err = sharedListenerManager.Start(newNodeConfig)
|
||||
if err != nil {
|
||||
@@ -1100,6 +1232,7 @@ func (this *Node) reloadServer() {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查硬盘
|
||||
func (this *Node) checkDisk() {
|
||||
if runtime.GOOS != "linux" {
|
||||
return
|
||||
@@ -1120,3 +1253,69 @@ func (this *Node) checkDisk() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查API节点地址
|
||||
func (this *Node) changeAPINodeAddrs(apiNodeAddrs []*serverconfigs.NetworkAddressConfig) {
|
||||
var addrs = []string{}
|
||||
for _, addr := range apiNodeAddrs {
|
||||
err := addr.Init()
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "changeAPINodeAddrs: validate api node address '"+configutils.QuoteIP(addr.Host)+":"+addr.PortRange+"' failed: "+err.Error())
|
||||
} else {
|
||||
addrs = append(addrs, addr.FullAddresses()...)
|
||||
}
|
||||
}
|
||||
sort.Strings(addrs)
|
||||
|
||||
if utils.EqualStrings(this.lastAPINodeAddrs, addrs) {
|
||||
return
|
||||
}
|
||||
|
||||
this.lastAPINodeAddrs = addrs
|
||||
|
||||
config, err := configs.LoadAPIConfig()
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "changeAPINodeAddrs: "+err.Error())
|
||||
return
|
||||
}
|
||||
if config == nil {
|
||||
return
|
||||
}
|
||||
var oldEndpoints = config.RPC.Endpoints
|
||||
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(addrs) > 0 {
|
||||
this.lastAPINodeVersion++
|
||||
var v = this.lastAPINodeVersion
|
||||
|
||||
// 异步检测,防止阻塞
|
||||
go func(v int64) {
|
||||
// 测试新的API节点地址
|
||||
if rpcClient.TestEndpoints(addrs) {
|
||||
config.RPC.Endpoints = addrs
|
||||
} else {
|
||||
config.RPC.Endpoints = oldEndpoints
|
||||
this.lastAPINodeAddrs = nil // 恢复为空,以便于下次更新重试
|
||||
}
|
||||
|
||||
// 检查测试中间有无新的变更
|
||||
if v != this.lastAPINodeVersion {
|
||||
return
|
||||
}
|
||||
|
||||
err = rpcClient.UpdateConfig(config)
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "changeAPINodeAddrs: update rpc config failed: "+err.Error())
|
||||
}
|
||||
}(v)
|
||||
return
|
||||
}
|
||||
|
||||
err = rpcClient.UpdateConfig(config)
|
||||
if err != nil {
|
||||
remotelogs.Error("NODE", "changeAPINodeAddrs: update rpc config failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,15 @@ type NodeStatusExecutor struct {
|
||||
cpuLogicalCount int
|
||||
cpuPhysicalCount int
|
||||
|
||||
apiCallStat *rpc.CallStat
|
||||
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
func NewNodeStatusExecutor() *NodeStatusExecutor {
|
||||
return &NodeStatusExecutor{
|
||||
ticker: time.NewTicker(30 * time.Second),
|
||||
ticker: time.NewTicker(30 * time.Second),
|
||||
apiCallStat: rpc.NewCallStat(10),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +81,11 @@ func (this *NodeStatusExecutor) update() {
|
||||
status.CacheTotalMemorySize = caches.SharedManager.TotalMemorySize()
|
||||
status.TrafficInBytes = teaconst.InTrafficBytes
|
||||
status.TrafficOutBytes = teaconst.OutTrafficBytes
|
||||
|
||||
apiSuccessPercent, apiAvgCostSeconds := this.apiCallStat.Sum()
|
||||
status.APISuccessPercent = apiSuccessPercent
|
||||
status.APIAvgCostSeconds = apiAvgCostSeconds
|
||||
|
||||
var localFirewall = firewalls.Firewall()
|
||||
if localFirewall != nil && !localFirewall.IsMock() {
|
||||
status.LocalFirewallName = localFirewall.Name()
|
||||
@@ -125,9 +133,13 @@ func (this *NodeStatusExecutor) update() {
|
||||
remotelogs.Error("NODE_STATUS", "failed to open rpc: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var before = time.Now()
|
||||
_, err = rpcClient.NodeRPC.UpdateNodeStatus(rpcClient.Context(), &pb.UpdateNodeStatusRequest{
|
||||
StatusJSON: jsonData,
|
||||
})
|
||||
var costSeconds = time.Since(before).Seconds()
|
||||
this.apiCallStat.Add(err == nil, costSeconds)
|
||||
if err != nil {
|
||||
if rpc.IsConnError(err) {
|
||||
remotelogs.Warn("NODE_STATUS", "rpc UpdateNodeStatus() failed: "+err.Error())
|
||||
@@ -140,7 +152,7 @@ func (this *NodeStatusExecutor) update() {
|
||||
|
||||
// 更新CPU
|
||||
func (this *NodeStatusExecutor) updateCPU(status *nodeconfigs.NodeStatus) {
|
||||
duration := time.Duration(0)
|
||||
var duration = time.Duration(0)
|
||||
if this.isFirstTime {
|
||||
duration = 100 * time.Millisecond
|
||||
}
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
package nodes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/configs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/events"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/trackers"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -64,6 +56,9 @@ func (this *SyncAPINodesTask) Stop() {
|
||||
}
|
||||
|
||||
func (this *SyncAPINodesTask) Loop() error {
|
||||
// 如果有节点定制的API节点地址
|
||||
var hasCustomizedAPINodeAddrs = sharedNodeConfig != nil && len(sharedNodeConfig.APINodeAddrs) > 0
|
||||
|
||||
config, err := configs.LoadAPIConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -96,21 +91,25 @@ func (this *SyncAPINodesTask) Loop() error {
|
||||
}
|
||||
|
||||
// 和现有的对比
|
||||
if this.isSame(newEndpoints, config.RPC.Endpoints) {
|
||||
if utils.EqualStrings(newEndpoints, config.RPC.Endpoints) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 测试是否有API节点可用
|
||||
var hasOk = this.testEndpoints(newEndpoints)
|
||||
var hasOk = rpcClient.TestEndpoints(newEndpoints)
|
||||
if !hasOk {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 修改RPC对象配置
|
||||
config.RPC.Endpoints = newEndpoints
|
||||
err = rpcClient.UpdateConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
// 更新当前RPC
|
||||
if !hasCustomizedAPINodeAddrs {
|
||||
err = rpcClient.UpdateConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到文件
|
||||
@@ -121,53 +120,3 @@ func (this *SyncAPINodesTask) Loop() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *SyncAPINodesTask) isSame(endpoints1 []string, endpoints2 []string) bool {
|
||||
sort.Strings(endpoints1)
|
||||
sort.Strings(endpoints2)
|
||||
return strings.Join(endpoints1, "&") == strings.Join(endpoints2, "&")
|
||||
}
|
||||
|
||||
func (this *SyncAPINodesTask) testEndpoints(endpoints []string) bool {
|
||||
if len(endpoints) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var wg = sync.WaitGroup{}
|
||||
wg.Add(len(endpoints))
|
||||
|
||||
var ok = false
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
go func(endpoint string) {
|
||||
defer wg.Done()
|
||||
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer func() {
|
||||
cancelFunc()
|
||||
}()
|
||||
var conn *grpc.ClientConn
|
||||
if u.Scheme == "http" {
|
||||
conn, err = grpc.DialContext(ctx, u.Host, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
|
||||
} else if u.Scheme == "https" {
|
||||
conn, err = grpc.DialContext(ctx, u.Host, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})), grpc.WithBlock())
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
ok = true
|
||||
}(endpoint)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ func (this *UserManager) UpdateUserServersIsEnabled(userId int64, isEnabled bool
|
||||
}
|
||||
|
||||
func (this *UserManager) CheckUserServersIsEnabled(userId int64) (isEnabled bool) {
|
||||
if userId <= 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
this.locker.RLock()
|
||||
u, ok := this.userMap[userId]
|
||||
if ok {
|
||||
|
||||
@@ -57,6 +57,79 @@ func TestRegexp_ParseKeywords(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexp_Special(t *testing.T) {
|
||||
var unescape = func(v string) string {
|
||||
//replace urlencoded characters
|
||||
|
||||
var chars = [][2]string{
|
||||
{`\s`, `(\s|%09|%0A|\+)`},
|
||||
{`\(`, `(\(|%28)`},
|
||||
{`=`, `(=|%3D)`},
|
||||
{`<`, `(<|%3C)`},
|
||||
{`\*`, `(\*|%2A)`},
|
||||
{`\\`, `(\\|%2F)`},
|
||||
{`!`, `(!|%21)`},
|
||||
{`/`, `(/|%2F)`},
|
||||
{`;`, `(;|%3B)`},
|
||||
{`\+`, `(\+|%20)`},
|
||||
}
|
||||
|
||||
for _, c := range chars {
|
||||
if !strings.Contains(v, c[0]) {
|
||||
continue
|
||||
}
|
||||
var pieces = strings.Split(v, c[0])
|
||||
|
||||
// 修复piece中错误的\
|
||||
for pieceIndex, piece := range pieces {
|
||||
var l = len(piece)
|
||||
if l == 0 {
|
||||
continue
|
||||
}
|
||||
if piece[l-1] != '\\' {
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算\的数量
|
||||
var countBackSlashes = 0
|
||||
for i := l - 1; i >= 0; i-- {
|
||||
if piece[i] == '\\' {
|
||||
countBackSlashes++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if countBackSlashes%2 == 1 {
|
||||
// 去掉最后一个
|
||||
pieces[pieceIndex] = piece[:len(piece)-1]
|
||||
}
|
||||
}
|
||||
|
||||
v = strings.Join(pieces, c[1])
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
for _, s := range []string{
|
||||
`\\s`,
|
||||
`\s\W`,
|
||||
`aaaa/\W`,
|
||||
`aaaa\/\W`,
|
||||
`aaaa\=\W`,
|
||||
`aaaa\\=\W`,
|
||||
`aaaa\\\=\W`,
|
||||
`aaaa\\\\=\W`,
|
||||
} {
|
||||
var es = unescape(s)
|
||||
t.Log(s, "=>", es)
|
||||
_, err := re.Compile(es)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexp_ParseKeywords2(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var logChan = make(chan *pb.NodeLog, 1024)
|
||||
var logChan = make(chan *pb.NodeLog, 64) // 队列数量不需要太长,因为日志通常仅仅为调试用
|
||||
|
||||
func init() {
|
||||
// 定期上传日志
|
||||
|
||||
71
internal/rpc/call_stat.go
Normal file
71
internal/rpc/call_stat.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type callStatItem struct {
|
||||
ok bool
|
||||
costSeconds float64
|
||||
}
|
||||
|
||||
type CallStat struct {
|
||||
size int
|
||||
items []*callStatItem
|
||||
|
||||
locker sync.Mutex
|
||||
}
|
||||
|
||||
func NewCallStat(size int) *CallStat {
|
||||
return &CallStat{
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *CallStat) Add(ok bool, costSeconds float64) {
|
||||
var size = this.size
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
|
||||
this.locker.Lock()
|
||||
this.items = append(this.items, &callStatItem{
|
||||
ok: ok,
|
||||
costSeconds: costSeconds,
|
||||
})
|
||||
if len(this.items) > size {
|
||||
this.items = this.items[1:]
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *CallStat) Sum() (successPercent float64, avgCostSeconds float64) {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
var size = this.size
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
|
||||
var totalItems = len(this.items)
|
||||
if totalItems <= size/2 /** 低于一半的采样率,不计入统计 **/ {
|
||||
successPercent = 100
|
||||
return
|
||||
}
|
||||
|
||||
var totalOkItems = 0
|
||||
var totalCostSeconds float64
|
||||
for _, item := range this.items {
|
||||
if item.ok {
|
||||
totalOkItems++
|
||||
}
|
||||
totalCostSeconds += item.costSeconds
|
||||
}
|
||||
successPercent = float64(totalOkItems) * 100 / float64(totalItems)
|
||||
avgCostSeconds = totalCostSeconds / float64(totalItems)
|
||||
|
||||
return
|
||||
}
|
||||
19
internal/rpc/call_stat_test.go
Normal file
19
internal/rpc/call_stat_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package rpc_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewCallStat(t *testing.T) {
|
||||
var stat = rpc.NewCallStat(10)
|
||||
stat.Add(true, 1)
|
||||
stat.Add(true, 2)
|
||||
stat.Add(true, 3)
|
||||
stat.Add(false, 4)
|
||||
stat.Add(true, 0)
|
||||
stat.Add(true, 1)
|
||||
t.Log(stat.Sum())
|
||||
}
|
||||
@@ -50,6 +50,7 @@ type RPCClient struct {
|
||||
SSLCertRPC pb.SSLCertServiceClient
|
||||
ScriptRPC pb.ScriptServiceClient
|
||||
UserRPC pb.UserServiceClient
|
||||
ClientAgentIPRPC pb.ClientAgentIPServiceClient
|
||||
}
|
||||
|
||||
func NewRPCClient(apiConfig *configs.APIConfig) (*RPCClient, error) {
|
||||
@@ -83,6 +84,7 @@ func NewRPCClient(apiConfig *configs.APIConfig) (*RPCClient, error) {
|
||||
client.SSLCertRPC = pb.NewSSLCertServiceClient(client)
|
||||
client.ScriptRPC = pb.NewScriptServiceClient(client)
|
||||
client.UserRPC = pb.NewUserServiceClient(client)
|
||||
client.ClientAgentIPRPC = pb.NewClientAgentIPServiceClient(client)
|
||||
|
||||
err := client.init()
|
||||
if err != nil {
|
||||
@@ -94,7 +96,6 @@ func NewRPCClient(apiConfig *configs.APIConfig) (*RPCClient, error) {
|
||||
|
||||
// Context 节点上下文信息
|
||||
func (this *RPCClient) Context() context.Context {
|
||||
var ctx = context.Background()
|
||||
var m = maps.Map{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"type": "node",
|
||||
@@ -111,6 +112,8 @@ func (this *RPCClient) Context() context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
var token = base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
var ctx = context.Background()
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "nodeId", this.apiConfig.NodeId, "token", token)
|
||||
return ctx
|
||||
}
|
||||
@@ -159,6 +162,64 @@ func (this *RPCClient) UpdateConfig(config *configs.APIConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// TestEndpoints 测试Endpoints是否可用
|
||||
func (this *RPCClient) TestEndpoints(endpoints []string) bool {
|
||||
if len(endpoints) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var wg = sync.WaitGroup{}
|
||||
wg.Add(len(endpoints))
|
||||
|
||||
var ok = false
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
go func(endpoint string) {
|
||||
defer wg.Done()
|
||||
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer func() {
|
||||
cancelFunc()
|
||||
}()
|
||||
var conn *grpc.ClientConn
|
||||
if u.Scheme == "http" {
|
||||
conn, err = grpc.DialContext(ctx, u.Host, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
|
||||
} else if u.Scheme == "https" {
|
||||
conn, err = grpc.DialContext(ctx, u.Host, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})), grpc.WithBlock())
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
var pingService = pb.NewPingServiceClient(conn)
|
||||
_, err = pingService.Ping(this.Context(), &pb.PingRequest{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
}(endpoint)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// 初始化
|
||||
func (this *RPCClient) init() error {
|
||||
// 重新连接
|
||||
@@ -203,12 +264,18 @@ func (this *RPCClient) pickConn() *grpc.ClientConn {
|
||||
defer this.locker.Unlock()
|
||||
|
||||
// 检查连接状态
|
||||
if len(this.conns) > 0 {
|
||||
var availableConns = []*grpc.ClientConn{}
|
||||
var countConns = len(this.conns)
|
||||
if countConns > 0 {
|
||||
if countConns == 1 {
|
||||
return this.conns[0]
|
||||
}
|
||||
|
||||
for _, stateArray := range [][2]connectivity.State{
|
||||
{connectivity.Ready, connectivity.Idle}, // 优先Ready和Idle
|
||||
{connectivity.Connecting, connectivity.Connecting},
|
||||
{connectivity.TransientFailure, connectivity.TransientFailure},
|
||||
} {
|
||||
var availableConns = []*grpc.ClientConn{}
|
||||
for _, conn := range this.conns {
|
||||
var state = conn.GetState()
|
||||
if state == stateArray[0] || state == stateArray[1] {
|
||||
@@ -219,26 +286,6 @@ func (this *RPCClient) pickConn() *grpc.ClientConn {
|
||||
return this.randConn(availableConns)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availableConns) > 0 {
|
||||
return this.randConn(availableConns)
|
||||
}
|
||||
|
||||
// 关闭
|
||||
for _, conn := range this.conns {
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 重新初始化
|
||||
err := this.init()
|
||||
if err != nil {
|
||||
// 错误提示已经在构造对象时打印过,所以这里不再重复打印
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(this.conns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return this.randConn(this.conns)
|
||||
@@ -247,14 +294,15 @@ func (this *RPCClient) pickConn() *grpc.ClientConn {
|
||||
func (this *RPCClient) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
|
||||
var conn = this.pickConn()
|
||||
if conn == nil {
|
||||
return errors.New("can not get available grpc connection")
|
||||
return errors.New("could not get available grpc connection")
|
||||
}
|
||||
return conn.Invoke(ctx, method, args, reply, opts...)
|
||||
}
|
||||
|
||||
func (this *RPCClient) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
var conn = this.pickConn()
|
||||
if conn == nil {
|
||||
return nil, errors.New("can not get available grpc connection")
|
||||
return nil, errors.New("could not get available grpc connection")
|
||||
}
|
||||
return conn.NewStream(ctx, desc, method, opts...)
|
||||
}
|
||||
|
||||
62
internal/rpc/rpc_test.go
Normal file
62
internal/rpc/rpc_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package rpc_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRPCConcurrentCall(t *testing.T) {
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var before = time.Now()
|
||||
defer func() {
|
||||
t.Log("cost:", time.Since(before).Seconds()*1000, "ms")
|
||||
}()
|
||||
|
||||
var concurrent = 3
|
||||
|
||||
var wg = sync.WaitGroup{}
|
||||
wg.Add(concurrent)
|
||||
|
||||
for i := 0; i < concurrent; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
_, err = rpcClient.NodeRPC.FindCurrentNodeConfig(rpcClient.Context(), &pb.FindCurrentNodeConfigRequest{})
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestRPC_Retry(t *testing.T) {
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var ticker = time.NewTicker(1 * time.Second)
|
||||
for range ticker.C {
|
||||
go func() {
|
||||
_, err = rpcClient.NodeRPC.FindCurrentNodeConfig(rpcClient.Context(), &pb.FindCurrentNodeConfigRequest{})
|
||||
if err != nil {
|
||||
t.Log(timeutil.Format("H:i:s"), err)
|
||||
} else {
|
||||
t.Log(timeutil.Format("H:i:s"), "success")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/trackers"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/agents"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
@@ -146,11 +147,16 @@ func (this *HTTPRequestStatManager) AddRemoteAddr(serverId int64, remoteAddr str
|
||||
}
|
||||
|
||||
// AddUserAgent 添加UserAgent
|
||||
func (this *HTTPRequestStatManager) AddUserAgent(serverId int64, userAgent string) {
|
||||
func (this *HTTPRequestStatManager) AddUserAgent(serverId int64, userAgent string, ip string) {
|
||||
if len(userAgent) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 是否包含一些知名Agent
|
||||
if len(userAgent) > 0 && len(ip) > 0 && agents.IsAgentFromUserAgent(userAgent) {
|
||||
agents.SharedQueue.Push(ip)
|
||||
}
|
||||
|
||||
select {
|
||||
case this.userAgentChan <- strconv.FormatInt(serverId, 10) + "@" + userAgent:
|
||||
default:
|
||||
|
||||
@@ -37,11 +37,11 @@ func TestHTTPRequestStatManager_Loop_Region(t *testing.T) {
|
||||
|
||||
func TestHTTPRequestStatManager_Loop_UserAgent(t *testing.T) {
|
||||
var manager = NewHTTPRequestStatManager()
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Macintosh; Intel Mac OS X 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76 Safari/537.36")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36", "")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36", "")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Macintosh; Intel Mac OS X 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76 Safari/537.36", "")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36", "")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", "")
|
||||
err := manager.Loop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -73,6 +73,15 @@ func (this *UserAgentParser) Parse(userAgent string) (result UserAgentParserResu
|
||||
result.BrowserName, result.BrowserVersion = this.parser.Browser()
|
||||
result.IsMobile = this.parser.Mobile()
|
||||
|
||||
// 忽略特殊字符
|
||||
if len(result.BrowserName) > 0 {
|
||||
for _, r := range result.BrowserName {
|
||||
if r == '$' || r == '"' || r == '\'' || r == '<' || r == '>' || r == ')' {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if this.cacheCursor == 0 {
|
||||
this.cacheMap1[userAgent] = result
|
||||
if len(this.cacheMap1) >= this.maxCacheItems {
|
||||
|
||||
39
internal/utils/agents/agent.go
Normal file
39
internal/utils/agents/agent.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
Code string
|
||||
Keywords []string // user agent keywords
|
||||
|
||||
suffixes []string // PTR suffixes
|
||||
reg *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewAgent(code string, suffixes []string, reg *regexp.Regexp, keywords []string) *Agent {
|
||||
return &Agent{
|
||||
Code: code,
|
||||
suffixes: suffixes,
|
||||
reg: reg,
|
||||
Keywords: keywords,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Agent) Match(ptr string) bool {
|
||||
if len(this.suffixes) > 0 {
|
||||
for _, suffix := range this.suffixes {
|
||||
if strings.HasSuffix(ptr, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if this.reg != nil {
|
||||
return this.reg.MatchString(ptr)
|
||||
}
|
||||
return false
|
||||
}
|
||||
9
internal/utils/agents/agent_ip.go
Normal file
9
internal/utils/agents/agent_ip.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents
|
||||
|
||||
type AgentIP struct {
|
||||
Id int64
|
||||
IP string
|
||||
AgentCode string
|
||||
}
|
||||
31
internal/utils/agents/agents.go
Normal file
31
internal/utils/agents/agents.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents
|
||||
|
||||
import "strings"
|
||||
|
||||
var AllAgents = []*Agent{
|
||||
NewAgent("baidu", []string{".baidu.com."}, nil, []string{"Baidu"}),
|
||||
NewAgent("google", []string{".googlebot.com."}, nil, []string{"Google"}),
|
||||
NewAgent("bing", []string{".search.msn.com."}, nil, []string{"bingbot"}),
|
||||
NewAgent("sogou", []string{".sogou.com."}, nil, []string{"Sogou"}),
|
||||
NewAgent("youdao", []string{".163.com."}, nil, []string{"Youdao"}),
|
||||
NewAgent("yahoo", []string{".yahoo.com."}, nil, []string{"Yahoo"}),
|
||||
NewAgent("bytedance", []string{".bytedance.com."}, nil, []string{"Bytespider"}),
|
||||
NewAgent("sm", []string{".sm.cn."}, nil, []string{"YisouSpider"}),
|
||||
NewAgent("yandex", []string{".yandex.com.", ".yndx.net."}, nil, []string{"Yandex"}),
|
||||
NewAgent("semrush", []string{".semrush.com."}, nil, []string{"SEMrush"}),
|
||||
}
|
||||
|
||||
func IsAgentFromUserAgent(userAgent string) bool {
|
||||
for _, agent := range AllAgents {
|
||||
if len(agent.Keywords) > 0 {
|
||||
for _, keyword := range agent.Keywords {
|
||||
if strings.Contains(userAgent, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
19
internal/utils/agents/agents_test.go
Normal file
19
internal/utils/agents/agents_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/agents"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsAgentFromUserAgent(t *testing.T) {
|
||||
t.Log(agents.IsAgentFromUserAgent("Mozilla/5.0 (Linux;u;Android 4.2.2;zh-cn;) AppleWebKit/534.46 (KHTML,like Gecko) Version/5.1 Mobile Safari/10600.6.3 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"))
|
||||
t.Log(agents.IsAgentFromUserAgent("Mozilla/5.0 (Linux;u;Android 4.2.2;zh-cn;)"))
|
||||
}
|
||||
|
||||
func BenchmarkIsAgentFromUserAgent(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
agents.IsAgentFromUserAgent("Mozilla/5.0 (Linux;u;Android 4.2.2;zh-cn;) AppleWebKit/534.46 (KHTML,like Gecko) Version/5.1 Mobile Safari/10600.6.3 (compatible; Yaho)")
|
||||
}
|
||||
}
|
||||
156
internal/utils/agents/db.go
Normal file
156
internal/utils/agents/db.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package agents
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/events"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
tableAgentIPs = "agentIPs"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
db *sql.DB
|
||||
path string
|
||||
|
||||
insertAgentIPStmt *sql.Stmt
|
||||
listAgentIPsStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func NewDB(path string) *DB {
|
||||
var db = &DB{path: path}
|
||||
|
||||
events.On(events.EventQuit, func() {
|
||||
_ = db.Close()
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (this *DB) Init() error {
|
||||
// 检查目录是否存在
|
||||
var dir = filepath.Dir(this.path)
|
||||
|
||||
_, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
err = os.MkdirAll(dir, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remotelogs.Println("DB", "create database dir '"+dir+"'")
|
||||
}
|
||||
|
||||
// TODO 思考 data.db 的数据安全性
|
||||
db, err := sql.Open("sqlite3", "file:"+this.path+"?cache=shared&mode=rwc&_journal_mode=WAL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
/**_, err = db.Exec("VACUUM")
|
||||
if err != nil {
|
||||
return err
|
||||
}**/
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS "` + tableAgentIPs + `" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"ip" varchar(64),
|
||||
"agentCode" varchar(128)
|
||||
);`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 预编译语句
|
||||
|
||||
// agent ip record statements
|
||||
this.insertAgentIPStmt, err = db.Prepare(`INSERT INTO "` + tableAgentIPs + `" ("id", "ip", "agentCode") VALUES (?, ?, ?)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
this.listAgentIPsStmt, err = db.Prepare(`SELECT "id", "ip", "agentCode" FROM "` + tableAgentIPs + `" ORDER BY "id" ASC LIMIT ? OFFSET ?`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
this.db = db
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *DB) InsertAgentIP(ipId int64, ip string, agentCode string) error {
|
||||
if this.db == nil {
|
||||
return errors.New("db should not be nil")
|
||||
}
|
||||
|
||||
this.log("InsertAgentIP", "id:", ipId, "ip:", ip, "agent:", agentCode)
|
||||
_, err := this.insertAgentIPStmt.Exec(ipId, ip, agentCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *DB) ListAgentIPs(offset int64, size int64) (agentIPs []*AgentIP, err error) {
|
||||
if this.db == nil {
|
||||
return nil, errors.New("db should not be nil")
|
||||
}
|
||||
rows, err := this.listAgentIPsStmt.Query(size, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = rows.Close()
|
||||
}()
|
||||
for rows.Next() {
|
||||
var agentIP = &AgentIP{}
|
||||
err = rows.Scan(&agentIP.Id, &agentIP.IP, &agentIP.AgentCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agentIPs = append(agentIPs, agentIP)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *DB) Close() error {
|
||||
if this.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, stmt := range []*sql.Stmt{
|
||||
this.insertAgentIPStmt,
|
||||
this.listAgentIPsStmt,
|
||||
} {
|
||||
if stmt != nil {
|
||||
_ = stmt.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return this.db.Close()
|
||||
}
|
||||
|
||||
// 打印日志
|
||||
func (this *DB) log(args ...any) {
|
||||
if !Tea.IsTesting() {
|
||||
return
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
args[0] = "[" + types.String(args[0]) + "]"
|
||||
log.Println(args...)
|
||||
}
|
||||
54
internal/utils/agents/ip_cache_map.go
Normal file
54
internal/utils/agents/ip_cache_map.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/zero"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type IPCacheMap struct {
|
||||
m map[string]zero.Zero
|
||||
list []string
|
||||
|
||||
locker sync.RWMutex
|
||||
maxLen int
|
||||
}
|
||||
|
||||
func NewIPCacheMap(maxLen int) *IPCacheMap {
|
||||
if maxLen <= 0 {
|
||||
maxLen = 65535
|
||||
}
|
||||
return &IPCacheMap{
|
||||
m: map[string]zero.Zero{},
|
||||
maxLen: maxLen,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *IPCacheMap) Add(ip string) {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
// 是否已经存在
|
||||
_, ok := this.m[ip]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 超出长度删除第一个
|
||||
if len(this.list) >= this.maxLen {
|
||||
delete(this.m, this.list[0])
|
||||
this.list = this.list[1:]
|
||||
}
|
||||
|
||||
// 加入新数据
|
||||
this.m[ip] = zero.Zero{}
|
||||
this.list = append(this.list, ip)
|
||||
}
|
||||
|
||||
func (this *IPCacheMap) Contains(ip string) bool {
|
||||
this.locker.RLock()
|
||||
defer this.locker.RUnlock()
|
||||
_, ok := this.m[ip]
|
||||
return ok
|
||||
}
|
||||
33
internal/utils/agents/ip_cache_map_test.go
Normal file
33
internal/utils/agents/ip_cache_map_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewIPCacheMap(t *testing.T) {
|
||||
var cacheMap = NewIPCacheMap(3)
|
||||
|
||||
t.Log("====")
|
||||
cacheMap.Add("1")
|
||||
cacheMap.Add("2")
|
||||
logs.PrintAsJSON(cacheMap.m, t)
|
||||
logs.PrintAsJSON(cacheMap.list, t)
|
||||
|
||||
t.Log("====")
|
||||
cacheMap.Add("3")
|
||||
logs.PrintAsJSON(cacheMap.m, t)
|
||||
logs.PrintAsJSON(cacheMap.list, t)
|
||||
|
||||
t.Log("====")
|
||||
cacheMap.Add("4")
|
||||
logs.PrintAsJSON(cacheMap.m, t)
|
||||
logs.PrintAsJSON(cacheMap.list, t)
|
||||
|
||||
t.Log("====")
|
||||
cacheMap.Add("3")
|
||||
logs.PrintAsJSON(cacheMap.m, t)
|
||||
logs.PrintAsJSON(cacheMap.list, t)
|
||||
}
|
||||
200
internal/utils/agents/manager.go
Normal file
200
internal/utils/agents/manager.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/events"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SharedManager = NewManager()
|
||||
|
||||
func init() {
|
||||
events.On(events.EventLoaded, func() {
|
||||
goman.New(func() {
|
||||
SharedManager.Start()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Manager Agent管理器
|
||||
type Manager struct {
|
||||
ipMap map[string]string // ip => agentCode
|
||||
locker sync.RWMutex
|
||||
|
||||
db *DB
|
||||
|
||||
lastId int64
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
ipMap: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Manager) SetDB(db *DB) {
|
||||
this.db = db
|
||||
}
|
||||
|
||||
func (this *Manager) Start() {
|
||||
remotelogs.Println("AGENT_MANAGER", "starting ...")
|
||||
|
||||
err := this.loadDB()
|
||||
if err != nil {
|
||||
remotelogs.Error("AGENT_MANAGER", "load database failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 从本地数据库中加载
|
||||
err = this.Load()
|
||||
if err != nil {
|
||||
remotelogs.Error("AGENT_MANAGER", "load failed: "+err.Error())
|
||||
}
|
||||
|
||||
// 先从API获取
|
||||
err = this.LoopAll()
|
||||
if err != nil {
|
||||
if rpc.IsConnError(err) {
|
||||
remotelogs.Debug("AGENT_MANAGER", "retrieve latest agent ip failed: "+err.Error())
|
||||
} else {
|
||||
remotelogs.Error("AGENT_MANAGER", "retrieve latest agent ip failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 定时获取
|
||||
var duration = 30 * time.Minute
|
||||
if Tea.IsTesting() {
|
||||
duration = 30 * time.Second
|
||||
}
|
||||
var ticker = time.NewTicker(duration)
|
||||
for range ticker.C {
|
||||
err = this.LoopAll()
|
||||
if err != nil {
|
||||
remotelogs.Error("AGENT_MANAGER", "retrieve latest agent ip failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Manager) Load() error {
|
||||
var offset int64 = 0
|
||||
var size int64 = 10000
|
||||
for {
|
||||
agentIPs, err := this.db.ListAgentIPs(offset, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(agentIPs) == 0 {
|
||||
break
|
||||
}
|
||||
for _, agentIP := range agentIPs {
|
||||
this.locker.Lock()
|
||||
this.ipMap[agentIP.IP] = agentIP.AgentCode
|
||||
this.locker.Unlock()
|
||||
|
||||
if agentIP.Id > this.lastId {
|
||||
this.lastId = agentIP.Id
|
||||
}
|
||||
}
|
||||
offset += size
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Manager) LoopAll() error {
|
||||
for {
|
||||
hasNext, err := this.Loop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasNext {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Loop 单次循环获取数据
|
||||
func (this *Manager) Loop() (hasNext bool, err error) {
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
ipsResp, err := rpcClient.ClientAgentIPRPC.ListClientAgentIPsAfterId(rpcClient.Context(), &pb.ListClientAgentIPsAfterIdRequest{
|
||||
Id: this.lastId,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(ipsResp.ClientAgentIPs) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
for _, agentIP := range ipsResp.ClientAgentIPs {
|
||||
if agentIP.ClientAgent == nil {
|
||||
// 设置ID
|
||||
if agentIP.Id > this.lastId {
|
||||
this.lastId = agentIP.Id
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// 写入到数据库
|
||||
err = this.db.InsertAgentIP(agentIP.Id, agentIP.Ip, agentIP.ClientAgent.Code)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 写入Map
|
||||
this.locker.Lock()
|
||||
this.ipMap[agentIP.Ip] = agentIP.ClientAgent.Code
|
||||
this.locker.Unlock()
|
||||
|
||||
// 设置ID
|
||||
if agentIP.Id > this.lastId {
|
||||
this.lastId = agentIP.Id
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// AddIP 添加记录
|
||||
func (this *Manager) AddIP(ip string, agentCode string) {
|
||||
this.locker.Lock()
|
||||
this.ipMap[ip] = agentCode
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
// LookupIP 查询IP所属Agent
|
||||
func (this *Manager) LookupIP(ip string) (agentCode string) {
|
||||
this.locker.RLock()
|
||||
defer this.locker.RUnlock()
|
||||
return this.ipMap[ip]
|
||||
}
|
||||
|
||||
// ContainsIP 检查是否有IP相关数据
|
||||
func (this *Manager) ContainsIP(ip string) bool {
|
||||
this.locker.RLock()
|
||||
defer this.locker.RUnlock()
|
||||
_, ok := this.ipMap[ip]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (this *Manager) loadDB() error {
|
||||
var db = NewDB(Tea.Root + "/data/agents.db")
|
||||
err := db.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.db = db
|
||||
return nil
|
||||
}
|
||||
32
internal/utils/agents/manager_test.go
Normal file
32
internal/utils/agents/manager_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/agents"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
var db = agents.NewDB(Tea.Root + "/data/agents.db")
|
||||
err := db.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var manager = agents.NewManager()
|
||||
manager.SetDB(db)
|
||||
err = manager.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = manager.Loop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log(manager.LookupIP("192.168.3.100"))
|
||||
}
|
||||
133
internal/utils/agents/queue.go
Normal file
133
internal/utils/agents/queue.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/events"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
events.On(events.EventLoaded, func() {
|
||||
goman.New(func() {
|
||||
SharedQueue.Start()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var SharedQueue = NewQueue()
|
||||
|
||||
type Queue struct {
|
||||
c chan string // chan ip
|
||||
cacheMap *IPCacheMap
|
||||
}
|
||||
|
||||
func NewQueue() *Queue {
|
||||
return &Queue{
|
||||
c: make(chan string, 128),
|
||||
cacheMap: NewIPCacheMap(65535),
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Queue) Start() {
|
||||
for ip := range this.c {
|
||||
err := this.Process(ip)
|
||||
if err != nil {
|
||||
// 不需要上报错误
|
||||
if Tea.IsTesting() {
|
||||
remotelogs.Debug("SharedParseQueue", err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push 将IP加入到处理队列
|
||||
func (this *Queue) Push(ip string) {
|
||||
// 是否在处理中
|
||||
if this.cacheMap.Contains(ip) {
|
||||
return
|
||||
}
|
||||
this.cacheMap.Add(ip)
|
||||
|
||||
// 加入到队列
|
||||
select {
|
||||
case this.c <- ip:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Process 处理IP
|
||||
func (this *Queue) Process(ip string) error {
|
||||
// 是否已经在库中
|
||||
if SharedManager.ContainsIP(ip) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ptr, err := this.ParseIP(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ptr) == 0 || ptr == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
//remotelogs.Debug("AGENT", ip+" => "+ptr)
|
||||
|
||||
var agentCode = this.ParsePtr(ptr)
|
||||
if len(agentCode) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 加入到本地
|
||||
SharedManager.AddIP(ip, agentCode)
|
||||
|
||||
var pbAgentIP = &pb.CreateClientAgentIPsRequest_AgentIPInfo{
|
||||
AgentCode: agentCode,
|
||||
Ip: ip,
|
||||
Ptr: ptr,
|
||||
}
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = rpcClient.ClientAgentIPRPC.CreateClientAgentIPs(rpcClient.Context(), &pb.CreateClientAgentIPsRequest{AgentIPs: []*pb.CreateClientAgentIPsRequest_AgentIPInfo{pbAgentIP}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseIP 分析IP的PTR值
|
||||
func (this *Queue) ParseIP(ip string) (ptr string, err error) {
|
||||
if len(ip) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
names, err := net.LookupAddr(ip)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(names) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return names[0], nil
|
||||
}
|
||||
|
||||
// ParsePtr 分析PTR对应的Agent
|
||||
func (this *Queue) ParsePtr(ptr string) (agentCode string) {
|
||||
for _, agent := range AllAgents {
|
||||
if agent.Match(ptr) {
|
||||
return agent.Code
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
76
internal/utils/agents/queue_test.go
Normal file
76
internal/utils/agents/queue_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package agents_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/agents"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseQueue_Process(t *testing.T) {
|
||||
var queue = agents.NewQueue()
|
||||
go queue.Start()
|
||||
time.Sleep(1 * time.Second)
|
||||
queue.Push("220.181.13.100")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
func TestParseQueue_ParseIP(t *testing.T) {
|
||||
var queue = agents.NewQueue()
|
||||
for _, ip := range []string{
|
||||
"192.168.1.100",
|
||||
"42.120.160.1",
|
||||
"42.236.10.98",
|
||||
"124.115.0.100",
|
||||
} {
|
||||
ptr, err := queue.ParseIP(ip)
|
||||
if err != nil {
|
||||
t.Log(ip, "=>", err)
|
||||
continue
|
||||
}
|
||||
t.Log(ip, "=>", ptr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseQueue_ParsePtr(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
var queue = agents.NewQueue()
|
||||
for _, s := range [][]string{
|
||||
{"baiduspider-220-181-108-101.crawl.baidu.com.", "baidu"},
|
||||
{"crawl-66-249-71-219.googlebot.com.", "google"},
|
||||
{"msnbot-40-77-167-31.search.msn.com.", "bing"},
|
||||
{"sogouspider-49-7-20-129.crawl.sogou.com.", "sogou"},
|
||||
{"m13102.mail.163.com.", "youdao"},
|
||||
{"yeurosport.pat1.tc2.yahoo.com.", "yahoo"},
|
||||
{"shenmaspider-42-120-160-1.crawl.sm.cn.", "sm"},
|
||||
{"93-158-161-39.spider.yandex.com.", "yandex"},
|
||||
{"25.bl.bot.semrush.com.", "semrush"},
|
||||
} {
|
||||
a.IsTrue(queue.ParsePtr(s[0]) == s[1])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQueue_ParsePtr(b *testing.B) {
|
||||
var queue = agents.NewQueue()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, s := range [][]string{
|
||||
{"baiduspider-220-181-108-101.crawl.baidu.com.", "baidu"},
|
||||
{"crawl-66-249-71-219.googlebot.com.", "google"},
|
||||
{"msnbot-40-77-167-31.search.msn.com.", "bing"},
|
||||
{"sogouspider-49-7-20-129.crawl.sogou.com.", "sogou"},
|
||||
{"m13102.mail.163.com.", "youdao"},
|
||||
{"yeurosport.pat1.tc2.yahoo.com.", "yahoo"},
|
||||
{"shenmaspider-42-120-160-1.crawl.sm.cn.", "sm"},
|
||||
{"93-158-161-39.spider.yandex.com.", "yandex"},
|
||||
{"93.158.164.218-red.dhcp.yndx.net.", "yandex"},
|
||||
{"25.bl.bot.semrush.com.", "semrush"},
|
||||
} {
|
||||
queue.ParsePtr(s[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,47 +2,39 @@
|
||||
|
||||
package utils
|
||||
|
||||
import "bytes"
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var SharedBufferPool = NewBufferPool()
|
||||
|
||||
// BufferPool pool for get byte slice
|
||||
type BufferPool struct {
|
||||
c chan *bytes.Buffer
|
||||
rawPool *sync.Pool
|
||||
}
|
||||
|
||||
// NewBufferPool 创建新对象
|
||||
func NewBufferPool(maxSize int) *BufferPool {
|
||||
if maxSize <= 0 {
|
||||
maxSize = 1024
|
||||
}
|
||||
pool := &BufferPool{
|
||||
c: make(chan *bytes.Buffer, maxSize),
|
||||
func NewBufferPool() *BufferPool {
|
||||
var pool = &BufferPool{}
|
||||
pool.rawPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &bytes.Buffer{}
|
||||
},
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
// Get 获取一个新的Buffer
|
||||
func (this *BufferPool) Get() (b *bytes.Buffer) {
|
||||
select {
|
||||
case b = <-this.c:
|
||||
b.Reset()
|
||||
default:
|
||||
b = &bytes.Buffer{}
|
||||
var buffer = this.rawPool.Get().(*bytes.Buffer)
|
||||
if buffer.Len() > 0 {
|
||||
buffer.Reset()
|
||||
}
|
||||
return
|
||||
return buffer
|
||||
}
|
||||
|
||||
// Put 放回一个使用过的byte slice
|
||||
func (this *BufferPool) Put(b *bytes.Buffer) {
|
||||
b.Reset()
|
||||
|
||||
select {
|
||||
case this.c <- b:
|
||||
default:
|
||||
// 已达最大容量,则抛弃
|
||||
}
|
||||
}
|
||||
|
||||
// Size 当前的数量
|
||||
func (this *BufferPool) Size() int {
|
||||
return len(this.c)
|
||||
this.rawPool.Put(b)
|
||||
}
|
||||
|
||||
47
internal/utils/buffer_pool_test.go
Normal file
47
internal/utils/buffer_pool_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewBufferPool(t *testing.T) {
|
||||
var pool = utils.NewBufferPool()
|
||||
var b = pool.Get()
|
||||
b.WriteString("Hello, World")
|
||||
t.Log(b.String())
|
||||
|
||||
pool.Put(b)
|
||||
t.Log(b.String())
|
||||
|
||||
b = pool.Get()
|
||||
t.Log(b.String())
|
||||
}
|
||||
|
||||
func BenchmarkNewBufferPool1(b *testing.B) {
|
||||
var data = []byte(strings.Repeat("Hello", 1024))
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
var buffer = &bytes.Buffer{}
|
||||
buffer.Write(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkNewBufferPool2(b *testing.B) {
|
||||
var pool = utils.NewBufferPool()
|
||||
var data = []byte(strings.Repeat("Hello", 1024))
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
var buffer = pool.Get()
|
||||
buffer.Write(data)
|
||||
pool.Put(buffer)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -79,6 +79,18 @@ func (this *ClockManager) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check chrony
|
||||
if config.CheckChrony {
|
||||
chronycExe, err := exec.LookPath("chronyc")
|
||||
if err == nil && len(chronycExe) > 0 {
|
||||
var chronyCmd = executils.NewTimeoutCmd(3*time.Second, chronycExe, "tracking")
|
||||
err = chronyCmd.Run()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var server = config.Server
|
||||
if len(server) == 0 {
|
||||
server = "pool.ntp.org"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var RegexpDigitNumber = regexp.MustCompile("^\\d+$")
|
||||
var RegexpDigitNumber = regexp.MustCompile(`^\d+$`)
|
||||
|
||||
func Get(object interface{}, keys []string) interface{} {
|
||||
if len(keys) == 0 {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
func MapToObject(m maps.Map, ptr interface{}) error {
|
||||
func MapToObject(m maps.Map, ptr any) error {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func MapToObject(m maps.Map, ptr interface{}) error {
|
||||
return json.Unmarshal(mJSON, ptr)
|
||||
}
|
||||
|
||||
func ObjectToMap(ptr interface{}) (maps.Map, error) {
|
||||
func ObjectToMap(ptr any) (maps.Map, error) {
|
||||
if ptr == nil {
|
||||
return maps.Map{}, nil
|
||||
}
|
||||
@@ -33,3 +33,12 @@ func ObjectToMap(ptr interface{}) (maps.Map, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func Copy(destPtr any, srcPtr any) error {
|
||||
data, err := json.Marshal(srcPtr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(data, destPtr)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
package jsonutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func PrintT(obj interface{}, t *testing.T) {
|
||||
func PrintT(obj any, t *testing.T) {
|
||||
data, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
@@ -15,3 +16,17 @@ func PrintT(obj interface{}, t *testing.T) {
|
||||
t.Log(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func Equal(obj1 any, obj2 any) bool {
|
||||
data1, err := json.Marshal(obj1)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
data2, err := json.Marshal(obj2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Equal(data1, data2)
|
||||
}
|
||||
|
||||
26
internal/utils/jsonutils/utils_test.go
Normal file
26
internal/utils/jsonutils/utils_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package jsonutils_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/jsonutils"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEqual(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
var m1 = maps.Map{"a": 1, "b2": true}
|
||||
var m2 = maps.Map{"b2": true, "a": 1}
|
||||
a.IsTrue(jsonutils.Equal(m1, m2))
|
||||
}
|
||||
|
||||
{
|
||||
var m1 = maps.Map{"a": 1, "b2": true, "c": nil}
|
||||
var m2 = maps.Map{"b2": true, "a": 1}
|
||||
a.IsFalse(jsonutils.Equal(m1, m2))
|
||||
}
|
||||
}
|
||||
135
internal/utils/readers/reader_concurrent.go
Normal file
135
internal/utils/readers/reader_concurrent.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package readers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type concurrentSubReader struct {
|
||||
main *ConcurrentReaderList
|
||||
index int
|
||||
}
|
||||
|
||||
func (this *concurrentSubReader) Read(p []byte) (n int, err error) {
|
||||
n, err = this.main.readIndex(p, this.index)
|
||||
this.index++
|
||||
return
|
||||
}
|
||||
|
||||
func (this *concurrentSubReader) Close() error {
|
||||
this.main.removeSubReader(this)
|
||||
|
||||
err := this.main.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConcurrentReaderList
|
||||
// TODO 动态调整 pieces = pieces[minPieceIndex:] 以节约内存
|
||||
type ConcurrentReaderList struct {
|
||||
locker sync.RWMutex
|
||||
readLocker sync.Mutex
|
||||
|
||||
mainReader io.ReadCloser
|
||||
subReaderMap map[*concurrentSubReader]bool
|
||||
pieces [][]byte
|
||||
lastErr error
|
||||
}
|
||||
|
||||
func NewConcurrentReaderList(mainReader io.ReadCloser) *ConcurrentReaderList {
|
||||
return &ConcurrentReaderList{
|
||||
mainReader: mainReader,
|
||||
subReaderMap: map[*concurrentSubReader]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func (this *ConcurrentReaderList) NewReader() io.ReadCloser {
|
||||
var subReader = &concurrentSubReader{
|
||||
main: this,
|
||||
}
|
||||
this.locker.Lock()
|
||||
this.subReaderMap[subReader] = true
|
||||
this.locker.Unlock()
|
||||
return subReader
|
||||
}
|
||||
|
||||
func (this *ConcurrentReaderList) read(p []byte) (n int, err error) {
|
||||
n, err = this.mainReader.Read(p)
|
||||
this.lastErr = err
|
||||
|
||||
if n > 0 {
|
||||
var piece = make([]byte, n)
|
||||
copy(piece, p[:n])
|
||||
this.locker.Lock()
|
||||
this.pieces = append(this.pieces, piece)
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (this *ConcurrentReaderList) readIndex(p []byte, index int) (n int, err error) {
|
||||
// 如果已经有数据
|
||||
this.locker.RLock()
|
||||
var countPieces = len(this.pieces)
|
||||
if index < countPieces {
|
||||
var piece = this.pieces[index]
|
||||
this.locker.RUnlock()
|
||||
var pn = len(piece)
|
||||
if len(p) < pn {
|
||||
err = errors.New("invalid buffer length '" + types.String(len(p)) + "' vs '" + types.String(len(piece)) + "'")
|
||||
return
|
||||
}
|
||||
n = pn
|
||||
copy(p, piece)
|
||||
return
|
||||
}
|
||||
this.locker.RUnlock()
|
||||
|
||||
if this.lastErr != nil {
|
||||
return 0, this.lastErr
|
||||
}
|
||||
|
||||
// 如果没有数据,则读取之
|
||||
this.readLocker.Lock()
|
||||
|
||||
// 再次检查数据是否已更新
|
||||
this.locker.RLock()
|
||||
if len(this.pieces) > countPieces || this.lastErr != nil {
|
||||
this.locker.RUnlock()
|
||||
this.readLocker.Unlock()
|
||||
return this.readIndex(p, index)
|
||||
}
|
||||
this.locker.RUnlock()
|
||||
|
||||
// 从原始Reader中读取
|
||||
n, err = this.read(p)
|
||||
this.readLocker.Unlock()
|
||||
if n > 0 {
|
||||
// 重新尝试
|
||||
return this.readIndex(p, index)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *ConcurrentReaderList) removeSubReader(subReader *concurrentSubReader) {
|
||||
this.locker.Lock()
|
||||
delete(this.subReaderMap, subReader)
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *ConcurrentReaderList) Close() error {
|
||||
this.locker.Lock()
|
||||
if len(this.subReaderMap) == 0 {
|
||||
this.locker.Unlock()
|
||||
return this.mainReader.Close()
|
||||
}
|
||||
this.locker.Unlock()
|
||||
return nil
|
||||
}
|
||||
81
internal/utils/readers/reader_concurrent_test.go
Normal file
81
internal/utils/readers/reader_concurrent_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package readers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/readers"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type testReader struct {
|
||||
t *testing.T
|
||||
|
||||
rawReader io.Reader
|
||||
}
|
||||
|
||||
func (this *testReader) Read(p []byte) (n int, err error) {
|
||||
time.Sleep(1 * time.Second) // 延迟
|
||||
return this.rawReader.Read(p)
|
||||
}
|
||||
|
||||
func (this *testReader) Close() error {
|
||||
this.t.Log("close")
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNewConcurrentReader(t *testing.T) {
|
||||
var originBuffer = &bytes.Buffer{}
|
||||
originBuffer.Write([]byte("0123456789_hello_world"))
|
||||
var originLength = originBuffer.Len()
|
||||
var concurrentReader = readers.NewConcurrentReaderList(&testReader{
|
||||
t: t,
|
||||
rawReader: originBuffer,
|
||||
})
|
||||
|
||||
var threads = 32
|
||||
var wg = &sync.WaitGroup{}
|
||||
wg.Add(threads)
|
||||
|
||||
var locker = &sync.Mutex{}
|
||||
var m = map[int][]byte{} // i => []byte
|
||||
|
||||
for i := 0; i < threads; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
var reader = concurrentReader.NewReader()
|
||||
|
||||
var buf = make([]byte, 4)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if n > 0 {
|
||||
locker.Lock()
|
||||
m[i] = append(m[i], buf[:n]...)
|
||||
locker.Unlock()
|
||||
//t.Log(i, string(buf[:n]))
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
t.Log("ERROR:", err)
|
||||
}
|
||||
}
|
||||
|
||||
_ = reader.Close()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for i, b := range m {
|
||||
if len(b) != originLength {
|
||||
t.Fatal("ERROR:", i, string(b))
|
||||
}
|
||||
t.Log(i, string(b))
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@ func ToValidUTF8string(v string) string {
|
||||
return strings.ToValidUTF8(v, "")
|
||||
}
|
||||
|
||||
// ContainsSameStrings 检查两个字符串slice内容是否一致
|
||||
func ContainsSameStrings(s1 []string, s2 []string) bool {
|
||||
// EqualStrings 检查两个字符串slice内容是否一致
|
||||
func EqualStrings(s1 []string, s2 []string) bool {
|
||||
if len(s1) != len(s2) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -59,9 +59,9 @@ func TestFormatAddressList(t *testing.T) {
|
||||
|
||||
func TestContainsSameStrings(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
a.IsFalse(utils.ContainsSameStrings([]string{"a"}, []string{"b"}))
|
||||
a.IsFalse(utils.ContainsSameStrings([]string{"a", "b"}, []string{"b"}))
|
||||
a.IsFalse(utils.ContainsSameStrings([]string{"a", "b"}, []string{"a", "b", "c"}))
|
||||
a.IsTrue(utils.ContainsSameStrings([]string{"a", "b"}, []string{"a", "b"}))
|
||||
a.IsTrue(utils.ContainsSameStrings([]string{"a", "b"}, []string{"b", "a"}))
|
||||
a.IsFalse(utils.EqualStrings([]string{"a"}, []string{"b"}))
|
||||
a.IsFalse(utils.EqualStrings([]string{"a", "b"}, []string{"b"}))
|
||||
a.IsFalse(utils.EqualStrings([]string{"a", "b"}, []string{"a", "b", "c"}))
|
||||
a.IsTrue(utils.EqualStrings([]string{"a", "b"}, []string{"a", "b"}))
|
||||
a.IsTrue(utils.EqualStrings([]string{"a", "b"}, []string{"b", "a"}))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package writers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"io"
|
||||
"time"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
// RateLimitWriter 限速写入
|
||||
type RateLimitWriter struct {
|
||||
rawWriter io.WriteCloser
|
||||
ctx context.Context
|
||||
|
||||
rateBytes int
|
||||
|
||||
@@ -18,9 +20,10 @@ type RateLimitWriter struct {
|
||||
before time.Time
|
||||
}
|
||||
|
||||
func NewRateLimitWriter(rawWriter io.WriteCloser, rateBytes int64) io.WriteCloser {
|
||||
func NewRateLimitWriter(ctx context.Context, rawWriter io.WriteCloser, rateBytes int64) io.WriteCloser {
|
||||
return &RateLimitWriter{
|
||||
rawWriter: rawWriter,
|
||||
ctx: ctx,
|
||||
rateBytes: types.Int(rateBytes),
|
||||
before: time.Now(),
|
||||
}
|
||||
@@ -71,6 +74,14 @@ func (this *RateLimitWriter) write(p []byte) (n int, err error) {
|
||||
n, err = this.rawWriter.Write(p)
|
||||
|
||||
if err == nil {
|
||||
select {
|
||||
case <-this.ctx.Done():
|
||||
err = io.EOF
|
||||
return
|
||||
default:
|
||||
|
||||
}
|
||||
|
||||
this.written += n
|
||||
|
||||
if this.written >= this.rateBytes {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
type recordIPTask struct {
|
||||
ip string
|
||||
listId int64
|
||||
expiredAt int64
|
||||
expiresAt int64
|
||||
level string
|
||||
serverId int64
|
||||
|
||||
@@ -54,7 +54,7 @@ func init() {
|
||||
IpListId: task.listId,
|
||||
IpFrom: task.ip,
|
||||
IpTo: "",
|
||||
ExpiredAt: task.expiredAt,
|
||||
ExpiredAt: task.expiresAt,
|
||||
Reason: reason,
|
||||
Type: ipType,
|
||||
EventLevel: task.level,
|
||||
@@ -105,11 +105,13 @@ func (this *RecordIPAction) Perform(waf *WAF, group *RuleGroup, set *RuleSet, re
|
||||
return true, false
|
||||
}
|
||||
|
||||
timeout := this.Timeout
|
||||
var timeout = this.Timeout
|
||||
var isForever = false
|
||||
if timeout <= 0 {
|
||||
isForever = true
|
||||
timeout = 86400 // 1天
|
||||
}
|
||||
expiredAt := time.Now().Unix() + int64(timeout)
|
||||
var expiresAt = time.Now().Unix() + int64(timeout)
|
||||
|
||||
if this.Type == "black" {
|
||||
writer.WriteHeader(http.StatusForbidden)
|
||||
@@ -117,10 +119,10 @@ func (this *RecordIPAction) Perform(waf *WAF, group *RuleGroup, set *RuleSet, re
|
||||
request.WAFClose()
|
||||
|
||||
// 先加入本地的黑名单
|
||||
SharedIPBlackList.Add(IPTypeAll, this.Scope, request.WAFServerId(), request.WAFRemoteIP(), expiredAt)
|
||||
SharedIPBlackList.Add(IPTypeAll, this.Scope, request.WAFServerId(), request.WAFRemoteIP(), expiresAt)
|
||||
} else {
|
||||
// 加入本地白名单
|
||||
SharedIPWhiteList.Add("set:"+types.String(set.Id), this.Scope, request.WAFServerId(), request.WAFRemoteIP(), expiredAt)
|
||||
SharedIPWhiteList.Add("set:"+types.String(set.Id), this.Scope, request.WAFServerId(), request.WAFRemoteIP(), expiresAt)
|
||||
}
|
||||
|
||||
// 上报
|
||||
@@ -130,11 +132,16 @@ func (this *RecordIPAction) Perform(waf *WAF, group *RuleGroup, set *RuleSet, re
|
||||
serverId = request.WAFServerId()
|
||||
}
|
||||
|
||||
var realExpiresAt = expiresAt
|
||||
if isForever {
|
||||
realExpiresAt = 0
|
||||
}
|
||||
|
||||
select {
|
||||
case recordIPTaskChan <- &recordIPTask{
|
||||
ip: request.WAFRemoteIP(),
|
||||
listId: this.IPListId,
|
||||
expiredAt: expiredAt,
|
||||
expiresAt: realExpiresAt,
|
||||
level: this.Level,
|
||||
serverId: serverId,
|
||||
sourceServerId: request.WAFServerId(),
|
||||
|
||||
22
internal/waf/checkpoints/request_cname.go
Normal file
22
internal/waf/checkpoints/request_cname.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type RequestCNAMECheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestCNAMECheckpoint) RequestValue(req requests.Request, param string, options maps.Map, ruleId int64) (value interface{}, hasRequestBody bool, sysErr error, userErr error) {
|
||||
value = req.Format("${cname}")
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestCNAMECheckpoint) ResponseValue(req requests.Request, resp *requests.Response, param string, options maps.Map, ruleId int64) (value interface{}, hasRequestBody bool, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options, ruleId)
|
||||
}
|
||||
return
|
||||
}
|
||||
26
internal/waf/checkpoints/request_is_cname.go
Normal file
26
internal/waf/checkpoints/request_is_cname.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type RequestIsCNAMECheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestIsCNAMECheckpoint) RequestValue(req requests.Request, param string, options maps.Map, ruleId int64) (value interface{}, hasRequestBody bool, sysErr error, userErr error) {
|
||||
if req.Format("${cname}") == req.Format("${host}") {
|
||||
value = 1
|
||||
} else {
|
||||
value = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestIsCNAMECheckpoint) ResponseValue(req requests.Request, resp *requests.Response, param string, options maps.Map, ruleId int64) (value interface{}, hasRequestBody bool, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options, ruleId)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -146,6 +146,22 @@ var AllCheckpoints = []*CheckpointDefinition{
|
||||
Instance: new(RequestHostCheckpoint),
|
||||
Priority: 100,
|
||||
},
|
||||
{
|
||||
Name: "CNAME",
|
||||
Prefix: "cname",
|
||||
Description: "当前网站服务CNAME,比如38b48e4f.goedge.cn",
|
||||
HasParams: false,
|
||||
Instance: new(RequestCNAMECheckpoint),
|
||||
Priority: 100,
|
||||
},
|
||||
{
|
||||
Name: "是否为CNAME",
|
||||
Prefix: "isCNAME",
|
||||
Description: "是否为CNAME,值为1(是)或0(否)",
|
||||
HasParams: false,
|
||||
Instance: new(RequestIsCNAMECheckpoint),
|
||||
Priority: 100,
|
||||
},
|
||||
{
|
||||
Name: "请求来源URL",
|
||||
Prefix: "referer",
|
||||
|
||||
@@ -100,7 +100,7 @@ func (this *IPList) RecordIP(ipType string,
|
||||
case recordIPTaskChan <- &recordIPTask{
|
||||
ip: ip,
|
||||
listId: firewallconfigs.GlobalListId,
|
||||
expiredAt: expiresAt,
|
||||
expiresAt: expiresAt,
|
||||
level: firewallconfigs.DefaultEventLevel,
|
||||
serverId: serverId,
|
||||
sourceServerId: serverId,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/checkpoints"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/utils"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/values"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
@@ -47,6 +48,10 @@ type Rule struct {
|
||||
isIP bool
|
||||
ipValue net.IP
|
||||
|
||||
ipRangeListValue *values.IPRangeList
|
||||
stringValues []string
|
||||
ipList *values.StringList
|
||||
|
||||
floatValue float64
|
||||
reg *re.Regexp
|
||||
}
|
||||
@@ -70,6 +75,21 @@ func (this *Rule) Init() error {
|
||||
this.floatValue = types.Float64(this.Value)
|
||||
case RuleOperatorNeq:
|
||||
this.floatValue = types.Float64(this.Value)
|
||||
case RuleOperatorContainsAny, RuleOperatorContainsAll:
|
||||
this.stringValues = []string{}
|
||||
if len(this.Value) > 0 {
|
||||
var lines = strings.Split(this.Value, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) > 0 {
|
||||
if this.IsCaseInsensitive {
|
||||
this.stringValues = append(this.stringValues, strings.ToLower(line))
|
||||
} else {
|
||||
this.stringValues = append(this.stringValues, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case RuleOperatorMatch:
|
||||
v := this.Value
|
||||
if this.IsCaseInsensitive && !strings.HasPrefix(v, "(?i)") {
|
||||
@@ -103,33 +123,10 @@ func (this *Rule) Init() error {
|
||||
if !this.isIP {
|
||||
return errors.New("value should be a valid ip")
|
||||
}
|
||||
case RuleOperatorInIPList:
|
||||
this.ipList = values.ParseStringList(this.Value, true)
|
||||
case RuleOperatorIPRange, RuleOperatorNotIPRange:
|
||||
if strings.Contains(this.Value, ",") {
|
||||
ipList := strings.SplitN(this.Value, ",", 2)
|
||||
ipString1 := strings.TrimSpace(ipList[0])
|
||||
ipString2 := strings.TrimSpace(ipList[1])
|
||||
|
||||
if len(ipString1) > 0 {
|
||||
ip1 := net.ParseIP(ipString1)
|
||||
if ip1 == nil {
|
||||
return errors.New("start ip is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
if len(ipString2) > 0 {
|
||||
ip2 := net.ParseIP(ipString2)
|
||||
if ip2 == nil {
|
||||
return errors.New("end ip is invalid")
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(this.Value, "/") {
|
||||
_, _, err := net.ParseCIDR(this.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return errors.New("invalid ip range")
|
||||
}
|
||||
this.ipRangeListValue = values.ParseIPRangeList(this.Value)
|
||||
}
|
||||
|
||||
if singleParamRegexp.MatchString(this.Param) {
|
||||
@@ -362,13 +359,13 @@ func (this *Rule) Test(value interface{}) bool {
|
||||
return types.Float64(value) != this.floatValue
|
||||
case RuleOperatorEqString:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.ToLower(types.String(value)) == strings.ToLower(this.Value)
|
||||
return strings.EqualFold(types.String(value), this.Value)
|
||||
} else {
|
||||
return types.String(value) == this.Value
|
||||
}
|
||||
case RuleOperatorNeqString:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.ToLower(types.String(value)) != strings.ToLower(this.Value)
|
||||
return !strings.EqualFold(types.String(value), this.Value)
|
||||
} else {
|
||||
return types.String(value) != this.Value
|
||||
}
|
||||
@@ -472,6 +469,33 @@ func (this *Rule) Test(value interface{}) bool {
|
||||
} else {
|
||||
return strings.HasSuffix(types.String(value), this.Value)
|
||||
}
|
||||
case RuleOperatorContainsAny:
|
||||
var stringValue = types.String(value)
|
||||
if this.IsCaseInsensitive {
|
||||
stringValue = strings.ToLower(stringValue)
|
||||
}
|
||||
if len(stringValue) > 0 && len(this.stringValues) > 0 {
|
||||
for _, v := range this.stringValues {
|
||||
if strings.Contains(stringValue, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
case RuleOperatorContainsAll:
|
||||
var stringValue = types.String(value)
|
||||
if this.IsCaseInsensitive {
|
||||
stringValue = strings.ToLower(stringValue)
|
||||
}
|
||||
if len(stringValue) > 0 && len(this.stringValues) > 0 {
|
||||
for _, v := range this.stringValues {
|
||||
if !strings.Contains(stringValue, v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case RuleOperatorContainsBinary:
|
||||
data, _ := base64.StdEncoding.DecodeString(types.String(this.Value))
|
||||
if this.IsCaseInsensitive {
|
||||
@@ -563,12 +587,12 @@ func (this *Rule) Test(value interface{}) bool {
|
||||
case RuleOperatorNotIPRange:
|
||||
return !this.containsIP(value)
|
||||
case RuleOperatorIPMod:
|
||||
pieces := strings.SplitN(this.Value, ",", 2)
|
||||
var pieces = strings.SplitN(this.Value, ",", 2)
|
||||
if len(pieces) == 1 {
|
||||
rem := types.Int64(pieces[0])
|
||||
var rem = types.Int64(pieces[0])
|
||||
return this.ipToInt64(net.ParseIP(types.String(value)))%10 == rem
|
||||
}
|
||||
div := types.Int64(pieces[0])
|
||||
var div = types.Int64(pieces[0])
|
||||
if div == 0 {
|
||||
return false
|
||||
}
|
||||
@@ -578,6 +602,11 @@ func (this *Rule) Test(value interface{}) bool {
|
||||
return this.ipToInt64(net.ParseIP(types.String(value)))%10 == types.Int64(this.Value)
|
||||
case RuleOperatorIPMod100:
|
||||
return this.ipToInt64(net.ParseIP(types.String(value)))%100 == types.Int64(this.Value)
|
||||
case RuleOperatorInIPList:
|
||||
if this.ipList != nil {
|
||||
return this.ipList.Contains(types.String(value))
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -590,65 +619,64 @@ func (this *Rule) SetCheckpointFinder(finder func(prefix string) checkpoints.Che
|
||||
this.checkpointFinder = finder
|
||||
}
|
||||
|
||||
var unescapeChars = [][2]string{
|
||||
{`\s`, `(\s|%09|%0A|\+)`},
|
||||
{`\(`, `(\(|%28)`},
|
||||
{`=`, `(=|%3D)`},
|
||||
{`<`, `(<|%3C)`},
|
||||
{`\*`, `(\*|%2A)`},
|
||||
{`\\`, `(\\|%2F)`},
|
||||
{`!`, `(!|%21)`},
|
||||
{`/`, `(/|%2F)`},
|
||||
{`;`, `(;|%3B)`},
|
||||
{`\+`, `(\+|%20)`},
|
||||
}
|
||||
|
||||
func (this *Rule) unescape(v string) string {
|
||||
//replace urlencoded characters
|
||||
v = strings.Replace(v, `\s`, `(\s|%09|%0A|\+)`, -1)
|
||||
v = strings.Replace(v, `\(`, `(\(|%28)`, -1)
|
||||
v = strings.Replace(v, `=`, `(=|%3D)`, -1)
|
||||
v = strings.Replace(v, `<`, `(<|%3C)`, -1)
|
||||
v = strings.Replace(v, `\*`, `(\*|%2A)`, -1)
|
||||
v = strings.Replace(v, `\\`, `(\\|%2F)`, -1)
|
||||
v = strings.Replace(v, `!`, `(!|%21)`, -1)
|
||||
v = strings.Replace(v, `/`, `(/|%2F)`, -1)
|
||||
v = strings.Replace(v, `;`, `(;|%3B)`, -1)
|
||||
v = strings.Replace(v, `\+`, `(\+|%20)`, -1)
|
||||
// replace urlencoded characters
|
||||
|
||||
for _, c := range unescapeChars {
|
||||
if !strings.Contains(v, c[0]) {
|
||||
continue
|
||||
}
|
||||
var pieces = strings.Split(v, c[0])
|
||||
|
||||
// 修复piece中错误的\
|
||||
for pieceIndex, piece := range pieces {
|
||||
var l = len(piece)
|
||||
if l == 0 {
|
||||
continue
|
||||
}
|
||||
if piece[l-1] != '\\' {
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算\的数量
|
||||
var countBackSlashes = 0
|
||||
for i := l - 1; i >= 0; i-- {
|
||||
if piece[i] == '\\' {
|
||||
countBackSlashes++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if countBackSlashes%2 == 1 {
|
||||
// 去掉最后一个
|
||||
pieces[pieceIndex] = piece[:len(piece)-1]
|
||||
}
|
||||
}
|
||||
|
||||
v = strings.Join(pieces, c[1])
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (this *Rule) containsIP(value interface{}) bool {
|
||||
ip := net.ParseIP(types.String(value))
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查IP范围格式
|
||||
if strings.Contains(this.Value, ",") {
|
||||
ipList := strings.SplitN(this.Value, ",", 2)
|
||||
ipString1 := strings.TrimSpace(ipList[0])
|
||||
ipString2 := strings.TrimSpace(ipList[1])
|
||||
|
||||
if len(ipString1) > 0 {
|
||||
ip1 := net.ParseIP(ipString1)
|
||||
if ip1 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if bytes.Compare(ip, ip1) < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(ipString2) > 0 {
|
||||
ip2 := net.ParseIP(ipString2)
|
||||
if ip2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if bytes.Compare(ip, ip2) > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} else if strings.Contains(this.Value, "/") {
|
||||
_, ipNet, err := net.ParseCIDR(this.Value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ipNet.Contains(ip)
|
||||
} else {
|
||||
func (this *Rule) containsIP(value any) bool {
|
||||
if this.ipRangeListValue == nil {
|
||||
return false
|
||||
}
|
||||
return this.ipRangeListValue.Contains(types.String(value))
|
||||
}
|
||||
|
||||
func (this *Rule) ipToInt64(ip net.IP) int64 {
|
||||
|
||||
@@ -18,6 +18,9 @@ const (
|
||||
RuleOperatorNotContains RuleOperator = "not contains"
|
||||
RuleOperatorPrefix RuleOperator = "prefix"
|
||||
RuleOperatorSuffix RuleOperator = "suffix"
|
||||
RuleOperatorContainsAny RuleOperator = "contains any"
|
||||
RuleOperatorContainsAll RuleOperator = "contains all"
|
||||
RuleOperatorInIPList RuleOperator = "in ip list"
|
||||
RuleOperatorHasKey RuleOperator = "has key" // has key in slice or map
|
||||
RuleOperatorVersionGt RuleOperator = "version gt"
|
||||
RuleOperatorVersionLt RuleOperator = "version lt"
|
||||
|
||||
@@ -5,5 +5,5 @@ package utils
|
||||
import "github.com/TeaOSLab/EdgeNode/internal/utils/sizes"
|
||||
|
||||
const (
|
||||
MaxBodySize = 4 * sizes.M
|
||||
MaxBodySize = 2 * sizes.M
|
||||
)
|
||||
|
||||
@@ -13,6 +13,10 @@ var cache = ttlcache.NewCache()
|
||||
|
||||
// MatchStringCache 正则表达式匹配字符串,并缓存结果
|
||||
func MatchStringCache(regex *re.Regexp, s string) bool {
|
||||
if regex == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果长度超过4096,大概率是不能重用的
|
||||
if len(s) > 4096 {
|
||||
return regex.MatchString(s)
|
||||
@@ -35,6 +39,10 @@ func MatchStringCache(regex *re.Regexp, s string) bool {
|
||||
|
||||
// MatchBytesCache 正则表达式匹配字节slice,并缓存结果
|
||||
func MatchBytesCache(regex *re.Regexp, byteSlice []byte) bool {
|
||||
if regex == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果长度超过4096,大概率是不能重用的
|
||||
if len(byteSlice) > 4096 {
|
||||
return regex.Match(byteSlice)
|
||||
|
||||
132
internal/waf/values/ip_range.go
Normal file
132
internal/waf/values/ip_range.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package values
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type IPRangeType = string
|
||||
|
||||
const (
|
||||
IPRangeTypeCIDR IPRangeType = "cidr" // CIDR
|
||||
IPRangeTypeSingeIP IPRangeType = "singleIP" // 单个IP
|
||||
IPRangeTypeRange IPRangeType = "range" // IP范围,IP1-IP2
|
||||
)
|
||||
|
||||
type IPRange struct {
|
||||
Type IPRangeType
|
||||
CIDR *net.IPNet
|
||||
IPFrom net.IP
|
||||
IPTo net.IP
|
||||
}
|
||||
|
||||
func (this *IPRange) Contains(netIP net.IP) bool {
|
||||
if netIP == nil {
|
||||
return false
|
||||
}
|
||||
switch this.Type {
|
||||
case IPRangeTypeCIDR:
|
||||
if this.CIDR != nil {
|
||||
return this.CIDR.Contains(netIP)
|
||||
}
|
||||
case IPRangeTypeSingeIP:
|
||||
if this.IPFrom != nil {
|
||||
return bytes.Equal(this.IPFrom, netIP)
|
||||
}
|
||||
case IPRangeTypeRange:
|
||||
return bytes.Compare(this.IPFrom, netIP) <= 0 && bytes.Compare(this.IPTo, netIP) >= 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type IPRangeList struct {
|
||||
Ranges []*IPRange
|
||||
}
|
||||
|
||||
func NewIPRangeList() *IPRangeList {
|
||||
return &IPRangeList{}
|
||||
}
|
||||
|
||||
func ParseIPRangeList(value string) *IPRangeList {
|
||||
var list = NewIPRangeList()
|
||||
|
||||
if len(value) == 0 {
|
||||
return list
|
||||
}
|
||||
|
||||
var lines = strings.Split(value, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, ",") { // IPFrom,IPTo
|
||||
var pieces = strings.SplitN(line, ",", 2)
|
||||
if len(pieces) == 2 {
|
||||
var ipFrom = net.ParseIP(strings.TrimSpace(pieces[0]))
|
||||
var ipTo = net.ParseIP(strings.TrimSpace(pieces[1]))
|
||||
if ipFrom != nil && ipTo != nil {
|
||||
if bytes.Compare(ipFrom, ipTo) > 0 {
|
||||
ipFrom, ipTo = ipTo, ipFrom
|
||||
}
|
||||
list.Ranges = append(list.Ranges, &IPRange{
|
||||
Type: IPRangeTypeRange,
|
||||
IPFrom: ipFrom,
|
||||
IPTo: ipTo,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(line, "-") { // IPFrom-IPTo
|
||||
var pieces = strings.SplitN(line, "-", 2)
|
||||
if len(pieces) == 2 {
|
||||
var ipFrom = net.ParseIP(strings.TrimSpace(pieces[0]))
|
||||
var ipTo = net.ParseIP(strings.TrimSpace(pieces[1]))
|
||||
if ipFrom != nil && ipTo != nil {
|
||||
if bytes.Compare(ipFrom, ipTo) > 0 {
|
||||
ipFrom, ipTo = ipTo, ipFrom
|
||||
}
|
||||
list.Ranges = append(list.Ranges, &IPRange{
|
||||
Type: IPRangeTypeRange,
|
||||
IPFrom: ipFrom,
|
||||
IPTo: ipTo,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(line, "/") { // CIDR
|
||||
_, cidr, _ := net.ParseCIDR(line)
|
||||
if cidr != nil {
|
||||
list.Ranges = append(list.Ranges, &IPRange{
|
||||
Type: IPRangeTypeCIDR,
|
||||
CIDR: cidr,
|
||||
})
|
||||
}
|
||||
} else { // single ip
|
||||
var netIP = net.ParseIP(line)
|
||||
if netIP != nil {
|
||||
list.Ranges = append(list.Ranges, &IPRange{
|
||||
Type: IPRangeTypeSingeIP,
|
||||
IPFrom: netIP,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (this *IPRangeList) Contains(ip string) bool {
|
||||
var netIP = net.ParseIP(ip)
|
||||
if netIP == nil {
|
||||
return false
|
||||
}
|
||||
for _, r := range this.Ranges {
|
||||
if r.Contains(netIP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
54
internal/waf/values/ip_range_test.go
Normal file
54
internal/waf/values/ip_range_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package values_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/values"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIPRange_ParseIPRangeList(t *testing.T) {
|
||||
{
|
||||
var r = values.ParseIPRangeList("")
|
||||
logs.PrintAsJSON(r, t)
|
||||
}
|
||||
{
|
||||
var r = values.ParseIPRangeList("192.168.2.1")
|
||||
logs.PrintAsJSON(r, t)
|
||||
}
|
||||
{
|
||||
var r = values.ParseIPRangeList(`192.168.2.1
|
||||
192.168.1.100/24
|
||||
192.168.1.1-192.168.2.100
|
||||
192.168.1.2,192.168.2.200
|
||||
192.168.2.200 - 192.168.2.100
|
||||
# 以下是错误的
|
||||
192.168
|
||||
192.168.100.1-1`)
|
||||
logs.PrintAsJSON(r, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPRange_Contains(t *testing.T) {
|
||||
{
|
||||
var a = assert.NewAssertion(t)
|
||||
var r = values.ParseIPRangeList(`192.168.2.1
|
||||
192.168.1.100/24
|
||||
192.168.1.1-192.168.2.100
|
||||
192.168.2.2,192.168.2.200
|
||||
192.168.3.200 - 192.168.3.100
|
||||
192.168.4.100
|
||||
192.168.5.1/26`)
|
||||
a.IsTrue(r.Contains("192.168.1.102"))
|
||||
a.IsTrue(r.Contains("192.168.2.101"))
|
||||
a.IsTrue(r.Contains("192.168.1.1"))
|
||||
a.IsTrue(r.Contains("192.168.2.100"))
|
||||
a.IsFalse(r.Contains("192.168.2.201"))
|
||||
a.IsTrue(r.Contains("192.168.3.101"))
|
||||
a.IsTrue(r.Contains("192.168.4.100"))
|
||||
a.IsTrue(r.Contains("192.168.5.63"))
|
||||
a.IsFalse(r.Contains("192.168.5.128"))
|
||||
}
|
||||
}
|
||||
48
internal/waf/values/number_list.go
Normal file
48
internal/waf/values/number_list.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package values
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/zero"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type NumberList struct {
|
||||
ValueMap map[float64]zero.Zero
|
||||
}
|
||||
|
||||
func NewNumberList() *NumberList {
|
||||
return &NumberList{
|
||||
ValueMap: map[float64]zero.Zero{},
|
||||
}
|
||||
}
|
||||
|
||||
func ParseNumberList(v string) *NumberList {
|
||||
var list = NewNumberList()
|
||||
if len(v) == 0 {
|
||||
return list
|
||||
}
|
||||
|
||||
var lines = strings.Split(v, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var values = strings.Split(line, ",")
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) > 0 {
|
||||
list.ValueMap[types.Float64(value)] = zero.Zero{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (this *NumberList) Contains(f float64) bool {
|
||||
_, ok := this.ValueMap[f]
|
||||
return ok
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user