Toby Chui 3 месяцев назад
Родитель
Сommit
ad6502a1fc

+ 1 - 1
def.go

@@ -43,7 +43,7 @@ const (
 	/* Build Constants */
 	SYSTEM_NAME       = "Zoraxy"
 	SYSTEM_VERSION    = "3.1.5"
-	DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
+	DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */
 
 	/* System Constants */
 	DATABASE_PATH              = "sys.db"

+ 17 - 5
main.go

@@ -60,19 +60,31 @@ func SetupCloseHandler() {
 func ShutdownSeq() {
 	SystemWideLogger.Println("Shutting down " + SYSTEM_NAME)
 	SystemWideLogger.Println("Closing Netstats Listener")
-	netstatBuffers.Close()
+	if netstatBuffers != nil {
+		netstatBuffers.Close()
+	}
+
 	SystemWideLogger.Println("Closing Statistic Collector")
-	statisticCollector.Close()
+	if statisticCollector != nil {
+		statisticCollector.Close()
+	}
+
 	if mdnsTickerStop != nil {
 		SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
 		// Stop the mdns service
 		mdnsTickerStop <- true
 	}
-	mdnsScanner.Close()
+	if mdnsScanner != nil {
+		mdnsScanner.Close()
+	}
 	SystemWideLogger.Println("Shutting down load balancer")
-	loadBalancer.Close()
+	if loadBalancer != nil {
+		loadBalancer.Close()
+	}
 	SystemWideLogger.Println("Closing Certificates Auto Renewer")
-	acmeAutoRenewer.Close()
+	if acmeAutoRenewer != nil {
+		acmeAutoRenewer.Close()
+	}
 	//Remove the tmp folder
 	SystemWideLogger.Println("Cleaning up tmp files")
 	os.RemoveAll("./tmp")

+ 4 - 2
mod/database/database.go

@@ -28,14 +28,16 @@ func NewDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error
 	return newDatabase(dbfile, backendType)
 }
 
+// Get the recommended backend type for the current system
 func GetRecommendedBackendType() dbinc.BackendType {
 	//Check if the system is running on RISCV hardware
 	if runtime.GOARCH == "riscv64" {
 		//RISCV hardware, currently only support FS emulated database
 		return dbinc.BackendFSOnly
 	} else if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
-		//Powerful hardware, use LevelDB
-		return dbinc.BackendLevelDB
+		//Powerful hardware
+		return dbinc.BackendBoltDB
+		//return dbinc.BackendLevelDB
 	}
 
 	//Default to BoltDB, the safest option

+ 46 - 4
mod/database/dbleveldb/dbleveldb.go

@@ -2,9 +2,11 @@ package dbleveldb
 
 import (
 	"encoding/json"
+	"log"
 	"path/filepath"
 	"strings"
 	"sync"
+	"time"
 
 	"github.com/syndtr/goleveldb/leveldb"
 	"github.com/syndtr/goleveldb/leveldb/util"
@@ -15,8 +17,11 @@ import (
 var _ dbinc.Backend = (*DB)(nil)
 
 type DB struct {
-	db    *leveldb.DB
-	Table sync.Map //For emulating table creation
+	db               *leveldb.DB
+	Table            sync.Map      //For emulating table creation
+	batch            leveldb.Batch //Batch write
+	writeFlushTicker *time.Ticker  //Ticker for flushing data into disk
+	writeFlushStop   chan bool     //Stop channel for write flush ticker
 }
 
 func NewDB(path string) (*DB, error) {
@@ -29,7 +34,39 @@ func NewDB(path string) (*DB, error) {
 	if err != nil {
 		return nil, err
 	}
-	return &DB{db: db, Table: sync.Map{}}, nil
+
+	thisDB := &DB{
+		db:    db,
+		Table: sync.Map{},
+		batch: leveldb.Batch{},
+	}
+
+	//Create a ticker to flush data into disk every 5 seconds
+	writeFlushTicker := time.NewTicker(5 * time.Second)
+	writeFlushStop := make(chan bool)
+	go func() {
+		for {
+			select {
+			case <-writeFlushTicker.C:
+				if thisDB.batch.Len() == 0 {
+					//No flushing needed
+					continue
+				}
+				err = db.Write(&thisDB.batch, nil)
+				if err != nil {
+					log.Println("[LevelDB] Failed to flush data into disk: ", err)
+				}
+				thisDB.batch.Reset()
+			case <-writeFlushStop:
+				return
+			}
+		}
+	}()
+
+	thisDB.writeFlushTicker = writeFlushTicker
+	thisDB.writeFlushStop = writeFlushStop
+
+	return thisDB, nil
 }
 
 func (d *DB) NewTable(tableName string) error {
@@ -66,7 +103,8 @@ func (d *DB) Write(tableName string, key string, value interface{}) error {
 	if err != nil {
 		return err
 	}
-	return d.db.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data, nil)
+	d.batch.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data)
+	return nil
 }
 
 func (d *DB) Read(tableName string, key string, assignee interface{}) error {
@@ -106,5 +144,9 @@ func (d *DB) ListTable(tableName string) ([][][]byte, error) {
 }
 
 func (d *DB) Close() {
+	//Write the remaining data in batch back into disk
+	d.writeFlushStop <- true
+	d.writeFlushTicker.Stop()
+	d.db.Write(&d.batch, nil)
 	d.db.Close()
 }

+ 47 - 15
mod/statistic/statistic.go

@@ -33,15 +33,15 @@ type DailySummary struct {
 }
 
 type RequestInfo struct {
-	IpAddr                        string
-	RequestOriginalCountryISOCode string
-	Succ                          bool
-	StatusCode                    int
-	ForwardType                   string
-	Referer                       string
-	UserAgent                     string
-	RequestURL                    string
-	Target                        string
+	IpAddr                        string //IP address of the downstream request
+	RequestOriginalCountryISOCode string //ISO code of the country where the request originated
+	Succ                          bool   //If the request is successful and resp generated by upstream instead of Zoraxy (except static web server)
+	StatusCode                    int    //HTTP status code of the request
+	ForwardType                   string //Forward type of the request, usually the proxy type (e.g. host-http, subdomain-websocket or vdir-http or any of the combination)
+	Referer                       string //Referer of the downstream request
+	UserAgent                     string //UserAgent of the downstream request
+	RequestURL                    string //Request URL
+	Target                        string //Target domain or hostname
 }
 
 type CollectorOption struct {
@@ -59,7 +59,7 @@ func NewStatisticCollector(option CollectorOption) (*Collector, error) {
 
 	//Create the collector object
 	thisCollector := Collector{
-		DailySummary: newDailySummary(),
+		DailySummary: NewDailySummary(),
 		Option:       &option,
 	}
 
@@ -87,6 +87,11 @@ func (c *Collector) SaveSummaryOfDay() {
 	c.Option.Database.Write("stats", summaryKey, saveData)
 }
 
+// Get the daily summary up until now
+func (c *Collector) GetCurrentDailySummary() *DailySummary {
+	return c.DailySummary
+}
+
 // Load the summary of a day given
 func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *DailySummary {
 	date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
@@ -99,7 +104,7 @@ func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *Daily
 
 // Reset today summary, for debug or restoring injections
 func (c *Collector) ResetSummaryOfDay() {
-	c.DailySummary = newDailySummary()
+	c.DailySummary = NewDailySummary()
 }
 
 // This function gives the current slot in the 288- 5 minutes interval of the day
@@ -185,8 +190,6 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
 			c.DailySummary.UserAgent.Store(ri.UserAgent, ua.(int)+1)
 		}
 
-		//ADD MORE HERE IF NEEDED
-
 		//Record request URL, if it is a page
 		ext := filepath.Ext(ri.RequestURL)
 
@@ -201,6 +204,8 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
 			c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1)
 		}
 	}()
+
+	//ADD MORE HERE IF NEEDED
 }
 
 // nightly task
@@ -223,7 +228,7 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
 			case <-time.After(duration):
 				// store daily summary to database and reset summary
 				c.SaveSummaryOfDay()
-				c.DailySummary = newDailySummary()
+				c.DailySummary = NewDailySummary()
 			case <-doneCh:
 				// stop the routine
 				return
@@ -234,7 +239,7 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
 	return doneCh
 }
 
-func newDailySummary() *DailySummary {
+func NewDailySummary() *DailySummary {
 	return &DailySummary{
 		TotalRequest:    0,
 		ErrorRequest:    0,
@@ -247,3 +252,30 @@ func newDailySummary() *DailySummary {
 		RequestURL:      &sync.Map{},
 	}
 }
+
+func PrintDailySummary(summary *DailySummary) {
+	summary.ForwardTypes.Range(func(key, value interface{}) bool {
+		println(key.(string), value.(int))
+		return true
+	})
+	summary.RequestOrigin.Range(func(key, value interface{}) bool {
+		println(key.(string), value.(int))
+		return true
+	})
+	summary.RequestClientIp.Range(func(key, value interface{}) bool {
+		println(key.(string), value.(int))
+		return true
+	})
+	summary.Referer.Range(func(key, value interface{}) bool {
+		println(key.(string), value.(int))
+		return true
+	})
+	summary.UserAgent.Range(func(key, value interface{}) bool {
+		println(key.(string), value.(int))
+		return true
+	})
+	summary.RequestURL.Range(func(key, value interface{}) bool {
+		println(key.(string), value.(int))
+		return true
+	})
+}

+ 215 - 0
mod/statistic/statistic_test.go

@@ -0,0 +1,215 @@
+package statistic_test
+
+import (
+	"net"
+	"os"
+	"testing"
+	"time"
+
+	"math/rand"
+
+	"imuslab.com/zoraxy/mod/database"
+	"imuslab.com/zoraxy/mod/database/dbinc"
+	"imuslab.com/zoraxy/mod/geodb"
+	"imuslab.com/zoraxy/mod/statistic"
+)
+
+const test_db_path = "test_db"
+
+func getNewDatabase() *database.Database {
+	db, err := database.NewDatabase(test_db_path, dbinc.BackendLevelDB)
+	if err != nil {
+		panic(err)
+	}
+	db.NewTable("stats")
+	return db
+}
+
+func clearDatabase(db *database.Database) {
+	db.Close()
+	os.RemoveAll(test_db_path)
+}
+
+func TestNewStatisticCollector(t *testing.T) {
+	db := getNewDatabase()
+	defer clearDatabase(db)
+	option := statistic.CollectorOption{Database: db}
+	collector, err := statistic.NewStatisticCollector(option)
+	if err != nil {
+		t.Fatalf("Expected no error, got %v", err)
+	}
+	if collector == nil {
+		t.Fatalf("Expected collector, got nil")
+	}
+
+}
+
+func TestSaveSummaryOfDay(t *testing.T) {
+	db := getNewDatabase()
+	defer clearDatabase(db)
+	option := statistic.CollectorOption{Database: db}
+	collector, _ := statistic.NewStatisticCollector(option)
+	collector.SaveSummaryOfDay()
+	// Add assertions to check if data is saved correctly
+}
+
+func TestLoadSummaryOfDay(t *testing.T) {
+	db := getNewDatabase()
+	defer clearDatabase(db)
+	option := statistic.CollectorOption{Database: db}
+	collector, _ := statistic.NewStatisticCollector(option)
+	year, month, day := time.Now().Date()
+	summary := collector.LoadSummaryOfDay(year, month, day)
+	if summary == nil {
+		t.Fatalf("Expected summary, got nil")
+	}
+}
+
+func TestResetSummaryOfDay(t *testing.T) {
+	db := getNewDatabase()
+	defer clearDatabase(db)
+	option := statistic.CollectorOption{Database: db}
+	collector, _ := statistic.NewStatisticCollector(option)
+	collector.ResetSummaryOfDay()
+	if collector.DailySummary.TotalRequest != 0 {
+		t.Fatalf("Expected TotalRequest to be 0, got %v", collector.DailySummary.TotalRequest)
+	}
+}
+
+func TestGetCurrentRealtimeStatIntervalId(t *testing.T) {
+	db := getNewDatabase()
+	defer clearDatabase(db)
+	option := statistic.CollectorOption{Database: db}
+	collector, _ := statistic.NewStatisticCollector(option)
+	intervalId := collector.GetCurrentRealtimeStatIntervalId()
+	if intervalId < 0 || intervalId > 287 {
+		t.Fatalf("Expected intervalId to be between 0 and 287, got %v", intervalId)
+	}
+}
+
+func TestRecordRequest(t *testing.T) {
+	db := getNewDatabase()
+	defer clearDatabase(db)
+	option := statistic.CollectorOption{Database: db}
+	collector, _ := statistic.NewStatisticCollector(option)
+	requestInfo := statistic.RequestInfo{
+		IpAddr:                        "127.0.0.1",
+		RequestOriginalCountryISOCode: "US",
+		Succ:                          true,
+		StatusCode:                    200,
+		ForwardType:                   "type1",
+		Referer:                       "http://example.com",
+		UserAgent:                     "Mozilla/5.0",
+		RequestURL:                    "/test",
+		Target:                        "target1",
+	}
+	collector.RecordRequest(requestInfo)
+	time.Sleep(1 * time.Second) // Wait for the goroutine to finish
+	if collector.DailySummary.TotalRequest != 1 {
+		t.Fatalf("Expected TotalRequest to be 1, got %v", collector.DailySummary.TotalRequest)
+	}
+}
+
+func TestScheduleResetRealtimeStats(t *testing.T) {
+	db := getNewDatabase()
+	defer clearDatabase(db)
+	option := statistic.CollectorOption{Database: db}
+	collector, _ := statistic.NewStatisticCollector(option)
+	stopChan := collector.ScheduleResetRealtimeStats()
+	if stopChan == nil {
+		t.Fatalf("Expected stopChan, got nil")
+	}
+	collector.Close()
+}
+
+func TestNewDailySummary(t *testing.T) {
+	summary := statistic.NewDailySummary()
+	if summary.TotalRequest != 0 {
+		t.Fatalf("Expected TotalRequest to be 0, got %v", summary.TotalRequest)
+	}
+	if summary.ForwardTypes == nil {
+		t.Fatalf("Expected ForwardTypes to be initialized, got nil")
+	}
+	if summary.RequestOrigin == nil {
+		t.Fatalf("Expected RequestOrigin to be initialized, got nil")
+	}
+	if summary.RequestClientIp == nil {
+		t.Fatalf("Expected RequestClientIp to be initialized, got nil")
+	}
+	if summary.Referer == nil {
+		t.Fatalf("Expected Referer to be initialized, got nil")
+	}
+	if summary.UserAgent == nil {
+		t.Fatalf("Expected UserAgent to be initialized, got nil")
+	}
+	if summary.RequestURL == nil {
+		t.Fatalf("Expected RequestURL to be initialized, got nil")
+	}
+}
+
+func generateTestRequestInfo(db *database.Database) statistic.RequestInfo {
+	//Generate a random IPv4 address
+	randomIpAddr := ""
+	for {
+		ip := net.IPv4(byte(rand.Intn(256)), byte(rand.Intn(256)), byte(rand.Intn(256)), byte(rand.Intn(256)))
+		if !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsMulticast() && !ip.IsUnspecified() {
+			randomIpAddr = ip.String()
+			break
+		}
+	}
+
+	//Resolve the country code for this IP
+	ipLocation := "unknown"
+	geoIpResolver, err := geodb.NewGeoDb(db, &geodb.StoreOptions{
+		AllowSlowIpv4LookUp: false,
+		AllowSlowIpv6Lookup: true, //Just to save some RAM
+	})
+
+	if err == nil {
+		ipInfo, _ := geoIpResolver.ResolveCountryCodeFromIP(randomIpAddr)
+		ipLocation = ipInfo.CountryIsoCode
+	}
+
+	forwardType := "host-http"
+	//Generate a random forward type between "subdomain-http" and "host-https"
+	if rand.Intn(2) == 1 {
+		forwardType = "subdomain-http"
+	}
+
+	//Generate 5 random refers URL and pick from there
+	referers := []string{"https://example.com", "https://example.org", "https://example.net", "https://example.io", "https://example.co"}
+	referer := referers[rand.Intn(5)]
+
+	return statistic.RequestInfo{
+		IpAddr:                        randomIpAddr,
+		RequestOriginalCountryISOCode: ipLocation,
+		Succ:                          true,
+		StatusCode:                    200,
+		ForwardType:                   forwardType,
+		Referer:                       referer,
+		UserAgent:                     "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
+		RequestURL:                    "/benchmark",
+		Target:                        "test.imuslab.internal",
+	}
+}
+
+func BenchmarkRecordRequest(b *testing.B) {
+	db := getNewDatabase()
+	defer clearDatabase(db)
+
+	option := statistic.CollectorOption{Database: db}
+	collector, _ := statistic.NewStatisticCollector(option)
+	var requestInfo statistic.RequestInfo = generateTestRequestInfo(db)
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		collector.RecordRequest(requestInfo)
+		collector.SaveSummaryOfDay()
+	}
+
+	//Write the current in-memory summary to database file
+	b.StopTimer()
+
+	//Print the generated summary
+	//testSummary := collector.GetCurrentDailySummary()
+	//statistic.PrintDailySummary(testSummary)
+}

BIN
sys_db/000038.log


BIN
sys_db/000040.ldb


BIN
sys_db/000053.ldb


BIN
sys_db/000054.ldb


BIN
sys_db/000057.ldb


BIN
sys_db/000058.log


+ 1 - 1
sys_db/CURRENT

@@ -1 +1 @@
-MANIFEST-000039
+MANIFEST-000059

+ 1 - 1
sys_db/CURRENT.bak

@@ -1 +1 @@
-MANIFEST-000036
+MANIFEST-000056

+ 79 - 0
sys_db/LOG

@@ -176,3 +176,82 @@
 23:28:41.223241 table@remove removed @27
 23:32:26.251358 db@close closing
 23:32:26.251358 db@close done T·0s
+=============== Dec 8, 2024 (CST) ===============
+11:14:00.988912 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+11:14:01.006433 version@stat F·[0 1] S·584B[0B 584B] Sc·[0.00 0.00]
+11:14:01.006433 db@open opening
+11:14:01.006433 journal@recovery F·1
+11:14:01.007470 journal@recovery recovering @38
+11:14:01.016342 memdb@flush created L0@41 N·2 S·268B "sta.._06,v42":"web..led,v41"
+11:14:01.020366 version@stat F·[1 1] S·852B[268B 584B] Sc·[0.25 0.00]
+11:14:01.024726 db@janitor F·4 G·0
+11:14:01.024726 db@open done T·18.2927ms
+11:19:58.302848 db@close closing
+11:19:58.307100 db@close done T·4.2519ms
+=============== Dec 8, 2024 (CST) ===============
+11:20:00.760188 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+11:20:00.760705 version@stat F·[1 1] S·852B[268B 584B] Sc·[0.25 0.00]
+11:20:00.760705 db@open opening
+11:20:00.760705 journal@recovery F·1
+11:20:00.760705 journal@recovery recovering @42
+11:20:00.762264 memdb@flush created L0@44 N·2 S·268B "sta.._08,v45":"web..led,v44"
+11:20:00.762790 version@stat F·[2 1] S·1KiB[536B 584B] Sc·[0.50 0.00]
+11:20:00.767032 db@janitor F·5 G·0
+11:20:00.767032 db@open done T·6.3266ms
+11:26:15.400918 db@close closing
+11:26:15.400918 db@close done T·0s
+=============== Dec 8, 2024 (CST) ===============
+11:26:16.230285 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+11:26:16.230968 version@stat F·[2 1] S·1KiB[536B 584B] Sc·[0.50 0.00]
+11:26:16.230968 db@open opening
+11:26:16.230968 journal@recovery F·1
+11:26:16.230968 journal@recovery recovering @45
+11:26:16.231998 memdb@flush created L0@47 N·4 S·380B "set..und,v49":"web..led,v47"
+11:26:16.231998 version@stat F·[3 1] S·1KiB[916B 584B] Sc·[0.75 0.00]
+11:26:16.234084 db@janitor F·6 G·0
+11:26:16.234084 db@open done T·3.1157ms
+11:33:39.978597 db@close closing
+11:33:39.978597 db@close done T·0s
+=============== Dec 8, 2024 (CST) ===============
+11:33:41.322406 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+11:33:41.322916 version@stat F·[3 1] S·1KiB[916B 584B] Sc·[0.75 0.00]
+11:33:41.322916 db@open opening
+11:33:41.322916 journal@recovery F·1
+11:33:41.322916 journal@recovery recovering @48
+11:33:41.325069 memdb@flush created L0@50 N·2 S·336B "sta.._08,v53":"web..led,v52"
+11:33:41.325069 version@stat F·[4 1] S·1KiB[1KiB 584B] Sc·[1.00 0.00]
+11:33:41.327136 db@janitor F·7 G·0
+11:33:41.327136 db@open done T·4.2196ms
+11:33:41.327653 table@compaction L0·4 -> L1·1 S·1KiB Q·54
+11:33:41.336308 table@build created L1@53 N·9 S·718B "aut../TC,v3":"web..led,v52"
+11:33:41.336826 version@stat F·[0 1] S·718B[0B 718B] Sc·[0.00 0.00]
+11:33:41.337345 table@compaction committed F-4 S-1KiB Ke·0 D·7 T·9.1897ms
+11:33:41.337345 table@remove removed @50
+11:33:41.337860 table@remove removed @47
+11:33:41.337860 table@remove removed @44
+11:33:41.337860 table@remove removed @41
+11:33:41.337860 table@remove removed @40
+11:39:14.185455 db@close closing
+11:39:14.185455 db@close done T·0s
+=============== Dec 8, 2024 (CST) ===============
+11:39:30.607651 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+11:39:30.608172 version@stat F·[0 1] S·718B[0B 718B] Sc·[0.00 0.00]
+11:39:30.608172 db@open opening
+11:39:30.608172 journal@recovery F·1
+11:39:30.608737 journal@recovery recovering @51
+11:39:30.610330 memdb@flush created L0@54 N·2 S·468B "sta.._08,v56":"web..led,v55"
+11:39:30.610330 version@stat F·[1 1] S·1KiB[468B 718B] Sc·[0.25 0.00]
+11:39:30.612393 db@janitor F·4 G·0
+11:39:30.612393 db@open done T·4.221ms
+12:21:04.938237 db@close closing
+12:21:04.941347 db@close done T·3.1096ms
+=============== Dec 8, 2024 (CST) ===============
+12:43:43.226917 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+12:43:43.227917 version@stat F·[1 1] S·1KiB[468B 718B] Sc·[0.25 0.00]
+12:43:43.227917 db@open opening
+12:43:43.227917 journal@recovery F·1
+12:43:43.227917 journal@recovery recovering @55
+12:43:43.228917 memdb@flush created L0@57 N·2 S·717B "sta.._08,v59":"web..led,v58"
+12:43:43.228917 version@stat F·[2 1] S·1KiB[1KiB 718B] Sc·[0.50 0.00]
+12:43:43.230920 db@janitor F·5 G·0
+12:43:43.230920 db@open done T·3.0036ms

BIN
sys_db/MANIFEST-000039


BIN
sys_db/MANIFEST-000059


+ 3 - 0
tools/flow-emulator/go.mod

@@ -0,0 +1,3 @@
+module imuslab.com/zoraxy/benchmark
+
+go 1.23.2

+ 51 - 0
tools/flow-emulator/main.go

@@ -0,0 +1,51 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+)
+
+var (
+	//Global variables
+	stopchan chan bool
+
+	//Runtime flags
+	benchmarkWebserverListeningPort int
+)
+
+func init() {
+	flag.IntVar(&benchmarkWebserverListeningPort, "port", 8123, "Port to listen on")
+	flag.Parse()
+}
+
+/* SIGTERM handler, do shutdown sequences before closing */
+func SetupCloseHandler() {
+	c := make(chan os.Signal, 2)
+	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		<-c
+		//Stop all request loops
+		fmt.Println("Stopping request generators")
+		if stopchan != nil {
+			stopchan <- true
+		}
+
+		// Wait for all goroutines to finish
+		time.Sleep(1 * time.Second)
+		os.Exit(0)
+	}()
+}
+
+func main() {
+	//Setup the SIGTERM handler
+	SetupCloseHandler()
+	//Start the web server
+	fmt.Println("Starting web server on port", benchmarkWebserverListeningPort)
+	fmt.Println("In Zoraxy, point your test proxy rule to this server at the given port")
+	startWebServer()
+	select {}
+}

+ 42 - 0
tools/flow-emulator/server.go

@@ -0,0 +1,42 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+)
+
+// Start the web server for reciving test request
+// in Zoraxy, point test.localhost to this server at the given port in the start variables
+func startWebServer() {
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		// Print the request details to console
+		fmt.Printf("Timestamp: %s\n", time.Now().Format(time.RFC1123))
+		fmt.Printf("Request type: %s\n", r.Method)
+		fmt.Printf("Payload size: %d bytes\n", r.ContentLength)
+		fmt.Printf("Request URI: %s\n", r.RequestURI)
+		fmt.Printf("User Agent: %s\n", r.UserAgent())
+		fmt.Printf("Remote Address: %s\n", r.RemoteAddr)
+		fmt.Println("----------------------------------------")
+
+		//Set header to text
+		w.Header().Set("Content-Type", "text/plain")
+		// Send response, print the request details to web page
+		w.Write([]byte("----------------------------------------\n"))
+		w.Write([]byte("Request type: " + r.Method + "\n"))
+		w.Write([]byte(fmt.Sprintf("Payload size: %d bytes\n", r.ContentLength)))
+		w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+		w.Write([]byte("User Agent: " + r.UserAgent() + "\n"))
+		w.Write([]byte("Remote Address: " + r.RemoteAddr + "\n"))
+		w.Write([]byte("----------------------------------------\n"))
+	})
+
+	go func() {
+		err := http.ListenAndServe(fmt.Sprintf(":%d", benchmarkWebserverListeningPort), nil)
+		if err != nil {
+			fmt.Printf("Failed to start server: %v\n", err)
+			stopchan <- true
+		}
+	}()
+
+}

+ 2 - 0
web/main.css

@@ -65,6 +65,8 @@ body{
     height: calc(100% - 51px);
     overflow-y: auto;
     width: 240px;
+    position: sticky;
+    top: 4em;
 }
 
 .contentWindow{