├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── addr.go ├── client.go ├── config.go ├── database.go ├── go.mod ├── go.sum ├── lsd.go ├── msg.go ├── msg_test.go ├── params.go ├── pb ├── nymo.pb.go └── nymo.proto ├── peer.go ├── peer_reserver.go ├── pow.go ├── pow_test.go ├── server.go ├── upnp.go ├── user.go └── util.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pb.go -diff -merge -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | out 5 | gen 6 | ### Go template 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | 23 | ### Windows template 24 | # Windows thumbnail cache files 25 | Thumbs.db 26 | Thumbs.db:encryptable 27 | ehthumbs.db 28 | ehthumbs_vista.db 29 | 30 | # Dump file 31 | *.stackdump 32 | 33 | # Folder config file 34 | [Dd]esktop.ini 35 | 36 | # Recycle Bin used on file shares 37 | $RECYCLE.BIN/ 38 | 39 | # Windows Installer files 40 | *.cab 41 | *.msi 42 | *.msix 43 | *.msm 44 | *.msp 45 | 46 | # Windows shortcuts 47 | *.lnk 48 | 49 | ### macOS template 50 | # General 51 | .DS_Store 52 | .AppleDouble 53 | .LSOverride 54 | 55 | # Icon must end with two \r 56 | Icon 57 | 58 | # Thumbnails 59 | ._* 60 | 61 | # Files that might appear in the root of a volume 62 | .DocumentRevisions-V100 63 | .fseventsd 64 | .Spotlight-V100 65 | .TemporaryItems 66 | .Trashes 67 | .VolumeIcon.icns 68 | .com.apple.timemachine.donotpresent 69 | 70 | # Directories potentially created on remote AFP share 71 | .AppleDB 72 | .AppleDesktop 73 | Network Trash Folder 74 | Temporary Items 75 | .apdisk 76 | 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD Zero Clause License 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 8 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nymo Network Core 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/nymo-net/nymo.svg)](https://pkg.go.dev/github.com/nymo-net/nymo) 4 | 5 | This repository contains a vanilla implementation of the 6 | open-standard [Nymo network protocol](https://github.com/nymo-net/nymo-net.github.io/blob/gh-pages/whitepaper.md) in Go as a library. 7 | 8 | A simple UPnP-IGD port-forwarding functionality is also implemented for the convenience of the user. 9 | 10 | ## Implementation 11 | 12 | To use this Nymo core library, implement [`Database`](https://pkg.go.dev/github.com/nymo-net/nymo#Database) and related interfaces. Read [documentation](https://pkg.go.dev/github.com/nymo-net/nymo#section-documentation) for more information on how to use this library. 13 | 14 | ## License 15 | 16 | All files under this repository are marked with [BSD Zero Clause License](./LICENSE). 17 | -------------------------------------------------------------------------------- /addr.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "crypto/elliptic" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "math/big" 8 | "strings" 9 | ) 10 | 11 | // Address represents the address of a Nymo user. This struct should not be initialized directly. 12 | type Address struct { 13 | cohort uint32 14 | x, y *big.Int 15 | } 16 | 17 | // Cohort returns the cohort number of the address. 18 | func (r *Address) Cohort() uint32 { 19 | return r.cohort 20 | } 21 | 22 | // Bytes returns the encoded address. 23 | func (r *Address) Bytes() []byte { 24 | return elliptic.MarshalCompressed(curve, r.x, r.y) 25 | } 26 | 27 | // ConvertAddrToStr returns the string version of an encoded Nymo user address. 28 | func ConvertAddrToStr(addr []byte) string { 29 | // first 6 bits is always 0, so truncate 30 | return protoPrefix + base64.RawURLEncoding.EncodeToString(addr)[1:] 31 | } 32 | 33 | // String returns the string version of the address. 34 | // It is equivalent to ConvertAddrToStr(address.Bytes()) 35 | func (r *Address) String() string { 36 | return ConvertAddrToStr(r.Bytes()) 37 | } 38 | 39 | func getCohort(x, y *big.Int) uint32 { 40 | hash := sha256.New() 41 | hash.Write(x.Bytes()) 42 | hash.Write(y.Bytes()) 43 | 44 | var h big.Int 45 | h.SetBytes(hash.Sum(nil)) 46 | h.Mod(&h, big.NewInt(cohortNumber)) 47 | 48 | return uint32(h.Uint64()) 49 | } 50 | 51 | // NewAddress converts the string version of an address into the internal representation. 52 | func NewAddress(addr string) *Address { 53 | if !strings.HasPrefix(addr, protoPrefix) { 54 | return nil 55 | } 56 | 57 | // first 6 bits should always be 0 ("A" in base 64) 58 | buf, err := base64.RawURLEncoding.DecodeString("A" + addr[len(protoPrefix):]) 59 | if err != nil { 60 | return nil 61 | } 62 | return NewAddressFromBytes(buf) 63 | } 64 | 65 | // NewAddressFromBytes converts the encoded address into the internal representation. 66 | func NewAddressFromBytes(addr []byte) *Address { 67 | x, y := elliptic.UnmarshalCompressed(curve, addr) 68 | if x == nil { 69 | return nil 70 | } 71 | return newAddress(x, y) 72 | } 73 | 74 | func newAddress(x, y *big.Int) *Address { 75 | return &Address{getCohort(x, y), x, y} 76 | } 77 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/http" 12 | "strings" 13 | 14 | "github.com/lucas-clemente/quic-go" 15 | "github.com/lucas-clemente/quic-go/http3" 16 | "github.com/nymo-net/nymo/pb" 17 | "golang.org/x/net/http2" 18 | "google.golang.org/protobuf/proto" 19 | ) 20 | 21 | var noMTlsAsked = errors.New("server did not properly ask for mTLS") 22 | var peerConnected = errors.New("peer connected") 23 | 24 | func (u *User) dialNewPeers(ctx context.Context) { 25 | u.peerCleanup() 26 | 27 | func() { 28 | enum := u.db.EnumeratePeers() 29 | defer enum.Close() 30 | 31 | var outErr error 32 | for u.shouldConnectPeers() && enum.Next(outErr) { 33 | url := enum.Url() 34 | if u.retry.noRetry(url) { 35 | outErr = nil 36 | continue 37 | } 38 | 39 | outErr = u.dialPeer(ctx, enum) 40 | if ctx.Err() != nil { 41 | return 42 | } 43 | if outErr != nil { 44 | u.retry.add(url, u.cfg.PeerRetryTime) 45 | } 46 | if errors.Is(outErr, peerConnected) { 47 | outErr = nil 48 | } 49 | } 50 | }() 51 | 52 | u.peerLock.RLock() 53 | defer u.peerLock.RUnlock() 54 | 55 | // ask for more in-cohort peers 56 | if u.numIn < u.cfg.MaxInCohortConn { 57 | in := u.numIn 58 | for _, p := range u.peers { 59 | if ctx.Err() != nil { 60 | return 61 | } 62 | if p != nil && p.requestPeer(u.peerSameCohort) { 63 | in++ 64 | if in >= u.cfg.MaxInCohortConn { 65 | break 66 | } 67 | } 68 | } 69 | } 70 | 71 | // ask for more out-of-cohort peers 72 | out := u.total - u.numIn 73 | if out < u.cfg.MaxOutCohortConn { 74 | for _, p := range u.peers { 75 | if ctx.Err() != nil { 76 | return 77 | } 78 | if p != nil && p.requestPeer(func(c uint32) bool { return !u.peerSameCohort(c) }) { 79 | out++ 80 | if out >= u.cfg.MaxOutCohortConn { 81 | break 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | func (u *User) dialPeer(ctx context.Context, handle PeerEnumerate) error { 89 | reserver := u.reserveServer(handle.Cohort()) 90 | if reserver == nil { 91 | return nil 92 | } 93 | defer reserver.rollback() 94 | 95 | var askedForHandshake bool 96 | var r http.RoundTripper 97 | var material []byte 98 | var peerId [hashTruncate]byte 99 | var setHandshake func() 100 | 101 | tlsConfig := &tls.Config{ 102 | InsecureSkipVerify: !u.cfg.VerifyServerCert, 103 | GetClientCertificate: func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) { 104 | if len(info.AcceptableCAs) > 0 { 105 | return nil, noMTlsAsked 106 | } 107 | askedForHandshake = true 108 | return &u.cert, nil 109 | }, 110 | } 111 | 112 | validateState := func(state tls.ConnectionState) (err error) { 113 | if !askedForHandshake { 114 | return noMTlsAsked 115 | } 116 | id := hasher(state.PeerCertificates[0].Raw) 117 | peerId = truncateHash(id[:]) 118 | if !reserver.reserveId(&peerId) { 119 | return peerConnected 120 | } 121 | material, err = state.ExportKeyingMaterial(nymoName, nil, hashSize) 122 | if err != nil { 123 | return err 124 | } 125 | setHandshake() 126 | return nil 127 | } 128 | 129 | addr := handle.Url() 130 | switch { 131 | case strings.HasPrefix(addr, "udp://"): 132 | r = &http3.RoundTripper{ 133 | TLSClientConfig: tlsConfig, 134 | Dial: func(_, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error) { 135 | session, err := quic.DialAddrEarly(addr, tlsCfg, cfg) 136 | if err != nil { 137 | return nil, err 138 | } 139 | err = validateState(session.ConnectionState().TLS.ConnectionState) 140 | if err != nil { 141 | _ = session.CloseWithError(0, "") 142 | return nil, err 143 | } 144 | return session, nil 145 | }, 146 | } 147 | case strings.HasPrefix(addr, "tcp://"): 148 | r = &http2.Transport{ 149 | TLSClientConfig: tlsConfig, 150 | DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { 151 | client, err := tls.Dial(network, addr, cfg) 152 | if err != nil { 153 | return nil, err 154 | } 155 | err = validateState(client.ConnectionState()) 156 | if err != nil { 157 | _ = client.Close() 158 | return nil, err 159 | } 160 | return client, nil 161 | }, 162 | } 163 | default: 164 | return fmt.Errorf("%s: unknown address format", addr) 165 | } 166 | 167 | request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https"+addr[3:], nil) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | reader, writer := io.Pipe() 173 | request.Body = reader 174 | 175 | setHandshake = func() { 176 | handshake := pb.PeerHandshake{ 177 | Version: nymoVersion, 178 | Pow: calcPoW(material), 179 | } 180 | 181 | marshal, err := proto.Marshal(&handshake) 182 | if err != nil { 183 | panic(err) 184 | } 185 | 186 | request.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString(marshal)) 187 | } 188 | 189 | resp, err := r.RoundTrip(request) 190 | if err != nil { 191 | return err 192 | } 193 | if resp.StatusCode != http.StatusOK { 194 | return resp.Body.Close() 195 | } 196 | 197 | p, err := u.newPeerAsClient(request.Context(), handle, resp.Body, writer, peerId, material) 198 | if err != nil { 199 | return err 200 | } 201 | reserver.commit(p) 202 | return nil 203 | } 204 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | // Config statically configures several parameters of Nymo core. 9 | type Config struct { 10 | // MaxInCohortConn is the number of maximum number of 11 | // possible in-cohort connections. 12 | MaxInCohortConn uint 13 | // MaxOutCohortConn is the number of maximum number of 14 | // possible out-of-cohort connections. 15 | MaxOutCohortConn uint 16 | 17 | // ListMessageTime is the interval at which messages are listed to the peers. 18 | ListMessageTime time.Duration 19 | // ScanPeerTime is the interval at which new peer connections are tried. 20 | ScanPeerTime time.Duration 21 | // PeerRetryTime is the interval at which a peer connection will be retried. 22 | PeerRetryTime time.Duration 23 | 24 | // Logger is a custom control over logging outputs. 25 | Logger *log.Logger 26 | 27 | // whether Local Peer Announce is enabled. 28 | LocalPeerAnnounce bool 29 | // whether Local Peer Discover is enabled. 30 | LocalPeerDiscover bool 31 | // whether server peer certificate should be validated (against its domain name). 32 | VerifyServerCert bool 33 | } 34 | 35 | // DefaultConfig returns a copy of default config for itemized modification. 36 | func DefaultConfig() *Config { 37 | return &Config{ 38 | MaxInCohortConn: 5, 39 | MaxOutCohortConn: 5, 40 | 41 | ListMessageTime: time.Minute * 5, 42 | ScanPeerTime: time.Second * 30, 43 | PeerRetryTime: time.Minute, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import "github.com/nymo-net/nymo/pb" 4 | 5 | // PeerHandle is a helper object for Nymo core to handle a peer connection. 6 | // Only one PeerHandle will be requested to any peer (with peer ID) before Disconnect is called. 7 | // 8 | // For the following documentation, 𝓜 and 𝓟 is the set of messages and peers stored in the database already, 9 | // 𝓢 is the set of messages we know that the peer knows, and 𝓡 is the set of peers we know that the peer knows. 10 | // For a newly encountered peer ID, 𝓢 = 𝓡 = ∅. 11 | // 12 | // Notice 𝓢 and 𝓡 requires consistency during the lifetime of a PeerHandle object, but isn't 13 | // required to be consistent across restarts, as messages/peers lists should be re-listed if not found. 14 | // However, for best performance, it is best if the implementation can persist 𝓢 and 𝓡 across restarts. 15 | // 16 | // It is then up to the implementation to optimize storage space usage. One way is to remove 17 | // information of a peer that we haven't connected in a long time. 18 | type PeerHandle interface { 19 | // AddKnownMessages adds a new batch of known messages 𝓑 by the peer to the database. 20 | // It should update 𝓢 <- 𝓢 ∪ 𝓑 and return 𝓑 \ 𝓜. 21 | AddKnownMessages([]*pb.Digest) []*pb.Digest 22 | // ListMessages returns 𝓓 = 𝓜 \ 𝓢, with size no more than the size parameter. 23 | // It should also record 𝓓 (see AckMessages for more information). 24 | ListMessages(size uint) []*pb.Digest 25 | // AckMessages confirms the receptions of the last ListMessages. That means 26 | // the implementation should update 𝓢 <- 𝓢 ∪ 𝓓. 27 | // 28 | // It is guaranteed that AckMessages will always be called between two ListMessages. 29 | AckMessages() 30 | // AddKnownPeers adds a new batch of known peers 𝓒 by the peer to the database. 31 | // It should update 𝓡 <- 𝓡 ∪ 𝓒 and return 𝓒 \ 𝓟. 32 | AddKnownPeers([]*pb.Digest) []*pb.Digest 33 | // ListPeers returns 𝓟 \ 𝓡, with size no more than the size parameter. 34 | ListPeers(size uint) []*pb.Digest 35 | // Disconnect is called when the peer disconnects, the implementation can release resources. 36 | // 37 | // The error argument is the error (if any) why disconnection happened. 38 | // The implementation can optionally use this information to rank and ban peers. 39 | Disconnect(error) 40 | } 41 | 42 | // PeerEnumerate is a helper object for Nymo core to iterate through 43 | // known peer URLs. The order is not specified but the implementation 44 | // can choose to list good-ranking peers in front. See Next for more information. 45 | type PeerEnumerate interface { 46 | // Url returns the peer URL that is being currently iterated over. 47 | Url() string 48 | // Cohort returns the cached cohort of the peer URL that is being currently iterated over. 49 | Cohort() uint32 50 | // Next iterates to the next peer URL, and should return false 51 | // if it no longer has more peer URLs to iterate over. 52 | // 53 | // The error argument is the error (if any) happened trying to 54 | // connect to the peer URL that is being currently iterated over. 55 | // The implementation can optionally use this information to rank and ban peers. 56 | // 57 | // Next is guaranteed to be called once before the iteration with error being nil, 58 | // meaning Url and Cohort should return the first result available after a call to Next. 59 | Next(error) bool 60 | // Connect confirms the connection to the URL that is being currently iterated over. 61 | // The implementation should return a PeerHandle of the given peer ID, and update the cached 62 | // cohort corresponding the URL, if necessary. 63 | // 64 | // A call to Connect shouldn't automatically advance the iterator. 65 | Connect(id [hashTruncate]byte, cohort uint32) PeerHandle 66 | // Close stops the iteration, and the implementation should release resources. 67 | Close() 68 | } 69 | 70 | // Database needs to be implemented by any frontend for user/database 71 | // related actions. Data encryption can be optionally implemented, 72 | // transparent to the Nymo core. 73 | // 74 | // Each peer is uniquely identified by its peer ID. A peer can have multiple 75 | // URLs, which is uniquely identified by either the URL string or the hash of it. 76 | // However, there is no relationship between the URL and the peer ID. 77 | // 78 | // Each message is uniquely identified by its hash. However, the implementation 79 | // should always use (hash, cohort) as the key as there might be adversaries 80 | // trying to cheat on what cohort the message was sent to. The actual cohort is only 81 | // confirmed when the message is received and stored by StoreMessage. 82 | type Database interface { 83 | // ClientHandle returns a PeerHandle of the given peer ID. 84 | // See PeerHandle for more information. 85 | ClientHandle(id [hashTruncate]byte) PeerHandle 86 | // AddPeer adds a peer (URL, URL hash, cohort) information to the database. 87 | // Notice that the cohort is only a cache field for better performance 88 | // (see PeerEnumerate for more information). 89 | AddPeer(url string, digest *pb.Digest) 90 | // EnumeratePeers returns a PeerEnumerate of all the known peers (added by AddPeer). 91 | // See PeerEnumerate for more information. 92 | EnumeratePeers() PeerEnumerate 93 | // GetUrlByHash returns the URL by the given URL hash. 94 | // The implementation can return anything if the hash is not found in the database 95 | // (it should not error out). 96 | GetUrlByHash(urlHash [hashTruncate]byte) (url string) 97 | 98 | // GetMessage returns the message data and PoW of the given message hash (stored by StoreMessage). 99 | // The implementation can return anything if the hash is not found in the database 100 | // (it should not error out). 101 | GetMessage(hash [hashSize]byte) (msg []byte, pow uint64) 102 | // IgnoreMessage tells the database to ignore the existence of the message (message hash, cohort). 103 | // (i.e. take message as if it were already received and removed, see StoreMessage) 104 | IgnoreMessage(digest *pb.Digest) 105 | // StoreMessage stores the message (message data, message hash, PoW, cohort) into the database. 106 | // 107 | // Nymo core might store the same message twice into the database in case of a race. 108 | // The implementation can check first if the message with the hash is already in the database, 109 | // and choose not to do nothing (return nil directly). Otherwise, it should call function f. 110 | // 111 | // If f returns a non-nil error, StoreMessage should abort and return that error. Otherwise, 112 | // it should proceed and store the message into the database. 113 | // 114 | // The implementation can choose to remove ancient messages by still storing the hash but removing 115 | // the actual data. 116 | StoreMessage(hash [hashSize]byte, c *pb.MsgContainer, f func() (cohort uint32, err error)) error 117 | // StoreDecryptedMessage stores a decrypted message (see Message for more information) into the database. 118 | // It might be called synchronously when the f function is called by the implementation. 119 | // 120 | // The implementation should not block the call for long. 121 | StoreDecryptedMessage(*Message) 122 | } 123 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nymo-net/nymo 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/huin/goupnp v1.0.2 7 | github.com/lucas-clemente/quic-go v0.25.0 8 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f 9 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 10 | google.golang.org/protobuf v1.27.1 11 | ) 12 | 13 | require ( 14 | github.com/cheekybits/genny v1.0.0 // indirect 15 | github.com/fsnotify/fsnotify v1.5.1 // indirect 16 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 17 | github.com/marten-seemann/qpack v0.2.1 // indirect 18 | github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect 19 | github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect 20 | github.com/marten-seemann/qtls-go1-18 v0.1.0 // indirect 21 | github.com/nxadm/tail v1.4.8 // indirect 22 | github.com/onsi/ginkgo v1.16.5 // indirect 23 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect 24 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect 25 | golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf // indirect 26 | golang.org/x/text v0.3.7 // indirect 27 | golang.org/x/tools v0.1.10 // indirect 28 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 29 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 5 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 6 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 7 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 8 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 9 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 10 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 11 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 12 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 13 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 14 | github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 15 | github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= 16 | github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 18 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 23 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 24 | github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= 25 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 26 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 27 | github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= 28 | github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 29 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 30 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 31 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 32 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 33 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 34 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 35 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 36 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 37 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 38 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 39 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 40 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 41 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 42 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 44 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 45 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 46 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 47 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 48 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 49 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 50 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 51 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 52 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 53 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 54 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 55 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 56 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 57 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 58 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 59 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 60 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 61 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 62 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 63 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 64 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 65 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 66 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 67 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 68 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 69 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 70 | github.com/huin/goupnp v1.0.2 h1:RfGLP+h3mvisuWEyybxNq5Eft3NWhHLPeUN72kpKZoI= 71 | github.com/huin/goupnp v1.0.2/go.mod h1:0dxJBVBHqTMjIUMkESDTNgOOx/Mw5wYIfyFmdzSamkM= 72 | github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= 73 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 74 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 75 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 76 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 77 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 78 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 79 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 80 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 81 | github.com/lucas-clemente/quic-go v0.25.0 h1:K+X9Gvd7JXsOHtU0N2icZ2Nw3rx82uBej3mP4CLgibc= 82 | github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg= 83 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 84 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 85 | github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= 86 | github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= 87 | github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= 88 | github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco= 89 | github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= 90 | github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk= 91 | github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8= 92 | github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI= 93 | github.com/marten-seemann/qtls-go1-18 v0.1.0 h1:gCiNAyl7K4yBBjKkI4LeJjMwIEyCweFoEQFOnRn2MuA= 94 | github.com/marten-seemann/qtls-go1-18 v0.1.0/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI= 95 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 96 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 97 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 98 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 99 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 100 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 101 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 102 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 103 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 104 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 105 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 106 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 107 | github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= 108 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 109 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 110 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 111 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 112 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 113 | github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= 114 | github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= 115 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 116 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 117 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 118 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 119 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 120 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 121 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 122 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 123 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 124 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 125 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 126 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 127 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 128 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 129 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 130 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 131 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 132 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 133 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 134 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 135 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 136 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 137 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 138 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 139 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 140 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 141 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 142 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 143 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 144 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 145 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 146 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 147 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 148 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 149 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 150 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 151 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 152 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 153 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 154 | github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= 155 | github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= 156 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 157 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 158 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 159 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 160 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 161 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 162 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 163 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 164 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 165 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 166 | golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 167 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 168 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 169 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= 170 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 171 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 172 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 173 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 174 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 175 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 176 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 177 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= 178 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 179 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 180 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 181 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 182 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 183 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 184 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 185 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 186 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 187 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 188 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 189 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 190 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 191 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 192 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 193 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 194 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 195 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 196 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 197 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 198 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= 199 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 200 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 201 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 202 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 203 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 204 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 205 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 206 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 207 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 208 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 209 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 210 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 211 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 212 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 213 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 214 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 215 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 216 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 217 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 218 | golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 219 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 223 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 225 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 226 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 227 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 228 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 229 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 232 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 233 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 234 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 235 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 236 | golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf h1:Fm4IcnUL803i92qDlmB0obyHmosDrxZWxJL3gIeNqOw= 237 | golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 238 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 239 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 240 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 241 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 242 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 243 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 244 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 245 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 246 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 247 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 248 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 249 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 250 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 251 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 252 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 253 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 254 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 255 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 256 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 257 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 258 | golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= 259 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 260 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 261 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 262 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 263 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 264 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 265 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 266 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 267 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 268 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 269 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 270 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 271 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 272 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 273 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 274 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 275 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 276 | google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 277 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 278 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 279 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 280 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 281 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 282 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 283 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 284 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 285 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 286 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 287 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 288 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 289 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 290 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 291 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 292 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 293 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 294 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 295 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 296 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 297 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 298 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 299 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 300 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 301 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 302 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 303 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 304 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 305 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 306 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 307 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 308 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 309 | -------------------------------------------------------------------------------- /lsd.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "math/rand" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "time" 13 | 14 | "github.com/nymo-net/nymo/pb" 15 | "golang.org/x/net/ipv4" 16 | "golang.org/x/net/ipv6" 17 | ) 18 | 19 | const ( 20 | multicastGroupIpv4 = "239.192.152.143:6770" 21 | multicastGroupIpv6 = "[ff15::efc0:988f]:6770" 22 | 23 | orgLocalTTL = 31 24 | announceTime = 5 * time.Minute 25 | 26 | maxUdpPayloadSize = 576 - 60 - 8 27 | ) 28 | 29 | func init() { 30 | rand.Seed(time.Now().UnixNano()) 31 | } 32 | 33 | func (u *User) peerAnnounce(ctx context.Context, c net.Conn, host string) error { 34 | ticker := time.NewTicker(announceTime) 35 | defer ticker.Stop() 36 | 37 | for { 38 | srvs := u.ListServers() 39 | if len(srvs) > 0 { 40 | srv := srvs[rand.Intn(len(srvs))] 41 | req := http.Request{ 42 | Method: "BT-SEARCH", 43 | Host: host, 44 | URL: &url.URL{Opaque: "*"}, 45 | Header: http.Header{ 46 | "User-Agent": nil, 47 | "Port": []string{strconv.Itoa(int(u.cohort))}, 48 | "Infohash": []string{srv}, 49 | }, 50 | } 51 | err := req.Write(c) 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | select { 57 | case <-ctx.Done(): 58 | return nil 59 | case <-ticker.C: 60 | } 61 | } 62 | } 63 | 64 | func (u *User) ipv4PeerAnnounce(ctx context.Context) error { 65 | var dialer net.Dialer 66 | 67 | c, err := dialer.DialContext(ctx, "udp4", multicastGroupIpv4) 68 | if err != nil { 69 | return err 70 | } 71 | defer c.Close() 72 | 73 | err = ipv4.NewPacketConn(c.(net.PacketConn)).SetMulticastTTL(orgLocalTTL) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return u.peerAnnounce(ctx, c, multicastGroupIpv4) 79 | } 80 | 81 | func (u *User) ipv6PeerAnnounce(ctx context.Context) error { 82 | var dialer net.Dialer 83 | 84 | c, err := dialer.DialContext(ctx, "udp6", multicastGroupIpv6) 85 | if err != nil { 86 | return err 87 | } 88 | defer c.Close() 89 | 90 | err = ipv6.NewPacketConn(c.(net.PacketConn)).SetMulticastHopLimit(orgLocalTTL) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return u.peerAnnounce(ctx, c, multicastGroupIpv6) 96 | } 97 | 98 | func parsePacket(p []byte, host string) ([]string, uint32) { 99 | req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(p))) 100 | if err != nil || req.Host != host || 101 | req.Method != "BT-SEARCH" || req.RequestURI != "*" || req.Proto != "HTTP/1.1" { 102 | return nil, 0 103 | } 104 | addrs := req.Header["Infohash"] 105 | cohort := req.Header["Port"] 106 | if len(cohort) != 1 || len(addrs) <= 0 { 107 | return nil, 0 108 | } 109 | c, err := strconv.Atoi(cohort[0]) 110 | if err != nil { 111 | return nil, 0 112 | } 113 | if c < 0 || c > cohortNumber { 114 | return nil, 0 115 | } 116 | return addrs, uint32(c) 117 | } 118 | 119 | func (u *User) peerDiscover(ctx context.Context, host string) error { 120 | addr, err := net.ResolveUDPAddr("udp", host) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | l, err := net.ListenMulticastUDP("udp", nil, addr) 126 | if err != nil { 127 | return err 128 | } 129 | go func() { 130 | defer l.Close() 131 | <-ctx.Done() 132 | }() 133 | 134 | buf := make([]byte, maxUdpPayloadSize) 135 | for { 136 | pu, err := l.Read(buf) 137 | if err != nil { 138 | if ctx.Err() != nil { 139 | return nil 140 | } 141 | return err 142 | } 143 | addrs, cohort := parsePacket(buf[:pu], host) 144 | for _, a := range addrs { 145 | hash := hasher([]byte(a)) 146 | u.db.AddPeer(a, &pb.Digest{ 147 | Hash: hash[:hashTruncate], 148 | Cohort: cohort, 149 | }) 150 | } 151 | } 152 | } 153 | 154 | func (u *User) ipv4PeerDiscover(ctx context.Context) error { 155 | return u.peerDiscover(ctx, multicastGroupIpv4) 156 | } 157 | 158 | func (u *User) ipv6PeerDiscover(ctx context.Context) error { 159 | return u.peerDiscover(ctx, multicastGroupIpv6) 160 | } 161 | -------------------------------------------------------------------------------- /msg.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/ecdsa" 7 | "crypto/elliptic" 8 | "math/big" 9 | "time" 10 | 11 | "github.com/nymo-net/nymo/pb" 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | // Message contains a decrypted message (sent to the user). 16 | type Message struct { 17 | // Sender is the sender of the message. 18 | Sender *Address 19 | // SendTime is the sender-specified time. 20 | SendTime time.Time 21 | // Content contains the protocol-specified message data. 22 | // It is a UTF-8 encoded string in vanilla implementation. 23 | Content []byte 24 | } 25 | 26 | func (u *User) decryptMessage(msg *pb.Message) *Message { 27 | if msg.TargetCohort != u.cohort { 28 | return nil 29 | } 30 | 31 | eKeyX, eKeyY := elliptic.UnmarshalCompressed(curve, msg.EphemeralPub) 32 | if eKeyX == nil { 33 | return nil 34 | } 35 | 36 | secret, iv := curve.ScalarMult(eKeyX, eKeyY, u.key.D.Bytes()) 37 | cp, err := aes.NewCipher(secret.Bytes()) 38 | if err != nil { 39 | return nil 40 | } 41 | 42 | encMsg := msg.EncMessage 43 | cipher.NewCBCDecrypter(cp, iv.Bytes()[8:][:aes.BlockSize]).CryptBlocks(encMsg, encMsg) 44 | block := trimBlock(encMsg) 45 | if block == nil { 46 | return nil 47 | } 48 | 49 | enc := new(pb.EncryptedMessage) 50 | err = proto.Unmarshal(block, enc) 51 | if err != nil { 52 | return nil 53 | } 54 | 55 | ret := new(pb.RealMessage) 56 | err = proto.Unmarshal(enc.Msg, ret) 57 | if err != nil { 58 | return nil 59 | } 60 | 61 | x, y := elliptic.UnmarshalCompressed(curve, ret.SenderID) 62 | h := hasher(enc.Msg) 63 | if x == nil || !ecdsa.Verify( 64 | &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, h[:], 65 | new(big.Int).SetBytes(enc.Signature[:curveByteLen]), 66 | new(big.Int).SetBytes(enc.Signature[curveByteLen:]), 67 | ) { 68 | return nil 69 | } 70 | 71 | return &Message{ 72 | Sender: newAddress(x, y), 73 | SendTime: time.UnixMilli(ret.SendTime), 74 | Content: ret.Message, 75 | } 76 | } 77 | 78 | // NewMessage send a new message with content msg to the recipient. 79 | // Database.StoreMessage might be called synchronously. 80 | func (u *User) NewMessage(recipient *Address, msg []byte) error { 81 | ephemeralKey, err := ecdsa.GenerateKey(curve, cReader) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | secret, iv := curve.ScalarMult(recipient.x, recipient.y, ephemeralKey.D.Bytes()) 87 | cp, err := aes.NewCipher(secret.Bytes()) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | rMsg := &pb.RealMessage{ 93 | Message: msg, 94 | SendTime: time.Now().UnixMilli(), 95 | SenderID: elliptic.MarshalCompressed(curve, u.key.X, u.key.Y), 96 | } 97 | rMsgBuf, err := proto.Marshal(rMsg) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | h := hasher(rMsgBuf) 103 | sigR, sigS, err := ecdsa.Sign(cReader, u.key, h[:]) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | enc := pb.EncryptedMessage{ 109 | Msg: rMsgBuf, 110 | Signature: make([]byte, curveByteLen*2), 111 | } 112 | sigR.FillBytes(enc.Signature[:curveByteLen]) 113 | sigS.FillBytes(enc.Signature[curveByteLen:]) 114 | 115 | marshal, err := proto.Marshal(&enc) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | marshal = padBlock(marshal) 121 | cipher.NewCBCEncrypter(cp, iv.Bytes()[8:][:aes.BlockSize]).CryptBlocks(marshal, marshal) 122 | 123 | mMsg, err := proto.Marshal(&pb.Message{ 124 | TargetCohort: recipient.cohort, 125 | EphemeralPub: elliptic.MarshalCompressed(curve, ephemeralKey.X, ephemeralKey.Y), 126 | EncMessage: marshal, 127 | }) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | msgHash := hasher(mMsg) 133 | return u.db.StoreMessage(msgHash, &pb.MsgContainer{ 134 | Msg: mMsg, 135 | Pow: calcPoW(msgHash[:]), 136 | }, func() (uint32, error) { return recipient.cohort, nil }) 137 | } 138 | -------------------------------------------------------------------------------- /msg_test.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "math/rand" 7 | "testing" 8 | 9 | "github.com/nymo-net/nymo/pb" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | func TestMsgEncDec(t *testing.T) { 14 | key, err := GenerateUser() 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | var db testDB 19 | u := OpenUser(&db, key, tls.Certificate{Certificate: [][]byte{{}}}, nil) 20 | 21 | buf := make([]byte, 2048) 22 | rand.Read(buf) 23 | err = u.NewMessage(u.Address(), buf) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | message := u.decryptMessage(db.last) 29 | if message == nil || !bytes.Equal(message.Content, buf) { 30 | t.Fatal("decrypt error") 31 | } 32 | } 33 | 34 | func TestMsgEncNoDec(t *testing.T) { 35 | key, err := GenerateUser() 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | key2, err := GenerateUser() 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | var db testDB 44 | u := OpenUser(&db, key, tls.Certificate{Certificate: [][]byte{{}}}, nil) 45 | u2 := OpenUser(new(testDB), key2, tls.Certificate{Certificate: [][]byte{{}}}, nil) 46 | 47 | buf := make([]byte, 2048) 48 | rand.Read(buf) 49 | err = u.NewMessage(u.Address(), buf) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | message := u2.decryptMessage(db.last) 55 | if message != nil { 56 | t.Fatal("decrypt success") 57 | } 58 | } 59 | 60 | type testDB struct { 61 | last *pb.Message 62 | } 63 | 64 | func (t *testDB) ClientHandle(id [hashTruncate]byte) PeerHandle { 65 | panic("implement me") 66 | } 67 | 68 | func (t *testDB) AddPeer(url string, digest *pb.Digest) { 69 | panic("implement me") 70 | } 71 | 72 | func (t *testDB) EnumeratePeers() PeerEnumerate { 73 | panic("implement me") 74 | } 75 | 76 | func (t *testDB) GetUrlByHash(urlHash [hashTruncate]byte) (url string) { 77 | panic("implement me") 78 | } 79 | 80 | func (t *testDB) GetMessage(hash [hashSize]byte) (msg []byte, pow uint64) { 81 | panic("implement me") 82 | } 83 | 84 | func (t *testDB) IgnoreMessage(digest *pb.Digest) { 85 | panic("implement me") 86 | } 87 | 88 | func (t *testDB) StoreMessage(hash [hashSize]byte, c *pb.MsgContainer, f func() (cohort uint32, err error)) error { 89 | _, err := f() 90 | if err == nil { 91 | t.last = new(pb.Message) 92 | err = proto.Unmarshal(c.Msg, t.last) 93 | } 94 | return err 95 | } 96 | 97 | func (t *testDB) StoreDecryptedMessage(message *Message) { 98 | panic("implement me") 99 | } 100 | -------------------------------------------------------------------------------- /params.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "crypto/elliptic" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "math" 9 | "time" 10 | "unsafe" 11 | ) 12 | 13 | const ( 14 | nymoName = "nymo" 15 | nymoVersion = "vanilla alpha-1" 16 | cohortNumber = 64 17 | hashSize = sha256.Size 18 | hashTruncate = 8 19 | bitStrength = sha256.Size*8 - 22 20 | protoPrefix = nymoName + "://" 21 | epsilon = 0.1 // 10% chance out-of-cohort message will be accepted 22 | 23 | msgListMax = 500 24 | peerListMax = 20 25 | 26 | uint16Size = int(unsafe.Sizeof(uint16(0))) 27 | maxPacketSize = math.MaxUint16 - uint16Size // 64 KiB 28 | ) 29 | 30 | var ( 31 | curve = elliptic.P256() // NIST P-256 32 | cReader = rand.Reader 33 | hasher = sha256.Sum256 34 | encoding = binary.BigEndian 35 | 36 | curveByteLen = (curve.Params().BitSize + 7) / 8 // 256 37 | 38 | emptyTime time.Time 39 | ) 40 | 41 | // Version returns the current version of Nymo core. 42 | func Version() string { 43 | return nymoVersion 44 | } 45 | -------------------------------------------------------------------------------- /pb/nymo.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.27.1 4 | // protoc v3.12.4 5 | // source: pb/nymo.proto 6 | 7 | package pb 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type PeerHandshake struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` 29 | Cohort uint32 `protobuf:"varint,2,opt,name=cohort,proto3" json:"cohort,omitempty"` 30 | Pow uint64 `protobuf:"varint,3,opt,name=pow,proto3" json:"pow,omitempty"` 31 | } 32 | 33 | func (x *PeerHandshake) Reset() { 34 | *x = PeerHandshake{} 35 | if protoimpl.UnsafeEnabled { 36 | mi := &file_pb_nymo_proto_msgTypes[0] 37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 38 | ms.StoreMessageInfo(mi) 39 | } 40 | } 41 | 42 | func (x *PeerHandshake) String() string { 43 | return protoimpl.X.MessageStringOf(x) 44 | } 45 | 46 | func (*PeerHandshake) ProtoMessage() {} 47 | 48 | func (x *PeerHandshake) ProtoReflect() protoreflect.Message { 49 | mi := &file_pb_nymo_proto_msgTypes[0] 50 | if protoimpl.UnsafeEnabled && x != nil { 51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 52 | if ms.LoadMessageInfo() == nil { 53 | ms.StoreMessageInfo(mi) 54 | } 55 | return ms 56 | } 57 | return mi.MessageOf(x) 58 | } 59 | 60 | // Deprecated: Use PeerHandshake.ProtoReflect.Descriptor instead. 61 | func (*PeerHandshake) Descriptor() ([]byte, []int) { 62 | return file_pb_nymo_proto_rawDescGZIP(), []int{0} 63 | } 64 | 65 | func (x *PeerHandshake) GetVersion() string { 66 | if x != nil { 67 | return x.Version 68 | } 69 | return "" 70 | } 71 | 72 | func (x *PeerHandshake) GetCohort() uint32 { 73 | if x != nil { 74 | return x.Cohort 75 | } 76 | return 0 77 | } 78 | 79 | func (x *PeerHandshake) GetPow() uint64 { 80 | if x != nil { 81 | return x.Pow 82 | } 83 | return 0 84 | } 85 | 86 | type HandshakeOK struct { 87 | state protoimpl.MessageState 88 | sizeCache protoimpl.SizeCache 89 | unknownFields protoimpl.UnknownFields 90 | 91 | Cohort uint32 `protobuf:"varint,1,opt,name=cohort,proto3" json:"cohort,omitempty"` 92 | } 93 | 94 | func (x *HandshakeOK) Reset() { 95 | *x = HandshakeOK{} 96 | if protoimpl.UnsafeEnabled { 97 | mi := &file_pb_nymo_proto_msgTypes[1] 98 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 99 | ms.StoreMessageInfo(mi) 100 | } 101 | } 102 | 103 | func (x *HandshakeOK) String() string { 104 | return protoimpl.X.MessageStringOf(x) 105 | } 106 | 107 | func (*HandshakeOK) ProtoMessage() {} 108 | 109 | func (x *HandshakeOK) ProtoReflect() protoreflect.Message { 110 | mi := &file_pb_nymo_proto_msgTypes[1] 111 | if protoimpl.UnsafeEnabled && x != nil { 112 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 113 | if ms.LoadMessageInfo() == nil { 114 | ms.StoreMessageInfo(mi) 115 | } 116 | return ms 117 | } 118 | return mi.MessageOf(x) 119 | } 120 | 121 | // Deprecated: Use HandshakeOK.ProtoReflect.Descriptor instead. 122 | func (*HandshakeOK) Descriptor() ([]byte, []int) { 123 | return file_pb_nymo_proto_rawDescGZIP(), []int{1} 124 | } 125 | 126 | func (x *HandshakeOK) GetCohort() uint32 { 127 | if x != nil { 128 | return x.Cohort 129 | } 130 | return 0 131 | } 132 | 133 | type Digest struct { 134 | state protoimpl.MessageState 135 | sizeCache protoimpl.SizeCache 136 | unknownFields protoimpl.UnknownFields 137 | 138 | Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` 139 | Cohort uint32 `protobuf:"varint,2,opt,name=cohort,proto3" json:"cohort,omitempty"` 140 | } 141 | 142 | func (x *Digest) Reset() { 143 | *x = Digest{} 144 | if protoimpl.UnsafeEnabled { 145 | mi := &file_pb_nymo_proto_msgTypes[2] 146 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 147 | ms.StoreMessageInfo(mi) 148 | } 149 | } 150 | 151 | func (x *Digest) String() string { 152 | return protoimpl.X.MessageStringOf(x) 153 | } 154 | 155 | func (*Digest) ProtoMessage() {} 156 | 157 | func (x *Digest) ProtoReflect() protoreflect.Message { 158 | mi := &file_pb_nymo_proto_msgTypes[2] 159 | if protoimpl.UnsafeEnabled && x != nil { 160 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 161 | if ms.LoadMessageInfo() == nil { 162 | ms.StoreMessageInfo(mi) 163 | } 164 | return ms 165 | } 166 | return mi.MessageOf(x) 167 | } 168 | 169 | // Deprecated: Use Digest.ProtoReflect.Descriptor instead. 170 | func (*Digest) Descriptor() ([]byte, []int) { 171 | return file_pb_nymo_proto_rawDescGZIP(), []int{2} 172 | } 173 | 174 | func (x *Digest) GetHash() []byte { 175 | if x != nil { 176 | return x.Hash 177 | } 178 | return nil 179 | } 180 | 181 | func (x *Digest) GetCohort() uint32 { 182 | if x != nil { 183 | return x.Cohort 184 | } 185 | return 0 186 | } 187 | 188 | type PeerList struct { 189 | state protoimpl.MessageState 190 | sizeCache protoimpl.SizeCache 191 | unknownFields protoimpl.UnknownFields 192 | 193 | Peers []*Digest `protobuf:"bytes,1,rep,name=peers,proto3" json:"peers,omitempty"` 194 | } 195 | 196 | func (x *PeerList) Reset() { 197 | *x = PeerList{} 198 | if protoimpl.UnsafeEnabled { 199 | mi := &file_pb_nymo_proto_msgTypes[3] 200 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 201 | ms.StoreMessageInfo(mi) 202 | } 203 | } 204 | 205 | func (x *PeerList) String() string { 206 | return protoimpl.X.MessageStringOf(x) 207 | } 208 | 209 | func (*PeerList) ProtoMessage() {} 210 | 211 | func (x *PeerList) ProtoReflect() protoreflect.Message { 212 | mi := &file_pb_nymo_proto_msgTypes[3] 213 | if protoimpl.UnsafeEnabled && x != nil { 214 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 215 | if ms.LoadMessageInfo() == nil { 216 | ms.StoreMessageInfo(mi) 217 | } 218 | return ms 219 | } 220 | return mi.MessageOf(x) 221 | } 222 | 223 | // Deprecated: Use PeerList.ProtoReflect.Descriptor instead. 224 | func (*PeerList) Descriptor() ([]byte, []int) { 225 | return file_pb_nymo_proto_rawDescGZIP(), []int{3} 226 | } 227 | 228 | func (x *PeerList) GetPeers() []*Digest { 229 | if x != nil { 230 | return x.Peers 231 | } 232 | return nil 233 | } 234 | 235 | type RequestPeer struct { 236 | state protoimpl.MessageState 237 | sizeCache protoimpl.SizeCache 238 | unknownFields protoimpl.UnknownFields 239 | 240 | UrlHash []byte `protobuf:"bytes,1,opt,name=urlHash,proto3" json:"urlHash,omitempty"` 241 | Pow uint64 `protobuf:"varint,2,opt,name=pow,proto3" json:"pow,omitempty"` 242 | } 243 | 244 | func (x *RequestPeer) Reset() { 245 | *x = RequestPeer{} 246 | if protoimpl.UnsafeEnabled { 247 | mi := &file_pb_nymo_proto_msgTypes[4] 248 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 249 | ms.StoreMessageInfo(mi) 250 | } 251 | } 252 | 253 | func (x *RequestPeer) String() string { 254 | return protoimpl.X.MessageStringOf(x) 255 | } 256 | 257 | func (*RequestPeer) ProtoMessage() {} 258 | 259 | func (x *RequestPeer) ProtoReflect() protoreflect.Message { 260 | mi := &file_pb_nymo_proto_msgTypes[4] 261 | if protoimpl.UnsafeEnabled && x != nil { 262 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 263 | if ms.LoadMessageInfo() == nil { 264 | ms.StoreMessageInfo(mi) 265 | } 266 | return ms 267 | } 268 | return mi.MessageOf(x) 269 | } 270 | 271 | // Deprecated: Use RequestPeer.ProtoReflect.Descriptor instead. 272 | func (*RequestPeer) Descriptor() ([]byte, []int) { 273 | return file_pb_nymo_proto_rawDescGZIP(), []int{4} 274 | } 275 | 276 | func (x *RequestPeer) GetUrlHash() []byte { 277 | if x != nil { 278 | return x.UrlHash 279 | } 280 | return nil 281 | } 282 | 283 | func (x *RequestPeer) GetPow() uint64 { 284 | if x != nil { 285 | return x.Pow 286 | } 287 | return 0 288 | } 289 | 290 | type ResponsePeer struct { 291 | state protoimpl.MessageState 292 | sizeCache protoimpl.SizeCache 293 | unknownFields protoimpl.UnknownFields 294 | 295 | Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` 296 | } 297 | 298 | func (x *ResponsePeer) Reset() { 299 | *x = ResponsePeer{} 300 | if protoimpl.UnsafeEnabled { 301 | mi := &file_pb_nymo_proto_msgTypes[5] 302 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 303 | ms.StoreMessageInfo(mi) 304 | } 305 | } 306 | 307 | func (x *ResponsePeer) String() string { 308 | return protoimpl.X.MessageStringOf(x) 309 | } 310 | 311 | func (*ResponsePeer) ProtoMessage() {} 312 | 313 | func (x *ResponsePeer) ProtoReflect() protoreflect.Message { 314 | mi := &file_pb_nymo_proto_msgTypes[5] 315 | if protoimpl.UnsafeEnabled && x != nil { 316 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 317 | if ms.LoadMessageInfo() == nil { 318 | ms.StoreMessageInfo(mi) 319 | } 320 | return ms 321 | } 322 | return mi.MessageOf(x) 323 | } 324 | 325 | // Deprecated: Use ResponsePeer.ProtoReflect.Descriptor instead. 326 | func (*ResponsePeer) Descriptor() ([]byte, []int) { 327 | return file_pb_nymo_proto_rawDescGZIP(), []int{5} 328 | } 329 | 330 | func (x *ResponsePeer) GetAddress() string { 331 | if x != nil { 332 | return x.Address 333 | } 334 | return "" 335 | } 336 | 337 | type MsgList struct { 338 | state protoimpl.MessageState 339 | sizeCache protoimpl.SizeCache 340 | unknownFields protoimpl.UnknownFields 341 | 342 | Messages []*Digest `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` 343 | } 344 | 345 | func (x *MsgList) Reset() { 346 | *x = MsgList{} 347 | if protoimpl.UnsafeEnabled { 348 | mi := &file_pb_nymo_proto_msgTypes[6] 349 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 350 | ms.StoreMessageInfo(mi) 351 | } 352 | } 353 | 354 | func (x *MsgList) String() string { 355 | return protoimpl.X.MessageStringOf(x) 356 | } 357 | 358 | func (*MsgList) ProtoMessage() {} 359 | 360 | func (x *MsgList) ProtoReflect() protoreflect.Message { 361 | mi := &file_pb_nymo_proto_msgTypes[6] 362 | if protoimpl.UnsafeEnabled && x != nil { 363 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 364 | if ms.LoadMessageInfo() == nil { 365 | ms.StoreMessageInfo(mi) 366 | } 367 | return ms 368 | } 369 | return mi.MessageOf(x) 370 | } 371 | 372 | // Deprecated: Use MsgList.ProtoReflect.Descriptor instead. 373 | func (*MsgList) Descriptor() ([]byte, []int) { 374 | return file_pb_nymo_proto_rawDescGZIP(), []int{6} 375 | } 376 | 377 | func (x *MsgList) GetMessages() []*Digest { 378 | if x != nil { 379 | return x.Messages 380 | } 381 | return nil 382 | } 383 | 384 | type MsgListAck struct { 385 | state protoimpl.MessageState 386 | sizeCache protoimpl.SizeCache 387 | unknownFields protoimpl.UnknownFields 388 | } 389 | 390 | func (x *MsgListAck) Reset() { 391 | *x = MsgListAck{} 392 | if protoimpl.UnsafeEnabled { 393 | mi := &file_pb_nymo_proto_msgTypes[7] 394 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 395 | ms.StoreMessageInfo(mi) 396 | } 397 | } 398 | 399 | func (x *MsgListAck) String() string { 400 | return protoimpl.X.MessageStringOf(x) 401 | } 402 | 403 | func (*MsgListAck) ProtoMessage() {} 404 | 405 | func (x *MsgListAck) ProtoReflect() protoreflect.Message { 406 | mi := &file_pb_nymo_proto_msgTypes[7] 407 | if protoimpl.UnsafeEnabled && x != nil { 408 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 409 | if ms.LoadMessageInfo() == nil { 410 | ms.StoreMessageInfo(mi) 411 | } 412 | return ms 413 | } 414 | return mi.MessageOf(x) 415 | } 416 | 417 | // Deprecated: Use MsgListAck.ProtoReflect.Descriptor instead. 418 | func (*MsgListAck) Descriptor() ([]byte, []int) { 419 | return file_pb_nymo_proto_rawDescGZIP(), []int{7} 420 | } 421 | 422 | type RequestMsg struct { 423 | state protoimpl.MessageState 424 | sizeCache protoimpl.SizeCache 425 | unknownFields protoimpl.UnknownFields 426 | 427 | Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` 428 | } 429 | 430 | func (x *RequestMsg) Reset() { 431 | *x = RequestMsg{} 432 | if protoimpl.UnsafeEnabled { 433 | mi := &file_pb_nymo_proto_msgTypes[8] 434 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 435 | ms.StoreMessageInfo(mi) 436 | } 437 | } 438 | 439 | func (x *RequestMsg) String() string { 440 | return protoimpl.X.MessageStringOf(x) 441 | } 442 | 443 | func (*RequestMsg) ProtoMessage() {} 444 | 445 | func (x *RequestMsg) ProtoReflect() protoreflect.Message { 446 | mi := &file_pb_nymo_proto_msgTypes[8] 447 | if protoimpl.UnsafeEnabled && x != nil { 448 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 449 | if ms.LoadMessageInfo() == nil { 450 | ms.StoreMessageInfo(mi) 451 | } 452 | return ms 453 | } 454 | return mi.MessageOf(x) 455 | } 456 | 457 | // Deprecated: Use RequestMsg.ProtoReflect.Descriptor instead. 458 | func (*RequestMsg) Descriptor() ([]byte, []int) { 459 | return file_pb_nymo_proto_rawDescGZIP(), []int{8} 460 | } 461 | 462 | func (x *RequestMsg) GetHash() []byte { 463 | if x != nil { 464 | return x.Hash 465 | } 466 | return nil 467 | } 468 | 469 | type RealMessage struct { 470 | state protoimpl.MessageState 471 | sizeCache protoimpl.SizeCache 472 | unknownFields protoimpl.UnknownFields 473 | 474 | Message []byte `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 475 | SenderID []byte `protobuf:"bytes,2,opt,name=senderID,proto3" json:"senderID,omitempty"` // sender's public key 476 | SendTime int64 `protobuf:"varint,3,opt,name=sendTime,proto3" json:"sendTime,omitempty"` 477 | } 478 | 479 | func (x *RealMessage) Reset() { 480 | *x = RealMessage{} 481 | if protoimpl.UnsafeEnabled { 482 | mi := &file_pb_nymo_proto_msgTypes[9] 483 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 484 | ms.StoreMessageInfo(mi) 485 | } 486 | } 487 | 488 | func (x *RealMessage) String() string { 489 | return protoimpl.X.MessageStringOf(x) 490 | } 491 | 492 | func (*RealMessage) ProtoMessage() {} 493 | 494 | func (x *RealMessage) ProtoReflect() protoreflect.Message { 495 | mi := &file_pb_nymo_proto_msgTypes[9] 496 | if protoimpl.UnsafeEnabled && x != nil { 497 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 498 | if ms.LoadMessageInfo() == nil { 499 | ms.StoreMessageInfo(mi) 500 | } 501 | return ms 502 | } 503 | return mi.MessageOf(x) 504 | } 505 | 506 | // Deprecated: Use RealMessage.ProtoReflect.Descriptor instead. 507 | func (*RealMessage) Descriptor() ([]byte, []int) { 508 | return file_pb_nymo_proto_rawDescGZIP(), []int{9} 509 | } 510 | 511 | func (x *RealMessage) GetMessage() []byte { 512 | if x != nil { 513 | return x.Message 514 | } 515 | return nil 516 | } 517 | 518 | func (x *RealMessage) GetSenderID() []byte { 519 | if x != nil { 520 | return x.SenderID 521 | } 522 | return nil 523 | } 524 | 525 | func (x *RealMessage) GetSendTime() int64 { 526 | if x != nil { 527 | return x.SendTime 528 | } 529 | return 0 530 | } 531 | 532 | type EncryptedMessage struct { 533 | state protoimpl.MessageState 534 | sizeCache protoimpl.SizeCache 535 | unknownFields protoimpl.UnknownFields 536 | 537 | Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` 538 | Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` // signed with sender's private key 539 | } 540 | 541 | func (x *EncryptedMessage) Reset() { 542 | *x = EncryptedMessage{} 543 | if protoimpl.UnsafeEnabled { 544 | mi := &file_pb_nymo_proto_msgTypes[10] 545 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 546 | ms.StoreMessageInfo(mi) 547 | } 548 | } 549 | 550 | func (x *EncryptedMessage) String() string { 551 | return protoimpl.X.MessageStringOf(x) 552 | } 553 | 554 | func (*EncryptedMessage) ProtoMessage() {} 555 | 556 | func (x *EncryptedMessage) ProtoReflect() protoreflect.Message { 557 | mi := &file_pb_nymo_proto_msgTypes[10] 558 | if protoimpl.UnsafeEnabled && x != nil { 559 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 560 | if ms.LoadMessageInfo() == nil { 561 | ms.StoreMessageInfo(mi) 562 | } 563 | return ms 564 | } 565 | return mi.MessageOf(x) 566 | } 567 | 568 | // Deprecated: Use EncryptedMessage.ProtoReflect.Descriptor instead. 569 | func (*EncryptedMessage) Descriptor() ([]byte, []int) { 570 | return file_pb_nymo_proto_rawDescGZIP(), []int{10} 571 | } 572 | 573 | func (x *EncryptedMessage) GetMsg() []byte { 574 | if x != nil { 575 | return x.Msg 576 | } 577 | return nil 578 | } 579 | 580 | func (x *EncryptedMessage) GetSignature() []byte { 581 | if x != nil { 582 | return x.Signature 583 | } 584 | return nil 585 | } 586 | 587 | type Message struct { 588 | state protoimpl.MessageState 589 | sizeCache protoimpl.SizeCache 590 | unknownFields protoimpl.UnknownFields 591 | 592 | TargetCohort uint32 `protobuf:"varint,1,opt,name=targetCohort,proto3" json:"targetCohort,omitempty"` 593 | EphemeralPub []byte `protobuf:"bytes,2,opt,name=ephemeralPub,proto3" json:"ephemeralPub,omitempty"` // ephemeral public key 594 | EncMessage []byte `protobuf:"bytes,3,opt,name=encMessage,proto3" json:"encMessage,omitempty"` // shared-key encrypted EncryptedMessage 595 | } 596 | 597 | func (x *Message) Reset() { 598 | *x = Message{} 599 | if protoimpl.UnsafeEnabled { 600 | mi := &file_pb_nymo_proto_msgTypes[11] 601 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 602 | ms.StoreMessageInfo(mi) 603 | } 604 | } 605 | 606 | func (x *Message) String() string { 607 | return protoimpl.X.MessageStringOf(x) 608 | } 609 | 610 | func (*Message) ProtoMessage() {} 611 | 612 | func (x *Message) ProtoReflect() protoreflect.Message { 613 | mi := &file_pb_nymo_proto_msgTypes[11] 614 | if protoimpl.UnsafeEnabled && x != nil { 615 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 616 | if ms.LoadMessageInfo() == nil { 617 | ms.StoreMessageInfo(mi) 618 | } 619 | return ms 620 | } 621 | return mi.MessageOf(x) 622 | } 623 | 624 | // Deprecated: Use Message.ProtoReflect.Descriptor instead. 625 | func (*Message) Descriptor() ([]byte, []int) { 626 | return file_pb_nymo_proto_rawDescGZIP(), []int{11} 627 | } 628 | 629 | func (x *Message) GetTargetCohort() uint32 { 630 | if x != nil { 631 | return x.TargetCohort 632 | } 633 | return 0 634 | } 635 | 636 | func (x *Message) GetEphemeralPub() []byte { 637 | if x != nil { 638 | return x.EphemeralPub 639 | } 640 | return nil 641 | } 642 | 643 | func (x *Message) GetEncMessage() []byte { 644 | if x != nil { 645 | return x.EncMessage 646 | } 647 | return nil 648 | } 649 | 650 | type MsgContainer struct { 651 | state protoimpl.MessageState 652 | sizeCache protoimpl.SizeCache 653 | unknownFields protoimpl.UnknownFields 654 | 655 | Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` 656 | Pow uint64 `protobuf:"varint,2,opt,name=pow,proto3" json:"pow,omitempty"` 657 | } 658 | 659 | func (x *MsgContainer) Reset() { 660 | *x = MsgContainer{} 661 | if protoimpl.UnsafeEnabled { 662 | mi := &file_pb_nymo_proto_msgTypes[12] 663 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 664 | ms.StoreMessageInfo(mi) 665 | } 666 | } 667 | 668 | func (x *MsgContainer) String() string { 669 | return protoimpl.X.MessageStringOf(x) 670 | } 671 | 672 | func (*MsgContainer) ProtoMessage() {} 673 | 674 | func (x *MsgContainer) ProtoReflect() protoreflect.Message { 675 | mi := &file_pb_nymo_proto_msgTypes[12] 676 | if protoimpl.UnsafeEnabled && x != nil { 677 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 678 | if ms.LoadMessageInfo() == nil { 679 | ms.StoreMessageInfo(mi) 680 | } 681 | return ms 682 | } 683 | return mi.MessageOf(x) 684 | } 685 | 686 | // Deprecated: Use MsgContainer.ProtoReflect.Descriptor instead. 687 | func (*MsgContainer) Descriptor() ([]byte, []int) { 688 | return file_pb_nymo_proto_rawDescGZIP(), []int{12} 689 | } 690 | 691 | func (x *MsgContainer) GetMsg() []byte { 692 | if x != nil { 693 | return x.Msg 694 | } 695 | return nil 696 | } 697 | 698 | func (x *MsgContainer) GetPow() uint64 { 699 | if x != nil { 700 | return x.Pow 701 | } 702 | return 0 703 | } 704 | 705 | var File_pb_nymo_proto protoreflect.FileDescriptor 706 | 707 | var file_pb_nymo_proto_rawDesc = []byte{ 708 | 0x0a, 0x0d, 0x70, 0x62, 0x2f, 0x6e, 0x79, 0x6d, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 709 | 0x04, 0x6e, 0x79, 0x6d, 0x6f, 0x22, 0x53, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x48, 0x61, 0x6e, 710 | 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 711 | 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 712 | 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x68, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 713 | 0x52, 0x06, 0x63, 0x6f, 0x68, 0x6f, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x6f, 0x77, 0x18, 714 | 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x70, 0x6f, 0x77, 0x22, 0x25, 0x0a, 0x0b, 0x48, 0x61, 715 | 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x4f, 0x4b, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x68, 716 | 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x63, 0x6f, 0x68, 0x6f, 0x72, 717 | 0x74, 0x22, 0x34, 0x0a, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 718 | 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 719 | 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x68, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 720 | 0x06, 0x63, 0x6f, 0x68, 0x6f, 0x72, 0x74, 0x22, 0x2e, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4c, 721 | 0x69, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 722 | 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6e, 0x79, 0x6d, 0x6f, 0x2e, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 723 | 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x39, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x65, 724 | 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x72, 0x6c, 0x48, 0x61, 0x73, 725 | 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x75, 0x72, 0x6c, 0x48, 0x61, 0x73, 0x68, 726 | 0x12, 0x10, 0x0a, 0x03, 0x70, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x70, 727 | 0x6f, 0x77, 0x22, 0x28, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x50, 0x65, 728 | 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 729 | 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x33, 0x0a, 0x07, 730 | 0x4d, 0x73, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 731 | 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6e, 0x79, 0x6d, 0x6f, 732 | 0x2e, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 733 | 0x73, 0x22, 0x0c, 0x0a, 0x0a, 0x4d, 0x73, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x6b, 0x22, 734 | 0x20, 0x0a, 0x0a, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x73, 0x67, 0x12, 0x12, 0x0a, 735 | 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 736 | 0x68, 0x22, 0x5f, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x6c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 737 | 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 738 | 0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 739 | 0x6e, 0x64, 0x65, 0x72, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x65, 740 | 0x6e, 0x64, 0x65, 0x72, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6e, 0x64, 0x54, 0x69, 741 | 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x65, 0x6e, 0x64, 0x54, 0x69, 742 | 0x6d, 0x65, 0x22, 0x42, 0x0a, 0x10, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 743 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 744 | 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 745 | 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 746 | 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x71, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 747 | 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x68, 0x6f, 0x72, 748 | 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 749 | 0x6f, 0x68, 0x6f, 0x72, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 750 | 0x61, 0x6c, 0x50, 0x75, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x65, 0x70, 0x68, 751 | 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x6e, 0x63, 752 | 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x65, 753 | 0x6e, 0x63, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x32, 0x0a, 0x0c, 0x4d, 0x73, 0x67, 754 | 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 755 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x70, 756 | 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x70, 0x6f, 0x77, 0x42, 0x06, 0x5a, 757 | 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 758 | } 759 | 760 | var ( 761 | file_pb_nymo_proto_rawDescOnce sync.Once 762 | file_pb_nymo_proto_rawDescData = file_pb_nymo_proto_rawDesc 763 | ) 764 | 765 | func file_pb_nymo_proto_rawDescGZIP() []byte { 766 | file_pb_nymo_proto_rawDescOnce.Do(func() { 767 | file_pb_nymo_proto_rawDescData = protoimpl.X.CompressGZIP(file_pb_nymo_proto_rawDescData) 768 | }) 769 | return file_pb_nymo_proto_rawDescData 770 | } 771 | 772 | var file_pb_nymo_proto_msgTypes = make([]protoimpl.MessageInfo, 13) 773 | var file_pb_nymo_proto_goTypes = []interface{}{ 774 | (*PeerHandshake)(nil), // 0: nymo.PeerHandshake 775 | (*HandshakeOK)(nil), // 1: nymo.HandshakeOK 776 | (*Digest)(nil), // 2: nymo.Digest 777 | (*PeerList)(nil), // 3: nymo.PeerList 778 | (*RequestPeer)(nil), // 4: nymo.RequestPeer 779 | (*ResponsePeer)(nil), // 5: nymo.ResponsePeer 780 | (*MsgList)(nil), // 6: nymo.MsgList 781 | (*MsgListAck)(nil), // 7: nymo.MsgListAck 782 | (*RequestMsg)(nil), // 8: nymo.RequestMsg 783 | (*RealMessage)(nil), // 9: nymo.RealMessage 784 | (*EncryptedMessage)(nil), // 10: nymo.EncryptedMessage 785 | (*Message)(nil), // 11: nymo.Message 786 | (*MsgContainer)(nil), // 12: nymo.MsgContainer 787 | } 788 | var file_pb_nymo_proto_depIdxs = []int32{ 789 | 2, // 0: nymo.PeerList.peers:type_name -> nymo.Digest 790 | 2, // 1: nymo.MsgList.messages:type_name -> nymo.Digest 791 | 2, // [2:2] is the sub-list for method output_type 792 | 2, // [2:2] is the sub-list for method input_type 793 | 2, // [2:2] is the sub-list for extension type_name 794 | 2, // [2:2] is the sub-list for extension extendee 795 | 0, // [0:2] is the sub-list for field type_name 796 | } 797 | 798 | func init() { file_pb_nymo_proto_init() } 799 | func file_pb_nymo_proto_init() { 800 | if File_pb_nymo_proto != nil { 801 | return 802 | } 803 | if !protoimpl.UnsafeEnabled { 804 | file_pb_nymo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 805 | switch v := v.(*PeerHandshake); i { 806 | case 0: 807 | return &v.state 808 | case 1: 809 | return &v.sizeCache 810 | case 2: 811 | return &v.unknownFields 812 | default: 813 | return nil 814 | } 815 | } 816 | file_pb_nymo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 817 | switch v := v.(*HandshakeOK); i { 818 | case 0: 819 | return &v.state 820 | case 1: 821 | return &v.sizeCache 822 | case 2: 823 | return &v.unknownFields 824 | default: 825 | return nil 826 | } 827 | } 828 | file_pb_nymo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 829 | switch v := v.(*Digest); i { 830 | case 0: 831 | return &v.state 832 | case 1: 833 | return &v.sizeCache 834 | case 2: 835 | return &v.unknownFields 836 | default: 837 | return nil 838 | } 839 | } 840 | file_pb_nymo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 841 | switch v := v.(*PeerList); i { 842 | case 0: 843 | return &v.state 844 | case 1: 845 | return &v.sizeCache 846 | case 2: 847 | return &v.unknownFields 848 | default: 849 | return nil 850 | } 851 | } 852 | file_pb_nymo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { 853 | switch v := v.(*RequestPeer); i { 854 | case 0: 855 | return &v.state 856 | case 1: 857 | return &v.sizeCache 858 | case 2: 859 | return &v.unknownFields 860 | default: 861 | return nil 862 | } 863 | } 864 | file_pb_nymo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { 865 | switch v := v.(*ResponsePeer); i { 866 | case 0: 867 | return &v.state 868 | case 1: 869 | return &v.sizeCache 870 | case 2: 871 | return &v.unknownFields 872 | default: 873 | return nil 874 | } 875 | } 876 | file_pb_nymo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { 877 | switch v := v.(*MsgList); i { 878 | case 0: 879 | return &v.state 880 | case 1: 881 | return &v.sizeCache 882 | case 2: 883 | return &v.unknownFields 884 | default: 885 | return nil 886 | } 887 | } 888 | file_pb_nymo_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { 889 | switch v := v.(*MsgListAck); i { 890 | case 0: 891 | return &v.state 892 | case 1: 893 | return &v.sizeCache 894 | case 2: 895 | return &v.unknownFields 896 | default: 897 | return nil 898 | } 899 | } 900 | file_pb_nymo_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { 901 | switch v := v.(*RequestMsg); i { 902 | case 0: 903 | return &v.state 904 | case 1: 905 | return &v.sizeCache 906 | case 2: 907 | return &v.unknownFields 908 | default: 909 | return nil 910 | } 911 | } 912 | file_pb_nymo_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { 913 | switch v := v.(*RealMessage); i { 914 | case 0: 915 | return &v.state 916 | case 1: 917 | return &v.sizeCache 918 | case 2: 919 | return &v.unknownFields 920 | default: 921 | return nil 922 | } 923 | } 924 | file_pb_nymo_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { 925 | switch v := v.(*EncryptedMessage); i { 926 | case 0: 927 | return &v.state 928 | case 1: 929 | return &v.sizeCache 930 | case 2: 931 | return &v.unknownFields 932 | default: 933 | return nil 934 | } 935 | } 936 | file_pb_nymo_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { 937 | switch v := v.(*Message); i { 938 | case 0: 939 | return &v.state 940 | case 1: 941 | return &v.sizeCache 942 | case 2: 943 | return &v.unknownFields 944 | default: 945 | return nil 946 | } 947 | } 948 | file_pb_nymo_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { 949 | switch v := v.(*MsgContainer); i { 950 | case 0: 951 | return &v.state 952 | case 1: 953 | return &v.sizeCache 954 | case 2: 955 | return &v.unknownFields 956 | default: 957 | return nil 958 | } 959 | } 960 | } 961 | type x struct{} 962 | out := protoimpl.TypeBuilder{ 963 | File: protoimpl.DescBuilder{ 964 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 965 | RawDescriptor: file_pb_nymo_proto_rawDesc, 966 | NumEnums: 0, 967 | NumMessages: 13, 968 | NumExtensions: 0, 969 | NumServices: 0, 970 | }, 971 | GoTypes: file_pb_nymo_proto_goTypes, 972 | DependencyIndexes: file_pb_nymo_proto_depIdxs, 973 | MessageInfos: file_pb_nymo_proto_msgTypes, 974 | }.Build() 975 | File_pb_nymo_proto = out.File 976 | file_pb_nymo_proto_rawDesc = nil 977 | file_pb_nymo_proto_goTypes = nil 978 | file_pb_nymo_proto_depIdxs = nil 979 | } 980 | -------------------------------------------------------------------------------- /pb/nymo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package nymo; 3 | 4 | option go_package = "./pb"; 5 | 6 | message PeerHandshake { 7 | string version = 1; 8 | uint32 cohort = 2; 9 | uint64 pow = 3; 10 | } 11 | 12 | message HandshakeOK { 13 | uint32 cohort = 1; 14 | } 15 | 16 | message Digest { 17 | bytes hash = 1; 18 | uint32 cohort = 2; 19 | } 20 | 21 | message PeerList { 22 | repeated Digest peers = 1; 23 | } 24 | 25 | message RequestPeer { 26 | bytes urlHash = 1; 27 | uint64 pow = 2; 28 | } 29 | 30 | message ResponsePeer { 31 | string address = 1; 32 | } 33 | 34 | message MsgList { 35 | repeated Digest messages = 1; 36 | } 37 | 38 | message MsgListAck {} 39 | 40 | message RequestMsg { 41 | bytes hash = 1; 42 | } 43 | 44 | message RealMessage { 45 | bytes message = 1; 46 | bytes senderID = 2; // sender's public key 47 | int64 sendTime = 3; 48 | } 49 | 50 | message EncryptedMessage { 51 | bytes msg = 1; 52 | bytes signature = 2; // signed with sender's private key 53 | } 54 | 55 | message Message { 56 | uint32 targetCohort = 1; 57 | bytes ephemeralPub = 2; // ephemeral public key 58 | bytes encMessage = 3; // shared-key encrypted EncryptedMessage 59 | } 60 | 61 | message MsgContainer { 62 | bytes msg = 1; 63 | uint64 pow = 2; 64 | } -------------------------------------------------------------------------------- /peer.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math/rand" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | "unsafe" 13 | 14 | "github.com/nymo-net/nymo/pb" 15 | "google.golang.org/protobuf/proto" 16 | "google.golang.org/protobuf/types/known/anypb" 17 | ) 18 | 19 | type peer struct { 20 | ctx context.Context 21 | handle PeerHandle 22 | 23 | reader io.ReadCloser 24 | writer io.Writer 25 | cohort uint32 26 | peers []*pb.Digest 27 | key []byte 28 | 29 | queue chan proto.Message 30 | 31 | msgReq sync.Map 32 | msgQ chan struct{} 33 | peerReq sync.Map 34 | 35 | msgProc uint32 36 | } 37 | 38 | func (p *peer) sendProto(msg proto.Message) { 39 | select { 40 | case <-p.ctx.Done(): 41 | case p.queue <- msg: 42 | } 43 | } 44 | 45 | func (p *peer) requestMsg(diff []*pb.Digest, u *User) { 46 | count := 0 47 | for _, digest := range diff { 48 | // deal with out-of-cohort message 49 | if !sameCohort(u.cohort, digest.Cohort) { 50 | // should not receive if msg cohort is same as peer 51 | if sameCohort(p.cohort, digest.Cohort) { 52 | continue 53 | } 54 | 55 | // 1-ε chance of ignoring an out-of-cohort message 56 | if rand.Float64() >= epsilon { 57 | u.db.IgnoreMessage(digest) 58 | continue 59 | } 60 | } 61 | 62 | p.msgReq.Store(*(*[hashSize]byte)(unsafe.Pointer(&digest.Hash[0])), digest.Cohort) 63 | p.sendProto(&pb.RequestMsg{Hash: digest.Hash}) 64 | count++ 65 | } 66 | 67 | for count > 0 { 68 | if _, ok := <-p.msgQ; !ok { 69 | break 70 | } 71 | count-- 72 | } 73 | atomic.StoreUint32(&p.msgProc, 0) 74 | p.sendProto(new(pb.MsgListAck)) 75 | } 76 | 77 | func (p *peer) requestPeer(pred func(uint32) bool) bool { 78 | for i, digest := range p.peers { 79 | if !pred(digest.Cohort) { 80 | continue 81 | } 82 | p.peerReq.Store(truncateHash(digest.Hash), digest) 83 | p.peers = append(p.peers[:i], p.peers[i+1:]...) 84 | p.sendProto(&pb.RequestPeer{ 85 | UrlHash: digest.Hash, 86 | Pow: calcPoW(append(p.key, digest.Hash...)), 87 | }) 88 | return true 89 | } 90 | return false 91 | } 92 | 93 | func (u *User) peerDownlink(p *peer) error { 94 | defer close(p.msgQ) 95 | defer p.reader.Close() 96 | 97 | var listTimer unsafe.Pointer 98 | 99 | defer func() { 100 | t := (*time.Timer)(atomic.SwapPointer(&listTimer, nil)) 101 | if t != nil { 102 | t.Stop() 103 | } 104 | }() 105 | 106 | listMsg := func() { 107 | if atomic.SwapPointer(&listTimer, nil) != nil { 108 | p.sendProto(&pb.MsgList{Messages: p.handle.ListMessages(msgListMax)}) 109 | } 110 | } 111 | 112 | for p.ctx.Err() == nil { 113 | var any anypb.Any 114 | err := recvMessage(p.reader, &any) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | n, err := any.UnmarshalNew() 120 | if err != nil { 121 | return err 122 | } 123 | 124 | switch msg := n.(type) { 125 | case *pb.RequestMsg: 126 | if len(msg.Hash) != hashSize { 127 | return fmt.Errorf("unexpected hash length") 128 | } 129 | cont := new(pb.MsgContainer) 130 | cont.Msg, cont.Pow = u.db.GetMessage(copyHash(msg.Hash)) 131 | p.sendProto(cont) 132 | case *pb.RequestPeer: 133 | if len(msg.UrlHash) != hashTruncate { 134 | return fmt.Errorf("unexpected hash length") 135 | } 136 | if !validatePoW(append(p.key, msg.UrlHash...), msg.Pow) { 137 | return fmt.Errorf("invalid pow") 138 | } 139 | p.sendProto(&pb.ResponsePeer{Address: u.db.GetUrlByHash(truncateHash(msg.UrlHash))}) 140 | case *pb.ResponsePeer: 141 | hash := hasher([]byte(msg.Address)) 142 | digest, loaded := p.peerReq.LoadAndDelete(truncateHash(hash[:])) 143 | if !loaded { 144 | return fmt.Errorf("unexpected peer response") 145 | } 146 | u.db.AddPeer(msg.Address, digest.(*pb.Digest)) 147 | case *pb.PeerList: 148 | if p.peers != nil { 149 | return fmt.Errorf("unexpected peer list") 150 | } 151 | for _, l := range msg.Peers { 152 | if len(l.Hash) != hashTruncate || l.Cohort > cohortNumber { 153 | return fmt.Errorf("unexpected hash length") 154 | } 155 | } 156 | p.peers = p.handle.AddKnownPeers(msg.Peers) 157 | case *pb.MsgList: 158 | if atomic.LoadUint32(&p.msgProc) != 0 { 159 | return fmt.Errorf("unexpected msg list") 160 | } 161 | for _, l := range msg.Messages { 162 | if len(l.Hash) != hashSize || l.Cohort >= cohortNumber { 163 | return fmt.Errorf("unexpected hash length") 164 | } 165 | } 166 | p.msgProc = 1 167 | go p.requestMsg(p.handle.AddKnownMessages(msg.Messages), u) 168 | case *pb.MsgListAck: 169 | if atomic.LoadPointer(&listTimer) != nil { 170 | return errors.New("unexpected peer msg ack") 171 | } 172 | p.handle.AckMessages() 173 | atomic.StorePointer(&listTimer, unsafe.Pointer(time.AfterFunc(u.cfg.ListMessageTime, listMsg))) 174 | case *pb.MsgContainer: 175 | if msg.Msg == nil { 176 | return fmt.Errorf("no msg") 177 | } 178 | // 1. retrieve request 179 | msgHash := hasher(msg.Msg) 180 | expCohort, loaded := p.msgReq.LoadAndDelete(msgHash) 181 | if !loaded { 182 | return fmt.Errorf("unexpected msg response") 183 | } 184 | p.msgQ <- struct{}{} 185 | err := u.db.StoreMessage(msgHash, msg, func() (uint32, error) { 186 | // 2. validate pow 187 | if !validatePoW(msgHash[:], msg.Pow) { 188 | return 0, errors.New("invalid pow") 189 | } 190 | // 3. try decode 191 | var m pb.Message 192 | err := proto.Unmarshal(msg.Msg, &m) 193 | if err != nil { 194 | return 0, err 195 | } 196 | if m.TargetCohort != expCohort.(uint32) { 197 | return 0, errors.New("unexpected pow") 198 | } 199 | // 4. try decrypt and store 200 | if m.TargetCohort == u.cohort { 201 | if rMsg := u.decryptMessage(&m); rMsg != nil { 202 | u.db.StoreDecryptedMessage(rMsg) 203 | } 204 | } 205 | return m.TargetCohort, nil 206 | }) 207 | if err != nil { 208 | return err 209 | } 210 | default: 211 | return fmt.Errorf("unknown message type %T", msg) 212 | } 213 | } 214 | 215 | return p.ctx.Err() 216 | } 217 | 218 | func (u *User) peerUplink(p *peer) { 219 | defer p.reader.Close() 220 | 221 | for { 222 | var msg proto.Message 223 | select { 224 | case msg = <-p.queue: 225 | case <-p.ctx.Done(): 226 | return 227 | } 228 | 229 | any, err := anypb.New(msg) 230 | if err != nil { 231 | panic(err) 232 | } 233 | 234 | err = sendMessage(p.writer, any) 235 | if err != nil { 236 | p.handle.Disconnect(err) 237 | return 238 | } 239 | } 240 | } 241 | 242 | func (u *User) runPeer(p *peer) *peer { 243 | p.queue = make(chan proto.Message, 10) 244 | p.queue <- &pb.PeerList{Peers: p.handle.ListPeers(peerListMax)} 245 | p.queue <- &pb.MsgList{Messages: p.handle.ListMessages(msgListMax)} 246 | go func() { 247 | p.handle.Disconnect(u.peerDownlink(p)) 248 | }() 249 | go u.peerUplink(p) 250 | return p 251 | } 252 | 253 | func (u *User) newPeerAsServer( 254 | ctx context.Context, handle PeerHandle, 255 | r io.ReadCloser, w io.Writer, 256 | material []byte, cohort uint32) (*peer, error) { 257 | if err := sendMessage(w, &pb.HandshakeOK{ 258 | Cohort: u.cohort, 259 | }); err != nil { 260 | return nil, err 261 | } 262 | 263 | return u.runPeer(&peer{ 264 | ctx: ctx, 265 | handle: handle, 266 | reader: r, 267 | writer: w, 268 | cohort: cohort, 269 | key: material, 270 | msgQ: make(chan struct{}, 100), 271 | }), nil 272 | } 273 | 274 | func (u *User) newPeerAsClient( 275 | ctx context.Context, handle PeerEnumerate, 276 | r io.ReadCloser, w io.Writer, 277 | id [hashTruncate]byte, sKey []byte) (*peer, error) { 278 | var ok pb.HandshakeOK 279 | if err := recvMessage(r, &ok); err != nil { 280 | return nil, err 281 | } 282 | 283 | return u.runPeer(&peer{ 284 | ctx: ctx, 285 | handle: handle.Connect(id, ok.Cohort), 286 | reader: r, 287 | writer: w, 288 | cohort: ok.Cohort, 289 | key: sKey, 290 | msgQ: make(chan struct{}, 100), 291 | }), nil 292 | } 293 | -------------------------------------------------------------------------------- /peer_reserver.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | type serverReserver struct { 4 | u *User 5 | cohort uint32 6 | id *[hashTruncate]byte 7 | } 8 | 9 | func (s *serverReserver) reserveId(id *[hashTruncate]byte) bool { 10 | if s.id != nil { 11 | panic("multiple reservation") 12 | } 13 | 14 | if !s.u.reserveId(*id) { 15 | return false 16 | } 17 | 18 | s.id = id 19 | return true 20 | } 21 | 22 | func (s *serverReserver) rollback() { 23 | u := s.u 24 | if u == nil { 25 | return 26 | } 27 | 28 | u.peerLock.Lock() 29 | defer u.peerLock.Unlock() 30 | 31 | if s.id != nil { 32 | delete(u.peers, *s.id) 33 | } 34 | 35 | u.total-- 36 | if u.peerSameCohort(s.cohort) { 37 | u.numIn-- 38 | } 39 | s.u = nil 40 | } 41 | 42 | func (s *serverReserver) commit(p *peer) { 43 | u := s.u 44 | u.peerLock.Lock() 45 | defer u.peerLock.Unlock() 46 | 47 | if !sameCohort(p.cohort, s.cohort) && u.peerSameCohort(s.cohort) { 48 | // bad path, the peer's cohort changed, and 49 | // originally we thought it was in-cohort 50 | u.numIn-- 51 | } 52 | 53 | u.peers[*s.id] = p 54 | s.u = nil 55 | } 56 | 57 | type clientReserver struct { 58 | u *User 59 | cohort *uint32 60 | id [hashTruncate]byte 61 | } 62 | 63 | func (c *clientReserver) reserveCohort(cohort uint32) bool { 64 | if c.cohort != nil { 65 | panic("multiple reservation") 66 | } 67 | 68 | if !c.u.reserveCohort(cohort) { 69 | return false 70 | } 71 | c.cohort = &cohort 72 | return true 73 | } 74 | 75 | func (c *clientReserver) rollback() { 76 | u := c.u 77 | if u == nil { 78 | return 79 | } 80 | 81 | u.peerLock.Lock() 82 | defer u.peerLock.Unlock() 83 | 84 | if c.cohort != nil { 85 | u.total-- 86 | if u.peerSameCohort(*c.cohort) { 87 | u.numIn-- 88 | } 89 | } 90 | 91 | delete(u.peers, c.id) 92 | c.u = nil 93 | } 94 | 95 | func (c *clientReserver) commit(p *peer) { 96 | u := c.u 97 | 98 | u.peerLock.Lock() 99 | defer u.peerLock.Unlock() 100 | 101 | u.peers[c.id] = p 102 | c.u = nil 103 | } 104 | 105 | func (u *User) shouldConnectPeers() bool { 106 | u.peerLock.RLock() 107 | defer u.peerLock.RUnlock() 108 | 109 | return u.total < u.cfg.MaxInCohortConn+u.cfg.MaxOutCohortConn 110 | } 111 | 112 | func (u *User) peerCleanup() { 113 | u.peerLock.Lock() 114 | defer u.peerLock.Unlock() 115 | 116 | for k, p := range u.peers { 117 | if p == nil { 118 | continue 119 | } 120 | if p.ctx.Err() != nil { 121 | delete(u.peers, k) 122 | u.total-- 123 | if u.peerSameCohort(p.cohort) { 124 | u.numIn-- 125 | } 126 | } 127 | } 128 | } 129 | 130 | func (u *User) reserveCohort(cohort uint32) bool { 131 | u.peerLock.Lock() 132 | defer u.peerLock.Unlock() 133 | 134 | if u.peerSameCohort(cohort) { 135 | if u.numIn >= u.cfg.MaxInCohortConn { 136 | return false 137 | } 138 | u.numIn++ 139 | } else { 140 | if u.total-u.numIn >= u.cfg.MaxOutCohortConn { 141 | return false 142 | } 143 | } 144 | u.total++ 145 | return true 146 | } 147 | 148 | func (u *User) reserveId(id [hashTruncate]byte) bool { 149 | u.peerLock.Lock() 150 | defer u.peerLock.Unlock() 151 | 152 | if _, ok := u.peers[id]; ok { 153 | return false 154 | } 155 | 156 | u.peers[id] = nil 157 | return true 158 | } 159 | 160 | func (u *User) reserveServer(cohort uint32) *serverReserver { 161 | if !u.reserveCohort(cohort) { 162 | return nil 163 | } 164 | return &serverReserver{ 165 | u: u, 166 | cohort: cohort, 167 | } 168 | } 169 | 170 | func (u *User) reserveClient(id [hashTruncate]byte) *clientReserver { 171 | if !u.reserveId(id) { 172 | return nil 173 | } 174 | return &clientReserver{ 175 | u: u, 176 | id: id, 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /pow.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "math" 7 | "math/big" 8 | ) 9 | 10 | func calcPoW(data []byte) uint64 { 11 | dataLen := len(data) 12 | powData := make([]byte, dataLen+8) 13 | copy(powData, data) 14 | 15 | var counter uint64 16 | var bInt big.Int 17 | for counter < math.MaxUint64 { 18 | encoding.PutUint64(powData[dataLen:], counter) 19 | hash := hasher(powData) 20 | bInt.SetBytes(hash[:]) 21 | if bInt.BitLen() < bitStrength { 22 | break 23 | } 24 | counter++ 25 | } 26 | 27 | return counter 28 | } 29 | 30 | func validatePoW(data []byte, pow uint64) bool { 31 | buf := bytes.NewBuffer(data) 32 | _ = binary.Write(buf, encoding, pow) 33 | var bInt big.Int 34 | hash := hasher(buf.Bytes()) 35 | bInt.SetBytes(hash[:]) 36 | return bInt.BitLen() < bitStrength 37 | } 38 | -------------------------------------------------------------------------------- /pow_test.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | func TestPoW(t *testing.T) { 9 | buf := make([]byte, 32) 10 | _, err := rand.Read(buf) 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | w := calcPoW(buf) 16 | if !validatePoW(buf, w) { 17 | t.Error("invalid pow") 18 | } 19 | } 20 | 21 | func TestPoWRandom(t *testing.T) { 22 | buf := make([]byte, 32) 23 | _, err := rand.Read(buf) 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | 28 | if validatePoW(buf, rand.Uint64()) { 29 | t.Error("invalid pow") 30 | } 31 | } -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "math" 11 | "net" 12 | "net/http" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/lucas-clemente/quic-go/http3" 18 | "github.com/nymo-net/nymo/pb" 19 | "google.golang.org/protobuf/proto" 20 | ) 21 | 22 | type server struct { 23 | user *User 24 | } 25 | 26 | func validate(r *http.Request) (*pb.PeerHandshake, []byte) { 27 | if r.Method != http.MethodPost { 28 | return nil, nil 29 | } 30 | if r.URL.Path != "/" { 31 | return nil, nil 32 | } 33 | 34 | auth := r.Header.Get("Authorization") 35 | const prefix = "Basic " 36 | if !strings.HasPrefix(auth, prefix) { 37 | return nil, nil 38 | } 39 | 40 | c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) 41 | if err != nil { 42 | return nil, nil 43 | } 44 | 45 | p := new(pb.PeerHandshake) 46 | err = proto.Unmarshal(c, p) 47 | if err != nil { 48 | return nil, nil 49 | } 50 | 51 | if p.Version != nymoVersion { 52 | return nil, nil 53 | } 54 | 55 | material, err := r.TLS.ExportKeyingMaterial(nymoName, nil, hashSize) 56 | if err != nil { 57 | return nil, nil 58 | } 59 | 60 | if validatePoW(material, p.Pow) { 61 | return p, material 62 | } 63 | return nil, nil 64 | } 65 | 66 | func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 67 | if len(r.TLS.PeerCertificates) <= 0 { 68 | http.DefaultServeMux.ServeHTTP(w, r) 69 | return 70 | } 71 | 72 | certHash := hasher(r.TLS.PeerCertificates[0].Raw) 73 | reserver := s.user.reserveClient(truncateHash(certHash[:])) 74 | if reserver == nil { 75 | w.WriteHeader(http.StatusTeapot) 76 | return 77 | } 78 | defer reserver.rollback() 79 | 80 | handshake, material := validate(r) 81 | if handshake == nil { 82 | http.DefaultServeMux.ServeHTTP(w, r) 83 | return 84 | } 85 | if !reserver.reserveCohort(handshake.Cohort) { 86 | w.WriteHeader(http.StatusTeapot) 87 | return 88 | } 89 | handle := s.user.db.ClientHandle(truncateHash(certHash[:])) 90 | 91 | w.WriteHeader(http.StatusOK) 92 | p, err := s.user.newPeerAsServer( 93 | r.Context(), handle, r.Body, &writeFlusher{w, w.(http.Flusher)}, material, handshake.Cohort) 94 | if err != nil { 95 | handle.Disconnect(err) 96 | return 97 | } 98 | 99 | reserver.commit(p) 100 | <-p.ctx.Done() 101 | } 102 | 103 | // RunServerUpnp discover Upnp server for NAT traversal. If successful, a new Nymo peer server will 104 | // listen on serverAddr and the server address mapped external IP address and port. 105 | func (u *User) RunServerUpnp(ctx context.Context, serverAddr string) error { 106 | var protocol string 107 | switch { 108 | case strings.HasPrefix(serverAddr, "udp://"): 109 | protocol = "UDP" 110 | case strings.HasPrefix(serverAddr, "tcp://"): 111 | protocol = "TCP" 112 | default: 113 | return fmt.Errorf("%s: unsupported address format", serverAddr) 114 | } 115 | 116 | addr := serverAddr[6:] 117 | host, portS, err := net.SplitHostPort(addr) 118 | if err != nil { 119 | return err 120 | } 121 | port, err := strconv.Atoi(portS) 122 | if err != nil { 123 | return err 124 | } 125 | if port <= 0 || port > math.MaxUint16 { 126 | return errors.New("port number out of range") 127 | } 128 | portS = strconv.Itoa(port) 129 | portU := uint16(port) 130 | 131 | client, err := pickRouterClient(ctx) 132 | if err != nil { 133 | return err 134 | } 135 | extAddr, err := client.GetExternalIPAddressCtx(ctx) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | err = client.AddPortMappingCtx(ctx, "", portU, protocol, portU, host, true, nymoName, 3600) 141 | if err != nil { 142 | return err 143 | } 144 | defer client.DeletePortMappingCtx(context.Background(), "", portU, protocol) 145 | 146 | ctx, cancel := context.WithCancel(ctx) 147 | 148 | go func() { 149 | defer cancel() 150 | 151 | ticker := time.NewTicker(time.Second * 3000) 152 | defer ticker.Stop() 153 | 154 | for { 155 | select { 156 | case <-ctx.Done(): 157 | return 158 | case <-ticker.C: 159 | err = client.AddPortMappingCtx(ctx, "", portU, protocol, portU, host, true, nymoName, 3600) 160 | if err != nil { 161 | u.cfg.Logger.Print(err) 162 | return 163 | } 164 | } 165 | } 166 | }() 167 | 168 | switch protocol { 169 | case "UDP": 170 | serverAddr = "udp://" + net.JoinHostPort(extAddr, portS) 171 | case "TCP": 172 | serverAddr = "tcp://" + net.JoinHostPort(extAddr, portS) 173 | } 174 | 175 | return u.RunServer(ctx, serverAddr, addr) 176 | } 177 | 178 | // RunServer runs a Nymo peer server with the given serverAddr and listenAddr, controlled by the ctx. 179 | func (u *User) RunServer(ctx context.Context, serverAddr, listenAddr string) error { 180 | srv := &http.Server{ 181 | Addr: listenAddr, 182 | Handler: &server{user: u}, 183 | TLSConfig: &tls.Config{ 184 | Certificates: []tls.Certificate{u.cert}, 185 | ClientAuth: tls.RequestClientCert, 186 | }, 187 | BaseContext: func(listener net.Listener) context.Context { 188 | return ctx 189 | }, 190 | ErrorLog: u.cfg.Logger, 191 | } 192 | var cl io.Closer = srv 193 | go func() { 194 | <-ctx.Done() 195 | _ = cl.Close() 196 | }() 197 | 198 | hash := hasher([]byte(serverAddr)) 199 | switch { 200 | case strings.HasPrefix(serverAddr, "udp://"): 201 | u.retry.addSelf(serverAddr) 202 | u.db.AddPeer(serverAddr, &pb.Digest{ 203 | Hash: hash[:hashTruncate], 204 | Cohort: u.cohort, 205 | }) 206 | s := &http3.Server{Server: srv} 207 | cl = s 208 | return s.ListenAndServe() 209 | case strings.HasPrefix(serverAddr, "tcp://"): 210 | u.retry.addSelf(serverAddr) 211 | u.db.AddPeer(serverAddr, &pb.Digest{ 212 | Hash: hash[:hashTruncate], 213 | Cohort: u.cohort, 214 | }) 215 | return srv.ListenAndServeTLS("", "") 216 | default: 217 | return fmt.Errorf("%s: unknown address format", serverAddr) 218 | } 219 | } 220 | 221 | // ListServers lists all currently listening servers. 222 | func (u *User) ListServers() (ret []string) { 223 | u.retry.l.Lock() 224 | defer u.retry.l.Unlock() 225 | 226 | for k, v := range u.retry.m { 227 | if v == emptyTime { 228 | ret = append(ret, k) 229 | } 230 | } 231 | 232 | return 233 | } 234 | -------------------------------------------------------------------------------- /upnp.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/huin/goupnp/dcps/internetgateway2" 8 | "golang.org/x/sync/errgroup" 9 | ) 10 | 11 | type routerClient interface { 12 | AddPortMappingCtx( 13 | ctx context.Context, 14 | NewRemoteHost string, 15 | NewExternalPort uint16, 16 | NewProtocol string, 17 | NewInternalPort uint16, 18 | NewInternalClient string, 19 | NewEnabled bool, 20 | NewPortMappingDescription string, 21 | NewLeaseDuration uint32, 22 | ) (err error) 23 | 24 | DeletePortMappingCtx( 25 | ctx context.Context, 26 | NewRemoteHost string, 27 | NewExternalPort uint16, 28 | NewProtocol string, 29 | ) (err error) 30 | 31 | GetExternalIPAddressCtx( 32 | ctx context.Context, 33 | ) (NewExternalIPAddress string, err error) 34 | } 35 | 36 | func pickRouterClient(ctx context.Context) (routerClient, error) { 37 | tasks, _ := errgroup.WithContext(ctx) 38 | 39 | var ip1Clients []*internetgateway2.WANIPConnection1 40 | tasks.Go(func() (err error) { 41 | ip1Clients, _, err = internetgateway2.NewWANIPConnection1Clients() 42 | return 43 | }) 44 | var ip2Clients []*internetgateway2.WANIPConnection2 45 | tasks.Go(func() (err error) { 46 | ip2Clients, _, err = internetgateway2.NewWANIPConnection2Clients() 47 | return 48 | }) 49 | var ppp1Clients []*internetgateway2.WANPPPConnection1 50 | tasks.Go(func() (err error) { 51 | ppp1Clients, _, err = internetgateway2.NewWANPPPConnection1Clients() 52 | return 53 | }) 54 | 55 | if err := tasks.Wait(); err != nil { 56 | return nil, err 57 | } 58 | 59 | switch { 60 | case len(ip2Clients) >= 1: 61 | return ip2Clients[0], nil 62 | case len(ip1Clients) >= 1: 63 | return ip1Clients[0], nil 64 | case len(ppp1Clients) >= 1: 65 | return ppp1Clients[0], nil 66 | default: 67 | return nil, errors.New("no services found") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "crypto/tls" 7 | "math/big" 8 | "sync" 9 | "time" 10 | 11 | "github.com/nymo-net/nymo/pb" 12 | ) 13 | 14 | type User struct { 15 | cfg Config 16 | db Database 17 | cohort uint32 18 | key *ecdsa.PrivateKey 19 | cert tls.Certificate 20 | 21 | peerLock sync.RWMutex 22 | peers map[[hashTruncate]byte]*peer 23 | total uint 24 | numIn uint 25 | retry peerRetrier 26 | } 27 | 28 | // Address returns the address of the user. 29 | func (u *User) Address() *Address { 30 | return &Address{ 31 | cohort: u.cohort, 32 | x: u.key.X, 33 | y: u.key.Y, 34 | } 35 | } 36 | 37 | // Run runs the main loop for user as a Nymo peer client. 38 | func (u *User) Run(ctx context.Context) { 39 | if u.cfg.LocalPeerAnnounce { 40 | go func() { 41 | if e := u.ipv4PeerAnnounce(ctx); e != nil { 42 | u.cfg.Logger.Print(e) 43 | } 44 | }() 45 | go func() { 46 | if e := u.ipv6PeerAnnounce(ctx); e != nil { 47 | u.cfg.Logger.Print(e) 48 | } 49 | }() 50 | } 51 | if u.cfg.LocalPeerDiscover { 52 | go func() { 53 | if e := u.ipv4PeerDiscover(ctx); e != nil { 54 | u.cfg.Logger.Print(e) 55 | } 56 | }() 57 | go func() { 58 | if e := u.ipv6PeerDiscover(ctx); e != nil { 59 | u.cfg.Logger.Print(e) 60 | } 61 | }() 62 | } 63 | 64 | for ctx.Err() == nil { 65 | u.dialNewPeers(ctx) 66 | t := time.NewTimer(u.cfg.ScanPeerTime) 67 | select { 68 | case <-t.C: 69 | case <-ctx.Done(): 70 | t.Stop() 71 | return 72 | } 73 | } 74 | } 75 | 76 | // AddPeer adds a peer URL, which computes the hash of the URL and synchronously calls Database.AddPeer. 77 | func (u *User) AddPeer(url string) { 78 | hash := hasher([]byte(url)) 79 | u.db.AddPeer(url, &pb.Digest{ 80 | Hash: hash[:hashTruncate], 81 | Cohort: cohortNumber, // XXX: when unknown, as wildcard 82 | }) 83 | } 84 | 85 | // OpenSupernode opens a supernode (a relay node). For arguments usage see OpenUser. 86 | func OpenSupernode(db Database, cert tls.Certificate, cfg *Config) *User { 87 | if cfg == nil { 88 | cfg = DefaultConfig() 89 | } 90 | 91 | hash := hasher(cert.Certificate[0]) 92 | return &User{ 93 | cfg: *cfg, 94 | db: db, 95 | cohort: cohortNumber, 96 | cert: cert, 97 | peers: map[[hashTruncate]byte]*peer{truncateHash(hash[:]): nil}, 98 | retry: peerRetrier{m: make(map[string]time.Time)}, 99 | } 100 | } 101 | 102 | // OpenUser opens a user generated by GenerateUser, where the userKey argument should be 103 | // exactly what GenerateUser returned. 104 | // 105 | // cert should contain a valid certificate that is used as the mTLS cert (on both client and server side). 106 | // 107 | // if cfg is nil, DefaultConfig will be used. 108 | func OpenUser(db Database, userKey []byte, cert tls.Certificate, cfg *Config) *User { 109 | if cfg == nil { 110 | cfg = DefaultConfig() 111 | } 112 | 113 | key := new(ecdsa.PrivateKey) 114 | key.Curve = curve 115 | key.D = new(big.Int).SetBytes(userKey) 116 | key.X, key.Y = curve.ScalarBaseMult(userKey) 117 | 118 | hash := hasher(cert.Certificate[0]) 119 | return &User{ 120 | cfg: *cfg, 121 | db: db, 122 | cohort: getCohort(key.X, key.Y), 123 | key: key, 124 | cert: cert, 125 | peers: map[[hashTruncate]byte]*peer{truncateHash(hash[:]): nil}, 126 | retry: peerRetrier{m: make(map[string]time.Time)}, 127 | } 128 | } 129 | 130 | // GenerateUser generates a new Nymo user. The returned key is opaque to the frontend 131 | // and should be stored secretly. 132 | func GenerateUser() ([]byte, error) { 133 | key, err := ecdsa.GenerateKey(curve, cReader) 134 | if err != nil { 135 | return nil, err 136 | } 137 | return key.D.Bytes(), nil 138 | } 139 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package nymo 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "encoding/binary" 7 | "io" 8 | "net/http" 9 | "sync" 10 | "time" 11 | "unsafe" 12 | 13 | "google.golang.org/protobuf/proto" 14 | ) 15 | 16 | func sameCohort(target, pov uint32) bool { 17 | return pov == target || target == cohortNumber 18 | } 19 | 20 | func (u *User) peerSameCohort(peer uint32) bool { 21 | return sameCohort(peer, u.cohort) 22 | } 23 | 24 | func sendMessage(conn io.Writer, m proto.Message) error { 25 | data, err := proto.Marshal(m) 26 | if err != nil { 27 | return err 28 | } 29 | if len(data) > maxPacketSize { 30 | panic("exceed size") 31 | } 32 | 33 | buf := make([]byte, uint16Size+len(data)) 34 | encoding.PutUint16(buf, uint16(len(data))) 35 | copy(buf[uint16Size:], data) 36 | 37 | _, err = conn.Write(buf) 38 | return err 39 | } 40 | 41 | func recvMessage(conn io.Reader, m proto.Message) error { 42 | var size uint16 43 | err := binary.Read(conn, encoding, &size) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | buf := make([]byte, size) 49 | _, err = io.ReadFull(conn, buf) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | return proto.Unmarshal(buf, m) 55 | } 56 | 57 | type writeFlusher struct { 58 | w io.Writer 59 | f http.Flusher 60 | } 61 | 62 | func (w *writeFlusher) Write(p []byte) (n int, err error) { 63 | defer w.f.Flush() 64 | return w.w.Write(p) 65 | } 66 | 67 | func padBlock(input []byte) []byte { 68 | pad := (len(input)/aes.BlockSize+1)*aes.BlockSize - len(input) 69 | return append(input, bytes.Repeat([]byte{byte(pad)}, pad)...) 70 | } 71 | 72 | func trimBlock(input []byte) []byte { 73 | c := input[len(input)-1] 74 | b := int(c) 75 | if b <= 0 || b > aes.BlockSize { 76 | return nil 77 | } 78 | for i := len(input) - b; i < len(input)-1; i++ { 79 | if input[i] != c { 80 | return nil 81 | } 82 | } 83 | return input[:len(input)-b] 84 | } 85 | 86 | func truncateHash(hash []byte) [hashTruncate]byte { 87 | if len(hash) < hashTruncate { 88 | panic("out of bounds") 89 | } 90 | return *(*[hashTruncate]byte)(unsafe.Pointer(&hash[0])) 91 | } 92 | 93 | func copyHash(hash []byte) [hashSize]byte { 94 | if len(hash) < hashSize { 95 | panic("out of bounds") 96 | } 97 | return *(*[hashSize]byte)(unsafe.Pointer(&hash[0])) 98 | } 99 | 100 | type peerRetrier struct { 101 | l sync.Mutex 102 | m map[string]time.Time 103 | } 104 | 105 | func (p *peerRetrier) addSelf(url string) { 106 | p.l.Lock() 107 | defer p.l.Unlock() 108 | 109 | p.m[url] = emptyTime 110 | } 111 | 112 | func (p *peerRetrier) add(url string, timeout time.Duration) { 113 | ddl := time.Now().Add(timeout) 114 | 115 | p.l.Lock() 116 | defer p.l.Unlock() 117 | 118 | t, ok := p.m[url] 119 | if !ok || (t != emptyTime && t.Before(ddl)) { 120 | p.m[url] = ddl 121 | } 122 | } 123 | 124 | func (p *peerRetrier) noRetry(url string) bool { 125 | p.l.Lock() 126 | defer p.l.Unlock() 127 | 128 | t, ok := p.m[url] 129 | if !ok { 130 | return false 131 | } 132 | if t == emptyTime || time.Until(t) > 0 { 133 | return true 134 | } 135 | delete(p.m, url) 136 | return false 137 | } 138 | --------------------------------------------------------------------------------