├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── generated-pr.yml │ ├── release.yml │ ├── stale.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── blocker.go ├── blocksdb.go ├── cmd ├── httpsubs │ └── main.go └── nopfs │ └── main.go ├── denylist.go ├── entry.go ├── go.mod ├── go.sum ├── ipfs ├── README.md ├── blockservice.go ├── go.mod ├── go.sum ├── ipfs.go ├── namesys.go └── resolver.go ├── logo.png ├── logo.xcf ├── nopfs-kubo-plugin ├── Makefile ├── README.md ├── go.mod ├── go.sum └── plugin.go ├── nopfs.go ├── nopfs_test.go ├── status.go ├── subscription.go ├── subscription_test.go ├── tester ├── README.md ├── test.deny └── tester.go └── util.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '20 2 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/generated-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Generated PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Kubo plugin 2 | 3 | on: 4 | push: 5 | 6 | 7 | env: 8 | # must be same as official ipfs builds. See distributions/.tool-versions 9 | GO_VERSION: "1.23.3" 10 | 11 | jobs: 12 | build-artifacts: 13 | name: "Build" 14 | strategy: 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 2 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ env.GO_VERSION }} 28 | - name: Extract release name from tag 29 | run: | 30 | RELEASE=$(basename ${{ github.ref_name }}) 31 | echo "RELEASE=$RELEASE" >> "$GITHUB_ENV" 32 | - name: Package plugin 33 | run: make dist-plugin 34 | - name: Record Go environment 35 | run: | 36 | echo "GOHOSTOS=$(go env GOHOSTOS)" >> "$GITHUB_ENV" 37 | echo "GOHOSTARCH=$(go env GOHOSTARCH)" >> "$GITHUB_ENV" 38 | - name: Rename package 39 | run: | 40 | NAME="nopfs-kubo-plugin_${{ env.RELEASE }}_${{ env.GOHOSTOS }}_${{ env.GOHOSTARCH }}.tar.gz" 41 | mv nopfs-kubo-plugin/nopfs-kubo-plugin.tar.gz "$NAME" 42 | echo "ARTIFACT_NAME=$NAME" >> "$GITHUB_ENV" 43 | - name: Archive artifacts 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: ${{ env.ARTIFACT_NAME }} 47 | path: nopfs-kubo-plugin_*.tar.gz 48 | 49 | test-artifacts: 50 | name: "Test" 51 | needs: build-artifacts 52 | strategy: 53 | matrix: 54 | os: 55 | - ubuntu-latest 56 | # macos test fail with dist-built ipfs. 57 | # - macos-latest 58 | runs-on: ${{ matrix.os }} 59 | steps: 60 | - uses: actions/checkout@v4 61 | with: 62 | fetch-depth: 2 63 | - name: Set up Go 64 | uses: actions/setup-go@v5 65 | with: 66 | go-version: ${{ env.GO_VERSION }} 67 | - name: Record variables 68 | run: | 69 | V=`cat nopfs-kubo-plugin/go.mod | grep github.com/ipfs/kubo | grep -o 'v.*'` 70 | echo "KUBO_VERSION=$V" >> "$GITHUB_ENV" 71 | GOHOSTOS=`go env GOHOSTOS` 72 | echo "GOHOSTOS=$GOHOSTOS" >> "$GITHUB_ENV" 73 | RELEASE=$(basename ${{ github.ref_name }}) 74 | echo "RELEASE=$RELEASE" >> "$GITHUB_ENV" 75 | echo PLUGIN_ARTIFACT=nopfs-kubo-plugin_${RELEASE}_${GOHOSTOS}_amd64.tar.gz >> "$GITHUB_ENV" 76 | - name: Download artifact 77 | id: download 78 | uses: actions/download-artifact@v4 79 | with: 80 | name: ${{ env.PLUGIN_ARTIFACT }} 81 | - name: Download unpack Kubo 82 | run: | 83 | wget -nv https://dist.ipfs.tech/kubo/${{ env.KUBO_VERSION }}/kubo_${{ env.KUBO_VERSION }}_${{ env.GOHOSTOS }}-amd64.tar.gz 84 | tar -xf kubo_${{ env.KUBO_VERSION }}_${{ env.GOHOSTOS }}-amd64.tar.gz 85 | tar -xf nopfs-kubo-plugin_*.tar.gz 86 | chmod +x kubo/ipfs 87 | - name: Initialize IPFS and copy plugin 88 | run: | 89 | export IPFS_PATH=$(pwd)/ipfs-config 90 | echo "IPFS_PATH=$IPFS_PATH" >> "$GITHUB_ENV" 91 | ./kubo/ipfs init 92 | mkdir -p ipfs-config/plugins 93 | cp nopfs-kubo-plugin/nopfs-kubo-plugin ipfs-config/plugins/ 94 | - name: Check IPFS works with the plugin 95 | run: ./kubo/ipfs version --all 96 | 97 | release: 98 | name: "Release" 99 | needs: test-artifacts 100 | runs-on: ubuntu-latest 101 | steps: 102 | - name: Download artifacts 103 | id: download 104 | uses: actions/download-artifact@v4 105 | - name: Extract release name from tag 106 | run: | 107 | RELEASE=$(basename ${{ github.ref_name }}) 108 | echo "RELEASE=$RELEASE" >> "$GITHUB_ENV" 109 | - name: Release 110 | uses: softprops/action-gh-release@v2 111 | if: startsWith(github.ref, 'refs/tags/nopfs-kubo-plugin/v') 112 | with: 113 | files: nopfs-kubo-plugin_*.tar.gz/* 114 | body: | 115 | This is a binary build of the NOpfs Kubo plugin targeting Kubo version ${{ env.RELEASE }}. 116 | 117 | To install, download the relevant asset for your platform, unpack the plugin file (`nopfs-kubo-plugin`) and place it in `~/.ipfs/plugins`. MacOS users will need to compile Kubo themselves, as the official releases have no CGO support. 118 | 119 | See the included README.md for more information. 120 | name: ${{ github.ref_name }} 121 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master, dependency-upgrades ] 8 | 9 | env: 10 | GO_VERSION: "1.23.3" 11 | 12 | jobs: 13 | tests: 14 | name: "Compact denylist format test suite" 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 2 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ env.GO_VERSION }} 25 | 26 | - name: Tests 27 | run: go test -v -timeout 15m -coverprofile=coverage.txt -covermode=atomic . 28 | 29 | - name: Coverage 30 | uses: codecov/codecov-action@v4 31 | with: 32 | directory: nopfs 33 | 34 | check: 35 | name: "Static, syntax and spelling checks" 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 2 41 | 42 | - name: Set up Go 43 | uses: actions/setup-go@v5 44 | with: 45 | go-version: ${{ env.GO_VERSION }} 46 | 47 | - name: Install staticcheck 48 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 49 | 50 | - name: Install misspell 51 | run: go install github.com/client9/misspell/cmd/misspell@latest 52 | 53 | - name: Check 54 | run: make check 55 | 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nopfs-kubo-plugin/nopfs-kubo-plugin 2 | nopfs-kubo-plugin/*.tar.gz 3 | nopfs-kubo-plugin/dist 4 | cmd/nopfs/nopfs 5 | 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | -------------------------------------------------------------------------------- /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 | plugin: 2 | $(MAKE) -C nopfs-kubo-plugin all 3 | 4 | install-plugin: 5 | $(MAKE) -C nopfs-kubo-plugin install 6 | 7 | dist-plugin: 8 | $(MAKE) -C nopfs-kubo-plugin dist 9 | 10 | check: 11 | go vet ./... 12 | staticcheck --checks all ./... 13 | misspell -error -locale US . 14 | 15 | .PHONY: plugin install-plugin check 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOpfs! 2 | 3 | NOPfs helps IPFS to say No! 4 | 5 |

6 | ipfs-lite 7 |

8 | 9 | NOPfs is an implementation of 10 | [IPIP-383](https://github.com/ipfs/specs/pull/383) which add supports for 11 | content blocking to the go-ipfs stack and particularly to Kubo. 12 | 13 | ## Content-blocking in Kubo 14 | 15 | 1. Grab a plugin release from the [releases](https://github.com/ipfs-shipyard/nopfs/releases) section matching your Kubo version and install the plugin file in `~/.ipfs/plugins`. 16 | 2. Write a custom denylist file (see [syntax](#denylist-syntax) below) or simply download one of the supported denylists from [Denyli.st](https://denyli.st) and place them in `~/.config/ipfs/denylists/` (ensure `.deny` extension). 17 | 3. Start Kubo (`ipfs daemon`). The plugin should be loaded automatically and existing denylists tracked for updates from that point (no restarts required). 18 | 19 | ## Denylist syntax 20 | 21 | Denylist files must have the `.deny` extension. The content consists of an optional header and a body made of blocking rules as follows: 22 | 23 | 24 | ``` 25 | version: 1 26 | name: IPFSorp blocking list 27 | description: A collection of bad things we have found in the universe 28 | author: abuse-ipfscorp@example.com 29 | hints: 30 | gateway_status: 410 31 | double_hash_fn: sha256 32 | double_hash_enc: hex 33 | --- 34 | # Blocking by CID - blocks wrapped multihash. 35 | # Does not block subpaths. 36 | /ipfs/bafybeihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq 37 | 38 | # Block all subpaths 39 | /ipfs/QmdWFA9FL52hx3j9EJZPQP1ZUH8Ygi5tLCX2cRDs6knSf8/* 40 | 41 | # Block some subpaths (equivalent rules) 42 | /ipfs/Qmah2YDTfrox4watLCr3YgKyBwvjq8FJZEFdWY6WtJ3Xt2/test* 43 | /ipfs/QmTuvSQbEDR3sarFAN9kAeXBpiBCyYYNxdxciazBba11eC/test/* 44 | 45 | # Block some subpaths with exceptions 46 | /ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked* 47 | +/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blockednot 48 | +/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/not 49 | +/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/exceptions* 50 | 51 | # Block IPNS domain name 52 | /ipns/domain.example 53 | 54 | # Block IPNS domain name and path 55 | /ipns/domain2.example/path 56 | 57 | # Block IPNS key - blocks wrapped multihash. 58 | /ipns/k51qzi5uqu5dhmzyv3zac033i7rl9hkgczxyl81lwoukda2htteop7d3x0y1mf 59 | 60 | # Legacy CID double-hash block 61 | # sha256(bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e/) 62 | # blocks only this CID 63 | //d9d295bde21f422d471a90f2a37ec53049fdf3e5fa3ee2e8f20e10003da429e7 64 | 65 | # Legacy Path double-hash block 66 | # Blocks bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e/path 67 | # but not any other paths. 68 | //3f8b9febd851873b3774b937cce126910699ceac56e72e64b866f8e258d09572 69 | 70 | # Double hash CID block 71 | # base58btc-sha256-multihash(QmVTF1yEejXd9iMgoRTFDxBv7HAz9kuZcQNBzHrceuK9HR) 72 | # Blocks bafybeidjwik6im54nrpfg7osdvmx7zojl5oaxqel5cmsz46iuelwf5acja 73 | # and QmVTF1yEejXd9iMgoRTFDxBv7HAz9kuZcQNBzHrceuK9HR etc. by multihash 74 | //QmX9dhRcQcKUw3Ws8485T5a9dtjrSCQaUAHnG4iK9i4ceM 75 | 76 | # Double hash Path block using blake3 hashing 77 | # base58btc-blake3-multihash(gW7Nhu4HrfDtphEivm3Z9NNE7gpdh5Tga8g6JNZc1S8E47/path) 78 | # Blocks /ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path 79 | # /ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path 80 | # /ipfs/f01701e20903cf61d46521b05f926ba1634628d0bba8a7ffb5b6d5a3ca310682ca63b5ef0/path etc... 81 | # But not /path2 82 | //QmbK7LDv5NNBvYQzNfm2eED17SNLt1yNMapcUhSuNLgkqz 83 | ``` 84 | 85 | You can create double-hashes by hand with the following command: 86 | 87 | ``` 88 | printf "QmecDgNqCRirkc3Cjz9eoRBNwXGckJ9WvTdmY16HP88768/my/path" \ 89 | | ipfs add --raw-leaves --only-hash --quiet \ 90 | | ipfs cid format -f '%M' -b base58btc 91 | ``` 92 | 93 | where: 94 | - `QmecDgNqCRirkc3Cjz9eoRBNwXGckJ9WvTdmY16HP88768` must always be a 95 | CidV0. If you have a CIDv1 you need to convert it to CIDv0 first. i.e 96 | `ipfs cid format -v0 bafybeihrw75yfhdx5qsqgesdnxejtjybscwuclpusvxkuttep6h7pkgmze` 97 | - `/my/path` is optional depending on whether you want to block a specific path. No wildcards supported here! 98 | - The command above should give `QmSju6XPmYLG611rmK7rEeCMFVuL6EHpqyvmEU6oGx3GR8`. Use it as `//QmSju6XPmYLG611rmK7rEeCMFVuL6EHpqyvmEU6oGx3GR8` on the denylist. 99 | 100 | 101 | ## Kubo plugin 102 | 103 | NOpfs Kubo plugin pre-built binary releases are available in the 104 | [releases](https://github.com/ipfs-shipyard/nopfs/releases) section. 105 | 106 | Simply grab the binary for your system and drop it in the `~/.ipfs/plugins` folder. 107 | 108 | From that point, starting Kubo should load the plugin and automatically work with denylists (files with extension `.deny`) found in `/etc/ipfs/denylists` and `$XDG_CONFIG_HOME/ipfs/denylists` (usually `~/.config/ipfs/denylists`). The plugin will log some lines as the ipfs daemon starts: 109 | 110 | ``` 111 | $ ipfs daemon --offline 112 | Initializing daemon... 113 | Kubo version: 0.21.0-rc1 114 | Repo version: 14 115 | System version: amd64/linux 116 | Golang version: go1.19.10 117 | 2023-06-13T21:26:56.951+0200 INFO nopfs nopfs-kubo-plugin/plugin.go:59 Loading Nopfs plugin: content blocking 118 | 2023-06-13T21:26:56.952+0200 INFO nopfs nopfs@v0.0.7/denylist.go:165 Processing /home/user/.config/ipfs/denylists/badbits.deny: badbits (2023-03-27) by @Protocol Labs 119 | ``` 120 | 121 | The plugin can be manually built and installed for different versions of Kubo with: 122 | 123 | ``` 124 | git checkout nopfs-kubo-plugin/v 125 | make plugin 126 | make install-plugin 127 | ``` 128 | 129 | ## Project layout 130 | 131 | The NOpfs contains three separate Go-modules (versioned separately): 132 | 133 | * The main module (`github.com/ipfs-shipyard/nopfs`) provides the implementation of a Blocker that works with IPIP-383 denylists (can parse, track and answer whether CIDs/paths are blocked) 134 | * The ipfs submodule (`github.com/ipfs-shipyard/nopfs/ipfs`) provides blocking-wrappers for types in the Boxo/stack (Resolver, BlockService etc.). It's versioning tracks Boxo tags. i.e. v0.10.0 is compatible with boxo@v0.10.0. 135 | * The nopfs-kubo-plugin submodule (`github.com/ipfs-shipyard/nopfs/nopfs-kubo-plugin`) contains only the code of the Kubo plugin, which injects blocking-wrappers into Kubo. It is tagged tracking Kubo releases. 136 | 137 | This allows using the Blocker separately, or relying on blocking-wrappers separately in a way that it is easy to identify and select dependency-aligned versions with your project, without specifying more dependencies that needed. 138 | 139 | ## Project status 140 | 141 | - [x] Support for blocking CIDs 142 | - [x] Support for blocking IPFS Paths 143 | - [x] Support for paths with wildcards (prefix paths) 144 | - [x] Support for blocking legacy [badbits anchors](https://badbits.dwebops.pub/denylist.json) 145 | - [x] Support for blocking double-hashed CIDs, IPFS paths, IPNS paths. 146 | - [x] Support for blocking prefix and non-prefix sub-path 147 | - [x] Support for denylist headers 148 | - [x] Support for denylist rule hints 149 | - [x] Support for allow rules (undo or create exceptions to earlier rules) 150 | - [x] Live processing of appended rules to denylists 151 | - [x] Content-blocking-enabled IPFS BlockService implementation 152 | - [x] Content-blocking-enabled IPFS NameSystem implementation 153 | - [x] Content-blocking-enabled IPFS Path resolver implementation 154 | - [x] Kubo plugin 155 | - [x] Automatic, comprehensive testing of all rule types and edge cases 156 | - [x] Work with a stable release of Kubo 157 | - [x] Prebuilt plugin binaries 158 | -------------------------------------------------------------------------------- /blocker.go: -------------------------------------------------------------------------------- 1 | package nopfs 2 | 3 | import ( 4 | "github.com/ipfs/boxo/path" 5 | "github.com/ipfs/go-cid" 6 | "go.uber.org/multierr" 7 | ) 8 | 9 | // A Blocker binds together multiple Denylists and can decide whether a path 10 | // or a CID is blocked. 11 | type Blocker struct { 12 | Denylists map[string]*Denylist 13 | } 14 | 15 | // NewBlocker creates a Blocker using the given denylist file paths. 16 | // For default denylist locations, you can use GetDenylistFiles(). 17 | // TODO: Options. 18 | func NewBlocker(files []string) (*Blocker, error) { 19 | 20 | blocker := Blocker{ 21 | Denylists: make(map[string]*Denylist), 22 | } 23 | 24 | var errors error 25 | for _, fname := range files { 26 | dl, err := NewDenylist(fname, true) 27 | if err != nil { 28 | errors = multierr.Append(errors, err) 29 | logger.Errorf("error opening and processing %s: %s", fname, err) 30 | 31 | continue 32 | } 33 | blocker.Denylists[fname] = dl 34 | } 35 | 36 | if n := len(multierr.Errors(errors)); n > 0 && n == len(files) { 37 | return nil, errors 38 | } 39 | return &blocker, nil 40 | } 41 | 42 | // Close stops all denylists from being processed and watched for updates. 43 | func (blocker *Blocker) Close() error { 44 | var err error 45 | for _, dl := range blocker.Denylists { 46 | err = multierr.Append(err, dl.Close()) 47 | } 48 | return err 49 | } 50 | 51 | // IsCidBlocked returns blocking status for a CID. A CID is blocked when a 52 | // Denylist reports it as blocked. A CID is not blocked when no denylist 53 | // reports it as blocked or it is explicitally allowed. Lookup stops as soon 54 | // as a defined "blocked" or "allowed" status is found. 55 | // 56 | // Lookup for "allowed" or "blocked" status happens in order of the denylist, 57 | // thus the denylist position during Blocker creation affects which one has 58 | // preference. 59 | // 60 | // Note that StatusResponse.Path will be unset. See Denylist.IsCidBlocked() 61 | // for more info. 62 | func (blocker *Blocker) IsCidBlocked(c cid.Cid) StatusResponse { 63 | for _, dl := range blocker.Denylists { 64 | resp := dl.IsCidBlocked(c) 65 | if resp.Status != StatusNotFound { 66 | return resp 67 | } 68 | } 69 | return StatusResponse{ 70 | Cid: c, 71 | Status: StatusNotFound, 72 | } 73 | } 74 | 75 | // IsPathBlocked returns blocking status for an IPFS Path. A Path is blocked 76 | // when a Denylist reports it as blocked. A Path is not blocked when no 77 | // denylist reports it as blocked or it is explicitally allowed. Lookup stops 78 | // as soon as a defined "blocked" or "allowed" status is found. 79 | // 80 | // Lookup for "allowed" or "blocked" status happens in order of the denylist, 81 | // thus the denylist position during Blocker creation affects which one has 82 | // preference. 83 | // 84 | // Note that StatusResponse.Cid will be unset. See Denylist.IsPathBlocked() 85 | // for more info. 86 | func (blocker *Blocker) IsPathBlocked(p path.Path) StatusResponse { 87 | for _, dl := range blocker.Denylists { 88 | resp := dl.IsPathBlocked(p) 89 | if resp.Status != StatusNotFound { 90 | return resp 91 | } 92 | } 93 | return StatusResponse{ 94 | Path: p, 95 | Status: StatusNotFound, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /blocksdb.go: -------------------------------------------------------------------------------- 1 | package nopfs 2 | 3 | import "sync" 4 | 5 | // BlocksDB is a key-value store of Entries. Keying may vary depending on 6 | // whether we are indexing IPNS names, CIDs etc. 7 | type BlocksDB struct { 8 | // TODO: this will eventually need to be replaced by a database with 9 | // its bloom filters etc. For a few million items this is just fine 10 | // though. 11 | blockDB sync.Map 12 | } 13 | 14 | // Load returns the Entries for a key. 15 | func (b *BlocksDB) Load(key string) (Entries, bool) { 16 | val, ok := b.blockDB.Load(key) 17 | if !ok { 18 | return nil, false 19 | } 20 | entries, ok := val.(Entries) 21 | if !ok { 22 | logger.Error("cannot convert to entries") 23 | return nil, false 24 | } 25 | return entries, true 26 | } 27 | 28 | // Store stores a new entry with the given key. If there are existing Entries, 29 | // the new Entry will be appended to them. 30 | func (b *BlocksDB) Store(key string, entry Entry) { 31 | entries, _ := b.Load(key) 32 | entries = append(entries, entry) 33 | b.blockDB.Store(key, entries) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/httpsubs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "time" 8 | 9 | "github.com/ipfs-shipyard/nopfs" 10 | ) 11 | 12 | func main() { 13 | if len(os.Args) != 3 { 14 | fmt.Fprintln(os.Stderr, "Usage: program ") 15 | os.Exit(1) 16 | } 17 | 18 | local := os.Args[1] 19 | remote := os.Args[2] 20 | 21 | fmt.Printf("%s: subscribed to %s. CTRL-C to stop\n", local, remote) 22 | 23 | subscriber, err := nopfs.NewHTTPSubscriber(remote, local, 1*time.Minute) 24 | if err != nil { 25 | fmt.Fprintln(os.Stderr, err) 26 | os.Exit(1) 27 | } 28 | 29 | c := make(chan os.Signal, 1) 30 | signal.Notify(c, os.Interrupt) 31 | <-c 32 | fmt.Println("Stopping") 33 | subscriber.Stop() 34 | } 35 | -------------------------------------------------------------------------------- /cmd/nopfs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "strings" 9 | "syscall" 10 | 11 | "github.com/ipfs-shipyard/nopfs" 12 | "github.com/ipfs/boxo/path" 13 | "github.com/ipfs/go-cid" 14 | logging "github.com/ipfs/go-log/v2" 15 | ) 16 | 17 | func printPrompt() { 18 | fmt.Print("> ") 19 | } 20 | 21 | func printUsage() { 22 | fmt.Println("Usage:") 23 | fmt.Println("> c ") 24 | fmt.Println("> p ") 25 | } 26 | 27 | func main() { 28 | logging.SetLogLevel("nopfs", "ERROR") 29 | filename := "test.deny" 30 | if len(os.Args) < 2 || os.Args[1] == "-h" || os.Args[1] == "--help" { 31 | fmt.Println("nopfs: denylist testing REPL") 32 | fmt.Println() 33 | fmt.Println("Usage: ./nopfs list.deny") 34 | return 35 | } 36 | filename = os.Args[1] 37 | blocker, err := nopfs.NewBlocker([]string{filename}) 38 | if err != nil { 39 | fmt.Println(err) 40 | return 41 | } 42 | 43 | sigCh := make(chan os.Signal, 1) 44 | signal.Notify(sigCh, syscall.SIGINT) 45 | 46 | reader := bufio.NewScanner(os.Stdin) 47 | printUsage() 48 | fmt.Println("[CTRL-D to exit]") 49 | printPrompt() 50 | for reader.Scan() { 51 | select { 52 | case <-sigCh: 53 | fmt.Println(blocker.Close()) 54 | return 55 | default: 56 | } 57 | 58 | text := reader.Text() 59 | typ, elem, found := strings.Cut(text, " ") 60 | if !found { 61 | fmt.Println("not found") 62 | printPrompt() 63 | continue 64 | } 65 | switch typ { 66 | case "p": 67 | p, err := path.NewPath(elem) 68 | if err != nil { 69 | fmt.Printf("error parsing path: %s\n", err) 70 | } else { 71 | status := blocker.IsPathBlocked(p) 72 | fmt.Printf("%s: %s\n", status.Status, status.Entry) 73 | } 74 | case "c": 75 | c, err := cid.Decode(elem) 76 | if err != nil { 77 | fmt.Println(err) 78 | } else { 79 | status := blocker.IsCidBlocked(c) 80 | fmt.Println(status) 81 | } 82 | default: 83 | fmt.Println("Usage:") 84 | fmt.Println("> c ") 85 | fmt.Println("> p ") 86 | } 87 | printPrompt() 88 | 89 | } 90 | fmt.Println() 91 | fmt.Println(blocker.Close()) 92 | } 93 | -------------------------------------------------------------------------------- /denylist.go: -------------------------------------------------------------------------------- 1 | package nopfs 2 | 3 | import ( 4 | "bufio" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/fsnotify/fsnotify" 14 | "github.com/ipfs/boxo/path" 15 | "github.com/ipfs/go-cid" 16 | "github.com/multiformats/go-multibase" 17 | "github.com/multiformats/go-multicodec" 18 | "github.com/multiformats/go-multihash" 19 | mhreg "github.com/multiformats/go-multihash/core" 20 | "go.uber.org/multierr" 21 | "gopkg.in/yaml.v3" 22 | ) 23 | 24 | // ErrHeaderNotFound is returned when no header can be Decoded. 25 | var ErrHeaderNotFound = errors.New("header not found") 26 | 27 | const maxHeaderSize = 1 << 20 // 1MiB per the spec 28 | const maxLineSize = 2 << 20 // 2MiB per the spec 29 | const currentVersion = 1 30 | 31 | // SafeCids is a map of known, innoffensive CIDs that correspond to 32 | // empty-blocks or empty-directories. Blocking these can break applications so 33 | // they are ignored (with a warning), when they appear on a denylist. 34 | var SafeCids = map[cid.Cid]string{ 35 | cid.MustParse("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"): "empty unixfs directory", 36 | cid.MustParse("bafyaabakaieac"): "empty unixfs directory inlined", 37 | cid.MustParse("bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"): "empty block", 38 | cid.MustParse("bafkqaaa"): "empty block inlined", 39 | cid.MustParse("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH"): "empty block dag-pb", 40 | cid.MustParse("bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua"): "empty block dag-cbor", 41 | cid.MustParse("baguqeeraiqjw7i2vwntyuekgvulpp2det2kpwt6cd7tx5ayqybqpmhfk76fa"): "empty block dag-json", 42 | } 43 | 44 | // DenylistHeader represents the header of a Denylist file. 45 | type DenylistHeader struct { 46 | Version int 47 | Name string 48 | Description string 49 | Author string 50 | Hints map[string]string 51 | 52 | headerBytes []byte 53 | headerLines uint64 54 | } 55 | 56 | // Decode decodes a DenlistHeader from a reader. Per the specification, the 57 | // maximum size of a header is 1KiB. If no header is found, ErrHeaderNotFound 58 | // is returned. 59 | func (h *DenylistHeader) Decode(r io.Reader) error { 60 | limRdr := &io.LimitedReader{ 61 | R: r, 62 | N: maxHeaderSize, 63 | } 64 | buf := bufio.NewReader(limRdr) 65 | 66 | h.headerBytes = nil 67 | h.headerLines = 0 68 | 69 | for { 70 | line, err := buf.ReadBytes('\n') 71 | if err == io.EOF { 72 | h.headerBytes = nil 73 | h.headerLines = 0 74 | return ErrHeaderNotFound 75 | } 76 | if err != nil { 77 | return err 78 | } 79 | h.headerLines++ 80 | if string(line) == "---\n" { 81 | break 82 | } 83 | h.headerBytes = append(h.headerBytes, line...) 84 | } 85 | 86 | err := yaml.Unmarshal(h.headerBytes, h) 87 | if err != nil { 88 | logger.Error(err) 89 | return err 90 | } 91 | 92 | // In the future this may need adapting to support several versions. 93 | if h.Version > 0 && h.Version != currentVersion { 94 | err = errors.New("unsupported denylist version") 95 | logger.Error(err) 96 | return err 97 | } 98 | return nil 99 | } 100 | 101 | // String provides a short string summary of the Header. 102 | func (h DenylistHeader) String() string { 103 | return fmt.Sprintf("%s (%s) by %s", h.Name, h.Description, h.Author) 104 | } 105 | 106 | // A Denylist represents a denylist file and its rules. It can parse and 107 | // follow a denylist file, and can answer questions about blocked or allowed 108 | // items in this denylist. 109 | type Denylist struct { 110 | Header DenylistHeader 111 | Filename string 112 | 113 | Entries Entries 114 | 115 | IPFSBlocksDB *BlocksDB 116 | IPNSBlocksDB *BlocksDB 117 | DoubleHashBlocksDB map[uint64]*BlocksDB // mhCode -> blocks using that code 118 | PathBlocksDB *BlocksDB 119 | PathPrefixBlocks Entries 120 | // MimeBlocksDB 121 | 122 | f io.ReadSeekCloser 123 | watcher *fsnotify.Watcher 124 | } 125 | 126 | // NewDenylist opens a denylist file and processes it (parses all its entries). 127 | // 128 | // If follow is false, the file handle is closed. 129 | // 130 | // If follow is true, the denylist file will be followed upon return. Any 131 | // appended rules will be processed live-updated in the 132 | // denylist. Denylist.Close() should be used when the Denylist or the 133 | // following is no longer needed. 134 | func NewDenylist(filepath string, follow bool) (*Denylist, error) { 135 | f, err := os.Open(filepath) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | dl := Denylist{ 141 | Filename: filepath, 142 | f: f, 143 | IPFSBlocksDB: &BlocksDB{}, 144 | IPNSBlocksDB: &BlocksDB{}, 145 | PathBlocksDB: &BlocksDB{}, 146 | DoubleHashBlocksDB: make(map[uint64]*BlocksDB), 147 | } 148 | 149 | err = dl.parseAndFollow(follow) 150 | return &dl, err 151 | } 152 | 153 | // NewDenylistReader processes a denylist from the given reader (parses all 154 | // its entries). 155 | func NewDenylistReader(r io.ReadSeekCloser) (*Denylist, error) { 156 | dl := Denylist{ 157 | Filename: "", 158 | f: r, 159 | IPFSBlocksDB: &BlocksDB{}, 160 | IPNSBlocksDB: &BlocksDB{}, 161 | PathBlocksDB: &BlocksDB{}, 162 | DoubleHashBlocksDB: make(map[uint64]*BlocksDB), 163 | } 164 | 165 | err := dl.parseAndFollow(false) 166 | return &dl, err 167 | } 168 | 169 | // read the header and make sure the reader is in the right position for 170 | // further processing. In case of no header a default one is used. 171 | func (dl *Denylist) readHeader() error { 172 | err := dl.Header.Decode(dl.f) 173 | if err == ErrHeaderNotFound { 174 | dl.Header.Version = 1 175 | dl.Header.Name = filepath.Base(dl.Filename) 176 | dl.Header.Description = "No header found" 177 | dl.Header.Author = "unknown" 178 | // reset the reader 179 | _, err = dl.f.Seek(0, 0) 180 | if err != nil { 181 | logger.Error(err) 182 | return err 183 | } 184 | logger.Warnf("Opening %s: empty header", dl.Filename) 185 | logger.Infof("Processing %s: %s", dl.Filename, dl.Header) 186 | return nil 187 | } else if err != nil { 188 | return err 189 | } 190 | 191 | logger.Infof("Processing %s: %s", dl.Filename, dl.Header) 192 | 193 | // We have to deal with the buffered reader reading beyond the header. 194 | _, err = dl.f.Seek(int64(len(dl.Header.headerBytes)+4), 0) 195 | if err != nil { 196 | logger.Error(err) 197 | return err 198 | } 199 | // The reader should be set at the line after ---\n now. 200 | // Reader to parse the rest of lines. 201 | 202 | return nil 203 | } 204 | 205 | // All closing on error is performed here. 206 | func (dl *Denylist) parseAndFollow(follow bool) error { 207 | if err := dl.readHeader(); err != nil { 208 | dl.Close() 209 | return err 210 | } 211 | 212 | // we will update N as we go after every line. Fixme: this is 213 | // going to play weird as the buffered reader will read-ahead 214 | // and consume N. 215 | limRdr := &io.LimitedReader{ 216 | R: dl.f, 217 | N: maxLineSize, 218 | } 219 | r := bufio.NewReader(limRdr) 220 | lineNumber := dl.Header.headerLines 221 | 222 | // we finished reading the file as it EOF'ed. 223 | if !follow { 224 | return dl.followLines(r, limRdr, lineNumber, nil) 225 | } 226 | // We now wait for new lines. 227 | 228 | watcher, err := fsnotify.NewWatcher() 229 | if err != nil { 230 | dl.Close() 231 | return err 232 | } 233 | dl.watcher = watcher 234 | err = watcher.Add(dl.Filename) 235 | if err != nil { 236 | dl.Close() 237 | return err 238 | } 239 | 240 | waitForWrite := func() error { 241 | for { 242 | select { 243 | case event := <-dl.watcher.Events: 244 | if event.Op&fsnotify.Write == fsnotify.Write { 245 | return nil 246 | } 247 | case err := <-dl.watcher.Errors: 248 | // TODO: log 249 | return err 250 | } 251 | } 252 | } 253 | 254 | go dl.followLines(r, limRdr, lineNumber, waitForWrite) 255 | return nil 256 | } 257 | 258 | // followLines reads lines from a buffered reader on top of a limited reader, 259 | // that we reset on every line. This enforces line-length limits. 260 | // If we pass a waitWrite() function, then it waits when finding EOF. 261 | // 262 | // Is this the right way of tailing a file? Pretty sure there are a 263 | // bunch of gotchas. It seems to work when saving on top of a file 264 | // though. Also, important that the limitedReader is there to avoid 265 | // parsing a huge lines. Also, this could be done by just having 266 | // watchers on the folder, but requires a small refactoring. 267 | func (dl *Denylist) followLines(r *bufio.Reader, limRdr *io.LimitedReader, lineNumber uint64, waitWrite func() error) error { 268 | line := "" 269 | limRdr.N = maxLineSize // reset 270 | 271 | for { 272 | partialLine, err := r.ReadString('\n') 273 | 274 | // limit reader exhausted 275 | if err == io.EOF && limRdr.N == 0 { 276 | err = fmt.Errorf("line too long. %s:%d", dl.Filename, lineNumber+1) 277 | logger.Error(err) 278 | dl.Close() 279 | return err 280 | } 281 | 282 | // Record how much of a line we have 283 | line += partialLine 284 | 285 | if err == io.EOF { 286 | if waitWrite != nil { // keep waiting 287 | err := waitWrite() 288 | if err != nil { 289 | logger.Error(err) 290 | dl.Close() 291 | return err 292 | } 293 | continue 294 | } else { // Finished 295 | return nil 296 | } 297 | } 298 | if err != nil { 299 | logger.Error(err) 300 | dl.Close() 301 | return err 302 | } 303 | 304 | // if we are here, no EOF, no error and ReadString() 305 | // found an \n so we have a full line. 306 | 307 | lineNumber++ 308 | // we have read up to \n 309 | if err := dl.parseLine(line, lineNumber); err != nil { 310 | logger.Error(err) 311 | // log error and continue with next line 312 | 313 | } 314 | // reset for next line 315 | line = "" 316 | limRdr.N = maxLineSize // reset 317 | } 318 | } 319 | 320 | // parseLine processes every full-line read and puts it into the BlocksDB etc. 321 | // so that things can be queried later. It turns lines into Entry objects. 322 | // 323 | // Note (hector): I'm using B58Encoded-multihash strings as keys for IPFS, 324 | // DoubleHash BlocksDB. Why? We could be using the multihash bytes directly. 325 | // Some reasons (this should be changed if there are better reasons to do so): 326 | // 327 | // - B58Encoded-strings produce readable keys. These will be readable keys 328 | // in a database, readable keys in the debug logs etc. Having readable 329 | // multihashes instead of raw bytes is nice. 330 | // 331 | // - If we assume IPFS mostly deals in CIDv0s (raw multihashes), we can 332 | // avoid parsing the Qmxxx multihash in /ipfs/Qmxxx/path and just use the 333 | // string directly for lookups. 334 | // 335 | // - If we used raw bytes, we would have to decode every cidV0, but we would 336 | // not have to b58-encode multihashes for lookups. Chances that this is 337 | // better but well (decide before going with permanent storage!). 338 | func (dl *Denylist) parseLine(line string, number uint64) error { 339 | line = strings.TrimSuffix(line, "\n") 340 | if len(line) == 0 || line[0] == '#' { 341 | return nil 342 | } 343 | 344 | e := Entry{ 345 | Line: number, 346 | RawValue: line, 347 | Hints: make(map[string]string), 348 | } 349 | 350 | // Every entry carries the header hints. They will be 351 | // overwritten by rule hints below when in conflict. 352 | for k, v := range dl.Header.Hints { 353 | e.Hints[k] = v 354 | } 355 | 356 | // the rule is always field-0. Anything else is hints. 357 | splitFields := strings.Fields(line) 358 | rule := splitFields[0] 359 | if len(splitFields) > 1 { // we have hints 360 | hintSlice := splitFields[1:] 361 | for _, kv := range hintSlice { 362 | key, value, ok := strings.Cut(kv, "=") 363 | if !ok { 364 | continue 365 | } 366 | e.Hints[key] = value 367 | } 368 | } 369 | 370 | // We treat + and - the same. Both serve to declare and 371 | // allow-rule. 372 | if unprefixed, found := cutPrefix(rule, "-"); found { 373 | e.AllowRule = true 374 | rule = unprefixed 375 | } else if unprefixed, found := cutPrefix(rule, "+"); found { 376 | e.AllowRule = true 377 | rule = unprefixed 378 | } else if unprefixed, found := cutPrefix(rule, "!"); found { 379 | e.AllowRule = true 380 | rule = unprefixed 381 | } 382 | 383 | switch { 384 | case strings.HasPrefix(rule, "//"): 385 | // Double-hash rule. 386 | // It can be a Multihash or a sha256-hex-encoded string. 387 | 388 | rule = strings.TrimPrefix(rule, "//") 389 | 390 | parseMultihash := func(mhStr string) (uint64, multihash.Multihash, error) { 391 | mh, err := multihash.FromB58String(rule) 392 | if err != nil { // not a b58 string usually 393 | return 0, nil, err 394 | } 395 | dmh, err := multihash.Decode(mh) 396 | if err != nil { // looked like a mhash but it was not. 397 | return 0, nil, err 398 | } 399 | 400 | // Identity hash doesn't make sense for double 401 | // hashing. In practice it is usually a hex string 402 | // that has been wrongly parsed as multihash. 403 | if dmh.Code == 0 { 404 | return 0, nil, errors.New("identity hash cannot be a double hash") 405 | } 406 | 407 | // if we are here it means we have something that 408 | // could be interpreted as a multihash but it may 409 | // still be a hex-encoded string that just parsed as 410 | // b58 fine. In any case, we should check we know how to 411 | // hash for this type of multihash. 412 | _, err = mhreg.GetVariableHasher(dmh.Code, dmh.Length) 413 | if err != nil { 414 | return 0, nil, err 415 | } 416 | 417 | return dmh.Code, mh, nil 418 | } 419 | 420 | parseHexString := func(hexStr string) (uint64, multihash.Multihash, error) { 421 | if len(hexStr) != 64 { 422 | return 0, nil, errors.New("hex string are sha2-256 hashes and must be 64 chars (32 bytes) long") 423 | } 424 | 425 | bs, err := hex.DecodeString(rule) 426 | if err != nil { 427 | return 0, nil, err 428 | } 429 | // We have a hex-encoded string and assume it is a 430 | // SHA2_256. TODO: could support hints here to use 431 | // different functions. 432 | mhBytes, err := multihash.Encode(bs, multihash.SHA2_256) 433 | if err != nil { 434 | return 0, nil, err 435 | } 436 | return multihash.SHA2_256, multihash.Multihash(mhBytes), nil 437 | } 438 | 439 | addRule := func(e Entry, mhType uint64, mh multihash.Multihash) error { 440 | bpath, _ := NewBlockedPath("") 441 | e.Path = bpath 442 | e.Multihash = mh 443 | 444 | // Store it in the appropriate BlocksDB (per mhtype). 445 | key := e.Multihash.B58String() 446 | if blocks := dl.DoubleHashBlocksDB[mhType]; blocks == nil { 447 | dl.DoubleHashBlocksDB[mhType] = &BlocksDB{} 448 | } 449 | dl.DoubleHashBlocksDB[mhType].Store(key, e) 450 | logger.Debugf("%s:%d: Double-hash rule. Func: %s. Key: %s. Entry: %s", filepath.Base(dl.Filename), number, multicodec.Code(mhType).String(), key, e) 451 | return nil 452 | } 453 | 454 | // We have to assume that perhaps one day a sha256 hex string 455 | // is going to parse as a valid multihash with an known 456 | // hashing function etc. And vice-versa perhaps. 457 | // 458 | // In a case where we cannot distinguish between a b58btc 459 | // multihash and a hex-string, we add rules for both, to make 460 | // sure we always block what should be blocked. 461 | code, mh, err1 := parseMultihash(rule) 462 | if err1 == nil { 463 | // clone the entry as add-rule modifies it. 464 | e1 := e.Clone() 465 | if err := addRule(e1, code, mh); err != nil { 466 | return err 467 | } 468 | } 469 | 470 | code, mh, err2 := parseHexString(rule) 471 | if err2 == nil { 472 | if err := addRule(e, code, mh); err != nil { 473 | return err 474 | } 475 | } 476 | 477 | if err1 != nil && err2 != nil { 478 | return fmt.Errorf("double-hash cannot be parsed as a multihash with a supported hashing function (%w) nor as a sha256 hex-encoded string (%w) (%s:%d)", err1, err2, dl.Filename, number) 479 | } 480 | 481 | case strings.HasPrefix(rule, "/ipfs/"), strings.HasPrefix(rule, "/ipld/"): 482 | // ipfs/ipld rule. We parse the CID and use the 483 | // b58-encoded-multihash as key to the Entry. 484 | 485 | rule = strings.TrimPrefix(rule, "/ipfs/") 486 | rule = strings.TrimPrefix(rule, "/ipld/") 487 | cidStr, subPath, _ := strings.Cut(rule, "/") 488 | 489 | c, err := cid.Decode(cidStr) 490 | if err != nil { 491 | return fmt.Errorf("error extracting cid %s (%s:%d): %w", cidStr, dl.Filename, number, err) 492 | } 493 | 494 | // Blocking these by mistake can break some applications (by 495 | // "some" we mean Kubo). 496 | if _, ok := SafeCids[c]; ok { 497 | logger.Warnf("Ignored: %s corresponds to a known empty folder or block and will not be blocked", c) 498 | return nil 499 | } 500 | 501 | e.Multihash = c.Hash() 502 | 503 | blockedPath, err := NewBlockedPath(subPath) 504 | if err != nil { 505 | return err 506 | } 507 | e.Path = blockedPath 508 | 509 | // Add to IPFS by component multihash 510 | key := e.Multihash.B58String() 511 | dl.IPFSBlocksDB.Store(key, e) 512 | logger.Debugf("%s:%d: IPFS rule. Key: %s. Entry: %s", filepath.Base(dl.Filename), number, key, e) 513 | case strings.HasPrefix(rule, "/ipns/"): 514 | // ipns rule. If it carries anything parseable as a CID, we 515 | // store indexed by the b58-multihash. Otherwise assume it is 516 | // a domain name and store that directly. 517 | rule, _ = cutPrefix(rule, "/ipns/") 518 | key, subPath, _ := strings.Cut(rule, "/") 519 | c, err := cid.Decode(key) 520 | if err == nil { // CID key handling. 521 | key = c.Hash().B58String() 522 | } 523 | blockedPath, err := NewBlockedPath(subPath) 524 | if err != nil { 525 | return err 526 | } 527 | e.Path = blockedPath 528 | 529 | // Add to IPFS by component multihash 530 | dl.IPNSBlocksDB.Store(key, e) 531 | logger.Debugf("%s:%d: IPNS rule. Key: %s. Entry: %s", filepath.Base(dl.Filename), number, key, e) 532 | default: 533 | // Blocked by path only. We store non-prefix paths directly. 534 | // We store prefixed paths separately as every path request 535 | // will have to loop them. 536 | blockedPath, err := NewBlockedPath(rule) 537 | if err != nil { 538 | return err 539 | } 540 | e.Path = blockedPath 541 | 542 | key := rule 543 | if blockedPath.Prefix { 544 | dl.PathPrefixBlocks = append(dl.PathPrefixBlocks, e) 545 | } else { 546 | dl.PathBlocksDB.Store(key, e) 547 | } 548 | logger.Debugf("%s:%d: Path rule. Key: %s. Entry: %s", filepath.Base(dl.Filename), number, key, e) 549 | } 550 | 551 | dl.Entries = append(dl.Entries, e) 552 | return nil 553 | 554 | } 555 | 556 | // Close closes the Denylist file handle and stops watching write events on it. 557 | func (dl *Denylist) Close() error { 558 | var err error 559 | if dl.watcher != nil { 560 | err = multierr.Append(err, dl.watcher.Close()) 561 | } 562 | if dl.f != nil { 563 | err = multierr.Append(err, dl.f.Close()) 564 | } 565 | 566 | return err 567 | } 568 | 569 | // IsSubpathBlocked returns Blocking Status for the given subpath. 570 | func (dl *Denylist) IsSubpathBlocked(subpath string) StatusResponse { 571 | // all "/" prefix and suffix trimming is done in BlockedPath.Matches. 572 | // every rule has been ingested without slashes on the ends 573 | 574 | logger.Debugf("IsSubpathBlocked load path: %s", subpath) 575 | pathBlockEntries, _ := dl.PathBlocksDB.Load(subpath) 576 | status, entry := pathBlockEntries.CheckPathStatus(subpath) 577 | if status != StatusNotFound { // hit 578 | return StatusResponse{ 579 | Status: status, 580 | Filename: dl.Filename, 581 | Entry: entry, 582 | } 583 | } 584 | // Check every prefix path. Note: this is very innefficient, we 585 | // should have some HAMT that we can traverse with every character if 586 | // we were to support a large number of subpath-prefix blocks. 587 | status, entry = dl.PathPrefixBlocks.CheckPathStatus(subpath) 588 | return StatusResponse{ 589 | Status: status, 590 | Filename: dl.Filename, 591 | Entry: entry, 592 | } 593 | } 594 | 595 | func toDNSLinkFQDN(label string) string { 596 | var result strings.Builder 597 | for i := 0; i < len(label); i++ { 598 | char := rune(label[i]) 599 | nextChar := rune(0) 600 | if i < len(label)-1 { 601 | nextChar = rune(label[i+1]) 602 | } 603 | 604 | if char == '-' && nextChar == '-' { 605 | result.WriteRune('-') 606 | i++ 607 | continue 608 | } 609 | 610 | if char == '-' { 611 | result.WriteRune('.') 612 | continue 613 | } 614 | 615 | result.WriteRune(char) 616 | } 617 | return result.String() 618 | } 619 | 620 | func (dl *Denylist) checkDoubleHashWithFn(caller string, origKey string, code uint64) (Status, Entry, error) { 621 | blocksdb, ok := dl.DoubleHashBlocksDB[code] 622 | if !ok { 623 | return StatusNotFound, Entry{}, nil 624 | } 625 | // Double-hash the key 626 | doubleHash, err := multihash.Sum([]byte(origKey), code, -1) 627 | if err != nil { 628 | // Usually this means an unsupported hash function was 629 | // registered. We log and ignore. 630 | logger.Error(err) 631 | return StatusNotFound, Entry{}, nil 632 | } 633 | b58DoubleHash := doubleHash.B58String() 634 | logger.Debugf("%s load IPNS doublehash: %d %s", caller, code, b58DoubleHash) 635 | entries, _ := blocksdb.Load(b58DoubleHash) 636 | status, entry := entries.CheckPathStatus("") // double-hashes cannot have entry-subpaths 637 | return status, entry, nil 638 | } 639 | 640 | func (dl *Denylist) checkDoubleHash(caller string, origKey string) (Status, Entry, error) { 641 | for mhCode := range dl.DoubleHashBlocksDB { 642 | status, entry, err := dl.checkDoubleHashWithFn(caller, origKey, mhCode) 643 | if err != nil { 644 | return status, entry, err 645 | } 646 | if status != StatusNotFound { // hit! 647 | return status, entry, nil 648 | } 649 | } 650 | return StatusNotFound, Entry{}, nil 651 | 652 | } 653 | 654 | // IsIPNSPathBlocked returns Blocking Status for a given IPNS name and its 655 | // subpath. The name is NOT an "/ipns/name" path, but just the name. 656 | func (dl *Denylist) IsIPNSPathBlocked(name, subpath string) StatusResponse { 657 | subpath = strings.TrimPrefix(subpath, "/") 658 | 659 | var p path.Path 660 | var err error 661 | if len(subpath) > 0 { 662 | p, err = path.NewPath("/ipns/" + name + "/" + subpath) 663 | } else { 664 | p, err = path.NewPath("/ipns/" + name) 665 | } 666 | if err != nil { 667 | return StatusResponse{ 668 | Status: StatusErrored, 669 | Error: err, 670 | } 671 | } 672 | key := name 673 | // Check if it is a CID and use the multihash as key then 674 | c, err := cid.Decode(key) 675 | if err == nil { 676 | key = c.Hash().B58String() 677 | // 678 | } else if !strings.ContainsRune(key, '.') { 679 | // not a CID. It must be a ipns-dnslink name if it does not 680 | // contain ".", maybe they got replaced by "-" 681 | // https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header 682 | key = toDNSLinkFQDN(key) 683 | } 684 | logger.Debugf("IsIPNSPathBlocked load: %s %s", key, subpath) 685 | entries, _ := dl.IPNSBlocksDB.Load(key) 686 | status, entry := entries.CheckPathStatus(subpath) 687 | if status != StatusNotFound { // hit! 688 | return StatusResponse{ 689 | Path: p, 690 | Status: status, 691 | Filename: dl.Filename, 692 | Entry: entry, 693 | } 694 | } 695 | 696 | // Double-hash blocking, works by double-hashing "/ipns//" 697 | // Legacy double-hashes for dnslink will hash "domain.com/" (trailing 698 | // slash) or "/" for ipns-key blocking 699 | legacyKey := name + "/" + subpath 700 | if c.Defined() { // we parsed a CID before 701 | legacyCid, err := cid.NewCidV1(c.Prefix().Codec, c.Hash()).StringOfBase(multibase.Base32) 702 | if err != nil { 703 | return StatusResponse{ 704 | Path: p, 705 | Status: StatusErrored, 706 | Filename: dl.Filename, 707 | Error: err, 708 | } 709 | } 710 | legacyKey = legacyCid + "/" + subpath 711 | } 712 | status, entry, err = dl.checkDoubleHashWithFn("IsIPNSPathBlocked (legacy)", legacyKey, multihash.SHA2_256) 713 | if status != StatusNotFound { // hit or error 714 | return StatusResponse{ 715 | Path: p, 716 | Status: status, 717 | Filename: dl.Filename, 718 | Entry: entry, 719 | Error: err, 720 | } 721 | } 722 | 723 | // Modern double-hash approach 724 | key = p.String() 725 | if c.Defined() { // the ipns path is a CID The 726 | // b58-encoded-multihash extracted from an IPNS name 727 | // when the IPNS is a CID. 728 | key = c.Hash().B58String() 729 | if len(subpath) > 0 { 730 | key += "/" + subpath 731 | } 732 | } 733 | 734 | status, entry, err = dl.checkDoubleHash("IsIPNSPathBlocked", key) 735 | return StatusResponse{ 736 | Path: p, 737 | Status: status, 738 | Filename: dl.Filename, 739 | Entry: entry, 740 | Error: err, 741 | } 742 | } 743 | 744 | // IsIPFSPathBlocked returns Blocking Status for a given IPFS CID and its 745 | // subpath. The cidStr is NOT an "/ipns/cid" path, but just the cid. 746 | func (dl *Denylist) IsIPFSPathBlocked(cidStr, subpath string) StatusResponse { 747 | return dl.isIPFSIPLDPathBlocked(cidStr, subpath, "ipfs") 748 | } 749 | 750 | // IsIPLDPathBlocked returns Blocking Status for a given IPLD CID and its 751 | // subpath. The cidStr is NOT an "/ipld/cid" path, but just the cid. 752 | func (dl *Denylist) IsIPLDPathBlocked(cidStr, subpath string) StatusResponse { 753 | return dl.isIPFSIPLDPathBlocked(cidStr, subpath, "ipld") 754 | } 755 | 756 | func (dl *Denylist) isIPFSIPLDPathBlocked(cidStr, subpath, protocol string) StatusResponse { 757 | subpath = strings.TrimPrefix(subpath, "/") 758 | 759 | var p path.Path 760 | var err error 761 | if len(subpath) > 0 { 762 | p, err = path.NewPath("/" + protocol + "/" + cidStr + "/" + subpath) 763 | } else { 764 | p, err = path.NewPath("/" + protocol + "/" + cidStr) 765 | } 766 | 767 | if err != nil { 768 | return StatusResponse{ 769 | Status: StatusErrored, 770 | Error: err, 771 | } 772 | } 773 | 774 | key := cidStr 775 | 776 | // This could be a shortcut to let the work to the 777 | // blockservice. Assuming IsCidBlocked() is going to be 778 | // called later down the stack (by IPFS). 779 | // 780 | // TODO: enable this with options. 781 | // if p.IsJustAKey() { 782 | // return false 783 | // } 784 | 785 | var c cid.Cid 786 | if len(key) != 46 || key[:2] != "Qm" { 787 | // Key is not a CIDv0, we need to convert other CIDs. 788 | // convert to Multihash (cidV0) 789 | c, err = cid.Decode(key) 790 | if err != nil { 791 | logger.Warnf("could not decode %s as CID: %s", key, err) 792 | return StatusResponse{ 793 | Path: p, 794 | Status: StatusErrored, 795 | Filename: dl.Filename, 796 | Error: err, 797 | } 798 | } 799 | key = c.Hash().B58String() 800 | } 801 | 802 | logger.Debugf("isIPFSIPLDPathBlocked load: %s %s", key, subpath) 803 | entries, _ := dl.IPFSBlocksDB.Load(key) 804 | status, entry := entries.CheckPathStatus(subpath) 805 | if status != StatusNotFound { // hit! 806 | return StatusResponse{ 807 | Path: p, 808 | Status: status, 809 | Filename: dl.Filename, 810 | Entry: entry, 811 | } 812 | } 813 | 814 | // Check for double-hashed entries. We need to lookup both the 815 | // multihash+path and the base32-cidv1 + path 816 | if !c.Defined() { // if we didn't decode before... 817 | c, err = cid.Decode(cidStr) 818 | if err != nil { 819 | logger.Warnf("could not decode %s as CID: %s", key, err) 820 | return StatusResponse{ 821 | Path: p, 822 | Status: StatusErrored, 823 | Filename: dl.Filename, 824 | Error: err, 825 | } 826 | } 827 | } 828 | 829 | prefix := c.Prefix() 830 | // Checks for legacy doublehash blocking 831 | // / 832 | // TODO: we should be able to disable this part with an Option 833 | // or a hint for denylists not using it. 834 | v1b32, err := cid.NewCidV1(prefix.Codec, c.Hash()).StringOfBase(multibase.Base32) // base32 string 835 | if err != nil { 836 | return StatusResponse{ 837 | Path: p, 838 | Status: StatusErrored, 839 | Filename: dl.Filename, 840 | Error: err, 841 | } 842 | } 843 | // badbits appends / on empty subpath. and hashes that 844 | // https://specs.ipfs.tech/compact-denylist-format/#double-hash 845 | v1b32path := v1b32 + "/" + subpath 846 | status, entry, err = dl.checkDoubleHashWithFn("IsIPFSIPLDPathBlocked (legacy)", v1b32path, multihash.SHA2_256) 847 | if status != StatusNotFound { // hit or error 848 | return StatusResponse{ 849 | Path: p, 850 | Status: status, 851 | Filename: dl.Filename, 852 | Entry: entry, 853 | Error: err, 854 | } 855 | } 856 | 857 | // Otherwise just check normal double-hashing of multihash 858 | // for all double-hashing functions used. 859 | // / 860 | v0path := c.Hash().B58String() 861 | if subpath != "" { 862 | v0path += "/" + subpath 863 | } 864 | status, entry, err = dl.checkDoubleHash("IsIPFSIPLDPathBlocked", v0path) 865 | return StatusResponse{ 866 | Path: p, 867 | Status: status, 868 | Filename: dl.Filename, 869 | Entry: entry, 870 | Error: err, 871 | } 872 | } 873 | 874 | // IsPathBlocked provides Blocking Status for a given path. This is done by 875 | // interpreting the full path and checking for blocked Path, IPFS, IPNS or 876 | // double-hashed items matching it. 877 | // 878 | // Matching is more efficient if: 879 | // 880 | // - Paths in the form of /ipfs/Qm/... (sha2-256-multihash) are used rather than CIDv1. 881 | // 882 | // - A single double-hashing pattern is used. 883 | // 884 | // - A small number of path-only match rules using prefixes are used. 885 | func (dl *Denylist) IsPathBlocked(p path.Path) StatusResponse { 886 | segments := p.Segments() 887 | if len(segments) < 2 { 888 | return StatusResponse{ 889 | Path: p, 890 | Status: StatusErrored, 891 | Filename: dl.Filename, 892 | Error: errors.New("path is too short"), 893 | } 894 | } 895 | proto := segments[0] 896 | key := segments[1] 897 | subpath := strings.Join(segments[2:], "/") 898 | 899 | // First, check that we are not blocking this subpath in general 900 | if len(subpath) > 0 { 901 | if resp := dl.IsSubpathBlocked(subpath); resp.Status != StatusNotFound { 902 | resp.Path = p 903 | return resp 904 | } 905 | } 906 | 907 | // Second, check that we are not blocking ipfs or ipns paths 908 | // like this one. 909 | 910 | // ["ipfs", "", ...] 911 | 912 | switch proto { 913 | case "ipns": 914 | return dl.IsIPNSPathBlocked(key, subpath) 915 | case "ipfs": 916 | return dl.IsIPFSPathBlocked(key, subpath) 917 | case "ipld": 918 | return dl.IsIPLDPathBlocked(key, subpath) 919 | default: 920 | return StatusResponse{ 921 | Path: p, 922 | Status: StatusNotFound, 923 | Filename: dl.Filename, 924 | } 925 | } 926 | } 927 | 928 | // IsCidBlocked provides Blocking Status for a given CID. This is done by 929 | // extracting the multihash and checking if it is blocked by any rule. 930 | func (dl *Denylist) IsCidBlocked(c cid.Cid) StatusResponse { 931 | b58 := c.Hash().B58String() 932 | logger.Debugf("IsCidBlocked load: %s", b58) 933 | entries, _ := dl.IPFSBlocksDB.Load(b58) 934 | // Look for an entry with an empty path 935 | // which means the Mhash itself is blocked. 936 | status, entry := entries.CheckPathStatus("") 937 | if status != StatusNotFound { // Hit! 938 | return StatusResponse{ 939 | Cid: c, 940 | Status: status, 941 | Filename: dl.Filename, 942 | Entry: entry, 943 | } 944 | } 945 | 946 | // Now check if a double-hash covers this CID 947 | 948 | // Legacy double-hashing support. 949 | // convert cid to v1 base32 950 | // the double-hash using multhash sha2-256 951 | // then check that 952 | prefix := c.Prefix() 953 | b32, err := cid.NewCidV1(prefix.Codec, c.Hash()).StringOfBase(multibase.Base32) 954 | if err != nil { 955 | return StatusResponse{ 956 | Cid: c, 957 | Status: StatusErrored, 958 | Filename: dl.Filename, 959 | Error: err, 960 | } 961 | } 962 | b32 += "/" // yes, needed 963 | status, entry, err = dl.checkDoubleHashWithFn("IsCidBlocked (legacy)", b32, multihash.SHA2_256) 964 | if status != StatusNotFound { // hit or error 965 | return StatusResponse{ 966 | Cid: c, 967 | Status: status, 968 | Filename: dl.Filename, 969 | Entry: entry, 970 | Error: err, 971 | } 972 | } 973 | 974 | // Otherwise, double-hash the multihash string. 975 | status, entry, err = dl.checkDoubleHash("IsCidBlocked", b58) 976 | return StatusResponse{ 977 | Cid: c, 978 | Status: status, 979 | Filename: dl.Filename, 980 | Entry: entry, 981 | Error: err, 982 | } 983 | } 984 | -------------------------------------------------------------------------------- /entry.go: -------------------------------------------------------------------------------- 1 | package nopfs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/multiformats/go-multihash" 10 | ) 11 | 12 | // Entry represents a rule (or a line) in a denylist file. 13 | type Entry struct { 14 | Line uint64 15 | AllowRule bool 16 | Hints map[string]string 17 | RawValue string 18 | Multihash multihash.Multihash // set for ipfs-paths mostly. 19 | Path BlockedPath 20 | } 21 | 22 | // String provides a single-line representation of the Entry. 23 | func (e Entry) String() string { 24 | path := e.Path.Path 25 | if len(path) == 0 { 26 | path = "(empty)" 27 | } 28 | return fmt.Sprintf("Path: %s. Prefix: %t. AllowRule: %t.", path, e.Path.Prefix, e.AllowRule) 29 | } 30 | 31 | func (e Entry) Clone() Entry { 32 | hints := make(map[string]string, len(e.Hints)) 33 | for k, v := range e.Hints { 34 | hints[k] = v 35 | } 36 | 37 | return Entry{ 38 | Line: e.Line, 39 | AllowRule: e.AllowRule, 40 | Hints: hints, 41 | RawValue: e.RawValue, 42 | Multihash: bytes.Clone(e.Multihash), 43 | Path: e.Path, 44 | } 45 | } 46 | 47 | // Entries is a slice of Entry. 48 | type Entries []Entry 49 | 50 | // CheckPathStatus returns whether the given path has a match in one of the Entries. 51 | func (entries Entries) CheckPathStatus(p string) (Status, Entry) { 52 | // start by the last one, since latter items have preference. 53 | for i := len(entries) - 1; i >= 0; i-- { 54 | e := entries[i] 55 | logger.Debugf("check-path: %s matches %s", e.Path.Path, p) 56 | if e.Path.Matches(p) { 57 | // if we find a negative rule that matches the path 58 | // then it is not blocked. 59 | if e.AllowRule { 60 | return StatusAllowed, e 61 | } 62 | return StatusBlocked, e 63 | } 64 | } 65 | return StatusNotFound, Entry{} 66 | } 67 | 68 | // BlockedPath represents the path part of a blocking rule. 69 | type BlockedPath struct { 70 | Path string 71 | Prefix bool 72 | } 73 | 74 | // NewBlockedPath takes a raw path, unscapes and sanitizes it, detecting and 75 | // handling wildcards. It may also represent an "allowed" path on "allowed" 76 | // rules. 77 | func NewBlockedPath(rawPath string) (BlockedPath, error) { 78 | if rawPath == "" { 79 | return BlockedPath{ 80 | Path: "", 81 | Prefix: false, 82 | }, nil 83 | } 84 | 85 | // Deal with * before Unescaping, as the path may have an * at the end 86 | // that has been escaped and should not be interpreted. 87 | prefix := false 88 | if strings.HasSuffix(rawPath, "/*") { 89 | rawPath = strings.TrimSuffix(rawPath, "/*") 90 | prefix = true 91 | } else if strings.HasSuffix(rawPath, "*") { 92 | rawPath = strings.TrimSuffix(rawPath, "*") 93 | prefix = true 94 | } 95 | 96 | path, err := url.QueryUnescape(rawPath) 97 | if err != nil { 98 | return BlockedPath{}, err 99 | } 100 | 101 | path = strings.TrimPrefix(path, "/") 102 | path = strings.TrimSuffix(path, "/") 103 | 104 | return BlockedPath{ 105 | Path: path, 106 | Prefix: prefix, 107 | }, nil 108 | } 109 | 110 | // Matches returns whether the given path matched the blocked (or allowed) path. 111 | func (bpath BlockedPath) Matches(path string) bool { 112 | // sanitize path 113 | path = strings.TrimSuffix(path, "/") 114 | path = strings.TrimPrefix(path, "/") 115 | 116 | // Matches all paths 117 | if bpath.Path == "*" { 118 | return true 119 | } 120 | 121 | // No path matches empty paths 122 | if bpath.Path == "" && path == "" { 123 | return true 124 | } 125 | 126 | // Prefix matches prefix 127 | // otherwise exact match 128 | // bpath.Path already sanitized 129 | if bpath.Prefix && strings.HasPrefix(path, bpath.Path) { 130 | return true 131 | } else if bpath.Path == path { 132 | return true 133 | } 134 | 135 | return false 136 | } 137 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ipfs-shipyard/nopfs 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.6.0 7 | github.com/ipfs/boxo v0.15.0 8 | github.com/ipfs/go-cid v0.4.1 9 | github.com/ipfs/go-log/v2 v2.5.1 10 | github.com/multiformats/go-multicodec v0.9.0 11 | github.com/multiformats/go-multihash v0.2.3 12 | go.uber.org/multierr v1.11.0 13 | gopkg.in/yaml.v3 v3.0.1 14 | ) 15 | 16 | require ( 17 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/minio/sha256-simd v1.0.1 // indirect 20 | github.com/mr-tron/base58 v1.2.0 // indirect 21 | github.com/multiformats/go-base32 v0.1.0 // indirect 22 | github.com/multiformats/go-base36 v0.2.0 // indirect 23 | github.com/multiformats/go-multibase v0.2.0 // indirect 24 | github.com/multiformats/go-varint v0.0.7 // indirect 25 | github.com/spaolacci/murmur3 v1.1.0 // indirect 26 | go.uber.org/zap v1.26.0 // indirect 27 | golang.org/x/crypto v0.14.0 // indirect 28 | golang.org/x/sys v0.13.0 // indirect 29 | lukechampine.com/blake3 v1.2.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 6 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 7 | github.com/ipfs/boxo v0.15.0 h1:BriLydj2nlK1nKeJQHxcKSuG5ZXcoutzhBklOtxC5pk= 8 | github.com/ipfs/boxo v0.15.0/go.mod h1:X5ulcbR5Nh7sm3Db8+08AApUo6FsGC5mb23QDKAoB/M= 9 | github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 10 | github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 11 | github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 12 | github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 13 | github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= 14 | github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 15 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 16 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 17 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 18 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 21 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 22 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 23 | github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 24 | github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 25 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 26 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 27 | github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 28 | github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 29 | github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 30 | github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 31 | github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 32 | github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 33 | github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 34 | github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 35 | github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 36 | github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 37 | github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 38 | github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 39 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 43 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 46 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 47 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 48 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 49 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 50 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 51 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 52 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 53 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 54 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 55 | go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 56 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 57 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 58 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 59 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 60 | golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= 61 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 62 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 63 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 64 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 66 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 67 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 68 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 69 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 71 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 80 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 82 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 83 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 84 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 85 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 86 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 87 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 88 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 89 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 90 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 92 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 93 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 95 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 97 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 98 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 99 | lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 100 | lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= 101 | -------------------------------------------------------------------------------- /ipfs/README.md: -------------------------------------------------------------------------------- 1 | # IPFS library helpers are wrappers 2 | 3 | This submodule contains wrapper implementations of IPFS-stack interfaces to 4 | introduce blocking-functionality as provided by NOpfs. 5 | 6 | This submodule has its own `go.mod` and `go.sum` files and depend on the 7 | version of Boxo that we are building it for. 8 | 9 | This submodule is tagged in the form `ipfs/v.` 10 | where the Boxo version corresponds to the Boxo release that the code targets 11 | and the release suffix to the release number for that version (in case of 12 | multiple). 13 | 14 | -------------------------------------------------------------------------------- /ipfs/blockservice.go: -------------------------------------------------------------------------------- 1 | package ipfs 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ipfs-shipyard/nopfs" 7 | blockservice "github.com/ipfs/boxo/blockservice" 8 | blockstore "github.com/ipfs/boxo/blockstore" 9 | exchange "github.com/ipfs/boxo/exchange" 10 | blocks "github.com/ipfs/go-block-format" 11 | "github.com/ipfs/go-cid" 12 | ) 13 | 14 | var _ blockservice.BlockService = (*BlockService)(nil) 15 | 16 | // BlockService implements a blocking BlockService. 17 | type BlockService struct { 18 | blocker *nopfs.Blocker 19 | bs blockservice.BlockService 20 | } 21 | 22 | // WrapBlockService wraps the given BlockService with a content-blocking layer 23 | // for Get and Add operations. 24 | func WrapBlockService(bs blockservice.BlockService, blocker *nopfs.Blocker) blockservice.BlockService { 25 | logger.Debug("BlockService wrapped with content blocker") 26 | 27 | return &BlockService{ 28 | blocker: blocker, 29 | bs: bs, 30 | } 31 | } 32 | 33 | // Closes the BlockService and the Blocker. 34 | func (nbs *BlockService) Close() error { 35 | nbs.blocker.Close() 36 | return nbs.bs.Close() 37 | } 38 | 39 | // Gets a block unless CID has been blocked. 40 | func (nbs *BlockService) GetBlock(ctx context.Context, c cid.Cid) (blocks.Block, error) { 41 | if err := nbs.blocker.IsCidBlocked(c).ToError(); err != nil { 42 | logger.Warn(err.Response) 43 | return nil, err 44 | } 45 | return nbs.bs.GetBlock(ctx, c) 46 | } 47 | 48 | // GetsBlocks reads several blocks. Blocked CIDs are filtered out of ks. 49 | func (nbs *BlockService) GetBlocks(ctx context.Context, ks []cid.Cid) <-chan blocks.Block { 50 | var filtered []cid.Cid 51 | for _, c := range ks { 52 | if err := nbs.blocker.IsCidBlocked(c).ToError(); err != nil { 53 | logger.Warn(err.Response) 54 | logger.Warnf("GetBlocks dropped blocked block: %s", err) 55 | } else { 56 | filtered = append(filtered, c) 57 | } 58 | } 59 | return nbs.bs.GetBlocks(ctx, filtered) 60 | } 61 | 62 | // Blockstore returns the underlying Blockstore. 63 | func (nbs *BlockService) Blockstore() blockstore.Blockstore { 64 | return nbs.bs.Blockstore() 65 | } 66 | 67 | // Exchange returns the underlying Exchange. 68 | func (nbs *BlockService) Exchange() exchange.Interface { 69 | return nbs.bs.Exchange() 70 | } 71 | 72 | // AddBlock adds a block unless the CID is blocked. 73 | func (nbs *BlockService) AddBlock(ctx context.Context, o blocks.Block) error { 74 | if err := nbs.blocker.IsCidBlocked(o.Cid()).ToError(); err != nil { 75 | logger.Warn(err.Response) 76 | return err 77 | } 78 | return nbs.bs.AddBlock(ctx, o) 79 | } 80 | 81 | // AddBlocks adds multiple blocks. Blocks with blocked CIDs are dropped. 82 | func (nbs *BlockService) AddBlocks(ctx context.Context, bs []blocks.Block) error { 83 | var filtered []blocks.Block 84 | for _, o := range bs { 85 | if err := nbs.blocker.IsCidBlocked(o.Cid()).ToError(); err != nil { 86 | logger.Warn(err.Response) 87 | logger.Warnf("AddBlocks dropped blocked block: %s", err) 88 | } else { 89 | filtered = append(filtered, o) 90 | } 91 | } 92 | return nbs.bs.AddBlocks(ctx, filtered) 93 | } 94 | 95 | // DeleteBlock deletes a block. 96 | func (nbs *BlockService) DeleteBlock(ctx context.Context, o cid.Cid) error { 97 | return nbs.bs.DeleteBlock(ctx, o) 98 | } 99 | -------------------------------------------------------------------------------- /ipfs/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ipfs-shipyard/nopfs/ipfs 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.3 6 | 7 | require ( 8 | github.com/ipfs-shipyard/nopfs v0.0.13 9 | github.com/ipfs/boxo v0.25.0 10 | github.com/ipfs/go-block-format v0.2.0 11 | github.com/ipfs/go-cid v0.4.1 12 | github.com/ipfs/go-log/v2 v2.5.1 13 | github.com/ipld/go-ipld-prime v0.21.0 14 | github.com/libp2p/go-libp2p v0.37.2 15 | ) 16 | 17 | require ( 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 21 | github.com/fsnotify/fsnotify v1.6.0 // indirect 22 | github.com/go-logr/logr v1.4.2 // indirect 23 | github.com/go-logr/stdr v1.2.2 // indirect 24 | github.com/gogo/protobuf v1.3.2 // indirect 25 | github.com/google/gopacket v1.1.19 // indirect 26 | github.com/google/uuid v1.6.0 // indirect 27 | github.com/hashicorp/errwrap v1.1.0 // indirect 28 | github.com/hashicorp/go-multierror v1.1.1 // indirect 29 | github.com/hashicorp/golang-lru v1.0.2 // indirect 30 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 31 | github.com/ipfs/bbloom v0.0.4 // indirect 32 | github.com/ipfs/go-datastore v0.6.0 // indirect 33 | github.com/ipfs/go-ipfs-util v0.0.3 // indirect 34 | github.com/ipfs/go-ipld-format v0.6.0 // indirect 35 | github.com/ipfs/go-metrics-interface v0.0.1 // indirect 36 | github.com/jbenet/goprocess v0.1.4 // indirect 37 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 38 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 39 | github.com/libp2p/go-cidranger v1.1.0 // indirect 40 | github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect 41 | github.com/libp2p/go-libp2p-kad-dht v0.27.0 // indirect 42 | github.com/libp2p/go-libp2p-kbucket v0.6.4 // indirect 43 | github.com/libp2p/go-libp2p-record v0.2.0 // indirect 44 | github.com/libp2p/go-libp2p-routing-helpers v0.7.4 // indirect 45 | github.com/libp2p/go-msgio v0.3.0 // indirect 46 | github.com/libp2p/go-netroute v0.2.1 // indirect 47 | github.com/mattn/go-isatty v0.0.20 // indirect 48 | github.com/miekg/dns v1.1.62 // indirect 49 | github.com/minio/sha256-simd v1.0.1 // indirect 50 | github.com/mr-tron/base58 v1.2.0 // indirect 51 | github.com/multiformats/go-base32 v0.1.0 // indirect 52 | github.com/multiformats/go-base36 v0.2.0 // indirect 53 | github.com/multiformats/go-multiaddr v0.13.0 // indirect 54 | github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect 55 | github.com/multiformats/go-multibase v0.2.0 // indirect 56 | github.com/multiformats/go-multicodec v0.9.0 // indirect 57 | github.com/multiformats/go-multihash v0.2.3 // indirect 58 | github.com/multiformats/go-multistream v0.6.0 // indirect 59 | github.com/multiformats/go-varint v0.0.7 // indirect 60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 61 | github.com/polydawn/refmt v0.89.0 // indirect 62 | github.com/prometheus/client_golang v1.20.5 // indirect 63 | github.com/prometheus/client_model v0.6.1 // indirect 64 | github.com/prometheus/common v0.60.0 // indirect 65 | github.com/prometheus/procfs v0.15.1 // indirect 66 | github.com/samber/lo v1.47.0 // indirect 67 | github.com/spaolacci/murmur3 v1.1.0 // indirect 68 | github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect 69 | github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect 70 | go.opencensus.io v0.24.0 // indirect 71 | go.opentelemetry.io/otel v1.31.0 // indirect 72 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 73 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 74 | go.uber.org/multierr v1.11.0 // indirect 75 | go.uber.org/zap v1.27.0 // indirect 76 | golang.org/x/crypto v0.28.0 // indirect 77 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 78 | golang.org/x/mod v0.21.0 // indirect 79 | golang.org/x/net v0.30.0 // indirect 80 | golang.org/x/sync v0.8.0 // indirect 81 | golang.org/x/sys v0.26.0 // indirect 82 | golang.org/x/text v0.19.0 // indirect 83 | golang.org/x/tools v0.26.0 // indirect 84 | gonum.org/v1/gonum v0.15.0 // indirect 85 | google.golang.org/protobuf v1.35.1 // indirect 86 | gopkg.in/yaml.v3 v3.0.1 // indirect 87 | lukechampine.com/blake3 v1.3.0 // indirect 88 | ) 89 | -------------------------------------------------------------------------------- /ipfs/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 4 | github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= 5 | github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 6 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 7 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 8 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 9 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 13 | github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= 14 | github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= 15 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 16 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 18 | github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= 19 | github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= 24 | github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= 25 | github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 26 | github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 27 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= 28 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 29 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 30 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 31 | github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= 32 | github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= 33 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 34 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 35 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 36 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 37 | github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= 38 | github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= 39 | github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= 40 | github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= 41 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 42 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 43 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 44 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 45 | github.com/gammazero/chanqueue v1.0.0 h1:FER/sMailGFA3DDvFooEkipAMU+3c9Bg3bheloPSz6o= 46 | github.com/gammazero/chanqueue v1.0.0/go.mod h1:fMwpwEiuUgpab0sH4VHiVcEoji1pSi+EIzeG4TPeKPc= 47 | github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34= 48 | github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo= 49 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 50 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 51 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 52 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 53 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 54 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 55 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 56 | github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 57 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 58 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 59 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 60 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 61 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 62 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 63 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 64 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 65 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 66 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 68 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 69 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 70 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 71 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 72 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 73 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 74 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 75 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 76 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 77 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 78 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 79 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 80 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 81 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 82 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 83 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 84 | github.com/google/pprof v0.0.0-20241017200806-017d972448fc h1:NGyrhhFhwvRAZg02jnYVg3GBQy0qGBKmFQJwaPmpmxs= 85 | github.com/google/pprof v0.0.0-20241017200806-017d972448fc/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 86 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 87 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 88 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 89 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 90 | github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= 91 | github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 92 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 93 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 94 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 95 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 96 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 97 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 98 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 99 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 100 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 101 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 102 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 103 | github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= 104 | github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= 105 | github.com/ipfs-shipyard/nopfs v0.0.13 h1:eXyI5x0+Y/dgjHl3RgSrVqg+1YwwybhEuRgo3BjNazM= 106 | github.com/ipfs-shipyard/nopfs v0.0.13/go.mod h1:mQyd0BElYI2gB/kq/Oue97obP4B3os4eBmgfPZ+hnrE= 107 | github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 108 | github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 109 | github.com/ipfs/boxo v0.25.0 h1:FNZaKVirUDafGz3Y9sccztynAUazs9GfSapLk/5c7is= 110 | github.com/ipfs/boxo v0.25.0/go.mod h1:MQVkL3V8RfuIsn+aajCR0MXLl8nRlz+5uGlHMWFVyuE= 111 | github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= 112 | github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= 113 | github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 114 | github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 115 | github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 116 | github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 117 | github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 118 | github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 119 | github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 120 | github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 121 | github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= 122 | github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= 123 | github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE= 124 | github.com/ipfs/go-ipfs-pq v0.0.3/go.mod h1:btNw5hsHBpRcSSgZtiNm/SLj5gYIZ18AKtv3kERkRb4= 125 | github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 126 | github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 127 | github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 128 | github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 129 | github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= 130 | github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= 131 | github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 132 | github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 133 | github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 134 | github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 135 | github.com/ipfs/go-peertaskqueue v0.8.1 h1:YhxAs1+wxb5jk7RvS0LHdyiILpNmRIRnZVztekOF0pg= 136 | github.com/ipfs/go-peertaskqueue v0.8.1/go.mod h1:Oxxd3eaK279FxeydSPPVGHzbwVeHjatZ2GA8XD+KbPU= 137 | github.com/ipfs/go-test v0.0.4 h1:DKT66T6GBB6PsDFLoO56QZPrOmzJkqU1FZH5C9ySkew= 138 | github.com/ipfs/go-test v0.0.4/go.mod h1:qhIM1EluEfElKKM6fnWxGn822/z9knUGM1+I/OAQNKI= 139 | github.com/ipfs/go-unixfsnode v1.9.2 h1:0A12BYs4XOtDPJTMlwmNPlllDfqcc4yie4e919hcUXk= 140 | github.com/ipfs/go-unixfsnode v1.9.2/go.mod h1:v1nuMFHf4QTIhFUdPMvg1nQu7AqDLvIdwyvJ531Ot1U= 141 | github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= 142 | github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= 143 | github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= 144 | github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= 145 | github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 146 | github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 147 | github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 148 | github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= 149 | github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= 150 | github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 151 | github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 152 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 153 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 154 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 155 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 156 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 157 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 158 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 159 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 160 | github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= 161 | github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= 162 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 163 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 164 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 165 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 166 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 167 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 168 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 169 | github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 170 | github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 171 | github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= 172 | github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= 173 | github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= 174 | github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= 175 | github.com/libp2p/go-libp2p v0.37.2 h1:Irh+n9aDPTLt9wJYwtlHu6AhMUipbC1cGoJtOiBqI9c= 176 | github.com/libp2p/go-libp2p v0.37.2/go.mod h1:M8CRRywYkqC6xKHdZ45hmqVckBj5z4mRLIMLWReypz8= 177 | github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= 178 | github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= 179 | github.com/libp2p/go-libp2p-kad-dht v0.27.0 h1:1Ea32tVTPiAfaLpPMbaBWFJgbsi/JpMqC2YBuFdf32o= 180 | github.com/libp2p/go-libp2p-kad-dht v0.27.0/go.mod h1:ixhjLuzaXSGtWsKsXTj7erySNuVC4UP7NO015cRrF14= 181 | github.com/libp2p/go-libp2p-kbucket v0.6.4 h1:OjfiYxU42TKQSB8t8WYd8MKhYhMJeO2If+NiuKfb6iQ= 182 | github.com/libp2p/go-libp2p-kbucket v0.6.4/go.mod h1:jp6w82sczYaBsAypt5ayACcRJi0lgsba7o4TzJKEfWA= 183 | github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= 184 | github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= 185 | github.com/libp2p/go-libp2p-routing-helpers v0.7.4 h1:6LqS1Bzn5CfDJ4tzvP9uwh42IB7TJLNFJA6dEeGBv84= 186 | github.com/libp2p/go-libp2p-routing-helpers v0.7.4/go.mod h1:we5WDj9tbolBXOuF1hGOkR+r7Uh1408tQbAKaT5n1LE= 187 | github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= 188 | github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= 189 | github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= 190 | github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= 191 | github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= 192 | github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= 193 | github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= 194 | github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= 195 | github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= 196 | github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= 197 | github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= 198 | github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= 199 | github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= 200 | github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= 201 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 202 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 203 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 204 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 205 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 206 | github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= 207 | github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= 208 | github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= 209 | github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= 210 | github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 211 | github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 212 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 213 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 214 | github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 215 | github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 216 | github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 217 | github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 218 | github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ= 219 | github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= 220 | github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= 221 | github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= 222 | github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= 223 | github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= 224 | github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 225 | github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 226 | github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 227 | github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 228 | github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 229 | github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 230 | github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= 231 | github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= 232 | github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 233 | github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 234 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 235 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 236 | github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= 237 | github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= 238 | github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= 239 | github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 240 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 241 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 242 | github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= 243 | github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= 244 | github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= 245 | github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= 246 | github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc= 247 | github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= 248 | github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= 249 | github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= 250 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 251 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 252 | github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= 253 | github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= 254 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 255 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 256 | github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= 257 | github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= 258 | github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= 259 | github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= 260 | github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= 261 | github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= 262 | github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= 263 | github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= 264 | github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= 265 | github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= 266 | github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= 267 | github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= 268 | github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= 269 | github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= 270 | github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= 271 | github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= 272 | github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk= 273 | github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= 274 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 275 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 276 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 277 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 278 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 279 | github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= 280 | github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 281 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 282 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 283 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 284 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 285 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 286 | github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= 287 | github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= 288 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 289 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 290 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 291 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 292 | github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= 293 | github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= 294 | github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= 295 | github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= 296 | github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= 297 | github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= 298 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 299 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 300 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 301 | github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= 302 | github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 303 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 304 | github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 305 | github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 306 | github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 307 | github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 308 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 309 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 310 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 311 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 312 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 313 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 314 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 315 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 316 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 317 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 318 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 319 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 320 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 321 | github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 322 | github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= 323 | github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= 324 | github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 325 | github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 326 | github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc h1:BCPnHtcboadS0DvysUuJXZ4lWVv5Bh5i7+tbIyi+ck4= 327 | github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc/go.mod h1:r45hJU7yEoA81k6MWNhpMj/kms0n14dkzkxYHoB96UM= 328 | github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= 329 | github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= 330 | github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= 331 | github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 332 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 333 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 334 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 335 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 336 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 337 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= 338 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= 339 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= 340 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= 341 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= 342 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 343 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 344 | go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= 345 | go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= 346 | go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= 347 | go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= 348 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 349 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 350 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 351 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 352 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 353 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 354 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 355 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 356 | go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 357 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 358 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 359 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 360 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 361 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 362 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 363 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 364 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 365 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= 366 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= 367 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 368 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 369 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 370 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 371 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 372 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 373 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 374 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 375 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 376 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 377 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 378 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 379 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 380 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 381 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 382 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 383 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 384 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 385 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 386 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 387 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 388 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 389 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 390 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 391 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 392 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 393 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 394 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 395 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 396 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 397 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 398 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 399 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 400 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 401 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 406 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 407 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 408 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 409 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 410 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 411 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 412 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 413 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 414 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 415 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 416 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 417 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 418 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 419 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 420 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 421 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 422 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 423 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 424 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 425 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 426 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 427 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 428 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 429 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 430 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 431 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 432 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 433 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 434 | gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= 435 | gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= 436 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 437 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 438 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 439 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 440 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 441 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 442 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 443 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 444 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 445 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 446 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 447 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 448 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 449 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 450 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 451 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 452 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 453 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 454 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 455 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 456 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 457 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 458 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 459 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 460 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 461 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 462 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 463 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 464 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 465 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 466 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 467 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 468 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 469 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 470 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 471 | lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= 472 | lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= 473 | -------------------------------------------------------------------------------- /ipfs/ipfs.go: -------------------------------------------------------------------------------- 1 | // Package ipfs provides wrapper implementations of key layers in the go-ipfs 2 | // stack to enable content-blocking. 3 | package ipfs 4 | 5 | import ( 6 | logging "github.com/ipfs/go-log/v2" 7 | ) 8 | 9 | var logger = logging.Logger("nopfs-blocks") 10 | -------------------------------------------------------------------------------- /ipfs/namesys.go: -------------------------------------------------------------------------------- 1 | package ipfs 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ipfs-shipyard/nopfs" 7 | "github.com/ipfs/boxo/namesys" 8 | "github.com/ipfs/boxo/path" 9 | crypto "github.com/libp2p/go-libp2p/core/crypto" 10 | ) 11 | 12 | var _ namesys.NameSystem = (*NameSystem)(nil) 13 | 14 | // NameSystem implements a blocking namesys.NameSystem implementation. 15 | type NameSystem struct { 16 | blocker *nopfs.Blocker 17 | ns namesys.NameSystem 18 | } 19 | 20 | // WrapNameSystem wraps the given NameSystem with a content-blocking layer 21 | // for Resolve operations. 22 | func WrapNameSystem(ns namesys.NameSystem, blocker *nopfs.Blocker) namesys.NameSystem { 23 | logger.Debug("NameSystem wrapped with content blocker") 24 | return &NameSystem{ 25 | blocker: blocker, 26 | ns: ns, 27 | } 28 | } 29 | 30 | // Resolve resolves an IPNS name unless it is blocked. 31 | func (ns *NameSystem) Resolve(ctx context.Context, p path.Path, options ...namesys.ResolveOption) (namesys.Result, error) { 32 | if err := ns.blocker.IsPathBlocked(p).ToError(); err != nil { 33 | logger.Warn(err.Response) 34 | return namesys.Result{}, err 35 | } 36 | return ns.ns.Resolve(ctx, p, options...) 37 | } 38 | 39 | // ResolveAsync resolves an IPNS name asynchronously unless it is blocked. 40 | func (ns *NameSystem) ResolveAsync(ctx context.Context, p path.Path, options ...namesys.ResolveOption) <-chan namesys.AsyncResult { 41 | status := ns.blocker.IsPathBlocked(p) 42 | if err := status.ToError(); err != nil { 43 | logger.Warn(err.Response) 44 | ch := make(chan namesys.AsyncResult, 1) 45 | ch <- namesys.AsyncResult{ 46 | Path: status.Path, 47 | Err: err, 48 | } 49 | close(ch) 50 | return ch 51 | } 52 | 53 | return ns.ns.ResolveAsync(ctx, p, options...) 54 | } 55 | 56 | // Publish publishes an IPNS record. 57 | func (ns *NameSystem) Publish(ctx context.Context, name crypto.PrivKey, value path.Path, options ...namesys.PublishOption) error { 58 | return ns.ns.Publish(ctx, name, value, options...) 59 | } 60 | -------------------------------------------------------------------------------- /ipfs/resolver.go: -------------------------------------------------------------------------------- 1 | package ipfs 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ipfs-shipyard/nopfs" 7 | "github.com/ipfs/boxo/path" 8 | "github.com/ipfs/boxo/path/resolver" 9 | "github.com/ipfs/go-cid" 10 | "github.com/ipld/go-ipld-prime" 11 | ) 12 | 13 | var _ resolver.Resolver = (*Resolver)(nil) 14 | 15 | // Resolver implements a blocking path.Resolver. 16 | type Resolver struct { 17 | blocker *nopfs.Blocker 18 | resolver resolver.Resolver 19 | } 20 | 21 | // WrapResolver wraps the given path Resolver with a content-blocking layer 22 | // for Resolve operations. 23 | func WrapResolver(res resolver.Resolver, blocker *nopfs.Blocker) resolver.Resolver { 24 | logger.Debugf("Path resolved wrapped with content blocker") 25 | return &Resolver{ 26 | blocker: blocker, 27 | resolver: res, 28 | } 29 | } 30 | 31 | // ResolveToLastNode checks if the given path is blocked before resolving. 32 | func (res *Resolver) ResolveToLastNode(ctx context.Context, fpath path.ImmutablePath) (cid.Cid, []string, error) { 33 | if err := res.blocker.IsPathBlocked(fpath).ToError(); err != nil { 34 | logger.Warn(err.Response) 35 | return cid.Undef, nil, err 36 | } 37 | return res.resolver.ResolveToLastNode(ctx, fpath) 38 | } 39 | 40 | // ResolvePath checks if the given path is blocked before resolving. 41 | func (res *Resolver) ResolvePath(ctx context.Context, fpath path.ImmutablePath) (ipld.Node, ipld.Link, error) { 42 | if err := res.blocker.IsPathBlocked(fpath).ToError(); err != nil { 43 | logger.Warn(err.Response) 44 | return nil, nil, err 45 | } 46 | return res.resolver.ResolvePath(ctx, fpath) 47 | } 48 | 49 | // ResolvePathComponents checks if the given path is blocked before resolving. 50 | func (res *Resolver) ResolvePathComponents(ctx context.Context, fpath path.ImmutablePath) ([]ipld.Node, error) { 51 | if err := res.blocker.IsPathBlocked(fpath).ToError(); err != nil { 52 | logger.Warn(err.Response) 53 | return nil, err 54 | } 55 | return res.resolver.ResolvePathComponents(ctx, fpath) 56 | } 57 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs-shipyard/nopfs/9f5e735ad88065efc6d7947ce8008d83cc828ed0/logo.png -------------------------------------------------------------------------------- /logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs-shipyard/nopfs/9f5e735ad88065efc6d7947ce8008d83cc828ed0/logo.xcf -------------------------------------------------------------------------------- /nopfs-kubo-plugin/Makefile: -------------------------------------------------------------------------------- 1 | SRC := $(shell find .. -type f -name '*.go') 2 | IPFS_PATH ?= $(HOME)/.ipfs 3 | 4 | export CGO_ENABLED := 1 5 | 6 | all: build 7 | 8 | build: $(SRC) 9 | go build -buildmode=plugin -trimpath -o nopfs-kubo-plugin 10 | 11 | install: build 12 | mkdir -p "$(IPFS_PATH)/.ipfs/plugins" 13 | install -Dm700 nopfs-kubo-plugin "$(IPFS_PATH)/plugins/nopfs-kubo-plugin" 14 | 15 | dist: build 16 | mkdir -p dist/nopfs-kubo-plugin 17 | cp nopfs-kubo-plugin README.md dist/nopfs-kubo-plugin/ 18 | chmod +x dist/nopfs-kubo-plugin/nopfs-kubo-plugin 19 | tar -C dist -zcf nopfs-kubo-plugin.tar.gz nopfs-kubo-plugin 20 | rm -rf nopfs-kubo-plugin 21 | echo "Packaged as nopfs-kubo-plugin.tar.gz" 22 | 23 | .PHONY: install 24 | -------------------------------------------------------------------------------- /nopfs-kubo-plugin/README.md: -------------------------------------------------------------------------------- 1 | # nopfs-kubo-plugin 2 | 3 | ## Installation 4 | 5 | 1. Copy the binary `nopfs-kubo-plugin` to `~/.ipfs/plugins`. 6 | 2. Write a custom denylist file or simply download the [BadBits denylist](https://badbits.dwebops.pub/badbits.deny) and place them in `~/.config/ipfs/denylists/`. 7 | 3. Start Kubo (`ipfs daemon`). The plugin should be loaded automatically and existing denylists tracked for updates from that point (no restarts required). See Kubo log output for confirmation. 8 | 9 | ## Denylist syntax 10 | 11 | Denylist files must have the `.deny` extension. The content consists of an optional header and a body made of blocking rules as follows: 12 | 13 | 14 | ``` 15 | version: 1 16 | name: IPFSorp blocking list 17 | description: A collection of bad things we have found in the universe 18 | author: abuse-ipfscorp@example.com 19 | hints: 20 | gateway_status: 410 21 | double_hash_fn: sha256 22 | double_hash_enc: hex 23 | --- 24 | # Blocking by CID - blocks wrapped multihash. 25 | # Does not block subpaths. 26 | /ipfs/bafybeihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq 27 | 28 | # Block all subpaths 29 | /ipfs/QmdWFA9FL52hx3j9EJZPQP1ZUH8Ygi5tLCX2cRDs6knSf8/* 30 | 31 | # Block some subpaths (equivalent rules) 32 | /ipfs/Qmah2YDTfrox4watLCr3YgKyBwvjq8FJZEFdWY6WtJ3Xt2/test* 33 | /ipfs/QmTuvSQbEDR3sarFAN9kAeXBpiBCyYYNxdxciazBba11eC/test/* 34 | 35 | # Block some subpaths with exceptions 36 | /ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked* 37 | +/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blockednot 38 | +/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/not 39 | +/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/exceptions* 40 | 41 | # Block IPNS domain name 42 | /ipns/domain.example 43 | 44 | # Block IPNS domain name and path 45 | /ipns/domain2.example/path 46 | 47 | # Block IPNS key - blocks wrapped multihash. 48 | /ipns/k51qzi5uqu5dhmzyv3zac033i7rl9hkgczxyl81lwoukda2htteop7d3x0y1mf 49 | 50 | # Legacy CID double-hash block 51 | # sha256(bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e/) 52 | # blocks only this CID 53 | //d9d295bde21f422d471a90f2a37ec53049fdf3e5fa3ee2e8f20e10003da429e7 54 | 55 | # Legacy Path double-hash block 56 | # Blocks bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e/path 57 | # but not any other paths. 58 | //3f8b9febd851873b3774b937cce126910699ceac56e72e64b866f8e258d09572 59 | 60 | # Double hash CID block 61 | # base58btc-sha256-multihash(QmVTF1yEejXd9iMgoRTFDxBv7HAz9kuZcQNBzHrceuK9HR) 62 | # Blocks bafybeidjwik6im54nrpfg7osdvmx7zojl5oaxqel5cmsz46iuelwf5acja 63 | # and QmVTF1yEejXd9iMgoRTFDxBv7HAz9kuZcQNBzHrceuK9HR etc. by multihash 64 | //QmX9dhRcQcKUw3Ws8485T5a9dtjrSCQaUAHnG4iK9i4ceM 65 | 66 | # Double hash Path block using blake3 hashing 67 | # base58btc-blake3-multihash(gW7Nhu4HrfDtphEivm3Z9NNE7gpdh5Tga8g6JNZc1S8E47/path) 68 | # Blocks /ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path 69 | # /ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path 70 | # /ipfs/f01701e20903cf61d46521b05f926ba1634628d0bba8a7ffb5b6d5a3ca310682ca63b5ef0/path etc... 71 | # But not /path2 72 | //QmbK7LDv5NNBvYQzNfm2eED17SNLt1yNMapcUhSuNLgkqz 73 | ``` 74 | 75 | -------------------------------------------------------------------------------- /nopfs-kubo-plugin/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ipfs-shipyard/nopfs/nopfs-kubo-plugin 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.3 6 | 7 | // Retract test releases that conflict with naming scheme. 8 | retract [v0.21.0-rc1-test10, v0.21.0-rc1-test999] 9 | 10 | require ( 11 | github.com/ipfs-shipyard/nopfs v0.0.13 12 | github.com/ipfs-shipyard/nopfs/ipfs v0.25.0 13 | github.com/ipfs/go-log/v2 v2.5.1 14 | github.com/ipfs/kubo v0.32.1 15 | go.uber.org/fx v1.23.0 16 | ) 17 | 18 | require ( 19 | bazil.org/fuse v0.0.0-20200117225306-7b5117fecadc // indirect 20 | github.com/Jorropo/jsync v1.0.1 // indirect 21 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect 22 | github.com/benbjohnson/clock v1.3.5 // indirect 23 | github.com/beorn7/perks v1.0.1 // indirect 24 | github.com/blang/semver/v4 v4.0.0 // indirect 25 | github.com/caddyserver/certmagic v0.21.4 // indirect 26 | github.com/caddyserver/zerossl v0.1.3 // indirect 27 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 28 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 29 | github.com/containerd/cgroups v1.1.0 // indirect 30 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 31 | github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect 32 | github.com/cskr/pubsub v1.0.2 // indirect 33 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 34 | github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect 35 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 36 | github.com/docker/go-units v0.5.0 // indirect 37 | github.com/dustin/go-humanize v1.0.1 // indirect 38 | github.com/elastic/gosigar v0.14.3 // indirect 39 | github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 // indirect 40 | github.com/felixge/httpsnoop v1.0.4 // indirect 41 | github.com/flynn/noise v1.1.0 // indirect 42 | github.com/francoispqt/gojay v1.2.13 // indirect 43 | github.com/fsnotify/fsnotify v1.7.0 // indirect 44 | github.com/gabriel-vasile/mimetype v1.4.6 // indirect 45 | github.com/gammazero/chanqueue v1.0.0 // indirect 46 | github.com/gammazero/deque v1.0.0 // indirect 47 | github.com/go-logr/logr v1.4.2 // indirect 48 | github.com/go-logr/stdr v1.2.2 // indirect 49 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 50 | github.com/godbus/dbus/v5 v5.1.0 // indirect 51 | github.com/gogo/protobuf v1.3.2 // indirect 52 | github.com/google/gopacket v1.1.19 // indirect 53 | github.com/google/pprof v0.0.0-20241017200806-017d972448fc // indirect 54 | github.com/google/uuid v1.6.0 // indirect 55 | github.com/gorilla/websocket v1.5.3 // indirect 56 | github.com/hashicorp/errwrap v1.1.0 // indirect 57 | github.com/hashicorp/go-multierror v1.1.1 // indirect 58 | github.com/hashicorp/golang-lru v1.0.2 // indirect 59 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 60 | github.com/huin/goupnp v1.3.0 // indirect 61 | github.com/ipfs/bbloom v0.0.4 // indirect 62 | github.com/ipfs/boxo v0.25.0 // indirect 63 | github.com/ipfs/go-bitfield v1.1.0 // indirect 64 | github.com/ipfs/go-block-format v0.2.0 // indirect 65 | github.com/ipfs/go-blockservice v0.5.2 // indirect 66 | github.com/ipfs/go-cid v0.4.1 // indirect 67 | github.com/ipfs/go-cidutil v0.1.0 // indirect 68 | github.com/ipfs/go-datastore v0.6.0 // indirect 69 | github.com/ipfs/go-ds-measure v0.2.0 // indirect 70 | github.com/ipfs/go-fs-lock v0.0.7 // indirect 71 | github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 72 | github.com/ipfs/go-ipfs-delay v0.0.1 // indirect 73 | github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 74 | github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect 75 | github.com/ipfs/go-ipfs-pq v0.0.3 // indirect 76 | github.com/ipfs/go-ipfs-redirects-file v0.1.2 // indirect 77 | github.com/ipfs/go-ipfs-util v0.0.3 // indirect 78 | github.com/ipfs/go-ipld-cbor v0.2.0 // indirect 79 | github.com/ipfs/go-ipld-format v0.6.0 // indirect 80 | github.com/ipfs/go-ipld-legacy v0.2.1 // indirect 81 | github.com/ipfs/go-log v1.0.5 // indirect 82 | github.com/ipfs/go-merkledag v0.11.0 // indirect 83 | github.com/ipfs/go-metrics-interface v0.0.1 // indirect 84 | github.com/ipfs/go-peertaskqueue v0.8.1 // indirect 85 | github.com/ipfs/go-unixfsnode v1.9.2 // indirect 86 | github.com/ipfs/go-verifcid v0.0.3 // indirect 87 | github.com/ipld/go-car v0.6.2 // indirect 88 | github.com/ipld/go-car/v2 v2.14.2 // indirect 89 | github.com/ipld/go-codec-dagpb v1.6.0 // indirect 90 | github.com/ipld/go-ipld-prime v0.21.0 // indirect 91 | github.com/ipshipyard/p2p-forge v0.0.2 // indirect 92 | github.com/jackpal/go-nat-pmp v1.0.2 // indirect 93 | github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect 94 | github.com/jbenet/goprocess v0.1.4 // indirect 95 | github.com/klauspost/compress v1.17.11 // indirect 96 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 97 | github.com/koron/go-ssdp v0.0.4 // indirect 98 | github.com/libdns/libdns v0.2.2 // indirect 99 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 100 | github.com/libp2p/go-cidranger v1.1.0 // indirect 101 | github.com/libp2p/go-doh-resolver v0.4.0 // indirect 102 | github.com/libp2p/go-flow-metrics v0.2.0 // indirect 103 | github.com/libp2p/go-libp2p v0.37.2 // indirect 104 | github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect 105 | github.com/libp2p/go-libp2p-kad-dht v0.28.1 // indirect 106 | github.com/libp2p/go-libp2p-kbucket v0.6.4 // indirect 107 | github.com/libp2p/go-libp2p-pubsub v0.12.0 // indirect 108 | github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect 109 | github.com/libp2p/go-libp2p-record v0.2.0 // indirect 110 | github.com/libp2p/go-libp2p-routing-helpers v0.7.4 // indirect 111 | github.com/libp2p/go-libp2p-xor v0.1.0 // indirect 112 | github.com/libp2p/go-msgio v0.3.0 // indirect 113 | github.com/libp2p/go-nat v0.2.0 // indirect 114 | github.com/libp2p/go-netroute v0.2.1 // indirect 115 | github.com/libp2p/go-reuseport v0.4.0 // indirect 116 | github.com/libp2p/go-yamux/v4 v4.0.1 // indirect 117 | github.com/libp2p/zeroconf/v2 v2.2.0 // indirect 118 | github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect 119 | github.com/mattn/go-isatty v0.0.20 // indirect 120 | github.com/mholt/acmez/v2 v2.0.3 // indirect 121 | github.com/miekg/dns v1.1.62 // indirect 122 | github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect 123 | github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect 124 | github.com/minio/sha256-simd v1.0.1 // indirect 125 | github.com/mr-tron/base58 v1.2.0 // indirect 126 | github.com/multiformats/go-base32 v0.1.0 // indirect 127 | github.com/multiformats/go-base36 v0.2.0 // indirect 128 | github.com/multiformats/go-multiaddr v0.13.0 // indirect 129 | github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect 130 | github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect 131 | github.com/multiformats/go-multibase v0.2.0 // indirect 132 | github.com/multiformats/go-multicodec v0.9.0 // indirect 133 | github.com/multiformats/go-multihash v0.2.3 // indirect 134 | github.com/multiformats/go-multistream v0.6.0 // indirect 135 | github.com/multiformats/go-varint v0.0.7 // indirect 136 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 137 | github.com/onsi/ginkgo/v2 v2.20.2 // indirect 138 | github.com/opencontainers/runtime-spec v1.2.0 // indirect 139 | github.com/opentracing/opentracing-go v1.2.0 // indirect 140 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 141 | github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect 142 | github.com/pion/datachannel v1.5.9 // indirect 143 | github.com/pion/dtls/v2 v2.2.12 // indirect 144 | github.com/pion/ice/v2 v2.3.36 // indirect 145 | github.com/pion/interceptor v0.1.37 // indirect 146 | github.com/pion/logging v0.2.2 // indirect 147 | github.com/pion/mdns v0.0.12 // indirect 148 | github.com/pion/randutil v0.1.0 // indirect 149 | github.com/pion/rtcp v1.2.14 // indirect 150 | github.com/pion/rtp v1.8.9 // indirect 151 | github.com/pion/sctp v1.8.33 // indirect 152 | github.com/pion/sdp/v3 v3.0.9 // indirect 153 | github.com/pion/srtp/v2 v2.0.20 // indirect 154 | github.com/pion/stun v0.6.1 // indirect 155 | github.com/pion/transport/v2 v2.2.10 // indirect 156 | github.com/pion/turn/v2 v2.1.6 // indirect 157 | github.com/pion/webrtc/v3 v3.3.4 // indirect 158 | github.com/pkg/errors v0.9.1 // indirect 159 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 160 | github.com/polydawn/refmt v0.89.0 // indirect 161 | github.com/prometheus/client_golang v1.20.5 // indirect 162 | github.com/prometheus/client_model v0.6.1 // indirect 163 | github.com/prometheus/common v0.60.0 // indirect 164 | github.com/prometheus/procfs v0.15.1 // indirect 165 | github.com/quic-go/qpack v0.5.1 // indirect 166 | github.com/quic-go/quic-go v0.48.2 // indirect 167 | github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect 168 | github.com/raulk/go-watchdog v1.3.0 // indirect 169 | github.com/samber/lo v1.47.0 // indirect 170 | github.com/spaolacci/murmur3 v1.1.0 // indirect 171 | github.com/stretchr/testify v1.9.0 // indirect 172 | github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb // indirect 173 | github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect 174 | github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect 175 | github.com/whyrusleeping/cbor-gen v0.1.2 // indirect 176 | github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f // indirect 177 | github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect 178 | github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7 // indirect 179 | github.com/wlynxg/anet v0.0.5 // indirect 180 | github.com/zeebo/blake3 v0.2.4 // indirect 181 | go.opencensus.io v0.24.0 // indirect 182 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect 183 | go.opentelemetry.io/otel v1.31.0 // indirect 184 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 185 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 186 | go.uber.org/atomic v1.11.0 // indirect 187 | go.uber.org/dig v1.18.0 // indirect 188 | go.uber.org/mock v0.5.0 // indirect 189 | go.uber.org/multierr v1.11.0 // indirect 190 | go.uber.org/zap v1.27.0 // indirect 191 | go4.org v0.0.0-20230225012048-214862532bf5 // indirect 192 | golang.org/x/crypto v0.28.0 // indirect 193 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 194 | golang.org/x/mod v0.21.0 // indirect 195 | golang.org/x/net v0.30.0 // indirect 196 | golang.org/x/sync v0.8.0 // indirect 197 | golang.org/x/sys v0.26.0 // indirect 198 | golang.org/x/text v0.19.0 // indirect 199 | golang.org/x/tools v0.26.0 // indirect 200 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 201 | gonum.org/v1/gonum v0.15.0 // indirect 202 | google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect 203 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect 204 | google.golang.org/protobuf v1.35.1 // indirect 205 | gopkg.in/yaml.v3 v3.0.1 // indirect 206 | lukechampine.com/blake3 v1.3.0 // indirect 207 | ) 208 | -------------------------------------------------------------------------------- /nopfs-kubo-plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ipfs-shipyard/nopfs" 5 | "github.com/ipfs-shipyard/nopfs/ipfs" 6 | logging "github.com/ipfs/go-log/v2" 7 | "github.com/ipfs/kubo/core" 8 | "github.com/ipfs/kubo/core/node" 9 | "github.com/ipfs/kubo/plugin" 10 | "go.uber.org/fx" 11 | ) 12 | 13 | var logger = logging.Logger("nopfs") 14 | 15 | // Plugins sets the list of plugins to be loaded. 16 | var Plugins = []plugin.Plugin{ 17 | &nopfsPlugin{}, 18 | } 19 | 20 | // fxtestPlugin is used for testing the fx plugin. 21 | // It merely adds an fx option that logs a debug statement, so we can verify that it works in tests. 22 | type nopfsPlugin struct{} 23 | 24 | var _ plugin.PluginFx = (*nopfsPlugin)(nil) 25 | 26 | func (p *nopfsPlugin) Name() string { 27 | return "nopfs" 28 | } 29 | 30 | func (p *nopfsPlugin) Version() string { 31 | return "0.0.1" 32 | } 33 | 34 | func (p *nopfsPlugin) Init(env *plugin.Environment) error { 35 | return nil 36 | } 37 | 38 | // MakeBlocker is a factory for the blocker so that it can be provided with Fx. 39 | func MakeBlocker() (*nopfs.Blocker, error) { 40 | files, err := nopfs.GetDenylistFiles() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return nopfs.NewBlocker(files) 46 | } 47 | 48 | // PathResolvers returns wrapped PathResolvers for Kubo. 49 | func PathResolvers(fetchers node.FetchersIn, blocker *nopfs.Blocker) node.PathResolversOut { 50 | res := node.PathResolverConfig(fetchers) 51 | return node.PathResolversOut{ 52 | IPLDPathResolver: ipfs.WrapResolver(res.IPLDPathResolver, blocker), 53 | UnixFSPathResolver: ipfs.WrapResolver(res.UnixFSPathResolver, blocker), 54 | } 55 | } 56 | 57 | func (p *nopfsPlugin) Options(info core.FXNodeInfo) ([]fx.Option, error) { 58 | logging.SetLogLevel("nopfs", "INFO") 59 | logger.Info("Loading Nopfs plugin: content blocking") 60 | 61 | opts := append( 62 | info.FXOptions, 63 | fx.Provide(MakeBlocker), 64 | fx.Decorate(ipfs.WrapBlockService), 65 | fx.Decorate(ipfs.WrapNameSystem), 66 | fx.Decorate(PathResolvers), 67 | ) 68 | return opts, nil 69 | } 70 | -------------------------------------------------------------------------------- /nopfs.go: -------------------------------------------------------------------------------- 1 | // Package nopfs implements content blocking for the IPFS stack. 2 | // 3 | // nopfs provides an implementation of the compact denylist format (IPIP-383), 4 | // with methods to check whether IPFS paths and CIDs are blocked. 5 | // 6 | // In order to seamlessly be inserted into the IPFS stack, content-blocking 7 | // wrappers for several components are provided (BlockService, NameSystem, 8 | // Resolver...). A Kubo plugin (see kubo/) can be used to give Kubo 9 | // content-blocking superpowers. 10 | package nopfs 11 | 12 | import ( 13 | logging "github.com/ipfs/go-log/v2" 14 | ) 15 | 16 | var logger = logging.Logger("nopfs") 17 | -------------------------------------------------------------------------------- /nopfs_test.go: -------------------------------------------------------------------------------- 1 | package nopfs 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/ipfs-shipyard/nopfs/tester" 8 | "github.com/ipfs/boxo/path" 9 | "github.com/ipfs/go-cid" 10 | logging "github.com/ipfs/go-log/v2" 11 | ) 12 | 13 | type testBlocker struct { 14 | Blocker 15 | } 16 | 17 | type testHeader struct { 18 | DenylistHeader 19 | } 20 | 21 | func (th testHeader) Name() string { 22 | return th.DenylistHeader.Name 23 | } 24 | 25 | func (th testHeader) Hints() map[string]string { 26 | return th.DenylistHeader.Hints 27 | } 28 | 29 | func (tb *testBlocker) ReadHeader(r io.Reader) (tester.Header, error) { 30 | dl := Denylist{} 31 | 32 | err := dl.Header.Decode(r) 33 | if err != nil { 34 | return testHeader{}, err 35 | } 36 | return testHeader{ 37 | DenylistHeader: dl.Header, 38 | }, nil 39 | } 40 | 41 | func (tb *testBlocker) ReadDenylist(r io.ReadSeekCloser) error { 42 | tb.Blocker.Denylists = make(map[string]*Denylist) 43 | dl, err := NewDenylistReader(r) 44 | if err != nil { 45 | return err 46 | } 47 | dl.Filename = "test" 48 | tb.Blocker.Denylists["test"] = dl 49 | return nil 50 | } 51 | 52 | func (tb *testBlocker) IsPathBlocked(p path.Path) bool { 53 | res := tb.Blocker.IsPathBlocked(p) 54 | return res.Status == StatusBlocked 55 | } 56 | 57 | func (tb *testBlocker) IsCidBlocked(c cid.Cid) bool { 58 | res := tb.Blocker.IsCidBlocked(c) 59 | return res.Status == StatusBlocked 60 | } 61 | 62 | func TestSuite(t *testing.T) { 63 | logging.SetLogLevel("nopfs", "ERROR") 64 | 65 | tb := testBlocker{ 66 | Blocker: Blocker{}, 67 | } 68 | 69 | suite := &tester.Suite{ 70 | TestHeader: true, 71 | TestCID: true, 72 | TestCIDPath: true, 73 | TestIPNSPath: true, 74 | TestDoubleHashLegacy: true, 75 | TestDoubleHash: true, 76 | } 77 | 78 | err := suite.Run(&tb) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package nopfs 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ipfs/boxo/path" 7 | "github.com/ipfs/go-cid" 8 | ) 9 | 10 | // Status values 11 | const ( 12 | StatusNotFound Status = iota 13 | StatusBlocked 14 | StatusAllowed 15 | StatusErrored 16 | ) 17 | 18 | // Status represent represents whether an item is blocked, allowed or simply 19 | // not found in a Denylist. 20 | type Status int 21 | 22 | func (st Status) String() string { 23 | switch st { 24 | case StatusNotFound: 25 | return "not found" 26 | case StatusBlocked: 27 | return "blocked" 28 | case StatusAllowed: 29 | return "allowed" 30 | case StatusErrored: 31 | return "errored" 32 | } 33 | return "unknown" 34 | } 35 | 36 | // StatusResponse provides full information for a content-block lookup, 37 | // including the Filename and the Entry, when an associated rule is found. 38 | type StatusResponse struct { 39 | Cid cid.Cid 40 | Path path.Path 41 | Status Status 42 | Filename string 43 | Entry Entry 44 | Error error 45 | } 46 | 47 | // String provides a string with the details of a StatusResponse. 48 | func (r StatusResponse) String() string { 49 | if err := r.Error; err != nil { 50 | return err.Error() 51 | } 52 | 53 | path := "" 54 | if c := r.Cid; c.Defined() { 55 | path = c.String() 56 | } else { 57 | path = r.Path.String() 58 | } 59 | 60 | return fmt.Sprintf("%s: %s (%s:%d)", 61 | path, r.Status, 62 | r.Filename, r.Entry.Line, 63 | ) 64 | } 65 | 66 | // ToError returns nil if the Status of the StatusResponse is Allowed or Not Found. 67 | // When the status is Blocked or Errored, it returns a StatusError. 68 | func (r StatusResponse) ToError() *StatusError { 69 | if r.Status != StatusBlocked && r.Status != StatusErrored { 70 | return nil 71 | } 72 | 73 | return &StatusError{Response: r} 74 | } 75 | 76 | // StatusError implements the error interface and can be used to provide 77 | // information about a blocked-status in the form of an error. 78 | type StatusError struct { 79 | Response StatusResponse 80 | } 81 | 82 | func (err *StatusError) Error() string { 83 | if err := err.Response.Error; err != nil { 84 | return err.Error() 85 | } 86 | if c := err.Response.Cid; c.Defined() { 87 | return c.String() + " is blocked and cannot be provided" 88 | } 89 | return err.Response.Path.String() + " is blocked and cannot be provided" 90 | } 91 | -------------------------------------------------------------------------------- /subscription.go: -------------------------------------------------------------------------------- 1 | package nopfs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "net/http" 9 | "os" 10 | "time" 11 | ) 12 | 13 | // HTTPSubscriber represents a type that subscribes to a remote URL and appends data to a local file. 14 | type HTTPSubscriber struct { 15 | remoteURL string 16 | localFile string 17 | interval time.Duration 18 | stopChannel chan struct{} 19 | } 20 | 21 | // NewHTTPSubscriber creates a new Subscriber instance with the given parameters. 22 | func NewHTTPSubscriber(remoteURL, localFile string, interval time.Duration) (*HTTPSubscriber, error) { 23 | logger.Infof("Subscribing to remote denylist: %s", remoteURL) 24 | 25 | sub := HTTPSubscriber{ 26 | remoteURL: remoteURL, 27 | localFile: localFile, 28 | interval: interval, 29 | stopChannel: make(chan struct{}, 1), 30 | } 31 | 32 | _, err := os.Stat(localFile) 33 | // if not found, we perform a first sync before returning. 34 | // this is necessary as otherwise the Blocker does not find much 35 | // of the file 36 | if err != nil && errors.Is(err, fs.ErrNotExist) { 37 | logger.Infof("Performing first sync on: %s", localFile) 38 | err := sub.downloadAndAppend() 39 | if err != nil { 40 | return nil, err 41 | } 42 | } else if err != nil { 43 | return nil, err 44 | } 45 | 46 | go sub.subscribe() 47 | 48 | return &sub, nil 49 | } 50 | 51 | // subscribe starts the subscription process. 52 | func (s *HTTPSubscriber) subscribe() { 53 | timer := time.NewTimer(0) 54 | 55 | for { 56 | select { 57 | case <-s.stopChannel: 58 | logger.Infof("Stopping subscription on: %s", s.localFile) 59 | if !timer.Stop() { 60 | <-timer.C 61 | } 62 | return 63 | case <-timer.C: 64 | err := s.downloadAndAppend() 65 | if err != nil { 66 | logger.Error(err) 67 | } 68 | timer.Reset(s.interval) 69 | } 70 | } 71 | } 72 | 73 | // Stop stops the subscription process. 74 | func (s *HTTPSubscriber) Stop() { 75 | close(s.stopChannel) 76 | } 77 | 78 | func (s *HTTPSubscriber) downloadAndAppend() error { 79 | localFile, err := os.OpenFile(s.localFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 80 | if err != nil { 81 | return err 82 | } 83 | defer localFile.Close() 84 | 85 | // Get the file size of the local file 86 | localFileInfo, err := localFile.Stat() 87 | if err != nil { 88 | return err 89 | } 90 | 91 | localFileSize := localFileInfo.Size() 92 | 93 | // Create a HTTP GET request with the Range header to download only the missing bytes 94 | req, err := http.NewRequest("GET", s.remoteURL, nil) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | rangeHeader := fmt.Sprintf("bytes=%d-", localFileSize) 100 | req.Header.Set("Range", rangeHeader) 101 | 102 | logger.Debugf("%s: requesting bytes from %d: %s", s.localFile, localFileSize, req.URL) 103 | 104 | resp, err := http.DefaultClient.Do(req) 105 | if err != nil { 106 | return err 107 | } 108 | defer resp.Body.Close() 109 | 110 | switch { 111 | case resp.StatusCode == http.StatusPartialContent: 112 | _, err = io.Copy(localFile, resp.Body) 113 | if err != nil { 114 | return err 115 | } 116 | logger.Infof("%s: appended %d bytes", s.localFile, resp.ContentLength) 117 | case (resp.StatusCode >= http.StatusBadRequest && 118 | resp.StatusCode != http.StatusRequestedRangeNotSatisfiable) || 119 | resp.StatusCode >= http.StatusInternalServerError: 120 | return fmt.Errorf("%s: server returned with unexpected code %d", s.localFile, resp.StatusCode) 121 | // error is ignored, we continued subscribed 122 | } 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /subscription_test.go: -------------------------------------------------------------------------------- 1 | package nopfs 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func createTestServer() *httptest.Server { 13 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | // Simulate serving a file from the server 15 | data := []byte("This is the remote content.") 16 | contentLength := len(data) 17 | 18 | // Get the "Range" header from the request 19 | rangeHeader := r.Header.Get("Range") 20 | 21 | if rangeHeader != "" { 22 | var start, end int 23 | 24 | // Check for a range request in the format "bytes %d-" 25 | if n, _ := fmt.Sscanf(rangeHeader, "bytes=%d-", &start); n == 1 { 26 | // Handle open end range requests 27 | if start >= contentLength { 28 | http.Error(w, "Invalid Range header", http.StatusRequestedRangeNotSatisfiable) 29 | return 30 | } 31 | end = contentLength - 1 32 | } else if n, _ := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); n == 2 { 33 | // Check for valid byte range 34 | if start < 0 || end >= contentLength || start > end { 35 | http.Error(w, "Invalid Range header", http.StatusRequestedRangeNotSatisfiable) 36 | return 37 | } 38 | } else { 39 | http.Error(w, "Invalid Range header", http.StatusBadRequest) 40 | return 41 | } 42 | 43 | // Calculate the content range and length for the response 44 | contentRange := fmt.Sprintf("bytes %d-%d/%d", start, end, contentLength) 45 | w.Header().Set("Content-Range", contentRange) 46 | w.Header().Set("Content-Length", fmt.Sprint(end-start+1)) 47 | w.WriteHeader(http.StatusPartialContent) 48 | 49 | // Write the selected byte range to the response 50 | _, _ = w.Write(data[start : end+1]) 51 | } else { 52 | // If no "Range" header, serve the entire content 53 | w.Header().Set("Content-Range", fmt.Sprintf("bytes 0-%d/%d", contentLength-1, contentLength)) 54 | w.Header().Set("Content-Length", fmt.Sprint(contentLength)) 55 | w.WriteHeader(http.StatusPartialContent) 56 | 57 | _, _ = w.Write(data) 58 | } 59 | })) 60 | } 61 | 62 | func TestHTTPSubscriber(t *testing.T) { 63 | remoteServer := createTestServer() 64 | defer remoteServer.Close() 65 | 66 | localFile := "test-local-file.txt" 67 | defer os.Remove(localFile) 68 | 69 | subscriber, err := NewHTTPSubscriber(remoteServer.URL, localFile, 500*time.Millisecond) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | // Allow some time for subscription to run 75 | time.Sleep(2 * time.Second) 76 | subscriber.Stop() 77 | 78 | localFileContent, err := os.ReadFile(localFile) 79 | if err != nil { 80 | t.Errorf("Error reading local file: %v", err) 81 | } 82 | 83 | expectedContent := "This is the remote content." 84 | if string(localFileContent) != expectedContent { 85 | t.Errorf("Local file content is incorrect. Got: %s, Expected: %s", string(localFileContent), expectedContent) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tester/README.md: -------------------------------------------------------------------------------- 1 | # Compact denylist format blocker validator 2 | 3 | This module provides a test suite checks that the rules present in test.deny 4 | are correctly understood and processed by a Blocker implementation. 5 | -------------------------------------------------------------------------------- /tester/test.deny: -------------------------------------------------------------------------------- 1 | version: 1 2 | name: "Testing denylist" 3 | description: "This denylist is used to test blocker implementations. See tester.go." 4 | author: "@hsanjuan" 5 | --- 6 | # Most CIDs used correspond to adding the rule name (rule1, rule2...) with raw-leaves disabled. ie. `echo -n "rule1" | ipfs add --raw-leaves=false --only-hash --cid-version=1`. 7 | 8 | # rule1 9 | # Blocking by CID 10 | # This should block not only this, but also any 11 | # CID with the same underlying multihash like 12 | # bafkreihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq or 13 | # QmesfgDQ3q6prBy2Kg2gKbW4MAGuWiRP2DVuGA5MZSERLo 14 | /ipfs/bafybeihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq 15 | 16 | # rule2 17 | # Block all subpaths 18 | /ipfs/QmdWFA9FL52hx3j9EJZPQP1ZUH8Ygi5tLCX2cRDs6knSf8/* 19 | 20 | # rule3 21 | # Block some subpaths 22 | /ipfs/Qmah2YDTfrox4watLCr3YgKyBwvjq8FJZEFdWY6WtJ3Xt2/test* 23 | 24 | # rule4 25 | # Block some subpaths alt 26 | /ipfs/QmTuvSQbEDR3sarFAN9kAeXBpiBCyYYNxdxciazBba11eC/test/* 27 | 28 | # rule5 29 | # Block some subpaths with exceptions 30 | /ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked* 31 | !/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blockednot 32 | !/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/not 33 | !/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/exceptions* 34 | 35 | # rule6 36 | # Block IPNS domain name 37 | /ipns/domain.example 38 | 39 | # rule7 40 | # Block IPNS domain name and path 41 | /ipns/domain2.example/path 42 | 43 | # rule8 44 | # Block IPNS key 45 | # This should block not only this string but 46 | # also base32 47 | # bafzaajaiaejcaotjfs57kieazxny5japcmy5p2pgv2cic77tu6ogghttvurnrufx 48 | # or 12D3KooWDkNqEJNmreF3NYYFK1ws7Ra2fuW6cHBTu567SPV3LdYA 49 | /ipns/k51qzi5uqu5dhmzyv3zac033i7rl9hkgczxyl81lwoukda2htteop7d3x0y1mf 50 | 51 | # rule10 52 | # Legacy CID double-hash block 53 | # sha256(bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e/) 54 | # blocks only this cid 55 | //d9d295bde21f422d471a90f2a37ec53049fdf3e5fa3ee2e8f20e10003da429e7 56 | # Legacy IPNS hash block 57 | # /ipns/k51qzi5uqu5dixwsch9wpd9rolqby1m0uqj5hhxwtxal0dwltastfmh01dlniq 58 | # becomes CIDv1 bafzaajaiaejca3vrvdzmu4qntwa2pn6apsd4ug5k63ckdyhnd3g6vdvgvujdw62s 59 | # then sha256(bafzaajaiaejca3vrvdzmu4qntwa2pn6apsd4ug5k63ckdyhnd3g6vdvgvujdw62s/) 60 | # should block /ipns/ in any format 61 | //6ef262a67f2c7caa9722b0fe46aced2f1559c749eab2bcf2f2701f43f802e900 62 | # Legacy DNSLink hash block 63 | # sha256(very-bad-example.eth/) 64 | //fb5a70b1aade810d21e8195a0da05f40ebd099e4b4d6bf088dc604e4fcf34263 65 | 66 | 67 | # rule11 68 | # Legacy Path double-hash block 69 | # This should block 70 | # bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e/path 71 | # (see previous rule) 72 | # but not any other paths. 73 | //3f8b9febd851873b3774b937cce126910699ceac56e72e64b866f8e258d09572 74 | 75 | # rule12 76 | # Double hash CID 77 | # base58btc-sha256-multihash(QmVTF1yEejXd9iMgoRTFDxBv7HAz9kuZcQNBzHrceuK9HR) 78 | # This should block bafybeidjwik6im54nrpfg7osdvmx7zojl5oaxqel5cmsz46iuelwf5acja 79 | # and its CIDv0 form QmVTF1yEejXd9iMgoRTFDxBv7HAz9kuZcQNBzHrceuK9HR 80 | # and generally any CID with that multihash. 81 | //QmX9dhRcQcKUw3Ws8485T5a9dtjrSCQaUAHnG4iK9i4ceM 82 | 83 | # rule13 84 | # Double hash Path block using blake3 hashing 85 | # base58btc-blake3-multihash(gW7Nhu4HrfDtphEivm3Z9NNE7gpdh5Tga8g6JNZc1S8E47/path) 86 | # This should block /ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path 87 | # And /ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path 88 | # (different codec) 89 | # And /ipfs/f01701e20903cf61d46521b05f926ba1634628d0bba8a7ffb5b6d5a3ca310682ca63b5ef0/path 90 | # But not /path2 91 | //gW813G35CnLsy7gRYYHuf63hrz71U1xoLFDVeV7actx6oX 92 | 93 | # rule14 94 | # Double hash IPNS blocks 95 | # /ipns/my.domain.com 96 | # becomes 97 | //QmX6zeaAb8mC285YbXe4ac7LmX3jvrHBiFrw8kNnCM1bk4 98 | # /ipns/my.domain2.com/path 99 | # should only block the specific path 100 | # and becomes 101 | //QmcwfDuZYijxztHVR7w71WtdDaETsrGUU3ARWbNiYD3Hhp 102 | # /ipns/k51qzi5uqu5dixwsch9wpd9rolqby1m0uqj5hhxwtxal0dwltastfmh01dabcd 103 | # is multihash 12D3KooWHGU91cJWKofZoHQ3hkVgs2c6WAJGxVCjMivwej9ufyuN 104 | # and hashed again becomes 105 | //QmdGtYErkPjfpUPjVMUypoX8XTE9PUTBBF6KE9AwWaBs1e 106 | # /ipns/k51qzi5uqu5dixwsch9wpd9rolqby1m0uqj5hhxwtxal0dwltastfmh01d1234/mypath 107 | # equivalent to /ipns/bafzaajaiaejca3vrvdzmu4qntwa2pn6apsd4ug5k63ckdyhnd3g6vdvgvujczuoq/mypath 108 | # is multihash 12D3KooWHGU91cJWKofZoHQ3hkVgs2c6WAJGxVCjMivwej9udmWo/mypath 109 | # and hashed again becomes 110 | //QmQouNeftXbxGRt4U8s838diCuMPPHCfpHTR5w8bRREs9d 111 | 112 | # rule15 113 | # These are known cids corresponding to empty blocks/folders 114 | # Even if they appear here, they should not be blocked 115 | # empty unixfs directory 116 | /ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn 117 | # empty unixfs directory inlined 118 | /ipfs/bafyaabakaieac 119 | # empty block 120 | /ipfs/bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku 121 | # empty block inlined 122 | /ipfs/bafkqaaa 123 | # empty block dag-pb 124 | /ipfs/QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH 125 | # empty block dag-cbor 126 | /ipfs/bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua 127 | # empty block dag-json 128 | /ipfs/baguqeeraiqjw7i2vwntyuekgvulpp2det2kpwt6cd7tx5ayqybqpmhfk76fa 129 | -------------------------------------------------------------------------------- /tester/tester.go: -------------------------------------------------------------------------------- 1 | // Package tester provides an implementation-agnostic way to test a Blocker. 2 | package tester 3 | 4 | import ( 5 | "bytes" 6 | _ "embed" // allow embedding test.deny 7 | "errors" 8 | "fmt" 9 | "io" 10 | 11 | "github.com/ipfs/boxo/path" 12 | "github.com/ipfs/go-cid" 13 | ) 14 | 15 | //go:embed test.deny 16 | var denylistFile []byte 17 | 18 | // Blocker defines the minimal interface that a blocker should support 19 | // to be tested. 20 | type Blocker interface { 21 | ReadHeader(r io.Reader) (Header, error) 22 | ReadDenylist(r io.ReadSeekCloser) error 23 | IsPathBlocked(p path.Path) bool 24 | IsCidBlocked(c cid.Cid) bool 25 | } 26 | 27 | // Header represents a denylist header. 28 | type Header interface { 29 | Name() string 30 | Hints() map[string]string 31 | } 32 | 33 | // Suite repesents the test suite and different test types can be 34 | // enabled/disabled to match what the Blocker implementation supports. 35 | type Suite struct { 36 | TestHeader bool 37 | TestCID bool 38 | TestCIDPath bool 39 | TestIPNSPath bool 40 | TestDoubleHashLegacy bool 41 | TestDoubleHash bool 42 | 43 | b Blocker 44 | } 45 | 46 | type tReader struct { 47 | *bytes.Reader 48 | } 49 | 50 | func (r *tReader) Close() error { 51 | return nil 52 | } 53 | 54 | // Run performs blocker-validation tests based on test.deny using the 55 | // given blocker. Only the enabled tests in the suite are performed. 56 | func (s *Suite) Run(b Blocker) error { 57 | s.b = b 58 | 59 | if s.TestHeader { 60 | if err := s.testHeader(); err != nil { 61 | return err 62 | } 63 | } 64 | 65 | br := bytes.NewReader(denylistFile) 66 | rdr := &tReader{Reader: br} 67 | 68 | if err := b.ReadDenylist(rdr); err != nil { 69 | return fmt.Errorf("error reading/parsing denylist: %w", err) 70 | } 71 | 72 | if s.TestCID { 73 | if err := s.testCID(); err != nil { 74 | return err 75 | } 76 | } 77 | 78 | if s.TestCIDPath { 79 | if err := s.testCIDPath(); err != nil { 80 | return err 81 | } 82 | } 83 | 84 | if s.TestIPNSPath { 85 | if err := s.testIPNSPath(); err != nil { 86 | return err 87 | } 88 | } 89 | 90 | if s.TestDoubleHashLegacy { 91 | if err := s.testDoubleHashLegacy(); err != nil { 92 | return err 93 | } 94 | } 95 | 96 | if s.TestDoubleHash { 97 | if err := s.testDoubleHash(); err != nil { 98 | return err 99 | } 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (s *Suite) testHeader() error { 106 | listWithHeader := bytes.NewBufferString(`version: 1 107 | name: test 108 | hints: 109 | a: b 110 | --- 111 | # empty 112 | `) 113 | 114 | listWithoutHeader2 := bytes.NewBufferString(`--- 115 | /ipfs/bafybeihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq 116 | `) 117 | h, err := s.b.ReadHeader(listWithHeader) 118 | if err != nil { 119 | return errors.New("error parsing header") 120 | } 121 | if h.Name() != "test" { 122 | return errors.New("header not parsed correctly") 123 | } 124 | if hints := h.Hints(); len(hints) != 1 || hints["a"] != "b" { 125 | return errors.New("header hints not parsed correctly") 126 | } 127 | 128 | if _, err := s.b.ReadHeader(listWithoutHeader2); err != nil { 129 | return fmt.Errorf("error parsing list with just --- separator: %w", err) 130 | } 131 | return nil 132 | } 133 | 134 | func (s *Suite) testCID() error { 135 | // rule1 136 | blockedCids := []cid.Cid{ 137 | cid.MustParse("bafybeihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq"), 138 | cid.MustParse("bafkreihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq"), 139 | cid.MustParse("QmesfgDQ3q6prBy2Kg2gKbW4MAGuWiRP2DVuGA5MZSERLo"), 140 | } 141 | 142 | // rule15 143 | allowCids := []cid.Cid{ 144 | cid.MustParse("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"), 145 | cid.MustParse("bafyaabakaieac"), 146 | cid.MustParse("bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"), 147 | cid.MustParse("bafkqaaa"), 148 | cid.MustParse("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH"), 149 | cid.MustParse("bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua"), 150 | cid.MustParse("baguqeeraiqjw7i2vwntyuekgvulpp2det2kpwt6cd7tx5ayqybqpmhfk76fa"), 151 | } 152 | 153 | for _, c := range blockedCids { 154 | if !s.b.IsCidBlocked(c) { 155 | return fmt.Errorf("testCID: %s should be blocked (rule1)", c) 156 | } 157 | } 158 | 159 | for _, c := range allowCids { 160 | if s.b.IsCidBlocked(c) { 161 | return fmt.Errorf("testCID: %s should NOT be blocked (rule15)", c) 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | func (s *Suite) testPaths(paths []string, testName, testRule string, allow bool) error { 168 | for _, p := range paths { 169 | ppath, err := path.NewPath(p) 170 | if err != nil { 171 | return err 172 | } 173 | blocked := s.b.IsPathBlocked(ppath) 174 | if !blocked && !allow { 175 | return fmt.Errorf("%s: path %s should be blocked (%s)", testName, p, testRule) 176 | } 177 | if blocked && allow { 178 | return fmt.Errorf("%s: path %s should be allowed (%s)", testName, p, testRule) 179 | } 180 | } 181 | return nil 182 | } 183 | 184 | func (s *Suite) testCIDPath() error { 185 | n := "testCIDPath" 186 | 187 | // rule1 188 | rule1 := []string{ 189 | "/ipfs/bafybeihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq", 190 | "/ipfs/bafkreihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq", 191 | "/ipfs/QmesfgDQ3q6prBy2Kg2gKbW4MAGuWiRP2DVuGA5MZSERLo", 192 | } 193 | rule1allowed := []string{ 194 | "/ipfs/bafybeihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq/sub2", 195 | "/ipfs/bafkreihvvulpp4evxj7x7armbqcyg6uezzuig6jp3lktpbovlqfkuqeuoq/sub3", 196 | "/ipfs/QmesfgDQ3q6prBy2Kg2gKbW4MAGuWiRP2DVuGA5MZSERLo/sub4", 197 | } 198 | if err := s.testPaths(rule1, n, "rule1", false); err != nil { 199 | return err 200 | } 201 | if err := s.testPaths(rule1allowed, n, "rule1", true); err != nil { 202 | return err 203 | } 204 | 205 | // rule2 206 | rule2 := []string{ 207 | "/ipfs/QmdWFA9FL52hx3j9EJZPQP1ZUH8Ygi5tLCX2cRDs6knSf8", 208 | "/ipfs/QmdWFA9FL52hx3j9EJZPQP1ZUH8Ygi5tLCX2cRDs6knSf8/a/b", 209 | "/ipfs/QmdWFA9FL52hx3j9EJZPQP1ZUH8Ygi5tLCX2cRDs6knSf8/z/", 210 | "/ipfs/QmdWFA9FL52hx3j9EJZPQP1ZUH8Ygi5tLCX2cRDs6knSf8/z", 211 | } 212 | if err := s.testPaths(rule2, n, "rule2", false); err != nil { 213 | return err 214 | } 215 | 216 | // rule3 217 | rule3 := []string{ 218 | "/ipfs/Qmah2YDTfrox4watLCr3YgKyBwvjq8FJZEFdWY6WtJ3Xt2/test", 219 | "/ipfs/Qmah2YDTfrox4watLCr3YgKyBwvjq8FJZEFdWY6WtJ3Xt2/test2", 220 | "/ipfs/Qmah2YDTfrox4watLCr3YgKyBwvjq8FJZEFdWY6WtJ3Xt2/test/one", 221 | } 222 | rule3allowed := []string{ 223 | "/ipfs/Qmah2YDTfrox4watLCr3YgKyBwvjq8FJZEFdWY6WtJ3Xt2/tes", 224 | "/ipfs/Qmah2YDTfrox4watLCr3YgKyBwvjq8FJZEFdWY6WtJ3Xt2", 225 | "/ipfs/Qmah2YDTfrox4watLCr3YgKyBwvjq8FJZEFdWY6WtJ3Xt2/one/test", 226 | } 227 | if err := s.testPaths(rule3, n, "rule3", false); err != nil { 228 | return err 229 | } 230 | if err := s.testPaths(rule3allowed, n, "rule3", true); err != nil { 231 | return err 232 | } 233 | 234 | // rule4 235 | rule4 := []string{ 236 | "/ipfs/QmTuvSQbEDR3sarFAN9kAeXBpiBCyYYNxdxciazBba11eC/test", 237 | "/ipfs/QmTuvSQbEDR3sarFAN9kAeXBpiBCyYYNxdxciazBba11eC/test2", 238 | "/ipfs/QmTuvSQbEDR3sarFAN9kAeXBpiBCyYYNxdxciazBba11eC/test/one", 239 | } 240 | rule4allowed := []string{ 241 | "/ipfs/QmTuvSQbEDR3sarFAN9kAeXBpiBCyYYNxdxciazBba11eC/tes", 242 | "/ipfs/QmTuvSQbEDR3sarFAN9kAeXBpiBCyYYNxdxciazBba11eC", 243 | "/ipfs/QmTuvSQbEDR3sarFAN9kAeXBpiBCyYYNxdxciazBba11eC/one/test", 244 | } 245 | if err := s.testPaths(rule4, n, "rule4", false); err != nil { 246 | return err 247 | } 248 | if err := s.testPaths(rule4allowed, n, "rule4", true); err != nil { 249 | return err 250 | } 251 | 252 | // rule5 253 | rule5 := []string{ 254 | "/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked", 255 | "/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked2", 256 | } 257 | rule5allowed := []string{ 258 | "/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blockednot", 259 | "/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/not", 260 | "/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/exceptions", 261 | "/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/exceptions2", 262 | "/ipfs/QmUboz9UsQBDeS6Tug1U8jgoFkgYxyYood9NDyVURAY9pK/blocked/exceptions/one", 263 | } 264 | if err := s.testPaths(rule5, n, "rule5", false); err != nil { 265 | return err 266 | } 267 | 268 | if err := s.testPaths(rule5allowed, n, "rule5", true); err != nil { 269 | return err 270 | } 271 | 272 | return nil 273 | } 274 | 275 | func (s *Suite) testIPNSPath() error { 276 | n := "testIPNS" 277 | // rule6 278 | rule6 := []string{ 279 | "/ipns/domain.example", 280 | "/ipns/domain-example", 281 | } 282 | rule6allowed := []string{ 283 | "/ipns/domainaefa.example", 284 | "/ipns/domain.example/path", 285 | "/ipns/domain--example", 286 | } 287 | 288 | if err := s.testPaths(rule6, n, "rule6", false); err != nil { 289 | return err 290 | } 291 | 292 | if err := s.testPaths(rule6allowed, n, "rule6", true); err != nil { 293 | return err 294 | } 295 | 296 | // rule7 297 | rule7 := []string{ 298 | "/ipns/domain2.example/path", 299 | } 300 | rule7allowed := []string{ 301 | "/ipns/domain2.example", 302 | "/ipns/domain2.example/path2", 303 | } 304 | 305 | if err := s.testPaths(rule7, n, "rule7", false); err != nil { 306 | return err 307 | } 308 | 309 | if err := s.testPaths(rule7allowed, n, "rule7", true); err != nil { 310 | return err 311 | } 312 | 313 | // rule8 314 | rule8 := []string{ 315 | "/ipns/k51qzi5uqu5dhmzyv3zac033i7rl9hkgczxyl81lwoukda2htteop7d3x0y1mf", 316 | "/ipns/bafzaajaiaejcaotjfs57kieazxny5japcmy5p2pgv2cic77tu6ogghttvurnrufx", 317 | "/ipns/12D3KooWDkNqEJNmreF3NYYFK1ws7Ra2fuW6cHBTu567SPV3LdYA", 318 | } 319 | rule8allowed := []string{ 320 | "/ipns/12D3KooWDkNqEJNmreF3NYYFK1ws7Ra2fuW6cHBTu567SPV3LdYA/path", 321 | } 322 | 323 | if err := s.testPaths(rule8, n, "rule8", false); err != nil { 324 | return err 325 | } 326 | 327 | if err := s.testPaths(rule8allowed, n, "rule8", true); err != nil { 328 | return err 329 | } 330 | 331 | return nil 332 | } 333 | 334 | func (s *Suite) testDoubleHashLegacy() error { 335 | n := "TestDoubleHashLegacy" 336 | // rule10 337 | c := cid.MustParse("bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e") 338 | if !s.b.IsCidBlocked(c) { 339 | return fmt.Errorf("%s: cid %s should be blocked (rule10)", n, c) 340 | } 341 | rule10 := []string{ 342 | "/ipns/k51qzi5uqu5dixwsch9wpd9rolqby1m0uqj5hhxwtxal0dwltastfmh01dlniq", 343 | "/ipns/bafzaajaiaejca3vrvdzmu4qntwa2pn6apsd4ug5k63ckdyhnd3g6vdvgvujdw62s", 344 | "/ipns/very-bad-example.eth", 345 | "/ipns/very-bad-example.eth/", 346 | } 347 | if err := s.testPaths(rule10, n, "rule10", false); err != nil { 348 | return err 349 | } 350 | 351 | //rule 11 (and 10) 352 | rule11 := []string{ 353 | "/ipfs/bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e/", 354 | "/ipfs/bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e", 355 | "/ipfs/bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e/path", 356 | } 357 | rule11allowed := []string{ 358 | "/ipfs/bafybeiefwqslmf6zyyrxodaxx4vwqircuxpza5ri45ws3y5a62ypxti42e/path2", 359 | } 360 | 361 | if err := s.testPaths(rule11, n, "rule11", false); err != nil { 362 | return err 363 | } 364 | 365 | if err := s.testPaths(rule11allowed, n, "rule11", true); err != nil { 366 | return err 367 | } 368 | 369 | return nil 370 | } 371 | 372 | func (s *Suite) testDoubleHash() error { 373 | n := "TestDoubleHash" 374 | //rule 13 375 | c1 := cid.MustParse("bafybeidjwik6im54nrpfg7osdvmx7zojl5oaxqel5cmsz46iuelwf5acja") 376 | c2 := cid.MustParse("QmVTF1yEejXd9iMgoRTFDxBv7HAz9kuZcQNBzHrceuK9HR") 377 | if !s.b.IsCidBlocked(c1) { 378 | return fmt.Errorf("%s: cid %s should be blocked (rule12)", n, c1) 379 | } 380 | 381 | if !s.b.IsCidBlocked(c2) { 382 | return fmt.Errorf("%s: cid %s should be blocked (rule12)", n, c2) 383 | } 384 | 385 | // rule13 386 | rule13 := []string{ 387 | "/ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path", 388 | "/ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path", 389 | "/ipfs/f01701e20903cf61d46521b05f926ba1634628d0bba8a7ffb5b6d5a3ca310682ca63b5ef0/path", 390 | } 391 | rule13allowed := []string{ 392 | "/ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/", 393 | "/ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/", 394 | "/ipfs/f01701e20903cf61d46521b05f926ba1634628d0bba8a7ffb5b6d5a3ca310682ca63b5ef0/", 395 | "/ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path2", 396 | "/ipfs/bafyb4ieqht3b2rssdmc7sjv2cy2gfdilxkfh7623nvndziyqnawkmo266a/path2", 397 | "/ipfs/f01701e20903cf61d46521b05f926ba1634628d0bba8a7ffb5b6d5a3ca310682ca63b5ef0/path2", 398 | } 399 | 400 | if err := s.testPaths(rule13, n, "rule13", false); err != nil { 401 | return err 402 | } 403 | 404 | if err := s.testPaths(rule13allowed, n, "rule13", true); err != nil { 405 | return err 406 | } 407 | 408 | // rule14 409 | rule14 := []string{ 410 | "/ipns/my.domain.com", 411 | "/ipns/my.domain2.com/path", 412 | "/ipns/k51qzi5uqu5dixwsch9wpd9rolqby1m0uqj5hhxwtxal0dwltastfmh01dabcd", 413 | "/ipns/k51qzi5uqu5dixwsch9wpd9rolqby1m0uqj5hhxwtxal0dwltastfmh01d1234/mypath", 414 | "/ipns/bafzaajaiaejca3vrvdzmu4qntwa2pn6apsd4ug5k63ckdyhnd3g6vdvgvujczuoq/mypath", 415 | } 416 | rule14allowed := []string{ 417 | "/ipns/my.domain2.com/path2", 418 | "/ipns/k51qzi5uqu5dixwsch9wpd9rolqby1m0uqj5hhxwtxal0dwltastfmh01d1234/mypath2", 419 | } 420 | 421 | if err := s.testPaths(rule14, n, "rule14", false); err != nil { 422 | return err 423 | } 424 | 425 | if err := s.testPaths(rule14allowed, n, "rule14", true); err != nil { 426 | return err 427 | } 428 | 429 | return nil 430 | } 431 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package nopfs 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // GetDenylistFiles returns a list of ".deny" files found in 12 | // $XDG_CONFIG_HOME/ipfs/denylists and /etc/ipfs/denylists. The files are 13 | // sortered by their names in their respective directories. 14 | func GetDenylistFiles() ([]string, error) { 15 | // First, look for denylists in $XDG_CONFIG_HOME/ipfs/denylists 16 | xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") 17 | if xdgConfigHome == "" { 18 | xdgConfigHome = os.Getenv("HOME") + "/.config" 19 | } 20 | ipfsDenylistPath := filepath.Join(xdgConfigHome, "ipfs", "denylists") 21 | ipfsDenylistFiles, err := GetDenylistFilesInDir(ipfsDenylistPath) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | // Then, look for denylists in /etc/ipfs/denylists 27 | etcDenylistPath := "/etc/ipfs/denylists" 28 | etcDenylistFiles, err := GetDenylistFilesInDir(etcDenylistPath) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return append(ipfsDenylistFiles, etcDenylistFiles...), nil 34 | } 35 | 36 | // GetDenylistFilesInDir returns a list of ".deny" files found in the given 37 | // directory. The files are sortered by their names. It returns an empty list 38 | // and no error if the directory does not exist. 39 | func GetDenylistFilesInDir(dirpath string) ([]string, error) { 40 | var denylistFiles []string 41 | 42 | // WalkDir outputs files in lexical order. 43 | err := filepath.WalkDir(dirpath, func(path string, d fs.DirEntry, err error) error { 44 | if err != nil { 45 | return err 46 | } 47 | if !d.IsDir() && filepath.Ext(path) == ".deny" { 48 | denylistFiles = append(denylistFiles, path) 49 | } 50 | return nil 51 | }) 52 | if !os.IsNotExist(err) && err != nil { 53 | return nil, fmt.Errorf("error walking %s: %w", dirpath, err) 54 | } 55 | return denylistFiles, nil 56 | } 57 | 58 | // cutPrefix imported from go1.20 59 | func cutPrefix(s, prefix string) (after string, found bool) { 60 | if !strings.HasPrefix(s, prefix) { 61 | return s, false 62 | } 63 | return s[len(prefix):], true 64 | } 65 | --------------------------------------------------------------------------------