diff --git a/api/common/interfaces.go b/api/common/interfaces.go
index e878ce226..e4e0dd7e1 100644
--- a/api/common/interfaces.go
+++ b/api/common/interfaces.go
@@ -7,9 +7,13 @@ import (
"encoding/json"
"fmt"
"net"
+ "net/url"
"strings"
+ "time"
"github.com/nknorg/nkn/v2/api/common/errcode"
+ "github.com/nknorg/nkn/v2/api/httpjson/client"
+ "github.com/nknorg/nkn/v2/api/webrtc"
"github.com/nknorg/nkn/v2/block"
"github.com/nknorg/nkn/v2/chain"
"github.com/nknorg/nkn/v2/common"
@@ -412,12 +416,13 @@ func getVersion(s Serverer, params map[string]interface{}, ctx context.Context)
return respPacking(errcode.SUCCESS, config.Version)
}
-func NodeInfo(wsAddr, rpcAddr string, pubkey, id []byte) map[string]string {
+func NodeInfo(wsAddr, rpcAddr string, pubkey, id []byte, sdp string) map[string]string {
nodeInfo := make(map[string]string)
nodeInfo["addr"] = wsAddr
nodeInfo["rpcAddr"] = rpcAddr
nodeInfo["pubkey"] = hex.EncodeToString(pubkey)
nodeInfo["id"] = hex.EncodeToString(id)
+ nodeInfo["sdp"] = sdp
return nodeInfo
}
@@ -444,7 +449,7 @@ func getWsAddr(s Serverer, params map[string]interface{}, ctx context.Context) m
return respPacking(errcode.INTERNAL_ERROR, err.Error())
}
- return respPacking(errcode.SUCCESS, NodeInfo(wsAddr, rpcAddr, pubkey, id))
+ return respPacking(errcode.SUCCESS, NodeInfo(wsAddr, rpcAddr, pubkey, id, ""))
}
func getWssAddr(s Serverer, params map[string]interface{}, ctx context.Context) map[string]interface{} {
@@ -467,7 +472,7 @@ func getWssAddr(s Serverer, params map[string]interface{}, ctx context.Context)
return respPacking(errcode.INTERNAL_ERROR, err.Error())
}
- return respPacking(errcode.SUCCESS, NodeInfo(wsAddr, rpcAddr, pubkey, id))
+ return respPacking(errcode.SUCCESS, NodeInfo(wsAddr, rpcAddr, pubkey, id, ""))
}
// getBalanceByAddr gets balance by address
@@ -953,6 +958,59 @@ func findSuccessorAddr(s Serverer, params map[string]interface{}, ctx context.Co
return respPacking(errcode.SUCCESS, addrs[0])
}
+// getPeerAddr get a node address
+// params: {"address":
}
+// return: {"resultOrData":|, "error":}
+func getPeerAddr(s Serverer, params map[string]interface{}, ctx context.Context) map[string]interface{} {
+ if len(params) < 1 {
+ return RespPacking("length of params is less than 1", errcode.INVALID_PARAMS)
+ }
+
+ str, ok := params["address"].(string)
+ if !ok {
+ return RespPacking("address should be a string", errcode.INTERNAL_ERROR)
+ }
+
+ clientID, _, _, err := address.ParseClientAddress(str)
+ if err != nil {
+ return RespPacking(err.Error(), errcode.INTERNAL_ERROR)
+ }
+
+ wsAddr, rpcAddr, pubkey, id, err := s.GetNetNode().FindWsAddr(clientID)
+ if err != nil {
+ return RespPacking(err.Error(), errcode.INTERNAL_ERROR)
+ }
+
+ n := s.GetNetNode()
+ if n == nil {
+ return nil
+ }
+
+ if n.GetWsAddr() == wsAddr {
+ offer := params["offer"].(string)
+ peer := webrtc.NewPeer(config.Parameters.StunList)
+
+ err = peer.Answer(offer)
+ if err != nil {
+ return RespPacking(err.Error(), errcode.INTERNAL_ERROR)
+ }
+ select {
+ case answer := <-peer.OnSdp:
+ return RespPacking(NodeInfo(wsAddr, rpcAddr, pubkey, id, answer), errcode.SUCCESS)
+ case <-time.After(10 * time.Second):
+ return RespPacking(fmt.Errorf("webrtc, wait for sdp time out"), errcode.INTERNAL_ERROR)
+ }
+ }
+
+ reqAddr := (&url.URL{Scheme: "http", Host: rpcAddr}).String()
+ wsAddr, rpcAddr, pubkey, id, sdp, err := client.GetPeerAddr(reqAddr, params)
+ if err != nil {
+ return RespPacking(err.Error(), errcode.INTERNAL_ERROR)
+ }
+
+ return RespPacking(NodeInfo(wsAddr, rpcAddr, pubkey, id, sdp), errcode.SUCCESS)
+}
+
var InitialAPIHandlers = map[string]APIHandler{
"getlatestblockhash": {Handler: getLatestBlockHash, AccessCtrl: BIT_JSONRPC},
"getblock": {Handler: getBlock, AccessCtrl: BIT_JSONRPC},
@@ -983,4 +1041,5 @@ var InitialAPIHandlers = map[string]APIHandler{
"findsuccessoraddr": {Handler: findSuccessorAddr, AccessCtrl: BIT_JSONRPC},
"findsuccessoraddrs": {Handler: findSuccessorAddrs, AccessCtrl: BIT_JSONRPC},
"getregistrant": {Handler: getRegistrant, AccessCtrl: BIT_JSONRPC},
+ "getpeeraddr": {Handler: getPeerAddr, AccessCtrl: BIT_JSONRPC},
}
diff --git a/api/httpjson/RPCserver.go b/api/httpjson/RPCserver.go
index 96fd9d555..217b4cbd6 100644
--- a/api/httpjson/RPCserver.go
+++ b/api/httpjson/RPCserver.go
@@ -130,6 +130,7 @@ func (s *RPCServer) Handle(w http.ResponseWriter, r *http.Request) {
}
method, ok = request["method"].(string)
if !ok {
+ log.Warning("RPC Server - No function to call for ", method)
code = errcode.INVALID_METHOD
}
if request["params"] != nil {
diff --git a/api/httpjson/client/client.go b/api/httpjson/client/client.go
index 650f61a36..562e9b954 100644
--- a/api/httpjson/client/client.go
+++ b/api/httpjson/client/client.go
@@ -222,3 +222,36 @@ func GetNonceByAddr(remote, addr string, txPool bool) (uint64, uint32, error) {
return nonce, ret.Result.CurrentHeight, nil
}
+
+func GetPeerAddr(remote string, params map[string]interface{}) (string, string, []byte, []byte, string, error) {
+ fmt.Println("......GetPeerAddr, remote: ", remote)
+ resp, err := Call(remote, "getpeeraddr", 0, params)
+ if err != nil {
+ return "", "", nil, nil, "", err
+ }
+
+ // log.Infof("Node-to-Node GetPeerAddr got resp: %v from %s\n", string(resp), remote)
+
+ var ret struct {
+ Result struct {
+ Addr string `json:"addr"`
+ RpcAddr string `json:"rpcAddr"`
+ Pubkey []byte `json:"pubkey"`
+ Id []byte `json:"id"`
+ Sdp string `json:"sdp"`
+ } `json:"result"`
+ Err map[string]interface{} `json:"error"`
+ }
+
+ if err := json.Unmarshal(resp, &ret); err != nil {
+ log.Error("Node-to-Node GetPeerAddr json.Unmarshal error: ", err)
+ return "", "", nil, nil, "", err
+ }
+ if len(ret.Err) != 0 { // resp.error NOT empty
+ log.Error("Node-to-Node GetPeerAddr ret.Err: ", ret.Err)
+ return "", "", nil, nil, "", fmt.Errorf("GetPeerAddr(%s) resp error: %v", remote, string(resp))
+ }
+
+ fmt.Printf("......GetPeerAddr got result: %+v\n", ret.Result)
+ return ret.Result.Addr, ret.Result.RpcAddr, ret.Result.Pubkey, ret.Result.Id, ret.Result.Sdp, nil
+}
diff --git a/api/webrtc/webrtc.go b/api/webrtc/webrtc.go
new file mode 100644
index 000000000..ac76d3823
--- /dev/null
+++ b/api/webrtc/webrtc.go
@@ -0,0 +1,395 @@
+package webrtc
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/nknorg/nkn/v2/api/ratelimiter"
+ "github.com/nknorg/nkn/v2/api/websocket/session"
+ "github.com/nknorg/nkn/v2/config"
+ "github.com/nknorg/nkn/v2/util/log"
+ "github.com/pion/webrtc/v4"
+)
+
+// compitable to websocket
+const (
+ UnknownMessage = 0
+ TextMessage = 1
+ BinaryMessage = 2
+ CloseMessage = 8
+ PingMessage = 9
+ PongMessage = 10
+
+ PingData = "ping"
+ PongData = "pong"
+)
+
+var NewConnection func(conn session.Conn, r *http.Request)
+
+type DataChannelMessage struct {
+ messageType int
+ data []byte
+}
+
+type Peer struct {
+ pc *webrtc.PeerConnection
+ dc *webrtc.DataChannel
+ offer string
+ answer string
+ OnSdp chan string
+ OnMessage chan DataChannelMessage
+
+ mutex sync.RWMutex
+ isConnected bool
+ readDeadline time.Time
+ writeDeadline time.Time
+ readLimit int64
+ pongHandler func(string) error
+}
+
+func NewPeer(urls []string) *Peer {
+ p := &Peer{
+ OnSdp: make(chan string, 1),
+ isConnected: false,
+ OnMessage: make(chan DataChannelMessage, 128),
+ }
+
+ config := webrtc.Configuration{
+ ICEServers: []webrtc.ICEServer{
+ {
+ URLs: urls,
+ },
+ },
+ }
+ var err error
+ pc, err := webrtc.NewPeerConnection(config)
+ if err != nil {
+ log.Error("NewPeerConnection error: ", err)
+ return nil
+ }
+
+ p.pc = pc
+ return p
+}
+
+func (c *Peer) Offer(label string) error {
+ if c.pc == nil {
+ return fmt.Errorf("PeerConnection not available")
+ }
+
+ c.pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
+ if candidate == nil {
+ localDesc := c.pc.LocalDescription()
+ encodedDescr, err := Encode(localDesc)
+ if err != nil {
+ log.Error("WebRTC OnICECandidate error: ", err)
+ return
+ }
+ c.offer = encodedDescr
+ c.OnSdp <- encodedDescr
+ }
+ })
+
+ dc, err := c.pc.CreateDataChannel(label, nil)
+ if err != nil {
+ return err
+ }
+ dc.OnOpen(func() {
+ log.Debugf("data channel %v has been opened\n", dc.Label())
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+ c.isConnected = true
+ })
+ dc.OnMessage(func(msg webrtc.DataChannelMessage) {
+ var dcmsg DataChannelMessage
+ if msg.IsString {
+ dcmsg.messageType = TextMessage
+ if string(msg.Data) == PongData {
+ if c.pongHandler != nil {
+ c.pongHandler(PongData)
+ } else {
+ log.Info("Pong handler not set")
+ }
+ return
+ } else if string(msg.Data) == PingData {
+ dc.SendText(PongData)
+ return
+ }
+ } else {
+ dcmsg.messageType = BinaryMessage
+ }
+ dcmsg.data = msg.Data
+ c.OnMessage <- dcmsg
+ })
+ dc.OnClose(func() {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+ c.isConnected = false
+ })
+
+ dc.OnError(func(err error) {
+ log.Errorf("Data Channel %s error: %s\n", dc.Label(), err.Error())
+ })
+
+ offer, err := c.pc.CreateOffer(&webrtc.OfferOptions{ICERestart: false})
+ if err != nil {
+ return nil
+ }
+ if err = c.pc.SetLocalDescription(offer); err != nil {
+ return nil
+ }
+
+ c.dc = dc
+ return nil
+}
+
+func (c *Peer) Answer(offerSdp string) error {
+ offer := webrtc.SessionDescription{}
+ err := Decode(offerSdp, &offer)
+ if err != nil {
+ return err
+ }
+
+ sdp, err := offer.Unmarshal()
+ if err != nil {
+ return err
+ }
+ limiter := ratelimiter.GetLimiter("webrtc:"+sdp.Origin.UnicastAddress, config.Parameters.WsIPRateLimit, int(config.Parameters.WsIPRateBurst))
+ if !limiter.Allow() {
+ return fmt.Errorf("webrtc connection limit of %s reached", sdp.Origin.UnicastAddress)
+ }
+
+ if err := c.pc.SetRemoteDescription(offer); err != nil {
+ return err
+ }
+
+ answer, err := c.pc.CreateAnswer(nil)
+ if err != nil {
+ return err
+ }
+
+ if err := c.pc.SetLocalDescription(answer); err != nil {
+ return err
+ }
+
+ c.pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
+ if candidate == nil {
+ localDesc := c.pc.LocalDescription()
+ encodedDescr, err := Encode(localDesc)
+ if err != nil {
+ log.Error("WebRTC OnICECandidate error: ", err)
+ return
+ }
+ c.answer = encodedDescr
+ c.OnSdp <- encodedDescr
+ }
+ })
+
+ c.pc.OnDataChannel(func(dc *webrtc.DataChannel) {
+ dc.OnOpen(func() {
+ c.dc = dc
+
+ c.mutex.Lock()
+ c.isConnected = true
+ c.mutex.Unlock()
+
+ if NewConnection != nil {
+ go NewConnection(c, nil)
+ }
+ })
+
+ dc.OnMessage(func(msg webrtc.DataChannelMessage) {
+ var dcmsg DataChannelMessage
+ if msg.IsString {
+ dcmsg.messageType = TextMessage
+ if string(msg.Data) == PingData {
+ dc.SendText(PongData)
+ return
+ } else if string(msg.Data) == PongData {
+ if c.pongHandler != nil {
+ c.pongHandler(PongData)
+ } else {
+ log.Info("Pong handler not set")
+ }
+ return
+ }
+ } else {
+ dcmsg.messageType = BinaryMessage
+ }
+ dcmsg.data = msg.Data
+ c.OnMessage <- dcmsg
+ })
+
+ dc.OnClose(func() {
+ c.mutex.Lock()
+ c.isConnected = false
+ c.mutex.Unlock()
+ })
+
+ dc.OnError(func(err error) {
+ log.Errorf("Data Channel %v error: %s\n", dc.Label(), err.Error())
+ })
+ })
+
+ return nil
+}
+
+func (c *Peer) IsConnected() bool {
+ c.mutex.RLock()
+ defer c.mutex.RUnlock()
+ return c.isConnected
+}
+
+func (c *Peer) SetRemoteSdp(sdp string) error {
+ answer := webrtc.SessionDescription{}
+ err := Decode(sdp, &answer)
+ if err != nil {
+ return err
+ }
+ return c.pc.SetRemoteDescription(answer)
+}
+
+func (c *Peer) WriteMessage(messageType int, data []byte) (err error) {
+ if c.dc == nil {
+ return fmt.Errorf("DataChannel not available")
+ }
+
+ if messageType == PingMessage {
+ return c.dc.SendText(PingData)
+ }
+ if messageType == PongMessage {
+ return c.dc.SendText(PongData)
+ }
+ if data == nil {
+ return fmt.Errorf("data to send is nil")
+ }
+
+ if c.writeDeadline.IsZero() {
+ if messageType == TextMessage {
+ err = c.dc.SendText(string(data))
+ } else {
+ err = c.dc.Send(data)
+ }
+ return err
+ }
+
+ var dur time.Duration
+ now := time.Now()
+ switch {
+ case c.writeDeadline.Before(now), c.writeDeadline == now:
+ return os.ErrDeadlineExceeded
+
+ case c.writeDeadline.After(now):
+ writeResult := make(chan error)
+ go func() {
+ if messageType == TextMessage {
+ err = c.dc.SendText(string(data))
+ } else {
+ err = c.dc.Send(data)
+ }
+
+ writeResult <- err
+ }()
+
+ dur = time.Until(c.writeDeadline)
+ select {
+ case err = <-writeResult:
+ return err
+ case <-time.After(dur):
+ return os.ErrDeadlineExceeded
+ }
+ }
+
+ return fmt.Errorf("unknown error")
+}
+
+// WriteJSON writes the JSON encoding of v as a message.
+func (c *Peer) WriteJSON(v interface{}) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+ return c.WriteMessage(TextMessage, b)
+}
+
+func (c *Peer) ReadMessage() (messageType int, data []byte, err error) {
+ if c.readDeadline.IsZero() {
+ msg := <-c.OnMessage
+ return msg.messageType, msg.data, nil
+ }
+
+ now := time.Now()
+ switch {
+ case c.readDeadline.After(now):
+ for {
+ oldReadDeadline := c.readDeadline
+ dur := time.Until(c.readDeadline)
+ select {
+ case msg := <-c.OnMessage:
+ return msg.messageType, msg.data, nil
+ case <-time.After(dur):
+ if c.readDeadline.After(oldReadDeadline) {
+ continue
+ }
+ return UnknownMessage, nil, os.ErrDeadlineExceeded
+ }
+ }
+
+ case c.readDeadline.Before(now), c.readDeadline == now:
+ return UnknownMessage, nil, os.ErrDeadlineExceeded
+ }
+
+ return UnknownMessage, nil, fmt.Errorf("unknown error")
+}
+
+func (c *Peer) SetWriteDeadline(t time.Time) error {
+ c.writeDeadline = t
+ return nil
+}
+
+func (c *Peer) SetReadDeadline(t time.Time) error {
+ c.readDeadline = t
+ return nil
+}
+
+func (c *Peer) SetReadLimit(l int64) {
+ c.readLimit = l
+}
+
+func (c *Peer) Close() error {
+ if c.dc != nil {
+ if err := c.dc.Close(); err != nil {
+ return err
+ }
+ }
+ return c.pc.Close()
+}
+
+func (c *Peer) SetPongHandler(f func(string) error) {
+ c.pongHandler = f
+}
+
+// Encode the input in base64
+func Encode(obj interface{}) (string, error) {
+ b, err := json.Marshal(obj)
+ if err != nil {
+ return "", err
+ }
+
+ return base64.StdEncoding.EncodeToString(b), nil
+}
+
+// Decode the input from base64
+func Decode(in string, obj interface{}) error {
+ b, err := base64.StdEncoding.DecodeString(in)
+ if err != nil {
+ return err
+ }
+
+ return json.Unmarshal(b, obj)
+}
diff --git a/api/websocket/server/relay.go b/api/websocket/server/relay.go
index 1900de4ea..d3bb83b6c 100644
--- a/api/websocket/server/relay.go
+++ b/api/websocket/server/relay.go
@@ -18,7 +18,7 @@ type sigChainInfo struct {
sigChainLen int
}
-func (ws *WsServer) sendOutboundRelayMessage(srcAddrStrPtr *string, msg *pb.OutboundMessage) {
+func (ms *MsgServer) sendOutboundRelayMessage(srcAddrStrPtr *string, msg *pb.OutboundMessage) {
if srcAddrStrPtr == nil {
log.Warningf("src addr is nil")
return
@@ -56,15 +56,15 @@ func (ws *WsServer) sendOutboundRelayMessage(srcAddrStrPtr *string, msg *pb.Outb
} else {
payload = payloads[0]
}
- err := ws.localNode.SendRelayMessage(*srcAddrStrPtr, dest, payload, msg.Signatures[i], msg.BlockHash, msg.Nonce, msg.MaxHoldingSeconds)
+ err := ms.localNode.SendRelayMessage(*srcAddrStrPtr, dest, payload, msg.Signatures[i], msg.BlockHash, msg.Nonce, msg.MaxHoldingSeconds)
if err != nil {
log.Error("Send relay message error:", err)
}
}
}
-func (ws *WsServer) sendInboundMessage(clientID string, inboundMsg *pb.InboundMessage) bool {
- clients := ws.SessionList.GetSessionsById(clientID)
+func (ms *MsgServer) sendInboundMessage(clientID string, inboundMsg *pb.InboundMessage) bool {
+ clients := ms.SessionList.GetSessionsById(clientID)
if clients == nil {
log.Debugf("Client Not Online: %s", clientID)
return false
@@ -105,7 +105,7 @@ func (ws *WsServer) sendInboundMessage(clientID string, inboundMsg *pb.InboundMe
return success
}
-func (ws *WsServer) sendInboundRelayMessage(relayMessage *pb.Relay, shouldSign bool) {
+func (ms *MsgServer) sendInboundRelayMessage(relayMessage *pb.Relay, shouldSign bool) {
clientID := relayMessage.DestId
msg := &pb.InboundMessage{
Src: address.AssembleClientAddress(relayMessage.SrcIdentifier, relayMessage.SrcPubkey),
@@ -119,34 +119,34 @@ func (ws *WsServer) sendInboundRelayMessage(relayMessage *pb.Relay, shouldSign b
}
}
- success := ws.sendInboundMessage(hex.EncodeToString(clientID), msg)
+ success := ms.sendInboundMessage(hex.EncodeToString(clientID), msg)
if success {
if shouldSign {
- ws.sigChainCache.Add(relayMessage.LastHash, &sigChainInfo{
+ ms.sigChainCache.Add(relayMessage.LastHash, &sigChainInfo{
blockHash: relayMessage.BlockHash,
sigChainLen: int(relayMessage.SigChainLen),
})
}
if time.Duration(relayMessage.MaxHoldingSeconds) > pongTimeout/time.Second {
- ok := ws.messageDeliveredCache.Push(relayMessage)
+ ok := ms.messageDeliveredCache.Push(relayMessage)
if !ok {
log.Warningf("MessageDeliveredCache full, discarding messages.")
}
}
} else if relayMessage.MaxHoldingSeconds > 0 {
- ws.messageBuffer.AddMessage(clientID, relayMessage)
+ ms.messageBuffer.AddMessage(clientID, relayMessage)
}
}
-func (ws *WsServer) startCheckingLostMessages() {
+func (ms *MsgServer) startCheckingLostMessages() {
for {
- v, ok := ws.messageDeliveredCache.Pop()
+ v, ok := ms.messageDeliveredCache.Pop()
if !ok {
break
}
if relayMessage, ok := v.(*pb.Relay); ok {
clientID := relayMessage.DestId
- clients := ws.SessionList.GetSessionsById(hex.EncodeToString(clientID))
+ clients := ms.SessionList.GetSessionsById(hex.EncodeToString(clientID))
if len(clients) > 0 {
threshold := time.Now().Add(-pongTimeout)
success := false
@@ -157,17 +157,17 @@ func (ws *WsServer) startCheckingLostMessages() {
}
}
if !success {
- ws.sendInboundRelayMessage(relayMessage, false)
+ ms.sendInboundRelayMessage(relayMessage, false)
}
continue
}
- ws.messageBuffer.AddMessage(clientID, relayMessage)
+ ms.messageBuffer.AddMessage(clientID, relayMessage)
}
}
}
-func (ws *WsServer) handleReceipt(receipt *pb.Receipt) error {
- v, ok := ws.sigChainCache.Get(receipt.PrevHash)
+func (ms *MsgServer) handleReceipt(receipt *pb.Receipt) error {
+ v, ok := ms.sigChainCache.Get(receipt.PrevHash)
if !ok {
return fmt.Errorf("sigchain info with last hash %x not found in cache", receipt.PrevHash)
}
diff --git a/api/websocket/server/server.go b/api/websocket/server/server.go
index a0d79737f..2e56998af 100644
--- a/api/websocket/server/server.go
+++ b/api/websocket/server/server.go
@@ -20,7 +20,7 @@ import (
"github.com/golang/protobuf/proto"
api "github.com/nknorg/nkn/v2/api/common"
"github.com/nknorg/nkn/v2/api/common/errcode"
- "github.com/nknorg/nkn/v2/api/ratelimiter"
+ "github.com/nknorg/nkn/v2/api/webrtc"
"github.com/nknorg/nkn/v2/api/websocket/messagebuffer"
"github.com/nknorg/nkn/v2/api/websocket/session"
"github.com/nknorg/nkn/v2/chain"
@@ -53,13 +53,8 @@ type Handler struct {
pushFlag bool
}
-type WsServer struct {
+type MsgServer struct {
sync.RWMutex
- Upgrader websocket.Upgrader
- listener net.Listener
- tlsListener net.Listener
- server *http.Server
- tlsServer *http.Server
SessionList *session.SessionList
ActionMap map[string]Handler
TxHashMap map[string]string //key: txHash value:sessionid
@@ -68,11 +63,11 @@ type WsServer struct {
messageBuffer *messagebuffer.MessageBuffer
messageDeliveredCache *DelayedChan
sigChainCache common.Cache
+ ws *wsServer
}
-func InitWsServer(localNode node.ILocalNode, wallet *vault.Wallet) *WsServer {
- ws := &WsServer{
- Upgrader: websocket.Upgrader{},
+func InitMsgServer(localNode node.ILocalNode, wallet *vault.Wallet) *MsgServer {
+ ws := &MsgServer{
SessionList: session.NewSessionList(),
TxHashMap: make(map[string]string),
localNode: localNode,
@@ -80,68 +75,31 @@ func InitWsServer(localNode node.ILocalNode, wallet *vault.Wallet) *WsServer {
messageBuffer: messagebuffer.NewMessageBuffer(true),
messageDeliveredCache: NewDelayedChan(messageDeliveredCacheSize, pongTimeout),
sigChainCache: common.NewGoCache(sigChainCacheExpiration, sigChainCacheCleanupInterval),
+ ws: &wsServer{},
}
return ws
}
-func (ws *WsServer) Start(wssCertReady chan struct{}) error {
- if config.Parameters.HttpWsPort == 0 {
- log.Error("Not configure HttpWsPort port ")
- return nil
- }
- ws.registryMethod()
- ws.Upgrader.CheckOrigin = func(r *http.Request) bool {
- return true
- }
+func (ms *MsgServer) Start(wssCertReady chan struct{}) error {
- var err error
+ ms.ws.Start(ms, wssCertReady)
+ ms.registryMethod()
- ws.listener, err = net.Listen("tcp", ":"+strconv.Itoa(int(config.Parameters.HttpWsPort)))
- if err != nil {
- log.Error("net.Listen: ", err.Error())
- return err
- }
-
- event.Queue.Subscribe(event.SendInboundMessageToClient, ws.sendInboundRelayMessageToClient)
+ go ms.startCheckingLostMessages()
+ go ms.startCheckingWrongClients()
- ws.server = &http.Server{Handler: http.HandlerFunc(ws.websocketHandler)}
- go ws.server.Serve(ws.listener)
+ event.Queue.Subscribe(event.SendInboundMessageToClient, ms.sendInboundRelayMessageToClient)
- go func(wssCertReady chan struct{}) {
- if wssCertReady == nil {
- return
- }
- for {
- select {
- case <-wssCertReady:
- log.Info("wss cert received")
- ws.tlsListener, err = ws.initTlsListen()
- if err != nil {
- log.Error("Https Cert: ", err.Error())
- }
- err = ws.server.Serve(ws.tlsListener)
- if err != nil {
- log.Error(err)
- }
- return
- case <-time.After(300 * time.Second):
- log.Info("wss server is unavailable yet")
- }
- }
- }(wssCertReady)
-
- go ws.startCheckingLostMessages()
-
- go ws.startCheckingWrongClients()
+ webrtc.NewConnection = ms.newConnection
return nil
}
-func (ws *WsServer) registryMethod() {
+func (ms *MsgServer) registryMethod() {
gettxhashmap := func(s api.Serverer, cmd map[string]interface{}, ctx context.Context) map[string]interface{} {
- ws.Lock()
- defer ws.Unlock()
- resp := api.RespPacking(len(ws.TxHashMap), errcode.SUCCESS)
+ ms.Lock()
+ defer ms.Unlock()
+ resp := api.RespPacking(len(ms.TxHashMap), errcode.SUCCESS)
return resp
}
@@ -151,7 +109,7 @@ func (ws *WsServer) registryMethod() {
}
getsessioncount := func(s api.Serverer, cmd map[string]interface{}, ctx context.Context) map[string]interface{} {
- return api.RespPacking(ws.SessionList.GetSessionCount(), errcode.SUCCESS)
+ return api.RespPacking(ms.SessionList.GetSessionCount(), errcode.SUCCESS)
}
setClient := func(s api.Serverer, cmd map[string]interface{}, ctx context.Context) map[string]interface{} {
@@ -191,7 +149,7 @@ func (ws *WsServer) registryMethod() {
}
if wsAddr != localAddr {
- return api.RespPacking(api.NodeInfo(wsAddr, rpcAddr, pubkey, id), errcode.WRONG_NODE)
+ return api.RespPacking(api.NodeInfo(wsAddr, rpcAddr, pubkey, id, ""), errcode.WRONG_NODE)
}
// client auth
@@ -227,7 +185,7 @@ func (ws *WsServer) registryMethod() {
go func() {
log.Warning("Client signature is not right, close its conneciton now")
time.Sleep(3 * time.Second) // sleep several second, let response reach client
- ws.SessionList.CloseSession(sess) // close this session
+ ms.SessionList.CloseSession(sess) // close this session
}()
return api.RespPacking(nil, errcode.INVALID_SIGNATURE)
}
@@ -236,7 +194,7 @@ func (ws *WsServer) registryMethod() {
}
newSessionID := hex.EncodeToString(clientID)
- session, err := ws.SessionList.ChangeSessionToClient(cmd["Userid"].(string), newSessionID)
+ session, err := ms.SessionList.ChangeSessionToClient(cmd["Userid"].(string), newSessionID)
if err != nil {
log.Error("Change session id error: ", err)
return api.RespPacking(nil, errcode.INTERNAL_ERROR)
@@ -244,9 +202,9 @@ func (ws *WsServer) registryMethod() {
session.SetClient(clientID, pubKey, &addrStr, isTlsClient)
go func() {
- messages := ws.messageBuffer.PopMessages(clientID)
+ messages := ms.messageBuffer.PopMessages(clientID)
for _, message := range messages {
- ws.sendInboundRelayMessage(message, true)
+ ms.sendInboundRelayMessage(message, true)
}
}()
@@ -260,7 +218,7 @@ func (ws *WsServer) registryMethod() {
}
res := make(map[string]interface{})
- res["node"] = api.NodeInfo(wsAddr, rpcAddr, pubkey, id)
+ res["node"] = api.NodeInfo(wsAddr, rpcAddr, pubkey, id, "")
res["sigChainBlockHash"] = hex.EncodeToString(sigChainBlockHash.ToArray())
return api.RespPacking(res, errcode.SUCCESS)
@@ -279,101 +237,10 @@ func (ws *WsServer) registryMethod() {
}
}
- ws.ActionMap = actionMap
-}
-
-func (ws *WsServer) Stop() {
- if ws.server != nil {
- ws.server.Shutdown(context.Background())
- log.Error("Close websocket ")
- }
-}
-
-// websocketHandler
-func (ws *WsServer) websocketHandler(w http.ResponseWriter, r *http.Request) {
- host, _, err := net.SplitHostPort(r.RemoteAddr)
- if err == nil {
- limiter := ratelimiter.GetLimiter("ws:"+host, config.Parameters.WsIPRateLimit, int(config.Parameters.WsIPRateBurst))
- if !limiter.Allow() {
- log.Infof("Ws connection limit of %s reached", host)
- w.WriteHeader(http.StatusTooManyRequests)
- return
- }
- }
-
- wsConn, err := ws.Upgrader.Upgrade(w, r, nil)
- if err != nil {
- log.Error("websocket Upgrader: ", err)
- return
- }
- defer wsConn.Close()
-
- sess, err := ws.SessionList.NewSession(wsConn)
- if err != nil {
- log.Error("websocket NewSession:", err)
- return
- }
-
- defer func() {
- ws.deleteTxHashs(sess.GetSessionId())
- ws.SessionList.CloseSession(sess)
- if err := recover(); err != nil {
- log.Error("websocket recover:", err)
- }
- }()
-
- wsConn.SetReadLimit(maxMessageSize)
- wsConn.SetReadDeadline(time.Now().Add(pongTimeout))
- wsConn.SetPongHandler(func(string) error {
- wsConn.SetReadDeadline(time.Now().Add(pongTimeout))
- sess.UpdateLastReadTime()
- return nil
- })
-
- // client auth
- err = ws.sendClientAuthChallenge(sess)
- if err != nil {
- log.Error("send client auth challenge: ", err)
- return
- }
-
- done := make(chan struct{})
- defer close(done)
- go func() {
- ticker := time.NewTicker(pingInterval)
- defer ticker.Stop()
- var err error
- for {
- select {
- case <-ticker.C:
- err = sess.Ping()
- if err != nil {
- return
- }
- case <-done:
- return
- }
- }
- }()
-
- for {
- messageType, bysMsg, err := wsConn.ReadMessage()
- if err != nil {
- log.Debugf("websocket read message error: %v", err)
- break
- }
-
- wsConn.SetReadDeadline(time.Now().Add(pongTimeout))
- sess.UpdateLastReadTime()
-
- err = ws.OnDataHandle(sess, messageType, bysMsg, r)
- if err != nil {
- log.Error(err)
- }
- }
+ ms.ActionMap = actionMap
}
-func (ws *WsServer) IsValidMsg(reqMsg map[string]interface{}) bool {
+func (ms *MsgServer) IsValidMsg(reqMsg map[string]interface{}) bool {
if _, ok := reqMsg["Hash"].(string); !ok && reqMsg["Hash"] != nil {
return false
}
@@ -386,7 +253,7 @@ func (ws *WsServer) IsValidMsg(reqMsg map[string]interface{}) bool {
return true
}
-func (ws *WsServer) OnDataHandle(curSession *session.Session, messageType int, bysMsg []byte, r *http.Request) error {
+func (ms *MsgServer) OnDataHandle(curSession *session.Session, messageType int, bysMsg []byte, httpr *http.Request) error {
if messageType == websocket.BinaryMessage {
msg := &pb.ClientMessage{}
err := proto.Unmarshal(bysMsg, msg)
@@ -422,14 +289,14 @@ func (ws *WsServer) OnDataHandle(curSession *session.Session, messageType int, b
if err != nil {
return fmt.Errorf("Unmarshal outbound message error: %v", err)
}
- ws.sendOutboundRelayMessage(curSession.GetAddrStr(), outboundMsg)
+ ms.sendOutboundRelayMessage(curSession.GetAddrStr(), outboundMsg)
case pb.ClientMessageType_RECEIPT:
receipt := &pb.Receipt{}
err = proto.Unmarshal(b, receipt)
if err != nil {
return fmt.Errorf("Unmarshal receipt error: %v", err)
}
- err = ws.handleReceipt(receipt)
+ err = ms.handleReceipt(receipt)
if err != nil {
return fmt.Errorf("Handle receipt error: %v", err)
}
@@ -444,24 +311,24 @@ func (ws *WsServer) OnDataHandle(curSession *session.Session, messageType int, b
if err := json.Unmarshal(bysMsg, &req); err != nil {
resp := api.ResponsePack(errcode.ILLEGAL_DATAFORMAT)
- ws.respondToSession(curSession, resp)
+ ms.respondToSession(curSession, resp)
return fmt.Errorf("websocket OnDataHandle: %v", err)
}
actionName, ok := req["Action"].(string)
if !ok {
resp := api.ResponsePack(errcode.INVALID_METHOD)
- ws.respondToSession(curSession, resp)
+ ms.respondToSession(curSession, resp)
return nil
}
- action, ok := ws.ActionMap[actionName]
+ action, ok := ms.ActionMap[actionName]
if !ok {
resp := api.ResponsePack(errcode.INVALID_METHOD)
- ws.respondToSession(curSession, resp)
+ ms.respondToSession(curSession, resp)
return nil
}
- if !ws.IsValidMsg(req) {
+ if !ms.IsValidMsg(req) {
resp := api.ResponsePack(errcode.INVALID_PARAMS)
- ws.respondToSession(curSession, resp)
+ ms.respondToSession(curSession, resp)
return nil
}
if height, ok := req["Height"].(float64); ok {
@@ -471,39 +338,45 @@ func (ws *WsServer) OnDataHandle(curSession *session.Session, messageType int, b
req["Raw"] = strconv.FormatInt(int64(raw), 10)
}
req["Userid"] = curSession.GetSessionId()
- req["IsTls"] = r.TLS != nil
+ ctx := context.Background()
+ if httpr != nil {
+ req["IsTls"] = httpr.TLS != nil
+ ctx = httpr.Context()
+ } else {
+ req["IsTls"] = false
+ }
req["session"] = curSession
- ret := action.handler(ws, req, r.Context())
+ ret := action.handler(ms, req, ctx)
resp := api.ResponsePack(ret["error"].(errcode.ErrCode))
resp["Action"] = actionName
resp["Result"] = ret["resultOrData"]
if txHash, ok := resp["Result"].(string); ok && action.pushFlag {
- ws.Lock()
- defer ws.Unlock()
- ws.TxHashMap[txHash] = curSession.GetSessionId()
+ ms.Lock()
+ defer ms.Unlock()
+ ms.TxHashMap[txHash] = curSession.GetSessionId()
}
- ws.respondToSession(curSession, resp)
+ ms.respondToSession(curSession, resp)
return nil
}
-func (ws *WsServer) SetTxHashMap(txhash string, sessionid string) {
- ws.Lock()
- defer ws.Unlock()
- ws.TxHashMap[txhash] = sessionid
+func (ms *MsgServer) SetTxHashMap(txhash string, sessionid string) {
+ ms.Lock()
+ defer ms.Unlock()
+ ms.TxHashMap[txhash] = sessionid
}
-func (ws *WsServer) deleteTxHashs(sSessionId string) {
- ws.Lock()
- defer ws.Unlock()
- for k, v := range ws.TxHashMap {
+func (ms *MsgServer) deleteTxHashs(sSessionId string) {
+ ms.Lock()
+ defer ms.Unlock()
+ for k, v := range ms.TxHashMap {
if v == sSessionId {
- delete(ws.TxHashMap, k)
+ delete(ms.TxHashMap, k)
}
}
}
-func (ws *WsServer) respondToSession(session *session.Session, resp map[string]interface{}) error {
+func (ms *MsgServer) respondToSession(session *session.Session, resp map[string]interface{}) error {
resp["Desc"] = errcode.ErrMessage[resp["Error"].(errcode.ErrCode)]
data, err := json.Marshal(resp)
if err != nil {
@@ -514,46 +387,46 @@ func (ws *WsServer) respondToSession(session *session.Session, resp map[string]i
return err
}
-func (ws *WsServer) respondToId(sSessionId string, resp map[string]interface{}) {
- sessions := ws.SessionList.GetSessionsById(sSessionId)
+func (ms *MsgServer) respondToId(sSessionId string, resp map[string]interface{}) {
+ sessions := ms.SessionList.GetSessionsById(sSessionId)
if sessions == nil {
log.Error("websocket sessionId Not Exist: " + sSessionId)
return
}
for _, session := range sessions {
- ws.respondToSession(session, resp)
+ ms.respondToSession(session, resp)
}
}
-func (ws *WsServer) PushTxResult(txHashStr string, resp map[string]interface{}) {
- ws.Lock()
- defer ws.Unlock()
- sSessionId := ws.TxHashMap[txHashStr]
- delete(ws.TxHashMap, txHashStr)
+func (ms *MsgServer) PushTxResult(txHashStr string, resp map[string]interface{}) {
+ ms.Lock()
+ defer ms.Unlock()
+ sSessionId := ms.TxHashMap[txHashStr]
+ delete(ms.TxHashMap, txHashStr)
if len(sSessionId) > 0 {
- ws.respondToId(sSessionId, resp)
+ ms.respondToId(sSessionId, resp)
}
- ws.PushResult(resp)
+ ms.PushResult(resp)
}
-func (ws *WsServer) PushResult(resp map[string]interface{}) {
+func (ms *MsgServer) PushResult(resp map[string]interface{}) {
resp["Desc"] = errcode.ErrMessage[resp["Error"].(errcode.ErrCode)]
data, err := json.Marshal(resp)
if err != nil {
log.Error("Websocket PushResult:", err)
return
}
- ws.Broadcast(data)
+ ms.Broadcast(data)
}
-func (ws *WsServer) Broadcast(data []byte) error {
- ws.SessionList.ForEachSession(func(s *session.Session) {
+func (ms *MsgServer) Broadcast(data []byte) error {
+ ms.SessionList.ForEachSession(func(s *session.Session) {
s.SendText(data)
})
return nil
}
-func (ws *WsServer) initTlsListen() (net.Listener, error) {
+func (ms *MsgServer) initTlsListen() (net.Listener, error) {
tlsConfig := &tls.Config{
GetCertificate: api.GetWssCertificate,
}
@@ -566,23 +439,23 @@ func (ws *WsServer) initTlsListen() (net.Listener, error) {
return listener, nil
}
-func (ws *WsServer) GetClientsById(cliendID []byte) []*session.Session {
- sessions := ws.SessionList.GetSessionsById(hex.EncodeToString(cliendID))
+func (ms *MsgServer) GetClientsById(cliendID []byte) []*session.Session {
+ sessions := ms.SessionList.GetSessionsById(hex.EncodeToString(cliendID))
return sessions
}
-func (ws *WsServer) GetNetNode() node.ILocalNode {
- return ws.localNode
+func (ms *MsgServer) GetNetNode() node.ILocalNode {
+ return ms.localNode
}
-func (ws *WsServer) NotifyWrongClients() {
- ws.SessionList.ForEachClient(func(client *session.Session) {
+func (ms *MsgServer) NotifyWrongClients() {
+ ms.SessionList.ForEachClient(func(client *session.Session) {
clientID := client.GetID()
if clientID == nil {
return
}
- localNode := ws.GetNetNode()
+ localNode := ms.GetNetNode()
var wsAddr, rpcAddr, localAddr string
var pubkey, id []byte
@@ -602,29 +475,29 @@ func (ws *WsServer) NotifyWrongClients() {
if wsAddr != localAddr {
resp := api.ResponsePack(errcode.WRONG_NODE)
- resp["Result"] = api.NodeInfo(wsAddr, rpcAddr, pubkey, id)
- ws.respondToSession(client, resp)
+ resp["Result"] = api.NodeInfo(wsAddr, rpcAddr, pubkey, id, "")
+ ms.respondToSession(client, resp)
}
})
}
-func (ws *WsServer) startCheckingWrongClients() {
+func (ms *MsgServer) startCheckingWrongClients() {
for {
time.Sleep(checkWrongClientsInterval)
- ws.NotifyWrongClients()
+ ms.NotifyWrongClients()
}
}
-func (ws *WsServer) sendInboundRelayMessageToClient(v interface{}) {
+func (ms *MsgServer) sendInboundRelayMessageToClient(v interface{}) {
if msg, ok := v.(*pb.Relay); ok {
- ws.sendInboundRelayMessage(msg, true)
+ ms.sendInboundRelayMessage(msg, true)
} else {
log.Error("Decode relay message failed")
}
}
// client auth, generate challenge
-func (ws *WsServer) sendClientAuthChallenge(sess *session.Session) error {
+func (ms *MsgServer) sendClientAuthChallenge(sess *session.Session) error {
resp := api.ResponsePack(errcode.SUCCESS)
resp["Action"] = "authChallenge"
@@ -633,6 +506,71 @@ func (ws *WsServer) sendClientAuthChallenge(sess *session.Session) error {
resp["Challenge"] = hex.EncodeToString(challenge)
sess.Challenge = challenge // save this challenge for verifying later.
- err := ws.respondToSession(sess, resp)
+ err := ms.respondToSession(sess, resp)
return err
}
+
+func (ms *MsgServer) newConnection(conn session.Conn, r *http.Request) {
+ sess, err := ms.SessionList.NewSession(conn)
+ if err != nil {
+ log.Error("websocket NewSession:", err)
+ return
+ }
+
+ defer func() {
+ ms.deleteTxHashs(sess.GetSessionId())
+ ms.SessionList.CloseSession(sess)
+ if err := recover(); err != nil {
+ log.Error("websocket recover:", err)
+ }
+ }()
+
+ conn.SetReadLimit(maxMessageSize)
+ conn.SetReadDeadline(time.Now().Add(pongTimeout))
+ conn.SetPongHandler(func(string) error {
+ conn.SetReadDeadline(time.Now().Add(pongTimeout))
+ sess.UpdateLastReadTime()
+ return nil
+ })
+
+ // client auth
+ err = ms.sendClientAuthChallenge(sess)
+ if err != nil {
+ log.Error("send client auth challenge: ", err)
+ return
+ }
+
+ done := make(chan struct{})
+ defer close(done)
+ go func() {
+ ticker := time.NewTicker(pingInterval)
+ defer ticker.Stop()
+ var err error
+ for {
+ select {
+ case <-ticker.C:
+ err = sess.Ping()
+ if err != nil {
+ return
+ }
+ case <-done:
+ return
+ }
+ }
+ }()
+
+ for {
+ messageType, bysMsg, err := conn.ReadMessage()
+ if err != nil {
+ log.Errorf("websocket read message error: %v", err)
+ break
+ }
+ conn.SetReadDeadline(time.Now().Add(pongTimeout))
+ sess.UpdateLastReadTime()
+
+ err = ms.OnDataHandle(sess, messageType, bysMsg, r)
+ if err != nil {
+ log.Error(err)
+ }
+ }
+}
diff --git a/api/websocket/server/wsserver.go b/api/websocket/server/wsserver.go
new file mode 100644
index 000000000..f91762165
--- /dev/null
+++ b/api/websocket/server/wsserver.go
@@ -0,0 +1,117 @@
+package server
+
+import (
+ "context"
+ "crypto/tls"
+ "net"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/gorilla/websocket"
+ api "github.com/nknorg/nkn/v2/api/common"
+ "github.com/nknorg/nkn/v2/api/ratelimiter"
+ "github.com/nknorg/nkn/v2/config"
+ "github.com/nknorg/nkn/v2/util/log"
+)
+
+// type conn interface {
+// Start(s *MsgServer, wssCertReady chan struct{}) error
+// }
+
+type wsServer struct {
+ s *MsgServer
+ Upgrader websocket.Upgrader
+ listener net.Listener
+ tlsListener net.Listener
+ server *http.Server
+ tlsServer *http.Server
+}
+
+func (ws *wsServer) Start(s *MsgServer, wssCertReady chan struct{}) error {
+ ws.s = s
+ if config.Parameters.HttpWsPort == 0 {
+ log.Error("Not configure HttpWsPort port ")
+ return nil
+ }
+ ws.Upgrader.CheckOrigin = func(r *http.Request) bool {
+ return true
+ }
+
+ var err error
+
+ ws.listener, err = net.Listen("tcp", ":"+strconv.Itoa(int(config.Parameters.HttpWsPort)))
+ if err != nil {
+ log.Error("net.Listen: ", err.Error())
+ return err
+ }
+
+ ws.server = &http.Server{Handler: http.HandlerFunc(ws.websocketHandler)}
+ go ws.server.Serve(ws.listener)
+
+ go func(wssCertReady chan struct{}) {
+ if wssCertReady == nil {
+ return
+ }
+ for {
+ select {
+ case <-wssCertReady:
+ log.Info("wss cert received")
+ ws.tlsListener, err = ws.initTlsListen()
+ if err != nil {
+ log.Error("Https Cert: ", err.Error())
+ }
+ err = ws.server.Serve(ws.tlsListener)
+ if err != nil {
+ log.Error(err)
+ }
+ return
+ case <-time.After(300 * time.Second):
+ log.Info("wss server is unavailable yet")
+ }
+ }
+ }(wssCertReady)
+
+ return nil
+}
+
+func (ws *wsServer) websocketHandler(w http.ResponseWriter, r *http.Request) {
+ host, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err == nil {
+ limiter := ratelimiter.GetLimiter("ws:"+host, config.Parameters.WsIPRateLimit, int(config.Parameters.WsIPRateBurst))
+ if !limiter.Allow() {
+ log.Infof("Ws connection limit of %s reached", host)
+ w.WriteHeader(http.StatusTooManyRequests)
+ return
+ }
+ }
+
+ wsServer, err := ws.Upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Error("websocket Upgrader: ", err)
+ return
+ }
+ defer wsServer.Close()
+
+ ws.s.newConnection(wsServer, r)
+}
+
+func (ws *wsServer) initTlsListen() (net.Listener, error) {
+ tlsConfig := &tls.Config{
+ GetCertificate: api.GetWssCertificate,
+ }
+
+ listener, err := tls.Listen("tcp", ":"+strconv.Itoa(int(config.Parameters.HttpWssPort)), tlsConfig)
+ if err != nil {
+ log.Error(err)
+ return nil, err
+ }
+ return listener, nil
+}
+
+func (ws *wsServer) Stop() {
+ if ws.server != nil {
+ ws.server.Shutdown(context.Background())
+ log.Error("Close websocket ")
+ }
+}
diff --git a/api/websocket/session/session.go b/api/websocket/session/session.go
index 5435ccfe5..a9d152219 100644
--- a/api/websocket/session/session.go
+++ b/api/websocket/session/session.go
@@ -13,6 +13,17 @@ const (
writeTimeout = 10 * time.Second
)
+type Conn interface {
+ SetReadLimit(int64)
+ SetReadDeadline(t time.Time) error
+ SetWriteDeadline(t time.Time) error
+ WriteMessage(messageType int, data []byte) (err error)
+ WriteJSON(v interface{}) error
+ ReadMessage() (messageType int, data []byte, err error)
+ SetPongHandler(func(string) error)
+ Close() error
+}
+
type Session struct {
sync.RWMutex
sessionID string
@@ -23,7 +34,7 @@ type Session struct {
lastReadTime time.Time
wsLock sync.Mutex
- ws *websocket.Conn
+ ws Conn
Challenge []byte // client auth, authorization challenge
connectTime time.Time // The time which the session is established.
@@ -33,7 +44,7 @@ func (s *Session) GetSessionId() string {
return s.sessionID
}
-func newSession(wsConn *websocket.Conn) (session *Session, err error) {
+func newSession(wsConn Conn) (session *Session, err error) {
sessionID := uuid.NewUUID().String()
session = &Session{
ws: wsConn,
diff --git a/api/websocket/session/sessionlist.go b/api/websocket/session/sessionlist.go
index a664671d4..51ac6f81b 100644
--- a/api/websocket/session/sessionlist.go
+++ b/api/websocket/session/sessionlist.go
@@ -3,8 +3,6 @@ package session
import (
"errors"
"sync"
-
- "github.com/gorilla/websocket"
)
type SessionList struct {
@@ -18,7 +16,7 @@ func NewSessionList() *SessionList {
}
}
-func (sl *SessionList) NewSession(wsConn *websocket.Conn) (*Session, error) {
+func (sl *SessionList) NewSession(wsConn Conn) (*Session, error) {
session, err := newSession(wsConn)
if err != nil {
return nil, err
diff --git a/api/websocket/websocket.go b/api/websocket/websocket.go
index 891605410..f28745eb2 100644
--- a/api/websocket/websocket.go
+++ b/api/websocket/websocket.go
@@ -16,7 +16,7 @@ import (
"github.com/nknorg/nkn/v2/vault"
)
-var ws *server.WsServer
+var ws *server.MsgServer
var (
pushBlockFlag bool = false
@@ -24,10 +24,10 @@ var (
pushBlockTxsFlag bool = false
)
-func NewServer(localNode node.ILocalNode, w *vault.Wallet) *server.WsServer {
+func NewServer(localNode node.ILocalNode, w *vault.Wallet) *server.MsgServer {
// common.SetNode(n)
event.Queue.Subscribe(event.NewBlockProduced, SendBlock2WSclient)
- ws = server.InitWsServer(localNode, w)
+ ws = server.InitMsgServer(localNode, w)
return ws
}
@@ -124,6 +124,6 @@ func PushSigChainBlockHash(v interface{}) {
}
}
-func GetServer() *server.WsServer {
+func GetServer() *server.MsgServer {
return ws
}
diff --git a/cmd/nknd/commands/root.go b/cmd/nknd/commands/root.go
index 36ed5f1fc..1c6f945be 100644
--- a/cmd/nknd/commands/root.go
+++ b/cmd/nknd/commands/root.go
@@ -100,6 +100,7 @@ func init() {
rootCmd.Flags().StringVar(&config.WebGuiListenAddress, "web-gui-listen-address", "", "web gui will listen this address (default: 127.0.0.1)")
rootCmd.Flags().BoolVar(&config.WebGuiCreateWallet, "web-gui-create-wallet", false, "web gui create/open wallet")
rootCmd.Flags().StringVar(&config.PasswordFile, "password-file", "", "read password from file, save password to file when --web-gui-create-wallet arguments be true and password file does not exist")
+ rootCmd.Flags().StringVar(&config.StunList, "stun", "", "Webrtc stun servers, multiple servers should be split by comma")
rootCmd.Flags().MarkHidden("passwd")
}
diff --git a/config.local.json b/config.local.json
index 562b3a41a..3500ac222 100644
--- a/config.local.json
+++ b/config.local.json
@@ -6,5 +6,10 @@
"SeedList": [
"http://127.0.0.1:30003"
],
+ "StunList": [
+ "stun:stun.l.google.com:19302",
+ "stun:stun.cloudflare.com:3478",
+ "stun:stunserver.stunprotocol.org:3478"
+ ],
"GenesisBlockProposer": ""
}
diff --git a/config.mainnet.json b/config.mainnet.json
index 2f3e2bdf5..8468c5ca6 100644
--- a/config.mainnet.json
+++ b/config.mainnet.json
@@ -46,5 +46,10 @@
"http://mainnet-seed-0043.nkn.org:30003",
"http://mainnet-seed-0044.nkn.org:30003"
],
+ "StunList": [
+ "stun:stun.l.google.com:19302",
+ "stun:stun.cloudflare.com:3478",
+ "stun:stunserver.stunprotocol.org:3478"
+ ],
"GenesisBlockProposer": "a0309f8280ca86687a30ca86556113a253762e40eb884fc6063cad2b1ebd7de5"
}
diff --git a/config.testnet.json b/config.testnet.json
index 92302ba9e..c1d6fa9be 100644
--- a/config.testnet.json
+++ b/config.testnet.json
@@ -7,6 +7,11 @@
"http://devnet-seed-0003.nkn.org:30003",
"http://devnet-seed-0004.nkn.org:30003"
],
+ "StunList": [
+ "stun:stun.l.google.com:19302",
+ "stun:stun.cloudflare.com:3478",
+ "stun:stunserver.stunprotocol.org:3478"
+ ],
"GenesisBlockProposer": "0149c42944eea91f094c16538eff0449d4d1e236f31c8c706b2e40e98402984c",
"BeneficiaryAddr": ""
}
diff --git a/config/config.go b/config/config.go
index 405398587..e184f21e2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -249,6 +249,7 @@ var (
WalletFile string
BeneficiaryAddr string
SeedList string
+ StunList string
GenesisBlockProposer string
AllowEmptyBeneficiaryAddress bool
WebGuiListenAddress string
@@ -332,6 +333,7 @@ var (
type Configuration struct {
Version int `json:"Version"`
SeedList []string `json:"SeedList"`
+ StunList []string `json:"StunList"`
HttpWssDomain string `json:"HttpWssDomain"`
HttpWssCert string `json:"HttpWssCert"`
HttpWssKey string `json:"HttpWssKey"`
@@ -449,6 +451,10 @@ func Init() error {
Parameters.SeedList = strings.Split(SeedList, ",")
}
+ if len(StunList) > 0 {
+ Parameters.StunList = strings.Split(StunList, ",")
+ }
+
if len(GenesisBlockProposer) > 0 {
Parameters.GenesisBlockProposer = GenesisBlockProposer
}
@@ -558,6 +564,10 @@ func (config *Configuration) verify() error {
return errors.New("seed list in config file should not be blank")
}
+ if len(config.StunList) == 0 {
+ return errors.New("stun list in config file should not be blank")
+ }
+
if config.NumTxnPerBlock > MaxNumTxnPerBlock {
return fmt.Errorf("NumTxnPerBlock cannot be greater than %d", MaxNumTxnPerBlock)
}
diff --git a/go.mod b/go.mod
index 168390e10..d46ffb89a 100644
--- a/go.mod
+++ b/go.mod
@@ -21,15 +21,17 @@ require (
github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40
github.com/spf13/cobra v1.4.0
github.com/spf13/pflag v1.0.5
- github.com/stretchr/testify v1.8.3
+ github.com/stretchr/testify v1.9.0
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954
github.com/wk8/go-ordered-map v1.0.0
- golang.org/x/crypto v0.17.0
- golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/crypto v0.21.0
+ golang.org/x/sys v0.18.0 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
google.golang.org/protobuf v1.30.0
)
+require github.com/pion/webrtc/v4 v4.0.0-beta.17
+
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cenkalti/backoff/v4 v4.0.0 // indirect
@@ -43,7 +45,7 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
- github.com/google/uuid v1.1.1 // indirect
+ github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/sessions v1.1.3 // indirect
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
@@ -65,6 +67,22 @@ require (
github.com/nknorg/go-nat v1.0.1 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+ github.com/pion/datachannel v1.5.6 // indirect
+ github.com/pion/dtls/v2 v2.2.10 // indirect
+ github.com/pion/ice/v3 v3.0.6 // indirect
+ github.com/pion/interceptor v0.1.29 // indirect
+ github.com/pion/logging v0.2.2 // indirect
+ github.com/pion/mdns/v2 v2.0.7 // indirect
+ github.com/pion/randutil v0.1.0 // indirect
+ github.com/pion/rtcp v1.2.14 // indirect
+ github.com/pion/rtp v1.8.5 // indirect
+ github.com/pion/sctp v1.8.16 // indirect
+ github.com/pion/sdp/v3 v3.0.9 // indirect
+ github.com/pion/srtp/v3 v3.0.1 // indirect
+ github.com/pion/stun/v2 v2.0.0 // indirect
+ github.com/pion/transport/v2 v2.2.4 // indirect
+ github.com/pion/transport/v3 v3.0.2 // indirect
+ github.com/pion/turn/v3 v3.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
@@ -77,8 +95,8 @@ require (
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3 // indirect
golang.org/x/arch v0.3.0 // indirect
- golang.org/x/net v0.17.0 // indirect
- golang.org/x/term v0.15.0 // indirect
+ golang.org/x/net v0.22.0 // indirect
+ golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index 9a97083b5..94208060a 100644
--- a/go.sum
+++ b/go.sum
@@ -173,8 +173,9 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
@@ -300,12 +301,12 @@ github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXW
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.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 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
@@ -320,6 +321,46 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
+github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
+github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
+github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA=
+github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
+github.com/pion/ice/v3 v3.0.6 h1:UC5vZCMhmve7yv+Y6E5eTnRTl+t9LLtmeBYQ9038Zm8=
+github.com/pion/ice/v3 v3.0.6/go.mod h1:4eMTUKQEjC1fGQGB6qUzy2ux9Pc1v9EsO3hNaii+kXI=
+github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
+github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
+github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
+github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
+github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
+github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
+github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
+github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
+github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
+github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
+github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
+github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
+github.com/pion/rtp v1.8.5 h1:uYzINfaK+9yWs7r537z/Rc1SvT8ILjBcmDOpJcTB+OU=
+github.com/pion/rtp v1.8.5/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
+github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
+github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
+github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
+github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
+github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
+github.com/pion/srtp/v3 v3.0.1 h1:AkIQRIZ+3tAOJMQ7G301xtrD1vekQbNeRO7eY1K8ZHk=
+github.com/pion/srtp/v3 v3.0.1/go.mod h1:3R3a1qIOIxBkVTLGFjafKK6/fJoTdQDhcC67HOyMbJ8=
+github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
+github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
+github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
+github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
+github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
+github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
+github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
+github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
+github.com/pion/turn/v3 v3.0.2 h1:iBonAIIKRwkVUJBFiFd/kSjytP7FlX0HwCyBDJPRDdU=
+github.com/pion/turn/v3 v3.0.2/go.mod h1:vw0Dz420q7VYAF3J4wJKzReLHIo2LGp4ev8nXQexYsc=
+github.com/pion/webrtc/v4 v4.0.0-beta.17 h1:KdAbozM+lQ3Dz1NJ0JATRDQ4W02WUhWwIkvjyBRODL0=
+github.com/pion/webrtc/v4 v4.0.0-beta.17/go.mod h1:I/Z0MFtc6Ok7mN7kZmA1xqU7KA9ycZZx/6eXz5+yD+4=
github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -368,6 +409,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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=
@@ -378,8 +420,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU=
@@ -409,6 +453,7 @@ github.com/xtaci/smux v1.2.11 h1:QI4M2HgkkpsVU3Bfcmyx10qURBEeHfKi7xDhGEORfu0=
github.com/xtaci/smux v1.2.11/go.mod h1:f+nYm6SpuHMy/SH0zpbvAFHT1QoMcgLOsWcFip5KfPw=
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 h1:dizWJqTWjwyD8KGcMOwgrkqu1JIkofYgKkmDeNE7oAs=
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA=
gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3 h1:qXqiXDgeQxspR3reot1pWme00CX1pXbxesdzND+EjbU=
@@ -435,8 +480,13 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -467,6 +517,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -497,8 +549,16 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -510,8 +570,10 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
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 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -549,17 +611,39 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
-golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
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.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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -603,6 +687,8 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
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=
@@ -662,8 +748,9 @@ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cn
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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=