├── .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 |
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 |
--------------------------------------------------------------------------------