├── .github ├── FUNDING.yml └── workflows │ ├── pull-request.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .goreleaser.yml ├── Formula.rb ├── LICENSE ├── Makefile ├── README.md ├── cmd └── pcp │ └── pcp.go ├── docs ├── demo-2021-01-27.gif ├── demo-2021-02-13.gif ├── docs │ └── pcp-logo.sketch ├── jesse-pinkman.jpg └── trautwein2021.pdf ├── gen.go ├── go.mod ├── go.sum ├── internal ├── format │ ├── format.go │ └── format_test.go ├── log │ └── log.go ├── mock │ ├── dht.go │ ├── discovery.go │ ├── ioutil.go │ ├── manet.go │ ├── time.go │ └── xdg.go └── wrap │ ├── dht.go │ ├── discovery.go │ ├── ioutil.go │ ├── manet.go │ ├── time.go │ └── xdg.go ├── pkg ├── config │ ├── config.go │ ├── settings.go │ └── settings_test.go ├── crypt │ ├── crypt.go │ ├── crypt_test.go │ ├── stream.go │ └── stream_test.go ├── dht │ ├── advertiser.go │ ├── advertiser_test.go │ ├── discoverer.go │ ├── discoverer_test.go │ ├── error.go │ ├── protocol.go │ └── protocol_test.go ├── mdns │ ├── advertiser.go │ ├── advertiser_test.go │ ├── discoverer.go │ └── protocol.go ├── node │ ├── node.go │ ├── pake.go │ ├── push.go │ ├── push_test.go │ ├── transfer.go │ └── transfer_test.go ├── pb │ ├── p2p.aux.go │ ├── p2p.pb.go │ └── p2p.proto ├── receive │ ├── cmd.go │ ├── node.go │ └── transfer.go ├── send │ ├── cmd.go │ └── node.go ├── service │ ├── service.go │ └── service_test.go └── words │ ├── words.go │ └── words_test.go └── test ├── transfer_dir └── file ├── transfer_dir_empty └── file ├── transfer_file └── file ├── transfer_file_subdir └── subdir │ └── file ├── transfer_link ├── file └── subdir │ └── file └── transfer_subdir └── subdir └── file /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: dennis-tra 4 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - main 5 | 6 | name: Pull Request 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | go-version: [1.15.x, 1.16.x] 12 | os: [ubuntu-latest, macos-latest] # windows-latest 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Installing Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - name: Checking out code 20 | uses: actions/checkout@v2 21 | - uses: actions/cache@v2 22 | with: 23 | path: | 24 | ~/go/pkg/mod 25 | ~/.cache/go-build 26 | ~/Library/Caches/go-build 27 | %LocalAppData%\go-build 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: | 30 | ${{ runner.os }}-go- 31 | - name: Running tests 32 | run: go test -v -short ./... 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 5 | 6 | name: Create Release 7 | jobs: 8 | release: 9 | name: Building Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checking out repository... 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 # required for the changelog to work correctly 16 | - name: Setting up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.16 20 | - name: Runnig GoReleaser 21 | uses: goreleaser/goreleaser-action@v2 22 | with: 23 | version: v0.157.0 24 | args: release --rm-dist 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_REPO_SCOPE }} 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Tests 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | go-version: [1.15.x, 1.16.x] 12 | os: [ubuntu-latest, macos-latest] # windows-latest 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Installing Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - name: Checking out code 20 | uses: actions/checkout@v2 21 | - uses: actions/cache@v2 22 | with: 23 | path: | 24 | ~/go/pkg/mod 25 | ~/.cache/go-build 26 | ~/Library/Caches/go-build 27 | %LocalAppData%\go-build 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: | 30 | ${{ runner.os }}-go- 31 | - name: Running tests 32 | run: go test -v -short ./... 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go ### 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | pcp$ 9 | 10 | out/** 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | .idea 19 | pcp 20 | 21 | dist 22 | 23 | ### Go Patch ### 24 | /vendor/ 25 | 26 | ### macOS ### 27 | # General 28 | .DS_Store 29 | .AppleDouble 30 | .LSOverride 31 | 32 | # Icon must end with two \r 33 | Icon 34 | 35 | # Thumbnails 36 | ._* 37 | 38 | # Files that might appear in the root of a volume 39 | .DocumentRevisions-V100 40 | .fseventsd 41 | .Spotlight-V100 42 | .TemporaryItems 43 | .Trashes 44 | .VolumeIcon.icns 45 | .com.apple.timemachine.donotpresent 46 | 47 | # Directories potentially created on remote AFP share 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | 54 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod download 6 | - go install github.com/golang/mock/mockgen@v1.5.0 7 | - go generate gen.go 8 | 9 | release: 10 | # Creat a draft commit that needs manual confirmation 11 | draft: true 12 | # If set to auto, will mark the release as not ready for 13 | # production if suffix contains -rc1 or similar. 14 | prerelease: auto 15 | name_template: "Release {{.Tag}}" 16 | 17 | builds: 18 | - main: ./cmd/pcp/pcp.go 19 | binary: pcp 20 | ldflags: -X main.RawVersion={{.RawVersion}} -X main.ShortCommit={{.ShortCommit}} 21 | env: 22 | - CGO_ENABLED=0 23 | goos: 24 | - darwin 25 | - linux 26 | # - windows 27 | - freebsd 28 | - netbsd 29 | - openbsd 30 | - dragonfly 31 | goarch: 32 | - amd64 33 | - 386 34 | - arm 35 | - arm64 36 | ignore: 37 | - goos: darwin 38 | goarch: 386 39 | nfpms: 40 | - formats: 41 | - apk 42 | - deb 43 | - rpm 44 | vendor: "dtrautwein.eu" 45 | homepage: "https://github.com/dennis-tra/pcp" 46 | maintainer: "Dennis Trautwein " 47 | description: "Command line peer-to-peer data transfer tool based on libp2p." 48 | license: "Apache License Version 2.0" 49 | file_name_template: "{{.ProjectName}}_{{.RawVersion}}_{{.Os}}-{{.Arch}}" 50 | replacements: 51 | darwin: macos 52 | archives: 53 | - format: tar.gz 54 | # format_overrides: 55 | # - goos: windows 56 | # format: zip 57 | replacements: 58 | darwin: macos 59 | 60 | checksum: 61 | name_template: 'checksums.txt' 62 | changelog: 63 | sort: desc 64 | brews: 65 | - tap: 66 | owner: dennis-tra 67 | name: homebrew-tap 68 | folder: Formula 69 | description: "pcp is a command line peer-to-peer data transfer tool based on libp2p." 70 | homepage: "https://github.com/dennis-tra/pcp" 71 | license: "Apache License Version 2.0" 72 | test: | 73 | system "#{bin}/croc --version" 74 | #scoop: 75 | # bucket: 76 | # owner: dennis-tra 77 | # name: scoop-bucket 78 | # homepage: "https://github.com/dennis-tra/pcp" 79 | # description: "pcp is a command line peer-to-peer data transfer tool based on libp2p." 80 | # license: "Apache License Version 2.0" 81 | -------------------------------------------------------------------------------- /Formula.rb: -------------------------------------------------------------------------------- 1 | class Pcp < Formula 2 | desc "📦 Command-line peer-to-peer data transfer tool based on libp2p" 3 | homepage "https://github.com/dennis-tra/pcp" 4 | url "https://github.com/dennis-tra/pcp/archive/refs/tags/v0.4.0.tar.gz" 5 | sha256 "09bf477afcca5aabd617c90f012063786715ab5715cce77a72f2a0ae758585ea" 6 | license "Apache-2.0" 7 | head "https://github.com/dennis-tra/pcp.git" 8 | 9 | depends_on "go" => :build 10 | 11 | def install 12 | system "go", "build", *std_go_args(:ldflags => "-X main.RawVersion=0.4.0 -X main.ShortCommit=7f638fe"), "cmd/pcp/pcp.go" 13 | end 14 | 15 | test do 16 | assert_match shell_output("#{bin}/pcp receive words-that-dont-exist", 1).chomp, "error: failed to initialize node: could not find all words in a single wordlist" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | all: clean install 4 | 5 | test: 6 | go test ./... 7 | 8 | build: 9 | go build -o dist/pcp cmd/pcp/pcp.go 10 | 11 | install: 12 | go install cmd/pcp/pcp.go 13 | 14 | format: 15 | gofumpt -w -l . 16 | 17 | proto: 18 | protoc -I=pkg/pb --go_out=pkg/pb --go_opt=paths=source_relative p2p.proto 19 | gofumpt -w -l ./pkg/pb/ 20 | 21 | tools: 22 | go install mvdan.cc/gofumpt@v0.1.1 23 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26.0 24 | go install github.com/golang/mock/mockgen@v1.5.0 25 | 26 | gen: 27 | go generate ./... 28 | 29 | # Remove only what we've created 30 | clean: 31 | rm -r dist 32 | 33 | .PHONY: all clean test install release proto format tools gen 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `pcp` - Peer Copy 2 | 3 | [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg)](https://github.com/RichardLitt/standard-readme) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/dennis-tra/pcp)](https://goreportcard.com/report/github.com/dennis-tra/pcp) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/de64b09a3731b8a8842b/maintainability)](https://codeclimate.com/github/dennis-tra/pcp/maintainability) 6 | [![Latest test suite run result](https://github.com/dennis-tra/pcp/actions/workflows/tests.yml/badge.svg)](https://github.com/dennis-tra/pcp/actions) 7 | [![Github Releases Download Count](https://img.shields.io/github/downloads/dennis-tra/pcp/total.svg)]() 8 | 9 | [comment]: <> ([![Test Coverage](https://api.codeclimate.com/v1/badges/de64b09a3731b8a8842b/test_coverage)](https://codeclimate.com/github/dennis-tra/pcp/test_coverage)) 10 | 11 | Command line peer-to-peer data transfer tool based on [libp2p](https://github.com/libp2p/go-libp2p). 12 | 13 | ![Demo animation](./docs/demo-2021-02-13.gif) 14 | 15 | _This tool was published at the [IFIP 2021](https://networking.ifip.org/2021/) conference. You can find the preprint [below](#research)._ 16 | 17 | ## Table of Contents 18 | 19 | - [Motivation](#motivation) 20 | - [Project Status](#project-status) 21 | - [How does it work?](#how-does-it-work) 22 | - [Usage](#usage) 23 | - [Install](#install) 24 | - [Release download](#release-download) | [From source](#from-source) | [Package managers](#package-managers) 25 | - [Development](#development) 26 | - [Protobuf definitions](#generate-protobuf-definitions) 27 | - [Feature Roadmap](#feature-roadmap) 28 | - [Related Efforts](#related-efforts) 29 | - [Maintainers](#maintainers) 30 | - [Acknowledgments](#acknowledgments) 31 | - [Research](#research) 32 | - [License](#license) 33 | 34 | ## Motivation 35 | 36 | There already exists a long list of file transfer tools (see [Related Efforts](#related-efforts)), so why bother 37 | building another one? The problem I had with the existing tools is that they rely on 38 | a [limited set of](https://github.com/schollz/croc/issues/289) [servers](https://magic-wormhole.readthedocs.io/en/latest/welcome.html#relays) 39 | to orchestrate peer matching and data relaying which poses a centralisation concern. Many of the usual centralisation 40 | vs. decentralisation arguments apply here, e.g. the servers are single points of failures, the service operator has the 41 | power over whom to serve and whom not, etc. Further, as 42 | this [recent issue in croc](https://github.com/schollz/croc/issues/289) shows, this is a real risk for sustainable 43 | operation of the provided service. 44 | 45 | [comment]: <> (The `identify` discovery mechanism serves the same role as `STUN`, but without the need for a set of `STUN` servers. The libp2p `Circuit Relay` protocol allows peers to communicate indirectly via a helpful intermediary peer that is found via the DHT. This replaces dedicated `TURN` servers.) 46 | 47 | ## Project Status 48 | 49 | The tool is in a very early stage, and I'm aware of performance, usability and **security** issues. Don't use it for anything serious. 50 | Although I criticised tools like [`magic-wormhole`](https://github.com/magic-wormhole/magic-wormhole) or [`croc`](https://github.com/schollz/croc) above, they are amazing and way more mature. 51 | 52 | There are also drawbacks with this approach: It's slower than established centralised methods if 53 | you want to transmit data across network boundaries. A DHT query to find your peer can easily take several minutes. 54 | Further, the bandwidth and geographic location of a potential relaying peer is not guaranteed which can lead to long transmission times. 55 | 56 | ## How does it work? 57 | 58 | When running `pcp send` you'll see four random words from a list of the Bitcoin improvement proposal [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md). 59 | There are lists in nine different languages of 2048 words each, currently only `english` is supported. 60 | The first word is interpreted as a channel ID in the range from 0 to 2047. 61 | `pcp` advertises the identifier `/pcp/{unix-timestamp}/{channel-id}` in its local network via [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) and a hashed version of this string in the [DHT](https://en.wikipedia.org/wiki/Distributed_hash_table) of [IPFS](https://en.wikipedia.org/wiki/InterPlanetary_File_System). 62 | The unix timestamp is the current time truncated to 5 minutes and the prefix `/pcp` is the protocol prefix. 63 | _In the future:_ When you enter a new 5-minute interval while `pcp send` is running it advertises an updated identifier. 64 | 65 | To receive the file your peer enters `pcp receive four-words-from-above` and `pcp` uses the first word together with the current time truncated to 5 minutes to find the sending peer in the [DHT](https://en.wikipedia.org/wiki/Distributed_hash_table) and in your local network via [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS). 66 | It also searches for an identifier of the previous 5-minute interval. 67 | As soon as the peer is found, both do a password authenticated key exchange ([PAKE](https://en.wikipedia.org/wiki/Password-authenticated_key_agreement)) to authenticate each other. 68 | In this procedure a comparably weak password (`four-words-from-above`) gets replaced with a strong session key that is used to encrypt all future communication. 69 | The default [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) encryption that [libp2p](https://github.com/libp2p/go-libp2p) provides is not sufficient in this case as we could still, in theory, talk to a wrong peer - just encrypted. 70 | 71 | After the peer is authenticated the receiver must confirm the file transfer, and the file gets transmitted. 72 | 73 | ## Usage 74 | 75 | The sending peer runs: 76 | 77 | ```shell 78 | $ pcp send my_file 79 | Code is: bubble-enemy-result-increase 80 | On the other machine run: 81 | pcp receive bubble-enemy-result-increase 82 | ``` 83 | 84 | The receiving peer runs: 85 | 86 | ```shell 87 | $ pcp receive bubble-enemy-result-increase 88 | Looking for peer bubble-enemy-result-increase... 89 | ``` 90 | 91 | If you're on different networks the lookup can take quite long (~ 2-3 minutes). Currently, there is no output while both parties are working on peer discovery, so just be very patient. 92 | 93 | ## Install 94 | 95 | ### Package managers 96 | 97 | ```shell 98 | brew install pcp 99 | ``` 100 | 101 | It's on the roadmap to also distribute `pcp` via `apt`, `yum`, `scoop` and more ... 102 | 103 | ### Release download 104 | 105 | Head over to the [releases](https://github.com/dennis-tra/pcp/releases) and download the latest archive for 106 | your platform. 107 | 108 | ### From source 109 | 110 | To compile it yourself run: 111 | 112 | ```shell 113 | go install github.com/dennis-tra/pcp/cmd/pcp@latest # Go 1.13 or higher is required 114 | ``` 115 | 116 | Make sure the `$GOPATH/bin` is in your `PATH` variable to access the installed `pcp` executable. 117 | 118 | ## Development 119 | 120 | ### Protobuf definitions 121 | 122 | First install the protoc compiler: 123 | 124 | ```shell 125 | make tools # downloads gofumpt and protoc 126 | make proto # generates protobuf 127 | ``` 128 | 129 | The current proto definitions were generated with `libprotoc 3.14.0`. 130 | 131 | ## Feature Roadmap 132 | 133 | Shamelessly copied from `croc`: 134 | 135 | - [x] allows any two computers to transfer data (using a relay) 136 | - ✅ using mDNS and DHT for peer discovery and [AutoRelay](https://docs.libp2p.io/concepts/circuit-relay/#autorelay) / [AutoNat](https://docs.libp2p.io/concepts/nat/#autonat) 137 | - [x] provides end-to-end encryption (using PAKE) 138 | - ✅ yup, it uses [`pake/v2`](https://github.com/schollz/pake/tree/v2.0.6) from `croc` 139 | - [x] enables easy cross-platform transfers (Windows, Linux, Mac) 140 | - ✅ Linux <-> Mac, ❌ Windows, but it's planned! 141 | - [x] allows multiple file transfers 142 | - ✅ it allows transferring directories 143 | - [ ] allows resuming transfers that are interrupted 144 | - ❌ not yet 145 | - [x] local server or port-forwarding not needed 146 | - ✅ thanks to [AutoNat](https://docs.libp2p.io/concepts/nat/#autonat) 147 | - [x] ipv6-first with ipv4 fallback 148 | - ✅ thanks to [libp2p](https://libp2p.io/) 149 | - [ ] can use proxy, like tor 150 | - ❌ not yet 151 | 152 | You can find a project plan in the [project tab](https://github.com/dennis-tra/pcp/projects/2) of this page. 153 | Some other ideas I would love to work on include: 154 | 155 | - [ ] browser interop via the means of [js-libp2p](https://github.com/libp2p/js-libp2p) 156 | - [ ] experimental decentralised NAT hole punching via DHT signaling servers - [Project Flare](https://github.com/libp2p/go-libp2p/issues/1039) 157 | 158 | ## Related Efforts 159 | 160 | - [`croc`](https://github.com/schollz/croc) - Easily and securely send things from one computer to another 161 | - [`magic-wormhole`](https://github.com/magic-wormhole/magic-wormhole) - get things from one computer to another, safely 162 | - [`dcp`](https://github.com/tom-james-watson/dat-cp) - Remote file copy, powered by the Dat protocol. 163 | - [`iwant`](https://github.com/nirvik/iWant) - CLI based decentralized peer to peer file sharing 164 | - [`p2pcopy`](https://github.com/psantosl/p2pcopy) - Small command line application to do p2p file copy behind firewalls 165 | without a central server. 166 | - [`zget`](https://github.com/nils-werner/zget) - Filename based peer to peer file transfer 167 | - [`sharedrop`](https://github.com/cowbell/sharedrop) - Easy P2P file transfer powered by WebRTC - inspired by Apple 168 | AirDrop 169 | - [`filepizza`](https://github.com/kern/filepizza) - Peer-to-peer file transfers in your browser 170 | - [`toss`](https://github.com/zerotier/toss) - Dead simple LAN file transfers from the command line 171 | - Forgot yours? [Open an issue](https://github.com/dennis-tra/pcp/issues/new) or submit a PR :) 172 | 173 | ## Maintainers 174 | 175 | [@dennis-tra](https://github.com/dennis-tra). 176 | 177 | ## Acknowledgments 178 | 179 | - [`go-libp2p`](https://github.com/libp2p/go-libp2p) - The Go implementation of the libp2p Networking Stack. 180 | - [`pake/v2`](https://github.com/schollz/pake/tree/v2.0.6) - PAKE library for generating a strong secret between parties over an insecure channel 181 | - [`progressbar`](https://github.com/schollz/progressbar) - A really basic thread-safe progress bar for Golang applications 182 | 183 | ## Contributing 184 | 185 | Feel free to dive in! [Open an issue](https://github.com/dennis-tra/pcp/issues/new) or submit PRs. 186 | 187 | ## Research 188 | 189 | This tool was submitted to the [International Federation for Information Processing 2021 (IFIP '21)](https://networking.ifip.org/2021/) conference and accepted for publication. You can find the preprint [here](docs/trautwein2021.pdf). 190 | 191 |
192 | Cite the paper with this BibTeX entry: 193 | Jesse Pinkman 194 |
195 | 196 | ```bibtex 197 | @inproceedings{Trautwein2021, 198 | title = {Introducing Peer Copy - A Fully Decentralized Peer-to-Peer File Transfer Tool}, 199 | author = {Trautwein, Dennis and Schubotz, Moritz and Gipp, Bela}, 200 | year = 2021, 201 | month = {June}, 202 | booktitle = {2021 IFIP Networking Conference (IFIP Networking)}, 203 | publisher = {IEEE}, 204 | address = {Espoo and Helsinki, Finland}, 205 | doi = {10.23919/IFIPNetworking52078.2021.9472842}, 206 | note = {ISBN 978-3-9031-7639-3}, 207 | topic = {misc} 208 | } 209 | ``` 210 | 211 | ## Support 212 | 213 | It would really make my day if you supported this project through [Buy Me A Coffee](https://www.buymeacoffee.com/dennistra). 214 | 215 | ## Other Projects 216 | 217 | You may be interested in one of my other projects: 218 | 219 | - [`image-stego`](https://github.com/dennis-tra/image-stego) - A novel way to image manipulation detection. Steganography-based image integrity - Merkle tree nodes embedded into image chunks so that each chunk's integrity can be verified on its own. 220 | - [`nebula-crawler`](https://github.com/dennis-tra/nebula-crawler) - A libp2p DHT crawler that also monitors the liveness and availability of peers. 🏆 Winner of the [DI2F Workshop Hackathon](https://research.protocol.ai/blog/2021/decentralising-the-internet-with-ipfs-and-filecoin-di2f-a-report-from-the-trenches) 🏆 221 | 222 | ## License 223 | 224 | [Apache License Version 2.0](LICENSE) © Dennis Trautwein 225 | -------------------------------------------------------------------------------- /cmd/pcp/pcp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/urfave/cli/v2" 11 | 12 | "github.com/dennis-tra/pcp/internal/log" 13 | "github.com/dennis-tra/pcp/pkg/receive" 14 | "github.com/dennis-tra/pcp/pkg/send" 15 | ) 16 | 17 | var ( 18 | // RawVersion and build tag of the 19 | // PCP command line tool. This is 20 | // replaced on build via e.g.: 21 | // -ldflags "-X main.RawVersion=${VERSION}" 22 | RawVersion = "dev" 23 | ShortCommit = "5f3759df" // quake 24 | ) 25 | 26 | func main() { 27 | // ShortCommit version tag 28 | verTag := fmt.Sprintf("v%s+%s", RawVersion, ShortCommit) 29 | 30 | app := &cli.App{ 31 | Name: "pcp", 32 | Authors: []*cli.Author{ 33 | { 34 | Name: "Dennis Trautwein", 35 | Email: "pcp@dtrautwein.eu", 36 | }, 37 | }, 38 | Usage: "Peer Copy, a peer-to-peer data transfer tool.", 39 | Version: verTag, 40 | EnableBashCompletion: true, 41 | Commands: []*cli.Command{ 42 | receive.Command, 43 | send.Command, 44 | }, 45 | Before: func(c *cli.Context) error { 46 | if c.Bool("debug") { 47 | log.SetLevel(log.DebugLevel) 48 | } 49 | return nil 50 | }, 51 | Flags: []cli.Flag{ 52 | &cli.BoolFlag{ 53 | Name: "debug", 54 | Usage: "enables debug log output", 55 | }, 56 | &cli.BoolFlag{ 57 | Name: "dht", 58 | Usage: "Only advertise via the DHT", 59 | }, 60 | &cli.BoolFlag{ 61 | Name: "mdns", 62 | Usage: "Only advertise via multicast DNS", 63 | }, 64 | &cli.BoolFlag{ 65 | Name: "homebrew", 66 | Usage: "if set transfers a hard coded file with a hard coded word sequence", 67 | Hidden: true, 68 | }, 69 | }, 70 | } 71 | 72 | sigs := make(chan os.Signal, 1) 73 | ctx, cancel := context.WithCancel(context.Background()) 74 | 75 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) 76 | go func() { 77 | <-sigs 78 | log.Infoln("Stopping...") 79 | signal.Stop(sigs) 80 | cancel() 81 | }() 82 | 83 | err := app.RunContext(ctx, os.Args) 84 | if err != nil { 85 | log.Infof("error: %v\n", err) 86 | os.Exit(1) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /docs/demo-2021-01-27.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-tra/pcp/c7a005be81126a26c96c13409d3dfaba73f00ae4/docs/demo-2021-01-27.gif -------------------------------------------------------------------------------- /docs/demo-2021-02-13.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-tra/pcp/c7a005be81126a26c96c13409d3dfaba73f00ae4/docs/demo-2021-02-13.gif -------------------------------------------------------------------------------- /docs/docs/pcp-logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-tra/pcp/c7a005be81126a26c96c13409d3dfaba73f00ae4/docs/docs/pcp-logo.sketch -------------------------------------------------------------------------------- /docs/jesse-pinkman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-tra/pcp/c7a005be81126a26c96c13409d3dfaba73f00ae4/docs/jesse-pinkman.jpg -------------------------------------------------------------------------------- /docs/trautwein2021.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-tra/pcp/c7a005be81126a26c96c13409d3dfaba73f00ae4/docs/trautwein2021.pdf -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -package mock -source internal/wrap/ioutil.go -destination internal/mock/ioutil.go 2 | //go:generate mockgen -package mock -source internal/wrap/xdg.go -destination internal/mock/xdg.go 3 | //go:generate mockgen -package mock -source internal/wrap/time.go -destination internal/mock/time.go 4 | //go:generate mockgen -package mock -source internal/wrap/dht.go -destination internal/mock/dht.go 5 | //go:generate mockgen -package mock -source internal/wrap/manet.go -destination internal/mock/manet.go 6 | //go:generate mockgen -package mock -source internal/wrap/discovery.go -destination internal/mock/discovery.go 7 | package pcp 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dennis-tra/pcp 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/adrg/xdg v0.3.0 7 | github.com/golang/mock v1.5.0 8 | github.com/golang/protobuf v1.4.3 9 | github.com/google/go-cmp v0.5.4 // indirect 10 | github.com/google/uuid v1.2.0 11 | github.com/ipfs/go-cid v0.0.7 12 | github.com/libp2p/go-libp2p v0.13.0 13 | github.com/libp2p/go-libp2p-core v0.8.5 14 | github.com/libp2p/go-libp2p-kad-dht v0.11.1 15 | github.com/multiformats/go-multiaddr v0.3.1 16 | github.com/multiformats/go-multihash v0.0.14 17 | github.com/multiformats/go-varint v0.0.6 18 | github.com/pkg/errors v0.9.1 19 | github.com/schollz/pake/v2 v2.0.6 20 | github.com/schollz/progressbar/v3 v3.7.6 21 | github.com/stretchr/testify v1.6.1 22 | github.com/tyler-smith/go-bip39 v1.1.0 23 | github.com/urfave/cli/v2 v2.3.0 24 | github.com/whyrusleeping/mdns v0.0.0-20190826153040-b9b60ed33aa9 25 | go.uber.org/atomic v1.7.0 // indirect 26 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 27 | golang.org/x/mod v0.4.0 // indirect 28 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 29 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect 30 | golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 // indirect 31 | golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c // indirect 32 | google.golang.org/protobuf v1.25.0 33 | ) 34 | -------------------------------------------------------------------------------- /internal/format/format.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // Bytes attaches a unit to the bytes value and makes it human readable. 10 | func Bytes(bytes int64) string { 11 | if bytes >= 1e12 { 12 | return fmt.Sprintf("%dTB", bytes/1e12) 13 | } else if bytes >= 1e9 { 14 | return fmt.Sprintf("%dGB", bytes/1e9) 15 | } else if bytes >= 1e6 { 16 | return fmt.Sprintf("%dMB", bytes/1e6) 17 | } else if bytes >= 1e3 { 18 | return fmt.Sprintf("%dKB", bytes/1e3) 19 | } else { 20 | return fmt.Sprintf("%dB", bytes) 21 | } 22 | } 23 | 24 | // Filename takes the given filename and rotates it like a carousel 25 | // through a fixed length string of maxLen. See tests for example. 26 | func Filename(fn string, iteration int, maxLen int) string { 27 | if len(fn) <= maxLen { 28 | return fn 29 | } 30 | out := make([]byte, maxLen) 31 | padded := fmt.Sprintf("%s%*s", fn, maxLen, " ") 32 | for i := 0; i < maxLen; i++ { 33 | if len(fn) <= maxLen { 34 | out[i] = padded[i] 35 | } else { 36 | out[i] = padded[(i+iteration)%len(padded)] 37 | } 38 | } 39 | return string(out) 40 | } 41 | 42 | // Progress builds a string of length `width` that indicates the `percent` progress being made by filled █ characters. 43 | func Progress(width int, percent float64) string { 44 | if width < 5 { 45 | return fmt.Sprintf("%*s", width, " ") 46 | } 47 | effWidth := width - 2 // brackets left and right 48 | filled := int(float64(effWidth) * percent) 49 | blank := effWidth - filled 50 | if percent >= 1.0 { 51 | return fmt.Sprintf("|%s|", strings.Repeat("█", effWidth)) 52 | } else { 53 | return fmt.Sprintf("|%s%s|", strings.Repeat("█", filled), strings.Repeat("-", blank)) 54 | } 55 | } 56 | 57 | // Speed formats the given bytes per second value to a human readable format. 58 | func Speed(bytesPerS int64) string { 59 | return fmt.Sprintf("%s/s", Bytes(bytesPerS)) 60 | } 61 | 62 | // TransferStatus takes the terminal width `twidth` and builds a string occupying the whole width indicating the 63 | // current transfer status. 64 | func TransferStatus(fn string, iteration int, twidth int, p float64, eta time.Duration, bytesPerS int64) string { 65 | fnStr := Filename(fn, iteration, 16) 66 | pStr := fmt.Sprintf("%.0f%%", p*100) 67 | etaStr := fmt.Sprintf("[eta %s]", eta.Round(time.Second)) 68 | speedStr := Speed(bytesPerS) 69 | progressWidth := twidth - len(fnStr) - len(pStr) - len(etaStr) - len(speedStr) - 4 70 | progressStr := Progress(progressWidth, p) 71 | return fmt.Sprintf("%s %s %s %s %s", fnStr, progressStr, pStr, speedStr, etaStr) 72 | } 73 | -------------------------------------------------------------------------------- /internal/format/format_test.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBytes(t *testing.T) { 12 | tests := []struct { 13 | bytes int64 14 | want string 15 | }{ 16 | {1000, "1KB"}, 17 | } 18 | 19 | for _, tt := range tests { 20 | run := fmt.Sprintf("Formatting %d", tt.bytes) 21 | t.Run(run, func(t *testing.T) { 22 | s := Bytes(tt.bytes) 23 | assert.Equal(t, tt.want, s) 24 | }) 25 | } 26 | } 27 | 28 | func TestFormatFilename(t *testing.T) { 29 | tests := []struct { 30 | name string 31 | iteration int 32 | maxLen int 33 | want string 34 | }{ 35 | {"pcp.go", 0, 3, "pcp"}, 36 | {"pcp.go", 2, 3, "p.g"}, 37 | {"pcp.go", 6, 3, " "}, 38 | {"pcp.go", 8, 3, " pc"}, 39 | {"test-file.txt", 0, 16, "test-file.txt"}, 40 | {"test-file.txt", 8, 16, "test-file.txt"}, 41 | {"test-file.txt", 16, 16, "test-file.txt"}, 42 | {"test-file.txt", 20, 16, "test-file.txt"}, 43 | {"", 0, 16, ""}, 44 | {"a-really-long-test-file.txt", 0, 16, "a-really-long-te"}, 45 | {"a-really-long-test-file.txt", 1, 16, "-really-long-tes"}, 46 | {"a-really-long-test-file.txt", 11, 16, "ng-test-file.txt"}, 47 | {"a-really-long-test-file.txt", 12, 16, "g-test-file.txt "}, 48 | {"a-really-long-test-file.txt", 16, 16, "st-file.txt "}, 49 | {"a-really-long-test-file.txt", 27, 16, " "}, 50 | {"a-really-long-test-file.txt", 28, 16, " a"}, 51 | {"a-really-long-test-file.txt", 33, 16, " a-real"}, 52 | } 53 | 54 | for _, tt := range tests { 55 | run := fmt.Sprintf("Formatting %s (i: %d, len: %d)", tt.name, tt.iteration, tt.maxLen) 56 | t.Run(run, func(t *testing.T) { 57 | s := Filename(tt.name, tt.iteration, tt.maxLen) 58 | assert.Equal(t, tt.want, s, "Expected filename %q (i: %d, len: %d) to equal %q, got: %q", tt.name, tt.iteration, tt.maxLen, tt.want, s) 59 | }) 60 | } 61 | } 62 | 63 | func TestFormatProgress(t *testing.T) { 64 | tests := []struct { 65 | width int 66 | percent float64 67 | want string 68 | }{ 69 | {10, 0.0, "|--------|"}, 70 | {10, 0.5, "|████----|"}, 71 | {10, 0.999, "|███████-|"}, 72 | {10, 1.0, "|████████|"}, 73 | {2, 0.1, " "}, 74 | {5, 0.2, "|---|"}, 75 | {5, 0.35, "|█--|"}, 76 | {5, 0.9, "|██-|"}, 77 | {5, 1.0, "|███|"}, 78 | } 79 | 80 | for _, tt := range tests { 81 | run := fmt.Sprintf("Formatting Progress (width: %d, percent: %.1f)", tt.width, tt.percent) 82 | t.Run(run, func(t *testing.T) { 83 | s := Progress(tt.width, tt.percent) 84 | assert.Equal(t, tt.want, s, "Expected width: %d, percent: %d to equal %q, got: %q", tt.width, tt.percent, tt.want, s) 85 | }) 86 | } 87 | } 88 | 89 | func TestFormatTransferStatus(t *testing.T) { 90 | tests := []struct { 91 | filename string 92 | iteration int 93 | twidth int 94 | percent float64 95 | eta time.Duration 96 | bytesPerS int64 97 | want string 98 | }{ 99 | { 100 | filename: "test.txt", 101 | iteration: 0, 102 | twidth: 80, 103 | percent: 0.4, 104 | eta: time.Minute, 105 | bytesPerS: 235000, 106 | want: "test.txt |██████████████████----------------------------| 40% 235KB/s [eta 1m0s]", 107 | }, 108 | { 109 | filename: "a-really-long-test-file.txt", 110 | iteration: 334, 111 | twidth: 80, 112 | percent: 0.6, 113 | eta: time.Minute + 5*time.Second, 114 | bytesPerS: 87000000, 115 | want: " a-real |███████████████████████----------------| 60% 87MB/s [eta 1m5s]", 116 | }, 117 | } 118 | 119 | for _, tt := range tests { 120 | run := fmt.Sprintf("Formatting Transfer Status") 121 | t.Run(run, func(t *testing.T) { 122 | s := TransferStatus(tt.filename, tt.iteration, tt.twidth, tt.percent, tt.eta, tt.bytesPerS) 123 | assert.Equal(t, tt.want, s) 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "golang.org/x/crypto/ssh/terminal" 11 | ) 12 | 13 | type Level uint8 14 | 15 | const ( 16 | DebugLevel Level = iota 17 | InfoLevel 18 | WarningLevel 19 | ErrorLevel 20 | ) 21 | 22 | var level Level 23 | 24 | func SetLevel(l Level) { 25 | level = l 26 | } 27 | 28 | // Out represents the writer to print the log messages to. 29 | // This is used for tests. 30 | var Out io.Writer = os.Stderr 31 | 32 | var tWidth int 33 | 34 | func init() { 35 | level = InfoLevel 36 | 37 | var err error 38 | tWidth, _, err = terminal.GetSize(int(os.Stdout.Fd())) 39 | if err != nil { 40 | tWidth, _, err = terminal.GetSize(int(os.Stderr.Fd())) 41 | if err != nil { 42 | tWidth = 80 43 | } 44 | } 45 | } 46 | 47 | func printTimestamp() { 48 | if level > DebugLevel { 49 | return 50 | } 51 | fmt.Printf("[%s] ", time.Now().Format(time.RFC3339)) 52 | } 53 | 54 | func Info(a ...interface{}) { 55 | if level > InfoLevel { 56 | return 57 | } 58 | printTimestamp() 59 | fmt.Fprint(Out, a...) 60 | } 61 | 62 | func Infoln(a ...interface{}) { 63 | if level > InfoLevel { 64 | return 65 | } 66 | printTimestamp() 67 | fmt.Fprintln(Out, a...) 68 | } 69 | 70 | func Infor(format string, a ...interface{}) { 71 | if level > InfoLevel { 72 | return 73 | } 74 | 75 | if level > DebugLevel { 76 | blank := fmt.Sprintf("\r%s\r", strings.Repeat(" ", tWidth)) 77 | printTimestamp() 78 | fmt.Fprint(Out, fmt.Sprintf("%s%s", blank, fmt.Sprintf(format, a...))) 79 | } else { 80 | printTimestamp() 81 | fmt.Fprintln(Out, fmt.Sprintf(strings.TrimSpace(format), a...)) 82 | } 83 | } 84 | 85 | func Infof(format string, a ...interface{}) { 86 | if level > InfoLevel { 87 | return 88 | } 89 | printTimestamp() 90 | fmt.Fprintf(Out, format, a...) 91 | } 92 | 93 | func Debug(a ...interface{}) { 94 | if level > DebugLevel { 95 | return 96 | } 97 | printTimestamp() 98 | fmt.Fprint(Out, a...) 99 | } 100 | 101 | func Debugln(a ...interface{}) { 102 | if level > DebugLevel { 103 | return 104 | } 105 | printTimestamp() 106 | fmt.Fprintln(Out, a...) 107 | } 108 | 109 | func Debugf(format string, a ...interface{}) { 110 | if level > DebugLevel { 111 | return 112 | } 113 | printTimestamp() 114 | fmt.Fprintf(Out, format, a...) 115 | } 116 | 117 | func Warning(a ...interface{}) { 118 | if level > WarningLevel { 119 | return 120 | } 121 | printTimestamp() 122 | fmt.Fprint(Out, a...) 123 | } 124 | 125 | func Warningln(a ...interface{}) { 126 | if level > WarningLevel { 127 | return 128 | } 129 | printTimestamp() 130 | fmt.Fprintln(Out, a...) 131 | } 132 | 133 | func Warningf(format string, a ...interface{}) { 134 | if level > WarningLevel { 135 | return 136 | } 137 | printTimestamp() 138 | fmt.Fprintf(Out, format, a...) 139 | } 140 | 141 | func Error(a ...interface{}) { 142 | printTimestamp() 143 | fmt.Fprint(Out, a...) 144 | } 145 | 146 | func Errorln(a ...interface{}) { 147 | printTimestamp() 148 | fmt.Fprintln(Out, a...) 149 | } 150 | 151 | func Errorf(format string, a ...interface{}) { 152 | printTimestamp() 153 | fmt.Fprintf(Out, format, a...) 154 | } 155 | -------------------------------------------------------------------------------- /internal/mock/dht.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/wrap/dht.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | cid "github.com/ipfs/go-cid" 13 | peer "github.com/libp2p/go-libp2p-core/peer" 14 | ) 15 | 16 | // MockDHTer is a mock of DHTer interface. 17 | type MockDHTer struct { 18 | ctrl *gomock.Controller 19 | recorder *MockDHTerMockRecorder 20 | } 21 | 22 | // MockDHTerMockRecorder is the mock recorder for MockDHTer. 23 | type MockDHTerMockRecorder struct { 24 | mock *MockDHTer 25 | } 26 | 27 | // NewMockDHTer creates a new mock instance. 28 | func NewMockDHTer(ctrl *gomock.Controller) *MockDHTer { 29 | mock := &MockDHTer{ctrl: ctrl} 30 | mock.recorder = &MockDHTerMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockDHTer) EXPECT() *MockDHTerMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // GetDefaultBootstrapPeerAddrInfos mocks base method. 40 | func (m *MockDHTer) GetDefaultBootstrapPeerAddrInfos() []peer.AddrInfo { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "GetDefaultBootstrapPeerAddrInfos") 43 | ret0, _ := ret[0].([]peer.AddrInfo) 44 | return ret0 45 | } 46 | 47 | // GetDefaultBootstrapPeerAddrInfos indicates an expected call of GetDefaultBootstrapPeerAddrInfos. 48 | func (mr *MockDHTerMockRecorder) GetDefaultBootstrapPeerAddrInfos() *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultBootstrapPeerAddrInfos", reflect.TypeOf((*MockDHTer)(nil).GetDefaultBootstrapPeerAddrInfos)) 51 | } 52 | 53 | // MockIpfsDHT is a mock of IpfsDHT interface. 54 | type MockIpfsDHT struct { 55 | ctrl *gomock.Controller 56 | recorder *MockIpfsDHTMockRecorder 57 | } 58 | 59 | // MockIpfsDHTMockRecorder is the mock recorder for MockIpfsDHT. 60 | type MockIpfsDHTMockRecorder struct { 61 | mock *MockIpfsDHT 62 | } 63 | 64 | // NewMockIpfsDHT creates a new mock instance. 65 | func NewMockIpfsDHT(ctrl *gomock.Controller) *MockIpfsDHT { 66 | mock := &MockIpfsDHT{ctrl: ctrl} 67 | mock.recorder = &MockIpfsDHTMockRecorder{mock} 68 | return mock 69 | } 70 | 71 | // EXPECT returns an object that allows the caller to indicate expected use. 72 | func (m *MockIpfsDHT) EXPECT() *MockIpfsDHTMockRecorder { 73 | return m.recorder 74 | } 75 | 76 | // FindProvidersAsync mocks base method. 77 | func (m *MockIpfsDHT) FindProvidersAsync(arg0 context.Context, arg1 cid.Cid, arg2 int) <-chan peer.AddrInfo { 78 | m.ctrl.T.Helper() 79 | ret := m.ctrl.Call(m, "FindProvidersAsync", arg0, arg1, arg2) 80 | ret0, _ := ret[0].(<-chan peer.AddrInfo) 81 | return ret0 82 | } 83 | 84 | // FindProvidersAsync indicates an expected call of FindProvidersAsync. 85 | func (mr *MockIpfsDHTMockRecorder) FindProvidersAsync(arg0, arg1, arg2 interface{}) *gomock.Call { 86 | mr.mock.ctrl.T.Helper() 87 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindProvidersAsync", reflect.TypeOf((*MockIpfsDHT)(nil).FindProvidersAsync), arg0, arg1, arg2) 88 | } 89 | 90 | // Provide mocks base method. 91 | func (m *MockIpfsDHT) Provide(arg0 context.Context, arg1 cid.Cid, arg2 bool) error { 92 | m.ctrl.T.Helper() 93 | ret := m.ctrl.Call(m, "Provide", arg0, arg1, arg2) 94 | ret0, _ := ret[0].(error) 95 | return ret0 96 | } 97 | 98 | // Provide indicates an expected call of Provide. 99 | func (mr *MockIpfsDHTMockRecorder) Provide(arg0, arg1, arg2 interface{}) *gomock.Call { 100 | mr.mock.ctrl.T.Helper() 101 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Provide", reflect.TypeOf((*MockIpfsDHT)(nil).Provide), arg0, arg1, arg2) 102 | } 103 | -------------------------------------------------------------------------------- /internal/mock/discovery.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/wrap/discovery.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | time "time" 11 | 12 | gomock "github.com/golang/mock/gomock" 13 | host "github.com/libp2p/go-libp2p-core/host" 14 | discovery "github.com/libp2p/go-libp2p/p2p/discovery" 15 | ) 16 | 17 | // MockDiscoverer is a mock of Discoverer interface. 18 | type MockDiscoverer struct { 19 | ctrl *gomock.Controller 20 | recorder *MockDiscovererMockRecorder 21 | } 22 | 23 | // MockDiscovererMockRecorder is the mock recorder for MockDiscoverer. 24 | type MockDiscovererMockRecorder struct { 25 | mock *MockDiscoverer 26 | } 27 | 28 | // NewMockDiscoverer creates a new mock instance. 29 | func NewMockDiscoverer(ctrl *gomock.Controller) *MockDiscoverer { 30 | mock := &MockDiscoverer{ctrl: ctrl} 31 | mock.recorder = &MockDiscovererMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockDiscoverer) EXPECT() *MockDiscovererMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // NewMdnsService mocks base method. 41 | func (m *MockDiscoverer) NewMdnsService(ctx context.Context, peerhost host.Host, interval time.Duration, serviceTag string) (discovery.Service, error) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "NewMdnsService", ctx, peerhost, interval, serviceTag) 44 | ret0, _ := ret[0].(discovery.Service) 45 | ret1, _ := ret[1].(error) 46 | return ret0, ret1 47 | } 48 | 49 | // NewMdnsService indicates an expected call of NewMdnsService. 50 | func (mr *MockDiscovererMockRecorder) NewMdnsService(ctx, peerhost, interval, serviceTag interface{}) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewMdnsService", reflect.TypeOf((*MockDiscoverer)(nil).NewMdnsService), ctx, peerhost, interval, serviceTag) 53 | } 54 | -------------------------------------------------------------------------------- /internal/mock/ioutil.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/wrap/ioutil.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | os "os" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockIoutiler is a mock of Ioutiler interface. 15 | type MockIoutiler struct { 16 | ctrl *gomock.Controller 17 | recorder *MockIoutilerMockRecorder 18 | } 19 | 20 | // MockIoutilerMockRecorder is the mock recorder for MockIoutiler. 21 | type MockIoutilerMockRecorder struct { 22 | mock *MockIoutiler 23 | } 24 | 25 | // NewMockIoutiler creates a new mock instance. 26 | func NewMockIoutiler(ctrl *gomock.Controller) *MockIoutiler { 27 | mock := &MockIoutiler{ctrl: ctrl} 28 | mock.recorder = &MockIoutilerMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockIoutiler) EXPECT() *MockIoutilerMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // ReadFile mocks base method. 38 | func (m *MockIoutiler) ReadFile(filename string) ([]byte, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "ReadFile", filename) 41 | ret0, _ := ret[0].([]byte) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // ReadFile indicates an expected call of ReadFile. 47 | func (mr *MockIoutilerMockRecorder) ReadFile(filename interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadFile", reflect.TypeOf((*MockIoutiler)(nil).ReadFile), filename) 50 | } 51 | 52 | // WriteFile mocks base method. 53 | func (m *MockIoutiler) WriteFile(filename string, data []byte, perm os.FileMode) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "WriteFile", filename, data, perm) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // WriteFile indicates an expected call of WriteFile. 61 | func (mr *MockIoutilerMockRecorder) WriteFile(filename, data, perm interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteFile", reflect.TypeOf((*MockIoutiler)(nil).WriteFile), filename, data, perm) 64 | } 65 | -------------------------------------------------------------------------------- /internal/mock/manet.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/wrap/manet.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | multiaddr "github.com/multiformats/go-multiaddr" 12 | ) 13 | 14 | // MockManeter is a mock of Maneter interface. 15 | type MockManeter struct { 16 | ctrl *gomock.Controller 17 | recorder *MockManeterMockRecorder 18 | } 19 | 20 | // MockManeterMockRecorder is the mock recorder for MockManeter. 21 | type MockManeterMockRecorder struct { 22 | mock *MockManeter 23 | } 24 | 25 | // NewMockManeter creates a new mock instance. 26 | func NewMockManeter(ctrl *gomock.Controller) *MockManeter { 27 | mock := &MockManeter{ctrl: ctrl} 28 | mock.recorder = &MockManeterMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockManeter) EXPECT() *MockManeterMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // IsPublicAddr mocks base method. 38 | func (m *MockManeter) IsPublicAddr(a multiaddr.Multiaddr) bool { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "IsPublicAddr", a) 41 | ret0, _ := ret[0].(bool) 42 | return ret0 43 | } 44 | 45 | // IsPublicAddr indicates an expected call of IsPublicAddr. 46 | func (mr *MockManeterMockRecorder) IsPublicAddr(a interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPublicAddr", reflect.TypeOf((*MockManeter)(nil).IsPublicAddr), a) 49 | } 50 | -------------------------------------------------------------------------------- /internal/mock/time.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/wrap/time.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | reflect "reflect" 9 | time "time" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockTimer is a mock of Timer interface. 15 | type MockTimer struct { 16 | ctrl *gomock.Controller 17 | recorder *MockTimerMockRecorder 18 | } 19 | 20 | // MockTimerMockRecorder is the mock recorder for MockTimer. 21 | type MockTimerMockRecorder struct { 22 | mock *MockTimer 23 | } 24 | 25 | // NewMockTimer creates a new mock instance. 26 | func NewMockTimer(ctrl *gomock.Controller) *MockTimer { 27 | mock := &MockTimer{ctrl: ctrl} 28 | mock.recorder = &MockTimerMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockTimer) EXPECT() *MockTimerMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // AfterFunc mocks base method. 38 | func (m *MockTimer) AfterFunc(d time.Duration, f func()) *time.Timer { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "AfterFunc", d, f) 41 | ret0, _ := ret[0].(*time.Timer) 42 | return ret0 43 | } 44 | 45 | // AfterFunc indicates an expected call of AfterFunc. 46 | func (mr *MockTimerMockRecorder) AfterFunc(d, f interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterFunc", reflect.TypeOf((*MockTimer)(nil).AfterFunc), d, f) 49 | } 50 | 51 | // NewTimer mocks base method. 52 | func (m *MockTimer) NewTimer(d time.Duration) *time.Timer { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "NewTimer", d) 55 | ret0, _ := ret[0].(*time.Timer) 56 | return ret0 57 | } 58 | 59 | // NewTimer indicates an expected call of NewTimer. 60 | func (mr *MockTimerMockRecorder) NewTimer(d interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTimer", reflect.TypeOf((*MockTimer)(nil).NewTimer), d) 63 | } 64 | 65 | // Now mocks base method. 66 | func (m *MockTimer) Now() time.Time { 67 | m.ctrl.T.Helper() 68 | ret := m.ctrl.Call(m, "Now") 69 | ret0, _ := ret[0].(time.Time) 70 | return ret0 71 | } 72 | 73 | // Now indicates an expected call of Now. 74 | func (mr *MockTimerMockRecorder) Now() *gomock.Call { 75 | mr.mock.ctrl.T.Helper() 76 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockTimer)(nil).Now)) 77 | } 78 | 79 | // Sleep mocks base method. 80 | func (m *MockTimer) Sleep(d time.Duration) { 81 | m.ctrl.T.Helper() 82 | m.ctrl.Call(m, "Sleep", d) 83 | } 84 | 85 | // Sleep indicates an expected call of Sleep. 86 | func (mr *MockTimerMockRecorder) Sleep(d interface{}) *gomock.Call { 87 | mr.mock.ctrl.T.Helper() 88 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sleep", reflect.TypeOf((*MockTimer)(nil).Sleep), d) 89 | } 90 | -------------------------------------------------------------------------------- /internal/mock/xdg.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/wrap/xdg.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockXdger is a mock of Xdger interface. 14 | type MockXdger struct { 15 | ctrl *gomock.Controller 16 | recorder *MockXdgerMockRecorder 17 | } 18 | 19 | // MockXdgerMockRecorder is the mock recorder for MockXdger. 20 | type MockXdgerMockRecorder struct { 21 | mock *MockXdger 22 | } 23 | 24 | // NewMockXdger creates a new mock instance. 25 | func NewMockXdger(ctrl *gomock.Controller) *MockXdger { 26 | mock := &MockXdger{ctrl: ctrl} 27 | mock.recorder = &MockXdgerMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockXdger) EXPECT() *MockXdgerMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // ConfigFile mocks base method. 37 | func (m *MockXdger) ConfigFile(relPath string) (string, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "ConfigFile", relPath) 40 | ret0, _ := ret[0].(string) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // ConfigFile indicates an expected call of ConfigFile. 46 | func (mr *MockXdgerMockRecorder) ConfigFile(relPath interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigFile", reflect.TypeOf((*MockXdger)(nil).ConfigFile), relPath) 49 | } 50 | -------------------------------------------------------------------------------- /internal/wrap/dht.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ipfs/go-cid" 7 | stddht "github.com/libp2p/go-libp2p-kad-dht" 8 | 9 | "github.com/libp2p/go-libp2p-core/peer" 10 | ) 11 | 12 | type DHTer interface { 13 | GetDefaultBootstrapPeerAddrInfos() []peer.AddrInfo 14 | } 15 | 16 | type DHT struct{} 17 | 18 | func (d DHT) GetDefaultBootstrapPeerAddrInfos() []peer.AddrInfo { 19 | return stddht.GetDefaultBootstrapPeerAddrInfos() 20 | } 21 | 22 | type IpfsDHT interface { 23 | Provide(context.Context, cid.Cid, bool) error 24 | FindProvidersAsync(context.Context, cid.Cid, int) <-chan peer.AddrInfo 25 | } 26 | -------------------------------------------------------------------------------- /internal/wrap/discovery.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/libp2p/go-libp2p-core/host" 8 | stddiscovery "github.com/libp2p/go-libp2p/p2p/discovery" 9 | ) 10 | 11 | type Discoverer interface { 12 | NewMdnsService(ctx context.Context, peerhost host.Host, interval time.Duration, serviceTag string) (stddiscovery.Service, error) 13 | } 14 | 15 | type Discovery struct{} 16 | 17 | func (d Discovery) NewMdnsService(ctx context.Context, peerhost host.Host, interval time.Duration, serviceTag string) (stddiscovery.Service, error) { 18 | return stddiscovery.NewMdnsService(ctx, peerhost, interval, serviceTag) 19 | } 20 | -------------------------------------------------------------------------------- /internal/wrap/ioutil.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "os" 5 | 6 | stdioutil "io/ioutil" 7 | ) 8 | 9 | type Ioutiler interface { 10 | ReadFile(filename string) ([]byte, error) 11 | WriteFile(filename string, data []byte, perm os.FileMode) error 12 | } 13 | 14 | type Ioutil struct{} 15 | 16 | func (a Ioutil) ReadFile(filename string) ([]byte, error) { 17 | return stdioutil.ReadFile(filename) 18 | } 19 | 20 | func (a Ioutil) WriteFile(filename string, data []byte, perm os.FileMode) error { 21 | return stdioutil.WriteFile(filename, data, perm) 22 | } 23 | -------------------------------------------------------------------------------- /internal/wrap/manet.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | ma "github.com/multiformats/go-multiaddr" 5 | manet "github.com/multiformats/go-multiaddr/net" 6 | ) 7 | 8 | type Maneter interface { 9 | IsPublicAddr(a ma.Multiaddr) bool 10 | } 11 | 12 | type Manet struct{} 13 | 14 | func (d Manet) IsPublicAddr(a ma.Multiaddr) bool { 15 | return manet.IsPublicAddr(a) 16 | } 17 | -------------------------------------------------------------------------------- /internal/wrap/time.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | stdtime "time" 5 | ) 6 | 7 | type Timer interface { 8 | AfterFunc(d stdtime.Duration, f func()) *stdtime.Timer 9 | NewTimer(d stdtime.Duration) *stdtime.Timer 10 | Now() stdtime.Time 11 | Sleep(d stdtime.Duration) 12 | } 13 | 14 | type Time struct{} 15 | 16 | func (t Time) AfterFunc(d stdtime.Duration, f func()) *stdtime.Timer { 17 | return stdtime.AfterFunc(d, f) 18 | } 19 | 20 | func (t Time) Now() stdtime.Time { 21 | return stdtime.Now() 22 | } 23 | 24 | func (t Time) NewTimer(d stdtime.Duration) *stdtime.Timer { 25 | return stdtime.NewTimer(d) 26 | } 27 | 28 | func (t Time) Sleep(d stdtime.Duration) { 29 | stdtime.Sleep(d) 30 | } 31 | -------------------------------------------------------------------------------- /internal/wrap/xdg.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | stdxdg "github.com/adrg/xdg" 5 | ) 6 | 7 | type Xdger interface { 8 | ConfigFile(relPath string) (string, error) 9 | } 10 | 11 | type Xdg struct{} 12 | 13 | func (a Xdg) ConfigFile(relPath string) (string, error) { 14 | return stdxdg.ConfigFile(relPath) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/urfave/cli/v2" 11 | 12 | "github.com/dennis-tra/pcp/internal/wrap" 13 | ) 14 | 15 | const ( 16 | Prefix = "pcp" 17 | ContextKey = "config" 18 | ) 19 | 20 | // settingsFile contains the path suffix that's appended to 21 | // an XDG compliant directory to find the settings file. 22 | var settingsFile = filepath.Join(Prefix, "settings.json") 23 | 24 | var ( 25 | appIoutil wrap.Ioutiler = wrap.Ioutil{} 26 | appXdg wrap.Xdger = wrap.Xdg{} 27 | ) 28 | 29 | // Config contains general user settings and peer identity 30 | // information. The configuration is split, so the identity 31 | // information can easier be saved with more restrict 32 | // access permissions as it contains the private Key. 33 | type Config struct { 34 | Settings *Settings 35 | } 36 | 37 | // Save saves the peer settings and identity information 38 | // to disk. 39 | func (c *Config) Save() error { 40 | err := c.Settings.Save() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func LoadConfig() (*Config, error) { 49 | settings, err := LoadSettings() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | c := &Config{ 55 | Settings: settings, 56 | } 57 | 58 | return c, nil 59 | } 60 | 61 | func FillContext(c *cli.Context) (*cli.Context, error) { 62 | conf, err := LoadConfig() 63 | if err != nil { 64 | return c, err 65 | } 66 | c.Context = context.WithValue(c.Context, ContextKey, conf) 67 | return c, nil 68 | } 69 | 70 | func FromContext(ctx context.Context) (*Config, error) { 71 | obj := ctx.Value(ContextKey) 72 | if obj == nil { 73 | return nil, fmt.Errorf("config not found in context") 74 | } 75 | config, ok := obj.(*Config) 76 | if !ok { 77 | return nil, fmt.Errorf("config not found in context") 78 | } 79 | 80 | return config, nil 81 | } 82 | 83 | func save(relPath string, obj interface{}, perm os.FileMode) error { 84 | path, err := appXdg.ConfigFile(relPath) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | data, err := json.Marshal(obj) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | err = appIoutil.WriteFile(path, data, perm) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/config/settings.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | type Settings struct { 9 | Path string `json:"-"` 10 | Exists bool `json:"-"` 11 | } 12 | 13 | func LoadSettings() (*Settings, error) { 14 | path, err := appXdg.ConfigFile(settingsFile) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | settings := &Settings{Path: path} 20 | data, err := appIoutil.ReadFile(path) 21 | if err == nil { 22 | err = json.Unmarshal(data, &settings) 23 | if err != nil { 24 | return nil, err 25 | } 26 | settings.Exists = true 27 | } else if !os.IsNotExist(err) { 28 | return nil, err 29 | } 30 | 31 | return settings, nil 32 | } 33 | 34 | func (s *Settings) Save() error { 35 | err := save(settingsFile, s, 0o744) 36 | if err == nil { 37 | s.Exists = true 38 | } 39 | return err 40 | } 41 | -------------------------------------------------------------------------------- /pkg/config/settings_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/dennis-tra/pcp/internal/mock" 13 | "github.com/dennis-tra/pcp/internal/wrap" 14 | ) 15 | 16 | func setup(t *testing.T) *gomock.Controller { 17 | appXdg = wrap.Xdg{} 18 | appIoutil = wrap.Ioutil{} 19 | return gomock.NewController(t) 20 | } 21 | 22 | func teardown(t *testing.T, ctrl *gomock.Controller) { 23 | ctrl.Finish() 24 | appXdg = wrap.Xdg{} 25 | appIoutil = wrap.Ioutil{} 26 | } 27 | 28 | func TestLoadSettings_returnsErrorOfConfigFile(t *testing.T) { 29 | ctrl := setup(t) 30 | defer teardown(t, ctrl) 31 | 32 | m := mock.NewMockXdger(ctrl) 33 | 34 | expectedErr := fmt.Errorf("some error") 35 | m.EXPECT(). 36 | ConfigFile(gomock.Any()). 37 | Return("", expectedErr) 38 | 39 | appXdg = m 40 | settings, err := LoadSettings() 41 | assert.Nil(t, settings) 42 | assert.Equal(t, expectedErr, err) 43 | } 44 | 45 | func TestLoadSettings_returnsErrorOfReadFile(t *testing.T) { 46 | ctrl := setup(t) 47 | defer teardown(t, ctrl) 48 | 49 | mioutil := mock.NewMockIoutiler(ctrl) 50 | mxdg := mock.NewMockXdger(ctrl) 51 | 52 | appIoutil = mioutil 53 | appXdg = mxdg 54 | 55 | mxdg. 56 | EXPECT(). 57 | ConfigFile(gomock.Eq(settingsFile)). 58 | Return("path", nil) 59 | 60 | expectedErr := fmt.Errorf("some error") 61 | mioutil. 62 | EXPECT(). 63 | ReadFile(gomock.Eq("path")). 64 | Return(nil, expectedErr) 65 | 66 | settings, err := LoadSettings() 67 | assert.Nil(t, settings) 68 | assert.Equal(t, expectedErr, err) 69 | } 70 | 71 | func TestLoadSettings_returnsErrorOnUnparsableData(t *testing.T) { 72 | ctrl := setup(t) 73 | defer teardown(t, ctrl) 74 | 75 | mioutil := mock.NewMockIoutiler(ctrl) 76 | mxdg := mock.NewMockXdger(ctrl) 77 | 78 | appIoutil = mioutil 79 | appXdg = mxdg 80 | 81 | mxdg. 82 | EXPECT(). 83 | ConfigFile(gomock.Eq(settingsFile)). 84 | Return("path", nil) 85 | 86 | data := []byte(`unparsable`) 87 | mioutil. 88 | EXPECT(). 89 | ReadFile(gomock.Eq("path")). 90 | Return(data, nil) 91 | 92 | settings, err := LoadSettings() 93 | assert.Nil(t, settings) 94 | assert.NotNil(t, err) 95 | } 96 | 97 | func TestLoadSettings_returnsEmptySettingsIfFileDoesNotExist(t *testing.T) { 98 | ctrl := setup(t) 99 | defer teardown(t, ctrl) 100 | 101 | mioutil := mock.NewMockIoutiler(ctrl) 102 | mxdg := mock.NewMockXdger(ctrl) 103 | 104 | appIoutil = mioutil 105 | appXdg = mxdg 106 | 107 | mxdg. 108 | EXPECT(). 109 | ConfigFile(gomock.Eq(settingsFile)). 110 | Return("path", nil) 111 | 112 | mioutil. 113 | EXPECT(). 114 | ReadFile(gomock.Eq("path")). 115 | Return(nil, os.ErrNotExist) 116 | 117 | settings, err := LoadSettings() 118 | require.NoError(t, err) 119 | assert.False(t, settings.Exists) 120 | assert.Equal(t, "path", settings.Path) 121 | } 122 | 123 | func TestLoadSettings_happyPath(t *testing.T) { 124 | ctrl := setup(t) 125 | defer teardown(t, ctrl) 126 | 127 | mioutil := mock.NewMockIoutiler(ctrl) 128 | mxdg := mock.NewMockXdger(ctrl) 129 | 130 | appIoutil = mioutil 131 | appXdg = mxdg 132 | 133 | mxdg. 134 | EXPECT(). 135 | ConfigFile(gomock.Eq(settingsFile)). 136 | Return("path", nil) 137 | 138 | data := []byte(`{}`) 139 | mioutil. 140 | EXPECT(). 141 | ReadFile(gomock.Eq("path")). 142 | Return(data, nil) 143 | 144 | settings, err := LoadSettings() 145 | require.NoError(t, err) 146 | assert.True(t, settings.Exists) 147 | assert.Equal(t, "path", settings.Path) 148 | } 149 | -------------------------------------------------------------------------------- /pkg/crypt/crypt.go: -------------------------------------------------------------------------------- 1 | // Taken and adapted from: 2 | // - https://github.com/schollz/croc/blob/8dc5bd6e046194d0c5b1dc34b0fd1602f8f6c7ad/src/crypt/crypt.go#L1 3 | // - https://github.com/blend/go-sdk/blob/29e67762ae016aba504d9de96bd99cd4b23728f7/crypto/stream.go#L23 4 | package crypt 5 | 6 | import ( 7 | "crypto/aes" 8 | "crypto/cipher" 9 | "crypto/rand" 10 | "io" 11 | 12 | "golang.org/x/crypto/scrypt" 13 | ) 14 | 15 | const NonceLength = 12 16 | 17 | // DeriveKey uses scrypt to generate a fixed size key for further 18 | // encryption/decryption steps aka to initialise the block cipher. 19 | // After minimal research I chose scrypt over pbkdf2 as apparently 20 | // scrypt contains pbkdf2? Which is a good thing I guess. The 21 | // parameters below are taken from an example test in the scrypt 22 | // package. 23 | func DeriveKey(pw []byte, salt []byte) ([]byte, error) { 24 | return scrypt.Key(pw, salt, 1<<15, 8, 1, 32) 25 | } 26 | 27 | // Encrypt will encrypt the data with the given key. 28 | func Encrypt(key []byte, data []byte) ([]byte, error) { 29 | // Create a new AES cipher 30 | block, err := aes.NewCipher(key) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | // Never use more than 2^32 random nonces with a given key because of the risk of a repeat. 36 | nonce := make([]byte, NonceLength) 37 | if _, err := io.ReadFull(rand.Reader, nonce); err != nil { 38 | return nil, err 39 | } 40 | 41 | gcm, err := cipher.NewGCM(block) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | // Prefix sealed data with nonce for decryption - see below. 47 | return append(nonce, gcm.Seal(nil, nonce, data, nil)...), nil 48 | } 49 | 50 | // Decrypt uses key to decrypt the given data. 51 | func Decrypt(key []byte, data []byte) ([]byte, error) { 52 | // Create a new AES cipher 53 | block, err := aes.NewCipher(key) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | gcm, err := cipher.NewGCM(block) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // Use prefixed nonce from above. 64 | return gcm.Open(nil, data[:NonceLength], data[NonceLength:], nil) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/crypt/crypt_test.go: -------------------------------------------------------------------------------- 1 | // Taken and adapted from: https://github.com/schollz/croc/blob/8dc5bd6e046194d0c5b1dc34b0fd1602f8f6c7ad/src/crypt/crypt_test.go#L1 2 | package crypt 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func BenchmarkEncrypt(b *testing.B) { 11 | salt := []byte{} 12 | bob, _ := DeriveKey([]byte("password"), salt) 13 | for i := 0; i < b.N; i++ { 14 | Encrypt(bob, []byte("hello, world")) 15 | } 16 | } 17 | 18 | func BenchmarkDecrypt(b *testing.B) { 19 | salt := []byte{} 20 | key, _ := DeriveKey([]byte("password"), salt) 21 | msg := []byte("hello, world") 22 | enc, _ := Encrypt(key, msg) 23 | b.ResetTimer() 24 | for i := 0; i < b.N; i++ { 25 | Decrypt(key, enc) 26 | } 27 | } 28 | 29 | func TestEncryption(t *testing.T) { 30 | salt := []byte("some bytes") 31 | key, err := DeriveKey([]byte("password"), salt) 32 | assert.Nil(t, err) 33 | msg := []byte("hello, world!") 34 | enc, err := Encrypt(key, msg) 35 | assert.Nil(t, err) 36 | dec, err := Decrypt(key, enc) 37 | assert.Nil(t, err) 38 | assert.Equal(t, msg, dec) 39 | 40 | // check reusing the salt 41 | key2, err := DeriveKey([]byte("password"), salt) 42 | dec, err = Decrypt(key2, enc) 43 | assert.Nil(t, err) 44 | assert.Equal(t, msg, dec) 45 | 46 | // check reusing the salt 47 | key2, err = DeriveKey([]byte("wrong password"), salt) 48 | dec, err = Decrypt(key2, enc) 49 | assert.NotNil(t, err) 50 | assert.NotEqual(t, msg, dec) 51 | 52 | // error with no password 53 | dec, err = Decrypt([]byte(""), key) 54 | assert.NotNil(t, err) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/crypt/stream.go: -------------------------------------------------------------------------------- 1 | // Taken and adapted from: 2 | // - https://github.com/schollz/croc/blob/8dc5bd6e046194d0c5b1dc34b0fd1602f8f6c7ad/src/crypt/crypt.go#L1 3 | // - https://github.com/blend/go-sdk/blob/29e67762ae016aba504d9de96bd99cd4b23728f7/crypto/stream.go#L23 4 | 5 | package crypt 6 | 7 | import ( 8 | "crypto/aes" 9 | "crypto/cipher" 10 | "crypto/hmac" 11 | "crypto/rand" 12 | "crypto/sha256" 13 | "fmt" 14 | "hash" 15 | "io" 16 | ) 17 | 18 | // StreamEncrypter implements the Reader interface to be used in 19 | // streaming scenarios. 20 | type StreamEncrypter struct { 21 | dest io.Writer 22 | block cipher.Block 23 | stream cipher.Stream 24 | mac hash.Hash 25 | iv []byte 26 | } 27 | 28 | // NewStreamEncrypter initializes a stream encrypter. 29 | func NewStreamEncrypter(key []byte, dest io.Writer) (*StreamEncrypter, error) { 30 | // Create a new AES cipher 31 | block, err := aes.NewCipher(key) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | iv := make([]byte, block.BlockSize()) 37 | if _, err = rand.Read(iv); err != nil { 38 | return nil, err 39 | } 40 | 41 | return &StreamEncrypter{ 42 | dest: dest, 43 | block: block, 44 | stream: cipher.NewCTR(block, iv), 45 | mac: hmac.New(sha256.New, key), 46 | iv: iv, 47 | }, nil 48 | } 49 | 50 | // Write writes bytes encrypted to the writer interface. 51 | func (s *StreamEncrypter) Write(p []byte) (int, error) { 52 | buf := make([]byte, len(p)) // Could we get rid of this allocation? 53 | s.stream.XORKeyStream(buf, p) 54 | n, writeErr := s.dest.Write(buf) 55 | if err := writeHash(s.mac, buf[:n]); err != nil { 56 | return n, err 57 | } 58 | 59 | return n, writeErr 60 | } 61 | 62 | func (s *StreamEncrypter) InitializationVector() []byte { 63 | return s.iv 64 | } 65 | 66 | // Hash should be called after the whole payload was read. It 67 | // returns the SHA-256 hash of the payload. 68 | func (s *StreamEncrypter) Hash() []byte { 69 | return s.mac.Sum(nil) 70 | } 71 | 72 | // StreamDecrypter is a decrypter for a stream of data with authentication 73 | type StreamDecrypter struct { 74 | src io.Reader 75 | block cipher.Block 76 | stream cipher.Stream 77 | mac hash.Hash 78 | } 79 | 80 | // NewStreamDecrypter creates a new stream decrypter 81 | func NewStreamDecrypter(key []byte, iv []byte, src io.Reader) (*StreamDecrypter, error) { 82 | block, err := aes.NewCipher(key) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return &StreamDecrypter{ 88 | src: src, 89 | block: block, 90 | stream: cipher.NewCTR(block, iv), 91 | mac: hmac.New(sha256.New, key), 92 | }, nil 93 | } 94 | 95 | // Read reads bytes from the underlying reader and then decrypts them. 96 | func (s *StreamDecrypter) Read(p []byte) (int, error) { 97 | n, readErr := s.src.Read(p) 98 | if n > 0 { 99 | err := writeHash(s.mac, p[:n]) 100 | if err != nil { 101 | return n, err 102 | } 103 | s.stream.XORKeyStream(p[:n], p[:n]) 104 | return n, readErr 105 | } 106 | return 0, io.EOF 107 | } 108 | 109 | // Authenticate verifies that the hash of the stream is correct. This should only be called after processing is finished 110 | func (s *StreamDecrypter) Authenticate(hash []byte) error { 111 | if !hmac.Equal(hash, s.mac.Sum(nil)) { 112 | return fmt.Errorf("authentication failed") 113 | } 114 | return nil 115 | } 116 | 117 | // writeHash takes the given bytes and "extends" the current hash value. 118 | // if not all bytes could be written an error is returned. 119 | func writeHash(mac hash.Hash, p []byte) error { 120 | m, err := mac.Write(p) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | if m != len(p) { 126 | return fmt.Errorf("could not write all bytes to hmac") 127 | } 128 | 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /pkg/crypt/stream_test.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestStreamEncrypterDecrypter(t *testing.T) { 12 | salt := []byte("salt") 13 | pw := []byte("password") 14 | key, err := DeriveKey(pw, salt) 15 | assert.Nil(t, err) 16 | 17 | var buf bytes.Buffer 18 | se, err := NewStreamEncrypter(key, &buf) 19 | assert.Nil(t, err) 20 | assert.NotNil(t, se) 21 | 22 | payload := []byte("some text") 23 | _, _ = se.Write(payload) 24 | 25 | sd, err := NewStreamDecrypter(key, se.InitializationVector(), &buf) 26 | assert.Nil(t, err) 27 | assert.NotNil(t, sd) 28 | 29 | decrypted, err := ioutil.ReadAll(sd) 30 | assert.Nil(t, err) 31 | assert.Equal(t, payload, decrypted) 32 | 33 | assert.Nil(t, sd.Authenticate(se.Hash())) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/dht/advertiser.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/libp2p/go-libp2p-core/host" 8 | 9 | "github.com/dennis-tra/pcp/internal/log" 10 | "github.com/dennis-tra/pcp/internal/wrap" 11 | ) 12 | 13 | var ( 14 | // Timeout for pushing our data to the DHT. 15 | provideTimeout = time.Minute 16 | 17 | // Interval between two checks whether we know our public 18 | // IP address. This can take time until e.g. the identify 19 | // protocol has determined one for us. 20 | pubAddrInter = 50 * time.Millisecond 21 | ) 22 | 23 | // Advertiser is responsible for writing and renewing the DHT entry. 24 | type Advertiser struct { 25 | *protocol 26 | } 27 | 28 | // NewAdvertiser creates a new Advertiser. 29 | func NewAdvertiser(h host.Host, dht wrap.IpfsDHT) *Advertiser { 30 | return &Advertiser{newProtocol(h, dht)} 31 | } 32 | 33 | // Advertise establishes a connection to a set of bootstrap peers 34 | // that we're using to connect to the DHT. Then it puts the 35 | // discovery identifier into the DHT (timeout 1 minute - provideTimeout) 36 | // and renews the identifier when a new time slot is reached. 37 | // Time slots are used as a kind of sharding for peer discovery. 38 | // pcp nodes says: "Hey, you can find me with channel ID 123". Then, 39 | // one hour later another, completely unrelated pcp node comes along and says 40 | // "Hey, you can find me with channel ID 123". A peer searching for 123 41 | // would find the new and the stale entry. To avoid finding the stale entry 42 | // we use the current time truncated to 5 minute intervals (TruncateDuration). 43 | // When pcp is advertising its own channel-id + time slot it can happen that 44 | // it rolls over to the next time slot. Than pcp just advertises the new time slot 45 | // as well. It can still be found with the old one. 46 | func (a *Advertiser) Advertise(chanID int) error { 47 | if err := a.Bootstrap(); err != nil { 48 | return err 49 | } 50 | 51 | if err := a.ServiceStarted(); err != nil { 52 | return err 53 | } 54 | defer a.ServiceStopped() 55 | 56 | log.Debugln("DHT - Waiting for public IP...") 57 | for { 58 | // Only advertise in the DHT if we have a public addr. 59 | if !a.HasPublicAddr() { 60 | select { 61 | case <-a.SigShutdown(): 62 | return nil 63 | case <-time.After(pubAddrInter): 64 | continue 65 | } 66 | } 67 | log.Debugln("DHT - Identified a public IP in", a.Addrs()) 68 | break 69 | } 70 | 71 | for { 72 | err := a.provide(a.ServiceContext(), a.DiscoveryID(chanID)) 73 | if err == context.Canceled { 74 | break 75 | } else if err != nil && err != context.DeadlineExceeded { 76 | log.Warningf("Error providing: %s\n", err) 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // HasPublicAddr returns true if there is at least one public 84 | // address associated with the current node - aka we got at 85 | // least three confirmations from peers through the identify 86 | // protocol. 87 | func (a *Advertiser) HasPublicAddr() bool { 88 | for _, addr := range a.Addrs() { 89 | if wrapmanet.IsPublicAddr(addr) { 90 | return true 91 | } 92 | } 93 | return false 94 | } 95 | 96 | // Shutdown stops the advertise mechanics. 97 | func (a *Advertiser) Shutdown() { 98 | a.Service.Shutdown() 99 | } 100 | 101 | // the context requires a timeout; it determines how long the DHT looks for 102 | // closest peers to the key/CID before it goes on to provide the record to them. 103 | // Not setting a timeout here will make the DHT wander forever. 104 | func (a *Advertiser) provide(ctx context.Context, id string) error { 105 | log.Debugln("DHT - Advertising", id) 106 | defer log.Debugln("DHT - Advertising", id, "done") 107 | cID, err := strToCid(id) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | ctx, cancel := context.WithTimeout(ctx, provideTimeout) 113 | defer cancel() 114 | return a.dht.Provide(ctx, cID, true) 115 | } 116 | -------------------------------------------------------------------------------- /pkg/dht/advertiser_test.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/golang/mock/gomock" 11 | "github.com/ipfs/go-cid" 12 | ma "github.com/multiformats/go-multiaddr" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/dennis-tra/pcp/internal/mock" 17 | "github.com/dennis-tra/pcp/pkg/service" 18 | ) 19 | 20 | func TestAdvertiser_Advertise(t *testing.T) { 21 | ctrl, local, net, teardown := setup(t) 22 | defer teardown(t) 23 | 24 | mockDefaultBootstrapPeers(t, ctrl, net, local) 25 | 26 | dht := mock.NewMockIpfsDHT(ctrl) 27 | a := NewAdvertiser(local, dht) 28 | 29 | var wg sync.WaitGroup 30 | wg.Add(5) 31 | 32 | dht.EXPECT(). 33 | Provide(gomock.Any(), gomock.Any(), true). 34 | DoAndReturn(func(ctx context.Context, c cid.Cid, brdcst bool) (err error) { 35 | wg.Done() 36 | select { 37 | case <-ctx.Done(): 38 | return ctx.Err() 39 | case <-time.After(10 * time.Millisecond): 40 | return nil 41 | } 42 | }).AnyTimes() 43 | 44 | go func() { 45 | wg.Wait() 46 | a.Shutdown() 47 | }() 48 | 49 | err := a.Advertise(333) 50 | assert.NoError(t, err) 51 | } 52 | 53 | func TestAdvertiser_Advertise_deadlineExceeded(t *testing.T) { 54 | ctrl, local, net, teardown := setup(t) 55 | defer teardown(t) 56 | 57 | provideTimeout = 10 * time.Millisecond 58 | 59 | mockDefaultBootstrapPeers(t, ctrl, net, local) 60 | 61 | dht := mock.NewMockIpfsDHT(ctrl) 62 | a := NewAdvertiser(local, dht) 63 | 64 | var wg sync.WaitGroup 65 | wg.Add(5) 66 | 67 | dht.EXPECT(). 68 | Provide(gomock.Any(), gomock.Any(), true). 69 | DoAndReturn(func(ctx context.Context, c cid.Cid, brdcst bool) (err error) { 70 | wg.Done() 71 | <-ctx.Done() 72 | return ctx.Err() 73 | }).Times(5) 74 | 75 | go func() { 76 | wg.Wait() 77 | a.Shutdown() 78 | }() 79 | 80 | err := a.Advertise(333) 81 | assert.NoError(t, err) 82 | } 83 | 84 | func TestAdvertiser_Advertise_provideAfterPublicAddr(t *testing.T) { 85 | ctrl, local, net, teardown := setup(t) 86 | defer teardown(t) 87 | 88 | pubAddrInter = 10 * time.Millisecond 89 | 90 | mockDefaultBootstrapPeers(t, ctrl, net, local) 91 | 92 | dht := mock.NewMockIpfsDHT(ctrl) 93 | manet := mock.NewMockManeter(ctrl) 94 | 95 | a := NewAdvertiser(local, dht) 96 | 97 | gomock.InOrder( 98 | manet.EXPECT().IsPublicAddr(local.Addrs()[0]).Return(false).Times(1), 99 | manet.EXPECT().IsPublicAddr(local.Addrs()[0]).Return(false).Times(1), 100 | manet.EXPECT().IsPublicAddr(local.Addrs()[0]).Return(true).Times(1), 101 | ) 102 | wrapmanet = manet 103 | 104 | var wg sync.WaitGroup 105 | wg.Add(1) 106 | 107 | dht.EXPECT(). 108 | Provide(gomock.Any(), gomock.Any(), true). 109 | DoAndReturn(func(ctx context.Context, c cid.Cid, brdcst bool) (err error) { 110 | wg.Done() 111 | <-ctx.Done() 112 | return ctx.Err() 113 | }).Times(1) 114 | 115 | go func() { 116 | wg.Wait() 117 | a.Shutdown() 118 | }() 119 | 120 | err := a.Advertise(333) 121 | assert.NoError(t, err) 122 | } 123 | 124 | func TestAdvertiser_Advertise_listensOnShutdownIfNoPublicAddr(t *testing.T) { 125 | ctrl, local, net, teardown := setup(t) 126 | defer teardown(t) 127 | 128 | mockDefaultBootstrapPeers(t, ctrl, net, local) 129 | 130 | dht := mock.NewMockIpfsDHT(ctrl) 131 | manet := mock.NewMockManeter(ctrl) 132 | 133 | var wg sync.WaitGroup 134 | wg.Add(2) 135 | 136 | manet.EXPECT(). 137 | IsPublicAddr(local.Addrs()[0]). 138 | DoAndReturn(func(a ma.Multiaddr) bool { 139 | wg.Done() 140 | return false 141 | }). 142 | AnyTimes() 143 | wrapmanet = manet 144 | 145 | a := NewAdvertiser(local, dht) 146 | dht.EXPECT(). 147 | Provide(gomock.Any(), gomock.Any(), true). 148 | Times(0) 149 | 150 | go func() { 151 | wg.Wait() 152 | a.Shutdown() 153 | }() 154 | 155 | err := a.Advertise(333) 156 | assert.NoError(t, err) 157 | } 158 | 159 | func TestAdvertiser_Advertise_propagatesServiceAlreadyStarted(t *testing.T) { 160 | ctrl, local, net, teardown := setup(t) 161 | defer teardown(t) 162 | 163 | mockDefaultBootstrapPeers(t, ctrl, net, local) 164 | 165 | a := NewAdvertiser(local, mock.NewMockIpfsDHT(ctrl)) 166 | 167 | err := a.ServiceStarted() 168 | require.NoError(t, err) 169 | 170 | err = a.Advertise(333) 171 | assert.Equal(t, service.ErrServiceAlreadyStarted, err) 172 | } 173 | 174 | func TestAdvertiser_Advertise_continuesToProvideOnError(t *testing.T) { 175 | ctrl, local, net, teardown := setup(t) 176 | defer teardown(t) 177 | 178 | TruncateDuration = 10 * time.Millisecond 179 | 180 | mockDefaultBootstrapPeers(t, ctrl, net, local) 181 | dht := mock.NewMockIpfsDHT(ctrl) 182 | 183 | var wg sync.WaitGroup 184 | wg.Add(5) 185 | 186 | var cids []string 187 | 188 | dht.EXPECT(). 189 | Provide(gomock.Any(), gomock.Any(), true). 190 | DoAndReturn(func(ctx context.Context, c cid.Cid, brdcst bool) (err error) { 191 | cids = append(cids, c.String()) 192 | wg.Done() 193 | select { 194 | case <-ctx.Done(): 195 | return ctx.Err() 196 | case <-time.After(10 * time.Millisecond): 197 | return fmt.Errorf("some error") 198 | } 199 | }).Times(5) 200 | 201 | a := NewAdvertiser(local, dht) 202 | 203 | go func() { 204 | wg.Wait() 205 | a.Shutdown() 206 | }() 207 | 208 | err := a.Advertise(333) 209 | require.NoError(t, err) 210 | } 211 | 212 | func TestAdvertiser_Advertise_mutatesDiscoveryIdentifier(t *testing.T) { 213 | ctrl, local, net, teardown := setup(t) 214 | defer teardown(t) 215 | 216 | TruncateDuration = 10 * time.Millisecond 217 | 218 | mockDefaultBootstrapPeers(t, ctrl, net, local) 219 | dht := mock.NewMockIpfsDHT(ctrl) 220 | 221 | var wg sync.WaitGroup 222 | wg.Add(2) 223 | 224 | var cids []string 225 | 226 | dht.EXPECT(). 227 | Provide(gomock.Any(), gomock.Any(), true). 228 | DoAndReturn(func(ctx context.Context, c cid.Cid, brdcst bool) (err error) { 229 | cids = append(cids, c.String()) 230 | wg.Done() 231 | select { 232 | case <-ctx.Done(): 233 | return ctx.Err() 234 | case <-time.After(50 * time.Millisecond): 235 | return nil 236 | } 237 | }).Times(2) 238 | 239 | a := NewAdvertiser(local, dht) 240 | 241 | go func() { 242 | wg.Wait() 243 | a.Shutdown() 244 | }() 245 | 246 | err := a.Advertise(333) 247 | require.NoError(t, err) 248 | 249 | assert.NotEqual(t, cids[0], cids[1]) 250 | } 251 | -------------------------------------------------------------------------------- /pkg/dht/discoverer.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/libp2p/go-libp2p-core/host" 8 | "github.com/libp2p/go-libp2p-core/peer" 9 | ma "github.com/multiformats/go-multiaddr" 10 | manet "github.com/multiformats/go-multiaddr/net" 11 | 12 | "github.com/dennis-tra/pcp/internal/log" 13 | "github.com/dennis-tra/pcp/internal/wrap" 14 | ) 15 | 16 | // Discoverer is responsible for reading the DHT for an 17 | // entry with the channel ID given below. 18 | type Discoverer struct { 19 | *protocol 20 | } 21 | 22 | // NewDiscoverer creates a new Discoverer. 23 | func NewDiscoverer(h host.Host, dht wrap.IpfsDHT) *Discoverer { 24 | return &Discoverer{newProtocol(h, dht)} 25 | } 26 | 27 | // Discover establishes a connection to a set of bootstrap peers 28 | // that we're using to connect to the DHT. It tries to find 29 | func (d *Discoverer) Discover(chanID int, handler func(info peer.AddrInfo)) error { 30 | if err := d.ServiceStarted(); err != nil { 31 | return err 32 | } 33 | defer d.ServiceStopped() 34 | 35 | if err := d.Bootstrap(); err != nil { 36 | return err 37 | } 38 | 39 | for { 40 | did := d.DiscoveryID(chanID) 41 | log.Debugln("DHT - Discovering", did) 42 | cID, err := strToCid(did) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Find new provider with a timeout, so the discovery ID is renewed if necessary. 48 | ctx, cancel := context.WithTimeout(d.ServiceContext(), provideTimeout) 49 | for pi := range d.dht.FindProvidersAsync(ctx, cID, 100) { 50 | log.Debugln("DHT - Found peer ", pi.ID) 51 | pi.Addrs = onlyPublic(pi.Addrs) 52 | if isRoutable(pi) { 53 | go handler(pi) 54 | } 55 | } 56 | log.Debugln("DHT - Discovering", did, " done.") 57 | 58 | // cannot defer cancel in this for loop 59 | cancel() 60 | 61 | select { 62 | case <-d.SigShutdown(): 63 | return nil 64 | default: 65 | } 66 | } 67 | } 68 | 69 | func (d *Discoverer) SetOffset(offset time.Duration) *Discoverer { 70 | d.offset = offset 71 | return d 72 | } 73 | 74 | func (d *Discoverer) Shutdown() { 75 | d.Service.Shutdown() 76 | } 77 | 78 | // Filter out addresses that are local - only allow public ones. 79 | func onlyPublic(addrs []ma.Multiaddr) []ma.Multiaddr { 80 | routable := []ma.Multiaddr{} 81 | for _, addr := range addrs { 82 | if manet.IsPublicAddr(addr) { 83 | routable = append(routable, addr) 84 | log.Debugf("\tpublic - %s\n", addr.String()) 85 | } else { 86 | log.Debugf("\tprivate - %s\n", addr.String()) 87 | } 88 | } 89 | return routable 90 | } 91 | 92 | func isRoutable(pi peer.AddrInfo) bool { 93 | return len(pi.Addrs) > 0 94 | } 95 | -------------------------------------------------------------------------------- /pkg/dht/discoverer_test.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" 10 | 11 | "github.com/golang/mock/gomock" 12 | "github.com/ipfs/go-cid" 13 | "github.com/libp2p/go-libp2p-core/peer" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | 17 | "github.com/dennis-tra/pcp/internal/mock" 18 | ) 19 | 20 | func TestDiscoverer_Discover_happyPath(t *testing.T) { 21 | ctrl, local, net, teardown := setup(t) 22 | defer teardown(t) 23 | 24 | mockDefaultBootstrapPeers(t, ctrl, net, local) 25 | 26 | dht := mock.NewMockIpfsDHT(ctrl) 27 | d := NewDiscoverer(local, dht) 28 | 29 | piChan := make(chan peer.AddrInfo) 30 | dht.EXPECT(). 31 | FindProvidersAsync(gomock.Any(), gomock.Any(), 100). 32 | DoAndReturn(func(ctx context.Context, cID cid.Cid, count int) <-chan peer.AddrInfo { 33 | go func() { 34 | <-ctx.Done() 35 | close(piChan) 36 | }() 37 | return piChan 38 | }) 39 | 40 | var wg sync.WaitGroup 41 | wg.Add(2) 42 | 43 | provider1, err := net.GenPeer() 44 | require.NoError(t, err) 45 | 46 | provider2, err := net.GenPeer() 47 | require.NoError(t, err) 48 | 49 | go func() { 50 | piChan <- peer.AddrInfo{ 51 | ID: provider1.ID(), 52 | Addrs: provider1.Addrs(), 53 | } 54 | 55 | piChan <- peer.AddrInfo{ 56 | ID: provider2.ID(), 57 | Addrs: provider2.Addrs(), 58 | } 59 | }() 60 | 61 | handler := func(pi peer.AddrInfo) { 62 | assert.True(t, pi.ID == provider1.ID() || pi.ID == provider2.ID()) 63 | wg.Done() 64 | } 65 | 66 | go func() { 67 | wg.Wait() 68 | d.Shutdown() 69 | }() 70 | 71 | err = d.Discover(333, handler) 72 | assert.NoError(t, err) 73 | } 74 | 75 | func TestDiscoverer_Discover_reschedulesFindProvider(t *testing.T) { 76 | ctrl, local, net, teardown := setup(t) 77 | defer teardown(t) 78 | 79 | mockDefaultBootstrapPeers(t, ctrl, net, local) 80 | 81 | dht := mock.NewMockIpfsDHT(ctrl) 82 | d := NewDiscoverer(local, dht) 83 | 84 | var wg sync.WaitGroup 85 | wg.Add(5) 86 | 87 | dht.EXPECT(). 88 | FindProvidersAsync(gomock.Any(), gomock.Any(), 100). 89 | DoAndReturn(func(ctx context.Context, cID cid.Cid, count int) <-chan peer.AddrInfo { 90 | piChan := make(chan peer.AddrInfo) 91 | go close(piChan) 92 | wg.Done() 93 | return piChan 94 | }).Times(5) 95 | 96 | go func() { 97 | wg.Wait() 98 | d.Shutdown() 99 | }() 100 | 101 | err := d.Discover(333, nil) 102 | assert.NoError(t, err) 103 | } 104 | 105 | func TestDiscoverer_Discover_callsFindProviderWithMutatingDiscoveryIDs(t *testing.T) { 106 | ctrl, local, net, teardown := setup(t) 107 | defer teardown(t) 108 | 109 | TruncateDuration = 10 * time.Millisecond 110 | 111 | mockDefaultBootstrapPeers(t, ctrl, net, local) 112 | 113 | dht := mock.NewMockIpfsDHT(ctrl) 114 | d := NewDiscoverer(local, dht) 115 | 116 | var wg sync.WaitGroup 117 | wg.Add(2) 118 | 119 | var cIDs []string 120 | dht.EXPECT(). 121 | FindProvidersAsync(gomock.Any(), gomock.Any(), 100). 122 | DoAndReturn(func(ctx context.Context, cID cid.Cid, count int) <-chan peer.AddrInfo { 123 | cIDs = append(cIDs, cID.String()) 124 | piChan := make(chan peer.AddrInfo) 125 | go func() { 126 | time.Sleep(2 * TruncateDuration) 127 | close(piChan) 128 | }() 129 | wg.Done() 130 | return piChan 131 | }).Times(2) 132 | 133 | go func() { 134 | wg.Wait() 135 | d.Shutdown() 136 | }() 137 | 138 | err := d.Discover(333, nil) 139 | assert.NoError(t, err) 140 | 141 | assert.NotEqual(t, cIDs[0], cIDs[1]) 142 | } 143 | 144 | func TestTimeCriticalDiscoverer_Discover_restartAsSoonAsCurrentTimeSlotIsExpired(t *testing.T) { 145 | if testing.Short() { 146 | t.Skip("skipping time critical test") // They are flaky on GitHub actions 147 | } 148 | 149 | ctrl, local, net, teardown := setup(t) 150 | defer teardown(t) 151 | 152 | provideTimeout = 20 * time.Millisecond 153 | 154 | mockDefaultBootstrapPeers(t, ctrl, net, local) 155 | 156 | dht := mock.NewMockIpfsDHT(ctrl) 157 | d := NewDiscoverer(local, dht) 158 | 159 | rounds := 5 160 | var wg sync.WaitGroup 161 | wg.Add(rounds) 162 | 163 | dht.EXPECT(). 164 | FindProvidersAsync(gomock.Any(), gomock.Any(), 100). 165 | DoAndReturn(func(ctx context.Context, cID cid.Cid, count int) <-chan peer.AddrInfo { 166 | piChan := make(chan peer.AddrInfo) 167 | go func() { 168 | <-ctx.Done() 169 | close(piChan) 170 | }() 171 | wg.Done() 172 | return piChan 173 | }).Times(rounds) 174 | 175 | go func() { 176 | wg.Wait() 177 | d.Shutdown() 178 | }() 179 | 180 | start := time.Now() 181 | err := d.Discover(333, nil) 182 | end := time.Now() 183 | 184 | assert.NoError(t, err) 185 | 186 | // Only 4 because last round is immediately termianated by d.Shutdown() 187 | assert.InDelta(t, 4*provideTimeout, end.Sub(start), float64(provideTimeout)) 188 | } 189 | 190 | func TestDiscoverer_SetOffset(t *testing.T) { 191 | net := mocknet.New(context.Background()) 192 | local, err := net.GenPeer() 193 | require.NoError(t, err) 194 | d := NewDiscoverer(local, nil) 195 | id1 := d.DiscoveryID(333) 196 | d.SetOffset(TruncateDuration * 3) 197 | id2 := d.DiscoveryID(333) 198 | assert.NotEqual(t, id1, id2) 199 | } 200 | -------------------------------------------------------------------------------- /pkg/dht/error.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dennis-tra/pcp/internal/log" 7 | ) 8 | 9 | type ErrConnThresholdNotReached struct { 10 | BootstrapErrs []error 11 | } 12 | 13 | func (e ErrConnThresholdNotReached) Error() string { 14 | return "could not establish enough connections to bootstrap peers" 15 | } 16 | 17 | func (e ErrConnThresholdNotReached) Log() { 18 | // If only one error is context.Canceled the user stopped the 19 | // program and we don't want to print errors. 20 | for _, err := range e.BootstrapErrs { 21 | if err == context.Canceled { 22 | return 23 | } 24 | } 25 | 26 | log.Warningln(e) 27 | for _, err := range e.BootstrapErrs { 28 | log.Warningf("\t%s\n", err) 29 | } 30 | 31 | log.Warningln("this means you will only be able to transfer files in your local network") 32 | } 33 | -------------------------------------------------------------------------------- /pkg/dht/protocol.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/ipfs/go-cid" 9 | "github.com/libp2p/go-libp2p-core/host" 10 | "github.com/libp2p/go-libp2p-core/peer" 11 | mh "github.com/multiformats/go-multihash" 12 | 13 | "github.com/dennis-tra/pcp/internal/wrap" 14 | "github.com/dennis-tra/pcp/pkg/service" 15 | ) 16 | 17 | // These wrapped top level functions are here for testing purposes. 18 | var ( 19 | wrapDHT wrap.DHTer = wrap.DHT{} 20 | wraptime wrap.Timer = wrap.Time{} 21 | wrapmanet wrap.Maneter = wrap.Manet{} 22 | ) 23 | 24 | var ( 25 | // ConnThreshold represents the minimum number of bootstrap peers we need a connection to. 26 | ConnThreshold = 3 27 | 28 | // TruncateDuration represents the time slot to which the current time is truncated. 29 | TruncateDuration = 5 * time.Minute 30 | 31 | // bootstrap holds the sync.Onces for each host, so that bootstrap is called for each host 32 | // only once. 33 | bootstrap = map[peer.ID]*sync.Once{} // may need locking in theory? 34 | ) 35 | 36 | // protocol encapsulates the logic for discovering peers 37 | // through providing it in the IPFS DHT. 38 | type protocol struct { 39 | host.Host 40 | 41 | // Service holds an abstraction of a long running 42 | // service that is started and stopped externally. 43 | *service.Service 44 | dht wrap.IpfsDHT 45 | 46 | offset time.Duration 47 | } 48 | 49 | func newProtocol(h host.Host, dht wrap.IpfsDHT) *protocol { 50 | bootstrap[h.ID()] = &sync.Once{} 51 | return &protocol{Host: h, dht: dht, Service: service.New("DHT")} 52 | } 53 | 54 | // Bootstrap connects to a set of bootstrap nodes to connect 55 | // to the DHT. 56 | func (p *protocol) Bootstrap() (err error) { 57 | // The receiving peer looks for the current and previous time slot. So it would call 58 | // bootstrap twice. Here we're limiting it to only one call. 59 | once := bootstrap[p.ID()] 60 | once.Do(func() { 61 | peers := wrapDHT.GetDefaultBootstrapPeerAddrInfos() 62 | peerCount := len(peers) 63 | if peerCount == 0 { 64 | err = fmt.Errorf("no bootstrap peers configured") 65 | return 66 | } 67 | 68 | // Asynchronously connect to all bootstrap peers and send 69 | // potential errors to a channel. This channel is used 70 | // to capture the errors and check if we have established 71 | // enough connections. An error group (errgroup) cannot 72 | // be used here as it exits as soon as an error is thrown 73 | // in one of the Go-Routines. 74 | var wg sync.WaitGroup 75 | errChan := make(chan error, peerCount) 76 | for _, bp := range peers { 77 | wg.Add(1) 78 | go func(pi peer.AddrInfo) { 79 | defer wg.Done() 80 | errChan <- p.Connect(p.ServiceContext(), pi) 81 | }(bp) 82 | } 83 | 84 | // Close error channel after all connection attempts are done 85 | // to signal the for-loop below to stop. 86 | go func() { 87 | wg.Wait() 88 | close(errChan) 89 | }() 90 | 91 | // Reading the error channel and collect errors. 92 | errs := ErrConnThresholdNotReached{BootstrapErrs: []error{}} 93 | for { 94 | err, ok := <-errChan 95 | if !ok { 96 | // channel was closed. 97 | break 98 | } else if err != nil { 99 | errs.BootstrapErrs = append(errs.BootstrapErrs, err) 100 | } 101 | } 102 | 103 | // If we could not establish enough connections return an error 104 | if peerCount-len(errs.BootstrapErrs) < ConnThreshold { 105 | err = errs 106 | } 107 | }) 108 | return 109 | } 110 | 111 | // TimeSlotStart returns the time when the current time slot started.f 112 | func (p *protocol) TimeSlotStart() time.Time { 113 | return p.refTime().Truncate(TruncateDuration) 114 | } 115 | 116 | // refTime returns the reference time to calculate the time slot from. 117 | func (p *protocol) refTime() time.Time { 118 | return wraptime.Now().Add(p.offset) 119 | } 120 | 121 | // DiscoveryID returns the string, that we use to advertise 122 | // via mDNS and the DHT. See chanID above for more information. 123 | // Using UnixNano for testing. 124 | func (p *protocol) DiscoveryID(chanID int) string { 125 | return fmt.Sprintf("/pcp/%d/%d", p.TimeSlotStart().UnixNano(), chanID) 126 | } 127 | 128 | // strToCid hashes the given string (SHA256) and produces a CID from that hash. 129 | func strToCid(str string) (cid.Cid, error) { 130 | h, err := mh.Sum([]byte(str), mh.SHA2_256, -1) 131 | if err != nil { 132 | return cid.Undef, err 133 | } 134 | return cid.NewCidV1(cid.Raw, h), nil 135 | } 136 | -------------------------------------------------------------------------------- /pkg/dht/protocol_test.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "strings" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/golang/mock/gomock" 12 | "github.com/libp2p/go-libp2p-core/host" 13 | "github.com/libp2p/go-libp2p-core/peer" 14 | "github.com/libp2p/go-libp2p/p2p/net/mock" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | 18 | "github.com/dennis-tra/pcp/internal/mock" 19 | "github.com/dennis-tra/pcp/internal/wrap" 20 | ) 21 | 22 | func setup(t *testing.T) (*gomock.Controller, host.Host, mocknet.Mocknet, func(t *testing.T)) { 23 | wrapDHT = wrap.DHT{} 24 | wrapmanet = wrap.Manet{} 25 | wraptime = wrap.Time{} 26 | bootstrap = map[peer.ID]*sync.Once{} 27 | 28 | ctrl := gomock.NewController(t) 29 | 30 | net := mocknet.New(context.Background()) 31 | 32 | tmpTruncateDuration := TruncateDuration 33 | tmpPubAddrInter := pubAddrInter 34 | tmpProvideTimeout := provideTimeout 35 | 36 | local, err := net.GenPeer() 37 | require.NoError(t, err) 38 | 39 | return ctrl, local, net, func(t *testing.T) { 40 | ctrl.Finish() 41 | 42 | TruncateDuration = tmpTruncateDuration 43 | pubAddrInter = tmpPubAddrInter 44 | provideTimeout = tmpProvideTimeout 45 | 46 | wrapDHT = wrap.DHT{} 47 | wrapmanet = wrap.Manet{} 48 | wraptime = wrap.Time{} 49 | } 50 | } 51 | 52 | func mockGetDefaultBootstrapPeerAddrInfos(ctrl *gomock.Controller, pis []peer.AddrInfo) { 53 | mockDHT := mock.NewMockDHTer(ctrl) 54 | mockDHT.EXPECT(). 55 | GetDefaultBootstrapPeerAddrInfos(). 56 | Return(pis) 57 | wrapDHT = mockDHT 58 | } 59 | 60 | func mockDefaultBootstrapPeers(t *testing.T, ctrl *gomock.Controller, net mocknet.Mocknet, h host.Host) { 61 | mockDHT := mock.NewMockDHTer(ctrl) 62 | mockDHT.EXPECT(). 63 | GetDefaultBootstrapPeerAddrInfos(). 64 | Return(genPeers(t, net, h, 3)) 65 | wrapDHT = mockDHT 66 | } 67 | 68 | func genPeers(t *testing.T, net mocknet.Mocknet, h host.Host, count int) []peer.AddrInfo { 69 | var peers []peer.AddrInfo 70 | for i := 0; i < count; i++ { 71 | p, err := net.GenPeer() 72 | require.NoError(t, err) 73 | 74 | _, err = net.LinkPeers(h.ID(), p.ID()) 75 | require.NoError(t, err) 76 | 77 | peers = append(peers, peer.AddrInfo{ 78 | ID: p.ID(), 79 | Addrs: p.Addrs(), 80 | }) 81 | } 82 | 83 | return peers 84 | } 85 | 86 | func TestProtocol_Bootstrap_connectsBootstrapPeers(t *testing.T) { 87 | ctrl, local, net, teardown := setup(t) 88 | defer teardown(t) 89 | 90 | peers := genPeers(t, net, local, ConnThreshold) 91 | mockGetDefaultBootstrapPeerAddrInfos(ctrl, peers) 92 | 93 | err := newProtocol(local, nil).Bootstrap() 94 | require.NoError(t, err) 95 | 96 | // Check if they are connected. 97 | assert.Len(t, net.Net(local.ID()).Peers(), ConnThreshold) 98 | } 99 | 100 | func TestTimeCriticalProtocol_Bootstrap_connectsBootstrapPeersInParallel(t *testing.T) { 101 | if testing.Short() { 102 | t.Skip("skipping time critical test") // They are flaky on GitHub actions 103 | } 104 | 105 | ctrl, local, net, teardown := setup(t) 106 | defer teardown(t) 107 | 108 | peers := genPeers(t, net, local, 100) 109 | mockGetDefaultBootstrapPeerAddrInfos(ctrl, peers) 110 | 111 | latency := 50 * time.Millisecond 112 | for _, p := range peers { 113 | links := net.LinksBetweenPeers(local.ID(), p.ID) 114 | for _, l := range links { 115 | l.SetOptions(mocknet.LinkOptions{ 116 | Latency: latency, 117 | }) 118 | } 119 | } 120 | 121 | start := time.Now() 122 | err := newProtocol(local, nil).Bootstrap() 123 | end := time.Now() 124 | 125 | require.NoError(t, err) 126 | 127 | assert.InDelta(t, latency, end.Sub(start), float64(latency)/2) 128 | 129 | // Check if they are connected. 130 | assert.Len(t, net.Net(local.ID()).Peers(), 100) 131 | } 132 | 133 | func TestProtocol_Bootstrap_errorOnNoConfiguredBootstrapPeers(t *testing.T) { 134 | ctrl, local, _, teardown := setup(t) 135 | defer teardown(t) 136 | 137 | mockGetDefaultBootstrapPeerAddrInfos(ctrl, []peer.AddrInfo{}) 138 | 139 | err := newProtocol(local, nil).Bootstrap() 140 | assert.Error(t, err) 141 | } 142 | 143 | func TestProtocol_Bootstrap_cantConnectToOneLessThanThreshold(t *testing.T) { 144 | ctrl, local, net, teardown := setup(t) 145 | defer teardown(t) 146 | 147 | peers := genPeers(t, net, local, ConnThreshold) 148 | mockGetDefaultBootstrapPeerAddrInfos(ctrl, peers) 149 | 150 | err := net.UnlinkPeers(local.ID(), peers[0].ID) 151 | require.NoError(t, err) 152 | 153 | err = newProtocol(local, nil).Bootstrap() 154 | assert.Error(t, err) 155 | 156 | errs, ok := err.(ErrConnThresholdNotReached) 157 | assert.True(t, ok) 158 | assert.Contains(t, errs.BootstrapErrs[0].Error(), local.ID().String()) 159 | assert.Contains(t, errs.BootstrapErrs[0].Error(), peers[0].ID.String()) 160 | } 161 | 162 | func TestProtocol_Bootstrap_cantConnectToMultipleLessThanThreshold(t *testing.T) { 163 | ctrl, local, net, teardown := setup(t) 164 | defer teardown(t) 165 | 166 | peers := genPeers(t, net, local, ConnThreshold) 167 | mockGetDefaultBootstrapPeerAddrInfos(ctrl, peers) 168 | 169 | err := net.UnlinkPeers(local.ID(), peers[0].ID) 170 | require.NoError(t, err) 171 | 172 | err = net.UnlinkPeers(local.ID(), peers[1].ID) 173 | require.NoError(t, err) 174 | 175 | err = newProtocol(local, nil).Bootstrap() 176 | assert.Error(t, err) 177 | 178 | errs, ok := err.(ErrConnThresholdNotReached) 179 | assert.True(t, ok) 180 | assert.Contains(t, errs.BootstrapErrs[0].Error(), local.ID().String()) 181 | errStr := strings.Join([]string{errs.BootstrapErrs[0].Error(), errs.BootstrapErrs[1].Error()}, " ") 182 | assert.Contains(t, errStr, peers[0].ID.String()) 183 | assert.Contains(t, errStr, peers[1].ID.String()) 184 | } 185 | 186 | func TestProtocol_Bootstrap_cantConnectButGreaterThanThreshold(t *testing.T) { 187 | ctrl, local, net, teardown := setup(t) 188 | defer teardown(t) 189 | 190 | peers := genPeers(t, net, local, ConnThreshold+1) 191 | mockGetDefaultBootstrapPeerAddrInfos(ctrl, peers) 192 | 193 | err := net.UnlinkPeers(local.ID(), peers[0].ID) 194 | require.NoError(t, err) 195 | 196 | err = newProtocol(local, nil).Bootstrap() 197 | assert.NoError(t, err) 198 | } 199 | 200 | func TestProtocol_DiscoveryIdentifier_returnsCorrect(t *testing.T) { 201 | ctrl, local, _, teardown := setup(t) 202 | defer teardown(t) 203 | 204 | now := time.Now() 205 | 206 | m := mock.NewMockTimer(ctrl) 207 | m.EXPECT().Now().Return(now) 208 | wraptime = m 209 | 210 | p := newProtocol(local, nil) 211 | id := p.DiscoveryID(333) 212 | 213 | unixNow := now.Truncate(TruncateDuration).UnixNano() 214 | assert.Equal(t, "/pcp/"+strconv.Itoa(int(unixNow))+"/333", id) 215 | } 216 | -------------------------------------------------------------------------------- /pkg/mdns/advertiser.go: -------------------------------------------------------------------------------- 1 | package mdns 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dennis-tra/pcp/internal/log" 7 | 8 | "github.com/libp2p/go-libp2p-core/host" 9 | ) 10 | 11 | type Advertiser struct { 12 | *protocol 13 | } 14 | 15 | func NewAdvertiser(h host.Host) *Advertiser { 16 | return &Advertiser{newProtocol(h)} 17 | } 18 | 19 | // Advertise broadcasts that we're providing data for the given code. 20 | // 21 | // TODO: NewMdnsService also polls for peers. This is quite chatty, so we could extract the server-only logic. 22 | func (a *Advertiser) Advertise(chanID int) error { 23 | if err := a.ServiceStarted(); err != nil { 24 | return err 25 | } 26 | defer a.ServiceStopped() 27 | 28 | for { 29 | did := a.DiscoveryID(chanID) 30 | log.Debugln("mDNS - Advertising ", did) 31 | ctx, cancel := context.WithTimeout(a.ServiceContext(), Timeout) 32 | mdns, err := wrapdiscovery.NewMdnsService(ctx, a, a.interval, did) 33 | if err != nil { 34 | cancel() 35 | return err 36 | } 37 | 38 | select { 39 | case <-a.SigShutdown(): 40 | log.Debugln("mDNS - Advertising", did, " done - shutdown signal") 41 | cancel() 42 | return mdns.Close() 43 | case <-ctx.Done(): 44 | log.Debugln("mDNS - Advertising", did, "done -", ctx.Err()) 45 | cancel() 46 | if ctx.Err() == context.DeadlineExceeded { 47 | _ = mdns.Close() 48 | continue 49 | } else if ctx.Err() == context.Canceled { 50 | _ = mdns.Close() 51 | return nil 52 | } 53 | } 54 | } 55 | } 56 | 57 | func (a *Advertiser) Shutdown() { 58 | a.Service.Shutdown() 59 | } 60 | -------------------------------------------------------------------------------- /pkg/mdns/advertiser_test.go: -------------------------------------------------------------------------------- 1 | package mdns 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/libp2p/go-libp2p/p2p/discovery" 10 | 11 | "github.com/dennis-tra/pcp/internal/wrap" 12 | "github.com/libp2p/go-libp2p-core/host" 13 | mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" 14 | 15 | "github.com/golang/mock/gomock" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | 19 | "github.com/dennis-tra/pcp/internal/mock" 20 | ) 21 | 22 | func setup(t *testing.T) (*gomock.Controller, host.Host, func(t *testing.T)) { 23 | wrapdiscovery = wrap.Discovery{} 24 | wraptime = wrap.Time{} 25 | 26 | ctrl := gomock.NewController(t) 27 | 28 | net := mocknet.New(context.Background()) 29 | 30 | tmpTruncateDuration := TruncateDuration 31 | tmpInterval := Interval 32 | tmpTimeout := Timeout 33 | 34 | local, err := net.GenPeer() 35 | require.NoError(t, err) 36 | 37 | return ctrl, local, func(t *testing.T) { 38 | ctrl.Finish() 39 | 40 | TruncateDuration = tmpTruncateDuration 41 | Interval = tmpInterval 42 | Timeout = tmpTimeout 43 | 44 | wrapdiscovery = wrap.Discovery{} 45 | wraptime = wrap.Time{} 46 | } 47 | } 48 | 49 | func TestAdvertiser_Advertise(t *testing.T) { 50 | ctrl, local, teardown := setup(t) 51 | defer teardown(t) 52 | 53 | d := mock.NewMockDiscoverer(ctrl) 54 | wrapdiscovery = d 55 | 56 | a := NewAdvertiser(local) 57 | 58 | var wg sync.WaitGroup 59 | wg.Add(1) 60 | 61 | d.EXPECT(). 62 | NewMdnsService(gomock.Any(), a, Interval, gomock.Any()). 63 | DoAndReturn(func(ctx context.Context, peerhost host.Host, interval time.Duration, serviceTag string) (discovery.Service, error) { 64 | wg.Done() 65 | return DummyMDNSService{}, nil 66 | }). 67 | Times(1) 68 | 69 | go func() { 70 | err := a.Advertise(333) 71 | assert.NoError(t, err) 72 | wg.Done() 73 | }() 74 | wg.Wait() 75 | wg.Add(1) 76 | 77 | a.Shutdown() 78 | wg.Wait() 79 | } 80 | 81 | func TestAdvertiser_Advertise_multipleTimes(t *testing.T) { 82 | ctrl, local, teardown := setup(t) 83 | defer teardown(t) 84 | 85 | Timeout = 20 * time.Millisecond 86 | 87 | d := mock.NewMockDiscoverer(ctrl) 88 | wrapdiscovery = d 89 | 90 | a := NewAdvertiser(local) 91 | 92 | var wg sync.WaitGroup 93 | wg.Add(5) 94 | 95 | d.EXPECT(). 96 | NewMdnsService(gomock.Any(), a, Interval, gomock.Any()). 97 | DoAndReturn(func(ctx context.Context, peerhost host.Host, interval time.Duration, serviceTag string) (discovery.Service, error) { 98 | wg.Done() 99 | return DummyMDNSService{}, nil 100 | }). 101 | Times(5) 102 | 103 | go func() { 104 | err := a.Advertise(333) 105 | assert.NoError(t, err) 106 | wg.Done() 107 | }() 108 | wg.Wait() 109 | wg.Add(1) 110 | 111 | a.Shutdown() 112 | wg.Wait() 113 | } 114 | 115 | type DummyMDNSService struct{} 116 | 117 | func (mdns DummyMDNSService) Close() error { 118 | return nil 119 | } 120 | func (mdns DummyMDNSService) RegisterNotifee(notifee discovery.Notifee) {} 121 | func (mdns DummyMDNSService) UnregisterNotifee(notifee discovery.Notifee) {} 122 | -------------------------------------------------------------------------------- /pkg/mdns/discoverer.go: -------------------------------------------------------------------------------- 1 | package mdns 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/libp2p/go-libp2p-core/host" 8 | "github.com/libp2p/go-libp2p-core/peer" 9 | ma "github.com/multiformats/go-multiaddr" 10 | manet "github.com/multiformats/go-multiaddr/net" 11 | "github.com/pkg/errors" 12 | "github.com/whyrusleeping/mdns" 13 | 14 | "github.com/dennis-tra/pcp/internal/log" 15 | ) 16 | 17 | type Discoverer struct { 18 | *protocol 19 | } 20 | 21 | func NewDiscoverer(h host.Host) *Discoverer { 22 | return &Discoverer{newProtocol(h)} 23 | } 24 | 25 | func (d *Discoverer) Discover(chanID int, handler func(info peer.AddrInfo)) error { 26 | if err := d.ServiceStarted(); err != nil { 27 | return err 28 | } 29 | defer d.ServiceStopped() 30 | 31 | for { 32 | entriesCh := make(chan *mdns.ServiceEntry, 16) 33 | go d.drainEntriesChan(entriesCh, handler) 34 | 35 | did := d.DiscoveryID(chanID) 36 | log.Debugln("mDNS - Discovering", did) 37 | qp := &mdns.QueryParam{ 38 | Domain: "local", 39 | Entries: entriesCh, 40 | Service: did, 41 | Timeout: time.Second * 5, 42 | } 43 | 44 | err := mdns.Query(qp) 45 | log.Debugln("mDNS - Discovering", did, " done.") 46 | if err != nil { 47 | log.Warningln("mDNS - query error", err) 48 | } 49 | close(entriesCh) 50 | 51 | select { 52 | case <-d.SigShutdown(): 53 | return nil 54 | default: 55 | } 56 | } 57 | } 58 | 59 | func (d *Discoverer) Shutdown() { 60 | d.Service.Shutdown() 61 | } 62 | 63 | func (d *Discoverer) drainEntriesChan(entries chan *mdns.ServiceEntry, handler func(info peer.AddrInfo)) { 64 | for entry := range entries { 65 | 66 | pi, err := parseServiceEntry(entry) 67 | if err != nil { 68 | continue 69 | } 70 | 71 | log.Debugln("mDNS - Found peer", pi.ID) 72 | 73 | if pi.ID == d.ID() { 74 | continue 75 | } 76 | 77 | pi.Addrs = onlyPrivate(pi.Addrs) 78 | if !isRoutable(pi) { 79 | continue 80 | } 81 | 82 | go handler(pi) 83 | } 84 | } 85 | 86 | func parseServiceEntry(entry *mdns.ServiceEntry) (peer.AddrInfo, error) { 87 | p, err := peer.Decode(entry.Info) 88 | if err != nil { 89 | return peer.AddrInfo{}, errors.Wrap(err, "error parsing peer ID from mdns entry") 90 | } 91 | 92 | var addr net.IP 93 | if entry.AddrV4 != nil { 94 | addr = entry.AddrV4 95 | } else if entry.AddrV6 != nil { 96 | addr = entry.AddrV6 97 | } else { 98 | return peer.AddrInfo{}, errors.Wrap(err, "error parsing multiaddr from mdns entry: no IP address found") 99 | } 100 | 101 | maddr, err := manet.FromNetAddr(&net.TCPAddr{IP: addr, Port: entry.Port}) 102 | if err != nil { 103 | return peer.AddrInfo{}, errors.Wrap(err, "error parsing multiaddr from mdns entry") 104 | } 105 | 106 | return peer.AddrInfo{ 107 | ID: p, 108 | Addrs: []ma.Multiaddr{maddr}, 109 | }, nil 110 | } 111 | 112 | func isRoutable(pi peer.AddrInfo) bool { 113 | return len(pi.Addrs) > 0 114 | } 115 | 116 | // Filter out addresses that are public - only allow private ones. 117 | func onlyPrivate(addrs []ma.Multiaddr) []ma.Multiaddr { 118 | routable := []ma.Multiaddr{} 119 | for _, addr := range addrs { 120 | if manet.IsPrivateAddr(addr) { 121 | routable = append(routable, addr) 122 | log.Debugf("\tprivate - %s\n", addr.String()) 123 | } else { 124 | log.Debugf("\tpublic - %s\n", addr.String()) 125 | } 126 | } 127 | return routable 128 | } 129 | -------------------------------------------------------------------------------- /pkg/mdns/protocol.go: -------------------------------------------------------------------------------- 1 | package mdns 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/libp2p/go-libp2p-core/host" 8 | 9 | "github.com/dennis-tra/pcp/internal/wrap" 10 | "github.com/dennis-tra/pcp/pkg/service" 11 | ) 12 | 13 | // These wrapped top level functions are here for testing purposes. 14 | var ( 15 | wraptime wrap.Timer = wrap.Time{} 16 | wrapdiscovery wrap.Discoverer = wrap.Discovery{} 17 | ) 18 | 19 | var ( 20 | // Interval is the frequency with which the pcp 21 | // service is advertised in the local network 22 | Interval = time.Second 23 | 24 | // Timeout is the time until a new advertisement 25 | // with a potentially new discovery ID is started. 26 | Timeout = time.Minute 27 | 28 | // TruncateDuration represents the time slot to which 29 | // the current time is truncated. 30 | TruncateDuration = 5 * time.Minute 31 | ) 32 | 33 | // protocol encapsulates the logic for discovering peers 34 | // via multicast DNS in the local network. 35 | type protocol struct { 36 | host.Host 37 | *service.Service 38 | interval time.Duration 39 | 40 | offset time.Duration 41 | } 42 | 43 | func newProtocol(h host.Host) *protocol { 44 | return &protocol{ 45 | Host: h, 46 | interval: Interval, 47 | Service: service.New("mDNS"), 48 | } 49 | } 50 | 51 | // TimeSlotStart returns the time when the current time slot started.f 52 | func (p *protocol) TimeSlotStart() time.Time { 53 | return p.refTime().Truncate(TruncateDuration) 54 | } 55 | 56 | // refTime returns the reference time to calculate the time slot from. 57 | func (p *protocol) refTime() time.Time { 58 | return wraptime.Now().Add(p.offset) 59 | } 60 | 61 | func (d *Discoverer) SetOffset(offset time.Duration) *Discoverer { 62 | d.offset = offset 63 | return d 64 | } 65 | 66 | // DiscoveryID returns the string, that we use to advertise 67 | // via mDNS and the DHT. See chanID above for more information. 68 | // Using UnixNano for testing. 69 | func (p *protocol) DiscoveryID(chanID int) string { 70 | return fmt.Sprintf("/pcp/%d/%d", p.TimeSlotStart().UnixNano(), chanID) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "sync" 10 | "time" 11 | 12 | "github.com/golang/protobuf/proto" 13 | "github.com/google/uuid" 14 | "github.com/libp2p/go-libp2p" 15 | "github.com/libp2p/go-libp2p-core/crypto" 16 | "github.com/libp2p/go-libp2p-core/host" 17 | "github.com/libp2p/go-libp2p-core/network" 18 | "github.com/libp2p/go-libp2p-core/peer" 19 | "github.com/libp2p/go-libp2p-core/routing" 20 | kaddht "github.com/libp2p/go-libp2p-kad-dht" 21 | "github.com/multiformats/go-varint" 22 | "github.com/pkg/errors" 23 | "github.com/urfave/cli/v2" 24 | 25 | "github.com/dennis-tra/pcp/internal/log" 26 | "github.com/dennis-tra/pcp/pkg/crypt" 27 | p2p "github.com/dennis-tra/pcp/pkg/pb" 28 | "github.com/dennis-tra/pcp/pkg/service" 29 | "github.com/dennis-tra/pcp/pkg/words" 30 | ) 31 | 32 | // Is set to true during test runs because the 33 | // generated peers won't have proper keys 34 | var skipMessageAuth = false 35 | 36 | type State string 37 | 38 | const ( 39 | Idle State = "idle" 40 | Discovering = "discovering" 41 | Advertising = "advertising" 42 | Connected = "connected" 43 | ) 44 | 45 | // Node encapsulates the logic for sending and receiving messages. 46 | type Node struct { 47 | host.Host 48 | *PushProtocol 49 | *TransferProtocol 50 | *PakeProtocol 51 | *service.Service 52 | 53 | // The public key of this node for easy access 54 | pubKey []byte 55 | 56 | // DHT is an accessor that is needed in the DHT discoverer/advertiser. 57 | DHT *kaddht.IpfsDHT 58 | 59 | ChanID int 60 | Words []string 61 | 62 | stateLk *sync.RWMutex 63 | state State 64 | } 65 | 66 | // New creates a new, fully initialized node with the given options. 67 | func New(c *cli.Context, wrds []string, opts ...libp2p.Option) (*Node, error) { 68 | log.Debugln("Initialising local node...") 69 | 70 | if c.Bool("homebrew") { 71 | wrds = words.HomebrewList() 72 | } 73 | ints, err := words.ToInts(wrds) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | node := &Node{ 79 | Service: service.New("node"), 80 | state: Idle, 81 | stateLk: &sync.RWMutex{}, 82 | Words: wrds, 83 | ChanID: ints[0], 84 | } 85 | node.PushProtocol = NewPushProtocol(node) 86 | node.TransferProtocol = NewTransferProtocol(node) 87 | node.PakeProtocol, err = NewPakeProtocol(node, wrds) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | key, pub, err := crypto.GenerateKeyPair(crypto.Secp256k1, 256) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | node.pubKey, err = pub.Raw() 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | opts = append(opts, 103 | libp2p.Identity(key), 104 | libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) { 105 | node.DHT, err = kaddht.New(c.Context, h) 106 | return node.DHT, err 107 | }), 108 | ) 109 | 110 | node.Host, err = libp2p.New(c.Context, opts...) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | return node, node.ServiceStarted() 116 | } 117 | 118 | func (n *Node) Shutdown() { 119 | if err := n.Host.Close(); err != nil { 120 | log.Warningln("error closing node", err) 121 | } 122 | 123 | n.ServiceStopped() 124 | } 125 | 126 | func (n *Node) SetState(s State) State { 127 | log.Debugln("Setting local node state to", s) 128 | n.stateLk.Lock() 129 | defer n.stateLk.Unlock() 130 | n.state = s 131 | return n.state 132 | } 133 | 134 | func (n *Node) GetState() State { 135 | n.stateLk.RLock() 136 | defer n.stateLk.RUnlock() 137 | return n.state 138 | } 139 | 140 | // Send prepares the message msg to be sent over the network stream s. 141 | // Send closes the stream for writing but leaves it open for reading. 142 | func (n *Node) Send(s network.Stream, msg p2p.HeaderMessage) error { 143 | defer func() { 144 | if err := s.CloseWrite(); err != nil { 145 | log.Warningln("Error closing writer part of stream after sending", err) 146 | } 147 | }() 148 | 149 | // Get own public key. 150 | pub, err := n.Host.Peerstore().PubKey(n.Host.ID()).Bytes() 151 | if err != nil { 152 | return err 153 | } 154 | 155 | hdr := &p2p.Header{ 156 | RequestId: uuid.New().String(), 157 | NodeId: peer.Encode(n.Host.ID()), 158 | NodePubKey: pub, 159 | Timestamp: time.Now().Unix(), 160 | } 161 | msg.SetHeader(hdr) 162 | log.Debugf("Sending message %T to %s with request ID %s\n", msg, s.Conn().RemotePeer().String(), hdr.RequestId) 163 | 164 | // Transform msg to binary to calculate the signature. 165 | data, err := proto.Marshal(msg) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | // Sign the data and attach the signature. 171 | key := n.Host.Peerstore().PrivKey(n.Host.ID()) 172 | signature, err := key.Sign(data) 173 | if err != nil { 174 | return err 175 | } 176 | hdr.Signature = signature 177 | msg.SetHeader(hdr) // Maybe unnecessary 178 | 179 | // Transform msg + signature to binary. 180 | data, err = proto.Marshal(msg) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | // Encrypt the data with the PAKE session key if it is found 186 | sKey, found := n.GetSessionKey(s.Conn().RemotePeer()) 187 | if found { 188 | data, err = crypt.Encrypt(sKey, data) 189 | if err != nil { 190 | return err 191 | } 192 | } 193 | 194 | // Transmit the data. 195 | _, err = s.Write(data) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | return nil 201 | } 202 | 203 | // authenticateMessage verifies the authenticity of the message payload. 204 | // It takes the given signature and verifies it against the given public 205 | // key. 206 | func (n *Node) authenticateMessage(msg p2p.HeaderMessage) (bool, error) { 207 | // This will be set to true during unit test runs as the 208 | // generated peers from mocknet won't have proper keys. 209 | if skipMessageAuth { 210 | return true, nil 211 | } 212 | 213 | // store a temp ref to signature and remove it from message msg 214 | // sign is a string to allow easy reset to zero-value (empty string) 215 | signature := msg.GetHeader().Signature 216 | msg.GetHeader().Signature = nil 217 | 218 | // marshall msg without the signature to protobufs3 binary format 219 | bin, err := proto.Marshal(msg) 220 | if err != nil { 221 | return false, err 222 | } 223 | 224 | // restore sig in message msg (for possible future use) 225 | msg.GetHeader().Signature = signature 226 | 227 | // restore peer id binary format from base58 encoded node id msg 228 | peerID, err := peer.Decode(msg.GetHeader().NodeId) 229 | if err != nil { 230 | return false, err 231 | } 232 | 233 | key, err := crypto.UnmarshalPublicKey(msg.GetHeader().NodePubKey) 234 | if err != nil { 235 | return false, fmt.Errorf("failed to extract key from message key msg") 236 | } 237 | 238 | // extract node id from the provided public key 239 | idFromKey, err := peer.IDFromPublicKey(key) 240 | if err != nil { 241 | return false, fmt.Errorf("failed to extract peer id from public key") 242 | } 243 | 244 | // verify that message author node id matches the provided node public key 245 | if idFromKey != peerID { 246 | return false, fmt.Errorf("node id and provided public key mismatch") 247 | } 248 | 249 | return key.Verify(bin, signature) 250 | } 251 | 252 | // Read drains the given stream and parses the content. It unmarshalls 253 | // it into the protobuf object. It also verifies the authenticity of the message. 254 | // Read closes the stream for reading but leaves it open for writing. 255 | func (n *Node) Read(s network.Stream, buf p2p.HeaderMessage) error { 256 | defer s.CloseRead() 257 | 258 | data, err := ioutil.ReadAll(s) 259 | if err != nil { 260 | if err2 := s.Reset(); err2 != nil { 261 | err = errors.Wrap(err, err2.Error()) 262 | } 263 | return err 264 | } 265 | 266 | log.Debugf("Reading message from %s\n", s.Conn().RemotePeer().String()) 267 | // Decrypt the data with the PAKE session key if it is found 268 | sKey, found := n.GetSessionKey(s.Conn().RemotePeer()) 269 | if found { 270 | data, err = crypt.Decrypt(sKey, data) 271 | if err != nil { 272 | return err 273 | } 274 | } 275 | 276 | if err = proto.Unmarshal(data, buf); err != nil { 277 | return err 278 | } 279 | log.Debugf("type %T with request ID %s\n", buf, buf.GetHeader().RequestId) 280 | 281 | valid, err := n.authenticateMessage(buf) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | if !valid { 287 | return fmt.Errorf("failed to authenticate message") 288 | } 289 | 290 | return nil 291 | } 292 | 293 | // WriteBytes writes the given bytes to the destination writer and 294 | // prefixes it with a uvarint indicating the length of the data. 295 | func (n *Node) WriteBytes(w io.Writer, data []byte) (int, error) { 296 | size := varint.ToUvarint(uint64(len(data))) 297 | return w.Write(append(size, data...)) 298 | } 299 | 300 | // ReadBytes reads an uvarint from the source reader to know how 301 | // much data is following. 302 | func (n *Node) ReadBytes(r io.Reader) ([]byte, error) { 303 | br := bufio.NewReader(r) // init byte reader 304 | l, err := varint.ReadUvarint(br) 305 | if err != nil { 306 | return nil, err 307 | } 308 | 309 | buf := make([]byte, l) 310 | _, err = br.Read(buf) 311 | return buf, err 312 | } 313 | 314 | // ResetOnShutdown resets the given stream if the node receives a shutdown 315 | // signal to indicate to our peer that we're not interested in the conversation 316 | // anymore. 317 | func (n *Node) ResetOnShutdown(s network.Stream) context.CancelFunc { 318 | cancel := make(chan struct{}) 319 | go func() { 320 | select { 321 | case <-n.SigShutdown(): 322 | s.Reset() 323 | case <-cancel: 324 | } 325 | }() 326 | return func() { close(cancel) } 327 | } 328 | 329 | // WaitForEOF waits for an EOF signal on the stream. This indicates that the peer 330 | // has received all data and won't read from this stream anymore. Alternatively 331 | // there is a 10 second timeout. 332 | func (n *Node) WaitForEOF(s network.Stream) error { 333 | log.Debugln("Waiting for stream reset from peer...") 334 | timeout := time.After(3 * time.Minute) 335 | done := make(chan error) 336 | go func() { 337 | buf := make([]byte, 1) 338 | n, err := s.Read(buf) 339 | if err == io.EOF && n == 0 { 340 | err = nil 341 | } else if n != 0 { 342 | err = fmt.Errorf("stream returned data unexpectedly") 343 | } 344 | done <- err 345 | close(done) 346 | }() 347 | select { 348 | case <-timeout: 349 | return fmt.Errorf("timeout") 350 | case err := <-done: 351 | return err 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /pkg/node/pake.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/elliptic" 7 | "fmt" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/libp2p/go-libp2p-core/network" 12 | "github.com/libp2p/go-libp2p-core/peer" 13 | "github.com/schollz/pake/v2" 14 | 15 | "github.com/dennis-tra/pcp/internal/log" 16 | "github.com/dennis-tra/pcp/pkg/crypt" 17 | ) 18 | 19 | // pattern: /protocol-name/request-or-response-message/version 20 | const ProtocolPake = "/pcp/pake/0.2.0" 21 | 22 | type PakeProtocol struct { 23 | node *Node 24 | 25 | // pwKey holds a scrypt derived key based on the users 26 | // given words that has the correct length to create 27 | // a new block cipher. It uses the key derivation 28 | // function (KDF) of scrypt. This is not the key that 29 | // is used to ultimately encrypt the communication. 30 | // This is the input key for PAKE. 31 | pwKey []byte 32 | 33 | // A map of peers that have successfully passed PAKE. 34 | // Peer.ID -> Session Key 35 | authedPeers sync.Map 36 | 37 | // Holds a key exchange handler that is called after 38 | // a successful key exchange. 39 | lk sync.RWMutex 40 | keh KeyExchangeHandler 41 | } 42 | 43 | func NewPakeProtocol(node *Node, words []string) (*PakeProtocol, error) { 44 | pw := []byte(strings.Join(words, "")) 45 | key, err := crypt.DeriveKey(pw, node.pubKey) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return &PakeProtocol{ 51 | node: node, 52 | pwKey: key, 53 | authedPeers: sync.Map{}, 54 | lk: sync.RWMutex{}, 55 | }, nil 56 | } 57 | 58 | // AddAuthenticatedPeer adds a peer ID and the session key that was 59 | // obtained via the password authenticated peer exchange (PAKE) to 60 | // a local peer store. 61 | func (p *PakeProtocol) AddAuthenticatedPeer(peerID peer.ID, key []byte) { 62 | log.Debugf("Adding authenticated peer %s to known peers\n", peerID) 63 | p.authedPeers.Store(peerID, key) 64 | } 65 | 66 | // IsAuthenticated checks if the given peer ID has successfully 67 | // passed a password authenticated key exchange. 68 | func (p *PakeProtocol) IsAuthenticated(peerID peer.ID) bool { 69 | _, found := p.authedPeers.Load(peerID) 70 | log.Debugf("Is peer %s authenticated: %v\n", peerID, found) 71 | return found 72 | } 73 | 74 | // GetSessionKey returns the session key that was obtain via 75 | // the password authenticated key exchange (PAKE) protocol. 76 | func (p *PakeProtocol) GetSessionKey(peerID peer.ID) ([]byte, bool) { 77 | key, found := p.authedPeers.Load(peerID) 78 | if !found { 79 | return nil, false 80 | } 81 | sKey, ok := key.([]byte) 82 | if !ok { 83 | p.authedPeers.Delete(peerID) 84 | return nil, false 85 | } 86 | return sKey, true 87 | } 88 | 89 | type KeyExchangeHandler interface { 90 | HandleSuccessfulKeyExchange(peerID peer.ID) 91 | } 92 | 93 | func (p *PakeProtocol) RegisterKeyExchangeHandler(keh KeyExchangeHandler) { 94 | log.Debugln("Registering key exchange handler") 95 | p.lk.Lock() 96 | defer p.lk.Unlock() 97 | p.keh = keh 98 | p.node.SetStreamHandler(ProtocolPake, p.onKeyExchange) 99 | } 100 | 101 | func (p *PakeProtocol) UnregisterKeyExchangeHandler() { 102 | log.Debugln("Unregistering key exchange handler") 103 | p.lk.Lock() 104 | defer p.lk.Unlock() 105 | p.node.RemoveStreamHandler(ProtocolPake) 106 | p.keh = nil 107 | } 108 | 109 | func (p *PakeProtocol) onKeyExchange(s network.Stream) { 110 | defer s.Close() 111 | defer p.node.ResetOnShutdown(s)() 112 | 113 | log.Infor("Authenticating peer...") 114 | 115 | // pick an elliptic curve 116 | curve := elliptic.P521() 117 | 118 | // initialize recipient Q ("1" indicates recipient) 119 | Q, err := pake.Init(p.pwKey, 1, curve) 120 | if err != nil { 121 | log.Warningln(err) 122 | return 123 | } 124 | 125 | log.Infor("Waiting for key information...") 126 | // Read init data from P 127 | dat, err := p.node.ReadBytes(s) 128 | if err != nil { 129 | log.Warningln(err) 130 | return 131 | } 132 | 133 | log.Infor("Calculating on key information...") 134 | // Use init data from P 135 | if err = Q.Update(dat); err != nil { 136 | log.Warningln(err) 137 | return 138 | } 139 | 140 | log.Infor("Sending key information...") 141 | // Send P calculated Data 142 | if _, err = p.node.WriteBytes(s, Q.Bytes()); err != nil { 143 | log.Warningln(err) 144 | return 145 | } 146 | 147 | log.Infor("Waiting for final key information...") 148 | // Read calculated data from P 149 | dat, err = p.node.ReadBytes(s) 150 | if err != nil { 151 | log.Warningln(err) 152 | return 153 | } 154 | 155 | log.Infor("Calculating on key information...") 156 | // Use calculated data from P 157 | if err = Q.Update(dat); err != nil { 158 | log.Warningln(err) 159 | return 160 | } 161 | 162 | // Access session key 163 | key, err := Q.SessionKey() 164 | if err != nil { 165 | log.Warningln(err) 166 | return 167 | } 168 | 169 | log.Infor("Proofing authenticity to peer...") 170 | // Send P encryption proof 171 | if err := p.SendProof(s, key); err != nil { 172 | log.Warningln(err) 173 | return 174 | } 175 | 176 | log.Infor("Verifying proof from peer...") 177 | // Read and verify encryption proof from P 178 | if err := p.ReceiveVerifyProof(s, key); err != nil { 179 | log.Warningln(err) 180 | return 181 | } 182 | 183 | p.AddAuthenticatedPeer(s.Conn().RemotePeer(), key) 184 | 185 | // We're done reading data from P 186 | if err = s.CloseRead(); err != nil { 187 | log.Warningln("error closing pake write", err) 188 | } 189 | 190 | // Tell P the proof was verified and is okay 191 | if _, err = p.node.WriteBytes(s, []byte("ok")); err != nil { 192 | log.Warningln(err) 193 | return 194 | } 195 | 196 | // Wait for P to close the stream, so we know confirmation was received. 197 | if err = p.node.WaitForEOF(s); err != nil { 198 | log.Warningln("error waiting for EOF", err) 199 | } 200 | 201 | // We're done sending data over the stream. 202 | if err = s.Close(); err != nil { 203 | log.Warningln("error closing pake stream", err) 204 | } 205 | 206 | log.Infor("Peer connected and authenticated!\n") 207 | 208 | p.lk.RLock() 209 | defer p.lk.RUnlock() 210 | if p.keh == nil { 211 | return 212 | } 213 | 214 | go p.keh.HandleSuccessfulKeyExchange(s.Conn().RemotePeer()) 215 | } 216 | 217 | func (p *PakeProtocol) StartKeyExchange(ctx context.Context, peerID peer.ID) ([]byte, error) { 218 | s, err := p.node.NewStream(ctx, peerID, ProtocolPake) 219 | if err != nil { 220 | return nil, err 221 | } 222 | defer s.Close() 223 | 224 | log.Infor("Authenticating peer...") 225 | 226 | // pick an elliptic curve 227 | curve := elliptic.P521() 228 | 229 | // initialize sender p ("0" indicates sender) 230 | P, err := pake.Init(p.pwKey, 0, curve) 231 | if err != nil { 232 | return nil, err 233 | } 234 | 235 | log.Infor("Sending key information...") 236 | // Send Q init data 237 | if _, err = p.node.WriteBytes(s, P.Bytes()); err != nil { 238 | return nil, err 239 | } 240 | 241 | log.Infor("Waiting for key information...") 242 | // Read calculated data from Q 243 | dat, err := p.node.ReadBytes(s) 244 | if err != nil { 245 | return nil, err 246 | } 247 | 248 | log.Infor("Calculating on key information...") 249 | // Use calculated data from Q 250 | if err = P.Update(dat); err != nil { 251 | return nil, err 252 | } 253 | 254 | log.Infor("Sending key information...") 255 | // Send Q calculated data 256 | if _, err = p.node.WriteBytes(s, P.Bytes()); err != nil { 257 | return nil, err 258 | } 259 | 260 | // Extract calculated key 261 | key, err := P.SessionKey() 262 | if err != nil { 263 | return nil, err 264 | } 265 | 266 | log.Infor("Verifying proof from peer...") 267 | // Read and verify encryption proof from Q 268 | if err = p.ReceiveVerifyProof(s, key); err != nil { 269 | return nil, err 270 | } 271 | 272 | p.AddAuthenticatedPeer(s.Conn().RemotePeer(), key) 273 | 274 | log.Infor("Proofing authenticity to peer...") 275 | // Send Q encryption proof 276 | if err = p.SendProof(s, key); err != nil { 277 | return nil, err 278 | } 279 | 280 | // We're done sending data to Q 281 | if err = s.CloseWrite(); err != nil { 282 | log.Warningln("error closing pake write", err) 283 | } 284 | 285 | log.Infor("Waiting for confirmation from peer...") 286 | // Read confirmation from P 287 | confirm, err := p.node.ReadBytes(s) 288 | if err != nil { 289 | return nil, err 290 | } 291 | 292 | if string(confirm) != "ok" { 293 | return nil, fmt.Errorf("peer did not respond with ok") 294 | } 295 | 296 | log.Infor("Peer connected and authenticated!\n") 297 | return key, nil 298 | } 299 | 300 | // SendProof takes the public key of our node and encrypts it with 301 | // the PAKE-derived session key. The recipient can decrypt the key 302 | // and verify that it matches. 303 | func (p *PakeProtocol) SendProof(s network.Stream, key []byte) error { 304 | challenge, err := crypt.Encrypt(key, p.node.pubKey) 305 | if err != nil { 306 | return err 307 | } 308 | 309 | if _, err := p.node.WriteBytes(s, challenge); err != nil { 310 | return err 311 | } 312 | 313 | return nil 314 | } 315 | 316 | // ReceiveVerifyProof reads proof data from the stream, decrypts it with the 317 | // given key, that was derived via PAKE, and checks if it matches the remote 318 | // public key. 319 | func (p *PakeProtocol) ReceiveVerifyProof(s network.Stream, key []byte) error { 320 | response, err := p.node.ReadBytes(s) 321 | if err != nil { 322 | return err 323 | } 324 | 325 | dec, err := crypt.Decrypt(key, response) 326 | if err != nil { 327 | return err 328 | } 329 | 330 | peerPubKey, err := p.node.Peerstore().PubKey(s.Conn().RemotePeer()).Raw() 331 | if err != nil { 332 | return err 333 | } 334 | 335 | if !bytes.Equal(dec, peerPubKey) { 336 | return fmt.Errorf("proof verification failed") 337 | } 338 | 339 | return nil 340 | } 341 | -------------------------------------------------------------------------------- /pkg/node/push.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/libp2p/go-libp2p-core/network" 8 | "github.com/libp2p/go-libp2p-core/peer" 9 | 10 | "github.com/dennis-tra/pcp/internal/log" 11 | p2p "github.com/dennis-tra/pcp/pkg/pb" 12 | ) 13 | 14 | // pattern: /protocol-name/request-or-response-message/version 15 | const ProtocolPushRequest = "/pcp/push/0.0.1" 16 | 17 | // PushProtocol . 18 | type PushProtocol struct { 19 | node *Node 20 | lk sync.RWMutex 21 | prh PushRequestHandler 22 | } 23 | 24 | type PushRequestHandler interface { 25 | HandlePushRequest(*p2p.PushRequest) (bool, error) 26 | } 27 | 28 | func NewPushProtocol(node *Node) *PushProtocol { 29 | return &PushProtocol{node: node, lk: sync.RWMutex{}} 30 | } 31 | 32 | func (p *PushProtocol) RegisterPushRequestHandler(prh PushRequestHandler) { 33 | log.Debugln("Registering push request handler") 34 | p.lk.Lock() 35 | defer p.lk.Unlock() 36 | p.prh = prh 37 | p.node.SetStreamHandler(ProtocolPushRequest, p.onPushRequest) 38 | } 39 | 40 | func (p *PushProtocol) UnregisterPushRequestHandler() { 41 | log.Debugln("Unregistering push request handler") 42 | p.lk.Lock() 43 | defer p.lk.Unlock() 44 | p.node.RemoveStreamHandler(ProtocolPushRequest) 45 | p.prh = nil 46 | } 47 | 48 | func (p *PushProtocol) onPushRequest(s network.Stream) { 49 | defer s.Close() 50 | defer p.node.ResetOnShutdown(s)() 51 | 52 | if !p.node.IsAuthenticated(s.Conn().RemotePeer()) { 53 | log.Infoln("Received push request from unauthenticated peer") 54 | s.Reset() // Tell peer to go away 55 | return 56 | } 57 | 58 | req := &p2p.PushRequest{} 59 | if err := p.node.Read(s, req); err != nil { 60 | log.Infoln(err) 61 | return 62 | } 63 | log.Debugln("Received push request", req.Name, req.Size) 64 | 65 | p.lk.RLock() 66 | defer p.lk.RUnlock() 67 | accept, err := p.prh.HandlePushRequest(req) 68 | if err != nil { 69 | log.Infoln(err) 70 | accept = false 71 | // Fall through and tell peer we won't handle the request 72 | } 73 | 74 | if err := p.node.Send(s, p2p.NewPushResponse(accept)); err != nil { 75 | log.Infoln(err) 76 | return 77 | } 78 | 79 | if err = p.node.WaitForEOF(s); err != nil { 80 | log.Infoln(err) 81 | return 82 | } 83 | } 84 | 85 | func (p *PushProtocol) SendPushRequest(ctx context.Context, peerID peer.ID, filename string, size int64, isDir bool) (bool, error) { 86 | s, err := p.node.NewStream(ctx, peerID, ProtocolPushRequest) 87 | if err != nil { 88 | return false, err 89 | } 90 | defer s.Close() 91 | 92 | log.Debugln("Sending push request", filename, size) 93 | if err = p.node.Send(s, p2p.NewPushRequest(filename, size, isDir)); err != nil { 94 | return false, err 95 | } 96 | 97 | resp := &p2p.PushResponse{} 98 | if err = p.node.Read(s, resp); err != nil { 99 | return false, err 100 | } 101 | 102 | return resp.Accept, nil 103 | } 104 | -------------------------------------------------------------------------------- /pkg/node/push_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | p2p "github.com/dennis-tra/pcp/pkg/pb" 12 | ) 13 | 14 | // TestTransferHandler is a mock transfer handler that can be registered for the TransferProtocol. 15 | type TestPushRequestHandler struct { 16 | handler func(*p2p.PushRequest) (bool, error) 17 | } 18 | 19 | func (prh *TestPushRequestHandler) HandlePushRequest(pr *p2p.PushRequest) (bool, error) { 20 | return prh.handler(pr) 21 | } 22 | 23 | func TestPushProtocol_RegisterPushRequestHandler_happyPath(t *testing.T) { 24 | skipMessageAuth = true 25 | 26 | ctx := context.Background() 27 | net := mocknet.New(ctx) 28 | 29 | node1, _ := setupNode(t, net) 30 | node2, _ := setupNode(t, net) 31 | authNodes(t, node1, node2) 32 | 33 | err := net.LinkAll() 34 | require.NoError(t, err) 35 | 36 | tprh := &TestPushRequestHandler{ 37 | handler: func(pr *p2p.PushRequest) (bool, error) { 38 | assert.NotEmpty(t, pr.Header.RequestId) 39 | assert.NotEmpty(t, pr.Header.NodePubKey) 40 | assert.NotEmpty(t, pr.Header.Signature) 41 | assert.NotEmpty(t, pr.Header.Timestamp) 42 | assert.Equal(t, "filename", pr.Name) 43 | assert.EqualValues(t, 1000, pr.Size) 44 | assert.True(t, pr.IsDir) 45 | return true, nil 46 | }, 47 | } 48 | 49 | node2.RegisterPushRequestHandler(tprh) 50 | 51 | accepted, err := node1.SendPushRequest(ctx, node2.ID(), "filename", 1000, true) 52 | require.NoError(t, err) 53 | 54 | node2.UnregisterPushRequestHandler() 55 | 56 | assert.True(t, accepted) 57 | } 58 | 59 | func TestPushProtocol_RegisterPushRequestHandler_unauthenticated(t *testing.T) { 60 | skipMessageAuth = true 61 | 62 | ctx := context.Background() 63 | net := mocknet.New(ctx) 64 | 65 | node1, _ := setupNode(t, net) 66 | node2, _ := setupNode(t, net) 67 | 68 | err := net.LinkAll() 69 | require.NoError(t, err) 70 | 71 | tprh := &TestPushRequestHandler{ 72 | handler: func(pr *p2p.PushRequest) (bool, error) { return true, nil }, 73 | } 74 | 75 | node2.RegisterPushRequestHandler(tprh) 76 | 77 | accept, err := node1.SendPushRequest(ctx, node2.ID(), "filename", 1000, true) 78 | require.Error(t, err) 79 | assert.Contains(t, err.Error(), "stream reset") 80 | assert.False(t, accept) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/node/transfer.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "archive/tar" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | 12 | "github.com/pkg/errors" 13 | 14 | "github.com/libp2p/go-libp2p-core/network" 15 | "github.com/libp2p/go-libp2p-core/peer" 16 | progress "github.com/schollz/progressbar/v3" 17 | 18 | "github.com/dennis-tra/pcp/internal/log" 19 | "github.com/dennis-tra/pcp/pkg/crypt" 20 | ) 21 | 22 | // pattern: /protocol-name/request-or-response-message/version 23 | const ( 24 | ProtocolTransfer = "/pcp/transfer/0.2.0" 25 | ) 26 | 27 | // TransferProtocol encapsulates data necessary to fulfill its protocol. 28 | type TransferProtocol struct { 29 | node *Node 30 | lk sync.RWMutex 31 | th TransferHandler 32 | } 33 | 34 | type TransferHandler interface { 35 | HandleFile(*tar.Header, io.Reader) 36 | Done() 37 | } 38 | 39 | func (t *TransferProtocol) RegisterTransferHandler(th TransferHandler) { 40 | log.Debugln("Registering transfer handler") 41 | t.lk.Lock() 42 | defer t.lk.Unlock() 43 | t.th = th 44 | t.node.SetStreamHandler(ProtocolTransfer, t.onTransfer) 45 | } 46 | 47 | func (t *TransferProtocol) UnregisterTransferHandler() { 48 | log.Debugln("Unregistering transfer handler") 49 | t.lk.Lock() 50 | defer t.lk.Unlock() 51 | t.node.RemoveStreamHandler(ProtocolTransfer) 52 | t.th = nil 53 | } 54 | 55 | // New TransferProtocol initializes a new TransferProtocol object with all 56 | // fields set to their default values. 57 | func NewTransferProtocol(node *Node) *TransferProtocol { 58 | return &TransferProtocol{node: node, lk: sync.RWMutex{}} 59 | } 60 | 61 | // onTransfer is called when the peer initiates a file transfer. 62 | func (t *TransferProtocol) onTransfer(s network.Stream) { 63 | defer t.th.Done() 64 | defer t.node.ResetOnShutdown(s)() 65 | 66 | // Get PAKE session key for stream decryption 67 | sKey, found := t.node.GetSessionKey(s.Conn().RemotePeer()) 68 | if !found { 69 | log.Warningln("Received transfer from unauthenticated peer:", s.Conn().RemotePeer()) 70 | s.Reset() // Tell peer to go away 71 | return 72 | } 73 | 74 | // Read initialization vector from stream. This is sent first from our peer. 75 | iv, err := t.node.ReadBytes(s) 76 | if err != nil { 77 | log.Warningln("Could not read stream initialization vector", err) 78 | s.Reset() // Stream is probably broken anyways 79 | return 80 | } 81 | 82 | t.lk.RLock() 83 | defer func() { 84 | if err = s.Close(); err != nil { 85 | log.Warningln(err) 86 | } 87 | t.lk.RUnlock() 88 | }() 89 | 90 | // Decrypt the stream 91 | sd, err := crypt.NewStreamDecrypter(sKey, iv, s) 92 | if err != nil { 93 | log.Warningln("Could not instantiate stream decrypter", err) 94 | return 95 | } 96 | 97 | // Drain tar archive 98 | tr := tar.NewReader(sd) 99 | for { 100 | hdr, err := tr.Next() 101 | if err == io.EOF { 102 | break // End of archive 103 | } else if err != nil { 104 | log.Warningln("Error reading next tar element", err) 105 | return 106 | } 107 | t.th.HandleFile(hdr, tr) 108 | } 109 | 110 | // Read file hash from the stream and check if it matches 111 | hash, err := t.node.ReadBytes(s) 112 | if err != nil { 113 | log.Warningln("Could not read hash", err) 114 | return 115 | } 116 | 117 | // Check if hashes match 118 | if err = sd.Authenticate(hash); err != nil { 119 | log.Warningln("Could not authenticate received data", err) 120 | return 121 | } 122 | } 123 | 124 | // Transfer can be called to transfer the given payload to the given peer. The PushRequest is used for displaying 125 | // the progress to the user. This function returns when the bytes where transmitted and we have received an 126 | // acknowledgment. 127 | func (t *TransferProtocol) Transfer(ctx context.Context, peerID peer.ID, basePath string) error { 128 | // Open a new stream to our peer. 129 | s, err := t.node.NewStream(ctx, peerID, ProtocolTransfer) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | defer s.Close() 135 | defer t.node.ResetOnShutdown(s)() 136 | 137 | base, err := os.Stat(basePath) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | // Get PAKE session key for stream encryption 143 | sKey, found := t.node.GetSessionKey(peerID) 144 | if !found { 145 | return fmt.Errorf("session key not found to encrypt data transfer") 146 | } 147 | 148 | // Initialize new stream encrypter 149 | se, err := crypt.NewStreamEncrypter(sKey, s) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | // Send the encryption initialization vector to our peer. 155 | // Does not need to be encrypted, just unique. 156 | // https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_.28IV.29 157 | _, err = t.node.WriteBytes(s, se.InitializationVector()) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | tw := tar.NewWriter(se) 163 | err = filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error { 164 | log.Debugln("Preparing file for transmission:", path) 165 | if err != nil { 166 | log.Debugln("Error walking file:", err) 167 | return err 168 | } 169 | 170 | hdr, err := tar.FileInfoHeader(info, "") 171 | if err != nil { 172 | return errors.Wrapf(err, "error writing tar file info header %s: %s", path, err) 173 | } 174 | 175 | // To preserve directory structure in the tar ball. 176 | hdr.Name, err = relPath(basePath, base.IsDir(), path) 177 | if err != nil { 178 | return errors.Wrapf(err, "error building relative path: %s (%v) %s", basePath, base.IsDir(), path) 179 | } 180 | 181 | if err = tw.WriteHeader(hdr); err != nil { 182 | return errors.Wrap(err, "error writing tar header") 183 | } 184 | 185 | // Continue as all information was written above with WriteHeader. 186 | if info.IsDir() { 187 | return nil 188 | } 189 | 190 | f, err := os.Open(path) 191 | if err != nil { 192 | return errors.Wrapf(err, "error opening file for taring at: %s", path) 193 | } 194 | defer f.Close() 195 | 196 | bar := progress.DefaultBytes(info.Size(), info.Name()) 197 | if _, err = io.Copy(io.MultiWriter(tw, bar), f); err != nil { 198 | return err 199 | } 200 | 201 | return nil 202 | }) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | if err = tw.Close(); err != nil { 208 | log.Debugln("Error closing tar ball", err) 209 | } 210 | 211 | // Send the hash of all sent data, so our recipient can check the data. 212 | _, err = t.node.WriteBytes(s, se.Hash()) 213 | if err != nil { 214 | return errors.Wrap(err, "error writing final hash to stream") 215 | } 216 | 217 | return t.node.WaitForEOF(s) 218 | } 219 | 220 | // relPath builds the path structure for the tar archive - this will be the structure as it is received. 221 | func relPath(basePath string, baseIsDir bool, targetPath string) (string, error) { 222 | if baseIsDir { 223 | rel, err := filepath.Rel(basePath, targetPath) 224 | if err != nil { 225 | return "", err 226 | } 227 | return filepath.Clean(filepath.Join(filepath.Base(basePath), rel)), nil 228 | } else { 229 | return filepath.Base(basePath), nil 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /pkg/node/transfer_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "archive/tar" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/dennis-tra/pcp/pkg/crypt" 17 | "github.com/dennis-tra/pcp/pkg/service" 18 | ) 19 | 20 | // TestTransferHandler is a mock transfer handler that can be registered for the TransferProtocol. 21 | type TestTransferHandler struct { 22 | handler func(*tar.Header, io.Reader) 23 | done func() 24 | } 25 | 26 | func (tth *TestTransferHandler) HandleFile(hdr *tar.Header, r io.Reader) { tth.handler(hdr, r) } 27 | 28 | func (tth *TestTransferHandler) Done() { 29 | tth.done() 30 | } 31 | 32 | func TestTransferProtocol_onTransfer(t *testing.T) { 33 | // The test sets up two nodes to simulate a file transfer. In every run node1 send a particular file or whole 34 | // directory from the top-level tests/ directory to node2. node2 then writes the received data to the tests/tmp/ 35 | // directory. After the file transfer has finished the assertTmpIntegrity checks if the contents of the tests/tmp/ 36 | // directory matches the contents of the particular test directory. 37 | // The abbreviation obj stands for object and captures the missing distinction between files and directories. 38 | tests := []struct { 39 | testObj string // Path as given by user. 40 | isDir bool 41 | }{ 42 | {testObj: "transfer_file/file", isDir: false}, 43 | {testObj: "transfer_dir_empty", isDir: true}, 44 | {testObj: "transfer_dir", isDir: true}, 45 | {testObj: "transfer_subdir", isDir: true}, 46 | {testObj: "transfer_file_subdir/subdir/file", isDir: false}, 47 | //{testObj: "transfer_link", isDir: true}, // doesn't work yet 48 | } 49 | for _, tt := range tests { 50 | t.Run("transfer-test: "+tt.testObj, func(t *testing.T) { 51 | ctx := context.Background() 52 | net := mocknet.New(ctx) 53 | 54 | node1, _ := setupNode(t, net) 55 | node2, done := setupNode(t, net) 56 | authNodes(t, node1, node2) 57 | 58 | err := net.LinkAll() 59 | require.NoError(t, err) 60 | 61 | err = node1.Transfer(ctx, node2.ID(), relTestDir(tt.testObj)) 62 | require.NoError(t, err) 63 | 64 | assertTmpIntegrity(t, tt.testObj, tt.isDir) 65 | 66 | <-done 67 | 68 | node1.UnregisterTransferHandler() 69 | node2.UnregisterTransferHandler() 70 | }) 71 | } 72 | // Clean up after ourselves 73 | require.NoError(t, os.RemoveAll(tmpDir())) 74 | } 75 | 76 | func TestTransferProtocol_onTransfer_senderNotAuthenticatedAtReceiver(t *testing.T) { 77 | ctx := context.Background() 78 | net := mocknet.New(ctx) 79 | 80 | node1, _ := setupNode(t, net) 81 | node2, _ := setupNode(t, net) 82 | 83 | // Simulate that the receiver is authenticated (from the perspective of the sender) 84 | key, err := crypt.DeriveKey([]byte{}, []byte{}) 85 | require.NoError(t, err) 86 | node1.PakeProtocol.authedPeers.Store(node2.ID(), key) 87 | 88 | err = net.LinkAll() 89 | require.NoError(t, err) 90 | 91 | err = node1.Transfer(ctx, node2.ID(), relTestDir("transfer_file/file")) 92 | require.Error(t, err) 93 | } 94 | 95 | func TestTransferProtocol_onTransfer_peersDifferentKeys(t *testing.T) { 96 | ctx := context.Background() 97 | net := mocknet.New(ctx) 98 | 99 | node1, _ := setupNode(t, net) 100 | node2, _ := setupNode(t, net) 101 | 102 | // Simulate that the receiver is authenticated (from the perspective of the sender) 103 | key1, err := crypt.DeriveKey([]byte{1}, []byte{1}) 104 | require.NoError(t, err) 105 | key2, err := crypt.DeriveKey([]byte{2}, []byte{2}) 106 | require.NoError(t, err) 107 | node1.PakeProtocol.authedPeers.Store(node2.ID(), key1) 108 | node2.PakeProtocol.authedPeers.Store(node1.ID(), key2) 109 | 110 | err = net.LinkAll() 111 | require.NoError(t, err) 112 | 113 | err = node1.Transfer(ctx, node2.ID(), relTestDir("transfer_file/file")) 114 | fmt.Println(err) 115 | require.Error(t, err) 116 | } 117 | 118 | func TestTransferProtocol_onTransfer_provokeErrCases(t *testing.T) { 119 | ctx := context.Background() 120 | net := mocknet.New(ctx) 121 | 122 | node1, _ := setupNode(t, net) 123 | node2, _ := setupNode(t, net) 124 | 125 | node1.RegisterTransferHandler(&TestTransferHandler{handler: tmpWriter(t), done: func() {}}) 126 | node2.RegisterTransferHandler(&TestTransferHandler{handler: tmpWriter(t), done: func() {}}) 127 | 128 | // Can't create stream 129 | err := node1.Transfer(ctx, "some-non-existing-node", "") 130 | require.Error(t, err) 131 | assert.Contains(t, err.Error(), "cannot connect") 132 | 133 | err = net.LinkAll() 134 | require.NoError(t, err) 135 | 136 | // Can't read object that the user wants to send 137 | err = node1.Transfer(ctx, node2.ID(), "") 138 | require.Error(t, err) 139 | assert.Contains(t, err.Error(), "no such file or directory") 140 | 141 | // Receiving peer is unauthenticated 142 | err = node1.Transfer(ctx, node2.ID(), relTestDir("transfer_file/file")) 143 | require.Error(t, err) 144 | assert.Contains(t, err.Error(), "session key not found") 145 | 146 | // Session key has wrong format 147 | node1.authedPeers.Store(node2.ID(), []byte{1, 2, 3}) 148 | err = node1.Transfer(ctx, node2.ID(), relTestDir("transfer_file/file")) 149 | require.Error(t, err) 150 | assert.Contains(t, err.Error(), "invalid key size 3") 151 | } 152 | 153 | // setupNode builds a node ready to handle a transfer. 154 | func setupNode(t *testing.T, net mocknet.Mocknet) (*Node, chan struct{}) { 155 | p, err := net.GenPeer() 156 | require.NoError(t, err) 157 | n := &Node{Service: service.New("node"), Host: p} 158 | n.PakeProtocol = &PakeProtocol{} 159 | n.TransferProtocol = NewTransferProtocol(n) 160 | done := make(chan struct{}) 161 | n.RegisterTransferHandler(&TestTransferHandler{handler: tmpWriter(t), done: func() { close(done) }}) 162 | n.PushProtocol = NewPushProtocol(n) 163 | return n, done 164 | } 165 | 166 | // authNodes generates a key and uses it to simulate that the two given nodes have a shared key aka are authenticated. 167 | func authNodes(t *testing.T, node1 *Node, node2 *Node) { 168 | key, err := crypt.DeriveKey([]byte{}, []byte{}) 169 | require.NoError(t, err) 170 | node1.PakeProtocol.authedPeers.Store(node2.ID(), key) 171 | node2.PakeProtocol.authedPeers.Store(node1.ID(), key) 172 | } 173 | 174 | func Test_relPath(t *testing.T) { 175 | tests := []struct { 176 | basePath string // Path given by user. 177 | baseIsDir bool 178 | targetPath string // Path retrieved from "walking" basePath. 179 | wantPath string // Path we want to put in the tar - this is how the files are extracted on the receiving side. 180 | }{ 181 | {basePath: "file", baseIsDir: false, targetPath: "file", wantPath: "file"}, 182 | {basePath: "a/file", baseIsDir: false, targetPath: "a/file", wantPath: "file"}, 183 | {basePath: "../../file", baseIsDir: false, targetPath: "../../file", wantPath: "file"}, 184 | {basePath: "../a", baseIsDir: true, targetPath: "../a/file", wantPath: "a/file"}, 185 | {basePath: "a/", baseIsDir: true, targetPath: "a/file", wantPath: "a/file"}, 186 | {basePath: "a", baseIsDir: true, targetPath: "a", wantPath: "a"}, 187 | {basePath: "a/b/", baseIsDir: true, targetPath: "a/b/file", wantPath: "b/file"}, 188 | {basePath: "../a/./b/", baseIsDir: true, targetPath: "../a/b/c/file", wantPath: "b/c/file"}, 189 | {basePath: "../a/./b/", baseIsDir: true, targetPath: "../a/b/c/file", wantPath: "b/c/file"}, 190 | } 191 | for _, tt := range tests { 192 | name := fmt.Sprintf("base: %s (%v), target: %s -> %s", tt.basePath, tt.baseIsDir, tt.targetPath, tt.wantPath) 193 | t.Run(name, func(t *testing.T) { 194 | got, err := relPath(tt.basePath, tt.baseIsDir, tt.targetPath) 195 | require.NoError(t, err) 196 | assert.Equal(t, tt.wantPath, got) 197 | }) 198 | } 199 | } 200 | 201 | // relTestDir is a helper to only deal with paths relative to the test directory. 202 | // This function prepends the necessary relative path components. 203 | func relTestDir(path string) string { 204 | return filepath.Join("..", "..", "test", path) 205 | } 206 | 207 | // tmpDir returns the directory where temporary files and directories are written. 208 | func tmpDir() string { 209 | return relTestDir("tmp") 210 | } 211 | 212 | // tmpWriter writes the content of hdr and reader r to the test/tmp directory for comparison. 213 | func tmpWriter(t *testing.T) func(hdr *tar.Header, r io.Reader) { 214 | tmpDir := relTestDir("tmp") 215 | require.NoError(t, os.RemoveAll(tmpDir)) 216 | require.NoError(t, os.Mkdir(tmpDir, 0o774)) 217 | 218 | return func(hdr *tar.Header, r io.Reader) { 219 | finfo := hdr.FileInfo() 220 | joined := filepath.Join(tmpDir, hdr.Name) 221 | if finfo.IsDir() { 222 | require.NoError(t, os.MkdirAll(joined, finfo.Mode())) 223 | } else { 224 | newFile, err := os.OpenFile(joined, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, finfo.Mode().Perm()) 225 | require.NoError(t, err) 226 | _, err = io.Copy(newFile, r) 227 | require.NoError(t, err) 228 | } 229 | } 230 | } 231 | 232 | // assertTmpIntegrity checks that the source directory/file is equal to the content that is found in the content 233 | // that was written to the tmp directory. 234 | func assertTmpIntegrity(t *testing.T, srcObj string, srcIsDir bool) { 235 | tmpObj := tmpDir() 236 | if srcIsDir { 237 | tmpObj = filepath.Join(tmpObj, srcObj) 238 | } else { 239 | tmpObj = filepath.Join(tmpObj, filepath.Base(srcObj)) 240 | } 241 | testObj := relTestDir(srcObj) 242 | 243 | type walkData struct { 244 | path string 245 | info os.FileInfo 246 | err error 247 | } 248 | 249 | var tmpWalkData []walkData 250 | var testWalkData []walkData 251 | 252 | err := filepath.Walk(tmpObj, func(path string, info os.FileInfo, err error) error { 253 | tmpWalkData = append(tmpWalkData, walkData{path, info, err}) 254 | return err 255 | }) 256 | require.NoError(t, err) 257 | 258 | err = filepath.Walk(testObj, func(path string, info os.FileInfo, err error) error { 259 | testWalkData = append(testWalkData, walkData{path, info, err}) 260 | return err 261 | }) 262 | require.NoError(t, err) 263 | 264 | require.Equal(t, len(tmpWalkData), len(testWalkData)) 265 | 266 | for i := 0; i < len(tmpWalkData); i++ { 267 | tmpWalk := tmpWalkData[i] 268 | testWalk := testWalkData[i] 269 | 270 | assert.NoError(t, tmpWalk.err) 271 | assert.NoError(t, testWalk.err) 272 | 273 | tmpRel, err := filepath.Rel(tmpObj, tmpWalk.path) 274 | require.NoError(t, err) 275 | 276 | testRel, err := filepath.Rel(testObj, testWalk.path) 277 | require.NoError(t, err) 278 | 279 | assert.Equal(t, tmpRel, testRel) 280 | assert.Equal(t, tmpWalk.info.Size(), testWalk.info.Size()) 281 | assert.Equal(t, tmpWalk.info.Mode(), testWalk.info.Mode()) 282 | assert.Equal(t, tmpWalk.info.IsDir(), testWalk.info.IsDir()) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /pkg/pb/p2p.aux.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "github.com/golang/protobuf/proto" 5 | "github.com/libp2p/go-libp2p-core/peer" 6 | ) 7 | 8 | type HeaderMessage interface { 9 | GetHeader() *Header 10 | SetHeader(*Header) 11 | PeerID() (peer.ID, error) 12 | proto.Message 13 | } 14 | 15 | func (x *PushRequest) SetHeader(hdr *Header) { 16 | x.Header = hdr 17 | } 18 | 19 | func (x *PushResponse) SetHeader(hdr *Header) { 20 | x.Header = hdr 21 | } 22 | 23 | func (x *PushRequest) PeerID() (peer.ID, error) { 24 | return peer.Decode(x.GetHeader().NodeId) 25 | } 26 | 27 | func (x *PushResponse) PeerID() (peer.ID, error) { 28 | return peer.Decode(x.GetHeader().NodeId) 29 | } 30 | 31 | func NewPushResponse(accept bool) *PushResponse { 32 | return &PushResponse{Accept: accept} 33 | } 34 | 35 | func NewPushRequest(name string, size int64, isDir bool) *PushRequest { 36 | return &PushRequest{ 37 | Name: name, 38 | Size: size, 39 | IsDir: isDir, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/pb/p2p.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.15.8 5 | // source: p2p.proto 6 | 7 | package proto 8 | 9 | import ( 10 | reflect "reflect" 11 | sync "sync" 12 | 13 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 14 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // A message object that is shared among all requests. 25 | type Header struct { 26 | state protoimpl.MessageState 27 | sizeCache protoimpl.SizeCache 28 | unknownFields protoimpl.UnknownFields 29 | 30 | // A unique request ID to allow idempotency. 31 | RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` 32 | // Unix time in milliseconds. 33 | Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` 34 | // The ID of the node that created the message (not the peer that may have sent it) 35 | NodeId string `protobuf:"bytes,3,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` 36 | // Authoring node Secp256k1 public key (32bytes) - protobufs serialized 37 | NodePubKey []byte `protobuf:"bytes,4,opt,name=node_pub_key,json=nodePubKey,proto3" json:"node_pub_key,omitempty"` 38 | // The signature of the message data. 39 | Signature []byte `protobuf:"bytes,5,opt,name=signature,proto3" json:"signature,omitempty"` 40 | } 41 | 42 | func (x *Header) Reset() { 43 | *x = Header{} 44 | if protoimpl.UnsafeEnabled { 45 | mi := &file_p2p_proto_msgTypes[0] 46 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 47 | ms.StoreMessageInfo(mi) 48 | } 49 | } 50 | 51 | func (x *Header) String() string { 52 | return protoimpl.X.MessageStringOf(x) 53 | } 54 | 55 | func (*Header) ProtoMessage() {} 56 | 57 | func (x *Header) ProtoReflect() protoreflect.Message { 58 | mi := &file_p2p_proto_msgTypes[0] 59 | if protoimpl.UnsafeEnabled && x != nil { 60 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 61 | if ms.LoadMessageInfo() == nil { 62 | ms.StoreMessageInfo(mi) 63 | } 64 | return ms 65 | } 66 | return mi.MessageOf(x) 67 | } 68 | 69 | // Deprecated: Use Header.ProtoReflect.Descriptor instead. 70 | func (*Header) Descriptor() ([]byte, []int) { 71 | return file_p2p_proto_rawDescGZIP(), []int{0} 72 | } 73 | 74 | func (x *Header) GetRequestId() string { 75 | if x != nil { 76 | return x.RequestId 77 | } 78 | return "" 79 | } 80 | 81 | func (x *Header) GetTimestamp() int64 { 82 | if x != nil { 83 | return x.Timestamp 84 | } 85 | return 0 86 | } 87 | 88 | func (x *Header) GetNodeId() string { 89 | if x != nil { 90 | return x.NodeId 91 | } 92 | return "" 93 | } 94 | 95 | func (x *Header) GetNodePubKey() []byte { 96 | if x != nil { 97 | return x.NodePubKey 98 | } 99 | return nil 100 | } 101 | 102 | func (x *Header) GetSignature() []byte { 103 | if x != nil { 104 | return x.Signature 105 | } 106 | return nil 107 | } 108 | 109 | // PushRequest is sent to the receiving peer for acceptance. 110 | // It contains basic information about the data that is 111 | // about to be transmitted. 112 | type PushRequest struct { 113 | state protoimpl.MessageState 114 | sizeCache protoimpl.SizeCache 115 | unknownFields protoimpl.UnknownFields 116 | 117 | // General meta information about the request. 118 | Header *Header `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` 119 | // The name of the file that is about to be transferred. 120 | // This will also be the name that the receiving peer 121 | // uses upon save. 122 | Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 123 | // The size of the file to be transmitted. 124 | Size int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"` 125 | // Whether or not the file is a directory. 126 | IsDir bool `protobuf:"varint,4,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"` 127 | // The number of files to be transferred. 128 | FileCount bool `protobuf:"varint,5,opt,name=file_count,json=fileCount,proto3" json:"file_count,omitempty"` 129 | } 130 | 131 | func (x *PushRequest) Reset() { 132 | *x = PushRequest{} 133 | if protoimpl.UnsafeEnabled { 134 | mi := &file_p2p_proto_msgTypes[1] 135 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 136 | ms.StoreMessageInfo(mi) 137 | } 138 | } 139 | 140 | func (x *PushRequest) String() string { 141 | return protoimpl.X.MessageStringOf(x) 142 | } 143 | 144 | func (*PushRequest) ProtoMessage() {} 145 | 146 | func (x *PushRequest) ProtoReflect() protoreflect.Message { 147 | mi := &file_p2p_proto_msgTypes[1] 148 | if protoimpl.UnsafeEnabled && x != nil { 149 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 150 | if ms.LoadMessageInfo() == nil { 151 | ms.StoreMessageInfo(mi) 152 | } 153 | return ms 154 | } 155 | return mi.MessageOf(x) 156 | } 157 | 158 | // Deprecated: Use PushRequest.ProtoReflect.Descriptor instead. 159 | func (*PushRequest) Descriptor() ([]byte, []int) { 160 | return file_p2p_proto_rawDescGZIP(), []int{1} 161 | } 162 | 163 | func (x *PushRequest) GetHeader() *Header { 164 | if x != nil { 165 | return x.Header 166 | } 167 | return nil 168 | } 169 | 170 | func (x *PushRequest) GetName() string { 171 | if x != nil { 172 | return x.Name 173 | } 174 | return "" 175 | } 176 | 177 | func (x *PushRequest) GetSize() int64 { 178 | if x != nil { 179 | return x.Size 180 | } 181 | return 0 182 | } 183 | 184 | func (x *PushRequest) GetIsDir() bool { 185 | if x != nil { 186 | return x.IsDir 187 | } 188 | return false 189 | } 190 | 191 | func (x *PushRequest) GetFileCount() bool { 192 | if x != nil { 193 | return x.FileCount 194 | } 195 | return false 196 | } 197 | 198 | // PushResponse is sent as a reply to the PushRequest message. 199 | // It just indicates if the receiving peer is willing to 200 | // accept the file. 201 | type PushResponse struct { 202 | state protoimpl.MessageState 203 | sizeCache protoimpl.SizeCache 204 | unknownFields protoimpl.UnknownFields 205 | 206 | // General meta information about the request. 207 | Header *Header `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` 208 | // Whether or not the user accepted the file transfer. 209 | Accept bool `protobuf:"varint,2,opt,name=accept,proto3" json:"accept,omitempty"` 210 | } 211 | 212 | func (x *PushResponse) Reset() { 213 | *x = PushResponse{} 214 | if protoimpl.UnsafeEnabled { 215 | mi := &file_p2p_proto_msgTypes[2] 216 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 217 | ms.StoreMessageInfo(mi) 218 | } 219 | } 220 | 221 | func (x *PushResponse) String() string { 222 | return protoimpl.X.MessageStringOf(x) 223 | } 224 | 225 | func (*PushResponse) ProtoMessage() {} 226 | 227 | func (x *PushResponse) ProtoReflect() protoreflect.Message { 228 | mi := &file_p2p_proto_msgTypes[2] 229 | if protoimpl.UnsafeEnabled && x != nil { 230 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 231 | if ms.LoadMessageInfo() == nil { 232 | ms.StoreMessageInfo(mi) 233 | } 234 | return ms 235 | } 236 | return mi.MessageOf(x) 237 | } 238 | 239 | // Deprecated: Use PushResponse.ProtoReflect.Descriptor instead. 240 | func (*PushResponse) Descriptor() ([]byte, []int) { 241 | return file_p2p_proto_rawDescGZIP(), []int{2} 242 | } 243 | 244 | func (x *PushResponse) GetHeader() *Header { 245 | if x != nil { 246 | return x.Header 247 | } 248 | return nil 249 | } 250 | 251 | func (x *PushResponse) GetAccept() bool { 252 | if x != nil { 253 | return x.Accept 254 | } 255 | return false 256 | } 257 | 258 | var File_p2p_proto protoreflect.FileDescriptor 259 | 260 | var file_p2p_proto_rawDesc = []byte{ 261 | 0x0a, 0x09, 0x70, 0x32, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9e, 0x01, 0x0a, 0x06, 262 | 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 263 | 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 264 | 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 265 | 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 266 | 0x61, 0x6d, 0x70, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 267 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 268 | 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 269 | 0x28, 0x0c, 0x52, 0x0a, 0x6e, 0x6f, 0x64, 0x65, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1c, 270 | 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 271 | 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x8c, 0x01, 0x0a, 272 | 0x0b, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x06, 273 | 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x48, 274 | 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 275 | 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 276 | 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 277 | 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x69, 0x73, 0x5f, 0x64, 0x69, 0x72, 0x18, 278 | 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x69, 0x73, 0x44, 0x69, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 279 | 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 280 | 0x52, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x47, 0x0a, 0x0c, 0x50, 281 | 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x68, 282 | 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x48, 0x65, 283 | 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 284 | 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 285 | 0x63, 0x65, 0x70, 0x74, 0x42, 0x25, 0x5a, 0x23, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 286 | 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x6e, 0x6e, 0x69, 0x73, 0x2d, 0x74, 0x72, 0x61, 0x2f, 0x70, 0x63, 287 | 0x70, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 288 | 0x74, 0x6f, 0x33, 289 | } 290 | 291 | var ( 292 | file_p2p_proto_rawDescOnce sync.Once 293 | file_p2p_proto_rawDescData = file_p2p_proto_rawDesc 294 | ) 295 | 296 | func file_p2p_proto_rawDescGZIP() []byte { 297 | file_p2p_proto_rawDescOnce.Do(func() { 298 | file_p2p_proto_rawDescData = protoimpl.X.CompressGZIP(file_p2p_proto_rawDescData) 299 | }) 300 | return file_p2p_proto_rawDescData 301 | } 302 | 303 | var ( 304 | file_p2p_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 305 | file_p2p_proto_goTypes = []interface{}{ 306 | (*Header)(nil), // 0: Header 307 | (*PushRequest)(nil), // 1: PushRequest 308 | (*PushResponse)(nil), // 2: PushResponse 309 | } 310 | ) 311 | 312 | var file_p2p_proto_depIdxs = []int32{ 313 | 0, // 0: PushRequest.header:type_name -> Header 314 | 0, // 1: PushResponse.header:type_name -> Header 315 | 2, // [2:2] is the sub-list for method output_type 316 | 2, // [2:2] is the sub-list for method input_type 317 | 2, // [2:2] is the sub-list for extension type_name 318 | 2, // [2:2] is the sub-list for extension extendee 319 | 0, // [0:2] is the sub-list for field type_name 320 | } 321 | 322 | func init() { file_p2p_proto_init() } 323 | func file_p2p_proto_init() { 324 | if File_p2p_proto != nil { 325 | return 326 | } 327 | if !protoimpl.UnsafeEnabled { 328 | file_p2p_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 329 | switch v := v.(*Header); i { 330 | case 0: 331 | return &v.state 332 | case 1: 333 | return &v.sizeCache 334 | case 2: 335 | return &v.unknownFields 336 | default: 337 | return nil 338 | } 339 | } 340 | file_p2p_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 341 | switch v := v.(*PushRequest); i { 342 | case 0: 343 | return &v.state 344 | case 1: 345 | return &v.sizeCache 346 | case 2: 347 | return &v.unknownFields 348 | default: 349 | return nil 350 | } 351 | } 352 | file_p2p_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 353 | switch v := v.(*PushResponse); i { 354 | case 0: 355 | return &v.state 356 | case 1: 357 | return &v.sizeCache 358 | case 2: 359 | return &v.unknownFields 360 | default: 361 | return nil 362 | } 363 | } 364 | } 365 | type x struct{} 366 | out := protoimpl.TypeBuilder{ 367 | File: protoimpl.DescBuilder{ 368 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 369 | RawDescriptor: file_p2p_proto_rawDesc, 370 | NumEnums: 0, 371 | NumMessages: 3, 372 | NumExtensions: 0, 373 | NumServices: 0, 374 | }, 375 | GoTypes: file_p2p_proto_goTypes, 376 | DependencyIndexes: file_p2p_proto_depIdxs, 377 | MessageInfos: file_p2p_proto_msgTypes, 378 | }.Build() 379 | File_p2p_proto = out.File 380 | file_p2p_proto_rawDesc = nil 381 | file_p2p_proto_goTypes = nil 382 | file_p2p_proto_depIdxs = nil 383 | } 384 | -------------------------------------------------------------------------------- /pkg/pb/p2p.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "github.com/dennis-tra/pcp/pkg/proto"; 3 | 4 | // A message object that is shared among all requests. 5 | message Header { 6 | 7 | // A unique request ID to allow idempotency. 8 | string request_id = 1; 9 | 10 | // Unix time in milliseconds. 11 | int64 timestamp = 2; 12 | 13 | // The ID of the node that created the message (not the peer that may have sent it) 14 | string node_id = 3; 15 | 16 | // Authoring node Secp256k1 public key (32bytes) - protobufs serialized 17 | bytes node_pub_key = 4; 18 | 19 | // The signature of the message data. 20 | bytes signature = 5; 21 | } 22 | 23 | // PushRequest is sent to the receiving peer for acceptance. 24 | // It contains basic information about the data that is 25 | // about to be transmitted. 26 | message PushRequest { 27 | 28 | // General meta information about the request. 29 | Header header = 1; 30 | 31 | // The name of the file that is about to be transferred. 32 | // This will also be the name that the receiving peer 33 | // uses upon save. 34 | string name = 2; 35 | 36 | // The size of the file to be transmitted. 37 | int64 size = 3; 38 | 39 | // Whether or not the file is a directory. 40 | bool is_dir = 4; 41 | 42 | // The number of files to be transferred. 43 | bool file_count = 5; 44 | } 45 | 46 | // PushResponse is sent as a reply to the PushRequest message. 47 | // It just indicates if the receiving peer is willing to 48 | // accept the file. 49 | message PushResponse { 50 | 51 | // General meta information about the request. 52 | Header header = 1; 53 | 54 | // Whether or not the user accepted the file transfer. 55 | bool accept = 2; 56 | } 57 | -------------------------------------------------------------------------------- /pkg/receive/cmd.go: -------------------------------------------------------------------------------- 1 | package receive 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/urfave/cli/v2" 10 | 11 | "github.com/dennis-tra/pcp/internal/log" 12 | "github.com/dennis-tra/pcp/pkg/config" 13 | p2p "github.com/dennis-tra/pcp/pkg/pb" 14 | ) 15 | 16 | // Command contains the receive sub-command configuration. 17 | var Command = &cli.Command{ 18 | Name: "receive", 19 | Usage: "search for peers in your local network and the DHT", 20 | Aliases: []string{"r", "get"}, 21 | Action: Action, 22 | ArgsUsage: "[WORD-CODE]", 23 | Flags: []cli.Flag{ 24 | &cli.BoolFlag{ 25 | Name: "auto-accept", 26 | Aliases: []string{"yes", "y"}, 27 | Usage: "automatically accept the file transfer", 28 | EnvVars: []string{"PCP_AUTO_ACCEPT"}, 29 | }, 30 | }, 31 | Description: `The receive subcommand starts searching for peers in your local 32 | network by sending out multicast DNS queries. These queries are 33 | based on the current time and the first word of the given list. It 34 | simultaneously queries the distributed hash table (DHT) with the 35 | exact same parameters. 36 | 37 | It is important to note that many networks restrict the use of 38 | multicasting, which prevents mDNS from functioning. Notably, 39 | multicast cannot be used in any sort of cloud, or shared infra- 40 | structure environment. However it works well in most office, home, 41 | or private infrastructure environments. 42 | 43 | After it has found a potential peer it starts a password authen- 44 | ticated key exchange (PAKE) with the remaining three words to 45 | proof that the peer is in possession of the password. While this 46 | is happening the tool still searches for other peers as the 47 | currently connected one could fail the authentication procedure. 48 | 49 | After the authentication was successful you need to confirm the 50 | file transfer. The confirmation dialog shows the name and size of 51 | the file. 52 | 53 | The file will be saved to your current working directory overwriting 54 | any files with the same name. If the transmission fails the file 55 | will contain the partial written bytes.`, 56 | } 57 | 58 | // Action is the function that is called when running pcp receive. 59 | func Action(c *cli.Context) error { 60 | c, err := config.FillContext(c) 61 | if err != nil { 62 | return errors.Wrap(err, "failed loading configuration") 63 | } 64 | 65 | words := strings.Split(c.Args().First(), "-") // transfer words 66 | 67 | local, err := InitNode(c, words) 68 | if err != nil { 69 | return errors.Wrap(err, fmt.Sprintf("failed to initialize node")) 70 | } 71 | 72 | // Search for identifier 73 | log.Infof("Looking for peer %s... \n", c.Args().First()) 74 | local.StartDiscovering(c) 75 | 76 | // Wait for the user to stop the tool or the transfer to finish. 77 | select { 78 | case <-c.Done(): 79 | local.Shutdown() 80 | return nil 81 | case <-local.SigDone(): 82 | return nil 83 | } 84 | } 85 | 86 | func printInformation(data *p2p.PushRequest) { 87 | log.Infoln("Sending request information:") 88 | log.Infoln("\tPeer:\t", data.Header.NodeId) 89 | log.Infoln("\tName:\t", data.Name) 90 | log.Infoln("\tSize:\t", data.Size) 91 | log.Infoln("\tSign:\t", hex.EncodeToString(data.Header.Signature)) 92 | log.Infoln("\tPubKey:\t", hex.EncodeToString(data.Header.GetNodePubKey())) 93 | } 94 | 95 | func help() { 96 | log.Infoln("y: accept the file transfer") 97 | log.Infoln("n: reject the file transfer") 98 | log.Infoln("i: show information about the sender and file to be received") 99 | log.Infoln("?: this help message") 100 | } 101 | -------------------------------------------------------------------------------- /pkg/receive/node.go: -------------------------------------------------------------------------------- 1 | package receive 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/urfave/cli/v2" 10 | 11 | "github.com/dennis-tra/pcp/internal/format" 12 | "github.com/dennis-tra/pcp/internal/log" 13 | "github.com/dennis-tra/pcp/pkg/dht" 14 | "github.com/dennis-tra/pcp/pkg/mdns" 15 | pcpnode "github.com/dennis-tra/pcp/pkg/node" 16 | p2p "github.com/dennis-tra/pcp/pkg/pb" 17 | "github.com/libp2p/go-libp2p-core/peer" 18 | "github.com/pkg/errors" 19 | ) 20 | 21 | type PeerState uint8 22 | 23 | const ( 24 | NotConnected PeerState = iota 25 | Connecting 26 | Connected 27 | FailedConnecting 28 | FailedAuthentication 29 | ) 30 | 31 | type Node struct { 32 | *pcpnode.Node 33 | 34 | autoAccept bool 35 | discoverers []Discoverer 36 | peerStates *sync.Map // TODO: Use PeerStore? 37 | } 38 | 39 | type Discoverer interface { 40 | Discover(chanID int, handler func(info peer.AddrInfo)) error 41 | Shutdown() 42 | } 43 | 44 | func InitNode(c *cli.Context, words []string) (*Node, error) { 45 | h, err := pcpnode.New(c, words) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | n := &Node{ 51 | Node: h, 52 | autoAccept: c.Bool("auto-accept"), 53 | peerStates: &sync.Map{}, 54 | discoverers: []Discoverer{}, 55 | } 56 | 57 | n.RegisterPushRequestHandler(n) 58 | 59 | return n, nil 60 | } 61 | 62 | func (n *Node) Shutdown() { 63 | n.StopDiscovering() 64 | n.UnregisterPushRequestHandler() 65 | n.UnregisterTransferHandler() 66 | n.Node.Shutdown() 67 | } 68 | 69 | func (n *Node) StartDiscovering(c *cli.Context) { 70 | n.SetState(pcpnode.Discovering) 71 | 72 | if c.Bool("mdns") == c.Bool("dht") { 73 | n.discoverers = []Discoverer{ 74 | dht.NewDiscoverer(n, n.DHT), 75 | dht.NewDiscoverer(n, n.DHT).SetOffset(-dht.TruncateDuration), 76 | mdns.NewDiscoverer(n.Node), 77 | mdns.NewDiscoverer(n.Node).SetOffset(-dht.TruncateDuration), 78 | } 79 | } else if c.Bool("mdns") { 80 | n.discoverers = []Discoverer{ 81 | mdns.NewDiscoverer(n.Node), 82 | mdns.NewDiscoverer(n.Node).SetOffset(-dht.TruncateDuration), 83 | } 84 | } else if c.Bool("dht") { 85 | n.discoverers = []Discoverer{ 86 | dht.NewDiscoverer(n, n.DHT), 87 | dht.NewDiscoverer(n, n.DHT).SetOffset(-dht.TruncateDuration), 88 | } 89 | } 90 | 91 | for _, discoverer := range n.discoverers { 92 | go func(d Discoverer) { 93 | err := d.Discover(n.ChanID, n.HandlePeer) 94 | if err == nil { 95 | return 96 | } 97 | 98 | // If the user is connected to another peer 99 | // we don't care about discover errors. 100 | if n.GetState() == pcpnode.Connected { 101 | return 102 | } 103 | 104 | switch e := err.(type) { 105 | case dht.ErrConnThresholdNotReached: 106 | e.Log() 107 | default: 108 | log.Warningln(err) 109 | } 110 | }(discoverer) 111 | } 112 | } 113 | 114 | func (n *Node) StopDiscovering() { 115 | var wg sync.WaitGroup 116 | for _, discoverer := range n.discoverers { 117 | wg.Add(1) 118 | go func(d Discoverer) { 119 | d.Shutdown() 120 | wg.Done() 121 | }(discoverer) 122 | } 123 | wg.Wait() 124 | } 125 | 126 | // HandlePeer is called async from the discoverers. It's okay to have long running tasks here. 127 | func (n *Node) HandlePeer(pi peer.AddrInfo) { 128 | if n.GetState() != pcpnode.Discovering { 129 | log.Debugln("Received a peer from the discoverer although we're not discovering") 130 | return 131 | } 132 | 133 | // Check if we have already seen the peer and exit early to not connect again. 134 | peerState, _ := n.peerStates.LoadOrStore(pi.ID, NotConnected) 135 | switch peerState.(PeerState) { 136 | case NotConnected: 137 | case Connecting: 138 | log.Debugln("Skipping node as we're already trying to connect", pi.ID) 139 | return 140 | case FailedConnecting: 141 | // TODO: Check if multiaddrs have changed and only connect if that's the case 142 | log.Debugln("We tried to connect previously but couldn't establish a connection, try again", pi.ID) 143 | case FailedAuthentication: 144 | log.Debugln("We tried to connect previously but the node didn't pass authentication -> skipping", pi.ID) 145 | return 146 | } 147 | 148 | log.Debugln("Connecting to peer:", pi.ID) 149 | n.peerStates.Store(pi.ID, Connecting) 150 | if err := n.Connect(n.ServiceContext(), pi); err != nil { 151 | log.Debugln("Error connecting to peer:", pi.ID, err) 152 | n.peerStates.Store(pi.ID, FailedConnecting) 153 | return 154 | } 155 | 156 | // Negotiate PAKE 157 | if _, err := n.StartKeyExchange(n.ServiceContext(), pi.ID); err != nil { 158 | log.Errorln("Peer didn't pass authentication:", err) 159 | n.peerStates.Store(pi.ID, FailedAuthentication) 160 | return 161 | } 162 | n.peerStates.Store(pi.ID, Connected) 163 | 164 | // We're authenticated so can initiate a transfer 165 | if n.GetState() == pcpnode.Connected { 166 | log.Debugln("already connected and authenticated with another node") 167 | return 168 | } 169 | n.SetState(pcpnode.Connected) 170 | 171 | // Stop the discovering process as we have found the valid peer 172 | n.StopDiscovering() 173 | } 174 | 175 | func (n *Node) HandlePushRequest(pr *p2p.PushRequest) (bool, error) { 176 | if n.autoAccept { 177 | return n.handleAccept(pr) 178 | } 179 | 180 | obj := "File" 181 | if pr.IsDir { 182 | obj = "Directory" 183 | } 184 | log.Infof("%s: %s (%s)\n", obj, pr.Name, format.Bytes(pr.Size)) 185 | for { 186 | log.Infof("Do you want to receive this %s? [y,n,i,?] ", strings.ToLower(obj)) 187 | scanner := bufio.NewScanner(os.Stdin) 188 | if !scanner.Scan() { 189 | return true, errors.Wrap(scanner.Err(), "failed reading from stdin") 190 | } 191 | 192 | // sanitize user input 193 | input := strings.ToLower(strings.TrimSpace(scanner.Text())) 194 | 195 | // Empty input, user just pressed enter => do nothing and prompt again 196 | if input == "" { 197 | continue 198 | } 199 | 200 | // Print the help text and prompt again 201 | if input == "?" { 202 | help() 203 | continue 204 | } 205 | 206 | // Print information about the send request 207 | if input == "i" { 208 | printInformation(pr) 209 | continue 210 | } 211 | 212 | // Accept the file transfer 213 | if input == "y" { 214 | return n.handleAccept(pr) 215 | } 216 | 217 | // Reject the file transfer 218 | if input == "n" { 219 | go n.Shutdown() 220 | return false, nil 221 | } 222 | 223 | log.Infoln("Invalid input") 224 | } 225 | } 226 | 227 | // handleAccept handles the case when the user accepted the transfer or provided 228 | // the corresponding command line flag. 229 | func (n *Node) handleAccept(pr *p2p.PushRequest) (bool, error) { 230 | done := n.TransferFinishHandler(pr.Size) 231 | th, err := NewTransferHandler(pr.Name, done) 232 | if err != nil { 233 | return true, err 234 | } 235 | n.RegisterTransferHandler(th) 236 | return true, nil 237 | } 238 | 239 | func (n *Node) TransferFinishHandler(size int64) chan int64 { 240 | done := make(chan int64) 241 | go func() { 242 | var received int64 243 | select { 244 | case <-n.SigShutdown(): 245 | return 246 | case received = <-done: 247 | } 248 | 249 | if received == size { 250 | log.Infoln("Successfully received file/directory!") 251 | } else { 252 | log.Infof("WARNING: Only received %d of %d bytes!\n", received, size) 253 | } 254 | 255 | n.Shutdown() 256 | }() 257 | return done 258 | } 259 | -------------------------------------------------------------------------------- /pkg/receive/transfer.go: -------------------------------------------------------------------------------- 1 | package receive 2 | 3 | import ( 4 | "archive/tar" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | progress "github.com/schollz/progressbar/v3" 10 | 11 | "github.com/dennis-tra/pcp/internal/log" 12 | ) 13 | 14 | type TransferHandler struct { 15 | filename string 16 | received int64 17 | done chan int64 18 | } 19 | 20 | func NewTransferHandler(filename string, done chan int64) (*TransferHandler, error) { 21 | return &TransferHandler{filename: filename, done: done}, nil 22 | } 23 | 24 | func (th *TransferHandler) Done() { 25 | th.done <- th.received 26 | close(th.done) 27 | } 28 | 29 | func (th *TransferHandler) HandleFile(hdr *tar.Header, src io.Reader) { 30 | cwd, err := os.Getwd() 31 | if err != nil { 32 | log.Warningln("error determining current working directory:", err) 33 | cwd = "." 34 | } 35 | 36 | finfo := hdr.FileInfo() 37 | joined := filepath.Join(cwd, hdr.Name) 38 | if finfo.IsDir() { 39 | err := os.MkdirAll(joined, finfo.Mode()) 40 | if err != nil { 41 | log.Warningln("error creating directory:", joined, err) 42 | } 43 | } 44 | newFile, err := os.OpenFile(joined, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, finfo.Mode().Perm()) 45 | if err != nil { 46 | log.Warningln("error creating file:", joined, err) 47 | return 48 | } 49 | 50 | bar := progress.DefaultBytes(hdr.Size, filepath.Base(hdr.Name)) 51 | n, err := io.Copy(io.MultiWriter(newFile, bar), src) 52 | th.received += n 53 | if err != nil { 54 | log.Warningln("error writing file content:", joined, err) 55 | return 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/send/cmd.go: -------------------------------------------------------------------------------- 1 | package send 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/dennis-tra/pcp/pkg/words" 9 | 10 | "github.com/urfave/cli/v2" 11 | 12 | "github.com/dennis-tra/pcp/internal/log" 13 | "github.com/dennis-tra/pcp/pkg/config" 14 | ) 15 | 16 | // Command holds the `send` subcommand configuration. 17 | var Command = &cli.Command{ 18 | Name: "send", 19 | Usage: "make the given file available to your peer", 20 | Aliases: []string{"s"}, 21 | Action: Action, 22 | Flags: []cli.Flag{ 23 | &cli.IntFlag{ 24 | Name: "w", 25 | Aliases: []string{"word-count"}, 26 | Usage: "the number of random words to use (min 3)", 27 | EnvVars: []string{"PCP_WORD_COUNT"}, 28 | Value: 4, 29 | }, 30 | }, 31 | ArgsUsage: `FILE`, 32 | Description: ` 33 | The send subcommand generates four random words based on the first 34 | bytes of a newly generated peer identity. The first word and the 35 | current time are used to generate an identifier that is broadcasted 36 | in your local network via mDNS and provided through the distributed 37 | hash table of the IPFS network. 38 | 39 | After a peer attempts to connect it starts a password authen- 40 | ticated key exchange (PAKE) with the remaining three words to 41 | proof that the peer is in possession of the password. While this 42 | is happening the tool still searches for other peers as the 43 | currently connected one could fail the authentication procedure. 44 | 45 | After the authentication was successful and the peer confirmed 46 | the file transfer the transmission is started. 47 | `, 48 | } 49 | 50 | // Action contains the logic for the send subcommand of the pcp program. It is 51 | // mainly responsible for input parsing and service initialisation. 52 | func Action(c *cli.Context) error { 53 | // Read config file and fill context with it. 54 | c, err := config.FillContext(c) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | // Try to open the file to check if we have access and fail early. 60 | filepath := c.Args().First() 61 | if err = validateFile(filepath); err != nil { 62 | return err 63 | } 64 | 65 | log.Debugln("Validating given word count:", c.Int("w")) 66 | if c.Int("w") < 3 && !c.Bool("homebrew") { 67 | return fmt.Errorf("the number of words must not be less than 3") 68 | } 69 | 70 | // Generate the random words 71 | _, wrds, err := words.Random("english", c.Int("w")) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // If homebrew flag is set, overwrite generated words with well known list 77 | if c.Bool("homebrew") { 78 | wrds = words.HomebrewList() 79 | } 80 | 81 | // Initialize node 82 | local, err := InitNode(c, filepath, wrds) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | // Broadcast the code to be found by peers. 88 | log.Infoln("Code is: ", strings.Join(local.Words, "-")) 89 | log.Infoln("On the other machine run:\n\tpcp receive", strings.Join(local.Words, "-")) 90 | 91 | local.StartAdvertising(c) 92 | 93 | // Wait for the user to stop the tool or the transfer to finish. 94 | select { 95 | case <-c.Done(): 96 | local.Shutdown() 97 | return nil 98 | case <-local.SigDone(): 99 | return nil 100 | } 101 | } 102 | 103 | // validateFile tries to open the file at the given path to check 104 | // if we have the correct permissions to read it. Further, it 105 | // checks whether the filepath represents a directory. This is 106 | // currently not supported. 107 | func validateFile(filepath string) error { 108 | log.Debugln("Validating given file:", filepath) 109 | 110 | if filepath == "" { 111 | return fmt.Errorf("please specify the file you want to transfer") 112 | } 113 | 114 | f, err := os.Open(filepath) 115 | if err != nil { 116 | return err 117 | } 118 | defer f.Close() 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pkg/send/node.go: -------------------------------------------------------------------------------- 1 | package send 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "sync" 9 | 10 | "github.com/libp2p/go-libp2p" 11 | "github.com/libp2p/go-libp2p-core/peer" 12 | "github.com/pkg/errors" 13 | "github.com/urfave/cli/v2" 14 | 15 | "github.com/dennis-tra/pcp/internal/log" 16 | "github.com/dennis-tra/pcp/pkg/dht" 17 | "github.com/dennis-tra/pcp/pkg/mdns" 18 | pcpnode "github.com/dennis-tra/pcp/pkg/node" 19 | ) 20 | 21 | // Node encapsulates the logic of advertising and transmitting 22 | // a particular file to a peer. 23 | type Node struct { 24 | *pcpnode.Node 25 | 26 | advertisers []Advertiser 27 | 28 | authPeers *sync.Map 29 | filepath string 30 | } 31 | 32 | type Advertiser interface { 33 | Advertise(chanID int) error 34 | Shutdown() 35 | } 36 | 37 | // InitNode returns a fully configured node ready to start 38 | // advertising that we want to send a specific file. 39 | func InitNode(c *cli.Context, filepath string, words []string) (*Node, error) { 40 | h, err := pcpnode.New(c, words, libp2p.EnableAutoRelay()) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | node := &Node{ 46 | Node: h, 47 | advertisers: []Advertiser{}, 48 | authPeers: &sync.Map{}, 49 | filepath: filepath, 50 | } 51 | 52 | node.RegisterKeyExchangeHandler(node) 53 | 54 | return node, nil 55 | } 56 | 57 | func (n *Node) Shutdown() { 58 | n.StopAdvertising() 59 | n.UnregisterKeyExchangeHandler() 60 | n.Node.Shutdown() 61 | } 62 | 63 | // StartAdvertising asynchronously advertises the given code through the means of all 64 | // registered advertisers. Currently these are multicast DNS and DHT. 65 | func (n *Node) StartAdvertising(c *cli.Context) { 66 | n.SetState(pcpnode.Advertising) 67 | 68 | if c.Bool("mdns") == c.Bool("dht") { 69 | n.advertisers = []Advertiser{ 70 | dht.NewAdvertiser(n, n.DHT), 71 | mdns.NewAdvertiser(n.Node), 72 | } 73 | } else if c.Bool("mdns") { 74 | n.advertisers = []Advertiser{ 75 | mdns.NewAdvertiser(n.Node), 76 | } 77 | } else if c.Bool("dht") { 78 | n.advertisers = []Advertiser{ 79 | dht.NewAdvertiser(n, n.DHT), 80 | } 81 | } 82 | 83 | for _, advertiser := range n.advertisers { 84 | go func(a Advertiser) { 85 | err := a.Advertise(n.ChanID) 86 | if err == nil { 87 | return 88 | } 89 | 90 | // If the user is connected to another peer 91 | // we don't care about discover errors. 92 | if n.GetState() == pcpnode.Connected { 93 | return 94 | } 95 | 96 | switch e := err.(type) { 97 | case dht.ErrConnThresholdNotReached: 98 | e.Log() 99 | default: 100 | log.Warningln(err) 101 | } 102 | }(advertiser) 103 | } 104 | } 105 | 106 | func (n *Node) StopAdvertising() { 107 | var wg sync.WaitGroup 108 | for _, advertiser := range n.advertisers { 109 | wg.Add(1) 110 | go func(a Advertiser) { 111 | a.Shutdown() 112 | wg.Done() 113 | }(advertiser) 114 | } 115 | wg.Wait() 116 | } 117 | 118 | func (n *Node) HandleSuccessfulKeyExchange(peerID peer.ID) { 119 | // We're authenticated so can initiate a transfer 120 | if n.GetState() == pcpnode.Connected { 121 | log.Debugln("already connected and authenticated with another node") 122 | return 123 | } 124 | n.SetState(pcpnode.Connected) 125 | 126 | n.UnregisterKeyExchangeHandler() 127 | go n.StopAdvertising() 128 | 129 | err := n.Transfer(peerID) 130 | if err != nil { 131 | log.Warningln("Error transferring file:", err) 132 | } 133 | 134 | n.Shutdown() 135 | } 136 | 137 | func (n *Node) Transfer(peerID peer.ID) error { 138 | filename := path.Base(n.filepath) 139 | size, err := totalSize(n.filepath) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | log.Infof("Asking for confirmation... ") 145 | accepted, err := n.SendPushRequest(n.ServiceContext(), peerID, filename, size, false) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | if !accepted { 151 | log.Infoln("Rejected!") 152 | return fmt.Errorf("rejected file transfer") 153 | } 154 | log.Infoln("Accepted!") 155 | 156 | if err = n.Node.Transfer(n.ServiceContext(), peerID, n.filepath); err != nil { 157 | return errors.Wrap(err, "could not transfer file to peer") 158 | } 159 | 160 | log.Infoln("Successfully sent file/directory!") 161 | return nil 162 | } 163 | 164 | func totalSize(path string) (int64, error) { 165 | // TODO: Add file count 166 | var size int64 167 | err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { 168 | if err != nil { 169 | return err 170 | } 171 | if info.IsDir() { 172 | return nil 173 | } 174 | size += info.Size() 175 | return nil 176 | }) 177 | return size, err 178 | } 179 | -------------------------------------------------------------------------------- /pkg/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/dennis-tra/pcp/internal/log" 10 | ) 11 | 12 | // State represents the lifecycle states of a service. 13 | type State uint8 14 | 15 | const ( 16 | // These are the concrete lifecycle manifestations. 17 | Idle State = iota 18 | Started 19 | Stopping 20 | Stopped 21 | ) 22 | 23 | // ErrServiceAlreadyStarted is returned if there are multiple calls to ServiceStarted. 24 | // If this happens somethings wrong :/ 25 | var ErrServiceAlreadyStarted = errors.New("the service was already started in the past") 26 | 27 | // Service represents an entity that runs in a 28 | // separate go routine and where its lifecycle 29 | // needs to be handled externally. 30 | type Service struct { 31 | // The name of the service for logging purposes 32 | name string 33 | 34 | // A context that can be used for long running 35 | // io operations of the service. This context 36 | // gets cancelled when the service receives a 37 | // shutdown signal. It's controversial to store 38 | // a context in a struct field but I believe 39 | // that it makes sense here. 40 | ctx context.Context 41 | cancel context.CancelFunc 42 | 43 | // The current state of this service. 44 | lk sync.RWMutex 45 | state State 46 | 47 | // When a message is sent to this channel it 48 | // starts to gracefully shut down. 49 | shutdown chan struct{} 50 | 51 | // When a message is sent to this channel 52 | // the service has shut down. 53 | done chan struct{} 54 | } 55 | 56 | // New instantiates an initialised Service struct. It 57 | // deliberately does not accept a context as an input 58 | // parameter as I consider long running service life- 59 | // cycle handling with contexts as a bad practice. 60 | // Contexts belong in request/response paths and 61 | // Services should be handled via channels. 62 | func New(name string) *Service { 63 | ctx, cancel := context.WithCancel(context.Background()) 64 | return &Service{ 65 | ctx: ctx, 66 | name: name, 67 | cancel: cancel, 68 | state: Idle, 69 | shutdown: make(chan struct{}), 70 | done: make(chan struct{}), 71 | } 72 | } 73 | 74 | // ServiceStarted marks this service as started. 75 | func (s *Service) ServiceStarted() error { 76 | log.Debugln(s.name, "- Service has started") 77 | 78 | s.lk.Lock() 79 | defer s.lk.Unlock() 80 | 81 | if s.state != Idle { 82 | return ErrServiceAlreadyStarted 83 | } 84 | s.state = Started 85 | 86 | go func() { 87 | select { 88 | case <-s.shutdown: 89 | case <-s.done: 90 | } 91 | s.cancel() 92 | }() 93 | 94 | return nil 95 | } 96 | 97 | // SigShutdown exposes the shutdown channel to listen for 98 | // shutdown instructions. 99 | func (s *Service) SigShutdown() chan struct{} { 100 | return s.shutdown 101 | } 102 | 103 | // SigDone exposes the done channel to listen for 104 | // service termination. 105 | func (s *Service) SigDone() chan struct{} { 106 | return s.done 107 | } 108 | 109 | // ServiceStopped marks this service as stopped and 110 | // ultimately releases an external call to Shutdown. 111 | func (s *Service) ServiceStopped() { 112 | s.lk.Lock() 113 | defer s.lk.Unlock() 114 | 115 | if s.state == Idle || s.state == Stopped { 116 | return 117 | } 118 | s.state = Stopped 119 | 120 | close(s.done) 121 | log.Debugln(s.name, "- Service has stopped") 122 | } 123 | 124 | // ServiceContext returns the context associated with this 125 | // service. This context is passed into requests or similar 126 | // that are initiated from this service. Doing it this way 127 | // we can cancel this contexts when someone shuts down 128 | // the service, which results in all requests being stopped. 129 | func (s *Service) ServiceContext() context.Context { 130 | return s.ctx 131 | } 132 | 133 | // Shutdown instructs the service to gracefully shut down. 134 | // This function blocks until the done channel was closed 135 | // which happens when ServiceStopped is called. 136 | func (s *Service) Shutdown() { 137 | log.Debugln(s.name, "- Service shutting down...") 138 | 139 | s.lk.Lock() 140 | if s.state != Started { 141 | s.lk.Unlock() 142 | return 143 | } 144 | s.state = Stopping 145 | s.lk.Unlock() 146 | 147 | close(s.shutdown) 148 | <-s.done 149 | log.Debugln(s.name, "- Service was shut down") 150 | } 151 | -------------------------------------------------------------------------------- /pkg/service/service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNewService_lifecycle(t *testing.T) { 12 | s := New("test") 13 | 14 | s.ServiceStopped() 15 | err := s.ServiceStarted() 16 | require.NoError(t, err) 17 | s.ServiceStopped() 18 | s.ServiceStopped() 19 | } 20 | 21 | func TestNewService_shutdown(t *testing.T) { 22 | s := New("test") 23 | 24 | err := s.ServiceStarted() 25 | require.NoError(t, err) 26 | 27 | var wg sync.WaitGroup 28 | wg.Add(1) 29 | go func() { 30 | s.Shutdown() 31 | wg.Done() 32 | }() 33 | go s.ServiceStopped() 34 | wg.Wait() 35 | } 36 | 37 | func TestNewService_contexts_stopped(t *testing.T) { 38 | s := New("test") 39 | err := s.ServiceStarted() 40 | require.NoError(t, err) 41 | 42 | var wg sync.WaitGroup 43 | for i := 0; i < 100; i++ { 44 | wg.Add(1) 45 | go func() { 46 | <-s.ServiceContext().Done() 47 | wg.Done() 48 | }() 49 | } 50 | go s.ServiceStopped() 51 | wg.Wait() 52 | } 53 | 54 | func TestNewService_contexts_shutdown(t *testing.T) { 55 | s := New("test") 56 | err := s.ServiceStarted() 57 | require.NoError(t, err) 58 | 59 | var wg sync.WaitGroup 60 | for i := 0; i < 100; i++ { 61 | wg.Add(1) 62 | go func() { 63 | <-s.ServiceContext().Done() 64 | wg.Done() 65 | }() 66 | } 67 | go s.Shutdown() 68 | wg.Wait() 69 | } 70 | 71 | func TestNewService_restart(t *testing.T) { 72 | s := New("test") 73 | err := s.ServiceStarted() 74 | require.NoError(t, err) 75 | 76 | var wg sync.WaitGroup 77 | for i := 0; i < 100; i++ { 78 | wg.Add(1) 79 | go func() { 80 | <-s.ServiceContext().Done() 81 | wg.Done() 82 | }() 83 | } 84 | wg.Add(1) 85 | go func() { 86 | s.Shutdown() 87 | wg.Done() 88 | }() 89 | go s.ServiceStopped() 90 | wg.Wait() 91 | 92 | err = s.ServiceStarted() 93 | require.Error(t, err) 94 | assert.Equal(t, ErrServiceAlreadyStarted, err) 95 | } 96 | 97 | func TestService_SigDone(t *testing.T) { 98 | s := New("test") 99 | err := s.ServiceStarted() 100 | require.NoError(t, err) 101 | s.ServiceStopped() 102 | <-s.SigDone() 103 | } 104 | 105 | func TestService_SigShutdown(t *testing.T) { 106 | s := New("test") 107 | err := s.ServiceStarted() 108 | require.NoError(t, err) 109 | go s.Shutdown() 110 | <-s.SigShutdown() 111 | } 112 | -------------------------------------------------------------------------------- /pkg/words/words.go: -------------------------------------------------------------------------------- 1 | package words 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/tyler-smith/go-bip39/wordlists" 10 | ) 11 | 12 | type Language string 13 | 14 | const ( 15 | English Language = "english" 16 | ChineseSimplified = "chinese_simplified" 17 | ChineseTraditional = "chinese_traditional" 18 | Czech = "czech" 19 | French = "french" 20 | Italian = "italian" 21 | Japanese = "japanese" 22 | Korean = "korean" 23 | Spanish = "spanish" 24 | ) 25 | 26 | var Lists = map[Language][]string{ 27 | English: wordlists.English, 28 | ChineseSimplified: wordlists.ChineseSimplified, 29 | ChineseTraditional: wordlists.ChineseTraditional, 30 | Czech: wordlists.Czech, 31 | French: wordlists.French, 32 | Italian: wordlists.Italian, 33 | Japanese: wordlists.Japanese, 34 | Korean: wordlists.Korean, 35 | Spanish: wordlists.Spanish, 36 | } 37 | 38 | var ErrUnsupportedLanguage = errors.New("unsupported language") 39 | 40 | // Random returns a slice of random words and their respective 41 | // integer values from the BIP39 wordlist of that given language. 42 | func Random(lang string, count int) ([]int, []string, error) { 43 | wordList, err := wordsForLang(lang) 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | words := make([]string, count) 48 | ints := make([]int, count) 49 | for i := 0; i < count; i++ { 50 | rint, err := rand.Int(rand.Reader, big.NewInt(int64(len(wordList)))) 51 | if err != nil { 52 | return nil, nil, err 53 | } 54 | words[i] = wordList[rint.Int64()] 55 | ints[i] = int(rint.Int64()) 56 | } 57 | return ints, words, nil 58 | } 59 | 60 | func ToInts(words []string) ([]int, error) { 61 | var ints []int 62 | ListLoop: 63 | for _, wordList := range Lists { 64 | ints = []int{} 65 | for _, word := range words { 66 | idx := wordInList(word, wordList) 67 | if idx == -1 { 68 | continue ListLoop 69 | } 70 | ints = append(ints, idx) 71 | } 72 | return ints, nil 73 | } 74 | return nil, fmt.Errorf("could not find all words in a single wordlist") 75 | } 76 | 77 | // HomebrewList returns a hard coded list of words, so that a full functional 78 | // test can be carried out after a homebrew installation. 79 | func HomebrewList() []string { 80 | return []string{ 81 | wordlists.English[0], 82 | wordlists.English[0], 83 | wordlists.English[0], 84 | wordlists.English[0], 85 | } 86 | } 87 | 88 | // Tried sort.SearchStrings 89 | func wordInList(word string, list []string) int { 90 | for i, w := range list { 91 | if w == word { 92 | return i 93 | } 94 | } 95 | return -1 96 | } 97 | 98 | func wordsForLang(lang string) ([]string, error) { 99 | switch Language(lang) { 100 | case English: 101 | return wordlists.English, nil 102 | case ChineseSimplified: 103 | return wordlists.ChineseSimplified, nil 104 | case ChineseTraditional: 105 | return wordlists.ChineseTraditional, nil 106 | case Czech: 107 | return wordlists.Czech, nil 108 | case French: 109 | return wordlists.French, nil 110 | case Italian: 111 | return wordlists.Italian, nil 112 | case Japanese: 113 | return wordlists.Japanese, nil 114 | case Korean: 115 | return wordlists.Korean, nil 116 | case Spanish: 117 | return wordlists.Spanish, nil 118 | default: 119 | return nil, ErrUnsupportedLanguage 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /pkg/words/words_test.go: -------------------------------------------------------------------------------- 1 | package words 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestToInts(t *testing.T) { 15 | testData := []struct { 16 | words []string 17 | err bool 18 | expected []int 19 | }{ 20 | {words: []string{}, expected: []int{}}, 21 | {words: []string{"abandon", "access"}, expected: []int{0, 10}}, 22 | {words: []string{"not found"}, err: true}, 23 | {words: []string{"access", "not found"}, err: true}, 24 | {words: []string{"abriter", "abaisser"}, expected: []int{10, 0}}, 25 | {words: []string{"access", "abaisser"}, err: true}, 26 | {words: []string{"orizzonte", "bipolare", "perforare", "tacciare"}, expected: []int{1174, 235, 1258, 1805}}, 27 | } 28 | 29 | for _, tt := range testData { 30 | t.Run(strings.Join(tt.words, "-"), func(t *testing.T) { 31 | res, err := ToInts(tt.words) 32 | if tt.err { 33 | require.Error(t, err) 34 | } else { 35 | require.NoError(t, err) 36 | assert.Equal(t, tt.expected, res) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestRandomToInts(t *testing.T) { 43 | langs := make([]Language, len(Lists)) 44 | i := 0 45 | for lang := range Lists { 46 | langs[i] = lang 47 | i++ 48 | } 49 | rand.Seed(time.Now().Unix()) 50 | for i := 0; i < 100; i++ { 51 | rlang := rand.Intn(len(langs)) 52 | rcount := 10 - rand.Intn(7) 53 | expInts, words, err := Random(string(langs[rlang]), rcount) 54 | errMsg := fmt.Sprintf("failed with lang %s and %d words", string(langs[rlang]), rcount) 55 | require.NoError(t, err, errMsg) 56 | actInts, err := ToInts(words) 57 | require.NoError(t, err, errMsg) 58 | assert.Equal(t, expInts, actInts, errMsg) 59 | } 60 | } 61 | 62 | func TestRandom_UnsupportedLanguage(t *testing.T) { 63 | _, _, err := Random("unsupported", 5) 64 | require.Error(t, err) 65 | assert.Equal(t, ErrUnsupportedLanguage, err) 66 | } 67 | -------------------------------------------------------------------------------- /test/transfer_dir/file: -------------------------------------------------------------------------------- 1 | CONTENT 2 | -------------------------------------------------------------------------------- /test/transfer_dir_empty/file: -------------------------------------------------------------------------------- 1 | CONTENT 2 | -------------------------------------------------------------------------------- /test/transfer_file/file: -------------------------------------------------------------------------------- 1 | CONTENT 2 | -------------------------------------------------------------------------------- /test/transfer_file_subdir/subdir/file: -------------------------------------------------------------------------------- 1 | CONTENT 2 | -------------------------------------------------------------------------------- /test/transfer_link/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-tra/pcp/c7a005be81126a26c96c13409d3dfaba73f00ae4/test/transfer_link/file -------------------------------------------------------------------------------- /test/transfer_link/subdir/file: -------------------------------------------------------------------------------- 1 | ../file -------------------------------------------------------------------------------- /test/transfer_subdir/subdir/file: -------------------------------------------------------------------------------- 1 | CONTENT 2 | --------------------------------------------------------------------------------