├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── .vscode ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── adapter.go ├── adapter_io.go ├── crypt ├── crypt.go └── crypt_test.go ├── emitter.go ├── example_test.go ├── go.mod ├── go.sum ├── idea ├── logo-1.svg ├── logo-2.svg └── middleware-overview.svg ├── log.go ├── message.go ├── middleware.go ├── middleware_crypt.go ├── middleware_headers.go ├── middleware_log.go ├── middleware_routes.go ├── mock_Adapter_test.go ├── mock_extensions_test.go ├── network_connection.go ├── network_connection_builder.go ├── network_connection_test.go ├── peer.go ├── peer_operator.go ├── peers.go ├── pipe.go ├── pipe_test.go ├── tcp_adapter.go ├── tcp_operator.go └── tcp_operator_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.js] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.scss] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.yaml] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve go2p 4 | 5 | --- 6 | 7 | **Problem Description:** 8 | 9 | **Steps to reproduce:** 10 | 11 | **Environment:** 12 | - OS: [e.g. MacOS, Windows] 13 | - Version of go2p: [e.g. 2.0] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for go2p 4 | 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Checklist 4 | - [ ] I've tested my changes. 5 | - [ ] I've read the [Contribution Guidelines](https://github.com/v-braun/go2p/blob/master/CONTRIBUTING.md). 6 | - [ ] I've updated the documentation if necessary. 7 | 8 | ### Motivation and Context 9 | 10 | 11 | 12 | 13 | 14 | ### Description 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | coverage.txt 15 | 16 | vendor 17 | 18 | bin -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # template file from: 2 | # https://gist.github.com/y0ssar1an/df2dab474520c4086926f672c52db139 3 | 4 | language: go 5 | 6 | # Force-enable Go modules. This will be unnecessary when Go 1.12 lands. 7 | env: 8 | - GO111MODULE=on 9 | 10 | # You don't need to test on very old version of the Go compiler. It's the user's 11 | # responsibility to keep their compilers up to date. 12 | go: 13 | - 1.12.x 14 | 15 | # Only clone the most recent commit. 16 | git: 17 | depth: 1 18 | 19 | # caching for gopath and pkg 20 | cache: 21 | directories: 22 | - $HOME/.cache/go-build 23 | - $HOME/gopath/pkg/mod 24 | 25 | # Don't email me the results of the test runs. 26 | notifications: 27 | email: false 28 | 29 | script: 30 | - make ci 31 | 32 | after_success: 33 | - bash <(curl -s https://codecov.io/bash) 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Wrapf", 4 | "deserialize", 5 | "emirpasic", 6 | "readed", 7 | "uuid" 8 | ] 9 | 10 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build", 6 | "type": "shell", 7 | "command": "go build ./...", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | } 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mail@viktor-braun.de. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to go2p 2 | 3 | Thank you for taking the time to contribute **go2p**! ❤️ 4 | 5 | ## I want to report a problem or ask a question 6 | 7 | - Read [the README](https://github.com/v-braun/go2p/blob/master/README.md). 8 | - Search for existing [issues](https://github.com/v-braun/go2p/issues). 9 | 10 | If the above doesn't help, please [submit an issue](https://github.com/v-braun/go2p/issues). 11 | 12 | ## I want to contribute to go2p 13 | 14 | ### Checking out the repository 15 | 16 | 1. Click the “Fork” button in the upper right corner of repo 17 | 2. Clone your fork: 18 | - `git clone https://github.com//go2p.git` 19 | 3. Create a new branch to work on: 20 | - `git checkout -b ` 21 | 4. Commit your changes 22 | - `git commit -am 'Add awesome feature'` 23 | 5. Push your changes 24 | - `git push origin ` 25 | 6. Create a new Pull Request 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Viktor Braun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # VERSION := $(shell git describe --tags) 2 | # BUILD := $(shell git rev-parse --short HEAD) 3 | PROJECTNAME := $(shell basename "$(PWD)") 4 | 5 | # Go related variables. 6 | GOBASE := $(shell pwd) 7 | GOPATH := $(GOBASE)/vendor:$(GOBASE) 8 | GOBIN := $(GOBASE)/bin 9 | GOFILES := $(wildcard *.go) 10 | 11 | WATCH_ADDR := localhost:7999 12 | 13 | # Use linker flags to provide version/build settings 14 | LDFLAGS=-ldflags "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD)" 15 | 16 | # Redirect error output to a file, so we can show it in development mode. 17 | STDERR := /tmp/.$(PROJECTNAME)-stderr.txt 18 | 19 | # PID file will keep the process id of the server 20 | PID := /tmp/.$(PROJECTNAME).pid 21 | 22 | # Make is verbose in Linux. Make it silent. 23 | MAKEFLAGS += --silent 24 | 25 | 26 | 27 | 28 | 29 | ## install: Install missing dependencies. Runs `go get` internally. e.g; make install get=github.com/foo/bar 30 | install: go-get 31 | 32 | ci: compile go-test-cover 33 | 34 | 35 | 36 | ## compile: Compile the binary. 37 | compile: 38 | @-touch $(STDERR) 39 | @-rm $(STDERR) 40 | @-$(MAKE) -s go-compile 2> $(STDERR) 41 | @cat $(STDERR) | sed -e '1s/.*/Error:/' | sed 's/make\[.*/ /' | sed "/^/s/^/ /" 1>&2 42 | 43 | 44 | ## clean: Clean build files. Runs `go clean` internally. 45 | clean: 46 | @-rm $(GOBIN)/$(PROJECTNAME) 2> /dev/null 47 | @-$(MAKE) go-clean 48 | 49 | 50 | tdd: 51 | @bash -c "trap 'make tdd-stop' EXIT; $(MAKE) tdd-setup" 52 | @bash -c "$(MAKE) watch run='make clean compile tdd-exec'" 53 | 54 | tdd-setup: 55 | @-$(MAKE) clean compile 56 | @echo " ℹ build stat for $(PROJECTNAME) is available at http://$(WATCH_ADDR)" 57 | @-$(MAKE) tdd-exec 58 | 59 | 60 | tdd-exec: 61 | echo " 🚀 Run tests ..." 62 | @$(MAKE) go-test 2>&1 63 | echo " ☑️ tests done" 64 | 65 | # @cat $(PID) | sed "/^/s/^/ \> PID: /" 66 | 67 | tdd-stop: 68 | @-touch $(PID) 69 | @-kill `cat $(PID)` 2> /dev/null || true 70 | @-rm $(PID) 71 | 72 | ## watch: Run given command when code changes. e.g; make watch run="echo 'hey'" 73 | watch: 74 | @GOPATH=$(GOPATH) GOBIN=$(GOBIN) yolo -i '*/*.go' -i '*.go' -e vendor -e bin -a "$(WATCH_ADDR)" -c "$(run)" 75 | 76 | 77 | go-compile: go-get go-build 78 | 79 | go-test: 80 | @go test ./... -coverpkg=./... -timeout 10s 81 | 82 | go-test-cover: 83 | @go test ./... -coverpkg=./... -coverprofile=coverage.txt -timeout 30s 84 | @go tool cover -func=coverage.txt 85 | 86 | go-build: 87 | @echo " ⚙️ Building binary..." 88 | @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build $(LDFLAGS) -o $(GOBIN)/$(PROJECTNAME) $(GOFILES) 89 | 90 | go-generate: 91 | @echo " 🛠 Generating dependency files..." 92 | @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go generate $(generate) 93 | 94 | go-get: 95 | @echo " 🔎 Checking if there is any missing dependencies..." 96 | @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get $(get) 97 | 98 | go-install: 99 | @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES) 100 | 101 | go-clean: 102 | @echo " 🗑 Cleaning build cache" 103 | @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean 104 | 105 | 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go2p 2 | > golang p2p framework 3 | 4 | By [v-braun - viktor-braun.de](https://viktor-braun.de). 5 | 6 | [![Build Status](https://img.shields.io/travis/v-braun/go2p.svg?style=flat-square)](https://travis-ci.org/v-braun/go2p) 7 | [![codecov](https://codecov.io/gh/v-braun/go2p/branch/master/graph/badge.svg)](https://codecov.io/gh/v-braun/go2p) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/v-braun/go2p)](https://goreportcard.com/report/github.com/v-braun/go2p) 9 | [![Documentation](https://godoc.org/github.com/v-braun/go2p?status.svg)](http://godoc.org/github.com/v-braun/go2p) 10 | ![PR welcome](https://img.shields.io/badge/PR-welcome-green.svg?style=flat-square) 11 | [![](https://img.shields.io/github/license/v-braun/go2p.svg?style=flat-square)](https://github.com/v-braun/go2p/blob/master/LICENSE) 12 | 13 | 14 | 15 |

16 | 17 |

18 | 19 | 20 | ## Description 21 | 22 | GO2P is a P2P framework, designed with flexibility and simplicity in mind. 23 | You can use a pre configured stack (encryption, compression, etc.) or built your own based on the existing modules. 24 | 25 | GO2P use the [middleware pattern](https://dzone.com/articles/understanding-middleware-pattern-in-expressjs) as a core pattern for messages. 26 | If you have used expressJS, OWIN or other HTTP/Web based frameworks you should be familiar with that. 27 | The basic idea is that an outgoing message is passed through multiple middleware functions. Each of this functions can manipulate the message. 28 | A middleware function could encrypt, compress, log or sign the message. 29 | Outgoing messages will be processed through the middleware functions and incomming messages in the inverted order: 30 | 31 |

32 | 33 |

34 | 35 | 36 | 37 | 38 | ## Installation 39 | ```sh 40 | go get github.com/v-braun/go2p 41 | ``` 42 | 43 | 44 | 45 | ## Usage 46 | 47 | > You like code? Checkout the [chat example](https://github.com/v-braun/go2p/blob/master/example_test.go) 48 | 49 | The simplest way to use this framework is to create a new instance of the full configured TCP based network stack: 50 | 51 | ``` go 52 | localAddr := "localhost:7077" 53 | net := go2p.NewNetworkConnectionTCP(*localAddr, &map[string]func(peer *go2p.Peer, msg *go2p.Message){ 54 | "msg": func(peer *go2p.Peer, msg *go2p.Message) { 55 | fmt.Printf("%s > %s\n", peer.RemoteAddress(), msg.PayloadGetString()) 56 | }, 57 | }) 58 | 59 | net.OnPeer(func(p *go2p.Peer) { 60 | fmt.Printf("new peer: %s\n", p.RemoteAddress()) 61 | }) 62 | 63 | err := net.Start() 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | defer net.Stop() 69 | 70 | 71 | // connects to another peer via tcp 72 | net.ConnectTo("tcp", "localhost:7077") 73 | 74 | // send a message to the 'msg' route 75 | net.SendBroadcast(go2p.NewMessageRoutedFromString("msg", "hello go2p")) 76 | 77 | 78 | 79 | ``` 80 | 81 | ## Advanced Usage 82 | 83 | The function NewNetworkConnectionTCP is a shorthand for the advanced configuration of a network stack. 84 | 85 | ``` go 86 | op := go2p.NewTCPOperator("tcp", localAddr) // creates a tcp based operator (net.Dialer and net.Listener) 87 | peerStore := go2p.NewDefaultPeerStore(10) // creates a simple peer store that limits connections to 10 88 | 89 | conn := go2p.NewNetworkConnection(). // creates a new instance of the builder 90 | WithOperator(op). // adds the operator to the network stack 91 | WithPeerStore(peerStore). // adds the peer store to the network stack 92 | WithMiddleware(go2p.Routes(routes)). // adds the routes middleware 93 | WithMiddleware(go2p.Headers()). // adds the headers middleware 94 | WithMiddleware(go2p.Crypt()). // adds encryption 95 | WithMiddleware(go2p.Log()). // adds logging 96 | Build() // creates the network 97 | ``` 98 | 99 | This code creates a new NetworkConnection that use tcp communication, a default PeerStore and some middlewares. 100 | Outgoing messages will now pass the following middlewares: 101 | ``` 102 | (app logic) -> Routing -> Headers -> Crypt -> Log -> (network) 103 | ``` 104 | 105 | Incomming messages will pass the following middlewares 106 | ``` 107 | (app logic) <- Routing <- Headers <- Crypt <- Log <- (network) 108 | ``` 109 | 110 | 111 | 112 | 113 | 114 | ## Authors 115 | 116 | ![image](https://avatars3.githubusercontent.com/u/4738210?v=3&s=50) 117 | [v-braun](https://github.com/v-braun/) 118 | 119 | 120 | 121 | ## Contributing 122 | 123 | Make sure to read these guides before getting started: 124 | - [Contribution Guidelines](https://github.com/v-braun/go2p/blob/master/CONTRIBUTING.md) 125 | - [Code of Conduct](https://github.com/v-braun/go2p/blob/master/CODE_OF_CONDUCT.md) 126 | 127 | ## License 128 | **go2p** is available under the MIT License. See [LICENSE](https://github.com/v-braun/go2p/blob/master/LICENSE) for details. 129 | -------------------------------------------------------------------------------- /adapter.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | type errorConstant string 4 | 5 | func (e errorConstant) Error() string { return string(e) } 6 | 7 | // DisconnectedError represents Error when a peer is disconnected 8 | const DisconnectedError = errorConstant("disconnected") 9 | 10 | // Adapter represents a wrapper around a network connection 11 | type Adapter interface { 12 | 13 | // ReadMessage should read from the underline connection 14 | // and return a Message object until all data was readed 15 | // The call should block until an entire Message was readed, 16 | // an error occoured or the underline connection was closed 17 | ReadMessage() (*Message, error) 18 | 19 | // WriteMessage write the given message to the underline connection 20 | WriteMessage(m *Message) error 21 | 22 | // Close should close the underline connection 23 | Close() 24 | 25 | // LocalAddress returns the local address (example: tcp:127.0.0.1:7000) 26 | LocalAddress() string 27 | 28 | // RemoteAddress returns the remote address (example: tcp:127.0.0.1:7000) 29 | RemoteAddress() string 30 | } 31 | -------------------------------------------------------------------------------- /adapter_io.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/v-braun/awaiter" 8 | ) 9 | 10 | type adapterIO struct { 11 | receive chan *Message 12 | send chan *Message 13 | 14 | awaiter awaiter.Awaiter 15 | 16 | adapter Adapter 17 | 18 | emitter *eventEmitter 19 | } 20 | 21 | func newAdapterIO(adapter Adapter) *adapterIO { 22 | io := new(adapterIO) 23 | io.receive = make(chan *Message) 24 | io.send = make(chan *Message) 25 | io.awaiter = awaiter.New() 26 | io.adapter = adapter 27 | io.emitter = newEventEmitter() 28 | 29 | return io 30 | } 31 | 32 | func (io *adapterIO) start() { 33 | io.awaiter.Go(func() { 34 | for { 35 | m, err := io.adapter.ReadMessage() 36 | if err != nil { 37 | io.handleError(err, "read") 38 | return 39 | } 40 | 41 | select { 42 | case io.receive <- m: 43 | continue 44 | case <-io.awaiter.CancelRequested(): 45 | return 46 | } 47 | } 48 | }) 49 | 50 | io.awaiter.Go(func() { 51 | for { 52 | select { 53 | case m := <-io.send: 54 | err := io.adapter.WriteMessage(m) 55 | if err != nil { 56 | io.handleError(err, "write") 57 | return 58 | } 59 | 60 | continue 61 | case <-io.awaiter.CancelRequested(): 62 | return 63 | } 64 | } 65 | }) 66 | } 67 | 68 | func isDisconnectErr(err error) bool { 69 | if err == DisconnectedError || err == io.EOF { 70 | return true 71 | } 72 | 73 | return false 74 | } 75 | func (io *adapterIO) handleError(err error, src string) { 76 | if isDisconnectErr(err) { 77 | io.emitter.EmitAsync("disconnect") 78 | return 79 | } 80 | 81 | io.emitter.EmitAsync("error", errors.Wrapf(err, "error during %s", src)) 82 | } 83 | 84 | func (io *adapterIO) sendMsg(m *Message) error { 85 | select { 86 | case io.send <- m: 87 | return nil 88 | case <-io.awaiter.CancelRequested(): 89 | return DisconnectedError 90 | } 91 | } 92 | 93 | func (io *adapterIO) receiveMsg() (*Message, error) { 94 | select { 95 | case m := <-io.receive: 96 | return m, nil 97 | case <-io.awaiter.CancelRequested(): 98 | return nil, DisconnectedError 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crypt/crypt.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/sha1" 9 | "crypto/sha256" 10 | "crypto/x509" 11 | "fmt" 12 | "io" 13 | 14 | "github.com/v-braun/go-must" 15 | 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | const encryptedPassLen = 256 20 | const nonceLen = 12 21 | 22 | // PubKey is a wrapper around an rsa.PublicKey 23 | type PubKey struct { 24 | pub *rsa.PublicKey 25 | Bytes []byte 26 | } 27 | 28 | // PrivKey is a wrapper around an rsa.PrivateKey 29 | type PrivKey struct { 30 | PubKey 31 | priv *rsa.PrivateKey 32 | Bytes []byte 33 | } 34 | 35 | // Generate returns a new PrivKey 36 | func Generate() *PrivKey { 37 | k, err := rsa.GenerateKey(rand.Reader, 2048) 38 | must.NoError(err, "could not generate keypair") 39 | 40 | result := &PrivKey{} 41 | result.priv = k 42 | result.pub = &k.PublicKey 43 | result.calcBytes() 44 | err = result.PubKey.calcBytes() 45 | must.NoError(err, "could not get bytes from pub key") 46 | 47 | return result 48 | } 49 | 50 | // PrivFromBytes retruns a PrivKey based on the provided bytes 51 | func PrivFromBytes(data []byte) (*PrivKey, error) { 52 | k, err := x509.ParsePKCS1PrivateKey(data) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | result := &PrivKey{} 58 | result.priv = k 59 | result.pub = &k.PublicKey 60 | result.calcBytes() 61 | result.PubKey.calcBytes() 62 | 63 | return result, nil 64 | } 65 | 66 | // PubFromBytes returns a PubKey based on data 67 | func PubFromBytes(data []byte) (*PubKey, error) { 68 | pub, err := x509.ParsePKIXPublicKey(data) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | key := pub.(*rsa.PublicKey) 74 | 75 | result := &PubKey{} 76 | result.pub = key 77 | err = result.calcBytes() 78 | return result, err 79 | } 80 | 81 | func (pk *PrivKey) calcBytes() { 82 | result := x509.MarshalPKCS1PrivateKey(pk.priv) 83 | pk.Bytes = result 84 | } 85 | 86 | // Decrypt returns decrypted data that was encrypted with the given pub key 87 | func (pk *PrivKey) Decrypt(pub *PubKey, encryptedData []byte) ([]byte, error) { 88 | if len(encryptedData) < encryptedPassLen { 89 | return nil, errors.Errorf("unexpected data length, min: %d, current: %d", encryptedPassLen, len(encryptedData)) 90 | } 91 | 92 | encryptedPass := encryptedData[:encryptedPassLen] 93 | decryptedPass, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, pk.priv, encryptedPass, nil) 94 | if err != nil { 95 | return nil, errors.Wrap(err, "failed decrypt pass") 96 | } 97 | 98 | nonce := encryptedData[encryptedPassLen : encryptedPassLen+nonceLen] 99 | encryptedData = encryptedData[encryptedPassLen+nonceLen:] 100 | decryptedData, err := dec(decryptedPass, nonce, encryptedData) 101 | if err != nil { 102 | return nil, errors.Wrap(err, "failed decrypt data") 103 | } 104 | 105 | //err = pub.checkSign(decryptedData, decryptedPass) 106 | 107 | return decryptedData, err 108 | } 109 | 110 | func (pk *PubKey) calcBytes() error { 111 | result, err := x509.MarshalPKIXPublicKey(pk.pub) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | pk.Bytes = result 117 | return nil 118 | } 119 | 120 | // func (pub *PubKey) checkSign(data []byte, sign []byte) error { 121 | // h := sha256.New() 122 | // h.Write(data) 123 | // d := h.Sum(nil) 124 | // return rsa.VerifyPKCS1v15(pub.pub, crypto.SHA256, d, sign) 125 | // } 126 | 127 | // func (priv *PrivKey) sign(data []byte) ([]byte, error) { 128 | // h := sha256.New() 129 | // h.Write(data) 130 | // d := h.Sum(nil) 131 | // return rsa.SignPKCS1v15(rand.Reader, priv.priv, crypto.SHA256, d) 132 | // } 133 | 134 | func hash(data []byte) []byte { 135 | h := sha256.New() 136 | _, err := h.Write(data) 137 | must.NoError(err, "could not write given data to hash") 138 | 139 | d := h.Sum(nil) 140 | 141 | return d 142 | } 143 | 144 | func enc(pass []byte, nonce []byte, data []byte) []byte { 145 | block, err := aes.NewCipher(pass) 146 | must.NoError(err, "unexpected error during create cipher") 147 | 148 | aead, err := cipher.NewGCM(block) 149 | must.NoError(err, "unexpected error during create gcm") 150 | 151 | encryptedData := aead.Seal(nil, nonce, data, nil) 152 | 153 | return encryptedData 154 | } 155 | 156 | func dec(pass []byte, nonce []byte, data []byte) ([]byte, error) { 157 | block, err := aes.NewCipher(pass) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | aesgcm, err := cipher.NewGCM(block) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | decryptedData, err := aesgcm.Open(nil, nonce, data, nil) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | return decryptedData, nil 173 | } 174 | 175 | func genNonce() []byte { 176 | nonce := make([]byte, nonceLen) 177 | 178 | _, err := io.ReadFull(rand.Reader, nonce) 179 | must.NoError(err, "could not read random number") 180 | 181 | return nonce 182 | } 183 | 184 | // Encrypt hash the data, encrypt the data with the hash, encrypt the hash with pk 185 | // and store the encrypted hash with the nonce within the data 186 | func (pk *PubKey) Encrypt(priv *PrivKey, decrypted []byte) ([]byte, error) { 187 | hash := hash(decrypted) 188 | 189 | nonce := genNonce() 190 | 191 | decryptedPass := hash // use the hash of the msg as its pass 192 | encryptedData := enc(decryptedPass, nonce, decrypted) 193 | 194 | encryptedPass, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, pk.pub, decryptedPass, nil) 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | if len(encryptedPass) != encryptedPassLen { 200 | panic(fmt.Sprintf("unexpected encrypted pass len %d allwoed: %d", encryptedPass, encryptedPassLen)) 201 | } 202 | 203 | result := append(encryptedPass, nonce...) 204 | result = append(result, encryptedData...) 205 | 206 | return result, err 207 | } 208 | 209 | // func newHasher() hash.Hash { 210 | // h := sha256.New() 211 | // return h 212 | // } 213 | 214 | // func hasherSize() int { 215 | // h := newHasher() 216 | // return h.Size() 217 | // } 218 | -------------------------------------------------------------------------------- /crypt/crypt_test.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFromBytes(t *testing.T) { 10 | k := Generate() 11 | 12 | privBytes := k.Bytes 13 | pubBytes := k.PubKey.Bytes 14 | 15 | k2, err := PrivFromBytes(privBytes) 16 | assert.NoError(t, err) 17 | 18 | pub2, err := PubFromBytes(pubBytes) 19 | assert.NoError(t, err) 20 | 21 | assert.EqualValues(t, privBytes, k2.Bytes) 22 | assert.EqualValues(t, pubBytes, k2.PubKey.Bytes) 23 | assert.EqualValues(t, pubBytes, pub2.Bytes) 24 | } 25 | 26 | func TestPrivBytesNegative(t *testing.T) { 27 | _, err := PrivFromBytes([]byte{1}) 28 | assert.Error(t, err) 29 | 30 | _, err = PubFromBytes([]byte{1}) 31 | assert.Error(t, err) 32 | } 33 | 34 | func TestDecryptNegative(t *testing.T) { 35 | data := []byte{} 36 | pk1 := Generate() 37 | pk2 := Generate() 38 | 39 | _, err := pk1.Decrypt(nil, data) 40 | assert.Error(t, err) 41 | 42 | data = make([]byte, 300) 43 | _, err = pk1.Decrypt(nil, data) 44 | assert.Error(t, err) 45 | 46 | encryptedData, err := pk1.Encrypt(pk2, []byte("hello")) 47 | encryptedData[len(encryptedData)-1] = encryptedData[len(encryptedData)-1] + 1 48 | _, err = pk1.Decrypt(&pk2.PubKey, encryptedData) 49 | assert.Error(t, err) 50 | } 51 | -------------------------------------------------------------------------------- /emitter.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import "github.com/olebedev/emitter" 4 | 5 | type eventEmitter struct { 6 | emitter *emitter.Emitter 7 | } 8 | 9 | func newEventEmitter() *eventEmitter { 10 | em := new(eventEmitter) 11 | em.emitter = new(emitter.Emitter) 12 | em.emitter.Use("*", emitter.Void) 13 | return em 14 | } 15 | 16 | func (em *eventEmitter) EmitAsync(topic string, args ...interface{}) { 17 | go em.emitter.Emit(topic, args...) 18 | } 19 | 20 | func (em *eventEmitter) On(topic string, handler func(args []interface{})) { 21 | em.emitter.On(topic, func(ev *emitter.Event) { 22 | handler(ev.Args) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package go2p_test 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/fatih/color" 12 | "github.com/sirupsen/logrus" 13 | "github.com/v-braun/go2p" 14 | prefixed "github.com/x-cray/logrus-prefixed-formatter" 15 | ) 16 | 17 | func example(t *testing.T) { 18 | logrus.SetFormatter(new(prefixed.TextFormatter)) 19 | logrus.SetOutput(os.Stdout) 20 | logrus.SetLevel(logrus.DebugLevel) 21 | 22 | localAddr := flag.String("laddr", "localhost:7071", "local ip address") 23 | flag.Parse() 24 | 25 | cyan := color.New(color.FgCyan).SprintFunc() 26 | blue := color.New(color.FgHiBlue).SprintFunc() 27 | green := color.New(color.FgHiGreen).SprintFunc() 28 | white := color.New(color.FgHiWhite).SprintFunc() 29 | peerName := color.New(color.BgBlue, color.FgHiWhite).SprintFunc() 30 | 31 | net := go2p.NewNetworkConnectionTCP(*localAddr, &map[string]func(peer *go2p.Peer, msg *go2p.Message){ 32 | "msg": func(peer *go2p.Peer, msg *go2p.Message) { 33 | fmt.Println(fmt.Sprintf("%s %s", peerName(peer.RemoteAddress()+" > "), msg.PayloadGetString())) 34 | }, 35 | }) 36 | 37 | err := net.Start() 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | net.OnPeer(func(p *go2p.Peer) { 43 | fmt.Printf("%s %s\n", cyan("new peer:"), green(p.RemoteAddress())) 44 | }) 45 | 46 | defer net.Stop() 47 | 48 | fmt.Println(cyan(` 49 | local server started! 50 | 51 | press: 52 | `)) 53 | 54 | fmt.Printf("%s %s\n", blue("[q][ENTER]"), white("to exit")) 55 | fmt.Printf("%s %s\n", blue("[c {ip address : port}][ENTER]"), white("to connect to another peer")) 56 | fmt.Printf("%s %s\n", blue("[any message][ENTER]"), white("to send a message to all peers")) 57 | 58 | reader := bufio.NewReader(os.Stdin) 59 | for { 60 | text, _ := reader.ReadString('\n') 61 | text = strings.TrimSpace(text) 62 | if text == "q" { 63 | return 64 | } else if strings.HasPrefix(text, "c ") { 65 | text = strings.TrimPrefix(text, "c ") 66 | net.ConnectTo("tcp", text) 67 | } else { 68 | net.SendBroadcast(go2p.NewMessageRoutedFromString("msg", text)) 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/v-braun/go2p 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/azer/logger v1.0.0 // indirect 7 | github.com/azer/yolo v0.0.0-20180819171155-df2a2bdacdd0 // indirect 8 | github.com/emirpasic/gods v1.12.0 9 | github.com/fatih/color v1.7.0 10 | github.com/fsnotify/fsnotify v1.4.7 // indirect 11 | github.com/google/uuid v1.1.1 12 | github.com/gorilla/websocket v1.4.1 // indirect 13 | github.com/mattn/go-colorable v0.1.2 // indirect 14 | github.com/mattn/go-isatty v0.0.9 // indirect 15 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 16 | github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee 17 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 18 | github.com/pkg/errors v0.8.1 19 | github.com/sirupsen/logrus v1.4.2 20 | github.com/stretchr/testify v1.2.2 21 | github.com/v-braun/awaiter v0.0.0-20190702174054-2d2c95947d12 22 | github.com/v-braun/go-must v0.0.0-20190710195319-bad755225388 23 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 24 | golang.org/x/crypto v0.0.0-20190909091759-094676da4a83 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/azer/is-terminal v1.0.0 h1:COvj8jmg2xMz0CqHn4Uu8X1m7Dmzmu0CpciBaLtJQBg= 2 | github.com/azer/is-terminal v1.0.0/go.mod h1:5geuIpRQvdv6g/Q1MwXHbmNUlFLg8QcheGk4dZOmxQU= 3 | github.com/azer/logger v1.0.0 h1:3T4BnTLyndJWHajOyECt2kAhnvP30KCrVAkYcMjHrXk= 4 | github.com/azer/logger v1.0.0/go.mod h1:iaDID7UeBTyUh31bjGFlLkr87k23z/mHMMLzt6YQQHU= 5 | github.com/azer/yolo v0.0.0-20180819171155-df2a2bdacdd0 h1:fde8zaqmhxxm5fplJM6s9JUO5HJbYZqPnTDka0CXUDg= 6 | github.com/azer/yolo v0.0.0-20180819171155-df2a2bdacdd0/go.mod h1:STi2LrClOuieEG7rszDymhqSV0pEVo2Aaf8+jwIAF2g= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 10 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 11 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 12 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 13 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 14 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 15 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 16 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 18 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 19 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 20 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 21 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 22 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 23 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 24 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 25 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 26 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 27 | github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee h1:IquUs3fIykn10zWDIyddanhpTqBvAHMaPnFhQuyYw5U= 28 | github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee/go.mod h1:eT2/Pcsim3XBjbvldGiJBvvgiqZkAFyiOJJsDKXs/ts= 29 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= 30 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= 31 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 32 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 33 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 35 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 36 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 37 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 38 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 40 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 41 | github.com/v-braun/awaiter v0.0.0-20190702174054-2d2c95947d12 h1:sPOTsY4L83vJNyn4d9x1LugIuLDhG5PjpjWrAMhCroc= 42 | github.com/v-braun/awaiter v0.0.0-20190702174054-2d2c95947d12/go.mod h1:OlsJ4cPX3/rjOz42LB03DO4vcEpNP4+JvE0RFKSvPcU= 43 | github.com/v-braun/go-must v0.0.0-20190710195319-bad755225388 h1:UnDU6yU/1Bxge2AjLBtIRS1zHGtlmQub8OrfVyreAYw= 44 | github.com/v-braun/go-must v0.0.0-20190710195319-bad755225388/go.mod h1:q25/0N7PMFeAT35YwA+gnDSVdehSjBDpNbru55CJwTI= 45 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= 46 | github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= 47 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 48 | golang.org/x/crypto v0.0.0-20190909091759-094676da4a83 h1:mgAKeshyNqWKdENOnQsg+8dRTwZFIwFaO3HNl52sweA= 49 | golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 50 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 51 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 52 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 55 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 57 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 59 | -------------------------------------------------------------------------------- /idea/logo-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 2P 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | func newLogger(name string) *logrus.Entry { 8 | l := logrus.New() 9 | l.SetLevel(logrus.StandardLogger().Level) 10 | l.Formatter = logrus.StandardLogger().Formatter 11 | l.Out = logrus.StandardLogger().Out 12 | result := l.WithField("prefix", name) 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "io" 7 | "net" 8 | "strings" 9 | 10 | "github.com/v-braun/go-must" 11 | 12 | "github.com/google/uuid" 13 | 14 | "github.com/emirpasic/gods/maps" 15 | "github.com/emirpasic/gods/maps/hashmap" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | func handleReadWriteErr(err error, msg string) error { 20 | if err == io.EOF { 21 | return DisconnectedError 22 | } 23 | 24 | if netErr, ok := err.(*net.OpError); ok { 25 | netErrMsg := netErr.Err.Error() 26 | if strings.Contains(netErrMsg, "use of closed network connection") { 27 | return DisconnectedError 28 | } 29 | } 30 | 31 | return errors.Wrap(err, msg) 32 | } 33 | 34 | // Message represents a p2p message 35 | type Message struct { 36 | payload []byte 37 | metadata maps.Map 38 | localID string 39 | } 40 | 41 | // NewMessageFromString creates a new Message from the given string 42 | func NewMessageFromString(data string) *Message { 43 | return NewMessageFromData([]byte(data)) 44 | } 45 | 46 | // NewMessageFromData creates a new Message from the given data 47 | func NewMessageFromData(data []byte) *Message { 48 | m := NewMessage() 49 | m.payload = data 50 | return m 51 | } 52 | 53 | // NewMessage creates a new empty Message 54 | func NewMessage() *Message { 55 | m := new(Message) 56 | m.payload = []byte{} 57 | m.metadata = hashmap.New() 58 | m.localID = uuid.New().String() 59 | return m 60 | } 61 | 62 | // Metadata returns a map of metadata assigned to this message 63 | func (m *Message) Metadata() maps.Map { 64 | return m.metadata 65 | } 66 | 67 | // ReadFromConn read all data from the given conn object into the payload 68 | // of the message instance 69 | func (m *Message) ReadFromConn(c net.Conn) error { 70 | reader := bufio.NewReader(c) 71 | err := m.ReadFromReader(reader) 72 | return err 73 | } 74 | 75 | // ReadFromReader read all data from the given reader object into the payload 76 | // of the message instance 77 | func (m *Message) ReadFromReader(reader *bufio.Reader) error { 78 | must.ArgNotNil(reader, "reader") 79 | 80 | sizeBuffer := make([]byte, 4) 81 | 82 | if err := read(reader, len(sizeBuffer), sizeBuffer, "failed read size"); err != nil { 83 | return err 84 | } 85 | 86 | size := int(binary.BigEndian.Uint32(sizeBuffer)) 87 | 88 | payloadBuffer := make([]byte, size) 89 | 90 | if err := read(reader, size, payloadBuffer, "failed read payload"); err != nil { 91 | return err 92 | } 93 | 94 | m.payload = payloadBuffer 95 | 96 | return nil 97 | } 98 | 99 | // WriteIntoConn writes the message payload into the given conn instance 100 | func (m *Message) WriteIntoConn(c net.Conn) error { 101 | writer := bufio.NewWriter(c) 102 | err := m.WriteIntoWriter(writer) 103 | return err 104 | } 105 | 106 | // WriteIntoWriter writes the message payload into the given writer instance 107 | func (m *Message) WriteIntoWriter(writer *bufio.Writer) error { 108 | must.ArgNotNil(writer, "writer") 109 | 110 | payload := m.payload 111 | 112 | size := uint32(len(payload)) 113 | sizeBuffer := make([]byte, 4) 114 | 115 | binary.BigEndian.PutUint32(sizeBuffer, size) 116 | 117 | if err := write(writer, sizeBuffer, "failed write size buffer"); err != nil { 118 | return err 119 | } 120 | if err := write(writer, payload, "failed write payload"); err != nil { 121 | return err 122 | } 123 | 124 | return writer.Flush() 125 | } 126 | 127 | // PayloadSetString sets the given string as payload of the message 128 | func (m *Message) PayloadSetString(value string) { 129 | m.payload = []byte(value) 130 | } 131 | 132 | // PayloadGetString returns the payload of the message as a string 133 | func (m *Message) PayloadGetString() string { 134 | if len(m.payload) <= 0 { 135 | return "" 136 | } 137 | 138 | result := string(m.payload) 139 | return result 140 | } 141 | 142 | // PayloadSet sets the payload with the given value 143 | func (m *Message) PayloadSet(value []byte) { 144 | m.payload = value 145 | } 146 | 147 | // PayloadGet returns the payload data 148 | func (m *Message) PayloadGet() []byte { 149 | return m.payload 150 | } 151 | 152 | func write(writer *bufio.Writer, buffer []byte, onErrMsg string) error { 153 | _, err := writer.Write(buffer) 154 | if err != nil { 155 | return handleReadWriteErr(err, onErrMsg) 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func read(reader *bufio.Reader, length int, buffer []byte, onErrMsg string) error { 162 | readed := 0 163 | for readed < length { 164 | currentReaded, err := reader.Read(buffer[readed:]) 165 | if err != nil { 166 | return handleReadWriteErr(err, onErrMsg) 167 | } 168 | 169 | readed += currentReaded 170 | } 171 | 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | // MiddlewareResult represents a result returned by a middleware 9 | // possible values are *Stop* and *Next* 10 | type MiddlewareResult int 11 | 12 | const ( 13 | // Stop will be returned by a middleware when the pipe execution should be stopped 14 | Stop MiddlewareResult = iota 15 | 16 | // Next will be returned by a middleware when the pipe execution should be continued 17 | Next MiddlewareResult = iota 18 | ) 19 | 20 | // MiddlewareFunc represents a middleware implementation function 21 | type MiddlewareFunc func(peer *Peer, pipe *Pipe, msg *Message) (MiddlewareResult, error) 22 | 23 | // Middleware represents a wrapped middleware function with 24 | // additional information for internal usage 25 | type Middleware struct { 26 | execute MiddlewareFunc 27 | name string 28 | pos int 29 | } 30 | 31 | // NewMiddleware wraps the provided action into a Middleware instance 32 | func NewMiddleware(name string, action MiddlewareFunc) *Middleware { 33 | return &Middleware{ 34 | name: name, 35 | execute: action, 36 | } 37 | } 38 | 39 | // String returns the string representation of this instance 40 | func (m *Middleware) String() string { 41 | return fmt.Sprintf("%s (%d)", m.name, m.pos) 42 | } 43 | 44 | type middlewares []*Middleware 45 | 46 | func newMiddlewares(actions ...*Middleware) middlewares { 47 | result := middlewares{} 48 | for idx, action := range actions { 49 | action.pos = idx 50 | result = append(result, action) 51 | } 52 | 53 | return result 54 | } 55 | 56 | func (ml middlewares) nextItems(op PipeOperation) middlewares { 57 | result := ml.Copy() 58 | sort.Sort(result) 59 | if op == Receive { 60 | sort.Sort(sort.Reverse(result)) 61 | } 62 | 63 | return result 64 | } 65 | 66 | func (ml middlewares) Copy() middlewares { 67 | result := make(middlewares, len(ml)) 68 | copy(result, ml) 69 | return result 70 | } 71 | 72 | func (ml middlewares) Len() int { 73 | return len(ml) 74 | } 75 | 76 | func (ml middlewares) Swap(i, j int) { 77 | ml[i], ml[j] = ml[j], ml[i] 78 | } 79 | func (ml middlewares) Less(i, j int) bool { 80 | return ml[i].pos < ml[j].pos 81 | } 82 | -------------------------------------------------------------------------------- /middleware_crypt.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/v-braun/go2p/crypt" 8 | ) 9 | 10 | var prefixHandshake = []byte("hello:") 11 | 12 | const cryptLabel = "middleware.crypt" 13 | const headerKeyPubKey = "middleware.crypt.pubkey" 14 | 15 | // Crypt returns the crypto middleware. 16 | // This middleware handles encryption in your communication 17 | // PublicKeys are exchanged on first peer communication 18 | func Crypt() (string, MiddlewareFunc) { 19 | key := crypt.Generate() 20 | 21 | f := func(peer *Peer, pipe *Pipe, msg *Message) (MiddlewareResult, error) { 22 | op, err := middlewareCryptImpl(key, peer, pipe, msg) 23 | return op, err 24 | } 25 | 26 | return "Crypt", f 27 | } 28 | 29 | func middlewareCryptImpl(myKey *crypt.PrivKey, peer *Peer, pipe *Pipe, msg *Message) (MiddlewareResult, error) { 30 | 31 | if isHandshakeDone(peer, pipe) { 32 | // handshake done, just handle the message 33 | err := messageHandle(peer, pipe, msg, myKey) 34 | if err != nil { 35 | return Stop, err 36 | } 37 | 38 | return Next, err 39 | } 40 | 41 | // no pub-key from remote, handle handshake 42 | if pipe.Operation() == Receive { 43 | // passive mode: 44 | // the remote send us the pub key 45 | // so the received message should be a handshake message 46 | err := handshakePassive(peer, pipe, msg, myKey) 47 | return Stop, err 48 | } 49 | 50 | // active mode: 51 | // the active message should be postpone after the key exchange 52 | err := handshakeActive(peer, pipe, myKey) 53 | if err != nil { 54 | return Stop, err 55 | } 56 | 57 | // handshake done, just handle the active message 58 | err = messageHandle(peer, pipe, msg, myKey) 59 | return Next, err 60 | } 61 | 62 | func messageHandle(peer *Peer, pipe *Pipe, msg *Message, myKey *crypt.PrivKey) error { 63 | key, _ := peer.Metadata().Get(headerKeyPubKey) 64 | theirKey := key.(*crypt.PubKey) 65 | 66 | if pipe.Operation() == Send { 67 | err := encrypt(msg, theirKey, myKey) 68 | return err 69 | } 70 | 71 | err := decrypt(msg, myKey, theirKey) 72 | return err 73 | } 74 | 75 | func encrypt(msg *Message, theirKey *crypt.PubKey, myKey *crypt.PrivKey) error { 76 | content := msg.PayloadGet() 77 | contentEnc, err := theirKey.Encrypt(myKey, content) 78 | if err != nil { 79 | return errors.Wrapf(err, "could not encrypt message (len: %d)", len(content)) 80 | } 81 | 82 | msg.PayloadSet(contentEnc) 83 | 84 | return nil 85 | } 86 | 87 | func decrypt(msg *Message, myKey *crypt.PrivKey, theirKey *crypt.PubKey) error { 88 | content := msg.PayloadGet() 89 | contentLen := len(content) 90 | content, err := myKey.Decrypt(theirKey, content) 91 | if err != nil { 92 | return errors.Wrapf(err, "could not decrypt (len: %d)", contentLen) 93 | } 94 | 95 | msg.PayloadSet(content) 96 | 97 | return nil 98 | } 99 | 100 | // handshake methods 101 | func isHandshakeDone(peer *Peer, pipe *Pipe) bool { 102 | _, found := peer.Metadata().Get(headerKeyPubKey) 103 | return found 104 | } 105 | 106 | func isHandshakeMsg(msg *Message) bool { 107 | content := msg.PayloadGet() 108 | if len(content) < len(prefixHandshake) { 109 | return false 110 | } 111 | 112 | prefix := content[:len(prefixHandshake)] 113 | equal := bytes.Equal(prefix, prefixHandshake) 114 | 115 | return equal 116 | } 117 | 118 | func handshakePassive(peer *Peer, pipe *Pipe, msg *Message, myKey *crypt.PrivKey) error { 119 | if err := handshakeHandleResponse(peer, pipe, msg); err != nil { 120 | errors.Wrapf(err, "received message from peer without a handshake | peer: %s", peer.RemoteAddress()) 121 | return err 122 | } 123 | 124 | err := handshakeSend(pipe, myKey) 125 | return err 126 | } 127 | 128 | func handshakeActive(peer *Peer, pipe *Pipe, myKey *crypt.PrivKey) error { 129 | if err := handshakeSend(pipe, myKey); err != nil { 130 | return err 131 | } 132 | 133 | msg, err := pipe.Receive() 134 | if err != nil { 135 | return err 136 | } 137 | 138 | err = handshakeHandleResponse(peer, pipe, msg) 139 | return err 140 | } 141 | 142 | func handshakeSend(pipe *Pipe, myKey *crypt.PrivKey) error { 143 | rq := NewMessage() 144 | 145 | content := append(prefixHandshake, myKey.PubKey.Bytes...) 146 | rq.PayloadSet(content) 147 | err := pipe.Send(rq) 148 | return err 149 | } 150 | 151 | func handshakeHandleResponse(peer *Peer, pipe *Pipe, msg *Message) error { 152 | if !isHandshakeMsg(msg) { 153 | return errors.Errorf("invalid handshake message | peer: %s", peer.RemoteAddress()) 154 | } 155 | 156 | content := msg.PayloadGet() 157 | 158 | result := content[len(prefixHandshake):] 159 | 160 | key, err := crypt.PubFromBytes(result) 161 | if err != nil { 162 | return err 163 | } 164 | peer.Metadata().Put(headerKeyPubKey, key) 165 | return err 166 | } 167 | -------------------------------------------------------------------------------- /middleware_headers.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/emirpasic/gods/maps/hashmap" 9 | ) 10 | 11 | // Headers creates the *headers* middleware store the Message.Annotations() within the payload. 12 | // With this middleware you can provide (http protocol) "header" like 13 | // behavior into your communication. 14 | // You can use it to annotate messages with id's or other information 15 | func Headers() (string, MiddlewareFunc) { 16 | return "headers", middlewareHeadersImpl 17 | } 18 | 19 | func getSizeBuffer(data []byte) []byte { 20 | size := uint32(len(data)) 21 | sizeBuffer := make([]byte, 4) 22 | binary.BigEndian.PutUint32(sizeBuffer, size) 23 | return sizeBuffer 24 | } 25 | 26 | func middlewareHeadersImpl(peer *Peer, pipe *Pipe, msg *Message) (MiddlewareResult, error) { 27 | annotations, ok := msg.Metadata().(*hashmap.Map) 28 | if !ok { 29 | panic("could not cast annotations to *hashmap.Map") 30 | } 31 | 32 | if pipe.Operation() == Send { 33 | body := msg.PayloadGet() 34 | 35 | header, err := annotations.ToJSON() 36 | if err != nil { 37 | return Next, errors.Wrap(err, "could not serialize annotations to json") 38 | } 39 | 40 | headerSize := getSizeBuffer(header) 41 | bodySize := getSizeBuffer(body) 42 | 43 | full := append(headerSize, bodySize...) 44 | full = append(full, header...) 45 | full = append(full, body...) 46 | 47 | msg.PayloadSet(full) 48 | } else if pipe.Operation() == Receive { 49 | full := msg.PayloadGet() 50 | 51 | headerSizeData := full[:4] 52 | // bodySizeData := full[4:8] 53 | 54 | headerSize := binary.BigEndian.Uint32(headerSizeData) 55 | // bodySize := binary.BigEndian.Uint32(bodySizeData) 56 | 57 | header := full[8 : 8+headerSize] 58 | body := full[8+headerSize:] 59 | 60 | msg.PayloadSet(body) 61 | err := annotations.FromJSON(header) 62 | 63 | return Next, errors.Wrap(err, "could not deserialize annotations from json") 64 | } 65 | 66 | return Next, nil 67 | } 68 | -------------------------------------------------------------------------------- /middleware_log.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Log creates a logging middleware for in and outgoing messages 8 | func Log() (string, MiddlewareFunc) { 9 | return "log", middlewareLogImpl 10 | } 11 | 12 | func middlewareLogImpl(peer *Peer, pipe *Pipe, msg *Message) (MiddlewareResult, error) { 13 | directions := make(map[PipeOperation]string) 14 | directions[Send] = "out->" 15 | directions[Receive] = "<--in" 16 | 17 | txt := fmt.Sprintf("%s %s (%d bytes) - local endpoint: %s", peer.RemoteAddress(), directions[pipe.Operation()], len(msg.PayloadGet()), peer.LocalAddress()) 18 | newLogger("middleware_log").Debug(txt) 19 | 20 | return Next, nil 21 | } 22 | -------------------------------------------------------------------------------- /middleware_routes.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import "github.com/sirupsen/logrus" 4 | 5 | var annotationKey = "middleware.routes" 6 | 7 | // RoutingTable represents handler registered by a path. 8 | // A message will be checked for the existence of an annotation with the name "__routes_path" 9 | // and this value will be used to find a route within the routing table 10 | type RoutingTable *map[string]func(peer *Peer, msg *Message) 11 | 12 | // EmptyRoutesTable is a table without any routes 13 | var EmptyRoutesTable = *new(RoutingTable) 14 | 15 | // Routes provides an route based middleware 16 | // You can listen to specific endpoints and send messages to them 17 | // This is similar to a controller/action pattern in HTTP frameworks 18 | func Routes(rt RoutingTable) (string, MiddlewareFunc) { 19 | if rt == EmptyRoutesTable { 20 | return "routes", func(peer *Peer, pipe *Pipe, msg *Message) (MiddlewareResult, error) { 21 | return Next, nil 22 | } 23 | } 24 | 25 | f := func(peer *Peer, pipe *Pipe, msg *Message) (MiddlewareResult, error) { 26 | op, err := middlewareRoutesImpl(rt, peer, pipe, msg) 27 | return op, err 28 | } 29 | return "routes", f 30 | } 31 | 32 | // NewMessageRoutedFromString creates a new routed message to the handler given by path 33 | // with the provided string content 34 | func NewMessageRoutedFromString(path string, content string) *Message { 35 | msg := NewMessageRoutedFromData(path, []byte(content)) 36 | return msg 37 | } 38 | 39 | // NewMessageRoutedFromData creates a new routed message to the handler given by path 40 | // with the provided data 41 | func NewMessageRoutedFromData(path string, data []byte) *Message { 42 | msg := NewMessageFromData(data) 43 | msg.Metadata().Put(annotationKey, path) 44 | return msg 45 | } 46 | 47 | func middlewareRoutesImpl(rt RoutingTable, peer *Peer, pipe *Pipe, msg *Message) (MiddlewareResult, error) { 48 | var log = newLogger("middleware_routes") 49 | if pipe.Operation() == Send { 50 | return Next, nil 51 | } 52 | 53 | routeHdr, found := msg.Metadata().Get(annotationKey) 54 | if !found { 55 | log.Debugf("msg has no %s key, skip routing", annotationKey) 56 | return Next, nil 57 | } 58 | 59 | routeStr := routeHdr.(string) 60 | route, hasRoute := (*rt)[routeStr] 61 | if !hasRoute { 62 | log.WithFields(logrus.Fields{ 63 | "route": route, 64 | "table": rt, 65 | }).Warn("found routing key in message, but miss value in routing table") 66 | return Next, nil 67 | } 68 | 69 | log.WithField("route", routeStr).Debug("execute route") 70 | go route(peer, msg) 71 | 72 | return Next, nil 73 | } 74 | -------------------------------------------------------------------------------- /mock_Adapter_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package go2p 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // MockAdapter is an autogenerated mock type for the Adapter type 8 | type MockAdapter struct { 9 | mock.Mock 10 | } 11 | 12 | // Address provides a mock function with given fields: 13 | func (_m *MockAdapter) Address() string { 14 | ret := _m.Called() 15 | 16 | var r0 string 17 | if rf, ok := ret.Get(0).(func() string); ok { 18 | r0 = rf() 19 | } else { 20 | r0 = ret.Get(0).(string) 21 | } 22 | 23 | return r0 24 | } 25 | 26 | // Close provides a mock function with given fields: 27 | func (_m *MockAdapter) Close() { 28 | _m.Called() 29 | } 30 | 31 | // ReadMessage provides a mock function with given fields: 32 | func (_m *MockAdapter) ReadMessage() (*Message, error) { 33 | ret := _m.Called() 34 | 35 | var r0 *Message 36 | if rf, ok := ret.Get(0).(func() *Message); ok { 37 | r0 = rf() 38 | } else { 39 | if ret.Get(0) != nil { 40 | r0 = ret.Get(0).(*Message) 41 | } 42 | } 43 | 44 | var r1 error 45 | if rf, ok := ret.Get(1).(func() error); ok { 46 | r1 = rf() 47 | } else { 48 | r1 = ret.Error(1) 49 | } 50 | 51 | return r0, r1 52 | } 53 | 54 | // WriteMessage provides a mock function with given fields: m 55 | func (_m *MockAdapter) WriteMessage(m *Message) error { 56 | ret := _m.Called(m) 57 | 58 | var r0 error 59 | if rf, ok := ret.Get(0).(func(*Message) error); ok { 60 | r0 = rf(m) 61 | } else { 62 | r0 = ret.Error(0) 63 | } 64 | 65 | return r0 66 | } 67 | -------------------------------------------------------------------------------- /mock_extensions_test.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | ) 6 | 7 | func (a *MockAdapter) SetupReadWithResponse(c *mock.Call, response string) chan struct{} { 8 | result := make(chan struct{}) 9 | // timeCalled := 0 10 | 11 | c.Run(func(arg mock.Arguments) { 12 | // if timeCalled == 0 { 13 | // timeCalled++ 14 | m := NewMessageFromData([]byte(response)) 15 | c.Return(m, nil) 16 | // } else if timeCalled == 1 { 17 | // timeCalled++ 18 | // m := NewMessageFromData([]byte(response)) 19 | // c.Return(m, nil) 20 | // close(result) 21 | // } else { 22 | // c.Return(nil, nil) 23 | // } 24 | }) 25 | 26 | return result 27 | } 28 | 29 | func (a *MockAdapter) SetupClose(readCall *mock.Call, res error) { 30 | a.On("Close").Return().Run(func(arg mock.Arguments) { 31 | readCall.Run(func(arg mock.Arguments) { 32 | readCall.Return(nil, res) 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /network_connection.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | /* 8 | NewNetworkConnectionTCP provides a full configured TCP based network 9 | It use the _DefaultMiddleware_ a TCP based operator and the following middleware: 10 | 11 | Routes, Headers, Crypt, Log 12 | 13 | 14 | */ 15 | func NewNetworkConnectionTCP(localAddr string, routes RoutingTable) *NetworkConnection { 16 | op := NewTCPOperator("tcp", localAddr) 17 | 18 | conn := NewNetworkConnection(). 19 | WithOperator(op). 20 | WithMiddleware(Routes(routes)). 21 | WithMiddleware(Headers()). 22 | WithMiddleware(Crypt()). 23 | WithMiddleware(Log()). 24 | Build() 25 | 26 | return conn 27 | } 28 | 29 | // NetworkConnection is the main entry point to the p2p network 30 | type NetworkConnection struct { 31 | middlewares middlewares 32 | operators []PeerOperator 33 | emitter *eventEmitter 34 | log *logrus.Entry 35 | peers *peers 36 | } 37 | 38 | // Send will send the provided message to the given address 39 | func (nc *NetworkConnection) Send(msg *Message, addr string) { 40 | nc.peers.lock(addr, func(peer *Peer) { 41 | nc.log.WithFields(logrus.Fields{ 42 | "local": peer.LocalAddress(), 43 | "remote": peer.RemoteAddress(), 44 | "len": len(msg.PayloadGet()), 45 | }).Debug("send messag") 46 | 47 | peer.send <- msg 48 | }) 49 | } 50 | 51 | // SendBroadcast will send the given message to all peers 52 | func (nc *NetworkConnection) SendBroadcast(msg *Message) { 53 | nc.peers.iteratePeer(func(peer *Peer) { 54 | nc.log.WithFields(logrus.Fields{ 55 | "local": peer.LocalAddress(), 56 | "remote": peer.RemoteAddress(), 57 | "len": len(msg.PayloadGet()), 58 | }).Debug("send messag") 59 | 60 | peer.send <- msg 61 | }) 62 | } 63 | 64 | // ConnectTo will Dial the provided peer by the given network 65 | func (nc *NetworkConnection) ConnectTo(network string, addr string) { 66 | for _, op := range nc.operators { 67 | nc.log.WithFields(logrus.Fields{ 68 | "network": network, 69 | "addr": addr, 70 | }).Debug("dial peer") 71 | 72 | op.Dial(network, addr) 73 | } 74 | } 75 | 76 | // DisconnectFrom will disconnects the given peer 77 | func (nc *NetworkConnection) DisconnectFrom(addr string) { 78 | nc.peers.lock(addr, func(peer *Peer) { 79 | nc.log.WithFields(logrus.Fields{ 80 | "local": peer.LocalAddress(), 81 | "remote": peer.RemoteAddress(), 82 | }).Debug("disconnect") 83 | 84 | peer.stop() 85 | go func(n *NetworkConnection, p *Peer) { 86 | n.peers.rm(p) 87 | }(nc, peer) 88 | }) 89 | } 90 | 91 | // Start will start up the p2p network stack 92 | func (nc *NetworkConnection) Start() error { 93 | nc.log.Debug("start network") 94 | 95 | // nc.peerStore.OnPeerAdd(func(peer *Peer) { 96 | // nc.emitter.EmitAsync("peer-new", peer) 97 | // }) 98 | // nc.peerStore.OnPeerWantRemove(func(peer *Peer) { 99 | // peer.stop() 100 | // nc.peerStore.RemovePeer(peer) 101 | // }) 102 | 103 | for _, op := range nc.operators { 104 | op.OnPeer(func(a Adapter) { 105 | p := newPeer(a, nc.middlewares) 106 | nc.peers.add(p) 107 | 108 | p.emitter.On("message", func(args []interface{}) { 109 | nc.emitter.EmitAsync("peer-message", args...) 110 | }) 111 | p.emitter.On("disconnect", func(args []interface{}) { 112 | p := args[0].(*Peer) 113 | p.stop() 114 | nc.peers.rm(p) 115 | nc.emitter.EmitAsync("peer-disconnect", p) 116 | }) 117 | p.emitter.On("error", func(args []interface{}) { 118 | p := args[0].(*Peer) 119 | err := args[1].(error) 120 | p.stop() 121 | nc.peers.rm(p) 122 | nc.emitter.EmitAsync("peer-error", p, err) 123 | }) 124 | 125 | <-p.start() 126 | 127 | nc.emitter.EmitAsync("peer-connect", p) 128 | }) 129 | 130 | err := op.Start() 131 | if err != nil { 132 | return err 133 | } 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // OnPeer registers the provided handler and call it when a new peer connection is created 140 | func (nc *NetworkConnection) OnPeer(handler func(p *Peer)) { 141 | nc.emitter.On("peer-connect", func(args []interface{}) { 142 | handler(args[0].(*Peer)) 143 | }) 144 | } 145 | 146 | // OnMessage regsiters the given handler and call it when a new message is received 147 | func (nc *NetworkConnection) OnMessage(handler func(p *Peer, msg *Message)) { 148 | nc.emitter.On("peer-message", func(args []interface{}) { 149 | handler(args[0].(*Peer), args[1].(*Message)) 150 | }) 151 | } 152 | 153 | // OnPeerError regsiters the given handler and call it when an error 154 | // during the peer communication occurs 155 | func (nc *NetworkConnection) OnPeerError(handler func(p *Peer, err error)) { 156 | nc.emitter.On("peer-error", func(args []interface{}) { 157 | handler(args[0].(*Peer), args[1].(error)) 158 | }) 159 | } 160 | 161 | // OnPeerDisconnect regsiters the given handler and call it when an the connection 162 | // is lost 163 | func (nc *NetworkConnection) OnPeerDisconnect(handler func(p *Peer)) { 164 | nc.emitter.On("peer-disconnect", func(args []interface{}) { 165 | handler(args[0].(*Peer)) 166 | }) 167 | } 168 | 169 | // Stop will shutdown the entire p2p network stack 170 | func (nc *NetworkConnection) Stop() { 171 | for _, op := range nc.operators { 172 | op.Stop() 173 | } 174 | 175 | nc.peers.iteratePeer(func(p *Peer) { 176 | nc.peers.rm(p) 177 | p.stop() 178 | }) 179 | } 180 | -------------------------------------------------------------------------------- /network_connection_builder.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | // NetworkConnectionBuilder provides a fluent interface to 4 | // create a NetworkConnection 5 | type NetworkConnectionBuilder struct { 6 | middlewares []*Middleware 7 | operators []PeerOperator 8 | } 9 | 10 | // NewNetworkConnection creates a new NetworkBuilder instance to setup a new NetworkConnection 11 | func NewNetworkConnection() *NetworkConnectionBuilder { 12 | b := new(NetworkConnectionBuilder) 13 | 14 | return b 15 | } 16 | 17 | // WithMiddleware attach a new Middleware to the NetworkConnection setup 18 | func (b *NetworkConnectionBuilder) WithMiddleware(name string, impl MiddlewareFunc) *NetworkConnectionBuilder { 19 | m := NewMiddleware(name, impl) 20 | b.middlewares = append(b.middlewares, m) 21 | return b 22 | } 23 | 24 | // WithOperator attach a new PeerOperator to the NetworkConnection setup 25 | func (b *NetworkConnectionBuilder) WithOperator(op PeerOperator) *NetworkConnectionBuilder { 26 | b.operators = append(b.operators, op) 27 | return b 28 | } 29 | 30 | // Build finalize the NetworkConnection setup and creates the new instance 31 | func (b *NetworkConnectionBuilder) Build() *NetworkConnection { 32 | nc := new(NetworkConnection) 33 | nc.peers = newPeers() 34 | nc.middlewares = newMiddlewares(b.middlewares...) 35 | nc.operators = b.operators 36 | nc.emitter = newEventEmitter() 37 | nc.log = newLogger("network-connection") 38 | 39 | return nc 40 | } 41 | -------------------------------------------------------------------------------- /network_connection_test.go: -------------------------------------------------------------------------------- 1 | package go2p_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/phayes/freeport" 11 | "github.com/pkg/errors" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/v-braun/go2p" 14 | ) 15 | 16 | type chatProtocoll []struct { 17 | out string 18 | in string 19 | } 20 | 21 | type networkConnWithAddress struct { 22 | net *go2p.NetworkConnection 23 | addr string 24 | fullAddr string 25 | } 26 | 27 | func getChatProtocoll() chatProtocoll { 28 | messages := []struct { 29 | out string 30 | in string 31 | }{{ 32 | out: "hello", 33 | in: "hi", 34 | }, { 35 | out: "how are you", 36 | in: "fine", 37 | }, { 38 | out: "nice to meet you", 39 | in: "you 2", 40 | }, { 41 | out: "bye", 42 | in: "see ya", 43 | }} 44 | 45 | return messages 46 | } 47 | 48 | func createTestNetworks(t *testing.T, routing go2p.RoutingTable) (*networkConnWithAddress, *networkConnWithAddress) { 49 | p1, err := freeport.GetFreePort() 50 | assert.NoError(t, err) 51 | 52 | p2, err := freeport.GetFreePort() 53 | assert.NoError(t, err) 54 | 55 | addr1 := fmt.Sprintf("127.0.0.1:%d", p1) 56 | addr2 := fmt.Sprintf("127.0.0.1:%d", p2) 57 | 58 | conn1 := go2p.NewNetworkConnectionTCP(addr1, routing) 59 | conn2 := go2p.NewNetworkConnectionTCP(addr2, routing) 60 | 61 | return &networkConnWithAddress{net: conn1, addr: addr1, fullAddr: "tcp:" + addr1}, &networkConnWithAddress{net: conn2, addr: addr2, fullAddr: "tcp:" + addr2} 62 | } 63 | 64 | func startNetworks(t *testing.T, networks ...*go2p.NetworkConnection) bool { 65 | for _, n := range networks { 66 | err := n.Start() 67 | if !assert.NoError(t, err) { 68 | return false 69 | } 70 | } 71 | 72 | return true 73 | } 74 | 75 | func registerPeerErrorHandlers(t *testing.T, networks ...*go2p.NetworkConnection) { 76 | for i, n := range networks { 77 | n.OnPeerError(func(p *go2p.Peer, err error) { 78 | msg := fmt.Sprintf("conn%d err: %+v", i, errors.Wrap(err, "unexpected peer error")) 79 | fmt.Println(msg) 80 | t.Fatal(msg) 81 | }) 82 | 83 | // n.OnPeerDisconnect(func(p *go2p.Peer) { 84 | // msg := fmt.Sprintf("conn%d disconnect: %+v\n", i, p.RemoteAddress()) 85 | // fmt.Println(msg) 86 | // t.Fatal(msg) 87 | // }) 88 | } 89 | } 90 | 91 | func TestChat(t *testing.T) { 92 | messages := getChatProtocoll() 93 | conn1, conn2 := createTestNetworks(t, go2p.EmptyRoutesTable) 94 | 95 | testDone := new(sync.WaitGroup) 96 | testDone.Add(1) 97 | 98 | conn1.net.OnPeer(func(p *go2p.Peer) { 99 | fmt.Printf("%s got peer %s\n", p.LocalAddress(), p.RemoteAddress()) 100 | msg := go2p.NewMessage() 101 | msg.PayloadSetString(messages[0].in) 102 | conn1.net.Send(msg, p.RemoteAddress()) 103 | }) 104 | 105 | conn1.net.OnMessage(func(p *go2p.Peer, m *go2p.Message) { 106 | txt := m.PayloadGetString() 107 | assert.Equal(t, messages[0].out, txt) 108 | 109 | fmt.Printf("%s got %s\n", "conn1", messages[0]) 110 | messages = messages[1:] 111 | if len(messages) == 0 { 112 | testDone.Done() 113 | } else { 114 | conn1.net.Send(go2p.NewMessageFromString(messages[0].in), p.RemoteAddress()) 115 | } 116 | 117 | }) 118 | 119 | conn2.net.OnMessage(func(p *go2p.Peer, m *go2p.Message) { 120 | txt := m.PayloadGetString() 121 | assert.Equal(t, messages[0].in, txt) 122 | 123 | fmt.Printf("%s got %s\n", "conn2", messages[0]) 124 | conn2.net.Send(go2p.NewMessageFromString(messages[0].out), p.RemoteAddress()) 125 | }) 126 | 127 | registerPeerErrorHandlers(t, conn1.net, conn2.net) 128 | if !startNetworks(t, conn1.net, conn2.net) { 129 | return 130 | } 131 | 132 | conn1.net.ConnectTo("tcp", conn2.addr) 133 | 134 | testDone.Wait() 135 | 136 | conn1.net.Stop() 137 | conn2.net.Stop() 138 | } 139 | 140 | func TestRouting(t *testing.T) { 141 | 142 | wgPings := &sync.WaitGroup{} 143 | wgPongs := &sync.WaitGroup{} 144 | 145 | sendPings := 3 146 | sendPongs := 3 147 | 148 | var conn1 *networkConnWithAddress 149 | var conn2 *networkConnWithAddress 150 | 151 | wgPings.Add(sendPings) 152 | wgPongs.Add(sendPongs) 153 | conn1, conn2 = createTestNetworks(t, &map[string]func(peer *go2p.Peer, msg *go2p.Message){ 154 | "play": func(peer *go2p.Peer, msg *go2p.Message) { 155 | // if peer.Address() != conn2.fullAddr { 156 | // assert.FailNow(t, "unexpected pong from addr: %s. conn1: %s conn2: %s", peer.Address(), conn1.addr, conn2.addr) 157 | // } 158 | 159 | if msg.PayloadGetString() == "ping" { 160 | fmt.Printf("ping %s -> %s \n", peer.RemoteAddress(), peer.LocalAddress()) 161 | wgPings.Done() 162 | if sendPongs > 0 { 163 | sendPongs -= 1 164 | conn2.net.Send(go2p.NewMessageRoutedFromString("play", "pong"), peer.RemoteAddress()) 165 | } 166 | } else if msg.PayloadGetString() == "pong" { 167 | fmt.Printf("pong %s -> %s \n", peer.RemoteAddress(), peer.LocalAddress()) 168 | // if peer.Address() != conn1.fullAddr { 169 | // assert.FailNow(t, "unexpected pong from addr: %s. conn1: %s conn2: %s", peer.Address(), conn1.addr, conn2.addr) 170 | // } 171 | 172 | wgPongs.Done() 173 | if sendPings > 0 { 174 | sendPings -= 1 175 | conn1.net.SendBroadcast(go2p.NewMessageRoutedFromString("play", "ping")) 176 | // conn1.net.Send(go2p.NewMessageRoutedFromString("play", "ping"), peer.RemoteAddress()) 177 | } 178 | } 179 | 180 | }, 181 | }) 182 | 183 | peerConnectedWg := sync.WaitGroup{} 184 | peerConnectedWg.Add(2) 185 | conn1.net.OnPeer(func(peer *go2p.Peer) { 186 | peerConnectedWg.Done() 187 | fmt.Printf("new peer on: %s with addr: %s\n", peer.LocalAddress(), peer.RemoteAddress()) 188 | }) 189 | 190 | conn2.net.OnPeer(func(peer *go2p.Peer) { 191 | peerConnectedWg.Done() 192 | fmt.Printf("new peer on: %s with addr: %s\n", peer.LocalAddress(), peer.RemoteAddress()) 193 | }) 194 | 195 | registerPeerErrorHandlers(t, conn1.net, conn2.net) 196 | if !startNetworks(t, conn1.net, conn2.net) { 197 | return 198 | } 199 | 200 | conn1.net.ConnectTo("tcp", conn2.addr) 201 | peerConnectedWg.Wait() 202 | conn1.net.Send(go2p.NewMessageFromString("not routed message"), conn2.fullAddr) 203 | conn1.net.Send(go2p.NewMessageRoutedFromString("play", "ping"), conn2.fullAddr) 204 | 205 | wgPings.Wait() 206 | wgPongs.Wait() 207 | 208 | conn1.net.Stop() 209 | conn2.net.Stop() 210 | } 211 | 212 | func TestLargeMessages(t *testing.T) { 213 | var conn1 *networkConnWithAddress 214 | var conn2 *networkConnWithAddress 215 | largeMsg := genLargeMessage(256) 216 | testDone := sync.WaitGroup{} 217 | testDone.Add(1) 218 | conn1, conn2 = createTestNetworks(t, &map[string]func(peer *go2p.Peer, msg *go2p.Message){ 219 | "say": func(peer *go2p.Peer, msg *go2p.Message) { 220 | assert.Equal(t, largeMsg, msg.PayloadGetString()) 221 | testDone.Done() 222 | }, 223 | }) 224 | 225 | peerConnectedWg := sync.WaitGroup{} 226 | peerConnectedWg.Add(2) 227 | conn1.net.OnPeer(func(peer *go2p.Peer) { 228 | peerConnectedWg.Done() 229 | }) 230 | 231 | conn2.net.OnPeer(func(peer *go2p.Peer) { 232 | peerConnectedWg.Done() 233 | }) 234 | 235 | registerPeerErrorHandlers(t, conn1.net, conn2.net) 236 | if !startNetworks(t, conn1.net, conn2.net) { 237 | return 238 | } 239 | 240 | conn1.net.ConnectTo("tcp", conn2.addr) 241 | peerConnectedWg.Wait() 242 | conn1.net.Send(go2p.NewMessageRoutedFromString("say", largeMsg), conn2.fullAddr) 243 | 244 | testDone.Wait() 245 | 246 | conn1.net.Stop() 247 | conn2.net.Stop() 248 | } 249 | 250 | func TestDisconnect(t *testing.T) { 251 | 252 | var conn1 *networkConnWithAddress 253 | var conn2 *networkConnWithAddress 254 | 255 | conn1, conn2 = createTestNetworks(t, &map[string]func(peer *go2p.Peer, msg *go2p.Message){}) 256 | 257 | peerConnectedWg := sync.WaitGroup{} 258 | peerConnectedWg.Add(2) 259 | 260 | var conn2Peer *go2p.Peer = nil 261 | conn1.net.OnPeer(func(peer *go2p.Peer) { 262 | peerConnectedWg.Done() 263 | conn2Peer = peer 264 | }) 265 | 266 | conn2.net.OnPeer(func(peer *go2p.Peer) { 267 | peerConnectedWg.Done() 268 | }) 269 | 270 | registerPeerErrorHandlers(t, conn1.net, conn2.net) 271 | if !startNetworks(t, conn1.net, conn2.net) { 272 | return 273 | } 274 | 275 | conn1.net.ConnectTo("tcp", conn2.addr) 276 | peerConnectedWg.Wait() 277 | 278 | testDone := sync.WaitGroup{} 279 | testDone.Add(1) 280 | conn1.net.OnPeerDisconnect(func(peer *go2p.Peer) { 281 | testDone.Done() 282 | }) 283 | 284 | conn1.net.DisconnectFrom(conn2Peer.RemoteAddress()) 285 | 286 | testDone.Wait() 287 | 288 | conn1.net.Stop() 289 | 290 | } 291 | 292 | func genLargeMessage(chars int) string { 293 | charset := "abcdefghijklmnopqrstuvwxyz" + 294 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 295 | rnd := rand.New(rand.NewSource(time.Now().UnixNano())) 296 | 297 | b := make([]byte, chars) 298 | for i := range b { 299 | b[i] = charset[rnd.Intn(len(charset))] 300 | } 301 | return string(b) 302 | } 303 | -------------------------------------------------------------------------------- /peer.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "github.com/v-braun/awaiter" 5 | 6 | "github.com/emirpasic/gods/maps" 7 | "github.com/emirpasic/gods/maps/hashmap" 8 | ) 9 | 10 | // Peer represents a connection to a remote peer 11 | type Peer struct { 12 | io *adapterIO 13 | send chan *Message 14 | middleware middlewares 15 | emitter *eventEmitter 16 | metadata maps.Map 17 | awaiter awaiter.Awaiter 18 | } 19 | 20 | func newPeer(adapter Adapter, middleware middlewares) *Peer { 21 | p := new(Peer) 22 | p.send = make(chan *Message, 10) 23 | p.io = newAdapterIO(adapter) 24 | p.awaiter = awaiter.New() 25 | p.middleware = middleware 26 | p.metadata = hashmap.New() 27 | p.emitter = newEventEmitter() 28 | 29 | return p 30 | } 31 | 32 | func (p *Peer) start() <-chan struct{} { 33 | done := make(chan struct{}) 34 | p.io.emitter.On("disconnect", func(args []interface{}) { 35 | p.emitter.EmitAsync("disconnect", p) 36 | }) 37 | p.io.emitter.On("error", func(args []interface{}) { 38 | p.emitter.EmitAsync("error", p, args[0]) 39 | }) 40 | 41 | p.io.start() 42 | 43 | p.awaiter.Go(func() { 44 | close(done) 45 | for { 46 | select { 47 | case m := <-p.io.receive: 48 | p.processPipe(m, Receive) 49 | continue 50 | case m := <-p.send: 51 | p.processPipe(m, Send) 52 | continue 53 | case <-p.awaiter.CancelRequested(): 54 | return 55 | } 56 | } 57 | }) 58 | 59 | return done 60 | } 61 | 62 | func (p *Peer) processPipe(m *Message, op PipeOperation) { 63 | from := 0 64 | to := len(p.middleware) 65 | pos := 0 66 | if op == Receive { 67 | pos = to 68 | } 69 | 70 | pipe := newPipe(p, p.middleware, op, pos, from, to) 71 | err := pipe.process(m) 72 | 73 | if err == ErrPipeStopProcessing { 74 | return 75 | } 76 | 77 | if err != nil { 78 | p.io.handleError(err, "processPipe") 79 | p.stopInternal() 80 | return 81 | } 82 | 83 | if op == Receive { 84 | p.emitter.EmitAsync("message", p, m) 85 | } else { 86 | err := p.io.sendMsg(m) 87 | if err != nil { 88 | p.io.handleError(err, "processPipe") 89 | p.stopInternal() 90 | return 91 | } 92 | } 93 | 94 | } 95 | 96 | func (p *Peer) stopInternal() { 97 | p.io.adapter.Close() 98 | p.io.awaiter.Cancel() 99 | p.awaiter.Cancel() 100 | } 101 | 102 | func (p *Peer) stop() { 103 | p.stopInternal() 104 | p.io.awaiter.AwaitSync() 105 | p.awaiter.AwaitSync() 106 | } 107 | 108 | // RemoteAddress returns the remote address of the current peer 109 | func (p *Peer) RemoteAddress() string { 110 | return p.io.adapter.RemoteAddress() 111 | } 112 | 113 | // LocalAddress returns the local address of the current peer 114 | func (p *Peer) LocalAddress() string { 115 | return p.io.adapter.LocalAddress() 116 | } 117 | 118 | // Metadata returns a map of metadata associated to this peer 119 | func (p *Peer) Metadata() maps.Map { 120 | return p.metadata 121 | } 122 | -------------------------------------------------------------------------------- /peer_operator.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import "errors" 4 | 5 | // ErrInvalidNetwork represents an invalid network part in the given address 6 | var ErrInvalidNetwork = errors.New("invalid network") 7 | 8 | // PeerOperator connect peers to the current network connection 9 | // I provides functionalities for dialing (active connection) 10 | // and listening (passive connections) over a protocol (tcp/udp/etc) 11 | type PeerOperator interface { 12 | 13 | // Dial connects to the given address by the given network 14 | Dial(network string, addr string) error 15 | 16 | // OnPeer registers a handler function that should be called 17 | // when a new peer connection is established 18 | OnPeer(handler func(p Adapter)) 19 | 20 | // Start the background listening jobs for the operator 21 | Start() error 22 | 23 | // Stop the background listening jobs for the operator 24 | Stop() 25 | } 26 | -------------------------------------------------------------------------------- /peers.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // DefaultPeerStore is a basic implementation of a PeerStore 8 | // It limits simultaneously connected peers to a configured capacity 9 | type peers struct { 10 | peers []*Peer 11 | mutex *sync.Mutex 12 | } 13 | 14 | // NewDefaultPeerStore creates a new basic PeerStore that limits 15 | // simultaneously connected peers by the provided capacity 16 | func newPeers() *peers { 17 | p := new(peers) 18 | p.peers = make([]*Peer, 0) 19 | p.mutex = new(sync.Mutex) 20 | 21 | return p 22 | } 23 | 24 | // AddPeer adds the given peer to the store 25 | func (p *peers) add(peer *Peer) { 26 | p.mutex.Lock() 27 | defer p.mutex.Unlock() 28 | 29 | p.peers = append(p.peers, peer) 30 | } 31 | 32 | // RemovePeer will remove the given peer from the store 33 | func (p *peers) rm(peer *Peer) { 34 | p.mutex.Lock() 35 | defer p.mutex.Unlock() 36 | 37 | peerIdx := -1 38 | for i, p := range p.peers { 39 | if p == peer { 40 | peerIdx = i 41 | break 42 | } 43 | } 44 | 45 | if peerIdx != -1 { 46 | p.peers = append(p.peers[:peerIdx], p.peers[peerIdx+1:]...) 47 | } 48 | } 49 | 50 | // IteratePeer will call the given handler for each peer 51 | func (p *peers) iteratePeer(handler func(peer *Peer)) { 52 | p.mutex.Lock() 53 | peersCopy := make([]*Peer, len(p.peers)) 54 | copy(peersCopy, p.peers) 55 | p.mutex.Unlock() 56 | 57 | for _, p := range peersCopy { 58 | handler(p) 59 | } 60 | } 61 | 62 | func (p *peers) lock(addr string, handler func(peer *Peer)) { 63 | p.mutex.Lock() 64 | defer p.mutex.Unlock() 65 | 66 | for _, p := range p.peers { 67 | if p.io.adapter.RemoteAddress() == addr { 68 | handler(p) 69 | return 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pipe.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // PipeOperation represents the pipe direction (Send or Receive) 10 | type PipeOperation int 11 | 12 | const ( 13 | // Send represents an outgoing message pipe processing 14 | Send PipeOperation = iota 15 | // Receive represents an incoming message pipe processing 16 | Receive PipeOperation = iota 17 | ) 18 | 19 | func (po PipeOperation) String() string { 20 | return [...]string{"Send", "Receive"}[po] 21 | } 22 | 23 | // ErrPipeStopProcessing is returned when the pipe has stopped it execution 24 | var ErrPipeStopProcessing = errors.New("pipe stopped") 25 | 26 | // Pipe handles the processing of an message 27 | type Pipe struct { 28 | peer *Peer 29 | 30 | allActions middlewares 31 | executingActions middlewares 32 | op PipeOperation 33 | 34 | pos int // instruction pointer 35 | 36 | log *logrus.Entry 37 | } 38 | 39 | func newPipe(peer *Peer, allActions middlewares, op PipeOperation, pos int, fromPos int, toPos int) *Pipe { 40 | p := new(Pipe) 41 | 42 | p.op = op 43 | p.pos = pos 44 | p.allActions = allActions 45 | p.executingActions = allActions[fromPos:toPos] 46 | p.log = newLogger("pipe") 47 | 48 | p.peer = peer 49 | 50 | return p 51 | } 52 | func (p *Pipe) process(msg *Message) error { 53 | nextItems := p.executingActions.nextItems(p.op) 54 | 55 | for _, m := range nextItems { 56 | p.log.WithFields(logrus.Fields{ 57 | "name": m.name, 58 | "pos": m.pos, 59 | "msg-len": len(msg.PayloadGet()), 60 | "op": p.op.String(), 61 | }).Debug("execute middleware") 62 | 63 | res, err := m.execute(p.peer, p, msg) 64 | if err != nil { 65 | p.log.WithFields(logrus.Fields{ 66 | "name": m.name, 67 | "pos": m.pos, 68 | "msg-len": len(msg.PayloadGet()), 69 | "err": err, 70 | }).Error("middleware error") 71 | return err 72 | } else if res == Stop { 73 | return ErrPipeStopProcessing 74 | } 75 | 76 | if p.op == Send { 77 | p.pos++ 78 | } else { 79 | p.pos-- 80 | } 81 | 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // Send will send the provided message during the current pipe execution. 88 | // 89 | // The message goes only through middlewares that are after the current pipe position 90 | func (p *Pipe) Send(msg *Message) error { 91 | pos := p.pos + 1 92 | from := p.pos + 1 93 | to := len(p.allActions) 94 | 95 | if pos > to { 96 | err := p.peer.io.sendMsg(msg) 97 | return err 98 | } 99 | 100 | subPipe := newPipe(p.peer, p.allActions, Send, pos, from, to) 101 | 102 | if err := subPipe.process(msg); err != nil { 103 | return err 104 | } 105 | 106 | err := p.peer.io.sendMsg(msg) 107 | return err 108 | } 109 | 110 | // Receive will block the current call until a message was read from the peer or 111 | // an error occurs. 112 | // 113 | // The message goes only through middlewares that are after the current pipe position 114 | func (p *Pipe) Receive() (*Message, error) { 115 | msg, err := p.peer.io.receiveMsg() 116 | if err == nil && msg == nil { 117 | panic("unexpected nil result from peer.receive") 118 | } else if err != nil { 119 | return nil, err 120 | } else { 121 | pos := p.pos + 1 122 | from := p.pos + 1 123 | to := len(p.allActions) 124 | if pos <= to { 125 | subPipe := newPipe(p.peer, p.allActions, Receive, pos, from, to) 126 | err = subPipe.process(msg) 127 | } 128 | } 129 | 130 | return msg, err 131 | } 132 | 133 | // Operation returns the current pipe operation (Send or Receive) 134 | func (p *Pipe) Operation() PipeOperation { 135 | return p.op 136 | } 137 | -------------------------------------------------------------------------------- /pipe_test.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func TestMiddlewareError(t *testing.T) { 12 | f := func(peer *Peer, pipe *Pipe, msg *Message) (MiddlewareResult, error) { 13 | return Stop, errors.New("fail") 14 | } 15 | 16 | p := newPipe(nil, newMiddlewares(NewMiddleware("fail", f)), Send, 0, 0, 1) 17 | err := p.process(NewMessage()) 18 | assert.Error(t, err) 19 | } 20 | -------------------------------------------------------------------------------- /tcp_adapter.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type adapterTCP struct { 9 | conn net.Conn 10 | } 11 | 12 | // NewAdapter creates a new TCP adapter that wraps the given net.Conn instance 13 | func NewAdapter(conn net.Conn) Adapter { 14 | a := new(adapterTCP) 15 | a.conn = conn 16 | return a 17 | } 18 | 19 | func (a *adapterTCP) ReadMessage() (*Message, error) { 20 | m := NewMessage() 21 | err := m.ReadFromConn(a.conn) 22 | return m, err 23 | } 24 | 25 | func (a *adapterTCP) WriteMessage(m *Message) error { 26 | err := m.WriteIntoConn(a.conn) 27 | return err 28 | } 29 | 30 | func (a *adapterTCP) Close() { 31 | a.conn.Close() 32 | } 33 | 34 | func (a *adapterTCP) RemoteAddress() string { 35 | addr := a.conn.RemoteAddr() 36 | res := fmt.Sprintf("%s:%s", addr.Network(), addr.String()) 37 | return res 38 | } 39 | 40 | func (a *adapterTCP) LocalAddress() string { 41 | addr := a.conn.LocalAddr() 42 | res := fmt.Sprintf("%s:%s", addr.Network(), addr.String()) 43 | return res 44 | } 45 | -------------------------------------------------------------------------------- /tcp_operator.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | var _ PeerOperator = (*OperatorTCP)(nil) 11 | 12 | // OperatorTCP is an implementation of the PeerOperator inteface that handles 13 | // TCP based connections. 14 | // It use an net.Listener for incoming connections and and tcp.Dialer for outgoing. 15 | type OperatorTCP struct { 16 | emitter *eventEmitter 17 | server net.Listener 18 | ctx context.Context 19 | cancel context.CancelFunc 20 | 21 | localNetwok string 22 | localAddr string 23 | } 24 | 25 | // NewTCPOperator creates a new TCP based PeerOperator instance 26 | func NewTCPOperator(network string, localAddr string) *OperatorTCP { 27 | o := new(OperatorTCP) 28 | o.emitter = newEventEmitter() 29 | o.localNetwok = network 30 | o.localAddr = localAddr 31 | return o 32 | } 33 | 34 | // Dial connects to the address by the given network 35 | func (o *OperatorTCP) Dial(network string, addr string) error { 36 | if network != "tcp" { 37 | return ErrInvalidNetwork 38 | } 39 | 40 | conn, err := net.Dial(network, addr) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | adapter := NewAdapter(conn) 46 | o.emitter.EmitAsync("new-peer", adapter) 47 | return nil 48 | } 49 | 50 | // OnPeer registers the given handler and calls it when a new peer connection is 51 | // established 52 | func (o *OperatorTCP) OnPeer(handler func(p Adapter)) { 53 | o.emitter.On("new-peer", func(args []interface{}) { 54 | handler(args[0].(Adapter)) 55 | }) 56 | } 57 | 58 | // OnError registers the given handler and calls it when a peer error occurs 59 | func (o *OperatorTCP) OnError(handler func(err error)) { 60 | o.emitter.On("error", func(args []interface{}) { 61 | handler(args[0].(error)) 62 | }) 63 | } 64 | 65 | // Start will start the net.Listener and waits for incoming connections 66 | func (o *OperatorTCP) Start() error { 67 | if o.localNetwok != "tcp" { 68 | return ErrInvalidNetwork 69 | } 70 | 71 | listener, err := net.Listen(o.localNetwok, o.localAddr) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | o.ctx, o.cancel = context.WithCancel(context.Background()) 77 | 78 | o.server = listener 79 | go o.listen(o.ctx) 80 | return nil 81 | } 82 | 83 | // Stop will close the underlining net.Listener 84 | func (o *OperatorTCP) Stop() { 85 | o.cancel() 86 | o.server.Close() 87 | } 88 | 89 | func (o *OperatorTCP) listen(ctx context.Context) { 90 | go (func(o *OperatorTCP, ctx context.Context) { 91 | for { 92 | conn, err := o.server.Accept() 93 | if err == nil && conn != nil { 94 | adapter := NewAdapter(conn) 95 | o.emitter.EmitAsync("new-peer", adapter) 96 | } else if tmpErr, ok := err.(net.Error); ok && tmpErr.Temporary() { 97 | o.emitter.EmitAsync("error", errors.Wrap(err, "temp error during listening"), true) 98 | } else if err != nil && ctx.Err() == nil { 99 | o.emitter.EmitAsync("error", errors.Wrap(err, "fatal error, wil stop listening")) 100 | break 101 | } else if ctx.Err() != nil { 102 | break 103 | } 104 | } 105 | 106 | })(o, ctx) 107 | } 108 | -------------------------------------------------------------------------------- /tcp_operator_test.go: -------------------------------------------------------------------------------- 1 | package go2p 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/phayes/freeport" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestTCPOperatorNegativeCases(t *testing.T) { 13 | op := NewTCPOperator("ttt", "10.10.10.10") 14 | 15 | err := op.Dial("ttt", "foo") 16 | assert.Error(t, err) 17 | 18 | err = op.Dial("tcp", "foo") 19 | assert.Error(t, err) 20 | 21 | err = op.Start() 22 | assert.Error(t, err) 23 | 24 | op = NewTCPOperator("tcp", "foo") 25 | err = op.Start() 26 | assert.Error(t, err) 27 | 28 | port, _ := freeport.GetFreePort() 29 | op = NewTCPOperator("tcp", fmt.Sprintf("localhost:%d", port)) 30 | onErrCalled := new(sync.WaitGroup) 31 | onErrCalled.Add(1) 32 | op.OnError(func(err error) { 33 | assert.Error(t, err) 34 | onErrCalled.Done() 35 | }) 36 | 37 | op.Start() 38 | op.server.Close() 39 | 40 | onErrCalled.Wait() 41 | 42 | } 43 | 44 | func TestPingPong(t *testing.T) { 45 | clientsWg := new(sync.WaitGroup) 46 | clientsWg.Add(2) 47 | 48 | msgWg := new(sync.WaitGroup) 49 | msgWg.Add(2) 50 | 51 | op1 := NewTCPOperator("tcp", "127.0.0.1:3377") 52 | op2 := NewTCPOperator("tcp", "127.0.0.1:3378") 53 | 54 | conn1 := NewNetworkConnection(). 55 | WithOperator(op1). 56 | Build() 57 | 58 | conn2 := NewNetworkConnection(). 59 | WithOperator(op2). 60 | Build() 61 | 62 | conn1.OnPeer(func(p *Peer) { 63 | clientsWg.Done() 64 | 65 | if p.RemoteAddress() != "tcp:127.0.0.1:3378" { 66 | t.Fatal("unexpected address", p.RemoteAddress()) 67 | return 68 | } 69 | 70 | clientsWg.Wait() 71 | conn1.Send(NewMessageFromData([]byte("hello")), p.RemoteAddress()) 72 | }) 73 | 74 | conn2.OnPeer(func(p *Peer) { 75 | clientsWg.Done() 76 | }) 77 | 78 | conn1.OnMessage(func(p *Peer, m *Message) { 79 | assert.Equal(t, "hello back", m.PayloadGetString()) 80 | fmt.Printf("from %s: %s\n", p.RemoteAddress(), m.PayloadGetString()) 81 | msgWg.Done() 82 | }) 83 | 84 | conn2.OnMessage(func(p *Peer, m *Message) { 85 | assert.Equal(t, "hello", m.PayloadGetString()) 86 | fmt.Printf("from %s: %s\n", p.RemoteAddress(), m.PayloadGetString()) 87 | go conn2.Send(NewMessageFromData([]byte("hello back")), p.RemoteAddress()) 88 | msgWg.Done() 89 | }) 90 | 91 | err := conn1.Start() 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | assert.NoError(t, err) 96 | 97 | err = conn2.Start() 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | conn1.ConnectTo("tcp", "localhost:3378") 103 | 104 | clientsWg.Wait() 105 | msgWg.Wait() 106 | 107 | conn1.Stop() 108 | conn2.Stop() 109 | } 110 | --------------------------------------------------------------------------------