add new file

This commit is contained in:
he liu 2022-01-23 17:30:38 +08:00
parent 05b2e55f39
commit c482967f8c
125 changed files with 9688 additions and 0 deletions

View File

@ -0,0 +1,23 @@
package bridge
import (
"crypto/tls"
"github.com/lucas-clemente/quic-go"
"net"
)
func StartTcpBridge(ln net.Listener, config *tls.Config, serverCheck, clientCheck func(string) bool) error {
h, err := NewTcpServer(ln, config, serverCheck, clientCheck)
if err != nil {
return err
}
return h.run()
}
func StartQUICBridge(ln net.PacketConn, config *tls.Config, quicConfig *quic.Config, clientCheck func(string) bool) error {
h, err := NewQUICServer(ln, config, quicConfig, clientCheck)
if err != nil {
return err
}
return h.run()
}

102
component/bridge/client.go Normal file
View File

@ -0,0 +1,102 @@
package bridge
import (
"context"
"ehang.io/nps/lib/logger"
"ehang.io/nps/lib/pb"
"ehang.io/nps/transport"
"github.com/pkg/errors"
"go.uber.org/zap"
"net"
"net/http"
"time"
)
type client struct {
*manager
tunnelId string
clientId string
control transport.Conn
data transport.Conn
httpClient *http.Client
pingErrTimes int
}
func NewClient(tunnelId string, clientId string, mg *manager) *client {
c := &client{tunnelId: tunnelId, clientId: clientId, manager: mg}
c.httpClient = &http.Client{Transport: &http.Transport{DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return c.NewControlConn()
}}}
go c.doPing()
return c
}
func (c *client) SetTunnel(conn transport.Conn, control bool) {
if control {
c.control = conn
} else {
c.data = conn
}
}
func (c *client) NewDataConn() (net.Conn, error) {
if c.data == nil {
return nil, errors.New("the data tunnel is not exist")
}
return c.data.Open()
}
func (c *client) NewControlConn() (net.Conn, error) {
if c.control == nil {
return nil, errors.New("the data tunnel is not exist")
}
return c.control.Open()
}
func (c *client) doPing() {
for range time.NewTicker(time.Second * 5).C {
if err := c.ping(); err != nil {
c.pingErrTimes++
logger.Error("do ping error", zap.Error(err))
} else {
logger.Debug("do ping success", zap.String("client id", c.clientId), zap.String("tunnel id", c.tunnelId))
c.pingErrTimes = 0
}
if c.pingErrTimes > 3 {
logger.Error("ping failed, close")
c.close()
return
}
}
}
func (c *client) close() {
if c.data != nil {
_ = c.data.Close()
}
if c.control != nil {
_ = c.control.Close()
}
_ = c.RemoveClient(c)
}
func (c *client) ping() error {
conn, err := c.NewDataConn()
if err != nil {
return errors.Wrap(err, "data ping")
}
_, err = pb.WriteMessage(conn, &pb.ClientRequest{ConnType: &pb.ClientRequest_Ping{Ping: &pb.Ping{Now: time.Now().String()}}})
if err != nil {
return errors.Wrap(err, "data ping")
}
_, err = pb.ReadMessage(conn, &pb.Ping{})
if err != nil {
return errors.Wrap(err, "data ping")
}
resp, err := c.httpClient.Get("http://nps.ehang.io/ping")
if err != nil {
return errors.Wrap(err, "control ping")
}
_ = resp.Body.Close()
return nil
}

View File

@ -0,0 +1,60 @@
package bridge
import (
"ehang.io/nps/lib/lb"
"ehang.io/nps/lib/logger"
"ehang.io/nps/transport"
"go.uber.org/zap"
"net"
"sync"
)
type manager struct {
clients map[string]*client
clientLb *lb.LoadBalancer
sync.Mutex
}
func NewManager() *manager {
return &manager{
clients: make(map[string]*client),
clientLb: lb.NewLoadBalancer(),
}
}
func (m *manager) SetClient(clientId string, tunnelId string, isControl bool, conn transport.Conn) error {
m.Lock()
defer m.Unlock()
client, ok := m.clients[tunnelId]
if !ok {
client = NewClient(tunnelId, clientId, m)
err := m.clientLb.SetClient(clientId, client)
if err != nil {
logger.Error("set client error", zap.Error(err), zap.String("clientId", clientId), zap.String("tunnelId", tunnelId))
return err
}
m.clients[tunnelId] = client
}
client.SetTunnel(conn, isControl)
return nil
}
func (m *manager) GetDataConn(clientId string) (net.Conn, error) {
c, err := m.clientLb.GetClient(clientId)
if err != nil {
return nil, err
}
return c.(*client).NewDataConn()
}
func (m *manager) RemoveClient(client *client) error {
m.Lock()
defer m.Unlock()
err := m.clientLb.RemoveClient(client.clientId, client)
if err != nil {
logger.Error("remove client error", zap.Error(err), zap.String("clientId", client.clientId), zap.String("tunnelId", client.tunnelId))
return err
}
delete(m.clients, client.tunnelId)
return nil
}

91
component/bridge/quic.go Normal file
View File

@ -0,0 +1,91 @@
package bridge
import (
"context"
"crypto/tls"
"ehang.io/nps/lib/logger"
"ehang.io/nps/lib/pb"
"ehang.io/nps/transport"
"github.com/lucas-clemente/quic-go"
"github.com/panjf2000/ants/v2"
"go.uber.org/zap"
"io"
"net"
)
type QUICServer struct {
packetConn net.PacketConn
tlsConfig *tls.Config
config *quic.Config
listener quic.Listener
gp *ants.PoolWithFunc
clientCheck func(string) bool
manager *manager
}
func NewQUICServer(packetConn net.PacketConn, tlsConfig *tls.Config, config *quic.Config, clientCheck func(string) bool) (*QUICServer, error) {
qs := &QUICServer{
packetConn: packetConn,
tlsConfig: tlsConfig,
config: config,
clientCheck: clientCheck,
manager: NewManager(),
}
var err error
if qs.listener, err = quic.Listen(packetConn, tlsConfig, config); err != nil {
return nil, err
}
qs.gp, err = ants.NewPoolWithFunc(1000000, func(i interface{}) {
session := i.(quic.Session)
logger.Debug("accept a session", zap.String("remote addr", session.RemoteAddr().String()))
stream, err := session.AcceptStream(context.Background())
if err != nil {
logger.Warn("accept stream error", zap.Error(err))
_ = session.CloseWithError(0, "check auth failed")
return
}
cr := &pb.ConnRequest{}
_, err = pb.ReadMessage(stream, cr)
if err != nil {
_ = session.CloseWithError(0, "check auth failed")
logger.Warn("read message error", zap.Error(err))
return
}
if !qs.clientCheck(cr.GetId()) {
_ = session.CloseWithError(0, "check auth failed")
logger.Error("check server id error", zap.String("id", cr.GetId()))
_ = qs.responseClient(stream, false, "id check failed")
return
}
qc := transport.NewQUIC(session)
_ = qc.Client()
_ = qs.responseClient(stream, true, "success")
err = qs.manager.SetClient(cr.GetId(), cr.GetNpcInfo().GetTunnelId(), cr.GetNpcInfo().GetIsControlTunnel(), qc)
if err != nil {
_ = session.CloseWithError(0, "check auth failed")
logger.Error("set client error", zap.Error(err), zap.String("info", cr.String()))
}
})
return qs, err
}
func (qs *QUICServer) responseClient(conn io.Writer, success bool, msg string) error {
_, err := pb.WriteMessage(conn, &pb.NpcResponse{Success: success, Message: msg})
return err
}
func (qs *QUICServer) run() error {
for {
session, err := qs.listener.Accept(context.Background())
if err != nil {
logger.Error("accept connection failed", zap.Error(err))
return err
}
err = qs.gp.Invoke(session)
if err != nil {
logger.Error("Invoke session error", zap.Error(err))
continue
}
}
}

116
component/bridge/tcp.go Normal file
View File

@ -0,0 +1,116 @@
package bridge
import (
"crypto/tls"
"ehang.io/nps/lib/logger"
"ehang.io/nps/lib/pb"
"ehang.io/nps/lib/pool"
"ehang.io/nps/transport"
"github.com/panjf2000/ants/v2"
"go.uber.org/zap"
"io"
"net"
"sync"
)
type tcpServer struct {
config *tls.Config // config must contain root ca and server cert
ln net.Listener
gp *ants.PoolWithFunc
manager *manager
serverCheck func(id string) bool
clientCheck func(id string) bool
}
func NewTcpServer(ln net.Listener, config *tls.Config, serverCheck, clientCheck func(string) bool) (*tcpServer, error) {
h := &tcpServer{
config: config,
ln: ln,
serverCheck: serverCheck,
clientCheck: clientCheck,
manager: NewManager(),
}
var err error
h.gp, err = ants.NewPoolWithFunc(1000000, func(i interface{}) {
conn := i.(net.Conn)
cr := &pb.ConnRequest{}
_, err := pb.ReadMessage(conn, cr)
if err != nil {
_ = conn.Close()
logger.Warn("read message error", zap.Error(err))
return
}
switch cr.ConnType.(type) {
case *pb.ConnRequest_AppInfo:
h.handleApp(cr, conn)
case *pb.ConnRequest_NpcInfo:
h.handleClient(cr, conn)
}
})
return h, err
}
func (h *tcpServer) handleApp(cr *pb.ConnRequest, conn net.Conn) {
if !h.serverCheck(cr.GetId()) {
_ = conn.Close()
logger.Error("check server id error", zap.String("id", cr.GetId()))
return
}
clientConn, err := h.manager.GetDataConn(cr.GetAppInfo().GetNpcId())
if err != nil {
logger.Error("get client error", zap.Error(err), zap.String("app_info", cr.String()))
return
}
_, err = pb.WriteMessage(clientConn, &pb.ClientRequest{ConnType: &pb.ClientRequest_AppInfo{AppInfo: cr.GetAppInfo()}})
if err != nil {
_ = clientConn.Close()
_ = conn.Close()
logger.Error("write app_info error", zap.Error(err))
return
}
var wg sync.WaitGroup
wg.Add(2)
_ = pool.CopyConnGoroutinePool.Invoke(pool.CopyConnGpParams{Writer: conn, Reader: clientConn, Wg: &wg})
_ = pool.CopyConnGoroutinePool.Invoke(pool.CopyConnGpParams{Writer: clientConn, Reader: conn, Wg: &wg})
wg.Wait()
}
func (h *tcpServer) responseClient(conn io.Writer, success bool, msg string) error {
_, err := pb.WriteMessage(conn, &pb.NpcResponse{Success: success, Message: msg})
return err
}
func (h *tcpServer) handleClient(cr *pb.ConnRequest, conn net.Conn) {
if !h.clientCheck(cr.GetId()) {
_ = conn.Close()
logger.Error("check server id error", zap.String("id", cr.GetId()))
_ = h.responseClient(conn, false, "id check failed")
return
}
yc := transport.NewYaMux(conn, nil)
err := yc.Client()
if err != nil {
_ = conn.Close()
_ = h.responseClient(conn, false, "client failed")
logger.Error("new yamux client error", zap.Error(err), zap.String("remote address", conn.RemoteAddr().String()))
return
}
_ = h.responseClient(conn, true, "success")
err = h.manager.SetClient(cr.GetId(), cr.GetNpcInfo().GetTunnelId(), cr.GetNpcInfo().GetIsControlTunnel(), yc)
if err != nil {
_ = conn.Close()
logger.Error("set client error", zap.Error(err), zap.String("info", cr.String()))
}
}
func (h *tcpServer) run() error {
h.ln = tls.NewListener(h.ln, h.config)
for {
conn, err := h.ln.Accept()
if err != nil {
logger.Error("Accept conn error", zap.Error(err))
return err
}
_ = h.gp.Invoke(conn)
}
}

129
component/client/client.go Normal file
View File

@ -0,0 +1,129 @@
package client
import (
"ehang.io/nps/core/action"
"ehang.io/nps/core/handler"
"ehang.io/nps/core/process"
"ehang.io/nps/core/rule"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"go.uber.org/zap"
"io"
"net"
"net/http"
"sync"
"sync/atomic"
"time"
)
type Client struct {
controlLn net.Listener
dataLn net.Listener
lastPongTime time.Time
mux *http.ServeMux
ticker *time.Ticker
closeCh chan struct{}
closed int32
wg sync.WaitGroup
}
func NewClient(controlLn, dataLn net.Listener) *Client {
return &Client{
controlLn: controlLn,
dataLn: dataLn,
mux: &http.ServeMux{},
ticker: time.NewTicker(time.Second * 5),
closeCh: make(chan struct{}, 0),
}
}
func (c *Client) ping(writer http.ResponseWriter, request *http.Request) {
c.lastPongTime = time.Now()
_, err := io.WriteString(writer, "pong")
if err != nil {
logger.Warn("write pong error", zap.Error(err))
return
}
logger.Debug("write pong success")
}
func (c *Client) Run() {
c.mux.HandleFunc("/ping", c.ping)
c.wg.Add(3)
go c.handleControlConn()
go c.handleDataConn()
go c.checkPing()
c.wg.Wait()
}
func (c *Client) HasPong() bool {
return time.Now().Sub(c.lastPongTime).Seconds() < 10
}
func (c *Client) checkPing() {
for {
select {
case <-c.ticker.C:
if !c.lastPongTime.IsZero() && time.Now().Sub(c.lastPongTime).Seconds() > 15 && c.controlLn != nil {
logger.Debug("close connection", zap.Time("lastPongTime", c.lastPongTime), zap.Time("now", time.Now()))
_ = c.controlLn.Close()
}
case <-c.closeCh:
c.wg.Done()
return
}
}
}
func (c *Client) handleDataConn() {
h := &handler.DefaultHandler{}
ac := &action.LocalAction{}
err := ac.Init()
if err != nil {
logger.Warn("init action failed", zap.Error(err))
return
}
appPr := &process.PbAppProcessor{}
_ = appPr.Init(ac)
h.AddRule(&rule.Rule{Handler: h, Process: appPr, Action: ac})
pingPr := &process.PbPingProcessor{}
_ = appPr.Init(ac)
h.AddRule(&rule.Rule{Handler: h, Process: pingPr, Action: ac})
var conn net.Conn
for {
conn, err = c.dataLn.Accept()
if err != nil {
logger.Error("accept connection failed", zap.Error(err))
break
}
go func(conn net.Conn) {
_, err = h.HandleConn(nil, enet.NewReaderConn(conn))
if err != nil {
logger.Warn("process failed", zap.Error(err))
return
}
}(conn)
}
c.wg.Done()
c.Close()
}
func (c *Client) handleControlConn() {
err := http.Serve(c.controlLn, c.mux)
if err != nil {
logger.Error("http error", zap.Error(err))
}
c.wg.Done()
c.Close()
}
func (c *Client) Close() {
if atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
c.closeCh <- struct{}{}
c.ticker.Stop()
_ = c.controlLn.Close()
_ = c.dataLn.Close()
}
}

69
component/client/conn.go Normal file
View File

@ -0,0 +1,69 @@
package client
import (
"crypto/tls"
"ehang.io/nps/lib/pb"
"ehang.io/nps/transport"
"github.com/lucas-clemente/quic-go"
"github.com/pkg/errors"
"io"
"net"
"time"
)
type TunnelCreator interface {
NewMux(bridgeAddr string, message *pb.ConnRequest, config *tls.Config) (net.Listener, error)
}
type BaseTunnelCreator struct{}
func (bc BaseTunnelCreator) handshake(npcInfo *pb.ConnRequest, rw io.ReadWriteCloser) error {
_, err := pb.WriteMessage(rw, npcInfo)
if err != nil {
return errors.Wrap(err, "write handshake message")
}
var resp pb.NpcResponse
_, err = pb.ReadMessage(rw, &resp)
if err != nil || !resp.Success {
return errors.Wrap(err, resp.Message)
}
return nil
}
type TcpTunnelCreator struct{ BaseTunnelCreator }
func (tc TcpTunnelCreator) NewMux(bridgeAddr string, message *pb.ConnRequest, config *tls.Config) (net.Listener, error) {
conn, err := tls.Dial("tcp", bridgeAddr, config)
if err != nil {
return nil, err
}
if err := tc.handshake(message, conn); err != nil {
return nil, err
}
server := transport.NewYaMux(conn, nil)
return server, server.Server()
}
type QUICTunnelCreator struct{ BaseTunnelCreator }
func (tc QUICTunnelCreator) NewMux(bridgeAddr string, message *pb.ConnRequest, config *tls.Config) (net.Listener, error) {
session, err := quic.DialAddr(bridgeAddr, config, &quic.Config{
MaxIncomingStreams: 1000000,
MaxIncomingUniStreams: 1000000,
MaxIdleTimeout: time.Minute,
KeepAlive: true,
})
if err != nil {
return nil, err
}
stream, err := session.OpenStream()
if err != nil {
return nil, err
}
err = tc.handshake(message, stream)
if err != nil {
return nil, err
}
server := transport.NewQUIC(session)
return server, server.Server()
}

44
component/client/npc.go Normal file
View File

@ -0,0 +1,44 @@
package client
import (
"crypto/tls"
"ehang.io/nps/lib/cert"
"ehang.io/nps/lib/logger"
"ehang.io/nps/lib/pb"
uuid "github.com/satori/go.uuid"
"go.uber.org/zap"
)
// StartNpc is used to connect to bridge
// proto is quic or tcp
// tlsConfig must contain a npc cert
func StartNpc(proto string, bridgeAddr string, tlsConfig *tls.Config) error {
id, err := cert.GetCertSnFromConfig(tlsConfig)
if err != nil {
return err
}
var creator TunnelCreator
if proto == "quic" {
creator = QUICTunnelCreator{}
} else {
creator = TcpTunnelCreator{}
}
connId := uuid.NewV1().String()
retry:
logger.Info("start connecting to bridge")
controlLn, err := creator.NewMux(bridgeAddr,
&pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: true}}}, tlsConfig)
if err != nil {
logger.Error("new control connection error", zap.Error(err))
goto retry
}
dataLn, err := creator.NewMux(bridgeAddr,
&pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: false}}}, tlsConfig)
if err != nil {
logger.Error("new data connection error", zap.Error(err))
goto retry
}
c := NewClient(controlLn, dataLn)
c.Run()
goto retry
}

1
component/component.go Normal file
View File

@ -0,0 +1 @@
package component

193
component/component_test.go Normal file
View File

@ -0,0 +1,193 @@
package component
import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"ehang.io/nps/component/bridge"
"ehang.io/nps/component/client"
"ehang.io/nps/lib/cert"
"ehang.io/nps/lib/pb"
"github.com/lucas-clemente/quic-go"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
"io/ioutil"
"net"
"os"
"path/filepath"
"sync"
"testing"
"time"
)
var createCertOnce sync.Once
func createCertFile(t *testing.T) (string, string) {
createCertOnce.Do(func() {
g := cert.NewX509Generator(pkix.Name{
Country: []string{"CN"},
Organization: []string{"Ehang.io"},
OrganizationalUnit: []string{"nps"},
Province: []string{"Beijing"},
CommonName: "nps",
Locality: []string{"Beijing"},
})
cert, key, err := g.CreateRootCa()
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "root_cert.pem"), cert, 0600))
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "root_key.pem"), key, 0600))
assert.NoError(t, g.InitRootCa(cert, key))
cert, key, err = g.CreateCert("bridge.nps.ehang.io")
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "bridge_cert.pem"), cert, 0600))
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "bridge_key.pem"), key, 0600))
cert, key, err = g.CreateCert("client.nps.ehang.io")
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "client_cert.pem"), cert, 0600))
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "client_key.pem"), key, 0600))
})
return filepath.Join(os.TempDir(), "cert.pem"), filepath.Join(os.TempDir(), "key.pem")
}
func TestTcpConnect(t *testing.T) {
createCertFile(t)
bridgeLn, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
buf, err := ioutil.ReadFile(filepath.Join(os.TempDir(), "root_cert.pem"))
assert.NoError(t, err)
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(buf)
crt, err := tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "bridge_cert.pem"), filepath.Join(os.TempDir(), "bridge_key.pem"))
assert.NoError(t, err)
bridgeTlsConfig := &tls.Config{
Certificates: []tls.Certificate{crt},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: pool,
}
crt, err = tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "client_cert.pem"), filepath.Join(os.TempDir(), "client_key.pem"))
assert.NoError(t, err)
clientConfig := &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{crt},
}
go func() {
assert.NoError(t, bridge.StartTcpBridge(bridgeLn, bridgeTlsConfig, func(s string) bool {
return true
}, func(s string) bool {
sn, err := cert.GetCertSnFromConfig(clientConfig)
assert.NoError(t, err)
assert.Equal(t, sn, s)
return true
}))
}()
var c *client.Client
go func() {
id, err := cert.GetCertSnFromConfig(clientConfig)
assert.NoError(t, err)
creator := client.TcpTunnelCreator{}
connId := uuid.NewV1().String()
controlLn, err := creator.NewMux(bridgeLn.Addr().String(),
&pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: true}}}, clientConfig)
assert.NoError(t, err)
dataLn, err := creator.NewMux(bridgeLn.Addr().String(),
&pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: false}}}, clientConfig)
assert.NoError(t, err)
c = client.NewClient(controlLn, dataLn)
c.Run()
}()
timeout := time.NewTimer(time.Second * 30)
ticker := time.NewTicker(time.Millisecond * 100)
for {
select {
case <-ticker.C:
if c != nil && c.HasPong() {
c.Close()
return
}
case <-timeout.C:
t.Fail()
return
}
}
}
func TestQUICConnect(t *testing.T) {
createCertFile(t)
bridgePacketConn, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
buf, err := ioutil.ReadFile(filepath.Join(os.TempDir(), "root_cert.pem"))
assert.NoError(t, err)
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(buf)
crt, err := tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "bridge_cert.pem"), filepath.Join(os.TempDir(), "bridge_key.pem"))
assert.NoError(t, err)
bridgeTlsConfig := &tls.Config{
Certificates: []tls.Certificate{crt},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: pool,
NextProtos: []string{"quic-nps"},
}
crt, err = tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "client_cert.pem"), filepath.Join(os.TempDir(), "client_key.pem"))
assert.NoError(t, err)
clientConfig := &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{crt},
NextProtos: []string{"quic-nps"},
}
go func() {
assert.NoError(t, bridge.StartQUICBridge(bridgePacketConn, bridgeTlsConfig, &quic.Config{
MaxIncomingStreams: 1000000,
MaxIncomingUniStreams: 1000000,
MaxIdleTimeout: time.Minute,
KeepAlive: true,
}, func(s string) bool {
sn, err := cert.GetCertSnFromConfig(clientConfig)
assert.NoError(t, err)
assert.Equal(t, sn, s)
return true
}))
}()
var c *client.Client
go func() {
id, err := cert.GetCertSnFromConfig(clientConfig)
assert.NoError(t, err)
creator := client.QUICTunnelCreator{}
connId := uuid.NewV1().String()
controlLn, err := creator.NewMux(bridgePacketConn.LocalAddr().String(),
&pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: true}}}, clientConfig)
assert.NoError(t, err)
dataLn, err := creator.NewMux(bridgePacketConn.LocalAddr().String(),
&pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: false}}}, clientConfig)
assert.NoError(t, err)
assert.NotEmpty(t, dataLn)
assert.NotEmpty(t, controlLn)
c = client.NewClient(controlLn, dataLn)
c.Run()
}()
timeout := time.NewTimer(time.Second * 30)
ticker := time.NewTicker(time.Millisecond * 100)
for {
select {
case <-ticker.C:
if c != nil && c.HasPong() {
c.Close()
return
}
case <-timeout.C:
t.Fail()
return
}
}
}

@ -0,0 +1 @@
Subproject commit 07c2567751c711541a4ff24af00f2cd018c18eab

View File

@ -0,0 +1,92 @@
package controller
import (
"crypto/x509/pkix"
"ehang.io/nps/lib/cert"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
uuid "github.com/satori/go.uuid"
"net/http"
)
type certServe struct {
baseController
cg *cert.X509Generator
}
func (cs *certServe) Init(rootCert []byte, rootKey []byte) error {
cs.cg = cert.NewX509Generator(pkix.Name{
Country: []string{"cn"},
Organization: []string{"ehang"},
OrganizationalUnit: []string{"nps"},
Province: []string{"beijing"},
CommonName: "nps",
Locality: []string{"beijing"},
})
return cs.cg.InitRootCa(rootCert, rootKey)
}
type certInfo struct {
Name string `json:"name"`
Uuid string `json:"uuid"`
CertType string `json:"cert_type"`
Cert string `json:"cert"`
Key string `json:"key"`
Sn string `json:"sn"`
Remark string `json:"remark"`
Status int `json:"status"`
}
// Create
// Cert type root|bridge|server|client|secret
func (cs *certServe) Create(c *gin.Context) {
var ci certInfo
err := c.BindJSON(&ci)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
crt, key, err := cs.cg.CreateCert(fmt.Sprintf("%s.nps.ehang.io", ci.CertType))
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
sn, err := cert.GetCertSnFromEncode(crt)
ci.Cert, ci.Key, ci.Sn, ci.Uuid = string(crt), string(key), sn, uuid.NewV4().String()
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
b, err := json.Marshal(ci)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
err = cs.db.Insert("cert", ci.Uuid, string(b))
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
}
func (cs *certServe) Update(c *gin.Context) {
var ci certInfo
err := c.BindJSON(&ci)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
b, err := json.Marshal(ci)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
err = cs.db.Update(cs.tableName, ci.Uuid, string(b))
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
}

View File

@ -0,0 +1,50 @@
package controller
import (
"ehang.io/nps/db"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type configServer struct {
db db.Db
}
func (cs *configServer) ChangeSystemConfig(c *gin.Context) {
type config struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
NewUsername string `json:"new_username"`
}
var cfg config
err := c.BindJSON(&cfg)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
oldPassword, err := cs.db.GetConfig("admin_pass")
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
fmt.Println(cfg, oldPassword)
if cfg.OldPassword != oldPassword {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": "old password does not match"})
return
}
if err := cs.db.SetConfig("admin_pass", cfg.NewPassword); err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
if cfg.NewUsername != "" {
if err := cs.db.SetConfig("admin_pass", cfg.NewUsername); err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "ok",
})
}

View File

@ -0,0 +1,101 @@
package controller
import (
"ehang.io/nps/db"
"ehang.io/nps/lib/logger"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net"
"net/http"
)
func StartController(ln net.Listener, db db.Db, rootCert []byte, rootKey []byte, staticRootPath string, pagePath string) error {
gin.SetMode(gin.ReleaseMode)
cfgServer := &configServer{db: db}
crtServer := &certServe{baseController: baseController{db: db, tableName: "cert"}}
err := crtServer.Init(rootCert, rootKey)
if err != nil {
return err
}
ruleServer := &ruleServer{baseController: baseController{db: db, tableName: "rule"}}
authMiddleware, err := newAuthMiddleware(db)
if err != nil {
return err
}
router := gin.New()
router.Use(CORSMiddleware(), gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
logger.Debug("http request",
zap.String("client_ip", param.ClientIP),
zap.String("method", param.Method),
zap.String("path", param.Path),
zap.String("proto", param.Request.Proto),
zap.Duration("latency", param.Latency),
zap.String("user_agent", param.Request.UserAgent()),
zap.String("error_message", param.ErrorMessage),
zap.Int("response_code", param.StatusCode),
)
return ""
}))
router.POST("/login", authMiddleware.LoginHandler)
router.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
claims := jwt.ExtractClaims(c)
logger.Warn("NoRoute", zap.Any("claims", claims))
c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
})
auth := router.Group("/auth")
auth.Use(authMiddleware.MiddlewareFunc())
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
auth.GET("/userinfo", userinfo)
v1 := router.Group("v1")
v1.Use(authMiddleware.MiddlewareFunc())
{
v1.PUT("/config", cfgServer.ChangeSystemConfig)
v1.GET("/status", status)
v1.POST("/cert", crtServer.Create)
v1.DELETE("/cert", crtServer.Delete)
v1.GET("/cert/page", crtServer.Page)
v1.PUT("/cert", crtServer.Update)
v1.POST("/rule", ruleServer.Create)
v1.DELETE("/rule", ruleServer.Delete)
v1.PUT("/rule", ruleServer.Update)
v1.GET("/rule", ruleServer.One)
v1.GET("/rule/page", ruleServer.Page)
v1.GET("/rule/field", ruleServer.Field)
v1.GET("/rule/limiter", ruleServer.Limiter)
}
router.Static("/static/", staticRootPath)
router.Static("/page/", pagePath)
go storeSystemStatus()
return router.RunListener(ln)
}
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
func userinfo(c *gin.Context) {
c.Data(http.StatusOK, "application/json; charset=utf-8",
[]byte(`{"code":0,"result":{"userId":"1","username":"vben","realName":"Vben Admin","avatar":"https://q1.qlogo.cn/g?b=qq&nk=190848757&s=640","desc":"manager","password":"123456","token":"fakeToken1","homePath":"/dashboard/analysis","roles":[{"roleName":"Super Admin","value":"super"}]},"message":"ok","type":"success"}`))
}

View File

@ -0,0 +1,149 @@
package controller
import (
"bytes"
"crypto/x509/pkix"
"ehang.io/nps/core/action"
"ehang.io/nps/core/handler"
"ehang.io/nps/core/process"
"ehang.io/nps/core/rule"
"ehang.io/nps/core/server"
"ehang.io/nps/db"
"ehang.io/nps/lib/cert"
"encoding/json"
"errors"
"fmt"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"sync"
"testing"
"time"
)
func TestController(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:3500")
assert.NoError(t, err)
err = os.Remove(filepath.Join(os.TempDir(), "test_control.db"))
d := db.NewSqliteDb(filepath.Join(os.TempDir(), "test_control.db"))
err = d.Init()
assert.NoError(t, err)
assert.NoError(t, d.SetConfig("admin_user", "admin"))
assert.NoError(t, d.SetConfig("admin_pass", "pass"))
cg := cert.NewX509Generator(pkix.Name{
Country: []string{"cn"},
Organization: []string{"ehang"},
OrganizationalUnit: []string{"nps"},
Province: []string{"beijing"},
CommonName: "nps",
Locality: []string{"beijing"},
})
assert.NoError(t, err)
cert, key, err := cg.CreateRootCa()
assert.NoError(t, err)
go func() {
err = StartController(ln, d, cert, key, "./web/static/", "./web/views/")
assert.NoError(t, err)
}()
resp, err := doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/login"), "POST", `{"username": "admin","password": "pass"}`)
assert.NoError(t, err)
assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0)
for i := 0; i < 18; i++ {
resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/cert"), "POST", fmt.Sprintf(`{"status":1,"name":"name_%d","cert_type": "client"}`, i))
assert.NoError(t, err)
assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0)
}
resp, err = doRequest(fmt.Sprintf("http://%s%s?page=%d&pageSize=%d", ln.Addr().String(), "/v1/cert/page", 4, 5), "GET", ``)
assert.NoError(t, err)
now := 2
var lastUuid string
assert.Equal(t, len(gjson.Parse(resp).Get("result.items").Array()), 3)
for _, v := range gjson.Parse(resp).Get("result.items").Array() {
assert.Equal(t, v.Get("name").String(), fmt.Sprintf(`name_%d`, now))
lastUuid = v.Get("uuid").String()
now--
}
resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/cert"), "DELETE", fmt.Sprintf(`{"uuid":"%s"}`, lastUuid))
assert.NoError(t, err)
assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0)
s := &server.TcpServer{ServerAddr: "127.0.0.1:0"}
h := &handler.DefaultHandler{}
p := &process.DefaultProcess{}
a := &action.LocalAction{}
rj := &rule.JsonRule{
Name: "test",
Status: 1,
Server: rule.JsonData{ObjType: s.GetName(), ObjData: getJson(t, s)},
Handler: rule.JsonData{ObjType: h.GetName(), ObjData: getJson(t, h)},
Process: rule.JsonData{ObjType: p.GetName(), ObjData: getJson(t, p)},
Action: rule.JsonData{ObjType: a.GetName(), ObjData: getJson(t, a)},
Limiters: []rule.JsonData{},
}
js := getJson(t, rj)
for i := 0; i < 18; i++ {
resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/rule"), "POST", js)
assert.NoError(t, err)
assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0)
}
resp, err = doRequest(fmt.Sprintf("http://%s%s?page=%d&pageSize=%d", ln.Addr().String(), "/v1/rule/page", 1, 10), "GET", ``)
assert.NoError(t, err)
assert.Equal(t, int(gjson.Parse(resp).Get("result.total").Int()), 18)
uuid := gjson.Parse(resp).Get("result.items").Array()[0].Get("uuid").String()
resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/rule"), "GET", fmt.Sprintf(`{"uuid": "%s"}`, uuid))
assert.NoError(t, err)
assert.Equal(t, gjson.Parse(resp).Get("result.uuid").String(), uuid)
rj.Uuid = uuid
resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/rule"), "PUT", getJson(t, rj))
assert.NoError(t, err)
assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0)
time.Sleep(time.Minute * 600)
}
func getJson(t *testing.T, i interface{}) string {
b, err := json.Marshal(i)
assert.NoError(t, err)
assert.NotEmpty(t, string(b))
return string(b)
}
var client *http.Client
var once sync.Once
var cookies []*http.Cookie
func doRequest(url string, method string, body string) (string, error) {
once.Do(func() {
client = &http.Client{}
})
payload := bytes.NewBufferString(body)
req, err := http.NewRequest(method, url, payload)
if err != nil {
return "", err
}
req.Header.Add("Content-Type", "application/json")
for _, c := range cookies {
req.AddCookie(c)
}
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
b, err := ioutil.ReadAll(res.Body)
if len(res.Cookies()) > 0 {
cookies = res.Cookies()
}
if res.StatusCode != 200 {
return string(b), errors.New("bad doRequest")
}
return string(b), err
}

105
component/controller/jwt.go Normal file
View File

@ -0,0 +1,105 @@
package controller
import (
"ehang.io/nps/db"
"net/http"
"time"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
type login struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
var identityKey = "id"
type User struct {
UserName string
}
func newAuthMiddleware(db db.Db) (authMiddleware *jwt.GinJWTMiddleware, err error) {
authMiddleware, err = jwt.New(&jwt.GinJWTMiddleware{
Realm: "nps",
Key: []byte("secret key"),
Timeout: time.Hour * 24,
MaxRefresh: time.Hour * 72,
IdentityKey: identityKey,
SendCookie: true,
LoginResponse: func(c *gin.Context, code int, message string, time time.Time) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"result": gin.H{
"token": message,
},
"message": "ok",
})
},
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(*User); ok {
return jwt.MapClaims{
identityKey: v.UserName,
}
}
return jwt.MapClaims{}
},
IdentityHandler: func(c *gin.Context) interface{} {
claims := jwt.ExtractClaims(c)
return &User{
UserName: claims[identityKey].(string),
}
},
Authenticator: func(c *gin.Context) (interface{}, error) {
var loginVals login
if err := c.ShouldBind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
userID := loginVals.Username
password := loginVals.Password
adminUser, err := db.GetConfig("admin_user")
if err != nil {
return "", jwt.ErrFailedAuthentication
}
adminPass, err := db.GetConfig("admin_pass")
if err != nil {
return "", jwt.ErrFailedAuthentication
}
if userID == adminUser && password == adminPass {
return &User{
UserName: userID,
}, nil
}
return nil, jwt.ErrFailedAuthentication
},
Authorizator: func(data interface{}, c *gin.Context) bool {
adminUser, err := db.GetConfig("admin_user")
if err != nil {
return false
}
if v, ok := data.(*User); ok && v.UserName ==adminUser {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
})
if err != nil {
return
}
err = authMiddleware.MiddlewareInit()
if err != nil {
return
}
return
}

View File

@ -0,0 +1,151 @@
package controller
import (
"ehang.io/nps/core/rule"
"ehang.io/nps/db"
"encoding/json"
"github.com/gin-gonic/gin"
uuid "github.com/satori/go.uuid"
"net/http"
"strconv"
)
type baseController struct {
db db.Db
tableName string
}
func (bc *baseController) Page(c *gin.Context) {
page, err := strconv.Atoi(c.Query("page"))
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
pageSize, err := strconv.Atoi(c.Query("pageSize"))
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
dataArr, err := bc.db.QueryPage(bc.tableName, pageSize, (page-1)*pageSize, c.Query("key"))
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
list := make([]map[string]interface{}, 0)
for _, s := range dataArr {
dd := make(map[string]interface{}, 0)
err = json.Unmarshal([]byte(s), &dd)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
list = append(list, dd)
}
n, err := bc.db.Count(bc.tableName, c.Query("key"))
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "ok",
"result": gin.H{
"items": list,
"total": n,
},
})
}
func (bc *baseController) Delete(c *gin.Context) {
type uid struct {
Uuid string
}
var js uid
err := c.BindJSON(&js)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
err = bc.db.Delete(bc.tableName, js.Uuid)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
}
type ruleServer struct {
baseController
}
func (rs *ruleServer) Create(c *gin.Context) {
var jr rule.JsonRule
err := c.BindJSON(&jr)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
jr.Uuid = uuid.NewV4().String()
b, err := json.Marshal(jr)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
err = rs.db.Insert(rs.tableName, jr.Uuid, string(b))
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
}
func (rs *ruleServer) Update(c *gin.Context) {
var jr rule.JsonRule
err := c.BindJSON(&jr)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
b, err := json.Marshal(jr)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
err = rs.db.Update(rs.tableName, jr.Uuid, string(b))
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
}
func (rs *ruleServer) One(c *gin.Context) {
var js map[string]string
err := c.BindJSON(&js)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
s, err := rs.db.QueryOne("rule", js["uuid"])
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
var r rule.JsonRule
err = json.Unmarshal([]byte(s), &r)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "result": r, "message": "ok"})
}
func (rs *ruleServer) Field(c *gin.Context) {
chains := rule.GetChains()
c.JSON(http.StatusOK, gin.H{"code": 0, "result": chains, "message": "ok"})
}
func (rs *ruleServer) Limiter(c *gin.Context) {
chains := rule.GetLimiters()
c.JSON(http.StatusOK, gin.H{"code": 0, "result": chains, "message": "ok"})
}

View File

@ -0,0 +1,152 @@
package controller
import (
"container/list"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/net"
"math"
"net/http"
"runtime"
"time"
)
var (
tData = list.New()
)
type timeData struct {
now time.Time
cpuData float64
load1Data float64
load5Data float64
load15Data float64
swapData float64
virtualData float64
bandwidthRecvData float64
bandwidthSendData float64
tcpConnNumData float64
udpConnNumData float64
diskData float64
}
type dataAddr []float64
func status(c *gin.Context) {
timeArr := make([]string, 0)
dataMap := make(map[string][]float64, 0)
dataMap["cpu"] = make([]float64, 0)
dataMap["load1"] = make([]float64, 0)
dataMap["load5"] = make([]float64, 0)
dataMap["load15"] = make([]float64, 0)
dataMap["swap"] = make([]float64, 0)
dataMap["virtual"] = make([]float64, 0)
dataMap["bandwidthRecvData"] = make([]float64, 0)
dataMap["bandwidthSendData"] = make([]float64, 0)
dataMap["tcpConnNumData"] = make([]float64, 0)
dataMap["udpConnNumData"] = make([]float64, 0)
dataMap["disk"] = make([]float64, 0)
now := tData.Front()
for {
if now == nil {
break
}
data := now.Value.(*timeData)
timeArr = append(timeArr, data.now.Format("01-02 15:04"))
dataMap["cpu"] = append(dataMap["cpu"], data.cpuData)
dataMap["load1"] = append(dataMap["load1"], data.load15Data)
dataMap["load5"] = append(dataMap["load5"], data.load5Data)
dataMap["load15"] = append(dataMap["load15"], data.load15Data)
dataMap["swap"] = append(dataMap["swap"], data.swapData)
dataMap["virtual"] = append(dataMap["virtual"], data.virtualData)
dataMap["bandwidthSend"] = append(dataMap["bandwidthSend"], data.bandwidthRecvData)
dataMap["bandwidthRecv"] = append(dataMap["bandwidthRecv"], data.bandwidthSendData)
dataMap["tcp"] = append(dataMap["v"], data.tcpConnNumData)
dataMap["udp"] = append(dataMap["udp"], data.udpConnNumData)
dataMap["disk"] = append(dataMap["disk"], data.diskData)
now = now.Next()
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "ok",
"result": gin.H{
"time": timeArr,
"data": dataMap,
},
})
}
func storeSystemStatus() {
path := "/"
if runtime.GOOS == "windows" {
path = "C:"
}
for range time.NewTicker(time.Second).C {
td := &timeData{now: time.Now()}
checkListLen(tData)
cpuPercent, err := cpu.Percent(0, true)
if err == nil {
var cpuAll float64
for _, v := range cpuPercent {
cpuAll += v
}
td.cpuData = float64(len(cpuPercent))
}
loads, err := load.Avg()
if err == nil {
td.load1Data = loads.Load1
td.load1Data = loads.Load5
td.load15Data = loads.Load15
}
swap, err := mem.SwapMemory()
if err == nil {
td.swapData = math.Round(swap.UsedPercent)
}
vir, err := mem.VirtualMemory()
if err == nil {
td.virtualData = math.Round(vir.UsedPercent)
}
io1, err := net.IOCounters(false)
if err == nil {
time.Sleep(time.Millisecond * 500)
io2, err := net.IOCounters(false)
if err == nil && len(io2) > 0 && len(io1) > 0 {
td.bandwidthRecvData = float64((io2[0].BytesRecv-io1[0].BytesRecv)*2) / 1024 / 1024
td.bandwidthSendData = float64((io2[0].BytesSent-io1[0].BytesSent)*2) / 1024 / 1024
}
}
conn, err := net.ProtoCounters(nil)
if err == nil {
for _, v := range conn {
if v.Protocol == "tcp" {
td.tcpConnNumData = float64(v.Stats["CurrEstab"])
}
if v.Protocol == "udp" {
td.udpConnNumData = float64(v.Stats["CurrEstab"])
}
}
}
usage, err := disk.Usage(path)
if err == nil {
td.diskData = math.Round(usage.UsedPercent)
}
tData.PushBack(td)
}
}
func checkListLen(lists ...*list.List) {
for _, l := range lists {
if l.Len() > 4320 {
if first := l.Front(); first != nil {
l.Remove(first)
}
}
}
}

View File

@ -0,0 +1,11 @@
package service
import "net"
type HttpService struct {
ln net.Listener
}
func NewHttpService(ln net.Listener) *HttpService {
return &HttpService{ln: ln}
}

110
core/action/action.go Normal file
View File

@ -0,0 +1,110 @@
package action
import (
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/logger"
"ehang.io/nps/lib/pool"
"errors"
"go.uber.org/zap"
"net"
"sync"
)
var bp = pool.NewBufferPool(MaxReadSize)
const MaxReadSize = 32 * 1024
var (
_ Action = (*AdminAction)(nil)
_ Action = (*BridgeAction)(nil)
_ Action = (*LocalAction)(nil)
_ Action = (*NpcAction)(nil)
)
type Action interface {
GetName() string
GetZhName() string
Init() error
RunConnWithAddr(net.Conn, string) error
RunConn(net.Conn) error
GetServeConnWithAddr(string) (net.Conn, error)
GetServerConn() (net.Conn, error)
CanServe() bool
RunPacketConn(conn net.PacketConn) error
}
type DefaultAction struct {
}
func (ba *DefaultAction) GetName() string {
return "default"
}
func (ba *DefaultAction) GetZhName() string {
return "默认"
}
func (ba *DefaultAction) Init() error {
return nil
}
func (ba *DefaultAction) RunConn(clientConn net.Conn) error {
return errors.New("not supported")
}
func (ba *DefaultAction) CanServe() bool {
return false
}
func (ba *DefaultAction) RunPacketConn(conn net.PacketConn) error {
return errors.New("not supported")
}
func (ba *DefaultAction) GetServerConn() (net.Conn, error) {
return nil, errors.New("can not get component connection")
}
func (ba *DefaultAction) GetServeConnWithAddr(addr string) (net.Conn, error) {
return nil, errors.New("can not get component connection")
}
func (ba *DefaultAction) startCopy(c1 net.Conn, c2 net.Conn) {
var wg sync.WaitGroup
wg.Add(2)
err := pool.CopyConnGoroutinePool.Invoke(&pool.CopyConnGpParams{
Reader: c2,
Writer: c1,
Wg: &wg,
})
if err != nil {
logger.Error("Invoke goroutine failed", zap.Error(err))
return
}
buf := bp.Get()
_, _ = common.CopyBuffer(c2, c1, buf)
bp.Put(buf)
if v, ok := c1.(*net.TCPConn); ok {
_ = v.CloseRead()
}
if v, ok := c2.(*net.TCPConn); ok {
_ = v.CloseWrite()
}
wg.Wait()
}
func (ba *DefaultAction) startCopyPacketConn(p1 net.PacketConn, p2 net.PacketConn) error {
var wg sync.WaitGroup
wg.Add(2)
_ = pool.CopyPacketGoroutinePool.Invoke(&pool.CopyPacketGpParams{
RPacket: p1,
WPacket: p2,
Wg: &wg,
})
_ = pool.CopyPacketGoroutinePool.Invoke(&pool.CopyPacketGpParams{
RPacket: p2,
WPacket: p1,
Wg: &wg,
})
wg.Wait()
return nil
}

32
core/action/admin.go Normal file
View File

@ -0,0 +1,32 @@
package action
import (
"ehang.io/nps/lib/enet"
"net"
)
var adminListener = enet.NewListener()
func GetAdminListener() net.Listener {
return adminListener
}
type AdminAction struct {
DefaultAction
}
func (la *AdminAction) GetName() string {
return "admin"
}
func (la *AdminAction) GetZhName() string {
return "转发到控制台"
}
func (la *AdminAction) RunConn(clientConn net.Conn) error {
return adminListener.SendConn(clientConn)
}
func (la *AdminAction) RunConnWithAddr(clientConn net.Conn, addr string) error {
return adminListener.SendConn(clientConn)
}

29
core/action/admin_test.go Normal file
View File

@ -0,0 +1,29 @@
package action
import (
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestAdminRunConn(t *testing.T) {
ac := &AdminAction{
DefaultAction: DefaultAction{},
}
finish := make(chan struct{}, 0)
go func() {
_, err := GetAdminListener().Accept()
assert.NoError(t, err)
finish <- struct{}{}
}()
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
assert.NoError(t, ac.RunConn(conn))
}()
_, err = net.Dial("tcp", ln.Addr().String())
assert.NoError(t, err)
<-finish
}

61
core/action/bridge.go Normal file
View File

@ -0,0 +1,61 @@
package action
import (
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pool"
"net"
)
var bridgeListener = enet.NewListener()
var bridgePacketConn enet.PacketConn
var packetBp = pool.NewBufferPool(1500)
func GetBridgeListener() net.Listener {
return bridgeListener
}
func GetBridgePacketConn() net.PacketConn {
return bridgePacketConn
}
type BridgeAction struct {
DefaultAction
WritePacketConn net.PacketConn `json:"-"`
}
func (ba *BridgeAction) GetName() string {
return "bridge"
}
func (ba *BridgeAction) GetZhName() string {
return "转发到网桥"
}
func (ba *BridgeAction) Init() error {
bridgePacketConn = enet.NewReaderPacketConn(ba.WritePacketConn, nil, ba.WritePacketConn.LocalAddr())
return nil
}
func (ba *BridgeAction) RunConn(clientConn net.Conn) error {
return bridgeListener.SendConn(clientConn)
}
func (ba *BridgeAction) RunConnWithAddr(clientConn net.Conn, addr string) error {
return bridgeListener.SendConn(clientConn)
}
func (ba *BridgeAction) RunPacketConn(pc net.PacketConn) error {
b := packetBp.Get()
defer packetBp.Put(b)
for {
n, addr, err := pc.ReadFrom(b)
if err != nil {
break
}
err = bridgePacketConn.SendPacket(b[:n], addr)
if err != nil {
break
}
}
return nil
}

View File

@ -0,0 +1,73 @@
package action
import (
"ehang.io/nps/lib/enet"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestBridgeRunConn(t *testing.T) {
packetConn, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
ac := &BridgeAction{
DefaultAction: DefaultAction{},
WritePacketConn: packetConn,
}
finish := make(chan struct{}, 0)
go func() {
_, err := GetBridgeListener().Accept()
assert.NoError(t, err)
finish <- struct{}{}
}()
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
assert.NoError(t, ac.RunConn(conn))
}()
_, err = net.Dial("tcp", ln.Addr().String())
assert.NoError(t, err)
<-finish
}
func TestBridgeRunPacket(t *testing.T) {
packetConn, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
ac := &BridgeAction{
DefaultAction: DefaultAction{},
WritePacketConn: packetConn,
}
assert.NoError(t, ac.Init())
go func() {
p := make([]byte, 1024)
pc := GetBridgePacketConn()
n, addr, err := pc.ReadFrom(p)
assert.NoError(t, err)
_, err = pc.WriteTo(p[:n], addr)
assert.NoError(t, err)
}()
go func() {
p := make([]byte, 1024)
n, addr, err := packetConn.ReadFrom(p)
assert.NoError(t, err)
bPacketConn := enet.NewReaderPacketConn(packetConn, p[:n], addr)
go func() {
err = ac.RunPacketConn(bPacketConn)
assert.NoError(t, err)
}()
err = bPacketConn.SendPacket(p[:n], addr)
assert.NoError(t, err)
}()
cPacketConn, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
b := []byte("12345")
_, err = cPacketConn.WriteTo(b, packetConn.LocalAddr())
assert.NoError(t, err)
p := make([]byte, 1024)
n, addr, err := cPacketConn.ReadFrom(p)
assert.NoError(t, err)
assert.Equal(t, addr.String(), packetConn.LocalAddr().String())
assert.Equal(t, p[:n], b)
}

77
core/action/local.go Normal file
View File

@ -0,0 +1,77 @@
package action
import (
"ehang.io/nps/lib/lb"
"net"
)
type LocalAction struct {
DefaultAction
TargetAddr []string `json:"target_addr" placeholder:"1.1.1.1:80\n1.1.1.2:80" zh_name:"目标地址"`
UnixSocket bool `json:"unix_sock" placeholder:"" zh_name:"转发到unix socket"`
networkTcp string
localLb lb.Algo
}
func (la *LocalAction) GetName() string {
return "local"
}
func (la *LocalAction) GetZhName() string {
return "转发到本地"
}
func (la *LocalAction) Init() error {
la.localLb = lb.GetLbAlgo("roundRobin")
for _, v := range la.TargetAddr {
_ = la.localLb.Append(v)
}
la.networkTcp = "tcp"
if la.UnixSocket {
// just support unix
la.networkTcp = "unix"
}
return nil
}
func (la *LocalAction) RunConn(clientConn net.Conn) error {
serverConn, err := la.GetServerConn()
if err != nil {
return err
}
la.startCopy(clientConn, serverConn)
return nil
}
func (la *LocalAction) RunConnWithAddr(clientConn net.Conn, addr string) error {
serverConn, err := la.GetServeConnWithAddr(addr)
if err != nil {
return err
}
la.startCopy(clientConn, serverConn)
return nil
}
func (la *LocalAction) CanServe() bool {
return true
}
func (la *LocalAction) GetServerConn() (net.Conn, error) {
addr, err := la.localLb.Next()
if err != nil {
return nil, err
}
return la.GetServeConnWithAddr(addr.(string))
}
func (la *LocalAction) GetServeConnWithAddr(addr string) (net.Conn, error) {
return net.Dial(la.networkTcp, addr)
}
func (la *LocalAction) RunPacketConn(pc net.PacketConn) error {
localPacketConn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
return err
}
return la.startCopyPacketConn(pc, localPacketConn)
}

View File

@ -0,0 +1 @@
package action

84
core/action/npc.go Normal file
View File

@ -0,0 +1,84 @@
package action
import (
"crypto/tls"
"ehang.io/nps/lib/cert"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pb"
"github.com/golang/protobuf/proto"
"github.com/pkg/errors"
"net"
)
type NpcAction struct {
NpcId string `json:"npc_id" required:"true" placeholder:"npc id" zh_name:"客户端"`
BridgeAddr string `json:"bridge_addr" placeholder:"127.0.0.1:8080" zh_name:"网桥地址"`
UnixSocket bool `json:"unix_sock" placeholder:"" zh_name:"转发到unix socket"`
networkTcp pb.ConnType
tlsConfig *tls.Config
connRequest *pb.ConnRequest
DefaultAction
}
func (na *NpcAction) GetName() string {
return "npc"
}
func (na *NpcAction) GetZhName() string {
return "转发到客户端"
}
func (na *NpcAction) Init() error {
if na.tlsConfig == nil {
return errors.New("tls config is nil")
}
sn, err := cert.GetCertSnFromConfig(na.tlsConfig)
if err != nil {
return errors.Wrap(err, "get serial number")
}
na.connRequest = &pb.ConnRequest{Id: sn}
na.networkTcp = pb.ConnType_tcp
if na.UnixSocket {
// just support unix
na.networkTcp = pb.ConnType_unix
}
return nil
}
func (na *NpcAction) RunConnWithAddr(clientConn net.Conn, addr string) error {
serverConn, err := na.GetServeConnWithAddr(addr)
if err != nil {
return err
}
na.startCopy(clientConn, serverConn)
return nil
}
func (na *NpcAction) CanServe() bool {
return true
}
func (na *NpcAction) GetServeConnWithAddr(addr string) (net.Conn, error) {
return dialBridge(na, na.networkTcp, addr)
}
func (na *NpcAction) RunPacketConn(pc net.PacketConn) error {
serverPacketConn, err := dialBridge(na, pb.ConnType_udp, "")
if err != nil {
return err
}
return na.startCopyPacketConn(pc, enet.NewTcpPacketConn(serverPacketConn))
}
func dialBridge(npc *NpcAction, connType pb.ConnType, addr string) (net.Conn, error) {
tlsConn, err := tls.Dial("tcp", npc.BridgeAddr, npc.tlsConfig)
if err != nil {
return nil, errors.Wrap(err, "dial bridge tls")
}
cr := proto.Clone(npc.connRequest).(*pb.ConnRequest)
cr.ConnType = &pb.ConnRequest_AppInfo{AppInfo: &pb.AppInfo{ConnType: connType, AppAddr: addr, NpcId: npc.NpcId}}
if _, err = pb.WriteMessage(tlsConn, cr); err != nil {
return nil, errors.Wrap(err, "write enet request")
}
return tlsConn, err
}

1
core/action/npc_test.go Normal file
View File

@ -0,0 +1 @@
package action

71
core/handler/default.go Normal file
View File

@ -0,0 +1,71 @@
package handler
import (
"ehang.io/nps/lib/enet"
)
var (
_ Handler = (*HttpHandler)(nil)
_ Handler = (*HttpsHandler)(nil)
_ Handler = (*RdpHandler)(nil)
_ Handler = (*RedisHandler)(nil)
_ Handler = (*Socks5Handler)(nil)
_ Handler = (*TransparentHandler)(nil)
_ Handler = (*DefaultHandler)(nil)
_ Handler = (*DnsHandler)(nil)
_ Handler = (*P2PHandler)(nil)
_ Handler = (*QUICHandler)(nil)
_ Handler = (*DefaultHandler)(nil)
_ Handler = (*Socks5UdpHandler)(nil)
)
type RuleRun interface {
RunConn(enet.Conn) (bool, error)
RunPacketConn(enet.PacketConn) (bool, error)
}
type DefaultHandler struct {
ruleList []RuleRun
}
func NewBaseTcpHandler() *DefaultHandler {
return &DefaultHandler{ruleList: make([]RuleRun, 0)}
}
func (b *DefaultHandler) GetName() string {
return "default"
}
func (b *DefaultHandler) GetZhName() string {
return "默认"
}
func (b *DefaultHandler) HandleConn(_ []byte, c enet.Conn) (bool, error) {
return b.processConn(c)
}
func (b *DefaultHandler) AddRule(r RuleRun) {
b.ruleList = append(b.ruleList, r)
}
func (b *DefaultHandler) HandlePacketConn(_ enet.PacketConn) (bool, error) {
return false, nil
}
func (b *DefaultHandler) processConn(c enet.Conn) (bool, error) {
for _, r := range b.ruleList {
if ok, err := r.RunConn(c); err != nil || ok {
return ok, err
}
}
return false, nil
}
func (b *DefaultHandler) processPacketConn(pc enet.PacketConn) (bool, error) {
for _, r := range b.ruleList {
if ok, err := r.RunPacketConn(pc); err != nil || ok {
return ok, err
}
}
return false, nil
}

35
core/handler/dns.go Normal file
View File

@ -0,0 +1,35 @@
package handler
import (
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"github.com/miekg/dns"
"go.uber.org/zap"
)
type DnsHandler struct {
DefaultHandler
}
func (dh *DnsHandler) GetName() string {
return "dns"
}
func (dh *DnsHandler) GetZhName() string {
return "dns协议"
}
func (dh *DnsHandler) HandlePacketConn(pc enet.PacketConn) (bool, error) {
b, _, err := pc.FirstPacket()
if err != nil {
logger.Warn("firstPacket error", zap.Error(err))
return false, nil
}
m := new(dns.Msg)
err = m.Unpack(b)
if err != nil {
logger.Debug("parse dns request error", zap.Error(err))
return false, nil
}
return dh.processPacketConn(pc)
}

46
core/handler/dns_test.go Normal file
View File

@ -0,0 +1,46 @@
package handler
import (
"ehang.io/nps/lib/enet"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
type testRule struct {
run bool
}
func (t *testRule) RunConn(c enet.Conn) (bool, error) {
t.run = true
return true, nil
}
func (t *testRule) RunPacketConn(_ enet.PacketConn) (bool, error) {
t.run = true
return true, nil
}
func TestHandleDnsPacket(t *testing.T) {
lPacketConn, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
h := DnsHandler{}
rule := &testRule{}
h.AddRule(rule)
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn("www.google.com"), dns.TypeA)
m.RecursionDesired = true
b, err := m.Pack()
assert.NoError(t, err)
pc := enet.NewReaderPacketConn(nil, b, lPacketConn.LocalAddr())
assert.NoError(t, pc.SendPacket(b, nil))
res, err := h.HandlePacketConn(pc)
assert.NoError(t, err)
assert.Equal(t, true, res)
assert.Equal(t, true, rule.run)
}

11
core/handler/handler.go Normal file
View File

@ -0,0 +1,11 @@
package handler
import "ehang.io/nps/lib/enet"
type Handler interface {
GetName() string
GetZhName() string
AddRule(RuleRun)
HandleConn([]byte, enet.Conn) (bool, error)
HandlePacketConn(enet.PacketConn) (bool, error)
}

30
core/handler/http.go Normal file
View File

@ -0,0 +1,30 @@
package handler
import (
"ehang.io/nps/lib/enet"
"net/http"
)
type HttpHandler struct {
DefaultHandler
}
func NewHttpHandler() *HttpHandler {
return &HttpHandler{}
}
func (h *HttpHandler) GetName() string {
return "http"
}
func (h *HttpHandler) GetZhName() string {
return "http协议"
}
func (h *HttpHandler) HandleConn(b []byte, c enet.Conn) (bool, error) {
switch string(b[:3]) {
case http.MethodGet[:3], http.MethodHead[:3], http.MethodPost[:3], http.MethodPut[:3], http.MethodPatch[:3], http.MethodDelete[:3], http.MethodConnect[:3], http.MethodOptions[:3], http.MethodTrace[:3]:
return h.processConn(c)
}
return false, nil
}

27
core/handler/http_test.go Normal file
View File

@ -0,0 +1,27 @@
package handler
import (
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httputil"
"testing"
)
func TestHandleHttpConn(t *testing.T) {
h := HttpHandler{}
rule := &testRule{}
h.AddRule(rule)
r, err := http.NewRequest("GET", "/", nil)
assert.NoError(t, err)
b, err := httputil.DumpRequest(r, false)
assert.NoError(t, err)
res, err := h.HandleConn(b, nil)
assert.NoError(t, err)
assert.Equal(t, true, res)
assert.Equal(t, true, rule.run)
}

33
core/handler/https.go Normal file
View File

@ -0,0 +1,33 @@
package handler
import (
"ehang.io/nps/lib/enet"
)
const (
recordTypeHandshake uint8 = 22
typeClientHello uint8 = 1
)
type HttpsHandler struct {
DefaultHandler
}
func NewHttpsHandler() *HttpsHandler {
return &HttpsHandler{}
}
func (h *HttpsHandler) GetName() string {
return "https"
}
func (h *HttpsHandler) GetZhName() string {
return "https协议"
}
func (h *HttpsHandler) HandleConn(b []byte, c enet.Conn) (bool, error) {
if b[0] == recordTypeHandshake{
return h.processConn(c)
}
return false, nil
}

View File

@ -0,0 +1,39 @@
package handler
import (
"crypto/tls"
"ehang.io/nps/lib/enet"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHandleHttpsConn(t *testing.T) {
h := HttpsHandler{}
rule := &testRule{}
h.AddRule(rule)
finish := make(chan struct{}, 0)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
buf := make([]byte, 1024)
n, err := conn.Read(buf)
assert.NoError(t, err)
res, err := h.HandleConn(buf[:n], enet.NewReaderConn(conn))
assert.NoError(t, err)
assert.Equal(t, true, res)
assert.Equal(t, true, rule.run)
finish <- struct{}{}
}()
go func() {
_, err = tls.Dial("tcp", ln.Addr().String(), &tls.Config{
InsecureSkipVerify: true,
})
assert.NoError(t, err)
}()
<-finish
}

32
core/handler/p2p.go Normal file
View File

@ -0,0 +1,32 @@
package handler
import (
"bytes"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"go.uber.org/zap"
)
type P2PHandler struct {
DefaultHandler
}
func (ph *P2PHandler) GetName() string {
return "p2p"
}
func (ph *P2PHandler) GetZhName() string {
return "点对点协议"
}
func (ph *P2PHandler) HandlePacketConn(pc enet.PacketConn) (bool, error) {
b, _, err := pc.FirstPacket()
if err != nil {
logger.Warn("firstPacket error", zap.Error(err))
return false, nil
}
if bytes.HasPrefix(b, []byte("p2p")) {
return ph.processPacketConn(pc)
}
return false, nil
}

26
core/handler/p2p_test.go Normal file
View File

@ -0,0 +1,26 @@
package handler
import (
"ehang.io/nps/lib/enet"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHandleP2PPacket(t *testing.T) {
h := P2PHandler{}
rule := &testRule{}
h.AddRule(rule)
addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
assert.NoError(t, err)
pc := enet.NewReaderPacketConn(nil, []byte("p2p xxxx"), addr)
assert.NoError(t, pc.SendPacket([]byte("p2p xxxx"), nil))
res, err := h.HandlePacketConn(pc)
assert.NoError(t, err)
assert.Equal(t, true, res)
assert.Equal(t, true, rule.run)
}

32
core/handler/quic.go Normal file
View File

@ -0,0 +1,32 @@
package handler
import (
"bytes"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"go.uber.org/zap"
)
type QUICHandler struct {
DefaultHandler
}
func (qh *QUICHandler) GetName() string {
return "quic"
}
func (qh *QUICHandler) GetZhName() string {
return "quic协议"
}
func (qh *QUICHandler) HandlePacketConn(pc enet.PacketConn) (bool, error) {
b, _, err := pc.FirstPacket()
if err != nil {
logger.Warn("firstPacket error", zap.Error(err))
return false, nil
}
if len(b) >= 5 && bytes.HasPrefix(b[1:5], []byte{0, 0, 0, 1}) {
return qh.processPacketConn(pc)
}
return false, nil
}

35
core/handler/quic_test.go Normal file
View File

@ -0,0 +1,35 @@
package handler
import (
"crypto/tls"
"ehang.io/nps/lib/enet"
"github.com/lucas-clemente/quic-go"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHandleQUICPacket(t *testing.T) {
h := QUICHandler{}
rule := &testRule{}
h.AddRule(rule)
finish := make(chan struct{}, 0)
packetConn, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
b := make([]byte, 1500)
n, addr, err := packetConn.ReadFrom(b)
assert.NoError(t, err)
pc := enet.NewReaderPacketConn(nil, b[:n], packetConn.LocalAddr())
assert.NoError(t, pc.SendPacket(b[:n], addr))
res, err := h.HandlePacketConn(pc)
assert.NoError(t, err)
assert.Equal(t, true, res)
assert.Equal(t, true, rule.run)
finish <- struct{}{}
}()
go quic.DialAddr(packetConn.LocalAddr().String(), &tls.Config{}, nil)
<-finish
}

24
core/handler/rdp.go Normal file
View File

@ -0,0 +1,24 @@
package handler
import (
"ehang.io/nps/lib/enet"
)
type RdpHandler struct {
DefaultHandler
}
func (rh *RdpHandler) GetName() string {
return "rdp"
}
func (rh *RdpHandler) GetZhName() string {
return "rdp协议"
}
func (rh *RdpHandler) HandleConn(b []byte, c enet.Conn) (bool, error) {
if b[0] == 3 && b[1] == 0 {
return rh.processConn(c)
}
return false, nil
}

37
core/handler/rdp_test.go Normal file
View File

@ -0,0 +1,37 @@
package handler
import (
"ehang.io/nps/lib/enet"
"github.com/icodeface/grdp"
"github.com/icodeface/grdp/glog"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHandleRdpConn(t *testing.T) {
h := RdpHandler{}
rule := &testRule{}
h.AddRule(rule)
finish := make(chan struct{}, 0)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
buf := make([]byte, 1024)
n, err := conn.Read(buf)
assert.NoError(t, err)
res, err := h.HandleConn(buf[:n], enet.NewReaderConn(conn))
assert.NoError(t, err)
assert.Equal(t, true, res)
assert.Equal(t, true, rule.run)
finish <- struct{}{}
}()
go func() {
grdp.NewClient(ln.Addr().String(), glog.DEBUG).Login("Administrator", "123456")
}()
<-finish
}

22
core/handler/redis.go Normal file
View File

@ -0,0 +1,22 @@
package handler
import "ehang.io/nps/lib/enet"
type RedisHandler struct {
DefaultHandler
}
func (rds *RedisHandler) GetName() string {
return "redis"
}
func (rds *RedisHandler) GetZhName() string {
return "redis协议"
}
func (rds *RedisHandler) HandleConn(b []byte, c enet.Conn) (bool, error) {
if b[0] == 42 && b[1] == 49 && b[2] == 13 {
return rds.processConn(c)
}
return false, nil
}

View File

@ -0,0 +1,40 @@
package handler
import (
"context"
"ehang.io/nps/lib/enet"
"github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHandleRedisConn(t *testing.T) {
h := RedisHandler{}
rule := &testRule{}
h.AddRule(rule)
finish := make(chan struct{}, 0)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
buf := make([]byte, 1024)
n, err := conn.Read(buf)
assert.NoError(t, err)
res, err := h.HandleConn(buf[:n], enet.NewReaderConn(conn))
assert.NoError(t, err)
assert.Equal(t, true, res)
assert.Equal(t, true, rule.run)
finish <- struct{}{}
}()
go func() {
rdb := redis.NewClient(&redis.Options{
Addr: ln.Addr().String(),
})
rdb.Ping(context.Background())
}()
<-finish
}

22
core/handler/socks5.go Normal file
View File

@ -0,0 +1,22 @@
package handler
import "ehang.io/nps/lib/enet"
type Socks5Handler struct {
DefaultHandler
}
func (sh *Socks5Handler) GetName() string {
return "socks5"
}
func (sh *Socks5Handler) GetZhName() string {
return "socks5协议"
}
func (sh *Socks5Handler) HandleConn(b []byte, c enet.Conn) (bool, error) {
if b[0] == 5 {
return sh.processConn(c)
}
return false, nil
}

View File

@ -0,0 +1,47 @@
package handler
import (
"crypto/tls"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"net/http"
"net/url"
"testing"
)
func TestHandleSocks5Conn(t *testing.T) {
h := Socks5Handler{}
rule := &testRule{}
h.AddRule(rule)
finish := make(chan struct{}, 0)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
buf := make([]byte, 1024)
n, err := conn.Read(buf)
assert.NoError(t, err)
res, err := h.HandleConn(buf[:n], enet.NewReaderConn(conn))
assert.NoError(t, err)
assert.Equal(t, true, res)
assert.Equal(t, true, rule.run)
finish <- struct{}{}
}()
go func() {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("socks5://%s", ln.Addr().String()))
},
}
client := &http.Client{Transport: transport}
_, _ = client.Get("https://google.com/")
}()
<-finish
}

View File

@ -0,0 +1,26 @@
package handler
import "ehang.io/nps/lib/enet"
type Socks5UdpHandler struct {
DefaultHandler
}
func (sh *Socks5UdpHandler) GetName() string {
return "socks5_udp"
}
func (sh *Socks5UdpHandler) GetZhName() string {
return "socks5 udp协议"
}
func (sh *Socks5UdpHandler) HandlePacketConn(pc enet.PacketConn) (bool, error) {
b, _, err := pc.FirstPacket()
if err != nil {
return true, err
}
if b[0] == 0 {
return sh.processPacketConn(pc)
}
return false, nil
}

View File

@ -0,0 +1,44 @@
package handler
import (
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestSocks5Handle(t *testing.T) {
h := Socks5UdpHandler{}
rule := &testRule{}
h.AddRule(rule)
finish := make(chan struct{}, 0)
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
buf := make([]byte, 1024)
n, addr, err := pc.ReadFrom(buf)
assert.NoError(t, err)
rPc := enet.NewReaderPacketConn(nil, buf[:n], addr)
res, err := h.HandlePacketConn(rPc)
assert.NoError(t, err)
assert.Equal(t, true, res)
assert.Equal(t, true, rule.run)
finish <- struct{}{}
}()
data := []byte("test")
go func() {
cPc, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
pAddr, err := common.ParseAddr("8.8.8.8:53")
assert.NoError(t, err)
b := append([]byte{0, 0, 0}, pAddr...)
b = append(b, data...)
_, err = cPc.WriteTo(b, pc.LocalAddr())
assert.NoError(t, err)
}()
<-finish
}

View File

@ -0,0 +1,21 @@
package handler
import (
"ehang.io/nps/lib/enet"
)
type TransparentHandler struct {
DefaultHandler
}
func (ts *TransparentHandler) GetName() string {
return "transparent"
}
func (ts *TransparentHandler) GetZhName() string {
return "linux透明代理协议"
}
func (ts *TransparentHandler) HandleConn(b []byte, c enet.Conn) (bool, error) {
return ts.processConn(c)
}

43
core/limiter/conn_num.go Normal file
View File

@ -0,0 +1,43 @@
package limiter
import (
"ehang.io/nps/lib/enet"
"errors"
"sync/atomic"
)
// ConnNumLimiter is used to limit the connection num of a service
type ConnNumLimiter struct {
baseLimiter
nowNum int32
MaxConnNum int32 `json:"max_conn_num" required:"true" placeholder:"10" zh_name:"最大连接数"` //0 means not limit
}
func (cl *ConnNumLimiter) GetName() string {
return "conn_num"
}
func (cl *ConnNumLimiter) GetZhName() string {
return "总连接数限制"
}
// DoLimit return an error if the connection num exceed the maximum
func (cl *ConnNumLimiter) DoLimit(c enet.Conn) (enet.Conn, error) {
if atomic.AddInt32(&cl.nowNum, 1) > cl.MaxConnNum && cl.MaxConnNum > 0 {
atomic.AddInt32(&cl.nowNum, -1)
return nil, errors.New("exceed maximum number of connections")
}
return &connNumConn{nowNum: &cl.nowNum}, nil
}
// connNumConn is an implementation of enet.Conn
type connNumConn struct {
nowNum *int32
enet.Conn
}
// Close decrease the connection num
func (cn *connNumConn) Close() error {
atomic.AddInt32(cn.nowNum, -1)
return cn.Conn.Close()
}

View File

@ -0,0 +1,35 @@
package limiter
import (
"ehang.io/nps/lib/enet"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestConnNumLimiter(t *testing.T) {
cl := ConnNumLimiter{MaxConnNum: 5}
assert.NoError(t, cl.Init())
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
nowNum := 0
close := make(chan struct{})
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
nowNum++
_, err = cl.DoLimit(enet.NewReaderConn(c))
if nowNum > 5 {
assert.Error(t, err)
close <- struct{}{}
} else {
assert.NoError(t, err)
}
}
}()
for i := 6; i > 0; i-- {
go net.Dial("tcp", ln.Addr().String())
}
<-close
}

87
core/limiter/flow.go Normal file
View File

@ -0,0 +1,87 @@
package limiter
import (
"ehang.io/nps/lib/enet"
"errors"
"sync/atomic"
)
// FlowStore is an interface to store or get the flow now
type FlowStore interface {
GetOutIn() (uint32, uint32)
AddOut(out uint32) uint32
AddIn(in uint32) uint32
}
// memStore is an implement of FlowStore
type memStore struct {
nowOut uint32
nowIn uint32
}
// GetOutIn return out and in num 0
func (m *memStore) GetOutIn() (uint32, uint32) {
return m.nowOut, m.nowIn
}
// AddOut is used to add out now
func (m *memStore) AddOut(out uint32) uint32 {
return atomic.AddUint32(&m.nowOut, out)
}
// AddIn is used to add in now
func (m *memStore) AddIn(in uint32) uint32 {
return atomic.AddUint32(&m.nowIn, in)
}
// FlowLimiter is used to limit the flow of a service
type FlowLimiter struct {
Store FlowStore
OutLimit uint32 `json:"out_limit" required:"true" placeholder:"1024(kb)" zh_name:"出口最大流量"` //unit: kb, 0 means not limit
InLimit uint32 `json:"in_limit" required:"true" placeholder:"1024(kb)" zh_name:"入口最大流量"` //unit: kb, 0 means not limit
}
func (f *FlowLimiter) GetName() string {
return "flow"
}
func (f *FlowLimiter) GetZhName() string {
return "流量限制"
}
// DoLimit return a flow limited enet.Conn
func (f *FlowLimiter) DoLimit(c enet.Conn) (enet.Conn, error) {
return &flowConn{fl: f, Conn: c}, nil
}
// Init is used to set out or in num now
func (f *FlowLimiter) Init() error {
if f.Store == nil {
f.Store = &memStore{}
}
return nil
}
// flowConn is an implement of
type flowConn struct {
enet.Conn
fl *FlowLimiter
}
// Read add the in flow num of the service
func (fs *flowConn) Read(b []byte) (n int, err error) {
n, err = fs.Conn.Read(b)
if fs.fl.InLimit > 0 && fs.fl.Store.AddIn(uint32(n)) > fs.fl.InLimit {
err = errors.New("exceed the in flow limit")
}
return
}
// Write add the out flow num of the service
func (fs *flowConn) Write(b []byte) (n int, err error) {
n, err = fs.Conn.Write(b)
if fs.fl.OutLimit > 0 && fs.fl.Store.AddOut(uint32(n)) > fs.fl.OutLimit {
err = errors.New("exceed the out flow limit")
}
return
}

59
core/limiter/flow_test.go Normal file
View File

@ -0,0 +1,59 @@
package limiter
import (
"bytes"
"ehang.io/nps/lib/enet"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestFlowLimiter(t *testing.T) {
cl := FlowLimiter{
OutLimit: 100,
InLimit: 100,
}
assert.NoError(t, cl.Init())
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
nowBytes := 0
close := make(chan struct{})
go func() {
buf := make([]byte, 10)
c, err := ln.Accept()
assert.NoError(t, err)
c, err = cl.DoLimit(enet.NewReaderConn(c))
for {
n, err := c.Read(buf)
nowBytes += n
if nowBytes > 100 {
assert.Error(t, err)
nowBytes = 0
for i := 11; i > 0; i-- {
n, err = c.Write(bytes.Repeat([]byte{0}, 10))
nowBytes += n
if nowBytes > 100 {
assert.Error(t, err)
close <- struct{}{}
} else {
assert.NoError(t, err)
}
}
} else {
assert.NoError(t, err)
}
}
}()
c, err := net.Dial("tcp", ln.Addr().String())
assert.NoError(t, err)
for i := 11; i > 0; i-- {
_, err := c.Write(bytes.Repeat([]byte{0}, 10))
assert.NoError(t, err)
}
buf := make([]byte, 10)
for i := 11; i > 0; i-- {
_, err := c.Read(buf)
assert.NoError(t, err)
}
<-close
}

View File

@ -0,0 +1,99 @@
package limiter
import (
"ehang.io/nps/lib/enet"
"github.com/pkg/errors"
"net"
"sync"
)
// ipNumMap is used to store the connection num of a ip address
type ipNumMap struct {
m map[string]int32
sync.Mutex
}
// AddOrSet is used to add connection num of a ip address
func (i *ipNumMap) AddOrSet(key string) {
i.Lock()
if v, ok := i.m[key]; ok {
i.m[key] = v + 1
} else {
i.m[key] = 1
}
i.Unlock()
}
// SubOrDel is used to decrease connection of a ip address
func (i *ipNumMap) SubOrDel(key string) {
i.Lock()
if v, ok := i.m[key]; ok {
i.m[key] = v - 1
if i.m[key] == 0 {
delete(i.m, key)
}
}
i.Unlock()
}
// Get return the connection num of a ip
func (i *ipNumMap) Get(key string) int32 {
return i.m[key]
}
// IpConnNumLimiter is used to limit the connection num of a service at the same time of same ip
type IpConnNumLimiter struct {
m *ipNumMap
MaxNum int32 `json:"max_num" required:"true" placeholder:"10" zh_name:"单ip最大连接数"`
sync.Mutex
}
func (cl *IpConnNumLimiter) GetName() string {
return "ip_conn_num"
}
func (cl *IpConnNumLimiter) GetZhName() string {
return "单ip连接数限制"
}
// Init the ipNumMap
func (cl *IpConnNumLimiter) Init() error {
cl.m = &ipNumMap{m: make(map[string]int32)}
return nil
}
// DoLimit reports whether the connection num of the ip exceed the maximum number
// If true, return error
func (cl *IpConnNumLimiter) DoLimit(c enet.Conn) (enet.Conn, error) {
ip, _, err := net.SplitHostPort(c.RemoteAddr().String())
if err != nil {
return c, errors.Wrap(err, "split ip addr")
}
if cl.m.Get(ip) >= cl.MaxNum {
return c, errors.Errorf("the ip(%s) exceed the maximum number(%d)", ip, cl.MaxNum)
}
return NewNumConn(c, ip, cl.m), nil
}
// numConn is an implement of enet.Conn
type numConn struct {
key string
m *ipNumMap
enet.Conn
}
// NewNumConn return a numConn
func NewNumConn(c enet.Conn, key string, m *ipNumMap) *numConn {
m.AddOrSet(key)
return &numConn{
m: m,
key: key,
Conn: c,
}
}
// Close is used to decrease the connection num of a ip when connection closing
func (n *numConn) Close() error {
n.m.SubOrDel(n.key)
return n.Conn.Close()
}

View File

@ -0,0 +1,35 @@
package limiter
import (
"ehang.io/nps/lib/enet"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestIpConnNumLimiter(t *testing.T) {
cl := IpConnNumLimiter{MaxNum: 5}
assert.NoError(t, cl.Init())
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
nowNum := 0
close := make(chan struct{})
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
nowNum++
_, err = cl.DoLimit(enet.NewReaderConn(c))
if nowNum > 5 {
assert.Error(t, err)
close <- struct{}{}
} else {
assert.NoError(t, err)
}
}
}()
for i := 6; i > 0; i-- {
go net.Dial("tcp", ln.Addr().String())
}
<-close
}

26
core/limiter/limiter.go Normal file
View File

@ -0,0 +1,26 @@
package limiter
import (
"ehang.io/nps/lib/enet"
)
var (
_ Limiter = (*RateLimiter)(nil)
_ Limiter = (*ConnNumLimiter)(nil)
_ Limiter = (*IpConnNumLimiter)(nil)
_ Limiter = (*FlowLimiter)(nil)
)
type Limiter interface {
DoLimit(conn enet.Conn) (enet.Conn, error)
Init() error
GetName() string
GetZhName() string
}
type baseLimiter struct {
}
func (bl *baseLimiter) Init() error {
return nil
}

67
core/limiter/rate.go Normal file
View File

@ -0,0 +1,67 @@
package limiter
import (
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/rate"
)
// RateLimiter is used to limit the speed of transport
type RateLimiter struct {
baseLimiter
RateLimit int64 `json:"rate_limit" required:"true" placeholder:"10(kb)" zh_name:"最大速度"`
rate *rate.Rate
}
func (rl *RateLimiter) GetName() string {
return "rate"
}
func (rl *RateLimiter) GetZhName() string {
return "带宽限制"
}
// Init the rate controller
func (rl *RateLimiter) Init() error {
if rl.RateLimit > 0 && rl.rate == nil {
rl.rate = rate.NewRate(rl.RateLimit)
rl.rate.Start()
}
return nil
}
// DoLimit return limited Conn
func (rl *RateLimiter) DoLimit(c enet.Conn) (enet.Conn, error) {
return NewRateConn(c, rl.rate), nil
}
// rateConn is used to limiter the rate fo connection
type rateConn struct {
enet.Conn
rate *rate.Rate
}
// NewRateConn return limited connection by rate interface
func NewRateConn(rc enet.Conn, rate *rate.Rate) enet.Conn {
return &rateConn{
Conn: rc,
rate: rate,
}
}
// Read data and remove capacity from rate pool
func (s *rateConn) Read(b []byte) (n int, err error) {
n, err = s.Conn.Read(b)
if s.rate != nil && err == nil {
err = s.rate.Get(int64(n))
}
return
}
// Write data and remove capacity from rate pool
func (s *rateConn) Write(b []byte) (n int, err error) {
n, err = s.Conn.Write(b)
if s.rate != nil && err == nil {
err = s.rate.Get(int64(n))
}
return
}

46
core/limiter/rate_test.go Normal file
View File

@ -0,0 +1,46 @@
package limiter
import (
"bytes"
"ehang.io/nps/lib/enet"
"github.com/stretchr/testify/assert"
"net"
"testing"
"time"
)
func TestRateLimiter(t *testing.T) {
cl := RateLimiter{
RateLimit: 100,
}
assert.NoError(t, cl.Init())
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
nowBytes := 0
close := make(chan struct{})
go func() {
buf := make([]byte, 10)
c, err := ln.Accept()
assert.NoError(t, err)
c, err = cl.DoLimit(enet.NewReaderConn(c))
go func() {
<-time.After(time.Second * 2)
if nowBytes > 500 {
t.Fail()
}
close <- struct{}{}
}()
for {
n, err := c.Read(buf)
nowBytes += n
assert.NoError(t, err)
}
}()
c, err := net.Dial("tcp", ln.Addr().String())
assert.NoError(t, err)
for i := 11; i > 0; i-- {
_, err := c.Write(bytes.Repeat([]byte{0}, 10000))
assert.NoError(t, err)
}
<-close
}

View File

@ -0,0 +1,94 @@
package process
import (
"bufio"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"encoding/base64"
"github.com/pkg/errors"
"go.uber.org/zap"
"net/http"
"strconv"
"strings"
)
type HttpProxyProcess struct {
DefaultProcess
BasicAuth map[string]string `json:"basic_auth" placeholder:"username1 password1\nusername2 password2" zh_name:"basic认证"`
}
func (hpp *HttpProxyProcess) GetName() string {
return "http_proxy"
}
func (hpp *HttpProxyProcess) GetZhName() string {
return "http代理"
}
func (hpp *HttpProxyProcess) ProcessConn(c enet.Conn) (bool, error) {
r, err := http.ReadRequest(bufio.NewReader(c))
if err != nil {
return false, errors.Wrap(err, "read proxy request")
}
if len(hpp.BasicAuth) != 0 && !hpp.checkAuth(r) {
return true, hpp.response(http.StatusProxyAuthRequired, map[string]string{"Proxy-Authenticate": "Basic realm=" + strconv.Quote("nps")}, c)
}
if r.Method == "CONNECT" {
err = hpp.response(200, map[string]string{}, c)
if err != nil {
return true, errors.Wrap(err, "http proxy response")
}
} else if err = c.Reset(0); err != nil {
logger.Warn("reset enet.Conn error", zap.Error(err))
return true, err
}
address := r.Host
if !strings.Contains(r.Host, ":") {
if r.URL.Scheme == "https" {
address = r.Host + ":443"
} else {
address = r.Host + ":80"
}
}
return true, hpp.ac.RunConnWithAddr(c, address)
}
func (hpp *HttpProxyProcess) response(statusCode int, headers map[string]string, c enet.Conn) error {
resp := &http.Response{
Status: http.StatusText(statusCode),
StatusCode: statusCode,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{},
}
for k, v := range headers {
resp.Header.Set(k, v)
}
return resp.Write(c)
}
func (hpp *HttpProxyProcess) checkAuth(r *http.Request) bool {
s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(s) != 2 {
s = strings.SplitN(r.Header.Get("Proxy-Authorization"), " ", 2)
if len(s) != 2 {
return false
}
}
b, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
return false
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
return false
}
for u, p := range hpp.BasicAuth {
if pair[0] == u && pair[1] == p {
return true
}
}
return false
}

View File

@ -0,0 +1,93 @@
package process
import (
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"net/http"
"net/url"
"testing"
)
func TestHttpProxyProcess(t *testing.T) {
sAddr, err := startHttps(t)
assert.NoError(t, err)
hsAddr, err := startHttp(t)
assert.NoError(t, err)
h := HttpProxyProcess{
DefaultProcess: DefaultProcess{},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("http://%s", ln.Addr().String()))
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
resp, err = client.Get(fmt.Sprintf("http://%s/now", hsAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestHttpProxyProcessBasic(t *testing.T) {
sAddr, err := startHttps(t)
h := HttpProxyProcess{
DefaultProcess: DefaultProcess{},
BasicAuth: map[string]string{"aaa": "bbb"},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("http://%s", ln.Addr().String()))
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.Error(t, err)
transport.Proxy = func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("http://%s:%s@%s", "aaa", "bbb", ln.Addr().String()))
}
resp, err = client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}

View File

@ -0,0 +1,79 @@
package process
import (
"bufio"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"github.com/pkg/errors"
"go.uber.org/zap"
"net"
"net/http"
"strings"
"time"
)
// HttpServeProcess is proxy and modify http request
type HttpServeProcess struct {
DefaultProcess
tls bool
Host string `json:"host" required:"true" placeholder:"eg: www.nps.com or *.nps.com" zh_name:"域名"`
RouteUrl string `json:"route_url" placeholder:"/api" zh_name:"匹配路径"`
HeaderModify map[string]string `json:"header_modify" placeholder:"字段 修改值\nHost www.nps-change.com\nAccept */*" zh_name:"请求头修改"`
HostModify string `json:"host_modify" placeholder:"www.nps-changed.com" zh_name:"请求域名"`
AddOrigin bool `json:"add_origin" zh_name:"添加来源"`
CacheTime int64 `json:"cache_time" placeholder:"600s" zh_name:"缓存时间"`
CachePath []string `json:"cache_path" placeholder:".jd\n.css\n.png" zh_name:"缓存路径"`
BasicAuth map[string]string `json:"basic_auth" placeholder:"username1 password1\nusername2 password2" zh_name:"basic认证"`
httpServe *HttpServe
ln *enet.Listener
}
func (hp *HttpServeProcess) GetName() string {
return "http_serve"
}
func (hp *HttpServeProcess) GetZhName() string {
return "http服务"
}
// Init the action of process
func (hp *HttpServeProcess) Init(ac action.Action) error {
hp.ac = ac
hp.ln = enet.NewListener()
hp.httpServe = NewHttpServe(hp.ln, ac)
hp.httpServe.SetModify(hp.HeaderModify, hp.HostModify, hp.AddOrigin)
if hp.CacheTime > 0 {
hp.httpServe.SetCache(hp.CachePath, time.Duration(hp.CacheTime)*time.Second)
}
if len(hp.BasicAuth) != 0 {
hp.httpServe.SetBasicAuth(hp.BasicAuth)
}
if !hp.tls {
go hp.httpServe.Serve()
}
return nil
}
// ProcessConn is used to determine whether to hit the rule
// If true, send enet to httpServe
func (hp *HttpServeProcess) ProcessConn(c enet.Conn) (bool, error) {
req, err := http.ReadRequest(bufio.NewReader(c))
if err != nil {
return false, errors.Wrap(err, "read request")
}
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
return false, errors.Wrap(err, "split host")
}
if !(common.HostContains(hp.Host, host) && (hp.RouteUrl == "" || strings.HasPrefix(req.URL.Path, hp.RouteUrl))) {
logger.Debug("do http proxy failed", zap.String("host", host), zap.String("url", hp.RouteUrl))
return false, nil
}
logger.Debug("do http proxy", zap.String("host", host), zap.String("url", hp.RouteUrl))
if err := c.Reset(0); err != nil {
return true, errors.Wrap(err, "reset connection data")
}
return true, hp.ln.SendConn(c)
}

View File

@ -0,0 +1,74 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHttpServeProcess(t *testing.T) {
sAddr, err := startHttp(t)
assert.NoError(t, err)
h := &HttpServeProcess{
DefaultProcess: DefaultProcess{},
Host: "127.0.0.1",
RouteUrl: "",
HeaderModify: map[string]string{"modify": "nps"},
HostModify: "ehang.io",
AddOrigin: true,
}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{sAddr},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
rep, err := doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/header/modify"))
assert.NoError(t, err)
assert.Equal(t, "nps", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/host"))
assert.NoError(t, err)
assert.Equal(t, "ehang.io", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/origin/xff"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/origin/xri"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
h.BasicAuth = map[string]string{"aaa": "bbb"}
h.Init(ac)
rep, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/now"))
assert.Error(t, err)
_, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/now"), "aaa", "bbb")
assert.NoError(t, err)
h.BasicAuth = map[string]string{}
h.CacheTime = 100
h.CachePath = []string{"/now"}
h.Init(ac)
var time1, time2 string
time1, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
time2, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, time1)
assert.Equal(t, time1, time2)
}

View File

@ -0,0 +1,36 @@
package process
import (
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
)
type HttpsProxyProcess struct {
CertFile string `json:"cert_file" required:"true" placeholder:"/var/cert/cert.pem" zh_name:"cert文件路径"`
KeyFile string `json:"key_file" required:"true" placeholder:"/var/cert/key.pem" zh_name:"key文件路径"`
config *tls.Config
HttpProxyProcess
}
func (hpp *HttpsProxyProcess) GetName() string {
return "https_proxy"
}
func (hpp *HttpsProxyProcess) GetZhName() string {
return "https代理"
}
func (hpp *HttpsProxyProcess) Init(ac action.Action) error {
cer, err := tls.LoadX509KeyPair(hpp.CertFile, hpp.KeyFile)
if err != nil {
return err
}
hpp.config = &tls.Config{Certificates: []tls.Certificate{cer}}
hpp.ac = ac
return nil
}
func (hpp *HttpsProxyProcess) ProcessConn(c enet.Conn) (bool, error) {
return hpp.HttpProxyProcess.ProcessConn(enet.NewReaderConn(tls.Server(c, hpp.config)))
}

View File

@ -0,0 +1,120 @@
package process
import (
"crypto/tls"
"crypto/x509/pkix"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/cert"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"testing"
)
var createCertOnce sync.Once
func createCertFile(t *testing.T) (string, string) {
createCertOnce.Do(func() {
g := cert.NewX509Generator(pkix.Name{
Country: []string{"CN"},
Organization: []string{"Ehang.io"},
OrganizationalUnit: []string{"nps"},
Province: []string{"Beijing"},
CommonName: "nps",
Locality: []string{"Beijing"},
})
cert, key, err := g.CreateRootCa()
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "cert.pem"), cert, 0600))
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "key.pem"), key, 0600))
})
return filepath.Join(os.TempDir(), "cert.pem"), filepath.Join(os.TempDir(), "key.pem")
}
func TestHttpsProxyProcess(t *testing.T) {
sAddr, err := startHttps(t)
certFilePath, keyFilePath := createCertFile(t)
h := HttpsProxyProcess{
HttpProxyProcess: HttpProxyProcess{},
CertFile: certFilePath,
KeyFile: keyFilePath,
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("https://%s", ln.Addr().String()))
},
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestHttpsProxyProcessBasic(t *testing.T) {
certFilePath, keyFilePath := createCertFile(t)
sAddr, err := startHttps(t)
h := HttpsProxyProcess{
HttpProxyProcess: HttpProxyProcess{
BasicAuth: map[string]string{"aaa": "bbb"},
},
CertFile: certFilePath,
KeyFile: keyFilePath,
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("https://%s", ln.Addr().String()))
},
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.Error(t, err)
transport.Proxy = func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("https://%s:%s@%s", "aaa", "bbb", ln.Addr().String()))
}
resp, err = client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}

View File

@ -0,0 +1,41 @@
package process
import (
"ehang.io/nps/lib/cert"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"github.com/pkg/errors"
)
// HttpsRedirectProcess is used to forward https request by ClientHelloMsg
type HttpsRedirectProcess struct {
DefaultProcess
Host string `json:"host" required:"true" placeholder:"https.nps.com" zh_name:"域名"`
}
func (hrp *HttpsRedirectProcess) GetName() string {
return "https_redirect"
}
func (hrp *HttpsRedirectProcess) GetZhName() string {
return "https透传"
}
// ProcessConn is used to determine whether to hit the host rule
func (hrp *HttpsRedirectProcess) ProcessConn(c enet.Conn) (bool, error) {
clientMsg := cert.ClientHelloMsg{}
buf, err := c.AllBytes()
if err != nil {
return false, errors.Wrap(err, "get bytes")
}
if !clientMsg.Unmarshal(buf[5:]) {
return false, errors.New("can not unmarshal client hello message")
}
if common.HostContains(hrp.Host, clientMsg.GetServerName()) {
if err = c.Reset(0); err != nil {
return false, errors.Wrap(err, "reset reader connection")
}
return true, errors.Wrap(hrp.ac.RunConn(c), "run action")
}
return false, nil
}

View File

@ -0,0 +1,43 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHttpsRedirectProcess(t *testing.T) {
sAddr, err := startHttps(t)
assert.NoError(t, err)
h := &HttpsRedirectProcess{
Host: "ehang.io",
}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{sAddr},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
_, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.Error(t, err)
h.Host = "*.github.com"
_, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
}

View File

@ -0,0 +1,51 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/cert"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type HttpsServeProcess struct {
CertFile string `json:"cert_file" required:"true" placeholder:"/var/cert/cert.pem" zh_name:"cert文件路径"`
KeyFile string `json:"key_file" required:"true" placeholder:"/var/cert/key.pem" zh_name:"key文件路径"`
HttpServeProcess
}
func (hsp *HttpsServeProcess) GetName() string {
return "https_serve"
}
func (hsp *HttpsServeProcess) GetZhName() string {
return "https服务"
}
func (hsp *HttpsServeProcess) Init(ac action.Action) error {
hsp.tls = true
err := hsp.HttpServeProcess.Init(ac)
go hsp.httpServe.ServeTLS(hsp.CertFile, hsp.KeyFile)
return err
}
func (hsp *HttpsServeProcess) ProcessConn(c enet.Conn) (bool, error) {
clientMsg := cert.ClientHelloMsg{}
b, err := c.AllBytes()
if err != nil {
return false, errors.Wrap(err, "get bytes")
}
if !clientMsg.Unmarshal(b[5:]) {
return false, errors.New("can not unmarshal client hello message")
}
if common.HostContains(hsp.Host, clientMsg.GetServerName()) {
logger.Debug("do https serve failed", zap.String("host", clientMsg.GetServerName()), zap.String("url", hsp.RouteUrl))
if err := c.Reset(0); err != nil {
return true, errors.Wrap(err, "reset reader connection")
}
return true, hsp.HttpServeProcess.ln.SendConn(c)
}
return false, nil
}

View File

@ -0,0 +1,78 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHttpsProcess(t *testing.T) {
certFile, keyFile := createCertFile(t)
sAddr, err := startHttp(t)
assert.NoError(t, err)
h := &HttpsServeProcess{
HttpServeProcess: HttpServeProcess{
Host: "www.github.com",
RouteUrl: "",
HeaderModify: map[string]string{"modify": "nps"},
HostModify: "ehang.io",
AddOrigin: true,
},
CertFile: certFile,
KeyFile: keyFile,
}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{sAddr},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
rep, err := doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/header/modify"))
assert.NoError(t, err)
assert.Equal(t, "nps", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/host"))
assert.NoError(t, err)
assert.Equal(t, "ehang.io", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/origin/xff"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/origin/xri"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
h.BasicAuth = map[string]string{"aaa": "bbb"}
assert.NoError(t, h.Init(ac))
rep, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.Error(t, err)
_, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"), "aaa", "bbb")
assert.NoError(t, err)
h.BasicAuth = map[string]string{}
h.CacheTime = 100
h.CachePath = []string{"/now"}
assert.NoError(t, h.Init(ac))
var time1, time2 string
time1, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
time2, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, time1)
assert.Equal(t, time1, time2)
}

45
core/process/pb_app.go Normal file
View File

@ -0,0 +1,45 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pb"
"github.com/pkg/errors"
)
type PbAppProcessor struct {
DefaultProcess
}
func (pp *PbAppProcessor) GetName() string {
return "pb_app"
}
func (pp *PbAppProcessor) ProcessConn(c enet.Conn) (bool, error) {
m := &pb.ClientRequest{}
n, err := pb.ReadMessage(c, m)
if err != nil {
return false, nil
}
if _, ok := m.ConnType.(*pb.ClientRequest_AppInfo); !ok {
return false, nil
}
if err := c.Reset(n + 4); err != nil {
return true, errors.Wrap(err, "reset connection data")
}
switch m.GetAppInfo().GetConnType() {
case pb.ConnType_udp:
return true, pp.RunUdp(c)
case pb.ConnType_tcp:
return true, pp.ac.RunConnWithAddr(c, m.GetAppInfo().GetAppAddr())
case pb.ConnType_unix:
ac := &action.LocalAction{TargetAddr: []string{m.GetAppInfo().GetAppAddr()}, UnixSocket: true}
_ = ac.Init()
return true, ac.RunConn(c)
}
return true, errors.Errorf("can not support the conn type(%d)", m.GetAppInfo().GetConnType())
}
func (pp *PbAppProcessor) RunUdp(c enet.Conn) error {
return pp.ac.RunPacketConn(enet.NewTcpPacketConn(c))
}

113
core/process/pb_app_test.go Normal file
View File

@ -0,0 +1,113 @@
package process
import (
"context"
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pb"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"net/http"
"testing"
"time"
)
func TestProtobufProcess(t *testing.T) {
sAddr, err := startHttps(t)
assert.NoError(t, err)
h := &PbAppProcessor{}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{sAddr},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
client := http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
MaxIdleConns: 10000,
IdleConnTimeout: 30 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := net.Dial("tcp", ln.Addr().String())
_, err = pb.WriteMessage(conn, &pb.AppInfo{AppAddr: sAddr})
return conn, err
},
}}
resp, err := client.Get(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, resp)
}
func TestProtobufUdpProcess(t *testing.T) {
finish := make(chan struct{}, 0)
lAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
assert.NoError(t, err)
udpServer, err := net.ListenUDP("udp", lAddr)
assert.NoError(t, err)
h := &PbAppProcessor{}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{udpServer.LocalAddr().String()},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
data := []byte{1, 2, 3, 4}
dataReturn := []byte{4, 5, 6, 7}
conn, err := net.Dial("tcp", ln.Addr().String())
_, err = pb.WriteMessage(conn, &pb.AppInfo{AppAddr: udpServer.LocalAddr().String(), ConnType: pb.ConnType_udp})
go func() {
b := make([]byte, 1024)
n, addr, err := udpServer.ReadFrom(b)
assert.NoError(t, err)
assert.Equal(t, b[:n], data)
_, err = udpServer.WriteTo(dataReturn, addr)
assert.NoError(t, err)
finish <- struct{}{}
}()
c := enet.NewTcpPacketConn(conn)
_, err = c.WriteTo(data, udpServer.LocalAddr())
assert.NoError(t, err)
<-finish
b := make([]byte, 1024)
n, addr, err := c.ReadFrom(b)
assert.NoError(t, err)
assert.Equal(t, dataReturn, b[:n])
assert.Equal(t, addr.String(), udpServer.LocalAddr().String())
}

29
core/process/pb_ping.go Normal file
View File

@ -0,0 +1,29 @@
package process
import (
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pb"
"time"
)
type PbPingProcessor struct {
DefaultProcess
}
func (pp *PbPingProcessor) GetName() string {
return "pb_ping"
}
func (pp *PbPingProcessor) ProcessConn(c enet.Conn) (bool, error) {
m := &pb.ClientRequest{}
_, err := pb.ReadMessage(c, m)
if err != nil {
return false, nil
}
if _, ok := m.ConnType.(*pb.ClientRequest_Ping); !ok {
return false, nil
}
m.GetPing().Now = time.Now().String()
_, err = pb.WriteMessage(c, m)
return true, err
}

View File

@ -0,0 +1,43 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pb"
"github.com/stretchr/testify/assert"
"net"
"testing"
"time"
)
func TestPbPingProcess(t *testing.T) {
h := &PbPingProcessor{}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
conn, err := net.Dial("tcp", ln.Addr().String())
assert.NoError(t, err)
_, err = pb.WriteMessage(conn, &pb.Ping{Now: time.Now().String()})
assert.NoError(t, err)
m := &pb.Ping{}
_, err = pb.ReadMessage(conn, m)
assert.NoError(t, err)
assert.NotEmpty(t, m.Now)
}

51
core/process/process.go Normal file
View File

@ -0,0 +1,51 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
)
var (
_ Process = (*DefaultProcess)(nil)
_ Process = (*HttpServeProcess)(nil)
_ Process = (*HttpsServeProcess)(nil)
_ Process = (*HttpProxyProcess)(nil)
_ Process = (*HttpsProxyProcess)(nil)
_ Process = (*HttpsRedirectProcess)(nil)
_ Process = (*Socks5Process)(nil)
_ Process = (*TransparentProcess)(nil)
)
type Process interface {
Init(action action.Action) error
GetName() string
GetZhName() string
ProcessConn(enet.Conn) (bool, error)
ProcessPacketConn(enet.PacketConn) (bool, error)
}
type DefaultProcess struct {
ac action.Action
}
func (bp *DefaultProcess) ProcessConn(c enet.Conn) (bool, error) {
return true, bp.ac.RunConn(c)
}
func (bp *DefaultProcess) GetName() string {
return "default"
}
func (bp *DefaultProcess) GetZhName() string {
return "默认"
}
// Init the action of process
func (bp *DefaultProcess) Init(ac action.Action) error {
bp.ac = ac
return nil
}
func (bp *DefaultProcess) ProcessPacketConn(pc enet.PacketConn) (bool, error) {
return true, bp.ac.RunPacketConn(pc)
}

169
core/process/serve.go Normal file
View File

@ -0,0 +1,169 @@
package process
import (
"context"
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/logger"
"github.com/gin-contrib/cache"
"github.com/gin-contrib/cache/persistence"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)
type HttpServe struct {
engine *gin.Engine
ln net.Listener
ac action.Action
httpServe *http.Server
cacheStore *persistence.InMemoryStore
cacheTime time.Duration
cachePath []string
headerModify map[string]string
hostModify string
addOrigin bool
reverseProxy *httputil.ReverseProxy
}
func NewHttpServe(ln net.Listener, ac action.Action) *HttpServe {
gin.SetMode(gin.ReleaseMode)
hs := &HttpServe{
ln: ln,
ac: ac,
engine: gin.New(),
}
hs.httpServe = &http.Server{
Handler: hs.engine,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}
hs.reverseProxy = &httputil.ReverseProxy{
Director: func(request *http.Request) {
_ = hs.transport(request)
hs.doModify(request)
},
Transport: &http.Transport{
MaxIdleConns: 10000,
IdleConnTimeout: 30 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return ac.GetServerConn()
},
},
}
serverHttp := func(w http.ResponseWriter, r *http.Request) {
hs.reverseProxy.ServeHTTP(w, r)
}
hs.engine.NoRoute(func(c *gin.Context) {
cached := false
for _, p := range hs.cachePath {
if strings.Contains(c.Request.RequestURI, p) {
cached = true
cache.CachePage(hs.cacheStore, hs.cacheTime, func(c *gin.Context) {
serverHttp(c.Writer, c.Request)
})(c)
}
}
if !cached {
serverHttp(c.Writer, c.Request)
}
})
hs.engine.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
logger.Debug("http request",
zap.String("client_ip", param.ClientIP),
zap.String("method", param.Method),
zap.String("path", param.Path),
zap.String("proto", param.Request.Proto),
zap.Duration("latency", param.Latency),
zap.String("user_agent", param.Request.UserAgent()),
zap.String("error_message", param.ErrorMessage),
zap.Int("response_code", param.StatusCode),
)
return ""
}))
return hs
}
func (hs *HttpServe) transport(req *http.Request) error {
ruri := req.URL.RequestURI()
req.URL.Scheme = "http"
if req.URL.Scheme != "" && req.URL.Opaque == "" {
ruri = req.URL.Scheme + "://" + req.Host + ruri
} else if req.Method == "CONNECT" && req.URL.Path == "" {
// CONNECT requests normally give just the host and port, not a full URL.
ruri = req.Host
if req.URL.Opaque != "" {
ruri = req.URL.Opaque
}
}
req.RequestURI = ""
var err error
req.URL, err = url.Parse(ruri)
return err
}
func (hs *HttpServe) SetBasicAuth(accounts map[string]string) {
hs.engine.Use(gin.BasicAuth(accounts), gin.Recovery())
}
func (hs *HttpServe) SetCache(cachePath []string, cacheTime time.Duration) {
hs.cachePath = cachePath
hs.cacheTime = cacheTime
hs.cacheStore = persistence.NewInMemoryStore(cacheTime * time.Second)
}
func (hs *HttpServe) SetModify(headerModify map[string]string, hostModify string, addOrigin bool) {
hs.headerModify = headerModify
hs.hostModify = hostModify
hs.addOrigin = addOrigin
return
}
func (hs *HttpServe) Serve() error {
return hs.httpServe.Serve(hs.ln)
}
func (hs *HttpServe) ServeTLS(certFile string, keyFile string) error {
return hs.httpServe.ServeTLS(hs.ln, certFile, keyFile)
}
// doModify is used to modify http request
func (hs *HttpServe) doModify(req *http.Request) {
if hs.hostModify != "" {
req.Host = hs.hostModify
}
for k, v := range hs.headerModify {
req.Header.Set(k, v)
}
addr := strings.Split(req.RemoteAddr, ":")[0]
if hs.addOrigin {
// XFF is setting in reverseProxy
req.Header.Set("X-Real-IP", addr)
}
}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
type render struct {
resp *http.Response
}
func (r *render) Render(writer http.ResponseWriter) error {
_, err := io.Copy(writer, r.resp.Body)
return err
}
func (r *render) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.resp.Header.Get("Content-Type")})
}

299
core/process/serve_test.go Normal file
View File

@ -0,0 +1,299 @@
package process
import (
"context"
"crypto/tls"
"ehang.io/nps/core/action"
"fmt"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/websocket"
"io/ioutil"
"net"
"net/http"
"sync"
"testing"
"time"
)
var startHttpOnce sync.Once
var startHttpsOnce sync.Once
var handleOnce sync.Once
var ln net.Listener
var lns net.Listener
var err error
func registerHandle() {
handleOnce.Do(func() {
http.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) {
msg := make([]byte, 512)
n, err := ws.Read(msg)
if err != nil {
return
}
ws.Write(msg[:n])
}))
http.HandleFunc("/now", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(time.Now().String()))
})
http.HandleFunc("/host", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.Host))
})
http.HandleFunc("/header/modify", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.Header.Get("modify")))
})
http.HandleFunc("/origin/xff", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.Header.Get("X-Forwarded-For")))
})
http.HandleFunc("/origin/xri", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.Header.Get("X-Real-IP")))
})
})
}
func startHttp(t *testing.T) (string, error) {
startHttpOnce.Do(func() {
ln, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return
}
registerHandle()
go http.Serve(ln, nil)
})
return ln.Addr().String(), err
}
func startHttps(t *testing.T) (string, error) {
startHttpsOnce.Do(func() {
lns, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return
}
registerHandle()
certFilePath, keyFilePath := createCertFile(t)
go http.ServeTLS(lns, nil, certFilePath, keyFilePath)
})
return lns.Addr().String(), err
}
func doRequest(params ...string) (string, error) {
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
req, err := http.NewRequest("GET", params[0], nil)
if err != nil {
return "", err
}
req.Header.Set("Connection", "close")
if len(params) >= 3 && params[1] != "" {
req.SetBasicAuth(params[1], params[2])
}
if req.URL.Scheme == "https" {
client.Transport = &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return tls.Dial(network, addr, &tls.Config{
InsecureSkipVerify: true,
ServerName: "www.github.com",
})
},
}
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "0", errors.Errorf("respond error, code %d", resp.StatusCode)
}
return string(b), nil
}
func createHttpServe(serverAddr string) (*HttpServe, error) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{serverAddr},
}
ac.Init()
return NewHttpServe(ln, ac), nil
}
func TestHttpServeWebsocket(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
go hs.Serve()
ws, err := websocket.Dial(fmt.Sprintf("ws://%s/ws", hs.ln.Addr().String()), "", fmt.Sprintf("http://%s/ws", hs.ln.Addr().String()))
assert.NoError(t, err)
defer ws.Close() //关闭连接
sendMsg := []byte("nps")
_, err = ws.Write(sendMsg)
assert.NoError(t, err)
msg := make([]byte, 512)
m, err := ws.Read(msg)
assert.NoError(t, err)
assert.Equal(t, sendMsg, msg[:m])
}
func TestHttpsServeWebsocket(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
cert, key := createCertFile(t)
go hs.ServeTLS(cert, key)
config, err := websocket.NewConfig(fmt.Sprintf("wss://%s/ws", hs.ln.Addr().String()), fmt.Sprintf("https://%s/ws", hs.ln.Addr().String()))
assert.NoError(t, err)
config.TlsConfig = &tls.Config{InsecureSkipVerify: true}
ws, err := websocket.DialConfig(config)
assert.NoError(t, err)
defer ws.Close() //关闭连接
sendMsg := []byte("nps")
_, err = ws.Write(sendMsg)
assert.NoError(t, err)
msg := make([]byte, 512)
m, err := ws.Read(msg)
assert.NoError(t, err)
assert.Equal(t, sendMsg, msg[:m])
}
func TestHttpServeModify(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
go hs.Serve()
hs.SetModify(map[string]string{"modify": "test"}, "ehang.io", true)
rep, err := doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/header/modify"))
assert.NoError(t, err)
assert.Equal(t, "test", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/host"))
assert.NoError(t, err)
assert.Equal(t, "ehang.io", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/origin/xff"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/origin/xri"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
}
func TestHttpsServeModify(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
cert, key := createCertFile(t)
go hs.ServeTLS(cert, key)
hs.SetModify(map[string]string{"modify": "test"}, "ehang.io", true)
rep, err := doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/header/modify"))
assert.NoError(t, err)
assert.Equal(t, "test", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/host"))
assert.NoError(t, err)
assert.Equal(t, "ehang.io", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/origin/xff"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/origin/xri"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
}
func TestHttpServeCache(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
go hs.Serve()
hs.SetCache([]string{"now"}, time.Second*10)
var time1, time2 string
time1, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/now"))
assert.NoError(t, err)
time2, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, time1)
assert.Equal(t, time1, time2)
}
func TestHttpsServeCache(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
cert, key := createCertFile(t)
go hs.ServeTLS(cert, key)
hs.SetCache([]string{"now"}, time.Second*10)
var time1, time2 string
time1, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/now"))
assert.NoError(t, err)
time2, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, time1)
assert.Equal(t, time1, time2)
}
func TestHttpServeBasicAuth(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
go hs.Serve()
hs.SetBasicAuth(map[string]string{"aaa": "bbb"})
_, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/now"), "aaa", "bbb")
assert.NoError(t, err)
}
func TestHttpsServeBasicAuth(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
cert, key := createCertFile(t)
go hs.ServeTLS(cert, key)
hs.SetBasicAuth(map[string]string{"aaa": "bbb"})
_, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/now"), "aaa", "bbb")
assert.NoError(t, err)
}

219
core/process/socks5.go Normal file
View File

@ -0,0 +1,219 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"encoding/binary"
"github.com/pkg/errors"
"github.com/robfig/go-cache"
"go.uber.org/zap"
"io"
"net"
"strconv"
"time"
)
type Socks5Process struct {
DefaultProcess
Accounts map[string]string `json:"accounts" placeholder:"username1 password1\nusername2 password2" zh_name:"授权账号密码"`
ServerIp string `json:"server_ip" placeholder:"123.123.123.123" zh_name:"udp连接地址"`
ipStore *cache.Cache
}
const (
ipV4 = 1
domainName = 3
ipV6 = 4
connectMethod = 1
bindMethod = 2
associateMethod = 3
// The maximum packet size of any udp Associate packet, based on ethernet's max size,
// minus the IP and UDP headers5. IPv4 has a 20 byte header, UDP adds an
// additional 4 bytes5. This is a total overhead of 24 bytes5. Ethernet's
// max packet size is 1500 bytes, 1500 - 24 = 1476.
maxUDPPacketSize = 1476
)
const (
succeeded uint8 = iota
serverFailure
notAllowed
networkUnreachable
hostUnreachable
connectionRefused
ttlExpired
commandNotSupported
addrTypeNotSupported
)
const (
UserPassAuth = uint8(2)
userAuthVersion = uint8(1)
authSuccess = uint8(0)
authFailure = uint8(1)
)
func (s5 *Socks5Process) GetName() string {
return "socks5"
}
func (s5 *Socks5Process) GetZhName() string {
return "socks5代理"
}
func (s5 *Socks5Process) Init(ac action.Action) error {
s5.ipStore = cache.New(time.Minute, time.Minute*2)
return s5.DefaultProcess.Init(ac)
}
func (s5 *Socks5Process) ProcessConn(c enet.Conn) (bool, error) {
return true, s5.handleConn(c)
}
func (s5 *Socks5Process) ProcessPacketConn(pc enet.PacketConn) (bool, error) {
ip, _, _ := net.SplitHostPort(pc.LocalAddr().String())
if _, ok := s5.ipStore.Get(ip); !ok {
return false, nil
}
_, addr, err := pc.FirstPacket()
if err != nil {
return false, errors.New("addr not found")
}
return true, s5.ac.RunPacketConn(enet.NewS5PacketConn(pc, addr))
}
func (s5 *Socks5Process) handleConn(c enet.Conn) error {
buf := make([]byte, 2)
if _, err := io.ReadFull(c, buf); err != nil {
return err
}
if version := buf[0]; version != 5 {
return errors.New("only support socks5")
}
nMethods := buf[1]
methods := make([]byte, nMethods)
if l, err := c.Read(methods); l != int(nMethods) || err != nil {
return errors.New("wrong method")
}
if len(s5.Accounts) > 0 {
buf[1] = UserPassAuth
_, err := c.Write(buf)
if err != nil {
return err
}
if err := s5.Auth(c); err != nil {
return errors.Wrap(err, "auth failed")
}
} else {
buf[1] = 0
_, _ = c.Write(buf)
}
return s5.handleRequest(c)
}
func (s5 *Socks5Process) Auth(c enet.Conn) error {
header := []byte{0, 0}
if _, err := io.ReadAtLeast(c, header, 2); err != nil {
return err
}
if header[0] != userAuthVersion {
return errors.New("auth type not support")
}
userLen := int(header[1])
user := make([]byte, userLen)
if _, err := io.ReadAtLeast(c, user, userLen); err != nil {
return err
}
if _, err := c.Read(header[:1]); err != nil {
return errors.New("the length of password is incorrect")
}
passLen := int(header[0])
pass := make([]byte, passLen)
if _, err := io.ReadAtLeast(c, pass, passLen); err != nil {
return err
}
p := s5.Accounts[string(user)]
if p == "" || string(pass) != p {
_, _ = c.Write([]byte{userAuthVersion, authFailure})
return errors.New("auth failure")
}
if _, err := c.Write([]byte{userAuthVersion, authSuccess}); err != nil {
return errors.Wrap(err, "write auth success")
}
return nil
}
func (s5 *Socks5Process) handleRequest(c enet.Conn) error {
header := make([]byte, 3)
_, err := io.ReadFull(c, header)
if err != nil {
return err
}
switch header[1] {
case connectMethod:
s5.handleConnect(c)
case associateMethod:
s5.handleUDP(c)
default:
s5.sendReply(c, commandNotSupported)
c.Close()
}
return nil
}
//enet
func (s5 *Socks5Process) handleConnect(c enet.Conn) {
addr, err := common.ReadAddr(c)
if err != nil {
s5.sendReply(c, addrTypeNotSupported)
logger.Warn("read socks addr error", zap.Error(err))
return
}
s5.sendReply(c, succeeded)
_ = s5.ac.RunConnWithAddr(c, addr.String())
return
}
func (s5 *Socks5Process) handleUDP(c net.Conn) {
_, err := common.ReadAddr(c)
if err != nil {
s5.sendReply(c, addrTypeNotSupported)
logger.Warn("read socks addr error", zap.Error(err))
return
}
ip, _, _ := net.SplitHostPort(c.RemoteAddr().String())
s5.ipStore.Set(ip, true, time.Minute)
s5.sendReply(c, succeeded)
}
func (s5 *Socks5Process) sendReply(c net.Conn, rep uint8) {
reply := []byte{
5,
rep,
0,
1,
}
localHost, localPort, _ := net.SplitHostPort(c.LocalAddr().String())
if s5.ServerIp != "" {
localHost = s5.ServerIp
}
ipBytes := net.ParseIP(localHost).To4()
nPort, _ := strconv.Atoi(localPort)
reply = append(reply, ipBytes...)
portBytes := make([]byte, 2)
binary.BigEndian.PutUint16(portBytes, uint16(nPort))
reply = append(reply, portBytes...)
_, _ = c.Write(reply)
}

152
core/process/socks5_test.go Normal file
View File

@ -0,0 +1,152 @@
package process
import (
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"golang.org/x/net/proxy"
"net"
"net/http"
"net/url"
"testing"
"time"
)
func TestSocks5ProxyProcess(t *testing.T) {
sAddr, err := startHttps(t)
assert.NoError(t, err)
h := Socks5Process{
DefaultProcess: DefaultProcess{},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("socks5://%s", ln.Addr().String()))
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestSocks5ProxyProcessAuth(t *testing.T) {
sAddr, err := startHttps(t)
h := Socks5Process{
DefaultProcess: DefaultProcess{},
Accounts: map[string]string{"aaa": "bbb"},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
auth := proxy.Auth{
User: "aaa",
Password: "bbb",
}
dialer, err := proxy.SOCKS5("tcp", ln.Addr().String(), nil, proxy.Direct)
assert.NoError(t, err)
tr := &http.Transport{Dial: dialer.Dial, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{
Transport: tr,
}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.Error(t, err)
dialer, err = proxy.SOCKS5("tcp", ln.Addr().String(), &auth, proxy.Direct)
assert.NoError(t, err)
tr = &http.Transport{Dial: dialer.Dial, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client = &http.Client{
Transport: tr,
}
resp, err = client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestSocks5ProxyProcessUdp(t *testing.T) {
h := Socks5Process{
DefaultProcess: DefaultProcess{},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
h.ipStore.Set("127.0.0.1", true, time.Minute)
serverPc, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
localPc, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
appPc, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
data := []byte("test")
go func() {
p := make([]byte, 1500)
n, addr, err := appPc.ReadFrom(p)
assert.NoError(t, err)
assert.Equal(t, p[:n], data)
_, err = appPc.WriteTo(data, addr)
assert.NoError(t, err)
}()
go func() {
p := make([]byte, 1500)
n, addr, err := serverPc.ReadFrom(p)
assert.NoError(t, err)
pc := enet.NewReaderPacketConn(serverPc, p[:n], addr)
err = pc.SendPacket(p[:n], addr)
assert.NoError(t, err)
b, err := h.ProcessPacketConn(pc)
assert.Equal(t, b, true)
assert.NoError(t, err)
}()
b := []byte{0, 0, 0}
pAddr, err := common.ParseAddr(appPc.LocalAddr().String())
assert.NoError(t, err)
b = append(b, pAddr...)
b = append(b, data...)
_, err = localPc.WriteTo(b, serverPc.LocalAddr())
assert.NoError(t, err)
p := make([]byte, 1500)
n, _, err := localPc.ReadFrom(p)
assert.NoError(t, err)
respAddr, err := common.SplitAddr(p[3:])
assert.NoError(t, err)
assert.Equal(t, respAddr.String(), appPc.LocalAddr().String())
assert.Equal(t, p[3+len(respAddr):n], data)
}

View File

@ -0,0 +1,56 @@
package process
import (
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"go.uber.org/zap"
"net"
"strconv"
"syscall"
)
const SO_ORIGINAL_DST = 80
type TransparentProcess struct {
DefaultProcess
}
func (tp *TransparentProcess) GetName() string {
return "transparent"
}
func (tp *TransparentProcess) GetZhName() string {
return "透明代理"
}
func (tp *TransparentProcess) ProcessConn(c enet.Conn) (bool, error) {
addr, err := tp.getAddress(c)
if err != nil {
logger.Debug("get syscall error", zap.Error(err))
return false, nil
}
return true, tp.ac.RunConnWithAddr(c, addr)
}
func (tp *TransparentProcess) getAddress(conn net.Conn) (string, error) {
// TODO: IPV6 support
sysrawConn, f := conn.(syscall.Conn)
if !f {
return "", nil
}
rawConn, err := sysrawConn.SyscallConn()
if err != nil {
return "", nil
}
var ip string
var port uint16
err = rawConn.Control(func(fd uintptr) {
addr, err := syscall.GetsockoptIPv6Mreq(int(fd), syscall.IPPROTO_IP, SO_ORIGINAL_DST)
if err != nil {
return
}
ip = net.IP(addr.Multiaddr[4:8]).String()
port = uint16(addr.Multiaddr[2])<<8 + uint16(addr.Multiaddr[3])
})
return net.JoinHostPort(ip, strconv.Itoa(int(port))), nil
}

View File

@ -0,0 +1,23 @@
// +build !linux
package process
import (
"ehang.io/nps/lib/enet"
)
type TransparentProcess struct {
DefaultProcess
}
func (tp *TransparentProcess) GetName() string {
return "transparent"
}
func (tp *TransparentProcess) GetZhName() string {
return "透明代理"
}
func (tp *TransparentProcess) ProcessConn(c enet.Conn) (bool, error) {
return false, nil
}

138
core/rule/list.go Normal file
View File

@ -0,0 +1,138 @@
package rule
import (
"ehang.io/nps/core/action"
"ehang.io/nps/core/handler"
"ehang.io/nps/core/limiter"
"ehang.io/nps/core/process"
"ehang.io/nps/core/server"
"github.com/fatih/structtag"
"reflect"
"strconv"
)
var orderMap map[string]int
var nowOrder = 2<<8 - 1
type children map[string]*List
var chains children
var limiters children
func init() {
orderMap = make(map[string]int, 0)
chains = make(map[string]*List, 0)
limiters = make(map[string]*List, 0)
chains.Append(&server.TcpServer{}).Append(&handler.HttpHandler{}).Append(&process.HttpServeProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{}, &action.AdminAction{})
chains.Append(&server.TcpServer{}).Append(&handler.HttpsHandler{}).Append(&process.HttpsServeProcess{HttpServeProcess: process.HttpServeProcess{}}).AppendMany(&action.NpcAction{}, &action.LocalAction{}, &action.AdminAction{})
chains.Append(&server.TcpServer{}).Append(&handler.HttpsHandler{}).Append(&process.HttpsRedirectProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{})
chains.Append(&server.TcpServer{}).Append(&handler.HttpHandler{}).Append(&process.HttpProxyProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{})
chains.Append(&server.TcpServer{}).Append(&handler.HttpsHandler{}).Append(&process.HttpsProxyProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{})
chains.Append(&server.TcpServer{}).Append(&handler.Socks5Handler{}).Append(&process.Socks5Process{}).AppendMany(&action.LocalAction{}, &action.NpcAction{})
chains.Append(&server.TcpServer{}).Append(&handler.TransparentHandler{}).Append(&process.TransparentProcess{}).AppendMany(&action.NpcAction{})
chains.Append(&server.TcpServer{}).Append(&handler.RdpHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{})
chains.Append(&server.TcpServer{}).Append(&handler.RedisHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{})
chains.Append(&server.UdpServer{}).Append(&handler.DnsHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{})
// TODO p2p
chains.Append(&server.UdpServer{}).Append(&handler.P2PHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{})
chains.Append(&server.UdpServer{}).Append(&handler.QUICHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.BridgeAction{})
chains.Append(&server.UdpServer{}).Append(&handler.Socks5UdpHandler{}).Append(&process.Socks5Process{}).AppendMany(&action.LocalAction{}, &action.NpcAction{})
chains.Append(&server.TcpServer{}).Append(&handler.DefaultHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.BridgeAction{}, &action.AdminAction{}, &action.NpcAction{}, &action.LocalAction{})
limiters.AppendMany(&limiter.RateLimiter{}, &limiter.ConnNumLimiter{}, &limiter.FlowLimiter{}, &limiter.IpConnNumLimiter{})
}
func GetLimiters() children {
return limiters
}
func GetChains() children {
return chains
}
type NameInterface interface {
GetName() string
GetZhName() string
}
type List struct {
ZhName string `json:"zh_name"`
Self interface{} `json:"-"`
Field []field `json:"field"`
Children children `json:"children"`
}
func (c children) AppendMany(child ...NameInterface) {
for _, cd := range child {
c.Append(cd)
}
}
func (c children) Append(child NameInterface) children {
if v, ok := c[child.GetName()]; ok {
return v.Children
}
if _, ok := orderMap[child.GetName()]; !ok {
orderMap[child.GetName()] = nowOrder
nowOrder--
}
cd := &List{Self: child, Field: getFieldName(child), Children: make(map[string]*List, 0), ZhName: child.GetZhName()}
c[child.GetName()] = cd
return cd.Children
}
type field struct {
FiledType string `json:"field_type"`
FieldName string `json:"field_name"`
FieldZhName string `json:"field_zh_name"`
FieldRequired bool `json:"field_required"`
FieldExample string `json:"field_example"`
}
func getFieldName(structName interface{}, child ...bool) []field {
result := make([]field, 0)
t := reflect.TypeOf(structName)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return result
}
fieldNum := t.NumField()
for i := 0; i < fieldNum; i++ {
if len(child) == 0 && t.Field(i).Type.Kind() == reflect.Struct {
value := reflect.ValueOf(structName)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
if value.Field(i).CanInterface() {
result = append(result, getFieldName(value.Field(i).Interface(), true)...)
}
}
tags, err := structtag.Parse(string(t.Field(i).Tag))
if err == nil {
tag, err := tags.Get("json")
if err == nil {
f := field{}
f.FiledType = t.Field(i).Type.Kind().String()
f.FieldName = tag.Name
tag, err = tags.Get("required")
if err == nil {
f.FieldRequired, _ = strconv.ParseBool(tag.Name)
}
tag, err = tags.Get("placeholder")
if err == nil {
f.FieldExample = tag.Name
}
tag, err = tags.Get("zh_name")
if err == nil {
f.FieldZhName = tag.Name
}
result = append(result, f)
}
}
}
return result
}

13
core/rule/list_test.go Normal file
View File

@ -0,0 +1,13 @@
package rule
import (
"ehang.io/nps/core/process"
"testing"
)
func TestGetFields(t *testing.T) {
h := process.HttpsServeProcess{HttpServeProcess: process.HttpServeProcess{}}
if len(getFieldName(h)) < 3 {
t.Fail()
}
}

71
core/rule/rule.go Normal file
View File

@ -0,0 +1,71 @@
package rule
import (
"ehang.io/nps/core/action"
"ehang.io/nps/core/handler"
"ehang.io/nps/core/limiter"
"ehang.io/nps/core/process"
"ehang.io/nps/core/server"
"ehang.io/nps/lib/enet"
"github.com/pkg/errors"
)
type Rule struct {
Server server.Server `json:"server"`
Handler handler.Handler `json:"handler"`
Process process.Process `json:"process"`
Action action.Action `json:"action"`
Limiters []limiter.Limiter `json:"limiters"`
}
var servers map[string]server.Server
func init() {
servers = make(map[string]server.Server, 0)
}
func (r *Rule) GetHandler() handler.Handler {
return r.Handler
}
func (r *Rule) Init() error {
s := r.Server
var ok bool
if s, ok = servers[r.Server.GetName()+":"+r.Server.GetServerAddr()]; !ok {
s = r.Server
err := s.Init()
servers[r.Server.GetName()+":"+r.Server.GetServerAddr()] = s
if err != nil {
return err
}
go s.Serve()
}
s.RegisterHandle(r)
r.Handler.AddRule(r)
if err := r.Action.Init(); err != nil {
return err
}
for _, l := range r.Limiters {
if err := l.Init(); err != nil {
return err
}
}
return r.Process.Init(r.Action)
}
func (r *Rule) RunConn(c enet.Conn) (bool, error) {
var err error
for _, lm := range r.Limiters {
if c, err = lm.DoLimit(c); err != nil {
return true, errors.Wrap(err, "rule run")
}
}
if err = c.Reset(0); err != nil {
return false, err
}
return r.Process.ProcessConn(c)
}
func (r *Rule) RunPacketConn(pc enet.PacketConn) (bool, error) {
return r.Process.ProcessPacketConn(pc)
}

92
core/rule/rule_json.go Normal file
View File

@ -0,0 +1,92 @@
package rule
import (
"ehang.io/nps/core/action"
"ehang.io/nps/core/handler"
"ehang.io/nps/core/limiter"
"ehang.io/nps/core/process"
"ehang.io/nps/core/server"
"encoding/json"
"github.com/pkg/errors"
"reflect"
)
type JsonData struct {
ObjType string `json:"obj_type"`
ObjData string `json:"obj_data"`
}
type JsonRule struct {
Name string `json:"name"`
Uuid string `json:"uuid"`
Status int `json:"status"`
Extend int `json:"extend"`
Server JsonData `json:"server"`
Handler JsonData `json:"handler"`
Process JsonData `json:"process"`
Action JsonData `json:"action"`
Limiters []JsonData `json:"limiters"`
Remark string `json:"remark"`
}
var NotFoundError = errors.New("not found")
func (jd *JsonRule) ToRule() (*Rule, error) {
r := &Rule{Limiters: make([]limiter.Limiter, 0)}
s, ok := chains[jd.Server.ObjType]
if !ok {
return nil, NotFoundError
}
r.Server = clone(s.Self).(server.Server)
err := json.Unmarshal([]byte(jd.Server.ObjData), r.Server)
if err != nil {
return nil, err
}
h, ok := s.Children[jd.Handler.ObjType]
if !ok {
return nil, NotFoundError
}
r.Handler = clone(h.Self).(handler.Handler)
err = json.Unmarshal([]byte(jd.Handler.ObjData), r.Handler)
if err != nil {
return nil, err
}
p, ok := h.Children[jd.Process.ObjType]
if !ok {
return nil, NotFoundError
}
r.Process = clone(p.Self).(process.Process)
err = json.Unmarshal([]byte(jd.Process.ObjData), r.Process)
if err != nil {
return nil, err
}
a, ok := p.Children[jd.Action.ObjType]
if !ok {
return nil, NotFoundError
}
r.Action = clone(a.Self).(action.Action)
err = json.Unmarshal([]byte(jd.Action.ObjData), r.Action)
if err != nil {
return nil, err
}
for _, v := range jd.Limiters {
l, ok := limiters[v.ObjType]
if !ok {
return nil, NotFoundError
}
lm := clone(l.Self).(limiter.Limiter)
err = json.Unmarshal([]byte(v.ObjData), lm)
if err != nil {
return nil, err
}
r.Limiters = append(r.Limiters, lm)
}
return r, nil
}
func clone(i interface{}) interface{} {
v := reflect.ValueOf(i).Elem()
vNew := reflect.New(v.Type())
vNew.Elem().Set(v)
return vNew.Interface()
}

View File

@ -0,0 +1,58 @@
package rule
import (
"ehang.io/nps/core/action"
"ehang.io/nps/core/handler"
"ehang.io/nps/core/process"
"ehang.io/nps/core/server"
"encoding/json"
"github.com/stretchr/testify/assert"
"reflect"
"testing"
)
func TestClone(t *testing.T) {
type person struct {
Name string
Age int
}
a := &person{
Name: "ALice",
Age: 20,
}
b := clone(a).(*person)
assert.Equal(t, a.Name, b.Name)
assert.Equal(t, a.Age, b.Age)
a.Name = "Bob"
a.Age = 21
assert.NotEqual(t, a.Name, b.Name)
assert.NotEqual(t, a.Age, b.Age)
assert.NotEqual(t, reflect.ValueOf(a).Pointer(), reflect.ValueOf(b).Pointer())
}
func getJson(t *testing.T, i interface{}) string {
b, err := json.Marshal(i)
assert.NoError(t, err)
assert.NotEmpty(t, string(b))
return string(b)
}
func TestJsonRule(t *testing.T) {
s := &server.TcpServer{ ServerAddr: "127.0.0.1:0"}
h := &handler.HttpHandler{}
p := &process.HttpServeProcess{}
a := &action.LocalAction{}
js := JsonRule{
Uuid: "",
Server: JsonData{s.GetName(), getJson(t, s)},
Handler: JsonData{h.GetName(), getJson(t, h)},
Process: JsonData{p.GetName(), getJson(t, p)},
Action: JsonData{a.GetName(), getJson(t, a)},
Limiters: make([]JsonData, 0),
}
rl, err := js.ToRule()
assert.NoError(t, err)
err = rl.Init()
assert.NoError(t, err)
assert.Equal(t, rl.Server.(*server.TcpServer).ServerAddr, "127.0.0.1:0")
}

45
core/rule/rule_test.go Normal file
View File

@ -0,0 +1,45 @@
package rule
import (
"ehang.io/nps/core/action"
"ehang.io/nps/core/handler"
"ehang.io/nps/core/limiter"
"ehang.io/nps/core/process"
"ehang.io/nps/core/server"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestRule(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
r := &Rule{
Server: &server.TcpServer{ServerAddr: "127.0.0.1:0"},
Handler: &handler.DefaultHandler{},
Process: &process.DefaultProcess{},
Action: &action.LocalAction{TargetAddr: []string{ln.Addr().String()}},
Limiters: make([]limiter.Limiter, 0),
}
err = r.Init()
assert.NoError(t, err)
data := []byte("test")
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
b := make([]byte, 1024)
n, err := conn.Read(b)
assert.NoError(t, err)
assert.Equal(t, data, b[:n])
_, err = conn.Write(b[:n])
assert.NoError(t, err)
}()
conn, err := net.Dial(r.Server.GetName(), r.Server.GetServerAddr())
assert.NoError(t, err)
_, err = conn.Write(data)
assert.NoError(t, err)
b := make([]byte, 1024)
n, err := conn.Read(b)
assert.NoError(t, err)
assert.Equal(t, b[:n], data)
}

30
core/rule/sort.go Normal file
View File

@ -0,0 +1,30 @@
package rule
import "ehang.io/nps/core/process"
type Sort []*Rule
func (s Sort) Len() int { return len(s) }
func (s Sort) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Less rule sort by
func (s Sort) Less(i, j int) bool {
iHandlerSort := orderMap[s[i].Handler.GetName()]
iProcessSort := orderMap[s[i].Process.GetName()]
jHandlerSort := orderMap[s[j].Handler.GetName()]
jProcessSort := orderMap[s[j].Process.GetName()]
iSort := iHandlerSort<<16 | iProcessSort<<8
jSort := jHandlerSort<<16 | jProcessSort<<8
if vi, ok := s[i].Process.(*process.HttpServeProcess); ok {
if vj, ok := s[j].Process.(*process.HttpServeProcess); ok {
iSort = iSort | (len(vj.RouteUrl) & (2 ^ 8 - 1))
jSort = jSort | (len(vi.RouteUrl) & (2 ^ 8 - 1))
}
}
if vi, ok := s[i].Process.(*process.HttpsServeProcess); ok {
if vj, ok := s[j].Process.(*process.HttpsServeProcess); ok {
iSort = iSort | (len(vj.RouteUrl) & (2 ^ 8 - 1))
jSort = jSort | (len(vi.RouteUrl) & (2 ^ 8 - 1))
}
}
return iSort > jSort
}

27
core/rule/sort_test.go Normal file
View File

@ -0,0 +1,27 @@
package rule
import (
"ehang.io/nps/core/handler"
"ehang.io/nps/core/process"
"sort"
"testing"
)
func TestSort_Len(t *testing.T) {
r1 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.TransparentProcess{}}
r2 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.DefaultProcess{}}
r3 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.HttpServeProcess{RouteUrl: "/test/aaa"}}
r4 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.Socks5Process{}}
r5 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.HttpServeProcess{RouteUrl: "/test"}}
r6 := &Rule{Handler: &handler.HttpsHandler{}, Process: &process.HttpsProxyProcess{}}
s := make(Sort, 0)
s = append(s, r1, r2, r3, r4, r5, r6)
sort.Sort(s)
expected := make(Sort, 0)
expected = append(expected, r6, r5, r3, r4, r1, r2)
for k, v := range expected {
if v != s[k] {
t.Fail()
}
}
}

32
core/server/server.go Normal file
View File

@ -0,0 +1,32 @@
package server
import "ehang.io/nps/core/handler"
type rule interface {
handler.RuleRun
GetHandler() handler.Handler
}
type Server interface {
Init() error
Serve()
GetServerAddr() string
GetName() string
GetZhName() string
RegisterHandle(rl rule)
}
type BaseServer struct {
handlers map[string]handler.Handler
}
func (bs *BaseServer) RegisterHandle(rl rule) {
var h handler.Handler
var ok bool
if h, ok = bs.handlers[rl.GetHandler().GetName()]; !ok {
h = rl.GetHandler()
bs.handlers[h.GetName()] = h
}
h.AddRule(rl)
return
}

97
core/server/tcp.go Normal file
View File

@ -0,0 +1,97 @@
package server
import (
"ehang.io/nps/core/handler"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"ehang.io/nps/lib/pool"
"github.com/panjf2000/ants/v2"
"go.uber.org/zap"
"io"
"net"
)
var bp = pool.NewBufferPool(1500)
type TcpServer struct {
BaseServer
ServerAddr string `json:"server_addr" required:"true" placeholder:"0.0.0.0:8080 or :8080" zh_name:"监听地址"`
listener net.Listener
gp *ants.PoolWithFunc
}
func (cm *TcpServer) GetServerAddr() string {
if cm.listener == nil {
return cm.ServerAddr
}
return cm.listener.Addr().String()
}
func (cm *TcpServer) Init() error {
var err error
cm.handlers = make(map[string]handler.Handler, 0)
if err = cm.listen(); err != nil {
return err
}
cm.gp, err = ants.NewPoolWithFunc(1000000, func(i interface{}) {
rc := enet.NewReaderConn(i.(net.Conn))
buf := bp.Get()
defer bp.Put(buf)
if _, err := io.ReadAtLeast(rc, buf, 3); err != nil {
logger.Warn("read handle type fom connection failed", zap.String("remote addr", rc.RemoteAddr().String()))
_ = rc.Close()
return
}
logger.Debug("read handle type", zap.Uint8("type 1", buf[0]), zap.Uint8("type 2", buf[1]),
zap.Uint8("type 3", buf[2]), zap.String("remote addr", rc.RemoteAddr().String()))
for _, h := range cm.handlers {
err = rc.Reset(0)
if err != nil {
logger.Warn("reset connection error", zap.Error(err), zap.String("remote addr", rc.RemoteAddr().String()))
_ = rc.Close()
return
}
ok, err := h.HandleConn(buf, rc)
if err != nil {
logger.Warn("handle connection error", zap.Error(err), zap.String("remote addr", rc.RemoteAddr().String()))
return
}
if ok {
logger.Debug("handle connection success", zap.String("remote addr", rc.RemoteAddr().String()))
return
}
}
})
return nil
}
func (cm *TcpServer) GetName() string {
return "tcp"
}
func (cm *TcpServer) GetZhName() string {
return "tcp服务"
}
// create a listener accept user and npc
func (cm *TcpServer) listen() error {
var err error
cm.listener, err = net.Listen("tcp", cm.ServerAddr)
if err != nil {
return err
}
return nil
}
func (cm *TcpServer) Serve() {
for {
c, err := cm.listener.Accept()
if err != nil {
logger.Error("accept enet error", zap.Error(err))
break
}
_ = cm.gp.Invoke(c)
}
}

80
core/server/udp.go Normal file
View File

@ -0,0 +1,80 @@
package server
import (
"ehang.io/nps/core/handler"
"ehang.io/nps/lib/logger"
"github.com/panjf2000/ants/v2"
"go.uber.org/zap"
"net"
)
type UdpServer struct {
ServerAddr string `json:"server_addr" required:"true" placeholder:"0.0.0.0:8080 or :8080" zh_name:"监听地址"`
gp *ants.PoolWithFunc
packetConn net.PacketConn
handlers map[string]handler.Handler
}
type udpPacket struct {
n int
buf []byte
addr net.Addr
}
func (us *UdpServer) Init() error {
us.handlers = make(map[string]handler.Handler, 0)
if err := us.listen(); err != nil {
return err
}
var err error
us.gp, err = ants.NewPoolWithFunc(1000000, func(i interface{}) {
p := i.(*udpPacket)
defer bp.Put(p.buf)
logger.Debug("accept a now packet", zap.String("remote addr", p.addr.String()))
})
return err
}
func (us *UdpServer) GetServerAddr() string {
if us.packetConn == nil {
return us.ServerAddr
}
return us.packetConn.LocalAddr().String()
}
func (us *UdpServer) GetName() string {
return "udp"
}
func (us *UdpServer) GetZhName() string {
return "udp服务"
}
func (us *UdpServer) listen() error {
addr, err := net.ResolveUDPAddr("udp", us.ServerAddr)
if err != nil {
return err
}
us.packetConn, err = net.ListenUDP("udp", addr)
if err != nil {
return err
}
return nil
}
func (us *UdpServer) Serve() {
for {
buf := bp.Get()
n, addr, err := us.packetConn.ReadFrom(buf)
if err != nil {
logger.Error("accept packet failed", zap.Error(err))
break
}
err = us.gp.Invoke(udpPacket{n: n, buf: buf, addr: addr})
if err != nil {
logger.Error("Invoke error", zap.Error(err))
}
}
}

216
db/db.go Normal file
View File

@ -0,0 +1,216 @@
package db
import (
"ehang.io/nps/lib/logger"
"github.com/pkg/errors"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
glog "gorm.io/gorm/logger"
"log"
"reflect"
"time"
)
type Db interface {
Init() error
SetConfig(key string, val string) error
GetConfig(key string) (string, error)
Insert(table string, uuid string, data string) error
Delete(table string, uuid string) error
Update(table string, uuid string, data string) error
Count(table string, filterValue string) (int64, error)
QueryOne(table string, uuid string) (string, error)
QueryAll(table string, filterValue string) ([]string, error)
QueryPage(table string, limit int, offset int, filterValue string) ([]string, error)
}
type dbLogger struct{}
func (_ dbLogger) Write(p []byte) (n int, err error) {
logger.Warn(string(p))
return len(p), nil
}
var _ Db = (*SqliteDb)(nil)
type SqliteDb struct {
Path string
db *gorm.DB
}
func NewSqliteDb(path string) *SqliteDb {
return &SqliteDb{Path: path}
}
func (sd *SqliteDb) SetConfig(key string, val string) error {
c := &Config{Key: key}
err := sd.db.First(c, "key = ?", key).Error
if err != nil {
return sd.db.Create(&Config{Key: key, Val: val}).Error
}
return sd.db.Model(c).Update("val", val).Error
}
func (sd *SqliteDb) GetConfig(key string) (string, error) {
c := &Config{}
err := sd.db.First(c, "key = ?", key).Error
return c.Val, err
}
func (sd *SqliteDb) Count(table string, filterValue string) (int64, error) {
i, err := sd.GetTable(table, "", "")
if err != nil {
return 0, err
}
var count int64
err = sd.db.Where("data LIKE ?", "%"+filterValue+"%").Model(i).Count(&count).Error
return count, err
}
func (sd *SqliteDb) QueryOne(table string, uuid string) (string, error) {
i, err := sd.GetTable(table, "", "")
if err != nil {
return "", err
}
err = sd.db.First(i, "uuid = ?", uuid).Error
if err != nil {
return "", err
}
return getData(i), err
}
func getData(i interface{}) string {
immutable := reflect.ValueOf(i)
return immutable.Elem().FieldByName("Data").String()
}
func (sd *SqliteDb) QueryAll(table string, filterValue string) (data []string, err error) {
list := make([]string, 0)
switch table {
case "rule":
var r []Rule
d := sd.db.Order("id desc")
if filterValue != "" {
d = d.Where("data LIKE ?", "%"+filterValue+"%")
}
err = d.Find(&r).Error
for _, v := range r {
list = append(list, v.Data)
}
case "cert":
var c []Cert
d := sd.db.Order("id desc")
if filterValue != "" {
d = d.Where("data LIKE ?", "%"+filterValue+"%")
}
err = d.Find(&c).Error
for _, v := range c {
list = append(list, v.Data)
}
default:
err = errors.New("error table")
}
return list, err
}
func (sd *SqliteDb) QueryPage(table string, limit int, offset int, filterValue string) (data []string, err error) {
list := make([]string, 0)
switch table {
case "rule":
var r []Rule
d := sd.db.Limit(limit).Offset(offset)
if filterValue != "" {
d = d.Where("data LIKE ?", "%"+filterValue+"%")
}
err = d.Order("id desc").Find(&r).Error
for _, v := range r {
list = append(list, v.Data)
}
case "cert":
var c []Cert
d := sd.db.Limit(limit).Offset(offset)
if filterValue != "" {
d = d.Where("data LIKE ?", "%"+filterValue+"%")
}
err = d.Order("id desc").Find(&c).Error
for _, v := range c {
list = append(list, v.Data)
}
default:
err = errors.New("error table")
}
return list, err
}
func (sd *SqliteDb) Insert(table string, uuid string, data string) error {
i, err := sd.GetTable(table, uuid, data)
if err != nil {
return err
}
return sd.db.Create(i).Error
}
func (sd *SqliteDb) Delete(table string, uuid string) error {
i, err := sd.GetTable(table, uuid, "")
if err != nil {
return err
}
return sd.db.Where("uuid", uuid).Delete(i).Error
}
func (sd *SqliteDb) Update(table string, uuid string, data string) error {
i, err := sd.GetTable(table, uuid, "")
if err != nil {
return err
}
return sd.db.Model(i).Where("uuid", uuid).Update("data", data).Error
}
func (sd *SqliteDb) Init() error {
var err error
newLogger := glog.New(
log.New(dbLogger{}, "\r\n", log.LstdFlags),
glog.Config{
SlowThreshold: time.Second,
LogLevel: glog.Silent,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
)
sd.db, err = gorm.Open(sqlite.Open(sd.Path), &gorm.Config{Logger: newLogger})
if err != nil {
return err
}
return sd.db.AutoMigrate(&Cert{}, &Rule{}, &Config{})
}
func (sd *SqliteDb) GetTable(table string, uuid string, data string) (i interface{}, err error) {
switch table {
case "rule":
i = &Rule{Uuid: uuid, Data: data}
case "cert":
i = &Cert{Uuid: uuid, Data: data}
default:
err = errors.New("error table")
}
return
}
type Cert struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Uuid string `gorm:"column:uuid" json:"uuid"`
Data string `gorm:"column:data" json:"data"`
}
type Rule struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Uuid string `gorm:"column:uuid" json:"uuid"`
Data string `gorm:"column:data" json:"data"`
}
type Config struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Key string `gorm:"column:key" json:"key"`
Val string `gorm:"column:val" json:"val"`
}

73
db/db_test.go Normal file
View File

@ -0,0 +1,73 @@
package db
import (
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"strconv"
"testing"
)
func TestSqlite(t *testing.T) {
err := os.Remove(filepath.Join(os.TempDir(), "test.db"))
assert.NoError(t, err)
d := NewSqliteDb(filepath.Join(os.TempDir(), "test.db"))
err = d.Init()
assert.NoError(t, err)
for _, tableName := range []string{"rule", "cert"} {
var firstUuid, lastUuid string
for i := 0; i < 1000; i++ {
uid := uuid.NewV4().String()
if i == 0 {
firstUuid = uid
}
lastUuid = uid
err = d.Insert(tableName, uid, "test"+strconv.Itoa(i))
assert.NoError(t, err)
}
n, err := d.Count(tableName,"")
assert.NoError(t, err)
assert.Equal(t, int(n), 1000)
list, err := d.QueryAll(tableName, "")
assert.NoError(t, err)
assert.Equal(t, len(list), 1000)
one, err := d.QueryOne(tableName, firstUuid)
assert.NoError(t, err)
assert.Equal(t, one, "test0")
list, err = d.QueryPage(tableName, 10, 10, "")
assert.NoError(t, err)
assert.Equal(t, len(list), 10)
assert.Equal(t, list[0], "test989")
err = d.Delete(tableName, lastUuid)
assert.NoError(t, err)
n, err = d.Count(tableName,"")
assert.NoError(t, err)
assert.Equal(t, n, int64(999))
one, err = d.QueryOne(tableName, firstUuid)
assert.NoError(t, err)
err = d.Update(tableName, firstUuid, "test_new")
assert.NoError(t, err)
one, err = d.QueryOne(tableName, firstUuid)
assert.NoError(t, err)
assert.Equal(t, one, "test_new")
}
err = d.SetConfig("test_key1", "test_val1")
assert.NoError(t, err)
v, err := d.GetConfig("test_key1")
assert.NoError(t, err)
assert.Equal(t, v, "test_val1")
v, err = d.GetConfig("test_key2")
assert.Error(t, err)
assert.Equal(t, v, "")
err = d.SetConfig("test_key2", "test_val2")
assert.NoError(t, err)
v, err = d.GetConfig("test_key2")
assert.NoError(t, err)
assert.Equal(t, v, "test_val2")
err = d.SetConfig("test_key1", "test_val2")
assert.NoError(t, err)
v, err = d.GetConfig("test_key1")
assert.NoError(t, err)
assert.Equal(t, v, "test_val2")
}

43
go.mod Normal file
View File

@ -0,0 +1,43 @@
module ehang.io/nps
go 1.16
require (
github.com/appleboy/gin-jwt v2.5.0+incompatible // indirect
github.com/appleboy/gin-jwt/v2 v2.7.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/gin-contrib/cache v1.1.0 // indirect
github.com/gin-contrib/cors v1.3.1 // indirect
github.com/gin-gonic/gin v1.7.6 // indirect
github.com/gliderlabs/ssh v0.3.3 // indirect
github.com/go-redis/redis/v8 v8.11.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87
github.com/icodeface/grdp v0.0.0-20200414055757-e0008b0b5cb2 // indirect
github.com/knadh/koanf v1.3.3 // indirect
github.com/lucas-clemente/quic-go v0.24.0
github.com/marten-seemann/qtls v0.10.0 // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/panjf2000/ants/v2 v2.4.6 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/tidwall/gjson v1.12.1 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.opentelemetry.io/otel v0.20.0 // indirect
go.uber.org/zap v1.19.1
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e // indirect
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gorm.io/driver/sqlite v1.2.6 // indirect
gorm.io/gorm v1.22.3 // indirect
)

546
go.sum Normal file
View File

@ -0,0 +1,546 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/appleboy/gin-jwt v2.5.0+incompatible h1:oLQTP1fiGDoDKoC2UDqXD9iqCP44ABIZMMenfH/xCqw=
github.com/appleboy/gin-jwt v2.5.0+incompatible/go.mod h1:pG7tv32IEe5wEh1NSQzcyD02ZZAqZWp07RdGiIhgaRQ=
github.com/appleboy/gin-jwt/v2 v2.7.0 h1:MjbX0OVC1hmb+cYNSW7yrlG8KfIN/X0qn5kqhAsHinY=
github.com/appleboy/gin-jwt/v2 v2.7.0/go.mod h1:AP2pmslwuqdHcjua9m8APdgqX9Ksx0ZM34d5RGvUYBU=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cache v1.1.0 h1:lM8B4YtzdQQM6ThTlvtNPeBNfW1mNdh/CMFQfenH1dk=
github.com/gin-contrib/cache v1.1.0/go.mod h1:9ylpYjLq309/y5hTpyuDxfPG+V6QlSB56vrWe6OhoLQ=
github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.7.6 h1:Ma2JlolDP9KCHuHTrW58EIIxVUQKxSxzuCKguCYyFas=
github.com/gin-gonic/gin v1.7.6/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/icodeface/grdp v0.0.0-20200414055757-e0008b0b5cb2 h1:ygCqbylErDVlQ3ykPaa8lmfrbPQoiGcOlSgC+Ej8VgI=
github.com/icodeface/grdp v0.0.0-20200414055757-e0008b0b5cb2/go.mod h1:AENknrjjTG+yAL3EFNMDxSALP140yz4RXJOqIwGulig=
github.com/icodeface/tls v0.0.0-20190904082144-a3e1fe30543e h1:3V+yaobzgt0CfQTbMoTEwDY5qbvrVnRgr96JBZ00Vhw=
github.com/icodeface/tls v0.0.0-20190904082144-a3e1fe30543e/go.mod h1:VJNHW2GxCtQP/IQtXykBIPBV8maPJ/dHWirVTwm9GwY=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knadh/koanf v1.3.3 h1:eNtBOzQDzkzIIPRCJCx/Ha3DeD/ZFwCAp8JxyqoVAls=
github.com/knadh/koanf v1.3.3/go.mod h1:1cfH5223ZeZUOs8FU2UdTmaNfHpqgtjV0+NHjRO43gs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lucas-clemente/quic-go v0.24.0 h1:ToR7SIIEdrgOhgVTHvPgdVRJfgVy+N0wQAagH7L4d5g=
github.com/lucas-clemente/quic-go v0.24.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0=
github.com/lunixbochs/struc v0.0.0-20190326164542-a9e4041416c2 h1:xvBq0/ARZLqmB57m6jds017I+KtXPcsKBHv6dUUac4A=
github.com/lunixbochs/struc v0.0.0-20190326164542-a9e4041416c2/go.mod h1:iOJu9pApjjmEmNq7PqlA5R9mDu/HMF5EM3llWKX/TyA=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls v0.10.0 h1:ECsuYUKalRL240rRD4Ri33ISb7kAQ3qGDlrrl55b2pc=
github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs=
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco=
github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk=
github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/memcachier/mc v2.0.1+incompatible h1:s8EDz0xrJLP8goitwZOoq1vA/sm0fPS4X3KAF0nyhWQ=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/panjf2000/ants/v2 v2.4.6 h1:drmj9mcygn2gawZ155dRbo+NfXEfAssjZNU1qoIb4gQ=
github.com/panjf2000/ants/v2 v2.4.6/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tidwall/gjson v1.9.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c h1:WtYZ93XtWSO5KlOMgPZu7hXY9WhMZpprvlm5VwvAl8c=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 h1:N46iQqOtHry7Hxzb9PGrP68oovQmj7EhudNoKHvbOvI=
gopkg.in/dgrijalva/jwt-go.v3 v3.2.0/go.mod h1:hdNXC2Z9yC029rvsQ/on2ZNQ44Z2XToVhpXXbR+J05A=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4=
gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY=
gorm.io/gorm v1.22.3 h1:/JS6z+GStEQvJNW3t1FTwJwG/gZ+A7crFdRqtvG5ehA=
gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

34
lib/cert/cert.go Normal file
View File

@ -0,0 +1,34 @@
package cert
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"github.com/pkg/errors"
)
// GetCertSnFromConfig return SerialNumber by tls.Config
func GetCertSnFromConfig(config *tls.Config) (string, error) {
if len(config.Certificates) == 0 || len(config.Certificates[0].Certificate) == 0 {
return "", errors.New("certificates is empty")
}
return GetCertSnFromBlock(config.Certificates[0].Certificate[0])
}
// GetCertSnFromEncode return SerialNumber by encoded cert
func GetCertSnFromEncode(b []byte) (string, error) {
block, _ := pem.Decode(b)
if block == nil {
return "", errors.New("block is not a cert encoded")
}
return GetCertSnFromBlock(block.Bytes)
}
// GetCertSnFromBlock return SerialNumber by decode block
func GetCertSnFromBlock(block []byte) (string, error) {
cert, err := x509.ParseCertificate(block)
if err != nil {
return "", errors.Wrap(err, "ParseCertificate")
}
return cert.SerialNumber.String(), nil
}

42
lib/cert/cert_test.go Normal file
View File

@ -0,0 +1,42 @@
package cert
import (
"crypto/tls"
"crypto/x509/pkix"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"testing"
)
func TestGetCertSerialNumber(t *testing.T) {
g := NewX509Generator(pkix.Name{
Country: []string{"CN"},
Organization: []string{"Ehang.io"},
OrganizationalUnit: []string{"nps"},
Province: []string{"Beijing"},
CommonName: "nps",
Locality: []string{"Beijing"},
})
cert, key, err := g.CreateRootCa()
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "cert.pem"), cert, 0600))
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "key.pem"), key, 0600))
assert.NoError(t, err)
cliCrt, err := tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "cert.pem"), filepath.Join(os.TempDir(), "key.pem"))
assert.NoError(t, err)
config := &tls.Config{
Certificates: []tls.Certificate{cliCrt},
}
sn1, err := GetCertSnFromConfig(config)
assert.NoError(t, err)
assert.NotEmpty(t, sn1)
sn2, err := GetCertSnFromEncode(cert)
assert.NoError(t, err)
assert.NotEmpty(t, sn2)
assert.Equal(t, sn1, sn2)
}

253
lib/cert/client_hello.go Normal file
View File

@ -0,0 +1,253 @@
package cert
import (
"strings"
)
type CurveID uint16
type SignatureScheme uint16
const (
statusTypeOCSP uint8 = 1
extensionServerName uint16 = 0
extensionStatusRequest uint16 = 5
extensionSupportedCurves uint16 = 10
extensionSupportedPoints uint16 = 11
extensionSignatureAlgorithms uint16 = 13
extensionALPN uint16 = 16
extensionSCT uint16 = 18 // https://tools.ietf.org/html/rfc6962#section-6
extensionSessionTicket uint16 = 35
extensionNextProtoNeg uint16 = 13172 // not IANA assigned
extensionRenegotiationInfo uint16 = 0xff01
scsvRenegotiation uint16 = 0x00ff
)
type ClientHelloMsg struct {
raw []byte
vers uint16
random []byte
sessionId []byte
cipherSuites []uint16
compressionMethods []uint8
nextProtoNeg bool
serverName string
ocspStapling bool
scts bool
supportedCurves []CurveID
supportedPoints []uint8
ticketSupported bool
sessionTicket []uint8
supportedSignatureAlgorithms []SignatureScheme
secureRenegotiation []byte
secureRenegotiationSupported bool
alpnProtocols []string
}
func (m *ClientHelloMsg) GetServerName() string {
return m.serverName
}
func (m *ClientHelloMsg) Unmarshal(data []byte) bool {
if len(data) < 42 {
return false
}
m.raw = data
m.vers = uint16(data[4])<<8 | uint16(data[5])
m.random = data[6:38]
sessionIdLen := int(data[38])
if sessionIdLen > 32 || len(data) < 39+sessionIdLen {
return false
}
m.sessionId = data[39 : 39+sessionIdLen]
data = data[39+sessionIdLen:]
if len(data) < 2 {
return false
}
// cipherSuiteLen is the number of bytes of cipher suite numbers. Since
// they are uint16s, the number must be even.
cipherSuiteLen := int(data[0])<<8 | int(data[1])
if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen {
return false
}
numCipherSuites := cipherSuiteLen / 2
m.cipherSuites = make([]uint16, numCipherSuites)
for i := 0; i < numCipherSuites; i++ {
m.cipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i])
if m.cipherSuites[i] == scsvRenegotiation {
m.secureRenegotiationSupported = true
}
}
data = data[2+cipherSuiteLen:]
if len(data) < 1 {
return false
}
compressionMethodsLen := int(data[0])
if len(data) < 1+compressionMethodsLen {
return false
}
m.compressionMethods = data[1 : 1+compressionMethodsLen]
data = data[1+compressionMethodsLen:]
m.nextProtoNeg = false
m.serverName = ""
m.ocspStapling = false
m.ticketSupported = false
m.sessionTicket = nil
m.supportedSignatureAlgorithms = nil
m.alpnProtocols = nil
m.scts = false
if len(data) == 0 {
// ClientHello is optionally followed by extension data
return true
}
if len(data) < 2 {
return false
}
extensionsLength := int(data[0])<<8 | int(data[1])
data = data[2:]
if extensionsLength != len(data) {
return false
}
for len(data) != 0 {
if len(data) < 4 {
return false
}
extension := uint16(data[0])<<8 | uint16(data[1])
length := int(data[2])<<8 | int(data[3])
data = data[4:]
if len(data) < length {
return false
}
switch extension {
case extensionServerName:
d := data[:length]
if len(d) < 2 {
return false
}
namesLen := int(d[0])<<8 | int(d[1])
d = d[2:]
if len(d) != namesLen {
return false
}
for len(d) > 0 {
if len(d) < 3 {
return false
}
nameType := d[0]
nameLen := int(d[1])<<8 | int(d[2])
d = d[3:]
if len(d) < nameLen {
return false
}
if nameType == 0 {
m.serverName = string(d[:nameLen])
// An SNI value may not include a
// trailing dot. See
// https://tools.ietf.org/html/rfc6066#section-3.
if strings.HasSuffix(m.serverName, ".") {
return false
}
break
}
d = d[nameLen:]
}
case extensionNextProtoNeg:
if length > 0 {
return false
}
m.nextProtoNeg = true
case extensionStatusRequest:
m.ocspStapling = length > 0 && data[0] == statusTypeOCSP
case extensionSupportedCurves:
// https://tools.ietf.org/html/rfc4492#section-5.5.1
if length < 2 {
return false
}
l := int(data[0])<<8 | int(data[1])
if l%2 == 1 || length != l+2 {
return false
}
numCurves := l / 2
m.supportedCurves = make([]CurveID, numCurves)
d := data[2:]
for i := 0; i < numCurves; i++ {
m.supportedCurves[i] = CurveID(d[0])<<8 | CurveID(d[1])
d = d[2:]
}
case extensionSupportedPoints:
// https://tools.ietf.org/html/rfc4492#section-5.5.2
if length < 1 {
return false
}
l := int(data[0])
if length != l+1 {
return false
}
m.supportedPoints = make([]uint8, l)
copy(m.supportedPoints, data[1:])
case extensionSessionTicket:
// https://tools.ietf.org/html/rfc5077#section-3.2
m.ticketSupported = true
m.sessionTicket = data[:length]
case extensionSignatureAlgorithms:
// https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1
if length < 2 || length&1 != 0 {
return false
}
l := int(data[0])<<8 | int(data[1])
if l != length-2 {
return false
}
n := l / 2
d := data[2:]
m.supportedSignatureAlgorithms = make([]SignatureScheme, n)
for i := range m.supportedSignatureAlgorithms {
m.supportedSignatureAlgorithms[i] = SignatureScheme(d[0])<<8 | SignatureScheme(d[1])
d = d[2:]
}
case extensionRenegotiationInfo:
if length == 0 {
return false
}
d := data[:length]
l := int(d[0])
d = d[1:]
if l != len(d) {
return false
}
m.secureRenegotiation = d
m.secureRenegotiationSupported = true
case extensionALPN:
if length < 2 {
return false
}
l := int(data[0])<<8 | int(data[1])
if l != length-2 {
return false
}
d := data[2:length]
for len(d) != 0 {
stringLen := int(d[0])
d = d[1:]
if stringLen == 0 || stringLen > len(d) {
return false
}
m.alpnProtocols = append(m.alpnProtocols, string(d[:stringLen]))
d = d[stringLen:]
}
case extensionSCT:
m.scts = true
if length != 0 {
return false
}
}
data = data[length:]
}
return true
}

108
lib/cert/generate.go Normal file
View File

@ -0,0 +1,108 @@
package cert
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"time"
)
var _ Generator = (*X509Generator)(nil)
type Generator interface {
CreateRootCa() ([]byte, []byte, error)
CreateCert(dnsName string) ([]byte, []byte, error)
InitRootCa(rootCa []byte, rootKey []byte) error
}
type X509Generator struct {
rootCert *x509.Certificate
rootRsaPrivate *rsa.PrivateKey
subject pkix.Name
}
func NewX509Generator(subject pkix.Name) *X509Generator {
return &X509Generator{
subject: subject,
}
}
func (cg *X509Generator) InitRootCa(rootCa []byte, rootKey []byte) error {
var err error
caBlock, _ := pem.Decode(rootCa)
cg.rootCert, err = x509.ParseCertificate(caBlock.Bytes)
if err != nil {
return err
}
keyBlock, _ := pem.Decode(rootKey)
cg.rootRsaPrivate, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
if err != nil {
return err
}
return nil
}
func (cg *X509Generator) CreateCert(dnsName string) ([]byte, []byte, error) {
return cg.create(false, dnsName)
}
func (cg *X509Generator) CreateRootCa() ([]byte, []byte, error) {
return cg.create(true, "")
}
func (cg *X509Generator) create(isRootCa bool, dnsName string) ([]byte, []byte, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
template := &x509.Certificate{
SerialNumber: serialNumber,
Subject: cg.subject,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(3, 0, 0),
BasicConstraintsValid: true,
IsCA: false,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment,
DNSNames: []string{dnsName},
}
if isRootCa {
template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
}
priKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
var ca []byte
if !isRootCa {
if cg.rootCert == nil || cg.rootRsaPrivate == nil {
return nil, nil, errors.New("root ca is not exist")
}
ca, err = x509.CreateCertificate(rand.Reader, template, cg.rootCert, &priKey.PublicKey, cg.rootRsaPrivate)
} else {
ca, err = x509.CreateCertificate(rand.Reader, template, template, &priKey.PublicKey, priKey)
}
if err != nil {
return nil, nil, err
}
caPem := &pem.Block{
Type: "CERTIFICATE",
Bytes: ca,
}
ca = pem.EncodeToMemory(caPem)
buf := x509.MarshalPKCS1PrivateKey(priKey)
keyPem := &pem.Block{
Type: "PRIVATE KEY",
Bytes: buf,
}
key := pem.EncodeToMemory(keyPem)
return ca, key, nil
}

62
lib/cert/generate_test.go Normal file
View File

@ -0,0 +1,62 @@
package cert
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"testing"
)
func TestCreateCert(t *testing.T) {
dnsName := "ehang.io"
g := NewX509Generator(pkix.Name{
Country: []string{"CN"},
Organization: []string{"ehang.io"},
OrganizationalUnit: []string{"nps"},
Province: []string{"Beijing"},
CommonName: "nps",
Locality: []string{"Beijing"},
})
// generate root ca
rootCa, rootKey, err := g.CreateRootCa()
if err != nil {
t.Fatal(err)
}
err = g.InitRootCa(rootCa, rootKey)
if err != nil {
t.Fatal(err)
}
// generate npc cert
clientCa, _, err := g.CreateCert(dnsName)
if err != nil {
t.Fatal(err)
}
// verify npc cert by root cert
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(rootCa)
if !ok {
panic("failed to parse root certificate")
}
block, _ := pem.Decode(clientCa)
if block == nil {
t.Fatal("failed to parse certificate PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatal("failed to parse certificate: " + err.Error())
}
opts := x509.VerifyOptions{
Roots: roots,
DNSName: dnsName,
Intermediates: x509.NewCertPool(),
}
if _, err := cert.Verify(opts); err != nil {
t.Fatal("failed to verify certificate: " + err.Error())
}
}

141
lib/common/addr.go Normal file
View File

@ -0,0 +1,141 @@
package common
import (
"errors"
"io"
"net"
"strconv"
)
var AddrError = errors.New("addr error")
// SOCKS address types as defined in RFC 1928 section 5.
const (
AtypIPv4 = 1
AtypDomainName = 3
AtypIPv6 = 4
)
// MaxAddrLen is the maximum size of SOCKS address in bytes.
const MaxAddrLen = 1 + 1 + 255 + 2
// Addr represents a SOCKS address as defined in RFC 1928 section 5.
type Addr []byte
// String serializes SOCKS address a to string form.
func (a Addr) String() string {
var host, port string
switch a[0] { // address type
case AtypDomainName:
host = string(a[2 : 2+int(a[1])])
port = strconv.Itoa((int(a[2+int(a[1])]) << 8) | int(a[2+int(a[1])+1]))
case AtypIPv4:
host = net.IP(a[1 : 1+net.IPv4len]).String()
port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1]))
case AtypIPv6:
host = net.IP(a[1 : 1+net.IPv6len]).String()
port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1]))
}
return net.JoinHostPort(host, port)
}
func readAddr(r io.Reader, b []byte) (Addr, error) {
if len(b) < MaxAddrLen {
return nil, io.ErrShortBuffer
}
_, err := io.ReadFull(r, b[:1]) // read 1st byte for address type
if err != nil {
return nil, err
}
switch b[0] {
case AtypDomainName:
_, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length
if err != nil {
return nil, err
}
_, err = io.ReadFull(r, b[2:2+int(b[1])+2])
return b[:1+1+int(b[1])+2], err
case AtypIPv4:
_, err = io.ReadFull(r, b[1:1+net.IPv4len+2])
return b[:1+net.IPv4len+2], err
case AtypIPv6:
_, err = io.ReadFull(r, b[1:1+net.IPv6len+2])
return b[:1+net.IPv6len+2], err
}
return nil, errors.New("addr type not supported")
}
// ReadAddr reads just enough bytes from r to get a valid Addr.
func ReadAddr(r io.Reader) (Addr, error) {
return readAddr(r, make([]byte, MaxAddrLen))
}
// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed.
func SplitAddr(b []byte) (Addr, error) {
addrLen := 1
if len(b) < addrLen {
return nil, AddrError
}
switch b[0] {
case AtypDomainName:
if len(b) < 2 {
return nil, AddrError
}
addrLen = 1 + 1 + int(b[1]) + 2
case AtypIPv4:
addrLen = 1 + net.IPv4len + 2
case AtypIPv6:
addrLen = 1 + net.IPv6len + 2
default:
return nil, AddrError
}
if len(b) < addrLen {
return nil, AddrError
}
return b[:addrLen], nil
}
// ParseAddr parses the address in string s. Returns nil if failed.
func ParseAddr(s string) (Addr, error) {
var addr Addr
host, port, err := net.SplitHostPort(s)
if err != nil {
return nil, AddrError
}
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
addr = make([]byte, 1+net.IPv4len+2)
addr[0] = AtypIPv4
copy(addr[1:], ip4)
} else {
addr = make([]byte, 1+net.IPv6len+2)
addr[0] = AtypIPv6
copy(addr[1:], ip)
}
} else {
if len(host) > 255 {
return nil, AddrError
}
addr = make([]byte, 1+1+len(host)+2)
addr[0] = AtypDomainName
addr[1] = byte(len(host))
copy(addr[2:], host)
}
portnum, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return nil, AddrError
}
addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum)
return addr, nil
}

Some files were not shown because too many files have changed in this diff Show More