mirror of
https://github.com/xjasonlyu/tun2socks.git
synced 2025-10-06 09:16:58 +08:00
Refactor(log): use go.uber.org/zap
(#389)
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
package observable
|
|
||||||
|
|
||||||
type Iterable <-chan any
|
|
@@ -1,65 +0,0 @@
|
|||||||
package observable
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Observable struct {
|
|
||||||
iterable Iterable
|
|
||||||
listener map[Subscription]*Subscriber
|
|
||||||
mux sync.Mutex
|
|
||||||
done bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Observable) process() {
|
|
||||||
for item := range o.iterable {
|
|
||||||
o.mux.Lock()
|
|
||||||
for _, sub := range o.listener {
|
|
||||||
sub.Emit(item)
|
|
||||||
}
|
|
||||||
o.mux.Unlock()
|
|
||||||
}
|
|
||||||
o.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Observable) close() {
|
|
||||||
o.mux.Lock()
|
|
||||||
defer o.mux.Unlock()
|
|
||||||
|
|
||||||
o.done = true
|
|
||||||
for _, sub := range o.listener {
|
|
||||||
sub.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Observable) Subscribe() (Subscription, error) {
|
|
||||||
o.mux.Lock()
|
|
||||||
defer o.mux.Unlock()
|
|
||||||
if o.done {
|
|
||||||
return nil, errors.New("observable is closed")
|
|
||||||
}
|
|
||||||
subscriber := newSubscriber()
|
|
||||||
o.listener[subscriber.Out()] = subscriber
|
|
||||||
return subscriber.Out(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Observable) UnSubscribe(sub Subscription) {
|
|
||||||
o.mux.Lock()
|
|
||||||
defer o.mux.Unlock()
|
|
||||||
subscriber, exist := o.listener[sub]
|
|
||||||
if !exist {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delete(o.listener, sub)
|
|
||||||
subscriber.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewObservable(any Iterable) *Observable {
|
|
||||||
observable := &Observable{
|
|
||||||
iterable: any,
|
|
||||||
listener: map[Subscription]*Subscriber{},
|
|
||||||
}
|
|
||||||
go observable.process()
|
|
||||||
return observable
|
|
||||||
}
|
|
@@ -1,148 +0,0 @@
|
|||||||
package observable
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"go.uber.org/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
func iterator(item []any) chan any {
|
|
||||||
ch := make(chan any)
|
|
||||||
go func() {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
for _, elm := range item {
|
|
||||||
ch <- elm
|
|
||||||
}
|
|
||||||
close(ch)
|
|
||||||
}()
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestObservable(t *testing.T) {
|
|
||||||
iter := iterator([]any{1, 2, 3, 4, 5})
|
|
||||||
src := NewObservable(iter)
|
|
||||||
data, err := src.Subscribe()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
count := 0
|
|
||||||
for range data {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
assert.Equal(t, count, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestObservable_MultiSubscribe(t *testing.T) {
|
|
||||||
iter := iterator([]any{1, 2, 3, 4, 5})
|
|
||||||
src := NewObservable(iter)
|
|
||||||
ch1, _ := src.Subscribe()
|
|
||||||
ch2, _ := src.Subscribe()
|
|
||||||
count := atomic.NewInt32(0)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(2)
|
|
||||||
waitCh := func(ch <-chan any) {
|
|
||||||
for range ch {
|
|
||||||
count.Inc()
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}
|
|
||||||
go waitCh(ch1)
|
|
||||||
go waitCh(ch2)
|
|
||||||
wg.Wait()
|
|
||||||
assert.Equal(t, int32(10), count.Load())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestObservable_UnSubscribe(t *testing.T) {
|
|
||||||
iter := iterator([]any{1, 2, 3, 4, 5})
|
|
||||||
src := NewObservable(iter)
|
|
||||||
data, err := src.Subscribe()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
src.UnSubscribe(data)
|
|
||||||
_, open := <-data
|
|
||||||
assert.False(t, open)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestObservable_SubscribeClosedSource(t *testing.T) {
|
|
||||||
iter := iterator([]any{1})
|
|
||||||
src := NewObservable(iter)
|
|
||||||
data, _ := src.Subscribe()
|
|
||||||
<-data
|
|
||||||
|
|
||||||
_, closed := src.Subscribe()
|
|
||||||
assert.NotNil(t, closed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
|
|
||||||
sub := Subscription(make(chan any))
|
|
||||||
iter := iterator([]any{1})
|
|
||||||
src := NewObservable(iter)
|
|
||||||
src.UnSubscribe(sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
|
||||||
iter := iterator([]any{1, 2, 3, 4, 5})
|
|
||||||
src := NewObservable(iter)
|
|
||||||
max := 100
|
|
||||||
|
|
||||||
var list []Subscription
|
|
||||||
for i := 0; i < max; i++ {
|
|
||||||
ch, _ := src.Subscribe()
|
|
||||||
list = append(list, ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(max)
|
|
||||||
waitCh := func(ch <-chan any) {
|
|
||||||
for range ch {
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ch := range list {
|
|
||||||
go waitCh(ch)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
for _, sub := range list {
|
|
||||||
_, more := <-sub
|
|
||||||
assert.False(t, more)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(list) > 0 {
|
|
||||||
_, more := <-list[0]
|
|
||||||
assert.False(t, more)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Benchmark_Observable_1000(b *testing.B) {
|
|
||||||
ch := make(chan any)
|
|
||||||
o := NewObservable(ch)
|
|
||||||
num := 1000
|
|
||||||
|
|
||||||
var subs []Subscription
|
|
||||||
for i := 0; i < num; i++ {
|
|
||||||
sub, _ := o.Subscribe()
|
|
||||||
subs = append(subs, sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(num)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for _, sub := range subs {
|
|
||||||
go func(s Subscription) {
|
|
||||||
for range s {
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}(sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
ch <- i
|
|
||||||
}
|
|
||||||
|
|
||||||
close(ch)
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
@@ -1,33 +0,0 @@
|
|||||||
package observable
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Subscription <-chan any
|
|
||||||
|
|
||||||
type Subscriber struct {
|
|
||||||
buffer chan any
|
|
||||||
once sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subscriber) Emit(item any) {
|
|
||||||
s.buffer <- item
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subscriber) Out() Subscription {
|
|
||||||
return s.buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subscriber) Close() {
|
|
||||||
s.once.Do(func() {
|
|
||||||
close(s.buffer)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSubscriber() *Subscriber {
|
|
||||||
sub := &Subscriber{
|
|
||||||
buffer: make(chan any, 200),
|
|
||||||
}
|
|
||||||
return sub
|
|
||||||
}
|
|
@@ -109,7 +109,7 @@ func general(k *Key) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.SetLevel(level)
|
log.SetLogger(log.Must(log.NewLeveled(level)))
|
||||||
|
|
||||||
if k.Interface != "" {
|
if k.Interface != "" {
|
||||||
iface, err := net.InterfaceByName(k.Interface)
|
iface, err := net.InterfaceByName(k.Interface)
|
||||||
@@ -156,7 +156,7 @@ func restAPI(k *Key) error {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := restapi.Start(host, token); err != nil {
|
if err := restapi.Start(host, token); err != nil {
|
||||||
log.Warnf("[RESTAPI] failed to start: %v", err)
|
log.Errorf("[RESTAPI] failed to start: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
log.Infof("[RESTAPI] serve at: %s", u)
|
log.Infof("[RESTAPI] serve at: %s", u)
|
||||||
@@ -175,7 +175,7 @@ func netstack(k *Key) (err error) {
|
|||||||
if k.TUNPreUp != "" {
|
if k.TUNPreUp != "" {
|
||||||
log.Infof("[TUN] pre-execute command: `%s`", k.TUNPreUp)
|
log.Infof("[TUN] pre-execute command: `%s`", k.TUNPreUp)
|
||||||
if preUpErr := execCommand(k.TUNPreUp); preUpErr != nil {
|
if preUpErr := execCommand(k.TUNPreUp); preUpErr != nil {
|
||||||
log.Warnf("[TUN] failed to pre-execute: %s: %v", k.TUNPreUp, preUpErr)
|
log.Errorf("[TUN] failed to pre-execute: %s: %v", k.TUNPreUp, preUpErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ func netstack(k *Key) (err error) {
|
|||||||
}
|
}
|
||||||
log.Infof("[TUN] post-execute command: `%s`", k.TUNPostUp)
|
log.Infof("[TUN] post-execute command: `%s`", k.TUNPostUp)
|
||||||
if postUpErr := execCommand(k.TUNPostUp); postUpErr != nil {
|
if postUpErr := execCommand(k.TUNPostUp); postUpErr != nil {
|
||||||
log.Warnf("[TUN] failed to post-execute: %s: %v", k.TUNPostUp, postUpErr)
|
log.Errorf("[TUN] failed to post-execute: %s: %v", k.TUNPostUp, postUpErr)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
4
go.mod
4
go.mod
@@ -14,10 +14,10 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/schema v1.4.1
|
github.com/gorilla/schema v1.4.1
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/sirupsen/logrus v1.9.3
|
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
go.uber.org/atomic v1.11.0
|
go.uber.org/atomic v1.11.0
|
||||||
go.uber.org/automaxprocs v1.5.3
|
go.uber.org/automaxprocs v1.5.3
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.25.0
|
||||||
golang.org/x/sys v0.22.0
|
golang.org/x/sys v0.22.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.5.0
|
||||||
@@ -30,8 +30,8 @@ require (
|
|||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/net v0.27.0 // indirect
|
golang.org/x/net v0.27.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
)
|
)
|
||||||
|
14
go.sum
14
go.sum
@@ -1,7 +1,5 @@
|
|||||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -32,21 +30,22 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||||
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
@@ -58,7 +57,6 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQX
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gvisor.dev/gvisor v0.0.0-20240713103206-39d6c232e61d h1:dFTIljP/5ReqgM7nMR4DauApFatUaSP8r9btX0sd8a8=
|
gvisor.dev/gvisor v0.0.0-20240713103206-39d6c232e61d h1:dFTIljP/5ReqgM7nMR4DauApFatUaSP8r9btX0sd8a8=
|
||||||
|
2
log/doc.go
Normal file
2
log/doc.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Package log is a thin wrapper based on "go.uber.org/zap".
|
||||||
|
package log
|
@@ -1,7 +1,6 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -9,26 +8,30 @@ import (
|
|||||||
glog "gvisor.dev/gvisor/pkg/log"
|
glog "gvisor.dev/gvisor/pkg/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _globalE = &emitter{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
EnableStackLog(true)
|
glog.SetTarget(_globalE)
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnableStackLog(v bool) {
|
type emitter struct {
|
||||||
if v {
|
logger *SugaredLogger
|
||||||
glog.SetTarget(&emitter{}) // built-in logger
|
|
||||||
} else {
|
|
||||||
glog.SetTarget(&glog.Writer{Next: io.Discard})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type emitter struct{}
|
func (e *emitter) setLogger(logger *SugaredLogger) {
|
||||||
|
e.logger = logger.WithOptions(pkgCallerSkip)
|
||||||
|
}
|
||||||
|
|
||||||
func (emitter) Emit(depth int, level glog.Level, _ time.Time, format string, args ...any) {
|
func (e *emitter) logf(level glog.Level, format string, args ...any) {
|
||||||
|
e.logger.Logf(1-Level(level), "[STACK] "+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *emitter) Emit(depth int, level glog.Level, _ time.Time, format string, args ...any) {
|
||||||
if _, file, line, ok := runtime.Caller(depth + 1); ok {
|
if _, file, line, ok := runtime.Caller(depth + 1); ok {
|
||||||
// Ignore (*gonet.TCPConn).RemoteAddr() warning: `ep.GetRemoteAddress() failed`.
|
// Ignore: gvisor.dev/gvisor/pkg/tcpip/adapters/gonet/gonet.go:457
|
||||||
if line == 457 && strings.HasSuffix(file, "/pkg/tcpip/adapters/gonet/gonet.go") {
|
if line == 457 && strings.HasSuffix(file, "gonet/gonet.go") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logf(Level(level)+2, "[STACK] "+format, args...)
|
e.logf(level, format, args...)
|
||||||
}
|
}
|
||||||
|
39
log/event.go
39
log/event.go
@@ -1,39 +0,0 @@
|
|||||||
package log
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/xjasonlyu/tun2socks/v2/common/observable"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_logCh = make(chan any)
|
|
||||||
_source = observable.NewObservable(_logCh)
|
|
||||||
)
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
Level Level `json:"level"`
|
|
||||||
Message string `json:"msg"`
|
|
||||||
Time time.Time `json:"time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newEvent(level Level, format string, args ...any) *Event {
|
|
||||||
event := &Event{
|
|
||||||
Level: level,
|
|
||||||
Time: time.Now(),
|
|
||||||
Message: fmt.Sprintf(format, args...),
|
|
||||||
}
|
|
||||||
_logCh <- event /* send all events to logCh */
|
|
||||||
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
|
|
||||||
func Subscribe() observable.Subscription {
|
|
||||||
sub, _ := _source.Subscribe()
|
|
||||||
return sub
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnSubscribe(sub observable.Subscription) {
|
|
||||||
_source.UnSubscribe(sub)
|
|
||||||
}
|
|
77
log/level.go
77
log/level.go
@@ -1,72 +1,31 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"go.uber.org/zap/zapcore"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Level uint32
|
// Level is an alias for zapcore.Level.
|
||||||
|
type Level = zapcore.Level
|
||||||
|
|
||||||
|
// Levels are aliases for Level.
|
||||||
const (
|
const (
|
||||||
SilentLevel Level = iota
|
DebugLevel = zapcore.DebugLevel
|
||||||
ErrorLevel
|
InfoLevel = zapcore.InfoLevel
|
||||||
WarnLevel
|
WarnLevel = zapcore.WarnLevel
|
||||||
InfoLevel
|
ErrorLevel = zapcore.ErrorLevel
|
||||||
DebugLevel
|
DPanicLevel = zapcore.DPanicLevel
|
||||||
|
PanicLevel = zapcore.PanicLevel
|
||||||
|
FatalLevel = zapcore.FatalLevel
|
||||||
|
InvalidLevel = zapcore.InvalidLevel
|
||||||
|
SilentLevel = InvalidLevel + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnmarshalJSON deserialize Level with json
|
// ParseLevel is a thin wrapper for zapcore.ParseLevel.
|
||||||
func (level *Level) UnmarshalJSON(data []byte) error {
|
func ParseLevel(text string) (Level, error) {
|
||||||
var lvl string
|
switch text {
|
||||||
if err := json.Unmarshal(data, &lvl); err != nil {
|
case "silent", "SILENT":
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := ParseLevel(lvl)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*level = l
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON serialize Level with json
|
|
||||||
func (level Level) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(level.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (level Level) String() string {
|
|
||||||
switch level {
|
|
||||||
case DebugLevel:
|
|
||||||
return "debug"
|
|
||||||
case InfoLevel:
|
|
||||||
return "info"
|
|
||||||
case WarnLevel:
|
|
||||||
return "warning"
|
|
||||||
case ErrorLevel:
|
|
||||||
return "error"
|
|
||||||
case SilentLevel:
|
|
||||||
return "silent"
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("not a valid level %d", level)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseLevel(lvl string) (Level, error) {
|
|
||||||
switch strings.ToLower(lvl) {
|
|
||||||
case "silent":
|
|
||||||
return SilentLevel, nil
|
return SilentLevel, nil
|
||||||
case "error":
|
|
||||||
return ErrorLevel, nil
|
|
||||||
case "warning":
|
|
||||||
return WarnLevel, nil
|
|
||||||
case "info":
|
|
||||||
return InfoLevel, nil
|
|
||||||
case "debug":
|
|
||||||
return DebugLevel, nil
|
|
||||||
default:
|
default:
|
||||||
return Level(0), fmt.Errorf("not a valid logrus Level: %q", lvl)
|
return zapcore.ParseLevel(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
108
log/log.go
108
log/log.go
@@ -1,63 +1,71 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"fmt"
|
||||||
"os"
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/atomic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// _defaultLevel is package default logging level.
|
// global Logger and SugaredLogger.
|
||||||
var _defaultLevel = atomic.NewUint32(uint32(InfoLevel))
|
var (
|
||||||
|
_globalMu sync.RWMutex
|
||||||
|
_globalL *Logger
|
||||||
|
_globalS *SugaredLogger
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
logrus.SetOutput(os.Stdout)
|
SetLogger(zap.Must(zap.NewProduction()))
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetOutput(out io.Writer) {
|
func NewLeveled(l Level, options ...Option) (*Logger, error) {
|
||||||
logrus.SetOutput(out)
|
switch l {
|
||||||
}
|
case SilentLevel:
|
||||||
|
return zap.NewNop(), nil
|
||||||
func SetLevel(level Level) {
|
|
||||||
_defaultLevel.Store(uint32(level))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Debugf(format string, args ...any) {
|
|
||||||
logf(DebugLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Infof(format string, args ...any) {
|
|
||||||
logf(InfoLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Warnf(format string, args ...any) {
|
|
||||||
logf(WarnLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Errorf(format string, args ...any) {
|
|
||||||
logf(ErrorLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Fatalf(format string, args ...any) {
|
|
||||||
logrus.Fatalf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logf(level Level, format string, args ...any) {
|
|
||||||
event := newEvent(level, format, args...)
|
|
||||||
if uint32(event.Level) > _defaultLevel.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch level {
|
|
||||||
case DebugLevel:
|
case DebugLevel:
|
||||||
logrus.WithTime(event.Time).Debugln(event.Message)
|
return zap.NewDevelopment(options...)
|
||||||
case InfoLevel:
|
case InfoLevel, WarnLevel, ErrorLevel, DPanicLevel, PanicLevel, FatalLevel:
|
||||||
logrus.WithTime(event.Time).Infoln(event.Message)
|
cfg := zap.NewProductionConfig()
|
||||||
case WarnLevel:
|
cfg.Level.SetLevel(l)
|
||||||
logrus.WithTime(event.Time).Warnln(event.Message)
|
return cfg.Build(options...)
|
||||||
case ErrorLevel:
|
default:
|
||||||
logrus.WithTime(event.Time).Errorln(event.Message)
|
return nil, fmt.Errorf("invalid level: %s", l)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLogger sets the global Logger and SugaredLogger.
|
||||||
|
func SetLogger(logger *Logger) {
|
||||||
|
_globalMu.Lock()
|
||||||
|
defer _globalMu.Unlock()
|
||||||
|
// apply pkgCallerSkip to global loggers.
|
||||||
|
_globalL = logger.WithOptions(pkgCallerSkip)
|
||||||
|
_globalS = _globalL.Sugar()
|
||||||
|
_globalE.setLogger(_globalS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logf(lvl Level, template string, args ...any) {
|
||||||
|
_globalMu.RLock()
|
||||||
|
s := _globalS
|
||||||
|
_globalMu.RUnlock()
|
||||||
|
s.Logf(lvl, template, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debugf(template string, args ...any) {
|
||||||
|
logf(DebugLevel, template, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Infof(template string, args ...any) {
|
||||||
|
logf(InfoLevel, template, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warnf(template string, args ...any) {
|
||||||
|
logf(WarnLevel, template, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Errorf(template string, args ...any) {
|
||||||
|
logf(ErrorLevel, template, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatalf(template string, args ...any) {
|
||||||
|
logf(FatalLevel, template, args...)
|
||||||
|
}
|
||||||
|
22
log/zap.go
Normal file
22
log/zap.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Must is an alias for zap.Must.
|
||||||
|
var Must = zap.Must
|
||||||
|
|
||||||
|
// logger aliases for zap.Logger and zap.SugaredLogger.
|
||||||
|
type (
|
||||||
|
Logger = zap.Logger
|
||||||
|
SugaredLogger = zap.SugaredLogger
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Option is an alias for zap.Option.
|
||||||
|
Option = zap.Option
|
||||||
|
)
|
||||||
|
|
||||||
|
// pkgCallerSkip skips the pkg wrapper code as the caller.
|
||||||
|
var pkgCallerSkip = zap.AddCallerSkip(2)
|
2
main.go
2
main.go
@@ -30,7 +30,7 @@ func init() {
|
|||||||
flag.StringVar(&configFile, "config", "", "YAML format configuration file")
|
flag.StringVar(&configFile, "config", "", "YAML format configuration file")
|
||||||
flag.StringVar(&key.Device, "device", "", "Use this device [driver://]name")
|
flag.StringVar(&key.Device, "device", "", "Use this device [driver://]name")
|
||||||
flag.StringVar(&key.Interface, "interface", "", "Use network INTERFACE (Linux/MacOS only)")
|
flag.StringVar(&key.Interface, "interface", "", "Use network INTERFACE (Linux/MacOS only)")
|
||||||
flag.StringVar(&key.LogLevel, "loglevel", "info", "Log level [debug|info|warning|error|silent]")
|
flag.StringVar(&key.LogLevel, "loglevel", "info", "Log level [debug|info|warn|error|silent]")
|
||||||
flag.StringVar(&key.Proxy, "proxy", "", "Use this proxy [protocol://]host[:port]")
|
flag.StringVar(&key.Proxy, "proxy", "", "Use this proxy [protocol://]host[:port]")
|
||||||
flag.StringVar(&key.RestAPI, "restapi", "", "HTTP statistic server listen address")
|
flag.StringVar(&key.RestAPI, "restapi", "", "HTTP statistic server listen address")
|
||||||
flag.StringVar(&key.TCPSendBufferSize, "tcp-sndbuf", "", "Set TCP send buffer size for netstack")
|
flag.StringVar(&key.TCPSendBufferSize, "tcp-sndbuf", "", "Set TCP send buffer size for netstack")
|
||||||
|
@@ -17,7 +17,7 @@ import (
|
|||||||
const defaultInterval = 1000
|
const defaultInterval = 1000
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
registerMountPoint("/connections", connectionRouter())
|
registerEndpoint("/connections", connectionRouter())
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectionRouter() http.Handler {
|
func connectionRouter() http.Handler {
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
registerMountPoint("/debug/pprof/", pprofRouter())
|
registerEndpoint("/debug/pprof/", pprofRouter())
|
||||||
}
|
}
|
||||||
|
|
||||||
func pprofRouter() http.Handler {
|
func pprofRouter() http.Handler {
|
||||||
|
@@ -19,7 +19,7 @@ func SetStatsFunc(s func() tcpip.Stats) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
registerMountPoint("/netstats", http.HandlerFunc(getNetStats))
|
registerEndpoint("/netstats", http.HandlerFunc(getNetStats))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNetStats(w http.ResponseWriter, r *http.Request) {
|
func getNetStats(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
V "github.com/xjasonlyu/tun2socks/v2/internal/version"
|
V "github.com/xjasonlyu/tun2socks/v2/internal/version"
|
||||||
"github.com/xjasonlyu/tun2socks/v2/log"
|
|
||||||
"github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
|
"github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,11 +24,11 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_mountPoints = make(map[string]http.Handler)
|
_endpoints = make(map[string]http.Handler)
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerMountPoint(pattern string, handler http.Handler) {
|
func registerEndpoint(pattern string, handler http.Handler) {
|
||||||
_mountPoints[pattern] = handler
|
_endpoints[pattern] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(addr, token string) error {
|
func Start(addr, token string) error {
|
||||||
@@ -46,11 +45,10 @@ func Start(addr, token string) error {
|
|||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(authenticator(token))
|
r.Use(authenticator(token))
|
||||||
r.Get("/", hello)
|
r.Get("/", hello)
|
||||||
r.Get("/logs", getLogs)
|
|
||||||
r.Get("/traffic", traffic)
|
r.Get("/traffic", traffic)
|
||||||
r.Get("/version", version)
|
r.Get("/version", version)
|
||||||
// attach HTTP handlers
|
// attach HTTP handlers
|
||||||
for pattern, handler := range _mountPoints {
|
for pattern, handler := range _endpoints {
|
||||||
r.Mount(pattern, handler)
|
r.Mount(pattern, handler)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -103,61 +101,6 @@ func authenticator(token string) func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLogs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
lvl := r.URL.Query().Get("level")
|
|
||||||
if lvl == "" {
|
|
||||||
lvl = "info" /* default */
|
|
||||||
}
|
|
||||||
|
|
||||||
level, err := log.ParseLevel(lvl)
|
|
||||||
if err != nil {
|
|
||||||
render.Status(r, http.StatusBadRequest)
|
|
||||||
render.JSON(w, r, ErrBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var wsConn *websocket.Conn
|
|
||||||
if websocket.IsWebSocketUpgrade(r) {
|
|
||||||
wsConn, err = _upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if wsConn == nil {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
render.Status(r, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
sub := log.Subscribe()
|
|
||||||
defer log.UnSubscribe(sub)
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
for elm := range sub {
|
|
||||||
buf.Reset()
|
|
||||||
|
|
||||||
e := elm.(*log.Event)
|
|
||||||
if e.Level > level {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = json.NewEncoder(buf).Encode(e); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if wsConn == nil {
|
|
||||||
_, err = w.Write(buf.Bytes())
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
} else {
|
|
||||||
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func traffic(w http.ResponseWriter, r *http.Request) {
|
func traffic(w http.ResponseWriter, r *http.Request) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
|
Reference in New Issue
Block a user