mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-27 21:37:57 +08:00
Merge branch 'master' into process-improvement-v0.18.0
This commit is contained in:
commit
96a0eb2899
@ -7,7 +7,7 @@ jobs:
|
||||
executor: continuation/default
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
- run:
|
||||
name: Generate params
|
||||
# for builds on the ipfs/kubo repo, use 2xlarge for faster builds
|
||||
# but since this is not available for many contributors, we otherwise use medium
|
||||
|
||||
@ -143,14 +143,17 @@ jobs:
|
||||
path: /tmp/circleci-test-results
|
||||
sharness:
|
||||
machine:
|
||||
image: ubuntu-2004:202010-01
|
||||
image: ubuntu-2204:2022.10.1
|
||||
resource_class: << pipeline.parameters.resource_class >>
|
||||
working_directory: ~/ipfs/kubo
|
||||
environment:
|
||||
<<: *default_environment
|
||||
TEST_NO_DOCKER: 0
|
||||
TEST_NO_PLUGIN: 1
|
||||
TEST_NO_FUSE: 1
|
||||
TEST_VERBOSE: 1
|
||||
TEST_JUNIT: 1
|
||||
TEST_EXPENSIVE: 1
|
||||
steps:
|
||||
- run: sudo apt update
|
||||
- run: |
|
||||
@ -159,7 +162,7 @@ jobs:
|
||||
tar xfz go1.19.1.linux-amd64.tar.gz
|
||||
echo "export PATH=$(pwd)/go/bin:\$PATH" >> ~/.bashrc
|
||||
- run: go version
|
||||
- run: sudo apt install socat net-tools fish
|
||||
- run: sudo apt install socat net-tools fish libxml2-utils
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
@ -183,7 +186,7 @@ jobs:
|
||||
command: echo "export TEST_DOCKER_HOST=$(ip -4 addr show docker0 | grep -Po 'inet \K[\d.]+')" >> $BASH_ENV
|
||||
- run:
|
||||
echo TEST_DOCKER_HOST=$TEST_DOCKER_HOST &&
|
||||
make -O -j << pipeline.parameters.make_jobs >> coverage/sharness_tests.coverprofile test/sharness/test-results/sharness.xml TEST_GENERATE_JUNIT=1 CONTINUE_ON_S_FAILURE=1 TEST_DOCKER_HOST=$TEST_DOCKER_HOST
|
||||
make -O -j << pipeline.parameters.make_jobs >> test_sharness coverage/sharness_tests.coverprofile test/sharness/test-results/sharness.xml CONTINUE_ON_S_FAILURE=1 TEST_DOCKER_HOST=$TEST_DOCKER_HOST
|
||||
- run:
|
||||
when: always
|
||||
command: bash <(curl -s https://codecov.io/bash) -cF sharness -X search -f coverage/sharness_tests.coverprofile
|
||||
@ -345,13 +348,13 @@ jobs:
|
||||
npx playwright install
|
||||
working_directory: ~/ipfs/kubo/ipfs-webui
|
||||
- run:
|
||||
name: Running upstream tests (finish early if they fail)
|
||||
name: Run ipfs-webui@main build and smoke-test to confirm the upstream repo is not broken
|
||||
command: |
|
||||
npm test || circleci-agent step halt
|
||||
npm test
|
||||
working_directory: ~/ipfs/kubo/ipfs-webui
|
||||
- run:
|
||||
name: Running tests with kubo built from current commit
|
||||
command: npm test
|
||||
name: Test ipfs-webui@main E2E against the locally built Kubo binary
|
||||
command: npm run test:e2e
|
||||
working_directory: ~/ipfs/kubo/ipfs-webui
|
||||
environment:
|
||||
IPFS_GO_EXEC: /tmp/circleci-workspace/bin/ipfs
|
||||
|
||||
200
.github/workflows/build.yml
vendored
Normal file
200
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,200 @@
|
||||
name: 'ci/gh-experiment: interop'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.19.1
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_NO_DOCKER: 1
|
||||
TEST_NO_FUSE: 1
|
||||
TEST_VERBOSE: 1
|
||||
TRAVIS: 1
|
||||
GIT_PAGER: cat
|
||||
IPFS_CHECK_RCMGR_DEFAULTS: 1
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: protocol/cache-go-action@v1
|
||||
with:
|
||||
name: ${{ github.job }}
|
||||
- run: make build
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: kubo
|
||||
path: cmd/ipfs/ipfs
|
||||
ipfs-interop:
|
||||
needs: [prepare]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
suites:
|
||||
- 'exchange-files'
|
||||
- 'files pin circuit ipns cid-version-agnostic ipns-pubsub pubsub'
|
||||
fail-fast: false
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.12.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: kubo
|
||||
path: cmd/ipfs
|
||||
- run: chmod +x cmd/ipfs/ipfs
|
||||
- run: |
|
||||
echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
||||
id: npm-cache-dir
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ github.job }}-
|
||||
- run: mkdir interop
|
||||
- run: |
|
||||
npm init -y
|
||||
npm install ipfs@^0.61.0
|
||||
npm install ipfs-interop@^8.0.10
|
||||
working-directory: interop
|
||||
- run: npx ipfs-interop -- -t node $(sed -e 's#[^ ]*#-f test/&.js#g' <<< '${{ matrix.suites }}')
|
||||
env:
|
||||
LIBP2P_TCP_REUSEPORT: false
|
||||
LIBP2P_ALLOW_WEAK_RSA_KEYS: 1
|
||||
IPFS_GO_EXEC: ${{ github.workspace }}/cmd/ipfs/ipfs
|
||||
working-directory: interop
|
||||
go-ipfs-api:
|
||||
needs: [prepare]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_NO_DOCKER: 1
|
||||
TEST_NO_FUSE: 1
|
||||
TEST_VERBOSE: 1
|
||||
TRAVIS: 1
|
||||
GIT_PAGER: cat
|
||||
IPFS_CHECK_RCMGR_DEFAULTS: 1
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: kubo
|
||||
path: cmd/ipfs
|
||||
- run: chmod +x cmd/ipfs/ipfs
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ipfs/go-ipfs-api
|
||||
path: go-ipfs-api
|
||||
- run: cmd/ipfs/ipfs daemon --init --enable-namesys-pubsub &
|
||||
- run: |
|
||||
while ! cmd/ipfs/ipfs id --api=/ip4/127.0.0.1/tcp/5001 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
timeout-minutes: 5
|
||||
- uses: protocol/cache-go-action@v1
|
||||
with:
|
||||
name: ${{ github.job }}
|
||||
- run: go test -count=1 -v ./...
|
||||
working-directory: go-ipfs-api
|
||||
- run: cmd/ipfs/ipfs shutdown
|
||||
if: always()
|
||||
go-ipfs-http-client:
|
||||
needs: [prepare]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_NO_DOCKER: 1
|
||||
TEST_NO_FUSE: 1
|
||||
TEST_VERBOSE: 1
|
||||
TRAVIS: 1
|
||||
GIT_PAGER: cat
|
||||
IPFS_CHECK_RCMGR_DEFAULTS: 1
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: kubo
|
||||
path: cmd/ipfs
|
||||
- run: chmod +x cmd/ipfs/ipfs
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ipfs/go-ipfs-http-client
|
||||
path: go-ipfs-http-client
|
||||
- uses: protocol/cache-go-action@v1
|
||||
with:
|
||||
name: ${{ github.job }}
|
||||
- run: echo '${{ github.workspace }}/cmd/ipfs' >> $GITHUB_PATH
|
||||
- run: go test -count=1 -v ./...
|
||||
working-directory: go-ipfs-http-client
|
||||
ipfs-webui:
|
||||
needs: [prepare]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NO_SANDBOX: true
|
||||
LIBP2P_TCP_REUSEPORT: false
|
||||
LIBP2P_ALLOW_WEAK_RSA_KEYS: 1
|
||||
E2E_IPFSD_TYPE: go
|
||||
TRAVIS: 1
|
||||
GIT_PAGER: cat
|
||||
IPFS_CHECK_RCMGR_DEFAULTS: 1
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.12.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: kubo
|
||||
path: cmd/ipfs
|
||||
- run: chmod +x cmd/ipfs/ipfs
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ipfs/ipfs-webui
|
||||
path: ipfs-webui
|
||||
- run: |
|
||||
echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
||||
id: npm-cache-dir
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ github.job }}-
|
||||
- run: |
|
||||
npm ci --prefer-offline --no-audit --progress=false
|
||||
npx playwright install
|
||||
working-directory: ipfs-webui
|
||||
- name: Run ipfs-webui@main build and smoke-test to confirm the upstream repo is not broken
|
||||
run: npm test
|
||||
working-directory: ipfs-webui
|
||||
- name: Test ipfs-webui@main E2E against the locally built Kubo binary
|
||||
run: npm run test:e2e
|
||||
env:
|
||||
IPFS_GO_EXEC: ${{ github.workspace }}/cmd/ipfs/ipfs
|
||||
working-directory: ipfs-webui
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -1,5 +1,5 @@
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
name: "CodeQL"
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
25
.github/workflows/docker-build.yml
vendored
Normal file
25
.github/workflows/docker-build.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: 'ci/gh-experiment: docker-build'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
IMAGE_NAME: ipfs/kubo
|
||||
WIP_IMAGE_TAG: wip
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.1
|
||||
- uses: actions/checkout@v3
|
||||
- run: docker build -t $IMAGE_NAME:$WIP_IMAGE_TAG .
|
||||
8
.github/workflows/docker-image.yml
vendored
8
.github/workflows/docker-image.yml
vendored
@ -30,15 +30,15 @@ jobs:
|
||||
- name: Get tags
|
||||
id: tags
|
||||
run: |
|
||||
TAGS="$(./bin/get-docker-tags.sh $(date -u +%F))"
|
||||
TAGS="${TAGS//$'\n'/'%0A'}"
|
||||
echo "::set-output name=value::$(echo $TAGS)"
|
||||
echo "value<<EOF" >> $GITHUB_OUTPUT
|
||||
./bin/get-docker-tags.sh "$(date -u +%F)" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
username: ${{ vars.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build Docker image and publish to Docker Hub
|
||||
|
||||
39
.github/workflows/gobuild.yml
vendored
Normal file
39
.github/workflows/gobuild.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: 'ci/gh-experiment: go build'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
runner:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
uses: ipfs/kubo/.github/workflows/runner.yml@master
|
||||
gobuild:
|
||||
needs: [runner]
|
||||
runs-on: ${{ fromJSON(needs.runner.outputs.config).labels }}
|
||||
env:
|
||||
TEST_NO_DOCKER: 1
|
||||
TEST_VERBOSE: 1
|
||||
TRAVIS: 1
|
||||
GIT_PAGER: cat
|
||||
IPFS_CHECK_RCMGR_DEFAULTS: 1
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: protocol/cache-go-action@v1
|
||||
with:
|
||||
name: ${{ github.job }}
|
||||
- run: make cmd/ipfs-try-build
|
||||
env:
|
||||
TEST_NO_FUSE: 0
|
||||
- run: make cmd/ipfs-try-build
|
||||
env:
|
||||
TEST_NO_FUSE: 1
|
||||
9
.github/workflows/golang-analysis.yml
vendored
9
.github/workflows/golang-analysis.yml
vendored
@ -1,8 +1,15 @@
|
||||
on: [push, pull_request]
|
||||
name: Go Checks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
name: All
|
||||
steps:
|
||||
|
||||
32
.github/workflows/golint.yml
vendored
Normal file
32
.github/workflows/golint.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: 'ci/gh-experiment: go lint'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
golint:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_NO_DOCKER: 1
|
||||
TEST_NO_FUSE: 1
|
||||
TEST_VERBOSE: 1
|
||||
TRAVIS: 1
|
||||
GIT_PAGER: cat
|
||||
IPFS_CHECK_RCMGR_DEFAULTS: 1
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: protocol/cache-go-action@v1
|
||||
with:
|
||||
name: ${{ github.job }}
|
||||
- run: make -O test_go_lint
|
||||
65
.github/workflows/gotest.yml
vendored
Normal file
65
.github/workflows/gotest.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
name: 'ci/gh-experiment: go test'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
gotest:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_NO_DOCKER: 1
|
||||
TEST_NO_FUSE: 1
|
||||
TEST_VERBOSE: 1
|
||||
TRAVIS: 1
|
||||
GIT_PAGER: cat
|
||||
IPFS_CHECK_RCMGR_DEFAULTS: 1
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: protocol/cache-go-action@v1
|
||||
with:
|
||||
name: ${{ github.job }}
|
||||
- run: |
|
||||
make -j 1 test/unit/gotest.junit.xml &&
|
||||
[[ ! $(jq -s -c 'map(select(.Action == "fail")) | .[]' test/unit/gotest.json) ]]
|
||||
- uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0
|
||||
if: always()
|
||||
with:
|
||||
name: unittests
|
||||
files: coverage/unit_tests.coverprofile
|
||||
- run: |
|
||||
# we want to first test with the kubo version in the go.mod file
|
||||
go test -v ./...
|
||||
|
||||
# we also want to test the examples against the current version of kubo
|
||||
# however, that version might be in a fork so we need to replace the dependency
|
||||
|
||||
# backup the go.mod and go.sum files to restore them after we run the tests
|
||||
cp go.mod go.mod.bak
|
||||
cp go.sum go.sum.bak
|
||||
|
||||
# make sure the examples run against the current version of kubo
|
||||
go mod edit -replace github.com/ipfs/kubo=./../../..
|
||||
go mod tidy
|
||||
|
||||
go test -v ./...
|
||||
|
||||
# restore the go.mod and go.sum files to their original state
|
||||
mv go.mod.bak go.mod
|
||||
mv go.sum.bak go.sum
|
||||
working-directory: docs/examples/kubo-as-a-library
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: unit
|
||||
path: test/unit/gotest.junit.xml
|
||||
if: always()
|
||||
33
.github/workflows/runner.yml
vendored
Normal file
33
.github/workflows/runner.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
name: 'ci/gh-experiment: choose runner'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
config:
|
||||
description: "The runner's configuration"
|
||||
value: ${{ jobs.choose.outputs.config }}
|
||||
|
||||
jobs:
|
||||
choose:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
config: ${{ steps.config.outputs.result }}
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
id: config
|
||||
with:
|
||||
script: |
|
||||
if (`${context.repo.owner}/${context.repo.repo}` === 'ipfs/kubo') {
|
||||
return {
|
||||
labels: ['self-hosted', 'linux', 'x64', 'kubo'],
|
||||
parallel: 10,
|
||||
aws: true
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
labels: ['ubuntu-latest'],
|
||||
parallel: 3,
|
||||
aws: false
|
||||
}
|
||||
}
|
||||
- run: echo ${{ steps.config.outputs.result }}
|
||||
124
.github/workflows/sharness.yml
vendored
Normal file
124
.github/workflows/sharness.yml
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
name: 'ci/gh-experiment: sharness'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
runner:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
uses: ipfs/kubo/.github/workflows/runner.yml@master
|
||||
sharness:
|
||||
needs: [runner]
|
||||
runs-on: ${{ fromJSON(needs.runner.outputs.config).labels }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.1
|
||||
- name: Checkout Kubo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: kubo
|
||||
- name: Install missing tools
|
||||
run: sudo apt install -y socat net-tools fish libxml2-utils
|
||||
- name: Checkout IPFS Pinning Service API
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ipfs-shipyard/rb-pinning-service-api
|
||||
ref: 773c3adbb421c551d2d89288abac3e01e1f7c3a8
|
||||
path: rb-pinning-service-api
|
||||
# TODO: check if docker compose (not docker-compose) is available on default gh runners
|
||||
- name: Start IPFS Pinning Service API
|
||||
run: |
|
||||
(for i in {1..3}; do docker compose pull && break || sleep 5; done) &&
|
||||
docker compose up -d
|
||||
working-directory: rb-pinning-service-api
|
||||
- name: Restore Go Cache
|
||||
uses: protocol/cache-go-action@v1
|
||||
with:
|
||||
name: ${{ github.job }}
|
||||
- name: Find IPFS Pinning Service API address
|
||||
run: echo "TEST_DOCKER_HOST=$(ip -4 addr show docker0 | grep -Po 'inet \K[\d.]+')" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: test/sharness/lib/dependencies
|
||||
key: ${{ runner.os }}-test-generate-junit-html-${{ hashFiles('test/sharness/lib/test-generate-junit-html.sh') }}
|
||||
- name: Run Sharness tests
|
||||
run: |
|
||||
make -O -j "$PARALLEL" \
|
||||
test_sharness \
|
||||
coverage/sharness_tests.coverprofile \
|
||||
test/sharness/test-results/sharness.xml \
|
||||
test/sharness/test-results/sharness.html \
|
||||
test/sharness/test-results/sharness-html
|
||||
working-directory: kubo
|
||||
env:
|
||||
TEST_NO_DOCKER: 0
|
||||
TEST_NO_PLUGIN: 1
|
||||
TEST_NO_FUSE: 1
|
||||
TEST_VERBOSE: 1
|
||||
TEST_JUNIT: 1
|
||||
TEST_EXPENSIVE: 1
|
||||
IPFS_CHECK_RCMGR_DEFAULTS: 1
|
||||
CONTINUE_ON_S_FAILURE: 1
|
||||
PARALLEL: ${{ fromJSON(needs.runner.outputs.config).parallel }}
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.1.0
|
||||
if: failure() || success()
|
||||
with:
|
||||
name: sharness
|
||||
files: kubo/coverage/sharness_tests.coverprofile
|
||||
- name: Aggregate results
|
||||
run: find kubo/test/sharness/test-results -name 't*-*.sh.*.counts' | kubo/test/sharness/lib/sharness/aggregate-results.sh > kubo/test/sharness/test-results/summary.txt
|
||||
- name: 👉️ If this step failed, go to «Summary» (top left) → «HTML Report» → inspect the «Failures» column
|
||||
run: |
|
||||
cat kubo/test/sharness/test-results/summary.txt &&
|
||||
grep 'failed\s*0' kubo/test/sharness/test-results/summary.txt
|
||||
- name: Add aggregate results to the summary
|
||||
if: failure() || success()
|
||||
run: |
|
||||
echo "# Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo >> $GITHUB_STEP_SUMMARY
|
||||
cat kubo/test/sharness/test-results/summary.txt >> $GITHUB_STEP_SUMMARY
|
||||
- name: Upload one-page HTML report to S3
|
||||
id: one-page
|
||||
uses: pl-strflt/tf-aws-gh-runner/.github/actions/upload-artifact@main
|
||||
if: fromJSON(needs.runner.outputs.config).aws && (failure() || success())
|
||||
with:
|
||||
source: kubo/test/sharness/test-results/sharness.html
|
||||
destination: sharness.html
|
||||
- name: Upload one-page HTML report
|
||||
if: (! fromJSON(needs.runner.outputs.config).aws) && (failure() || success())
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: sharness.html
|
||||
path: kubo/test/sharness/test-results/sharness.html
|
||||
- name: Upload full HTML report to S3
|
||||
id: full
|
||||
uses: pl-strflt/tf-aws-gh-runner/.github/actions/upload-artifact@main
|
||||
if: fromJSON(needs.runner.outputs.config).aws && (failure() || success())
|
||||
with:
|
||||
source: kubo/test/sharness/test-results/sharness-html
|
||||
destination: sharness-html/
|
||||
- name: Upload full HTML report
|
||||
if: (! fromJSON(needs.runner.outputs.config).aws) && (failure() || success())
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: sharness-html
|
||||
path: kubo/test/sharness/test-results/sharness-html
|
||||
- name: Add S3 links to the summary
|
||||
if: fromJSON(needs.runner.outputs.config).aws && (failure() || success())
|
||||
run: echo "$MD" >> $GITHUB_STEP_SUMMARY
|
||||
env:
|
||||
MD: |
|
||||
# HTML Reports
|
||||
|
||||
- View the [one page HTML report](${{ steps.one-page.outputs.url }})
|
||||
- View the [full HTML report](${{ steps.full.outputs.url }}index.html)
|
||||
@ -1,3 +1,8 @@
|
||||
linters:
|
||||
enable:
|
||||
- stylecheck
|
||||
|
||||
linters-settings:
|
||||
stylecheck:
|
||||
dot-import-whitelist:
|
||||
- github.com/ipfs/kubo/test/cli/testutils
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Kubo Changelogs
|
||||
|
||||
- [v0.19](docs/changelogs/v0.19.md)
|
||||
- [v0.18](docs/changelogs/v0.18.md)
|
||||
- [v0.17](docs/changelogs/v0.17.md)
|
||||
- [v0.16](docs/changelogs/v0.16.md)
|
||||
- [v0.15](docs/changelogs/v0.15.md)
|
||||
|
||||
74
README.md
74
README.md
@ -62,13 +62,13 @@ Before opening an issue, consider using one of the following locations to ensure
|
||||
- [openSUSE](#opensuse)
|
||||
- [Guix](#guix)
|
||||
- [Snap](#snap)
|
||||
- [Unofficial MacOS packages](#unofficial-macos-packages)
|
||||
- [MacPorts](#macports)
|
||||
- [Nix](#nix-1)
|
||||
- [Homebrew](#homebrew)
|
||||
- [Unofficial Windows packages](#unofficial-windows-packages)
|
||||
- [Chocolatey](#chocolatey)
|
||||
- [Scoop](#scoop)
|
||||
- [Unofficial MacOS packages](#unofficial-macos-packages)
|
||||
- [MacPorts](#macports)
|
||||
- [Nix](#nix-macos)
|
||||
- [Homebrew](#homebrew)
|
||||
- [Build from Source](#build-from-source)
|
||||
- [Install Go](#install-go)
|
||||
- [Download and Compile IPFS](#download-and-compile-ipfs)
|
||||
@ -166,10 +166,12 @@ $ ipfs get /ipns/dist.ipfs.tech/kubo/$VERSION/kubo_$VERSION_windows-amd64.zip
|
||||
|
||||
### Unofficial Linux packages
|
||||
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [Nix](#nix-linux)
|
||||
- [ArchLinux](#arch-linux)
|
||||
- [Nix](#nix)
|
||||
- [Solus](#solus)
|
||||
- [openSUSE](#opensuse)
|
||||
- [Guix](#guix)
|
||||
- [Snap](#snap)
|
||||
|
||||
#### Arch Linux
|
||||
|
||||
@ -193,11 +195,10 @@ You can also install the Package by using its attribute name, which is also `ipf
|
||||
|
||||
#### Solus
|
||||
|
||||
In solus, kubo (go-ipfs) is available in the main repository as
|
||||
[go-ipfs](https://dev.getsol.us/source/go-ipfs/repository/master/).
|
||||
[Package for Solus](https://dev.getsol.us/source/kubo/repository/master/)
|
||||
|
||||
```
|
||||
$ sudo eopkg install go-ipfs
|
||||
$ sudo eopkg install kubo
|
||||
```
|
||||
|
||||
You can also install it through the Solus software center.
|
||||
@ -208,16 +209,36 @@ You can also install it through the Solus software center.
|
||||
|
||||
#### Guix
|
||||
|
||||
GNU's functional package manager, [Guix](https://www.gnu.org/software/guix/), also provides a go-ipfs package:
|
||||
|
||||
```
|
||||
$ guix package -i go-ipfs
|
||||
```
|
||||
[Community Package for go-ipfs](https://packages.guix.gnu.org/packages/go-ipfs/0.11.0/) is no out-of-date.
|
||||
|
||||
#### Snap
|
||||
|
||||
No longer supported, see rationale in [kubo#8688](https://github.com/ipfs/kubo/issues/8688).
|
||||
|
||||
### Unofficial Windows packages
|
||||
|
||||
- [Chocolatey](#chocolatey)
|
||||
- [Scoop](#scoop)
|
||||
|
||||
#### Chocolatey
|
||||
|
||||
No longer supported, see rationale in [kubo#9341](https://github.com/ipfs/kubo/issues/9341).
|
||||
|
||||
#### Scoop
|
||||
|
||||
Scoop provides kubo as `kubo` in its 'extras' bucket.
|
||||
|
||||
```Powershell
|
||||
PS> scoop bucket add extras
|
||||
PS> scoop install kubo
|
||||
```
|
||||
|
||||
### Unofficial macOS packages
|
||||
|
||||
- [MacPorts](#macports)
|
||||
- [Nix](#nix-macos)
|
||||
- [Homebrew](#homebrew)
|
||||
|
||||
#### MacPorts
|
||||
|
||||
The package [ipfs](https://ports.macports.org/port/ipfs) currently points to kubo (go-ipfs) and is being maintained.
|
||||
@ -244,31 +265,6 @@ A Homebrew formula [ipfs](https://formulae.brew.sh/formula/ipfs) is maintained t
|
||||
$ brew install --formula ipfs
|
||||
```
|
||||
|
||||
### Unofficial Windows packages
|
||||
|
||||
- [Chocolatey](#chocolatey)
|
||||
- [Scoop](#scoop)
|
||||
|
||||
#### Chocolatey
|
||||
|
||||
No longer supported, see rationale in [kubo#9341](https://github.com/ipfs/kubo/issues/9341).
|
||||
|
||||
#### Scoop
|
||||
|
||||
Scoop provides kubo as `kubo` in its 'extras' bucket.
|
||||
|
||||
```Powershell
|
||||
PS> scoop bucket add extras
|
||||
PS> scoop install kubo
|
||||
```
|
||||
|
||||
### Unofficial macOS packages
|
||||
|
||||
- [MacPorts](#macports)
|
||||
- [Nix](#nix-macos)
|
||||
- [Homebrew](#homebrew)
|
||||
|
||||
|
||||
### Build from Source
|
||||
|
||||

|
||||
|
||||
3
Rules.mk
3
Rules.mk
@ -136,8 +136,7 @@ help:
|
||||
@echo ' test_go_expensive - Run all go tests and compile on all platforms'
|
||||
@echo ' test_go_race - Run go tests with the race detector enabled'
|
||||
@echo ' test_go_lint - Run the `golangci-lint` vetting tool'
|
||||
@echo ' test_sharness_short - Run short sharness tests'
|
||||
@echo ' test_sharness_expensive - Run all sharness tests'
|
||||
@echo ' test_sharness - Run sharness tests'
|
||||
@echo ' coverage - Collects coverage info from unit tests and sharness'
|
||||
@echo
|
||||
.PHONY: help
|
||||
|
||||
18
appveyor.yml
18
appveyor.yml
@ -14,8 +14,8 @@ environment:
|
||||
GOPATH: c:\gopath
|
||||
TEST_VERBOSE: 1
|
||||
#TEST_NO_FUSE: 1
|
||||
#TEST_SUITE: test_sharness_expensive
|
||||
#GOFLAGS: -tags nofuse
|
||||
#TEST_SUITE: test_sharness
|
||||
#GOFLAGS: -tags nofuse
|
||||
global:
|
||||
BASH: C:\cygwin\bin\bash
|
||||
matrix:
|
||||
@ -23,27 +23,27 @@ environment:
|
||||
GOVERSION: 1.5.1
|
||||
GOROOT: c:\go
|
||||
DOWNLOADPLATFORM: "x64"
|
||||
|
||||
|
||||
install:
|
||||
# Enable make
|
||||
#- SET PATH=c:\MinGW\bin;%PATH%
|
||||
#- copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe
|
||||
- go version
|
||||
- go env
|
||||
|
||||
|
||||
# Cygwin build script
|
||||
#
|
||||
# NOTES:
|
||||
#
|
||||
# The stdin/stdout file descriptor appears not to be valid for the Appveyor
|
||||
# build which causes failures as certain functions attempt to redirect
|
||||
# build which causes failures as certain functions attempt to redirect
|
||||
# default file handles. Ensure a dummy file descriptor is opened with 'exec'.
|
||||
#
|
||||
#
|
||||
build_script:
|
||||
- '%BASH% -lc "cd $APPVEYOR_BUILD_FOLDER; exec 0</dev/null; export PATH=$GOPATH/bin:$PATH; make nofuse"'
|
||||
|
||||
|
||||
test_script:
|
||||
- '%BASH% -lc "cd $APPVEYOR_BUILD_FOLDER; exec 0</dev/null; export PATH=$GOPATH/bin:$PATH; export GOFLAGS=''-tags nofuse''; export TEST_NO_FUSE=1; export TEST_VERBOSE=1; export TEST_SUITE=test_sharness_expensive; make $TEST_SUITE"'
|
||||
|
||||
- '%BASH% -lc "cd $APPVEYOR_BUILD_FOLDER; exec 0</dev/null; export PATH=$GOPATH/bin:$PATH; export GOFLAGS=''-tags nofuse''; export TEST_NO_FUSE=1; export TEST_VERBOSE=1; export TEST_EXPENSIVE=1; export TEST_SUITE=test_sharness; make $TEST_SUITE"'
|
||||
|
||||
#build:
|
||||
# parallel: true
|
||||
|
||||
@ -3,13 +3,3 @@
|
||||
This directory contains the go-ipfs assets:
|
||||
|
||||
* Getting started documentation (`init-doc`).
|
||||
* Directory listing HTML template (`dir-index-html`).
|
||||
|
||||
## Re-generating
|
||||
|
||||
Edit the source files and use `go generate` from within the
|
||||
assets directory:
|
||||
|
||||
```
|
||||
go generate .
|
||||
```
|
||||
|
||||
@ -1,30 +1,22 @@
|
||||
//go:generate npm run build --prefix ./dir-index-html/
|
||||
package assets
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
gopath "path"
|
||||
"strconv"
|
||||
|
||||
"github.com/ipfs/kubo/core"
|
||||
"github.com/ipfs/kubo/core/coreapi"
|
||||
|
||||
"github.com/cespare/xxhash"
|
||||
cid "github.com/ipfs/go-cid"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
options "github.com/ipfs/interface-go-ipfs-core/options"
|
||||
"github.com/ipfs/interface-go-ipfs-core/path"
|
||||
)
|
||||
|
||||
//go:embed init-doc dir-index-html/dir-index.html dir-index-html/knownIcons.txt
|
||||
//go:embed init-doc
|
||||
var Asset embed.FS
|
||||
|
||||
// AssetHash a non-cryptographic hash of all embedded assets
|
||||
var AssetHash string
|
||||
|
||||
// initDocPaths lists the paths for the docs we want to seed during --init
|
||||
var initDocPaths = []string{
|
||||
gopath.Join("init-doc", "about"),
|
||||
@ -36,32 +28,6 @@ var initDocPaths = []string{
|
||||
gopath.Join("init-doc", "ping"),
|
||||
}
|
||||
|
||||
func init() {
|
||||
sum := xxhash.New()
|
||||
err := fs.WalkDir(Asset, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := Asset.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(sum, file)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
panic("error creating asset sum: " + err.Error())
|
||||
}
|
||||
|
||||
AssetHash = strconv.FormatUint(sum.Sum64(), 32)
|
||||
}
|
||||
|
||||
// SeedInitDocs adds the list of embedded init documentation to the passed node, pins it and returns the root key
|
||||
func SeedInitDocs(nd *core.IpfsNode) (cid.Cid, error) {
|
||||
return addAssetList(nd, initDocPaths)
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
# dag-index-html
|
||||
|
||||
> HTML representation for non-UnixFS DAGs such as DAG-CBOR.
|
||||
File diff suppressed because one or more lines are too long
@ -1,26 +0,0 @@
|
||||
# dir-index-html
|
||||
|
||||
> Directory listing HTML for HTTP gateway
|
||||
|
||||

|
||||
|
||||
## Updating
|
||||
|
||||
When making updates to the directory listing page template, please note the following:
|
||||
|
||||
1. Make your changes to the (human-friendly) source documents in the `src` directory and run `npm run build`
|
||||
3. Before testing or releasing, go to the top-level `./assets` directory and make sure to run the `go generate .` script to update the bindata version
|
||||
|
||||
## Testing
|
||||
|
||||
1. Make sure you have [Go](https://golang.org/dl/) installed
|
||||
2. Start the test server, which lives in its own directory:
|
||||
|
||||
```bash
|
||||
> cd test
|
||||
> go run .
|
||||
```
|
||||
This will listen on [`localhost:3000`](http://localhost:3000/) and reload the template every time you refresh the page.
|
||||
|
||||
If you get a "no such file or directory" error upon trying `go run .`, make sure you ran `npm run build` to generate the minified artifact that the test is looking for.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
package dirindexhtml
|
||||
@ -1,65 +0,0 @@
|
||||
.aac
|
||||
.aiff
|
||||
.ai
|
||||
.avi
|
||||
.bmp
|
||||
.c
|
||||
.cpp
|
||||
.css
|
||||
.dat
|
||||
.dmg
|
||||
.doc
|
||||
.dotx
|
||||
.dwg
|
||||
.dxf
|
||||
.eps
|
||||
.exe
|
||||
.flv
|
||||
.gif
|
||||
.h
|
||||
.hpp
|
||||
.html
|
||||
.ics
|
||||
.iso
|
||||
.java
|
||||
.jpg
|
||||
.jpeg
|
||||
.js
|
||||
.key
|
||||
.less
|
||||
.mid
|
||||
.mkv
|
||||
.mov
|
||||
.mp3
|
||||
.mp4
|
||||
.mpg
|
||||
.odf
|
||||
.ods
|
||||
.odt
|
||||
.otp
|
||||
.ots
|
||||
.ott
|
||||
.pdf
|
||||
.php
|
||||
.png
|
||||
.ppt
|
||||
.psd
|
||||
.py
|
||||
.qt
|
||||
.rar
|
||||
.rb
|
||||
.rtf
|
||||
.sass
|
||||
.scss
|
||||
.sql
|
||||
.tga
|
||||
.tgz
|
||||
.tiff
|
||||
.txt
|
||||
.wav
|
||||
.wmv
|
||||
.xls
|
||||
.xlsx
|
||||
.xml
|
||||
.yml
|
||||
.zip
|
||||
@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "dir-index-html",
|
||||
"description": "Directory listing HTML for go-ipfs gateways",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/ipfs/go-ipfs",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "cd test && go run .",
|
||||
"build": "npm run build:clean && npm run build:remove-style-links && npm run build:minify-wrap-css && npm run build:combine-html-css && npm run build:remove-unused",
|
||||
"build:clean": "rm dir-index.html",
|
||||
"build:remove-style-links": "sed '/<link rel=\"stylesheet\"/d' ./src/dir-index.html > ./base-html.html",
|
||||
"build:minify-wrap-css": "(echo \"<style>\" && cat ./src/icons.css ./src/style.css | tr -d \"\t\n\r\" && echo && echo \"</style>\") > ./minified-wrapped-style.html",
|
||||
"build:combine-html-css": "sed '/<\\/title>/ r ./minified-wrapped-style.html' ./base-html.html > ./dir-index.html",
|
||||
"build:remove-unused": "rm ./base-html.html && rm ./minified-wrapped-style.html"
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
{{ $root := . }}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="A directory of content-addressed files hosted on IPFS">
|
||||
<meta property="og:title" content="Files on IPFS">
|
||||
<meta property="og:description" content="{{ .Path }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="https://gateway.ipfs.io/ipfs/QmSDeYAe9mga6NdTozAZuyGL3Q1XjsLtvX28XFxJH8oPjq">
|
||||
<meta name="twitter:title" content="{{ .Path }}">
|
||||
<meta name="twitter:description" content="A directory of files hosted on the distributed, decentralized web using IPFS">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="https://gateway.ipfs.io/ipfs/QmSDeYAe9mga6NdTozAZuyGL3Q1XjsLtvX28XFxJH8oPjq">
|
||||
<meta name="twitter:creator" content="@ipfs">
|
||||
<meta name="twitter:site" content="@ipfs">
|
||||
<meta name="image" content="https://gateway.ipfs.io/ipfs/QmSDeYAe9mga6NdTozAZuyGL3Q1XjsLtvX28XFxJH8oPjq">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" />
|
||||
<link rel="stylesheet" href="style.css"/>
|
||||
<link rel="stylesheet" href="icons.css">
|
||||
<title>{{ .Path }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page-header">
|
||||
<div id="page-header-logo" class="ipfs-logo"> </div>
|
||||
<div id="page-header-menu">
|
||||
<div class="menu-item-wide"><a href="https://ipfs.tech" target="_blank" rel="noopener noreferrer">About IPFS</a></div>
|
||||
<div class="menu-item-wide"><a href="https://ipfs.tech#install" target="_blank" rel="noopener noreferrer">Install IPFS</a></div>
|
||||
<div class="menu-item-narrow"><a href="https://ipfs.tech" target="_blank" rel="noopener noreferrer">About</a></div>
|
||||
<div class="menu-item-narrow"><a href="https://ipfs.tech#install" target="_blank" rel="noopener noreferrer">Install</a></div>
|
||||
<div>
|
||||
<a href="https://github.com/ipfs/kubo/issues/new/choose" target="_blank" rel="noopener noreferrer" title="Report a bug">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.4 21"><circle cx="7.5" cy="4.8" r="1"/><circle cx="11.1" cy="4.8" r="1"/><path d="M12.7 8.4c-0.5-1.5-1.9-2.5-3.5-2.5 -1.6 0-3 1-3.5 2.5H12.7z"/><path d="M8.5 9.7H5c-0.5 0.8-0.7 1.7-0.7 2.7 0 2.6 1.8 4.8 4.2 5.2V9.7z"/><path d="M13.4 9.7H9.9v7.9c2.4-0.4 4.2-2.5 4.2-5.2C14.1 11.4 13.9 10.5 13.4 9.7z"/><circle cx="15.7" cy="12.9" r="1"/><circle cx="15.1" cy="15.4" r="1"/><circle cx="15.3" cy="10.4" r="1"/><circle cx="2.7" cy="12.9" r="1"/><circle cx="3.3" cy="15.4" r="1"/><circle cx="3.1" cy="10.4" r="1"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
<div id="content-header" class="d-flex flex-wrap">
|
||||
<div>
|
||||
<strong>
|
||||
Index of
|
||||
{{ range .Breadcrumbs -}}
|
||||
/{{ if .Path }}<a href="{{ $root.GatewayURL }}{{ .Path | urlEscape }}">{{ .Name }}</a>{{ else }}{{ .Name }}{{ end }}
|
||||
{{- else }}
|
||||
{{ .Path }}
|
||||
{{ end }}
|
||||
</strong>
|
||||
{{ if .Hash }}
|
||||
<div class="ipfs-hash" translate="no">
|
||||
{{ .Hash }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if .Size }}
|
||||
<div class="no-linebreak flex-shrink-1 ml-auto">
|
||||
<strong> {{ .Size }}</strong>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table>
|
||||
{{ if .BackLink }}
|
||||
<tr>
|
||||
<td class="type-icon">
|
||||
<div class="ipfs-_blank"> </div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{.BackLink | urlEscape}}">..</a>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Listing }}
|
||||
<tr>
|
||||
<td class="type-icon">
|
||||
<div class="{{iconFromExt .Name}}"> </div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ .Path | urlEscape }}">{{ .Name }}</a>
|
||||
</td>
|
||||
<td class="no-linebreak">
|
||||
{{ if .Hash }}
|
||||
<a class="ipfs-hash" translate="no" href={{ if $root.DNSLink }}"https://cid.ipfs.tech/#{{ .Hash | urlEscape}}" target="_blank" rel="noreferrer noopener"{{ else }}"{{ $root.GatewayURL }}/ipfs/{{ .Hash | urlEscape}}?filename={{ .Name | urlEscape }}"{{ end }}>
|
||||
{{ .ShortHash }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td class="no-linebreak">{{ .Size }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@ -1,212 +0,0 @@
|
||||
body {
|
||||
color:#34373f;
|
||||
font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size:14px;
|
||||
line-height:1.43;
|
||||
margin:0;
|
||||
word-break:break-all;
|
||||
-webkit-text-size-adjust:100%;
|
||||
-ms-text-size-adjust:100%;
|
||||
-webkit-tap-highlight-color:transparent
|
||||
}
|
||||
|
||||
a {
|
||||
color:#117eb3;
|
||||
text-decoration:none
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color:#00b0e9;
|
||||
text-decoration:underline
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:visited {
|
||||
color:#00b0e9
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight:700
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse:collapse;
|
||||
border-spacing:0;
|
||||
max-width:100%;
|
||||
width:100%
|
||||
}
|
||||
|
||||
table:last-child {
|
||||
border-bottom-left-radius:3px;
|
||||
border-bottom-right-radius:3px
|
||||
}
|
||||
|
||||
tr:first-child td {
|
||||
border-top:0
|
||||
}
|
||||
|
||||
tr:nth-of-type(even) {
|
||||
background-color:#f7f8fa
|
||||
}
|
||||
|
||||
td {
|
||||
border-top:1px solid #d9dbe2;
|
||||
padding:.65em;
|
||||
vertical-align:top
|
||||
}
|
||||
|
||||
#page-header {
|
||||
align-items:center;
|
||||
background:#0b3a53;
|
||||
border-bottom:4px solid #69c4cd;
|
||||
color:#fff;
|
||||
display:flex;
|
||||
font-size:1.12em;
|
||||
font-weight:500;
|
||||
justify-content:space-between;
|
||||
padding:0 1em
|
||||
}
|
||||
|
||||
#page-header a {
|
||||
color:#69c4cd
|
||||
}
|
||||
|
||||
#page-header a:active {
|
||||
color:#9ad4db
|
||||
}
|
||||
|
||||
#page-header a:hover {
|
||||
color:#fff
|
||||
}
|
||||
|
||||
#page-header-logo {
|
||||
height:2.25em;
|
||||
margin:.7em .7em .7em 0;
|
||||
width:7.15em
|
||||
}
|
||||
|
||||
#page-header-menu {
|
||||
align-items:center;
|
||||
display:flex;
|
||||
margin:.65em 0
|
||||
}
|
||||
|
||||
#page-header-menu div {
|
||||
margin:0 .6em
|
||||
}
|
||||
|
||||
#page-header-menu div:last-child {
|
||||
margin:0 0 0 .6em
|
||||
}
|
||||
|
||||
#page-header-menu svg {
|
||||
fill:#69c4cd;
|
||||
height:1.8em;
|
||||
margin-top:.125em
|
||||
}
|
||||
|
||||
#page-header-menu svg:hover {
|
||||
fill:#fff
|
||||
}
|
||||
|
||||
.menu-item-narrow {
|
||||
display:none
|
||||
}
|
||||
|
||||
#content {
|
||||
border:1px solid #d9dbe2;
|
||||
border-radius:4px;
|
||||
margin:1em
|
||||
}
|
||||
|
||||
#content-header {
|
||||
background-color:#edf0f4;
|
||||
border-bottom:1px solid #d9dbe2;
|
||||
border-top-left-radius:3px;
|
||||
border-top-right-radius:3px;
|
||||
padding:.7em 1em
|
||||
}
|
||||
|
||||
.type-icon,
|
||||
.type-icon>* {
|
||||
width:1.15em
|
||||
}
|
||||
|
||||
.no-linebreak {
|
||||
white-space:nowrap
|
||||
}
|
||||
|
||||
.ipfs-hash {
|
||||
color:#7f8491;
|
||||
font-family:monospace
|
||||
}
|
||||
|
||||
@media only screen and (max-width:500px) {
|
||||
.menu-item-narrow {
|
||||
display:inline
|
||||
}
|
||||
.menu-item-wide {
|
||||
display:none
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
#page-header {
|
||||
display:none
|
||||
}
|
||||
#content-header,
|
||||
.ipfs-hash,
|
||||
body {
|
||||
color:#000
|
||||
}
|
||||
#content-header {
|
||||
border-bottom:1px solid #000
|
||||
}
|
||||
#content {
|
||||
border:1px solid #000
|
||||
}
|
||||
a,
|
||||
a:visited {
|
||||
color:#000;
|
||||
text-decoration:underline
|
||||
}
|
||||
a[href]:after {
|
||||
content:" (" attr(href) ")"
|
||||
}
|
||||
tr {
|
||||
page-break-inside:avoid
|
||||
}
|
||||
tr:nth-of-type(even) {
|
||||
background-color:transparent
|
||||
}
|
||||
td {
|
||||
border-top:1px solid #000
|
||||
}
|
||||
}
|
||||
|
||||
@-ms-viewport {
|
||||
width:device-width
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display:flex
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-flow:wrap
|
||||
}
|
||||
|
||||
.flex-shrink-1 {
|
||||
flex-shrink:1
|
||||
}
|
||||
|
||||
.ml-auto {
|
||||
margin-left:auto
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
display:block;
|
||||
width:100%;
|
||||
overflow-x:auto;
|
||||
-webkit-overflow-scrolling:touch
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
module github.com/ipfs/dir-index-html/test
|
||||
|
||||
go 1.17
|
||||
@ -1,117 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const templateFile = "../dir-index.html"
|
||||
|
||||
// Copied from go-ipfs/core/corehttp/gateway_indexPage.go
|
||||
type listingTemplateData struct {
|
||||
GatewayURL string
|
||||
DNSLink bool
|
||||
Listing []directoryItem
|
||||
Size string
|
||||
Path string
|
||||
Breadcrumbs []breadcrumb
|
||||
BackLink string
|
||||
Hash string
|
||||
FastDirIndexThreshold int
|
||||
}
|
||||
|
||||
type directoryItem struct {
|
||||
Size string
|
||||
Name string
|
||||
Path string
|
||||
Hash string
|
||||
ShortHash string
|
||||
}
|
||||
|
||||
type breadcrumb struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
var testPath = "/ipfs/QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7/a/b/c"
|
||||
var testData = listingTemplateData{
|
||||
GatewayURL: "//localhost:3000",
|
||||
DNSLink: true,
|
||||
Listing: []directoryItem{{
|
||||
Size: "25 MiB",
|
||||
Name: "short-film.mov",
|
||||
Path: testPath + "/short-film.mov",
|
||||
Hash: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR",
|
||||
ShortHash: "QmbW\u2026sMnR",
|
||||
}, {
|
||||
Size: "23 KiB",
|
||||
Name: "250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg",
|
||||
Path: testPath + "/250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg",
|
||||
Hash: "QmUwrKrMTrNv8QjWGKMMH5QV9FMPUtRCoQ6zxTdgxATQW6",
|
||||
ShortHash: "QmUw\u2026TQW6",
|
||||
}, {
|
||||
Size: "1 KiB",
|
||||
Name: "this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt",
|
||||
Path: testPath + "/this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt",
|
||||
Hash: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
|
||||
ShortHash: "bafy\u2026bzdi",
|
||||
}},
|
||||
Size: "25 MiB",
|
||||
Path: testPath,
|
||||
Breadcrumbs: []breadcrumb{{
|
||||
Name: "ipfs",
|
||||
}, {
|
||||
Name: "QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7",
|
||||
Path: testPath + "/../../..",
|
||||
}, {
|
||||
Name: "a",
|
||||
Path: testPath + "/../..",
|
||||
}, {
|
||||
Name: "b",
|
||||
Path: testPath + "/..",
|
||||
}, {
|
||||
Name: "c",
|
||||
Path: testPath,
|
||||
}},
|
||||
BackLink: testPath + "/..",
|
||||
Hash: "QmFooBazBar2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7",
|
||||
}
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Ha-ha, tricked you! There are no files here!", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
listingTemplate, err := template.New("dir-index.html").Funcs(template.FuncMap{
|
||||
"iconFromExt": func(name string) string {
|
||||
return "ipfs-_blank" // place-holder
|
||||
},
|
||||
"urlEscape": func(rawUrl string) string {
|
||||
pathUrl := url.URL{Path: rawUrl}
|
||||
return pathUrl.String()
|
||||
},
|
||||
}).ParseFiles(templateFile)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = listingTemplate.Execute(w, &testData)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
if _, err := os.Stat(templateFile); err != nil {
|
||||
wd, _ := os.Getwd()
|
||||
fmt.Printf("could not open template file %q, relative to %q: %s\n", templateFile, wd, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("listening on localhost:3000\n")
|
||||
http.ListenAndServe("localhost:3000", mux)
|
||||
}
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
coreiface "github.com/ipfs/interface-go-ipfs-core"
|
||||
"github.com/ipfs/interface-go-ipfs-core/options"
|
||||
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
|
||||
@ -30,6 +30,8 @@ import (
|
||||
fsrepo "github.com/ipfs/kubo/repo/fsrepo"
|
||||
"github.com/ipfs/kubo/repo/fsrepo/migrations"
|
||||
"github.com/ipfs/kubo/repo/fsrepo/migrations/ipfsfetcher"
|
||||
p2pcrypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||
pnet "github.com/libp2p/go-libp2p/core/pnet"
|
||||
sockets "github.com/libp2p/go-socket-activation"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
@ -61,6 +63,7 @@ const (
|
||||
routingOptionNoneKwd = "none"
|
||||
routingOptionCustomKwd = "custom"
|
||||
routingOptionDefaultKwd = "default"
|
||||
routingOptionAutoKwd = "auto"
|
||||
unencryptTransportKwd = "disable-transport-encryption"
|
||||
unrestrictedAPIAccessKwd = "unrestricted-api"
|
||||
writableKwd = "writable"
|
||||
@ -89,7 +92,7 @@ For example, to change the 'Gateway' port:
|
||||
|
||||
ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/8082
|
||||
|
||||
The API address can be changed the same way:
|
||||
The RPC API address can be changed the same way:
|
||||
|
||||
ipfs config Addresses.API /ip4/127.0.0.1/tcp/5002
|
||||
|
||||
@ -100,14 +103,14 @@ other computers in the network, use 0.0.0.0 as the ip address:
|
||||
|
||||
ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/8080
|
||||
|
||||
Be careful if you expose the API. It is a security risk, as anyone could
|
||||
Be careful if you expose the RPC API. It is a security risk, as anyone could
|
||||
control your node remotely. If you need to control the node remotely,
|
||||
make sure to protect the port as you would other services or database
|
||||
(firewall, authenticated proxy, etc).
|
||||
|
||||
HTTP Headers
|
||||
|
||||
ipfs supports passing arbitrary headers to the API and Gateway. You can
|
||||
ipfs supports passing arbitrary headers to the RPC API and Gateway. You can
|
||||
do this by setting headers on the API.HTTPHeaders and Gateway.HTTPHeaders
|
||||
keys:
|
||||
|
||||
@ -141,18 +144,6 @@ environment variable:
|
||||
|
||||
export IPFS_PATH=/path/to/ipfsrepo
|
||||
|
||||
Routing
|
||||
|
||||
IPFS by default will use a DHT for content routing. There is an alternative
|
||||
that operates the DHT in a 'client only' mode that can be enabled by
|
||||
running the daemon as:
|
||||
|
||||
ipfs daemon --routing=dhtclient
|
||||
|
||||
Or you can set routing to dhtclient in the config:
|
||||
|
||||
ipfs config Routing.Type dhtclient
|
||||
|
||||
DEPRECATION NOTICE
|
||||
|
||||
Previously, ipfs used an environment variable as seen below:
|
||||
@ -402,14 +393,30 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment
|
||||
|
||||
routingOption, _ := req.Options[routingOptionKwd].(string)
|
||||
if routingOption == routingOptionDefaultKwd {
|
||||
routingOption = cfg.Routing.Type
|
||||
routingOption = cfg.Routing.Type.WithDefault(routingOptionAutoKwd)
|
||||
if routingOption == "" {
|
||||
routingOption = routingOptionAutoKwd
|
||||
}
|
||||
}
|
||||
|
||||
// Private setups can't leverage peers returned by default IPNIs (Routing.Type=auto)
|
||||
// To avoid breaking existing setups, switch them to DHT-only.
|
||||
if routingOption == routingOptionAutoKwd {
|
||||
if key, _ := repo.SwarmKey(); key != nil || pnet.ForcePrivateNetwork {
|
||||
log.Error("Private networking (swarm.key / LIBP2P_FORCE_PNET) does not work with public HTTP IPNIs enabled by Routing.Type=auto. Kubo will use Routing.Type=dht instead. Update config to remove this message.")
|
||||
routingOption = routingOptionDHTKwd
|
||||
}
|
||||
}
|
||||
|
||||
switch routingOption {
|
||||
case routingOptionSupernodeKwd:
|
||||
return errors.New("supernode routing was never fully implemented and has been removed")
|
||||
case routingOptionDefaultKwd, routingOptionAutoKwd:
|
||||
ncfg.Routing = libp2p.ConstructDefaultRouting(
|
||||
cfg.Identity.PeerID,
|
||||
cfg.Addresses.Swarm,
|
||||
cfg.Identity.PrivKey,
|
||||
)
|
||||
case routingOptionDHTClientKwd:
|
||||
ncfg.Routing = libp2p.DHTClientOption
|
||||
case routingOptionDHTKwd:
|
||||
@ -446,8 +453,29 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment
|
||||
fmt.Printf("Swarm key fingerprint: %x\n", node.PNetFingerprint)
|
||||
}
|
||||
|
||||
if (pnet.ForcePrivateNetwork || node.PNetFingerprint != nil) && routingOption == routingOptionAutoKwd {
|
||||
// This should never happen, but better safe than sorry
|
||||
log.Fatal("Private network does not work with Routing.Type=auto. Update your config to Routing.Type=dht (or none, and do manual peering)")
|
||||
}
|
||||
|
||||
printSwarmAddrs(node)
|
||||
|
||||
if node.PrivateKey.Type() == p2pcrypto.RSA {
|
||||
fmt.Print(`
|
||||
Warning: You are using an RSA Peer ID, which was replaced by Ed25519
|
||||
as the default recommended in Kubo since September 2020. Signing with
|
||||
RSA Peer IDs is more CPU-intensive than with other key types.
|
||||
It is recommended that you change your public key type to ed25519
|
||||
by using the following command:
|
||||
|
||||
ipfs key rotate -o rsa-key-backup -t ed25519
|
||||
|
||||
After changing your key type, restart your node for the changes to
|
||||
take effect.
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// We wait for the node to close first, as the node has children
|
||||
// that it will wait for before closing, such as the API server.
|
||||
|
||||
@ -19,7 +19,7 @@ import (
|
||||
fsrepo "github.com/ipfs/kubo/repo/fsrepo"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
options "github.com/ipfs/interface-go-ipfs-core/options"
|
||||
config "github.com/ipfs/kubo/config"
|
||||
)
|
||||
|
||||
@ -12,26 +12,27 @@ import (
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
util "github.com/ipfs/kubo/cmd/ipfs/util"
|
||||
"github.com/ipfs/kubo/cmd/ipfs/util"
|
||||
oldcmds "github.com/ipfs/kubo/commands"
|
||||
core "github.com/ipfs/kubo/core"
|
||||
"github.com/ipfs/kubo/core"
|
||||
corecmds "github.com/ipfs/kubo/core/commands"
|
||||
corehttp "github.com/ipfs/kubo/core/corehttp"
|
||||
loader "github.com/ipfs/kubo/plugin/loader"
|
||||
repo "github.com/ipfs/kubo/repo"
|
||||
fsrepo "github.com/ipfs/kubo/repo/fsrepo"
|
||||
"github.com/ipfs/kubo/core/corehttp"
|
||||
"github.com/ipfs/kubo/plugin/loader"
|
||||
"github.com/ipfs/kubo/repo"
|
||||
"github.com/ipfs/kubo/repo/fsrepo"
|
||||
"github.com/ipfs/kubo/tracing"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
"github.com/ipfs/go-ipfs-cmds/cli"
|
||||
cmdhttp "github.com/ipfs/go-ipfs-cmds/http"
|
||||
u "github.com/ipfs/go-ipfs-util"
|
||||
logging "github.com/ipfs/go-log"
|
||||
loggables "github.com/libp2p/go-libp2p-loggables"
|
||||
ma "github.com/multiformats/go-multiaddr"
|
||||
madns "github.com/multiformats/go-multiaddr-dns"
|
||||
manet "github.com/multiformats/go-multiaddr/net"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
// log is the command logger
|
||||
@ -77,9 +78,19 @@ func printErr(err error) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func newUUID(key string) logging.Metadata {
|
||||
ids := "#UUID-ERROR#"
|
||||
if id, err := uuid.NewRandom(); err == nil {
|
||||
ids = id.String()
|
||||
}
|
||||
return logging.Metadata{
|
||||
key: ids,
|
||||
}
|
||||
}
|
||||
|
||||
func mainRet() (exitCode int) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
ctx := logging.ContextWithLoggable(context.Background(), loggables.Uuid("session"))
|
||||
ctx := logging.ContextWithLoggable(context.Background(), newUUID("session"))
|
||||
var err error
|
||||
|
||||
tp, err := tracing.NewTracerProvider(ctx)
|
||||
@ -109,7 +120,7 @@ func mainRet() (exitCode int) {
|
||||
os.Args[1] = "version"
|
||||
}
|
||||
|
||||
//Handle `ipfs help` and `ipfs help <sub-command>`
|
||||
// Handle `ipfs help` and `ipfs help <sub-command>`
|
||||
if os.Args[1] == "help" {
|
||||
if len(os.Args) > 2 {
|
||||
os.Args = append(os.Args[:1], os.Args[2:]...)
|
||||
|
||||
@ -19,7 +19,7 @@ import (
|
||||
fsrepo "github.com/ipfs/kubo/repo/fsrepo"
|
||||
|
||||
fsnotify "github.com/fsnotify/fsnotify"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
process "github.com/jbenet/goprocess"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
@ -4,6 +4,7 @@ codecov:
|
||||
- "!travis-ci.org"
|
||||
- "!ci.ipfs.team:8111"
|
||||
- "!ci.ipfs.team"
|
||||
- "!github.com"
|
||||
notify:
|
||||
require_ci_to_pass: no
|
||||
after_n_builds: 2
|
||||
|
||||
@ -45,14 +45,6 @@ type Gateway struct {
|
||||
// PathPrefixes was removed: https://github.com/ipfs/go-ipfs/issues/7702
|
||||
PathPrefixes []string
|
||||
|
||||
// FastDirIndexThreshold is the maximum number of items in a directory
|
||||
// before the Gateway switches to a shallow, faster listing which only
|
||||
// requires the root node. This allows for listing big directories fast,
|
||||
// without the linear slowdown caused by reading size metadata from child
|
||||
// nodes.
|
||||
// Setting to 0 will enable fast listings for all directories.
|
||||
FastDirIndexThreshold *OptionalInteger `json:",omitempty"`
|
||||
|
||||
// FIXME: Not yet implemented: https://github.com/ipfs/kubo/issues/8059
|
||||
APICommands []string
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ func InitWithIdentity(identity Identity) (*Config, error) {
|
||||
},
|
||||
|
||||
Routing: Routing{
|
||||
Type: "dht",
|
||||
Type: nil,
|
||||
Methods: nil,
|
||||
Routers: nil,
|
||||
},
|
||||
@ -76,8 +76,8 @@ func InitWithIdentity(identity Identity) (*Config, error) {
|
||||
APICommands: []string{},
|
||||
},
|
||||
Reprovider: Reprovider{
|
||||
Interval: "12h",
|
||||
Strategy: "all",
|
||||
Interval: nil,
|
||||
Strategy: nil,
|
||||
},
|
||||
Pinning: Pinning{
|
||||
RemoteServices: map[string]RemotePinningService{},
|
||||
@ -96,11 +96,11 @@ func InitWithIdentity(identity Identity) (*Config, error) {
|
||||
|
||||
// DefaultConnMgrHighWater is the default value for the connection managers
|
||||
// 'high water' mark
|
||||
const DefaultConnMgrHighWater = 150
|
||||
const DefaultConnMgrHighWater = 96
|
||||
|
||||
// DefaultConnMgrLowWater is the default value for the connection managers 'low
|
||||
// water' mark
|
||||
const DefaultConnMgrLowWater = 50
|
||||
const DefaultConnMgrLowWater = 32
|
||||
|
||||
// DefaultConnMgrGracePeriod is the default value for the connection managers
|
||||
// grace period
|
||||
@ -110,13 +110,21 @@ const DefaultConnMgrGracePeriod = time.Second * 20
|
||||
// type.
|
||||
const DefaultConnMgrType = "basic"
|
||||
|
||||
// DefaultResourceMgrMinInboundConns is a MAGIC number that probably a good
|
||||
// enough number of inbound conns to be a good network citizen.
|
||||
const DefaultResourceMgrMinInboundConns = 800
|
||||
|
||||
func addressesConfig() Addresses {
|
||||
return Addresses{
|
||||
Swarm: []string{
|
||||
"/ip4/0.0.0.0/tcp/4001",
|
||||
"/ip6/::/tcp/4001",
|
||||
"/ip4/0.0.0.0/udp/4001/quic",
|
||||
"/ip4/0.0.0.0/udp/4001/quic-v1",
|
||||
"/ip4/0.0.0.0/udp/4001/quic-v1/webtransport",
|
||||
"/ip6/::/udp/4001/quic",
|
||||
"/ip6/::/udp/4001/quic-v1",
|
||||
"/ip6/::/udp/4001/quic-v1/webtransport",
|
||||
},
|
||||
Announce: []string{},
|
||||
AppendAnnounce: []string{},
|
||||
|
||||
@ -174,9 +174,9 @@ functionality - performance of content discovery and data
|
||||
fetching may be degraded.
|
||||
`,
|
||||
Transform: func(c *Config) error {
|
||||
c.Routing.Type = "dhtclient"
|
||||
c.Routing.Type = NewOptionalString("dhtclient") // TODO: https://github.com/ipfs/kubo/issues/9480
|
||||
c.AutoNAT.ServiceMode = AutoNATServiceDisabled
|
||||
c.Reprovider.Interval = "0"
|
||||
c.Reprovider.Interval = NewOptionalDuration(0)
|
||||
|
||||
lowWater := int64(20)
|
||||
highWater := int64(40)
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
// LastSeenMessagesStrategy is a strategy that calculates the TTL countdown
|
||||
// based on the last time a Pubsub message is seen. This means that if a message
|
||||
// is received and then seen again within the specified TTL window, it
|
||||
// won't be emitted until the TTL countdown expires from the last time the
|
||||
// message was seen.
|
||||
LastSeenMessagesStrategy = "last-seen"
|
||||
|
||||
// FirstSeenMessagesStrategy is a strategy that calculates the TTL
|
||||
// countdown based on the first time a Pubsub message is seen. This means that if
|
||||
// a message is received and then seen again within the specified TTL
|
||||
// window, it won't be emitted.
|
||||
FirstSeenMessagesStrategy = "first-seen"
|
||||
|
||||
// DefaultSeenMessagesStrategy is the strategy that is used by default if
|
||||
// no Pubsub.SeenMessagesStrategy is specified.
|
||||
DefaultSeenMessagesStrategy = LastSeenMessagesStrategy
|
||||
)
|
||||
|
||||
type PubsubConfig struct {
|
||||
// Router can be either floodsub (legacy) or gossipsub (new and
|
||||
// backwards compatible).
|
||||
@ -12,7 +31,11 @@ type PubsubConfig struct {
|
||||
// Enable pubsub (--enable-pubsub-experiment)
|
||||
Enabled Flag `json:",omitempty"`
|
||||
|
||||
// SeenMessagesTTL configures the duration after which a previously seen
|
||||
// message ID can be forgotten about.
|
||||
// SeenMessagesTTL is a value that controls the time window within which
|
||||
// duplicate messages will be identified and won't be emitted.
|
||||
SeenMessagesTTL *OptionalDuration `json:",omitempty"`
|
||||
|
||||
// SeenMessagesStrategy is a setting that determines how the time-to-live
|
||||
// (TTL) countdown for deduplicating messages is calculated.
|
||||
SeenMessagesStrategy *OptionalString `json:",omitempty"`
|
||||
}
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
const DefaultReproviderInterval = time.Hour * 22 // https://github.com/ipfs/kubo/pull/9326
|
||||
const DefaultReproviderStrategy = "all"
|
||||
|
||||
type Reprovider struct {
|
||||
Interval string // Time period to reprovide locally stored objects to the network
|
||||
Strategy string // Which keys to announce
|
||||
Interval *OptionalDuration `json:",omitempty"` // Time period to reprovide locally stored objects to the network
|
||||
Strategy *OptionalString `json:",omitempty"` // Which keys to announce
|
||||
}
|
||||
|
||||
@ -10,9 +10,10 @@ import (
|
||||
type Routing struct {
|
||||
// Type sets default daemon routing mode.
|
||||
//
|
||||
// Can be one of "dht", "dhtclient", "dhtserver", "none", or "custom".
|
||||
// When "custom" is set, you can specify a list of Routers.
|
||||
Type string
|
||||
// Can be one of "auto", "dht", "dhtclient", "dhtserver", "none", or "custom".
|
||||
// When unset or set to "auto", DHT and implicit routers are used.
|
||||
// When "custom" is set, user-provided Routing.Routers is used.
|
||||
Type *OptionalString `json:",omitempty"`
|
||||
|
||||
Routers Routers
|
||||
|
||||
@ -21,10 +22,7 @@ type Routing struct {
|
||||
|
||||
type Router struct {
|
||||
|
||||
// Currenly supported Types are "reframe", "dht", "parallel", "sequential".
|
||||
// Reframe type allows to add other resolvers using the Reframe spec:
|
||||
// https://github.com/ipfs/specs/tree/main/reframe
|
||||
// In the future we will support "dht" and other Types here.
|
||||
// Router type ID. See RouterType for more info.
|
||||
Type RouterType
|
||||
|
||||
// Parameters are extra configuration that this router might need.
|
||||
@ -106,11 +104,11 @@ func (r *RouterParser) UnmarshalJSON(b []byte) error {
|
||||
type RouterType string
|
||||
|
||||
const (
|
||||
RouterTypeReframe RouterType = "reframe"
|
||||
RouterTypeHTTP RouterType = "http"
|
||||
RouterTypeDHT RouterType = "dht"
|
||||
RouterTypeSequential RouterType = "sequential"
|
||||
RouterTypeParallel RouterType = "parallel"
|
||||
RouterTypeReframe RouterType = "reframe" // More info here: https://github.com/ipfs/specs/tree/main/reframe . Actually deprecated.
|
||||
RouterTypeHTTP RouterType = "http" // HTTP JSON API for delegated routing systems (IPIP-337).
|
||||
RouterTypeDHT RouterType = "dht" // DHT router.
|
||||
RouterTypeSequential RouterType = "sequential" // Router helper to execute several routers sequentially.
|
||||
RouterTypeParallel RouterType = "parallel" // Router helper to execute several routers in parallel.
|
||||
)
|
||||
|
||||
type DHTMode string
|
||||
|
||||
@ -13,7 +13,7 @@ func TestRouterParameters(t *testing.T) {
|
||||
sec := time.Second
|
||||
min := time.Minute
|
||||
r := Routing{
|
||||
Type: "custom",
|
||||
Type: NewOptionalString("custom"),
|
||||
Routers: map[string]RouterParser{
|
||||
"router-dht": {Router{
|
||||
Type: RouterTypeDHT,
|
||||
@ -113,7 +113,7 @@ func TestRouterMissingParameters(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
r := Routing{
|
||||
Type: "custom",
|
||||
Type: NewOptionalString("custom"),
|
||||
Routers: map[string]RouterParser{
|
||||
"router-wrong-reframe": {Router{
|
||||
Type: RouterTypeReframe,
|
||||
|
||||
@ -218,6 +218,11 @@ type OptionalDuration struct {
|
||||
value *time.Duration
|
||||
}
|
||||
|
||||
// NewOptionalDuration returns an OptionalDuration from a string
|
||||
func NewOptionalDuration(d time.Duration) *OptionalDuration {
|
||||
return &OptionalDuration{value: &d}
|
||||
}
|
||||
|
||||
func (d *OptionalDuration) UnmarshalJSON(input []byte) error {
|
||||
switch string(input) {
|
||||
case "null", "undefined", "\"null\"", "", "default", "\"\"", "\"default\"":
|
||||
|
||||
@ -84,6 +84,7 @@ func NewNode(ctx context.Context, cfg *BuildCfg) (*IpfsNode, error) {
|
||||
return nil, fmt.Errorf("building fx opts: %w", err)
|
||||
}
|
||||
}
|
||||
//nolint:staticcheck // https://github.com/ipfs/kubo/pull/9423#issuecomment-1341038770
|
||||
opts = append(opts, fx.Extract(n))
|
||||
|
||||
app := fx.New(opts...)
|
||||
|
||||
@ -12,8 +12,8 @@ import (
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
ipld "github.com/ipfs/go-ipld-format"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
mfs "github.com/ipfs/go-mfs"
|
||||
coreiface "github.com/ipfs/interface-go-ipfs-core"
|
||||
"github.com/ipfs/interface-go-ipfs-core/options"
|
||||
|
||||
@ -8,10 +8,10 @@ import (
|
||||
e "github.com/ipfs/kubo/core/commands/e"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
bitswap "github.com/ipfs/go-bitswap"
|
||||
decision "github.com/ipfs/go-bitswap/decision"
|
||||
cidutil "github.com/ipfs/go-cidutil"
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
bitswap "github.com/ipfs/go-libipfs/bitswap"
|
||||
decision "github.com/ipfs/go-libipfs/bitswap/decision"
|
||||
peer "github.com/libp2p/go-libp2p/core/peer"
|
||||
)
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
|
||||
cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
|
||||
"github.com/ipfs/kubo/core/commands/cmdutils"
|
||||
|
||||
@ -9,9 +9,9 @@ import (
|
||||
"github.com/ipfs/kubo/core/commands/cmdenv"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/ipfs/go-ipfs-cmds"
|
||||
"github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/interface-go-ipfs-core"
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
iface "github.com/ipfs/interface-go-ipfs-core"
|
||||
"github.com/ipfs/interface-go-ipfs-core/path"
|
||||
)
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ package cmdenv
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
)
|
||||
|
||||
// GetFileArg returns the next file from the directory or an error
|
||||
|
||||
@ -173,6 +173,7 @@ func TestCommands(t *testing.T) {
|
||||
"/multibase/transcode",
|
||||
"/multibase/list",
|
||||
"/name",
|
||||
"/name/inspect",
|
||||
"/name/publish",
|
||||
"/name/pubsub",
|
||||
"/name/pubsub/cancel",
|
||||
|
||||
@ -9,9 +9,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
blocks "github.com/ipfs/go-block-format"
|
||||
cid "github.com/ipfs/go-cid"
|
||||
ipld "github.com/ipfs/go-ipld-format"
|
||||
blocks "github.com/ipfs/go-libipfs/blocks"
|
||||
iface "github.com/ipfs/interface-go-ipfs-core"
|
||||
"github.com/ipfs/kubo/core/commands/cmdenv"
|
||||
|
||||
|
||||
@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
|
||||
cid "github.com/ipfs/go-cid"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
ipld "github.com/ipfs/go-ipld-format"
|
||||
ipldlegacy "github.com/ipfs/go-ipld-legacy"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
iface "github.com/ipfs/interface-go-ipfs-core"
|
||||
"github.com/ipfs/interface-go-ipfs-core/options"
|
||||
"github.com/ipfs/kubo/core/commands/cmdenv"
|
||||
|
||||
@ -4,17 +4,17 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
blocks "github.com/ipfs/go-block-format"
|
||||
"github.com/ipfs/go-cid"
|
||||
ipldlegacy "github.com/ipfs/go-ipld-legacy"
|
||||
blocks "github.com/ipfs/go-libipfs/blocks"
|
||||
"github.com/ipfs/kubo/core/commands/cmdenv"
|
||||
"github.com/ipfs/kubo/core/commands/cmdutils"
|
||||
"github.com/ipld/go-ipld-prime/multicodec"
|
||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
ipld "github.com/ipfs/go-ipld-format"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
mc "github.com/multiformats/go-multicodec"
|
||||
|
||||
// Expected minimal set of available format/ienc codecs.
|
||||
|
||||
@ -16,7 +16,7 @@ import (
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
"github.com/ipfs/go-libipfs/tar"
|
||||
"github.com/ipfs/interface-go-ipfs-core/path"
|
||||
)
|
||||
|
||||
@ -1,7 +1,25 @@
|
||||
package name
|
||||
|
||||
import (
|
||||
"github.com/ipfs/go-ipfs-cmds"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
"github.com/ipfs/go-ipns"
|
||||
ipns_pb "github.com/ipfs/go-ipns/pb"
|
||||
cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
ic "github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
mbase "github.com/multiformats/go-multibase"
|
||||
)
|
||||
|
||||
type IpnsEntry struct {
|
||||
@ -62,5 +80,204 @@ Resolve the value of a dnslink:
|
||||
"publish": PublishCmd,
|
||||
"resolve": IpnsCmd,
|
||||
"pubsub": IpnsPubsubCmd,
|
||||
"inspect": IpnsInspectCmd,
|
||||
},
|
||||
}
|
||||
|
||||
type IpnsInspectValidation struct {
|
||||
Valid bool
|
||||
Reason string
|
||||
PublicKey peer.ID
|
||||
}
|
||||
|
||||
// IpnsInspectEntry contains the deserialized values from an IPNS Entry:
|
||||
// https://github.com/ipfs/specs/blob/main/ipns/IPNS.md#record-serialization-format
|
||||
type IpnsInspectEntry struct {
|
||||
Value string
|
||||
ValidityType *ipns_pb.IpnsEntry_ValidityType
|
||||
Validity *time.Time
|
||||
Sequence uint64
|
||||
TTL *uint64
|
||||
PublicKey string
|
||||
SignatureV1 string
|
||||
SignatureV2 string
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
type IpnsInspectResult struct {
|
||||
Entry IpnsInspectEntry
|
||||
Validation *IpnsInspectValidation
|
||||
}
|
||||
|
||||
var IpnsInspectCmd = &cmds.Command{
|
||||
Status: cmds.Experimental,
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Inspects an IPNS Record",
|
||||
ShortDescription: `
|
||||
Prints values inside of IPNS Record protobuf and its DAG-CBOR Data field.
|
||||
Passing --verify will verify signature against provided public key.
|
||||
`,
|
||||
LongDescription: `
|
||||
Prints values inside of IPNS Record protobuf and its DAG-CBOR Data field.
|
||||
|
||||
The input can be a file or STDIN, the output can be JSON:
|
||||
|
||||
$ ipfs routing get "/ipns/$PEERID" > ipns_record
|
||||
$ ipfs name inspect --enc=json < ipns_record
|
||||
|
||||
Values in PublicKey, SignatureV1 and SignatureV2 fields are raw bytes encoded
|
||||
in Multibase. The Data field is DAG-CBOR represented as DAG-JSON.
|
||||
|
||||
Passing --verify will verify signature against provided public key.
|
||||
|
||||
`,
|
||||
},
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.FileArg("record", true, false, "The IPNS record payload to be verified.").EnableStdin(),
|
||||
},
|
||||
Options: []cmds.Option{
|
||||
cmds.StringOption("verify", "CID of the public IPNS key to validate against."),
|
||||
},
|
||||
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
||||
file, err := cmdenv.GetFileArg(req.Files.Entries())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
_, err = io.Copy(&b, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var entry ipns_pb.IpnsEntry
|
||||
err = proto.Unmarshal(b.Bytes(), &entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoder, err := mbase.EncoderByName("base64")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := &IpnsInspectResult{
|
||||
Entry: IpnsInspectEntry{
|
||||
Value: string(entry.Value),
|
||||
ValidityType: entry.ValidityType,
|
||||
Sequence: *entry.Sequence,
|
||||
TTL: entry.Ttl,
|
||||
PublicKey: encoder.Encode(entry.PubKey),
|
||||
SignatureV1: encoder.Encode(entry.SignatureV1),
|
||||
SignatureV2: encoder.Encode(entry.SignatureV2),
|
||||
Data: nil,
|
||||
},
|
||||
}
|
||||
|
||||
if len(entry.Data) != 0 {
|
||||
// This is hacky. The variable node (datamodel.Node) doesn't directly marshal
|
||||
// to JSON. Therefore, we need to first decode from DAG-CBOR, then encode in
|
||||
// DAG-JSON and finally unmarshal it from JSON. Since DAG-JSON is a subset
|
||||
// of JSON, that should work. Then, we can store the final value in the
|
||||
// result.Entry.Data for further inspection.
|
||||
node, err := ipld.Decode(entry.Data, dagcbor.Decode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = dagjson.Encode(node, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(buf.Bytes(), &result.Entry.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
validity, err := ipns.GetEOL(&entry)
|
||||
if err == nil {
|
||||
result.Entry.Validity = &validity
|
||||
}
|
||||
|
||||
verify, ok := req.Options["verify"].(string)
|
||||
if ok {
|
||||
key := strings.TrimPrefix(verify, "/ipns/")
|
||||
id, err := peer.Decode(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.Validation = &IpnsInspectValidation{
|
||||
PublicKey: id,
|
||||
}
|
||||
|
||||
pub, err := id.ExtractPublicKey()
|
||||
if err != nil {
|
||||
// Make sure it works with all those RSA that cannot be embedded into the
|
||||
// Peer ID.
|
||||
if len(entry.PubKey) > 0 {
|
||||
pub, err = ic.UnmarshalPublicKey(entry.PubKey)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ipns.Validate(pub, &entry)
|
||||
if err == nil {
|
||||
result.Validation.Valid = true
|
||||
} else {
|
||||
result.Validation.Reason = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return cmds.EmitOnce(res, result)
|
||||
},
|
||||
Type: IpnsInspectResult{},
|
||||
Encoders: cmds.EncoderMap{
|
||||
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *IpnsInspectResult) error {
|
||||
tw := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0)
|
||||
defer tw.Flush()
|
||||
|
||||
fmt.Fprintf(tw, "Value:\t%q\n", string(out.Entry.Value))
|
||||
fmt.Fprintf(tw, "Validity Type:\t%q\n", out.Entry.ValidityType)
|
||||
if out.Entry.Validity != nil {
|
||||
fmt.Fprintf(tw, "Validity:\t%s\n", out.Entry.Validity.Format(time.RFC3339Nano))
|
||||
}
|
||||
fmt.Fprintf(tw, "Sequence:\t%d\n", out.Entry.Sequence)
|
||||
if out.Entry.TTL != nil {
|
||||
fmt.Fprintf(tw, "TTL:\t%d\n", *out.Entry.TTL)
|
||||
}
|
||||
fmt.Fprintf(tw, "PublicKey:\t%q\n", out.Entry.PublicKey)
|
||||
fmt.Fprintf(tw, "Signature V1:\t%q\n", out.Entry.SignatureV1)
|
||||
fmt.Fprintf(tw, "Signature V2:\t%q\n", out.Entry.SignatureV2)
|
||||
|
||||
data, err := json.Marshal(out.Entry.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(tw, "Data:\t%s\n", string(data))
|
||||
|
||||
if out.Validation == nil {
|
||||
tw.Flush()
|
||||
fmt.Fprintf(w, "\nThis record was not validated.\n")
|
||||
} else {
|
||||
tw.Flush()
|
||||
fmt.Fprintf(w, "\nValidation results:\n")
|
||||
|
||||
fmt.Fprintf(tw, "\tValid:\t%v\n", out.Validation.Valid)
|
||||
if out.Validation.Reason != "" {
|
||||
fmt.Fprintf(tw, "\tReason:\t%s\n", out.Validation.Reason)
|
||||
}
|
||||
fmt.Fprintf(tw, "\tPublicKey:\t%s\n", out.Validation.PublicKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -135,6 +134,7 @@ const (
|
||||
)
|
||||
|
||||
var provideRefRoutingCmd = &cmds.Command{
|
||||
Status: cmds.Experimental,
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Announce to the network that you are providing given values.",
|
||||
},
|
||||
@ -346,6 +346,7 @@ var findPeerRoutingCmd = &cmds.Command{
|
||||
}
|
||||
|
||||
var getValueRoutingCmd = &cmds.Command{
|
||||
Status: cmds.Experimental,
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Given a key, query the routing system for its best value.",
|
||||
ShortDescription: `
|
||||
@ -362,78 +363,30 @@ Different key types can specify other 'best' rules.
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("key", true, true, "The key to find a value for."),
|
||||
},
|
||||
Options: []cmds.Option{
|
||||
cmds.BoolOption(dhtVerboseOptionName, "v", "Print extra information."),
|
||||
},
|
||||
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
||||
nd, err := cmdenv.GetNode(env)
|
||||
api, err := cmdenv.GetApi(env, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !nd.IsOnline {
|
||||
return ErrNotOnline
|
||||
}
|
||||
|
||||
dhtkey, err := escapeDhtKey(req.Arguments[0])
|
||||
r, err := api.Routing().Get(req.Context, req.Arguments[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(req.Context)
|
||||
ctx, events := routing.RegisterForQueryEvents(ctx)
|
||||
|
||||
var getErr error
|
||||
go func() {
|
||||
defer cancel()
|
||||
var val []byte
|
||||
val, getErr = nd.Routing.GetValue(ctx, dhtkey)
|
||||
if getErr != nil {
|
||||
routing.PublishQueryEvent(ctx, &routing.QueryEvent{
|
||||
Type: routing.QueryError,
|
||||
Extra: getErr.Error(),
|
||||
})
|
||||
} else {
|
||||
routing.PublishQueryEvent(ctx, &routing.QueryEvent{
|
||||
Type: routing.Value,
|
||||
Extra: base64.StdEncoding.EncodeToString(val),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
for e := range events {
|
||||
if err := res.Emit(e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return getErr
|
||||
return res.Emit(r)
|
||||
},
|
||||
Encoders: cmds.EncoderMap{
|
||||
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
|
||||
pfm := pfuncMap{
|
||||
routing.Value: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
|
||||
if verbose {
|
||||
_, err := fmt.Fprintf(out, "got value: '%s'\n", obj.Extra)
|
||||
return err
|
||||
}
|
||||
res, err := base64.StdEncoding.DecodeString(obj.Extra)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = out.Write(res)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
verbose, _ := req.Options[dhtVerboseOptionName].(bool)
|
||||
return printEvent(out, w, verbose, pfm)
|
||||
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out []byte) error {
|
||||
_, err := w.Write(out)
|
||||
return err
|
||||
}),
|
||||
},
|
||||
Type: routing.QueryEvent{},
|
||||
Type: []byte{},
|
||||
}
|
||||
|
||||
var putValueRoutingCmd = &cmds.Command{
|
||||
Status: cmds.Experimental,
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Write a key/value pair to the routing system.",
|
||||
ShortDescription: `
|
||||
@ -459,20 +412,8 @@ identified by QmFoo.
|
||||
cmds.StringArg("key", true, false, "The key to store the value at."),
|
||||
cmds.FileArg("value-file", true, false, "A path to a file containing the value to store.").EnableStdin(),
|
||||
},
|
||||
Options: []cmds.Option{
|
||||
cmds.BoolOption(dhtVerboseOptionName, "v", "Print extra information."),
|
||||
},
|
||||
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
||||
nd, err := cmdenv.GetNode(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !nd.IsOnline {
|
||||
return ErrNotOnline
|
||||
}
|
||||
|
||||
key, err := escapeDhtKey(req.Arguments[0])
|
||||
api, err := cmdenv.GetApi(env, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -488,50 +429,20 @@ identified by QmFoo.
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(req.Context)
|
||||
ctx, events := routing.RegisterForQueryEvents(ctx)
|
||||
|
||||
var putErr error
|
||||
go func() {
|
||||
defer cancel()
|
||||
putErr = nd.Routing.PutValue(ctx, key, []byte(data))
|
||||
if putErr != nil {
|
||||
routing.PublishQueryEvent(ctx, &routing.QueryEvent{
|
||||
Type: routing.QueryError,
|
||||
Extra: putErr.Error(),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
for e := range events {
|
||||
if err := res.Emit(e); err != nil {
|
||||
return err
|
||||
}
|
||||
err = api.Routing().Put(req.Context, req.Arguments[0], data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return putErr
|
||||
return res.Emit([]byte(fmt.Sprintf("%s added", req.Arguments[0])))
|
||||
},
|
||||
Encoders: cmds.EncoderMap{
|
||||
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
|
||||
pfm := pfuncMap{
|
||||
routing.FinalPeer: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
|
||||
if verbose {
|
||||
fmt.Fprintf(out, "* closest peer %s\n", obj.ID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
routing.Value: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
|
||||
fmt.Fprintf(out, "%s\n", obj.ID.Pretty())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
verbose, _ := req.Options[dhtVerboseOptionName].(bool)
|
||||
|
||||
return printEvent(out, w, verbose, pfm)
|
||||
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out []byte) error {
|
||||
_, err := w.Write(out)
|
||||
return err
|
||||
}),
|
||||
},
|
||||
Type: routing.QueryEvent{},
|
||||
Type: []byte{},
|
||||
}
|
||||
|
||||
type printFunc func(obj *routing.QueryEvent, out io.Writer, verbose bool) error
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
"github.com/ipfs/kubo/commands"
|
||||
"github.com/ipfs/kubo/config"
|
||||
"github.com/ipfs/kubo/core/commands/cmdenv"
|
||||
@ -337,12 +337,15 @@ The scope can be one of the following:
|
||||
- all -- reports the resource usage for all currently active scopes.
|
||||
|
||||
The output of this command is JSON.
|
||||
|
||||
To see all resources that are close to hitting their respective limit, one can do something like:
|
||||
ipfs swarm stats --min-used-limit-perc=90 all
|
||||
`},
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("scope", true, false, "scope of the stat report"),
|
||||
},
|
||||
Options: []cmds.Option{
|
||||
cmds.IntOption(swarmUsedResourcesPercentageName, "Display only resources that are using above the specified percentage"),
|
||||
cmds.IntOption(swarmUsedResourcesPercentageName, "Only display resources that are using above the specified percentage of their respective limit"),
|
||||
},
|
||||
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
||||
node, err := cmdenv.GetNode(env)
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs-cmds"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
"github.com/ipfs/interface-go-ipfs-core/options"
|
||||
)
|
||||
|
||||
|
||||
@ -221,7 +221,7 @@ func GetNode(t *testing.T, reframeURLs ...string) *IpfsNode {
|
||||
API: []string{"/ip4/127.0.0.1/tcp/0"},
|
||||
},
|
||||
Routing: config.Routing{
|
||||
Type: "custom",
|
||||
Type: config.NewOptionalString("custom"),
|
||||
Routers: routers,
|
||||
Methods: config.Methods{
|
||||
config.MethodNameFindPeers: config.Method{
|
||||
|
||||
@ -6,9 +6,9 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
blocks "github.com/ipfs/go-block-format"
|
||||
cid "github.com/ipfs/go-cid"
|
||||
pin "github.com/ipfs/go-ipfs-pinner"
|
||||
blocks "github.com/ipfs/go-libipfs/blocks"
|
||||
coreiface "github.com/ipfs/interface-go-ipfs-core"
|
||||
caopts "github.com/ipfs/interface-go-ipfs-core/options"
|
||||
path "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
|
||||
@ -144,6 +144,11 @@ func (api *CoreAPI) PubSub() coreiface.PubSubAPI {
|
||||
return (*PubSubAPI)(api)
|
||||
}
|
||||
|
||||
// Routing returns the RoutingAPI interface implementation backed by the kubo node
|
||||
func (api *CoreAPI) Routing() coreiface.RoutingAPI {
|
||||
return (*RoutingAPI)(api)
|
||||
}
|
||||
|
||||
// WithOptions returns api with global options applied
|
||||
func (api *CoreAPI) WithOptions(opts ...options.ApiOption) (coreiface.CoreAPI, error) {
|
||||
settings := api.parentOpts // make sure to copy
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
ipath "github.com/ipfs/go-path"
|
||||
coreiface "github.com/ipfs/interface-go-ipfs-core"
|
||||
caopts "github.com/ipfs/interface-go-ipfs-core/options"
|
||||
nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
|
||||
path "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
ci "github.com/libp2p/go-libp2p/core/crypto"
|
||||
peer "github.com/libp2p/go-libp2p/core/peer"
|
||||
@ -37,8 +38,6 @@ func (e *ipnsEntry) Value() path.Path {
|
||||
return e.value
|
||||
}
|
||||
|
||||
type requestContextKey string
|
||||
|
||||
// Publish announces new IPNS name and returns the new IPNS entry.
|
||||
func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.NamePublishOption) (coreiface.IpnsEntry, error) {
|
||||
ctx, span := tracing.Span(ctx, "CoreAPI.NameAPI", "Publish", trace.WithAttributes(attribute.String("path", p.String())))
|
||||
@ -76,13 +75,17 @@ func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.Nam
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if options.TTL != nil {
|
||||
// nolint: staticcheck // non-backward compatible change
|
||||
ctx = context.WithValue(ctx, requestContextKey("ipns-publish-ttl"), *options.TTL)
|
||||
eol := time.Now().Add(options.ValidTime)
|
||||
|
||||
publishOptions := []nsopts.PublishOption{
|
||||
nsopts.PublishWithEOL(eol),
|
||||
}
|
||||
|
||||
eol := time.Now().Add(options.ValidTime)
|
||||
err = api.namesys.PublishWithEOL(ctx, k, pth, eol)
|
||||
if options.TTL != nil {
|
||||
publishOptions = append(publishOptions, nsopts.PublishWithTTL(*options.TTL))
|
||||
}
|
||||
|
||||
err = api.namesys.Publish(ctx, k, pth, publishOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
53
core/coreapi/routing.go
Normal file
53
core/coreapi/routing.go
Normal file
@ -0,0 +1,53 @@
|
||||
package coreapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/ipfs/go-path"
|
||||
coreiface "github.com/ipfs/interface-go-ipfs-core"
|
||||
peer "github.com/libp2p/go-libp2p/core/peer"
|
||||
)
|
||||
|
||||
type RoutingAPI CoreAPI
|
||||
|
||||
func (r *RoutingAPI) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
if !r.nd.IsOnline {
|
||||
return nil, coreiface.ErrOffline
|
||||
}
|
||||
|
||||
dhtKey, err := normalizeKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.routing.GetValue(ctx, dhtKey)
|
||||
}
|
||||
|
||||
func (r *RoutingAPI) Put(ctx context.Context, key string, value []byte) error {
|
||||
if !r.nd.IsOnline {
|
||||
return coreiface.ErrOffline
|
||||
}
|
||||
|
||||
dhtKey, err := normalizeKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.routing.PutValue(ctx, dhtKey, value)
|
||||
}
|
||||
|
||||
func normalizeKey(s string) (string, error) {
|
||||
parts := path.SplitList(s)
|
||||
if len(parts) != 3 ||
|
||||
parts[0] != "" ||
|
||||
!(parts[1] == "ipns" || parts[1] == "pk") {
|
||||
return "", errors.New("invalid key")
|
||||
}
|
||||
|
||||
k, err := peer.Decode(parts[2])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path.Join(append(parts[:2], string(k))), nil
|
||||
}
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
"github.com/ipfs/go-merkledag"
|
||||
uio "github.com/ipfs/go-unixfs/io"
|
||||
"github.com/ipfs/interface-go-ipfs-core/options"
|
||||
|
||||
@ -17,8 +17,8 @@ import (
|
||||
cidutil "github.com/ipfs/go-cidutil"
|
||||
filestore "github.com/ipfs/go-filestore"
|
||||
bstore "github.com/ipfs/go-ipfs-blockstore"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
ipld "github.com/ipfs/go-ipld-format"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
merkledag "github.com/ipfs/go-merkledag"
|
||||
dagtest "github.com/ipfs/go-merkledag/test"
|
||||
mfs "github.com/ipfs/go-mfs"
|
||||
@ -271,32 +271,36 @@ func (api *UnixfsAPI) processLink(ctx context.Context, linkres ft.LinkResult, se
|
||||
lnk.Type = coreiface.TFile
|
||||
lnk.Size = linkres.Link.Size
|
||||
case cid.DagProtobuf:
|
||||
if !settings.ResolveChildren {
|
||||
break
|
||||
}
|
||||
|
||||
linkNode, err := linkres.Link.GetNode(ctx, api.dag)
|
||||
if err != nil {
|
||||
lnk.Err = err
|
||||
break
|
||||
}
|
||||
|
||||
if pn, ok := linkNode.(*merkledag.ProtoNode); ok {
|
||||
d, err := ft.FSNodeFromBytes(pn.Data())
|
||||
if settings.ResolveChildren {
|
||||
linkNode, err := linkres.Link.GetNode(ctx, api.dag)
|
||||
if err != nil {
|
||||
lnk.Err = err
|
||||
break
|
||||
}
|
||||
switch d.Type() {
|
||||
case ft.TFile, ft.TRaw:
|
||||
lnk.Type = coreiface.TFile
|
||||
case ft.THAMTShard, ft.TDirectory, ft.TMetadata:
|
||||
lnk.Type = coreiface.TDirectory
|
||||
case ft.TSymlink:
|
||||
lnk.Type = coreiface.TSymlink
|
||||
lnk.Target = string(d.Data())
|
||||
|
||||
if pn, ok := linkNode.(*merkledag.ProtoNode); ok {
|
||||
d, err := ft.FSNodeFromBytes(pn.Data())
|
||||
if err != nil {
|
||||
lnk.Err = err
|
||||
break
|
||||
}
|
||||
switch d.Type() {
|
||||
case ft.TFile, ft.TRaw:
|
||||
lnk.Type = coreiface.TFile
|
||||
case ft.THAMTShard, ft.TDirectory, ft.TMetadata:
|
||||
lnk.Type = coreiface.TDirectory
|
||||
case ft.TSymlink:
|
||||
lnk.Type = coreiface.TSymlink
|
||||
lnk.Target = string(d.Data())
|
||||
}
|
||||
if !settings.UseCumulativeSize {
|
||||
lnk.Size = d.FileSize()
|
||||
}
|
||||
}
|
||||
lnk.Size = d.FileSize()
|
||||
}
|
||||
|
||||
if settings.UseCumulativeSize {
|
||||
lnk.Size = linkres.Link.Size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
coreiface "github.com/ipfs/interface-go-ipfs-core"
|
||||
"github.com/ipfs/go-libipfs/gateway"
|
||||
options "github.com/ipfs/interface-go-ipfs-core/options"
|
||||
path "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
version "github.com/ipfs/kubo"
|
||||
core "github.com/ipfs/kubo/core"
|
||||
coreapi "github.com/ipfs/kubo/core/coreapi"
|
||||
@ -17,47 +14,6 @@ import (
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
type GatewayConfig struct {
|
||||
Headers map[string][]string
|
||||
Writable bool
|
||||
FastDirIndexThreshold int
|
||||
}
|
||||
|
||||
// NodeAPI defines the minimal set of API services required by a gateway handler
|
||||
type NodeAPI interface {
|
||||
// Unixfs returns an implementation of Unixfs API
|
||||
Unixfs() coreiface.UnixfsAPI
|
||||
|
||||
// Block returns an implementation of Block API
|
||||
Block() coreiface.BlockAPI
|
||||
|
||||
// Dag returns an implementation of Dag API
|
||||
Dag() coreiface.APIDagService
|
||||
|
||||
// ResolvePath resolves the path using Unixfs resolver
|
||||
ResolvePath(context.Context, path.Path) (path.Resolved, error)
|
||||
}
|
||||
|
||||
// A helper function to clean up a set of headers:
|
||||
// 1. Canonicalizes.
|
||||
// 2. Deduplicates.
|
||||
// 3. Sorts.
|
||||
func cleanHeaderSet(headers []string) []string {
|
||||
// Deduplicate and canonicalize.
|
||||
m := make(map[string]struct{}, len(headers))
|
||||
for _, h := range headers {
|
||||
m[http.CanonicalHeaderKey(h)] = struct{}{}
|
||||
}
|
||||
result := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
result = append(result, k)
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func GatewayOption(writable bool, paths ...string) ServeOption {
|
||||
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
|
||||
cfg, err := n.Repo.Config()
|
||||
@ -75,17 +31,16 @@ func GatewayOption(writable bool, paths ...string) ServeOption {
|
||||
headers[http.CanonicalHeaderKey(h)] = v
|
||||
}
|
||||
|
||||
AddAccessControlHeaders(headers)
|
||||
gateway.AddAccessControlHeaders(headers)
|
||||
|
||||
offlineAPI, err := api.WithOptions(options.Api.Offline(true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gateway := NewGatewayHandler(GatewayConfig{
|
||||
Headers: headers,
|
||||
Writable: writable,
|
||||
FastDirIndexThreshold: int(cfg.Gateway.FastDirIndexThreshold.WithDefault(100)),
|
||||
gateway := gateway.NewHandler(gateway.Config{
|
||||
Headers: headers,
|
||||
Writable: writable,
|
||||
}, api, offlineAPI)
|
||||
|
||||
gateway = otelhttp.NewHandler(gateway, "Gateway.Request")
|
||||
@ -97,50 +52,6 @@ func GatewayOption(writable bool, paths ...string) ServeOption {
|
||||
}
|
||||
}
|
||||
|
||||
// AddAccessControlHeaders adds default headers used for controlling
|
||||
// cross-origin requests. This function adds several values to the
|
||||
// Access-Control-Allow-Headers and Access-Control-Expose-Headers entries.
|
||||
// If the Access-Control-Allow-Origin entry is missing a value of '*' is
|
||||
// added, indicating that browsers should allow requesting code from any
|
||||
// origin to access the resource.
|
||||
// If the Access-Control-Allow-Methods entry is missing a value of 'GET' is
|
||||
// added, indicating that browsers may use the GET method when issuing cross
|
||||
// origin requests.
|
||||
func AddAccessControlHeaders(headers map[string][]string) {
|
||||
// Hard-coded headers.
|
||||
const ACAHeadersName = "Access-Control-Allow-Headers"
|
||||
const ACEHeadersName = "Access-Control-Expose-Headers"
|
||||
const ACAOriginName = "Access-Control-Allow-Origin"
|
||||
const ACAMethodsName = "Access-Control-Allow-Methods"
|
||||
|
||||
if _, ok := headers[ACAOriginName]; !ok {
|
||||
// Default to *all*
|
||||
headers[ACAOriginName] = []string{"*"}
|
||||
}
|
||||
if _, ok := headers[ACAMethodsName]; !ok {
|
||||
// Default to GET
|
||||
headers[ACAMethodsName] = []string{http.MethodGet}
|
||||
}
|
||||
|
||||
headers[ACAHeadersName] = cleanHeaderSet(
|
||||
append([]string{
|
||||
"Content-Type",
|
||||
"User-Agent",
|
||||
"Range",
|
||||
"X-Requested-With",
|
||||
}, headers[ACAHeadersName]...))
|
||||
|
||||
headers[ACEHeadersName] = cleanHeaderSet(
|
||||
append([]string{
|
||||
"Content-Length",
|
||||
"Content-Range",
|
||||
"X-Chunked-Output",
|
||||
"X-Stream-Output",
|
||||
"X-Ipfs-Path",
|
||||
"X-Ipfs-Roots",
|
||||
}, headers[ACEHeadersName]...))
|
||||
}
|
||||
|
||||
func VersionOption() ServeOption {
|
||||
return func(_ *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
|
||||
mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,55 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
"github.com/ipfs/kubo/tracing"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// serveRawBlock returns bytes behind a raw block
|
||||
func (i *gatewayHandler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) {
|
||||
ctx, span := tracing.Span(ctx, "Gateway", "ServeRawBlock", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
|
||||
defer span.End()
|
||||
blockCid := resolvedPath.Cid()
|
||||
blockReader, err := i.api.Block().Get(ctx, resolvedPath)
|
||||
if err != nil {
|
||||
webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
block, err := io.ReadAll(blockReader)
|
||||
if err != nil {
|
||||
webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
content := bytes.NewReader(block)
|
||||
|
||||
// Set Content-Disposition
|
||||
var name string
|
||||
if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
|
||||
name = urlFilename
|
||||
} else {
|
||||
name = blockCid.String() + ".bin"
|
||||
}
|
||||
setContentDispositionHeader(w, name, "attachment")
|
||||
|
||||
// Set remaining headers
|
||||
modtime := addCacheControlHeaders(w, r, contentPath, blockCid)
|
||||
w.Header().Set("Content-Type", "application/vnd.ipld.raw")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^)
|
||||
|
||||
// ServeContent will take care of
|
||||
// If-None-Match+Etag, Content-Length and range requests
|
||||
_, dataSent, _ := ServeContent(w, r, name, modtime, content)
|
||||
|
||||
if dataSent {
|
||||
// Update metrics
|
||||
i.rawBlockGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
|
||||
}
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
blocks "github.com/ipfs/go-block-format"
|
||||
cid "github.com/ipfs/go-cid"
|
||||
coreiface "github.com/ipfs/interface-go-ipfs-core"
|
||||
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
"github.com/ipfs/kubo/tracing"
|
||||
gocar "github.com/ipld/go-car"
|
||||
selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// serveCAR returns a CAR stream for specific DAG+selector
|
||||
func (i *gatewayHandler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) {
|
||||
ctx, span := tracing.Span(ctx, "Gateway", "ServeCAR", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
|
||||
defer span.End()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
switch carVersion {
|
||||
case "": // noop, client does not care about version
|
||||
case "1": // noop, we support this
|
||||
default:
|
||||
err := fmt.Errorf("only version=1 is supported")
|
||||
webError(w, "unsupported CAR version", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rootCid := resolvedPath.Cid()
|
||||
|
||||
// Set Content-Disposition
|
||||
var name string
|
||||
if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
|
||||
name = urlFilename
|
||||
} else {
|
||||
name = rootCid.String() + ".car"
|
||||
}
|
||||
setContentDispositionHeader(w, name, "attachment")
|
||||
|
||||
// Set Cache-Control (same logic as for a regular files)
|
||||
addCacheControlHeaders(w, r, contentPath, rootCid)
|
||||
|
||||
// Weak Etag W/ because we can't guarantee byte-for-byte identical
|
||||
// responses, but still want to benefit from HTTP Caching. Two CAR
|
||||
// responses for the same CID and selector will be logically equivalent,
|
||||
// but when CAR is streamed, then in theory, blocks may arrive from
|
||||
// datastore in non-deterministic order.
|
||||
etag := `W/` + getEtag(r, rootCid)
|
||||
w.Header().Set("Etag", etag)
|
||||
|
||||
// Finish early if Etag match
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
// Make it clear we don't support range-requests over a car stream
|
||||
// Partial downloads and resumes should be handled using requests for
|
||||
// sub-DAGs and IPLD selectors: https://github.com/ipfs/go-ipfs/issues/8769
|
||||
w.Header().Set("Accept-Ranges", "none")
|
||||
|
||||
w.Header().Set("Content-Type", "application/vnd.ipld.car; version=1")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^)
|
||||
|
||||
// Same go-car settings as dag.export command
|
||||
store := dagStore{dag: i.api.Dag(), ctx: ctx}
|
||||
|
||||
// TODO: support selectors passed as request param: https://github.com/ipfs/kubo/issues/8769
|
||||
dag := gocar.Dag{Root: rootCid, Selector: selectorparse.CommonSelector_ExploreAllRecursively}
|
||||
car := gocar.NewSelectiveCar(ctx, store, []gocar.Dag{dag}, gocar.TraverseLinksOnlyOnce())
|
||||
|
||||
if err := car.Write(w); err != nil {
|
||||
// We return error as a trailer, however it is not something browsers can access
|
||||
// (https://github.com/mdn/browser-compat-data/issues/14703)
|
||||
// Due to this, we suggest client always verify that
|
||||
// the received CAR stream response is matching requested DAG selector
|
||||
w.Header().Set("X-Stream-Error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
i.carStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
|
||||
}
|
||||
|
||||
// FIXME(@Jorropo): https://github.com/ipld/go-car/issues/315
|
||||
type dagStore struct {
|
||||
dag coreiface.APIDagService
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (ds dagStore) Get(_ context.Context, c cid.Cid) (blocks.Block, error) {
|
||||
return ds.dag.Get(ds.ctx, c)
|
||||
}
|
||||
@ -1,258 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cid "github.com/ipfs/go-cid"
|
||||
ipldlegacy "github.com/ipfs/go-ipld-legacy"
|
||||
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
"github.com/ipfs/kubo/assets"
|
||||
dih "github.com/ipfs/kubo/assets/dag-index-html"
|
||||
"github.com/ipfs/kubo/tracing"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/multicodec"
|
||||
mc "github.com/multiformats/go-multicodec"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// codecToContentType maps the supported IPLD codecs to the HTTP Content
|
||||
// Type they should have.
|
||||
var codecToContentType = map[uint64]string{
|
||||
uint64(mc.Json): "application/json",
|
||||
uint64(mc.Cbor): "application/cbor",
|
||||
uint64(mc.DagJson): "application/vnd.ipld.dag-json",
|
||||
uint64(mc.DagCbor): "application/vnd.ipld.dag-cbor",
|
||||
}
|
||||
|
||||
// contentTypeToCodecs maps the HTTP Content Type to the respective
|
||||
// possible codecs. If the original data is in one of those codecs,
|
||||
// we stream the raw bytes. Otherwise, we encode in the last codec
|
||||
// of the list.
|
||||
var contentTypeToCodecs = map[string][]uint64{
|
||||
"application/json": {uint64(mc.Json), uint64(mc.DagJson)},
|
||||
"application/vnd.ipld.dag-json": {uint64(mc.DagJson)},
|
||||
"application/cbor": {uint64(mc.Cbor), uint64(mc.DagCbor)},
|
||||
"application/vnd.ipld.dag-cbor": {uint64(mc.DagCbor)},
|
||||
}
|
||||
|
||||
// contentTypeToExtension maps the HTTP Content Type to the respective file
|
||||
// extension, used in Content-Disposition header when downloading the file.
|
||||
var contentTypeToExtension = map[string]string{
|
||||
"application/json": ".json",
|
||||
"application/vnd.ipld.dag-json": ".json",
|
||||
"application/cbor": ".cbor",
|
||||
"application/vnd.ipld.dag-cbor": ".cbor",
|
||||
}
|
||||
|
||||
func (i *gatewayHandler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) {
|
||||
ctx, span := tracing.Span(ctx, "Gateway", "ServeCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", requestedContentType)))
|
||||
defer span.End()
|
||||
|
||||
cidCodec := resolvedPath.Cid().Prefix().Codec
|
||||
responseContentType := requestedContentType
|
||||
|
||||
// If the resolved path still has some remainder, return error for now.
|
||||
// TODO: handle this when we have IPLD Patch (https://ipld.io/specs/patch/) via HTTP PUT
|
||||
// TODO: (depends on https://github.com/ipfs/kubo/issues/4801 and https://github.com/ipfs/kubo/issues/4782)
|
||||
if resolvedPath.Remainder() != "" {
|
||||
path := strings.TrimSuffix(resolvedPath.String(), resolvedPath.Remainder())
|
||||
err := fmt.Errorf("%q of %q could not be returned: reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented: try reading %q instead", resolvedPath.Remainder(), resolvedPath.String(), path)
|
||||
webError(w, "unsupported pathing", err, http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
// If no explicit content type was requested, the response will have one based on the codec from the CID
|
||||
if requestedContentType == "" {
|
||||
cidContentType, ok := codecToContentType[cidCodec]
|
||||
if !ok {
|
||||
// Should not happen unless function is called with wrong parameters.
|
||||
err := fmt.Errorf("content type not found for codec: %v", cidCodec)
|
||||
webError(w, "internal error", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
responseContentType = cidContentType
|
||||
}
|
||||
|
||||
// Set HTTP headers (for caching etc)
|
||||
modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid())
|
||||
name := setCodecContentDisposition(w, r, resolvedPath, responseContentType)
|
||||
w.Header().Set("Content-Type", responseContentType)
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// No content type is specified by the user (via Accept, or format=). However,
|
||||
// we support this format. Let's handle it.
|
||||
if requestedContentType == "" {
|
||||
isDAG := cidCodec == uint64(mc.DagJson) || cidCodec == uint64(mc.DagCbor)
|
||||
acceptsHTML := strings.Contains(r.Header.Get("Accept"), "text/html")
|
||||
download := r.URL.Query().Get("download") == "true"
|
||||
|
||||
if isDAG && acceptsHTML && !download {
|
||||
i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath)
|
||||
} else {
|
||||
i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, the user has requested a specific content type. Let's first get
|
||||
// the codecs that can be used with this content type.
|
||||
codecs, ok := contentTypeToCodecs[requestedContentType]
|
||||
if !ok {
|
||||
// This is never supposed to happen unless function is called with wrong parameters.
|
||||
err := fmt.Errorf("unsupported content type: %s", requestedContentType)
|
||||
webError(w, err.Error(), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If we need to convert, use the last codec (strict dag- variant)
|
||||
toCodec := codecs[len(codecs)-1]
|
||||
|
||||
// If the requested content type has "dag-", ALWAYS go through the encoding
|
||||
// process in order to validate the content.
|
||||
if strings.Contains(requestedContentType, "dag-") {
|
||||
i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, check if the data is encoded with the requested content type.
|
||||
// If so, we can directly stream the raw data. serveRawBlock cannot be directly
|
||||
// used here as it sets different headers.
|
||||
for _, codec := range codecs {
|
||||
if resolvedPath.Cid().Prefix().Codec == codec {
|
||||
i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, if nothing of the above is true, we have to actually convert the codec.
|
||||
i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime)
|
||||
}
|
||||
|
||||
func (i *gatewayHandler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path) {
|
||||
// A HTML directory index will be presented, be sure to set the correct
|
||||
// type instead of relying on autodetection (which may fail).
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
// Clear Content-Disposition -- we want HTML to be rendered inline
|
||||
w.Header().Del("Content-Disposition")
|
||||
|
||||
// Generated index requires custom Etag (output may change between Kubo versions)
|
||||
dagEtag := getDagIndexEtag(resolvedPath.Cid())
|
||||
w.Header().Set("Etag", dagEtag)
|
||||
|
||||
// Remove Cache-Control for now to match UnixFS dir-index-html responses
|
||||
// (we don't want browser to cache HTML forever)
|
||||
// TODO: if we ever change behavior for UnixFS dir listings, same changes should be applied here
|
||||
w.Header().Del("Cache-Control")
|
||||
|
||||
cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec)
|
||||
if err := dih.DagIndexTemplate.Execute(w, dih.DagIndexTemplateData{
|
||||
Path: contentPath.String(),
|
||||
CID: resolvedPath.Cid().String(),
|
||||
CodecName: cidCodec.String(),
|
||||
CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)),
|
||||
}); err != nil {
|
||||
webError(w, "failed to generate HTML listing for this DAG: try fetching raw block with ?format=raw", err, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *gatewayHandler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime time.Time) {
|
||||
blockCid := resolvedPath.Cid()
|
||||
blockReader, err := i.api.Block().Get(ctx, resolvedPath)
|
||||
if err != nil {
|
||||
webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
block, err := io.ReadAll(blockReader)
|
||||
if err != nil {
|
||||
webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
content := bytes.NewReader(block)
|
||||
|
||||
// ServeContent will take care of
|
||||
// If-None-Match+Etag, Content-Length and range requests
|
||||
_, _, _ = ServeContent(w, r, name, modtime, content)
|
||||
}
|
||||
|
||||
func (i *gatewayHandler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec uint64, modtime time.Time) {
|
||||
obj, err := i.api.Dag().Get(ctx, resolvedPath.Cid())
|
||||
if err != nil {
|
||||
webError(w, "ipfs dag get "+html.EscapeString(resolvedPath.String()), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
universal, ok := obj.(ipldlegacy.UniversalNode)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%T is not a valid IPLD node", obj)
|
||||
webError(w, err.Error(), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
finalNode := universal.(ipld.Node)
|
||||
|
||||
encoder, err := multicodec.LookupEncoder(toCodec)
|
||||
if err != nil {
|
||||
webError(w, err.Error(), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure IPLD node conforms to the codec specification.
|
||||
var buf bytes.Buffer
|
||||
err = encoder(finalNode, &buf)
|
||||
if err != nil {
|
||||
webError(w, err.Error(), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Sets correct Last-Modified header. This code is borrowed from the standard
|
||||
// library (net/http/server.go) as we cannot use serveFile.
|
||||
if !(modtime.IsZero() || modtime.Equal(unixEpochTime)) {
|
||||
w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
_, _ = w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentType string) string {
|
||||
var dispType, name string
|
||||
|
||||
ext, ok := contentTypeToExtension[contentType]
|
||||
if !ok {
|
||||
// Should never happen.
|
||||
ext = ".bin"
|
||||
}
|
||||
|
||||
if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
|
||||
name = urlFilename
|
||||
} else {
|
||||
name = resolvedPath.Cid().String() + ext
|
||||
}
|
||||
|
||||
// JSON should be inlined, but ?download=true should still override
|
||||
if r.URL.Query().Get("download") == "true" {
|
||||
dispType = "attachment"
|
||||
} else {
|
||||
switch ext {
|
||||
case ".json": // codecs that serialize to JSON can be rendered by browsers
|
||||
dispType = "inline"
|
||||
default: // everything else is assumed binary / opaque bytes
|
||||
dispType = "attachment"
|
||||
}
|
||||
}
|
||||
|
||||
setContentDispositionHeader(w, name, dispType)
|
||||
return name
|
||||
}
|
||||
|
||||
func getDagIndexEtag(dagCid cid.Cid) string {
|
||||
return `"DagIndex-` + assets.AssetHash + `_CID-` + dagCid.String() + `"`
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
"github.com/ipfs/kubo/tracing"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var unixEpochTime = time.Unix(0, 0)
|
||||
|
||||
func (i *gatewayHandler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) {
|
||||
ctx, span := tracing.Span(ctx, "Gateway", "ServeTAR", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
|
||||
defer span.End()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Get Unixfs file
|
||||
file, err := i.api.Unixfs().Get(ctx, resolvedPath)
|
||||
if err != nil {
|
||||
webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
rootCid := resolvedPath.Cid()
|
||||
|
||||
// Set Cache-Control and read optional Last-Modified time
|
||||
modtime := addCacheControlHeaders(w, r, contentPath, rootCid)
|
||||
|
||||
// Weak Etag W/ because we can't guarantee byte-for-byte identical
|
||||
// responses, but still want to benefit from HTTP Caching. Two TAR
|
||||
// responses for the same CID will be logically equivalent,
|
||||
// but when TAR is streamed, then in theory, files and directories
|
||||
// may arrive in different order (depends on TAR lib and filesystem/inodes).
|
||||
etag := `W/` + getEtag(r, rootCid)
|
||||
w.Header().Set("Etag", etag)
|
||||
|
||||
// Finish early if Etag match
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
// Set Content-Disposition
|
||||
var name string
|
||||
if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
|
||||
name = urlFilename
|
||||
} else {
|
||||
name = rootCid.String() + ".tar"
|
||||
}
|
||||
setContentDispositionHeader(w, name, "attachment")
|
||||
|
||||
// Construct the TAR writer
|
||||
tarw, err := files.NewTarWriter(w)
|
||||
if err != nil {
|
||||
webError(w, "could not build tar writer", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer tarw.Close()
|
||||
|
||||
// Sets correct Last-Modified header. This code is borrowed from the standard
|
||||
// library (net/http/server.go) as we cannot use serveFile without throwing the entire
|
||||
// TAR into the memory first.
|
||||
if !(modtime.IsZero() || modtime.Equal(unixEpochTime)) {
|
||||
w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/x-tar")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^)
|
||||
|
||||
// The TAR has a top-level directory (or file) named by the CID.
|
||||
if err := tarw.WriteFile(file, rootCid.String()); err != nil {
|
||||
w.Header().Set("X-Stream-Error", err.Error())
|
||||
// Trailer headers do not work in web browsers
|
||||
// (see https://github.com/mdn/browser-compat-data/issues/14703)
|
||||
// and we have limited options around error handling in browser contexts.
|
||||
// To improve UX/DX, we finish response stream with error message, allowing client to
|
||||
// (1) detect error by having corrupted TAR
|
||||
// (2) be able to reason what went wrong by instecting the tail of TAR stream
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
"github.com/ipfs/kubo/tracing"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (i *gatewayHandler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) {
|
||||
ctx, span := tracing.Span(ctx, "Gateway", "ServeUnixFS", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
|
||||
defer span.End()
|
||||
|
||||
// Handling UnixFS
|
||||
dr, err := i.api.Unixfs().Get(ctx, resolvedPath)
|
||||
if err != nil {
|
||||
webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer dr.Close()
|
||||
|
||||
// Handling Unixfs file
|
||||
if f, ok := dr.(files.File); ok {
|
||||
logger.Debugw("serving unixfs file", "path", contentPath)
|
||||
i.serveFile(ctx, w, r, resolvedPath, contentPath, f, begin)
|
||||
return
|
||||
}
|
||||
|
||||
// Handling Unixfs directory
|
||||
dir, ok := dr.(files.Directory)
|
||||
if !ok {
|
||||
internalWebError(w, fmt.Errorf("unsupported UnixFS type"))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugw("serving unixfs directory", "path", contentPath)
|
||||
i.serveDirectory(ctx, w, r, resolvedPath, contentPath, dir, begin, logger)
|
||||
}
|
||||
@ -1,287 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
gopath "path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
redirects "github.com/ipfs/go-ipfs-redirects-file"
|
||||
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Resolving a UnixFS path involves determining if the provided `path.Path` exists and returning the `path.Resolved`
|
||||
// corresponding to that path. For UnixFS, path resolution is more involved.
|
||||
//
|
||||
// When a path under requested CID does not exist, Gateway will check if a `_redirects` file exists
|
||||
// underneath the root CID of the path, and apply rules defined there.
|
||||
// See sepcification introduced in: https://github.com/ipfs/specs/pull/290
|
||||
//
|
||||
// Scenario 1:
|
||||
// If a path exists, we always return the `path.Resolved` corresponding to that path, regardless of the existence of a `_redirects` file.
|
||||
//
|
||||
// Scenario 2:
|
||||
// If a path does not exist, usually we should return a `nil` resolution path and an error indicating that the path
|
||||
// doesn't exist. However, a `_redirects` file may exist and contain a redirect rule that redirects that path to a different path.
|
||||
// We need to evaluate the rule and perform the redirect if present.
|
||||
//
|
||||
// Scenario 3:
|
||||
// Another possibility is that the path corresponds to a rewrite rule (i.e. a rule with a status of 200).
|
||||
// In this case, we don't perform a redirect, but do need to return a `path.Resolved` and `path.Path` corresponding to
|
||||
// the rewrite destination path.
|
||||
//
|
||||
// Note that for security reasons, redirect rules are only processed when the request has origin isolation.
|
||||
// See https://github.com/ipfs/specs/pull/290 for more information.
|
||||
func (i *gatewayHandler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, logger *zap.SugaredLogger) (newResolvedPath ipath.Resolved, newContentPath ipath.Path, continueProcessing bool, hadMatchingRule bool) {
|
||||
redirectsFile := i.getRedirectsFile(r, contentPath, logger)
|
||||
if redirectsFile != nil {
|
||||
redirectRules, err := i.getRedirectRules(r, redirectsFile)
|
||||
if err != nil {
|
||||
internalWebError(w, err)
|
||||
return nil, nil, false, true
|
||||
}
|
||||
|
||||
redirected, newPath, err := i.handleRedirectsFileRules(w, r, contentPath, redirectRules)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("trouble processing _redirects file at %q: %w", redirectsFile.String(), err)
|
||||
internalWebError(w, err)
|
||||
return nil, nil, false, true
|
||||
}
|
||||
|
||||
if redirected {
|
||||
return nil, nil, false, true
|
||||
}
|
||||
|
||||
// 200 is treated as a rewrite, so update the path and continue
|
||||
if newPath != "" {
|
||||
// Reassign contentPath and resolvedPath since the URL was rewritten
|
||||
contentPath = ipath.New(newPath)
|
||||
resolvedPath, err = i.api.ResolvePath(r.Context(), contentPath)
|
||||
if err != nil {
|
||||
internalWebError(w, err)
|
||||
return nil, nil, false, true
|
||||
}
|
||||
|
||||
return resolvedPath, contentPath, true, true
|
||||
}
|
||||
}
|
||||
// No matching rule, paths remain the same, continue regular processing
|
||||
return resolvedPath, contentPath, true, false
|
||||
}
|
||||
|
||||
func (i *gatewayHandler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, redirectRules []redirects.Rule) (redirected bool, newContentPath string, err error) {
|
||||
// Attempt to match a rule to the URL path, and perform the corresponding redirect or rewrite
|
||||
pathParts := strings.Split(contentPath.String(), "/")
|
||||
if len(pathParts) > 3 {
|
||||
// All paths should start with /ipfs/cid/, so get the path after that
|
||||
urlPath := "/" + strings.Join(pathParts[3:], "/")
|
||||
rootPath := strings.Join(pathParts[:3], "/")
|
||||
// Trim off the trailing /
|
||||
urlPath = strings.TrimSuffix(urlPath, "/")
|
||||
|
||||
for _, rule := range redirectRules {
|
||||
// Error right away if the rule is invalid
|
||||
if !rule.MatchAndExpandPlaceholders(urlPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
// We have a match!
|
||||
|
||||
// Rewrite
|
||||
if rule.Status == 200 {
|
||||
// Prepend the rootPath
|
||||
toPath := rootPath + rule.To
|
||||
return false, toPath, nil
|
||||
}
|
||||
|
||||
// Or 4xx
|
||||
if rule.Status == 404 || rule.Status == 410 || rule.Status == 451 {
|
||||
toPath := rootPath + rule.To
|
||||
content4xxPath := ipath.New(toPath)
|
||||
err := i.serve4xx(w, r, content4xxPath, rule.Status)
|
||||
return true, toPath, err
|
||||
}
|
||||
|
||||
// Or redirect
|
||||
if rule.Status >= 301 && rule.Status <= 308 {
|
||||
http.Redirect(w, r, rule.To, rule.Status)
|
||||
return true, "", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No redirects matched
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
func (i *gatewayHandler) getRedirectRules(r *http.Request, redirectsFilePath ipath.Resolved) ([]redirects.Rule, error) {
|
||||
// Convert the path into a file node
|
||||
node, err := i.api.Unixfs().Get(r.Context(), redirectsFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get _redirects: %w", err)
|
||||
}
|
||||
defer node.Close()
|
||||
|
||||
// Convert the node into a file
|
||||
f, ok := node.(files.File)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not parse _redirects: %w", err)
|
||||
}
|
||||
|
||||
// Parse redirect rules from file
|
||||
redirectRules, err := redirects.Parse(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse _redirects: %w", err)
|
||||
}
|
||||
|
||||
return redirectRules, nil
|
||||
}
|
||||
|
||||
// Returns a resolved path to the _redirects file located in the root CID path of the requested path
|
||||
func (i *gatewayHandler) getRedirectsFile(r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) ipath.Resolved {
|
||||
// contentPath is the full ipfs path to the requested resource,
|
||||
// regardless of whether path or subdomain resolution is used.
|
||||
rootPath := getRootPath(contentPath)
|
||||
|
||||
// Check for _redirects file.
|
||||
// Any path resolution failures are ignored and we just assume there's no _redirects file.
|
||||
// Note that ignoring these errors also ensures that the use of the empty CID (bafkqaaa) in tests doesn't fail.
|
||||
path := ipath.Join(rootPath, "_redirects")
|
||||
resolvedPath, err := i.api.ResolvePath(r.Context(), path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
// Returns the root CID Path for the given path
|
||||
func getRootPath(path ipath.Path) ipath.Path {
|
||||
parts := strings.Split(path.String(), "/")
|
||||
return ipath.New(gopath.Join("/", path.Namespace(), parts[2]))
|
||||
}
|
||||
|
||||
func (i *gatewayHandler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPath ipath.Path, status int) error {
|
||||
resolved4xxPath, err := i.api.ResolvePath(r.Context(), content4xxPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
node, err := i.api.Unixfs().Get(r.Context(), resolved4xxPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer node.Close()
|
||||
|
||||
f, ok := node.(files.File)
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert node for %d page to file", status)
|
||||
}
|
||||
|
||||
size, err := f.Size()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get size of %d page", status)
|
||||
}
|
||||
|
||||
log.Debugf("using _redirects: custom %d file at %q", status, content4xxPath)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
||||
addCacheControlHeaders(w, r, content4xxPath, resolved4xxPath.Cid())
|
||||
w.WriteHeader(status)
|
||||
_, err = io.CopyN(w, f, size)
|
||||
return err
|
||||
}
|
||||
|
||||
func hasOriginIsolation(r *http.Request) bool {
|
||||
_, gw := r.Context().Value(requestContextKey("gw-hostname")).(string)
|
||||
_, dnslink := r.Context().Value("dnslink-hostname").(string)
|
||||
|
||||
if gw || dnslink {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isUnixfsResponseFormat(responseFormat string) bool {
|
||||
// The implicit response format is UnixFS
|
||||
return responseFormat == ""
|
||||
}
|
||||
|
||||
// Deprecated: legacy ipfs-404.html files are superseded by _redirects file
|
||||
// This is provided only for backward-compatibility, until websites migrate
|
||||
// to 404s managed via _redirects file (https://github.com/ipfs/specs/pull/290)
|
||||
func (i *gatewayHandler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool {
|
||||
resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
dr, err := i.api.Unixfs().Get(r.Context(), resolved404Path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer dr.Close()
|
||||
|
||||
f, ok := dr.(files.File)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
size, err := f.Size()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
log.Debugw("using pretty 404 file", "path", contentPath)
|
||||
w.Header().Set("Content-Type", ctype)
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, err = io.CopyN(w, f, size)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) {
|
||||
filename404, ctype, err := preferred404Filename(r.Header.Values("Accept"))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
pathComponents := strings.Split(contentPath.String(), "/")
|
||||
|
||||
for idx := len(pathComponents); idx >= 3; idx-- {
|
||||
pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...)
|
||||
parsed404Path := ipath.New("/" + pretty404)
|
||||
if parsed404Path.IsValid() != nil {
|
||||
break
|
||||
}
|
||||
resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return resolvedPath, ctype, nil
|
||||
}
|
||||
|
||||
return nil, "", fmt.Errorf("no pretty 404 in any parent folder")
|
||||
}
|
||||
|
||||
func preferred404Filename(acceptHeaders []string) (string, string, error) {
|
||||
// If we ever want to offer a 404 file for a different content type
|
||||
// then this function will need to parse q weightings, but for now
|
||||
// the presence of anything matching HTML is enough.
|
||||
for _, acceptHeader := range acceptHeaders {
|
||||
accepted := strings.Split(acceptHeader, ",")
|
||||
for _, spec := range accepted {
|
||||
contentType := strings.SplitN(spec, ";", 1)[0]
|
||||
switch contentType {
|
||||
case "*/*", "text/*", "text/html":
|
||||
return "ipfs-404.html", "text/html", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("there is no 404 file for the requested content types")
|
||||
}
|
||||
@ -1,222 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
gopath "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
cid "github.com/ipfs/go-cid"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
path "github.com/ipfs/go-path"
|
||||
"github.com/ipfs/go-path/resolver"
|
||||
options "github.com/ipfs/interface-go-ipfs-core/options"
|
||||
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
"github.com/ipfs/kubo/assets"
|
||||
"github.com/ipfs/kubo/tracing"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// serveDirectory returns the best representation of UnixFS directory
|
||||
//
|
||||
// It will return index.html if present, or generate directory listing otherwise.
|
||||
func (i *gatewayHandler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, dir files.Directory, begin time.Time, logger *zap.SugaredLogger) {
|
||||
ctx, span := tracing.Span(ctx, "Gateway", "ServeDirectory", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
|
||||
defer span.End()
|
||||
|
||||
// HostnameOption might have constructed an IPNS/IPFS path using the Host header.
|
||||
// In this case, we need the original path for constructing redirects
|
||||
// and links that match the requested URL.
|
||||
// For example, http://example.net would become /ipns/example.net, and
|
||||
// the redirects and links would end up as http://example.net/ipns/example.net
|
||||
requestURI, err := url.ParseRequestURI(r.RequestURI)
|
||||
if err != nil {
|
||||
webError(w, "failed to parse request path", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
originalURLPath := requestURI.Path
|
||||
|
||||
// Ensure directory paths end with '/'
|
||||
if originalURLPath[len(originalURLPath)-1] != '/' {
|
||||
// don't redirect to trailing slash if it's go get
|
||||
// https://github.com/ipfs/kubo/pull/3963
|
||||
goget := r.URL.Query().Get("go-get") == "1"
|
||||
if !goget {
|
||||
suffix := "/"
|
||||
// preserve query parameters
|
||||
if r.URL.RawQuery != "" {
|
||||
suffix = suffix + "?" + r.URL.RawQuery
|
||||
}
|
||||
// /ipfs/cid/foo?bar must be redirected to /ipfs/cid/foo/?bar
|
||||
redirectURL := originalURLPath + suffix
|
||||
logger.Debugw("directory location moved permanently", "status", http.StatusMovedPermanently)
|
||||
http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if directory has index.html, if so, serveFile
|
||||
idxPath := ipath.Join(contentPath, "index.html")
|
||||
idx, err := i.api.Unixfs().Get(ctx, idxPath)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
f, ok := idx.(files.File)
|
||||
if !ok {
|
||||
internalWebError(w, files.ErrNotReader)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugw("serving index.html file", "path", idxPath)
|
||||
// write to request
|
||||
i.serveFile(ctx, w, r, resolvedPath, idxPath, f, begin)
|
||||
return
|
||||
case resolver.ErrNoLink:
|
||||
logger.Debugw("no index.html; noop", "path", idxPath)
|
||||
default:
|
||||
internalWebError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// See statusResponseWriter.WriteHeader
|
||||
// and https://github.com/ipfs/kubo/issues/7164
|
||||
// Note: this needs to occur before listingTemplate.Execute otherwise we get
|
||||
// superfluous response.WriteHeader call from prometheus/client_golang
|
||||
if w.Header().Get("Location") != "" {
|
||||
logger.Debugw("location moved permanently", "status", http.StatusMovedPermanently)
|
||||
w.WriteHeader(http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
|
||||
// A HTML directory index will be presented, be sure to set the correct
|
||||
// type instead of relying on autodetection (which may fail).
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
// Generated dir index requires custom Etag (output may change between go-ipfs versions)
|
||||
dirEtag := getDirListingEtag(resolvedPath.Cid())
|
||||
w.Header().Set("Etag", dirEtag)
|
||||
|
||||
if r.Method == http.MethodHead {
|
||||
logger.Debug("return as request's HTTP method is HEAD")
|
||||
return
|
||||
}
|
||||
|
||||
// Optimization 1:
|
||||
// List children without fetching their root blocks (fast, but no size info)
|
||||
results, err := i.api.Unixfs().Ls(ctx, resolvedPath, options.Unixfs.ResolveChildren(false))
|
||||
if err != nil {
|
||||
internalWebError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// storage for directory listing
|
||||
dirListing := make([]directoryItem, 0, len(results))
|
||||
|
||||
for link := range results {
|
||||
if link.Err != nil {
|
||||
internalWebError(w, err)
|
||||
return
|
||||
}
|
||||
hash := link.Cid.String()
|
||||
di := directoryItem{
|
||||
Size: "", // no size because we did not fetch child nodes
|
||||
Name: link.Name,
|
||||
Path: gopath.Join(originalURLPath, link.Name),
|
||||
Hash: hash,
|
||||
ShortHash: shortHash(hash),
|
||||
}
|
||||
dirListing = append(dirListing, di)
|
||||
}
|
||||
|
||||
// Optimization 2: fetch sizes only for dirs below FastDirIndexThreshold
|
||||
if len(dirListing) < i.config.FastDirIndexThreshold {
|
||||
dirit := dir.Entries()
|
||||
linkNo := 0
|
||||
for dirit.Next() {
|
||||
size := "?"
|
||||
if s, err := dirit.Node().Size(); err == nil {
|
||||
// Size may not be defined/supported. Continue anyways.
|
||||
size = humanize.Bytes(uint64(s))
|
||||
}
|
||||
dirListing[linkNo].Size = size
|
||||
linkNo++
|
||||
}
|
||||
}
|
||||
|
||||
// construct the correct back link
|
||||
// https://github.com/ipfs/kubo/issues/1365
|
||||
backLink := originalURLPath
|
||||
|
||||
// don't go further up than /ipfs/$hash/
|
||||
pathSplit := path.SplitList(contentPath.String())
|
||||
switch {
|
||||
// skip backlink when listing a content root
|
||||
case len(pathSplit) == 3: // url: /ipfs/$hash
|
||||
backLink = ""
|
||||
|
||||
// skip backlink when listing a content root
|
||||
case len(pathSplit) == 4 && pathSplit[3] == "": // url: /ipfs/$hash/
|
||||
backLink = ""
|
||||
|
||||
// add the correct link depending on whether the path ends with a slash
|
||||
default:
|
||||
if strings.HasSuffix(backLink, "/") {
|
||||
backLink += ".."
|
||||
} else {
|
||||
backLink += "/.."
|
||||
}
|
||||
}
|
||||
|
||||
size := "?"
|
||||
if s, err := dir.Size(); err == nil {
|
||||
// Size may not be defined/supported. Continue anyways.
|
||||
size = humanize.Bytes(uint64(s))
|
||||
}
|
||||
|
||||
hash := resolvedPath.Cid().String()
|
||||
|
||||
// Gateway root URL to be used when linking to other rootIDs.
|
||||
// This will be blank unless subdomain or DNSLink resolution is being used
|
||||
// for this request.
|
||||
var gwURL string
|
||||
|
||||
// Get gateway hostname and build gateway URL.
|
||||
if h, ok := r.Context().Value(requestContextKey("gw-hostname")).(string); ok {
|
||||
gwURL = "//" + h
|
||||
} else {
|
||||
gwURL = ""
|
||||
}
|
||||
|
||||
dnslink := hasDNSLinkOrigin(gwURL, contentPath.String())
|
||||
|
||||
// See comment above where originalUrlPath is declared.
|
||||
tplData := listingTemplateData{
|
||||
GatewayURL: gwURL,
|
||||
DNSLink: dnslink,
|
||||
Listing: dirListing,
|
||||
Size: size,
|
||||
Path: contentPath.String(),
|
||||
Breadcrumbs: breadcrumbs(contentPath.String(), dnslink),
|
||||
BackLink: backLink,
|
||||
Hash: hash,
|
||||
FastDirIndexThreshold: i.config.FastDirIndexThreshold,
|
||||
}
|
||||
|
||||
logger.Debugw("request processed", "tplDataDNSLink", dnslink, "tplDataSize", size, "tplDataBackLink", backLink, "tplDataHash", hash)
|
||||
|
||||
if err := listingTemplate.Execute(w, tplData); err != nil {
|
||||
internalWebError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
i.unixfsGenDirGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
|
||||
}
|
||||
|
||||
func getDirListingEtag(dirCid cid.Cid) string {
|
||||
return `"DirIndex-` + assets.AssetHash + `_CID-` + dirCid.String() + `"`
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
gopath "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
||||
"github.com/ipfs/kubo/tracing"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// serveFile returns data behind a file along with HTTP headers based on
|
||||
// the file itself, its CID and the contentPath used for accessing it.
|
||||
func (i *gatewayHandler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, file files.File, begin time.Time) {
|
||||
_, span := tracing.Span(ctx, "Gateway", "ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
|
||||
defer span.End()
|
||||
|
||||
// Set Cache-Control and read optional Last-Modified time
|
||||
modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid())
|
||||
|
||||
// Set Content-Disposition
|
||||
name := addContentDispositionHeader(w, r, contentPath)
|
||||
|
||||
// Prepare size value for Content-Length HTTP header (set inside of http.ServeContent)
|
||||
size, err := file.Size()
|
||||
if err != nil {
|
||||
http.Error(w, "cannot serve files with unknown sizes", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
// We override null files to 200 to avoid issues with fragment caching reverse proxies.
|
||||
// Also whatever you are asking for, it's cheaper to just give you the complete file (nothing).
|
||||
// TODO: remove this if clause once https://github.com/golang/go/issues/54794 is fixed in two latest releases of go
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Lazy seeker enables efficient range-requests and HTTP HEAD responses
|
||||
content := &lazySeeker{
|
||||
size: size,
|
||||
reader: file,
|
||||
}
|
||||
|
||||
// Calculate deterministic value for Content-Type HTTP header
|
||||
// (we prefer to do it here, rather than using implicit sniffing in http.ServeContent)
|
||||
var ctype string
|
||||
if _, isSymlink := file.(*files.Symlink); isSymlink {
|
||||
// We should be smarter about resolving symlinks but this is the
|
||||
// "most correct" we can be without doing that.
|
||||
ctype = "inode/symlink"
|
||||
} else {
|
||||
ctype = mime.TypeByExtension(gopath.Ext(name))
|
||||
if ctype == "" {
|
||||
// uses https://github.com/gabriel-vasile/mimetype library to determine the content type.
|
||||
// Fixes https://github.com/ipfs/kubo/issues/7252
|
||||
mimeType, err := mimetype.DetectReader(content)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("cannot detect content-type: %s", err.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctype = mimeType.String()
|
||||
_, err = content.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
http.Error(w, "seeker can't seek", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Strip the encoding from the HTML Content-Type header and let the
|
||||
// browser figure it out.
|
||||
//
|
||||
// Fixes https://github.com/ipfs/kubo/issues/2203
|
||||
if strings.HasPrefix(ctype, "text/html;") {
|
||||
ctype = "text/html"
|
||||
}
|
||||
}
|
||||
// Setting explicit Content-Type to avoid mime-type sniffing on the client
|
||||
// (unifies behavior across gateways and web browsers)
|
||||
w.Header().Set("Content-Type", ctype)
|
||||
|
||||
// special fixup around redirects
|
||||
w = &statusResponseWriter{w}
|
||||
|
||||
// ServeContent will take care of
|
||||
// If-None-Match+Etag, Content-Length and range requests
|
||||
_, dataSent, _ := ServeContent(w, r, name, modtime, content)
|
||||
|
||||
// Was response successful?
|
||||
if dataSent {
|
||||
// Update metrics
|
||||
i.unixfsFileGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
|
||||
}
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
ipfspath "github.com/ipfs/go-path"
|
||||
"github.com/ipfs/kubo/assets"
|
||||
)
|
||||
|
||||
// structs for directory listing
|
||||
type listingTemplateData struct {
|
||||
GatewayURL string
|
||||
DNSLink bool
|
||||
Listing []directoryItem
|
||||
Size string
|
||||
Path string
|
||||
Breadcrumbs []breadcrumb
|
||||
BackLink string
|
||||
Hash string
|
||||
FastDirIndexThreshold int
|
||||
}
|
||||
|
||||
type directoryItem struct {
|
||||
Size string
|
||||
Name string
|
||||
Path string
|
||||
Hash string
|
||||
ShortHash string
|
||||
}
|
||||
|
||||
type breadcrumb struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
func breadcrumbs(urlPath string, dnslinkOrigin bool) []breadcrumb {
|
||||
var ret []breadcrumb
|
||||
|
||||
p, err := ipfspath.ParsePath(urlPath)
|
||||
if err != nil {
|
||||
// No breadcrumbs, fallback to bare Path in template
|
||||
return ret
|
||||
}
|
||||
segs := p.Segments()
|
||||
contentRoot := segs[1]
|
||||
for i, seg := range segs {
|
||||
if i == 0 {
|
||||
ret = append(ret, breadcrumb{Name: seg})
|
||||
} else {
|
||||
ret = append(ret, breadcrumb{
|
||||
Name: seg,
|
||||
Path: "/" + strings.Join(segs[0:i+1], "/"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the /ipns/<fqdn> prefix from breadcrumb Paths when directory
|
||||
// listing on a DNSLink website (loaded due to Host header in HTTP
|
||||
// request). Necessary because the hostname most likely won't have a
|
||||
// public gateway mounted.
|
||||
if dnslinkOrigin {
|
||||
prefix := "/ipns/" + contentRoot
|
||||
for i, crumb := range ret {
|
||||
if strings.HasPrefix(crumb.Path, prefix) {
|
||||
ret[i].Path = strings.Replace(crumb.Path, prefix, "", 1)
|
||||
}
|
||||
}
|
||||
// Make contentRoot breadcrumb link to the website root
|
||||
ret[1].Path = "/"
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func shortHash(hash string) string {
|
||||
if len(hash) <= 8 {
|
||||
return hash
|
||||
}
|
||||
return (hash[0:4] + "\u2026" + hash[len(hash)-4:])
|
||||
}
|
||||
|
||||
// helper to detect DNSLink website context
|
||||
// (when hostname from gwURL is matching /ipns/<fqdn> in path)
|
||||
func hasDNSLinkOrigin(gwURL string, path string) bool {
|
||||
if gwURL != "" {
|
||||
fqdn := stripPort(strings.TrimPrefix(gwURL, "//"))
|
||||
return strings.HasPrefix(path, "/ipns/"+fqdn)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var listingTemplate *template.Template
|
||||
|
||||
func init() {
|
||||
knownIconsBytes, err := assets.Asset.ReadFile("dir-index-html/knownIcons.txt")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
knownIcons := make(map[string]struct{})
|
||||
for _, ext := range strings.Split(strings.TrimSuffix(string(knownIconsBytes), "\n"), "\n") {
|
||||
knownIcons[ext] = struct{}{}
|
||||
}
|
||||
|
||||
// helper to guess the type/icon for it by the extension name
|
||||
iconFromExt := func(name string) string {
|
||||
ext := path.Ext(name)
|
||||
_, ok := knownIcons[ext]
|
||||
if !ok {
|
||||
// default blank icon
|
||||
return "ipfs-_blank"
|
||||
}
|
||||
return "ipfs-" + ext[1:] // slice of the first dot
|
||||
}
|
||||
|
||||
// custom template-escaping function to escape a full path, including '#' and '?'
|
||||
urlEscape := func(rawUrl string) string {
|
||||
pathURL := url.URL{Path: rawUrl}
|
||||
return pathURL.String()
|
||||
}
|
||||
|
||||
// Directory listing template
|
||||
dirIndexBytes, err := assets.Asset.ReadFile("dir-index-html/dir-index.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
listingTemplate = template.Must(template.New("dir").Funcs(template.FuncMap{
|
||||
"iconFromExt": iconFromExt,
|
||||
"urlEscape": urlEscape,
|
||||
}).Parse(string(dirIndexBytes)))
|
||||
}
|
||||
@ -9,7 +9,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
namesys "github.com/ipfs/go-namesys"
|
||||
version "github.com/ipfs/kubo"
|
||||
@ -19,7 +18,7 @@ import (
|
||||
|
||||
datastore "github.com/ipfs/go-datastore"
|
||||
syncds "github.com/ipfs/go-datastore/sync"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
path "github.com/ipfs/go-path"
|
||||
iface "github.com/ipfs/interface-go-ipfs-core"
|
||||
nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
|
||||
@ -68,11 +67,7 @@ func (m mockNamesys) ResolveAsync(ctx context.Context, name string, opts ...nsop
|
||||
return out
|
||||
}
|
||||
|
||||
func (m mockNamesys) Publish(ctx context.Context, name ci.PrivKey, value path.Path) error {
|
||||
return errors.New("not implemented for mockNamesys")
|
||||
}
|
||||
|
||||
func (m mockNamesys) PublishWithEOL(ctx context.Context, name ci.PrivKey, value path.Path, _ time.Time) error {
|
||||
func (m mockNamesys) Publish(ctx context.Context, name ci.PrivKey, value path.Path, opts ...nsopts.PublishOption) error {
|
||||
return errors.New("not implemented for mockNamesys")
|
||||
}
|
||||
|
||||
@ -656,28 +651,3 @@ func TestVersion(t *testing.T) {
|
||||
t.Fatalf("response doesn't contain protocol version:\n%s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtagMatch(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
header string // value in If-None-Match HTTP header
|
||||
cidEtag string
|
||||
dirEtag string
|
||||
expected bool // expected result of etagMatch(header, cidEtag, dirEtag)
|
||||
}{
|
||||
{"", `"etag"`, "", false}, // no If-None-Match
|
||||
{"", "", `"etag"`, false}, // no If-None-Match
|
||||
{`"etag"`, `"etag"`, "", true}, // file etag match
|
||||
{`W/"etag"`, `"etag"`, "", true}, // file etag match
|
||||
{`"foo", W/"bar", W/"etag"`, `"etag"`, "", true}, // file etag match (array)
|
||||
{`"foo",W/"bar",W/"etag"`, `"etag"`, "", true}, // file etag match (compact array)
|
||||
{`"etag"`, "", `W/"etag"`, true}, // dir etag match
|
||||
{`"etag"`, "", `W/"etag"`, true}, // dir etag match
|
||||
{`W/"etag"`, "", `W/"etag"`, true}, // dir etag match
|
||||
{`*`, `"etag"`, "", true}, // wildcard etag match
|
||||
} {
|
||||
result := etagMatch(test.header, test.cidEtag, test.dirEtag)
|
||||
if result != test.expected {
|
||||
t.Fatalf("unexpected result of etagMatch(%q, %q, %q), got %t, expected %t", test.header, test.cidEtag, test.dirEtag, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
cid "github.com/ipfs/go-cid"
|
||||
"github.com/ipfs/go-libipfs/gateway"
|
||||
namesys "github.com/ipfs/go-namesys"
|
||||
core "github.com/ipfs/kubo/core"
|
||||
coreapi "github.com/ipfs/kubo/core/coreapi"
|
||||
@ -225,7 +226,7 @@ func HostnameOption() ServeOption {
|
||||
if !cfg.Gateway.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) {
|
||||
// rewrite path and handle as DNSLink
|
||||
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
|
||||
ctx := context.WithValue(r.Context(), requestContextKey("dnslink-hostname"), host)
|
||||
ctx := context.WithValue(r.Context(), gateway.DNSLinkHostnameKey, host)
|
||||
childMux.ServeHTTP(w, withHostnameContext(r.WithContext(ctx), host))
|
||||
return
|
||||
}
|
||||
@ -247,8 +248,6 @@ type wildcardHost struct {
|
||||
spec *config.GatewaySpec
|
||||
}
|
||||
|
||||
type requestContextKey string
|
||||
|
||||
// Extends request context to include hostname of a canonical gateway root
|
||||
// (subdomain root or dnslink fqdn)
|
||||
func withHostnameContext(r *http.Request, hostname string) *http.Request {
|
||||
@ -257,7 +256,7 @@ func withHostnameContext(r *http.Request, hostname string) *http.Request {
|
||||
// Host header, subdomain gateways have more comples rules (knownSubdomainDetails)
|
||||
// More: https://github.com/ipfs/dir-index-html/issues/42
|
||||
// nolint: staticcheck // non-backward compatible change
|
||||
ctx := context.WithValue(r.Context(), requestContextKey("gw-hostname"), hostname)
|
||||
ctx := context.WithValue(r.Context(), gateway.GatewayHostnameKey, hostname)
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
cid "github.com/ipfs/go-cid"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
path "github.com/ipfs/go-path"
|
||||
config "github.com/ipfs/kubo/config"
|
||||
coreapi "github.com/ipfs/kubo/core/coreapi"
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// The HTTP server uses seek to determine the file size. Actually _seeking_ can
|
||||
// be slow so we wrap the seeker in a _lazy_ seeker.
|
||||
type lazySeeker struct {
|
||||
reader io.ReadSeeker
|
||||
|
||||
size int64
|
||||
offset int64
|
||||
realOffset int64
|
||||
}
|
||||
|
||||
func (s *lazySeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekEnd:
|
||||
return s.Seek(s.size+offset, io.SeekStart)
|
||||
case io.SeekCurrent:
|
||||
return s.Seek(s.offset+offset, io.SeekStart)
|
||||
case io.SeekStart:
|
||||
if offset < 0 {
|
||||
return s.offset, fmt.Errorf("invalid seek offset")
|
||||
}
|
||||
s.offset = offset
|
||||
return s.offset, nil
|
||||
default:
|
||||
return s.offset, fmt.Errorf("invalid whence: %d", whence)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *lazySeeker) Read(b []byte) (int, error) {
|
||||
// If we're past the end, EOF.
|
||||
if s.offset >= s.size {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// actually seek
|
||||
for s.offset != s.realOffset {
|
||||
off, err := s.reader.Seek(s.offset, io.SeekStart)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
s.realOffset = off
|
||||
}
|
||||
off, err := s.reader.Read(b)
|
||||
s.realOffset += int64(off)
|
||||
s.offset += int64(off)
|
||||
return off, err
|
||||
}
|
||||
|
||||
func (s *lazySeeker) Close() error {
|
||||
if closer, ok := s.reader.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -1,136 +0,0 @@
|
||||
package corehttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type badSeeker struct {
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
var errBadSeek = fmt.Errorf("bad seeker")
|
||||
|
||||
func (bs badSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
off, err := bs.ReadSeeker.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return off, errBadSeek
|
||||
}
|
||||
|
||||
func TestLazySeekerError(t *testing.T) {
|
||||
underlyingBuffer := strings.NewReader("fubar")
|
||||
s := &lazySeeker{
|
||||
reader: badSeeker{underlyingBuffer},
|
||||
size: underlyingBuffer.Size(),
|
||||
}
|
||||
off, err := s.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if off != s.size {
|
||||
t.Fatal("expected to seek to the end")
|
||||
}
|
||||
|
||||
// shouldn't have actually seeked.
|
||||
b, err := io.ReadAll(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(b) != 0 {
|
||||
t.Fatal("expected to read nothing")
|
||||
}
|
||||
|
||||
// shouldn't need to actually seek.
|
||||
off, err = s.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if off != 0 {
|
||||
t.Fatal("expected to seek to the start")
|
||||
}
|
||||
b, err = io.ReadAll(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != "fubar" {
|
||||
t.Fatal("expected to read string")
|
||||
}
|
||||
|
||||
// should fail the second time.
|
||||
off, err = s.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if off != 0 {
|
||||
t.Fatal("expected to seek to the start")
|
||||
}
|
||||
// right here...
|
||||
b, err = io.ReadAll(s)
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error, got output %s", string(b))
|
||||
}
|
||||
if err != errBadSeek {
|
||||
t.Fatalf("expected a bad seek error, got %s", err)
|
||||
}
|
||||
if len(b) != 0 {
|
||||
t.Fatalf("expected to read nothing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLazySeeker(t *testing.T) {
|
||||
underlyingBuffer := strings.NewReader("fubar")
|
||||
s := &lazySeeker{
|
||||
reader: underlyingBuffer,
|
||||
size: underlyingBuffer.Size(),
|
||||
}
|
||||
expectByte := func(b byte) {
|
||||
t.Helper()
|
||||
var buf [1]byte
|
||||
n, err := io.ReadFull(s, buf[:])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("expected to read one byte, read %d", n)
|
||||
}
|
||||
if buf[0] != b {
|
||||
t.Fatalf("expected %b, got %b", b, buf[0])
|
||||
}
|
||||
}
|
||||
expectSeek := func(whence int, off, expOff int64, expErr string) {
|
||||
t.Helper()
|
||||
n, err := s.Seek(off, whence)
|
||||
if expErr == "" {
|
||||
if err != nil {
|
||||
t.Fatal("unexpected seek error: ", err)
|
||||
}
|
||||
} else {
|
||||
if err == nil || err.Error() != expErr {
|
||||
t.Fatalf("expected %s, got %s", err, expErr)
|
||||
}
|
||||
}
|
||||
if n != expOff {
|
||||
t.Fatalf("expected offset %d, got, %d", expOff, n)
|
||||
}
|
||||
}
|
||||
|
||||
expectSeek(io.SeekEnd, 0, s.size, "")
|
||||
b, err := io.ReadAll(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(b) != 0 {
|
||||
t.Fatal("expected to read nothing")
|
||||
}
|
||||
expectSeek(io.SeekEnd, -1, s.size-1, "")
|
||||
expectByte('r')
|
||||
expectSeek(io.SeekStart, 0, 0, "")
|
||||
expectByte('f')
|
||||
expectSeek(io.SeekCurrent, 1, 2, "")
|
||||
expectByte('b')
|
||||
expectSeek(io.SeekCurrent, -100, 3, "invalid seek offset")
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
package corehttp
|
||||
|
||||
// TODO: move to IPNS
|
||||
const WebUIPath = "/ipfs/bafybeibjbq3tmmy7wuihhhwvbladjsd3gx3kfjepxzkq6wylik6wc3whzy" // v2.20.0
|
||||
const WebUIPath = "/ipfs/bafybeifeqt7mvxaniphyu2i3qhovjaf3sayooxbh5enfdqtiehxjv2ldte" // v2.22.0
|
||||
|
||||
// WebUIPaths is a list of all past webUI paths.
|
||||
var WebUIPaths = []string{
|
||||
WebUIPath,
|
||||
"/ipfs/bafybeiequgo72mrvuml56j4gk7crewig5bavumrrzhkqbim6b3s2yqi7ty",
|
||||
"/ipfs/bafybeibjbq3tmmy7wuihhhwvbladjsd3gx3kfjepxzkq6wylik6wc3whzy",
|
||||
"/ipfs/bafybeiavrvt53fks6u32n5p2morgblcmck4bh4ymf4rrwu7ah5zsykmqqa",
|
||||
"/ipfs/bafybeiageaoxg6d7npaof6eyzqbwvbubyler7bq44hayik2hvqcggg7d2y",
|
||||
"/ipfs/bafybeidb5eryh72zajiokdggzo7yct2d6hhcflncji5im2y5w26uuygdsm",
|
||||
|
||||
@ -11,10 +11,10 @@ import (
|
||||
"github.com/ipfs/go-cid"
|
||||
bstore "github.com/ipfs/go-ipfs-blockstore"
|
||||
chunker "github.com/ipfs/go-ipfs-chunker"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
pin "github.com/ipfs/go-ipfs-pinner"
|
||||
posinfo "github.com/ipfs/go-ipfs-posinfo"
|
||||
ipld "github.com/ipfs/go-ipld-format"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
logging "github.com/ipfs/go-log"
|
||||
dag "github.com/ipfs/go-merkledag"
|
||||
"github.com/ipfs/go-mfs"
|
||||
|
||||
@ -14,14 +14,14 @@ import (
|
||||
"github.com/ipfs/kubo/gc"
|
||||
"github.com/ipfs/kubo/repo"
|
||||
|
||||
blocks "github.com/ipfs/go-block-format"
|
||||
"github.com/ipfs/go-blockservice"
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipfs/go-datastore"
|
||||
syncds "github.com/ipfs/go-datastore/sync"
|
||||
blockstore "github.com/ipfs/go-ipfs-blockstore"
|
||||
files "github.com/ipfs/go-ipfs-files"
|
||||
pi "github.com/ipfs/go-ipfs-posinfo"
|
||||
blocks "github.com/ipfs/go-libipfs/blocks"
|
||||
"github.com/ipfs/go-libipfs/files"
|
||||
dag "github.com/ipfs/go-merkledag"
|
||||
coreiface "github.com/ipfs/interface-go-ipfs-core"
|
||||
config "github.com/ipfs/kubo/config"
|
||||
|
||||
@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-bitswap"
|
||||
"github.com/ipfs/go-bitswap/network"
|
||||
blockstore "github.com/ipfs/go-ipfs-blockstore"
|
||||
exchange "github.com/ipfs/go-ipfs-exchange-interface"
|
||||
"github.com/ipfs/go-libipfs/bitswap"
|
||||
"github.com/ipfs/go-libipfs/bitswap/network"
|
||||
"github.com/ipfs/kubo/config"
|
||||
irouting "github.com/ipfs/kubo/routing"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/ipfs/go-log"
|
||||
"github.com/ipfs/kubo/config"
|
||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||
"github.com/libp2p/go-libp2p-pubsub/timecache"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
|
||||
"github.com/ipfs/kubo/core/node/libp2p"
|
||||
@ -66,6 +67,18 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config) fx.Option {
|
||||
pubsub.WithSeenMessagesTTL(cfg.Pubsub.SeenMessagesTTL.WithDefault(pubsub.TimeCacheDuration)),
|
||||
)
|
||||
|
||||
var seenMessagesStrategy timecache.Strategy
|
||||
configSeenMessagesStrategy := cfg.Pubsub.SeenMessagesStrategy.WithDefault(config.DefaultSeenMessagesStrategy)
|
||||
switch configSeenMessagesStrategy {
|
||||
case config.LastSeenMessagesStrategy:
|
||||
seenMessagesStrategy = timecache.Strategy_LastSeen
|
||||
case config.FirstSeenMessagesStrategy:
|
||||
seenMessagesStrategy = timecache.Strategy_FirstSeen
|
||||
default:
|
||||
return fx.Error(fmt.Errorf("unsupported Pubsub.SeenMessagesStrategy %q", configSeenMessagesStrategy))
|
||||
}
|
||||
pubsubOptions = append(pubsubOptions, pubsub.WithSeenMessagesStrategy(seenMessagesStrategy))
|
||||
|
||||
switch cfg.Pubsub.Router {
|
||||
case "":
|
||||
fallthrough
|
||||
@ -291,7 +304,12 @@ func Online(bcfg *BuildCfg, cfg *config.Config) fx.Option {
|
||||
fx.Provide(p2p.New),
|
||||
|
||||
LibP2P(bcfg, cfg),
|
||||
OnlineProviders(cfg.Experimental.StrategicProviding, cfg.Experimental.AcceleratedDHTClient, cfg.Reprovider.Strategy, cfg.Reprovider.Interval),
|
||||
OnlineProviders(
|
||||
cfg.Experimental.StrategicProviding,
|
||||
cfg.Experimental.AcceleratedDHTClient,
|
||||
cfg.Reprovider.Strategy.WithDefault(config.DefaultReproviderStrategy),
|
||||
cfg.Reprovider.Interval.WithDefault(config.DefaultReproviderInterval),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -304,7 +322,12 @@ func Offline(cfg *config.Config) fx.Option {
|
||||
fx.Provide(libp2p.Routing),
|
||||
fx.Provide(libp2p.ContentRouting),
|
||||
fx.Provide(libp2p.OfflineRouting),
|
||||
OfflineProviders(cfg.Experimental.StrategicProviding, cfg.Experimental.AcceleratedDHTClient, cfg.Reprovider.Strategy, cfg.Reprovider.Interval),
|
||||
OfflineProviders(
|
||||
cfg.Experimental.StrategicProviding,
|
||||
cfg.Experimental.AcceleratedDHTClient,
|
||||
cfg.Reprovider.Strategy.WithDefault(config.DefaultReproviderStrategy),
|
||||
cfg.Reprovider.Interval.WithDefault(config.DefaultReproviderInterval),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ import (
|
||||
"go.opencensus.io/stats/view"
|
||||
"go.uber.org/fx"
|
||||
|
||||
config "github.com/ipfs/kubo/config"
|
||||
"github.com/ipfs/kubo/config"
|
||||
"github.com/ipfs/kubo/core/node/helpers"
|
||||
"github.com/ipfs/kubo/repo"
|
||||
)
|
||||
@ -52,7 +52,8 @@ func ResourceManager(cfg config.SwarmConfig) interface{} {
|
||||
return nil, opts, fmt.Errorf("opening IPFS_PATH: %w", err)
|
||||
}
|
||||
|
||||
limitConfig, err := createDefaultLimitConfig(cfg)
|
||||
var limitConfig rcmgr.LimitConfig
|
||||
defaultComputedLimitConfig, err := createDefaultLimitConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, opts, err
|
||||
}
|
||||
@ -61,10 +62,19 @@ func ResourceManager(cfg config.SwarmConfig) interface{} {
|
||||
// is documented in docs/config.md.
|
||||
// Any changes here should be reflected there.
|
||||
if cfg.ResourceMgr.Limits != nil {
|
||||
l := *cfg.ResourceMgr.Limits
|
||||
// This effectively overrides the computed default LimitConfig with any vlues from cfg.ResourceMgr.Limits
|
||||
l.Apply(limitConfig)
|
||||
limitConfig = l
|
||||
userSuppliedOverrideLimitConfig := *cfg.ResourceMgr.Limits
|
||||
// This effectively overrides the computed default LimitConfig with any non-zero values from cfg.ResourceMgr.Limits.
|
||||
// Because of how how Apply works, any 0 value for a user supplied override
|
||||
// will be overriden with a computed default value.
|
||||
// There currently isn't a way for a user to supply a 0-value override.
|
||||
userSuppliedOverrideLimitConfig.Apply(defaultComputedLimitConfig)
|
||||
limitConfig = userSuppliedOverrideLimitConfig
|
||||
} else {
|
||||
limitConfig = defaultComputedLimitConfig
|
||||
}
|
||||
|
||||
if err := ensureConnMgrMakeSenseVsResourceMgr(limitConfig, cfg.ConnMgr); err != nil {
|
||||
return nil, opts, err
|
||||
}
|
||||
|
||||
limiter := rcmgr.NewFixedLimiter(limitConfig)
|
||||
@ -113,7 +123,7 @@ func ResourceManager(cfg config.SwarmConfig) interface{} {
|
||||
manager = lrm
|
||||
} else {
|
||||
fmt.Println("go-libp2p resource manager protection disabled")
|
||||
manager = network.NullResourceManager
|
||||
manager = &network.NullResourceManager{}
|
||||
}
|
||||
|
||||
opts.Opts = append(opts.Opts, libp2p.ResourceManager(manager))
|
||||
@ -598,3 +608,41 @@ func NetResetLimit(mgr network.ResourceManager, repo repo.Repo, scope string) (r
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ensureConnMgrMakeSenseVsResourceMgr(rcm rcmgr.LimitConfig, cmgr config.ConnMgr) error {
|
||||
if cmgr.Type.WithDefault(config.DefaultConnMgrType) == "none" {
|
||||
return nil // none connmgr, no checks to do
|
||||
}
|
||||
highWater := cmgr.HighWater.WithDefault(config.DefaultConnMgrHighWater)
|
||||
if rcm.System.ConnsInbound <= rcm.System.Conns {
|
||||
if int64(rcm.System.ConnsInbound) <= highWater {
|
||||
// nolint
|
||||
return fmt.Errorf(`
|
||||
Unable to initialize libp2p due to conflicting limit configuration:
|
||||
ResourceMgr.Limits.System.ConnsInbound (%d) must be bigger than ConnMgr.HighWater (%d)
|
||||
`, rcm.System.ConnsInbound, highWater)
|
||||
}
|
||||
} else if int64(rcm.System.Conns) <= highWater {
|
||||
// nolint
|
||||
return fmt.Errorf(`
|
||||
Unable to initialize libp2p due to conflicting limit configuration:
|
||||
ResourceMgr.Limits.System.Conns (%d) must be bigger than ConnMgr.HighWater (%d)
|
||||
`, rcm.System.Conns, highWater)
|
||||
}
|
||||
if rcm.System.StreamsInbound <= rcm.System.Streams {
|
||||
if int64(rcm.System.StreamsInbound) <= highWater {
|
||||
// nolint
|
||||
return fmt.Errorf(`
|
||||
Unable to initialize libp2p due to conflicting limit configuration:
|
||||
ResourceMgr.Limits.System.StreamsInbound (%d) must be bigger than ConnMgr.HighWater (%d)
|
||||
`, rcm.System.StreamsInbound, highWater)
|
||||
}
|
||||
} else if int64(rcm.System.Streams) <= highWater {
|
||||
// nolint
|
||||
return fmt.Errorf(`
|
||||
Unable to initialize libp2p due to conflicting limit configuration:
|
||||
ResourceMgr.Limits.System.Streams (%d) must be bigger than ConnMgr.HighWater (%d)
|
||||
`, rcm.System.Streams, highWater)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -44,17 +44,18 @@ var noLimitIncrease = rcmgr.BaseLimitIncrease{
|
||||
// This file defines implicit limit defaults used when Swarm.ResourceMgr.Enabled
|
||||
|
||||
// createDefaultLimitConfig creates LimitConfig to pass to libp2p's resource manager.
|
||||
// The defaults follow the documentation in docs/config.md.
|
||||
// The defaults follow the documentation in docs/libp2p-resource-management.md.
|
||||
// Any changes in the logic here should be reflected there.
|
||||
func createDefaultLimitConfig(cfg config.SwarmConfig) (rcmgr.LimitConfig, error) {
|
||||
maxMemoryDefaultString := humanize.Bytes(uint64(memory.TotalMemory()) / 4)
|
||||
maxMemoryDefaultString := humanize.Bytes(uint64(memory.TotalMemory()) / 2)
|
||||
maxMemoryString := cfg.ResourceMgr.MaxMemory.WithDefault(maxMemoryDefaultString)
|
||||
maxMemory, err := humanize.ParseBytes(maxMemoryString)
|
||||
if err != nil {
|
||||
return rcmgr.LimitConfig{}, err
|
||||
}
|
||||
|
||||
numFD := cfg.ResourceMgr.MaxFileDescriptors.WithDefault(int64(fd.GetNumFDs()) / 2)
|
||||
maxMemoryMB := maxMemory / (1024 * 1024)
|
||||
maxFD := int(cfg.ResourceMgr.MaxFileDescriptors.WithDefault(int64(fd.GetNumFDs()) / 2))
|
||||
|
||||
// We want to see this message on startup, that's why we are using fmt instead of log.
|
||||
fmt.Printf(`
|
||||
@ -65,65 +66,53 @@ Computing default go-libp2p Resource Manager limits based on:
|
||||
Applying any user-supplied overrides on top.
|
||||
Run 'ipfs swarm limit all' to see the resulting limits.
|
||||
|
||||
`, maxMemoryString, numFD)
|
||||
`, maxMemoryString, maxFD)
|
||||
|
||||
// At least as of 2023-01-25, it's possible to open a connection that
|
||||
// doesn't ask for any memory usage with the libp2p Resource Manager/Accountant
|
||||
// (see https://github.com/libp2p/go-libp2p/issues/2010#issuecomment-1404280736).
|
||||
// As a result, we can't curretly rely on Memory limits to full protect us.
|
||||
// Until https://github.com/libp2p/go-libp2p/issues/2010 is addressed,
|
||||
// we take a proxy now of restricting to 1 inbound connection per MB.
|
||||
// Note: this is more generous than go-libp2p's default autoscaled limits which do
|
||||
// 64 connections per 1GB
|
||||
// (see https://github.com/libp2p/go-libp2p/blob/master/p2p/host/resource-manager/limit_defaults.go#L357 ).
|
||||
systemConnsInbound := int(1 * maxMemoryMB)
|
||||
|
||||
scalingLimitConfig := rcmgr.ScalingLimitConfig{
|
||||
SystemBaseLimit: rcmgr.BaseLimit{
|
||||
Memory: int64(maxMemory),
|
||||
FD: int(numFD),
|
||||
FD: maxFD,
|
||||
|
||||
// By default, we just limit connections on the inbound side.
|
||||
Conns: bigEnough,
|
||||
ConnsInbound: rcmgr.DefaultLimits.SystemBaseLimit.ConnsInbound, // same as libp2p default
|
||||
ConnsInbound: systemConnsInbound,
|
||||
ConnsOutbound: bigEnough,
|
||||
|
||||
// We limit streams since they not only take up memory and CPU.
|
||||
// The Memory limit protects us on the memory side,
|
||||
// but a StreamsInbound limit helps protect against unbound CPU consumption from stream processing.
|
||||
Streams: bigEnough,
|
||||
StreamsInbound: rcmgr.DefaultLimits.SystemBaseLimit.StreamsInbound,
|
||||
StreamsInbound: bigEnough,
|
||||
StreamsOutbound: bigEnough,
|
||||
},
|
||||
// Most limits don't see an increase because they're already infinite/bigEnough or at their max value.
|
||||
// The values that should scale based on the amount of memory allocated to libp2p need to increase accordingly.
|
||||
SystemLimitIncrease: rcmgr.BaseLimitIncrease{
|
||||
Memory: 0,
|
||||
FDFraction: 0,
|
||||
|
||||
Conns: 0,
|
||||
ConnsInbound: rcmgr.DefaultLimits.SystemLimitIncrease.ConnsInbound,
|
||||
ConnsOutbound: 0,
|
||||
|
||||
Streams: 0,
|
||||
StreamsInbound: rcmgr.DefaultLimits.SystemLimitIncrease.StreamsInbound,
|
||||
StreamsOutbound: 0,
|
||||
},
|
||||
SystemLimitIncrease: noLimitIncrease,
|
||||
|
||||
// Transient connections won't cause any memory to accounted for by the resource manager.
|
||||
// Only established connections do.
|
||||
// As a result, we can't rely on System.Memory to protect us from a bunch of transient connection being opened.
|
||||
// We limit the same values as the System scope, but only allow the Transient scope to take 25% of what is allowed for the System scope.
|
||||
TransientBaseLimit: rcmgr.BaseLimit{
|
||||
Memory: rcmgr.DefaultLimits.TransientBaseLimit.Memory,
|
||||
FD: rcmgr.DefaultLimits.TransientBaseLimit.FD,
|
||||
Memory: int64(maxMemory / 4),
|
||||
FD: maxFD / 4,
|
||||
|
||||
Conns: bigEnough,
|
||||
ConnsInbound: rcmgr.DefaultLimits.TransientBaseLimit.ConnsInbound,
|
||||
ConnsInbound: systemConnsInbound / 4,
|
||||
ConnsOutbound: bigEnough,
|
||||
|
||||
Streams: bigEnough,
|
||||
StreamsInbound: rcmgr.DefaultLimits.TransientBaseLimit.StreamsInbound,
|
||||
StreamsInbound: bigEnough,
|
||||
StreamsOutbound: bigEnough,
|
||||
},
|
||||
|
||||
TransientLimitIncrease: rcmgr.BaseLimitIncrease{
|
||||
Memory: rcmgr.DefaultLimits.TransientLimitIncrease.Memory,
|
||||
FDFraction: rcmgr.DefaultLimits.TransientLimitIncrease.FDFraction,
|
||||
|
||||
Conns: 0,
|
||||
ConnsInbound: rcmgr.DefaultLimits.TransientLimitIncrease.ConnsInbound,
|
||||
ConnsOutbound: 0,
|
||||
|
||||
Streams: 0,
|
||||
StreamsInbound: rcmgr.DefaultLimits.TransientLimitIncrease.StreamsInbound,
|
||||
StreamsOutbound: 0,
|
||||
},
|
||||
TransientLimitIncrease: noLimitIncrease,
|
||||
|
||||
// Lets get out of the way of the allow list functionality.
|
||||
// If someone specified "Swarm.ResourceMgr.Allowlist" we should let it go through.
|
||||
@ -184,7 +173,26 @@ Run 'ipfs swarm limit all' to see the resulting limits.
|
||||
// Whatever limits libp2p has specifically tuned for its protocols/services we'll apply.
|
||||
libp2p.SetDefaultServiceLimits(&scalingLimitConfig)
|
||||
|
||||
defaultLimitConfig := scalingLimitConfig.Scale(int64(maxMemory), int(numFD))
|
||||
defaultLimitConfig := scalingLimitConfig.Scale(int64(maxMemory), maxFD)
|
||||
|
||||
// Simple checks to overide autoscaling ensuring limits make sense versus the connmgr values.
|
||||
// There are ways to break this, but this should catch most problems already.
|
||||
// We might improve this in the future.
|
||||
// See: https://github.com/ipfs/kubo/issues/9545
|
||||
if cfg.ConnMgr.Type.WithDefault(config.DefaultConnMgrType) != "none" {
|
||||
maxInboundConns := int64(defaultLimitConfig.System.ConnsInbound)
|
||||
if connmgrHighWaterTimesTwo := cfg.ConnMgr.HighWater.WithDefault(config.DefaultConnMgrHighWater) * 2; maxInboundConns < connmgrHighWaterTimesTwo {
|
||||
maxInboundConns = connmgrHighWaterTimesTwo
|
||||
}
|
||||
|
||||
if maxInboundConns < config.DefaultResourceMgrMinInboundConns {
|
||||
maxInboundConns = config.DefaultResourceMgrMinInboundConns
|
||||
}
|
||||
|
||||
// Scale System.StreamsInbound as well, but use the existing ratio of StreamsInbound to ConnsInbound
|
||||
defaultLimitConfig.System.StreamsInbound = int(maxInboundConns * int64(defaultLimitConfig.System.StreamsInbound) / int64(defaultLimitConfig.System.ConnsInbound))
|
||||
defaultLimitConfig.System.ConnsInbound = int(maxInboundConns)
|
||||
}
|
||||
|
||||
return defaultLimitConfig, nil
|
||||
}
|
||||
|
||||
@ -50,11 +50,11 @@ func (n *loggingResourceManager) start(ctx context.Context) {
|
||||
n.limitExceededErrs = make(map[string]int)
|
||||
|
||||
for e, count := range errs {
|
||||
n.logger.Warnf("Protected from exceeding resource limits %d times: %q.", count, e)
|
||||
n.logger.Warnf("Protected from exceeding resource limits %d times. libp2p message: %q.", count, e)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
n.logger.Warnf("Consider inspecting logs and raising the resource manager limits. Documentation: https://github.com/ipfs/kubo/blob/master/docs/config.md#swarmresourcemgr")
|
||||
n.logger.Warnf("Learn more about potential actions to take at: https://github.com/ipfs/kubo/blob/master/docs/libp2p-resource-management.md")
|
||||
}
|
||||
|
||||
n.mut.Unlock()
|
||||
|
||||
@ -55,7 +55,7 @@ func TestLoggingResourceManager(t *testing.T) {
|
||||
if oLogs.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
require.Equal(t, "Protected from exceeding resource limits 2 times: \"system: cannot reserve inbound connection: resource limit exceeded\".", oLogs.All()[0].Message)
|
||||
require.Equal(t, "Protected from exceeding resource limits 2 times. libp2p message: \"system: cannot reserve inbound connection: resource limit exceeded\".", oLogs.All()[0].Message)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,13 +7,9 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/kubo/core/node/helpers"
|
||||
irouting "github.com/ipfs/kubo/routing"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
ds "github.com/ipfs/go-datastore"
|
||||
offroute "github.com/ipfs/go-ipfs-routing/offline"
|
||||
config "github.com/ipfs/kubo/config"
|
||||
"github.com/ipfs/kubo/repo"
|
||||
dht "github.com/libp2p/go-libp2p-kad-dht"
|
||||
ddht "github.com/libp2p/go-libp2p-kad-dht/dual"
|
||||
"github.com/libp2p/go-libp2p-kad-dht/fullrt"
|
||||
@ -24,9 +20,12 @@ import (
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/libp2p/go-libp2p/core/routing"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"go.uber.org/fx"
|
||||
|
||||
config "github.com/ipfs/kubo/config"
|
||||
"github.com/ipfs/kubo/core/node/helpers"
|
||||
"github.com/ipfs/kubo/repo"
|
||||
irouting "github.com/ipfs/kubo/routing"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
@ -77,6 +76,21 @@ func BaseRouting(experimentalDHTClient bool) interface{} {
|
||||
})
|
||||
}
|
||||
|
||||
if pr, ok := in.Router.(routinghelpers.ComposableRouter); ok {
|
||||
for _, r := range pr.Routers() {
|
||||
if dht, ok := r.(*ddht.DHT); ok {
|
||||
dr = dht
|
||||
lc.Append(fx.Hook{
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return dr.Close()
|
||||
},
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dr != nil && experimentalDHTClient {
|
||||
cfg, err := in.Repo.Config()
|
||||
if err != nil {
|
||||
|
||||
@ -2,6 +2,9 @@ package libp2p
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-datastore"
|
||||
"github.com/ipfs/kubo/config"
|
||||
@ -23,6 +26,79 @@ type RoutingOption func(
|
||||
...peer.AddrInfo,
|
||||
) (routing.Routing, error)
|
||||
|
||||
// Default HTTP routers used in parallel to DHT when Routing.Type = "auto"
|
||||
var defaultHTTPRouters = []string{
|
||||
"https://cid.contact", // https://github.com/ipfs/kubo/issues/9422#issuecomment-1338142084
|
||||
// TODO: add an independent router from Cloudflare
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Override HTTP routers if custom ones were passed via env
|
||||
if routers := os.Getenv("IPFS_HTTP_ROUTERS"); routers != "" {
|
||||
defaultHTTPRouters = strings.Split(routers, " ")
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructDefaultRouting returns routers used when Routing.Type is unset or set to "auto"
|
||||
func ConstructDefaultRouting(peerID string, addrs []string, privKey string) func(
|
||||
ctx context.Context,
|
||||
host host.Host,
|
||||
dstore datastore.Batching,
|
||||
validator record.Validator,
|
||||
bootstrapPeers ...peer.AddrInfo,
|
||||
) (routing.Routing, error) {
|
||||
return func(
|
||||
ctx context.Context,
|
||||
host host.Host,
|
||||
dstore datastore.Batching,
|
||||
validator record.Validator,
|
||||
bootstrapPeers ...peer.AddrInfo,
|
||||
) (routing.Routing, error) {
|
||||
// Defined routers will be queried in parallel (optimizing for response speed)
|
||||
// Different trade-offs can be made by setting Routing.Type = "custom" with own Routing.Routers
|
||||
var routers []*routinghelpers.ParallelRouter
|
||||
|
||||
// Run the default DHT routing (same as Routing.Type = "dht")
|
||||
dhtRouting, err := DHTOption(ctx, host, dstore, validator, bootstrapPeers...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routers = append(routers, &routinghelpers.ParallelRouter{
|
||||
Router: dhtRouting,
|
||||
IgnoreError: false,
|
||||
Timeout: 5 * time.Minute, // https://github.com/ipfs/kubo/pull/9475#discussion_r1042501333
|
||||
ExecuteAfter: 0,
|
||||
})
|
||||
|
||||
// Append HTTP routers for additional speed
|
||||
for _, endpoint := range defaultHTTPRouters {
|
||||
httpRouter, err := irouting.ConstructHTTPRouter(endpoint, peerID, addrs, privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &irouting.Composer{
|
||||
GetValueRouter: routinghelpers.Null{},
|
||||
PutValueRouter: routinghelpers.Null{},
|
||||
ProvideRouter: routinghelpers.Null{}, // modify this when indexers supports provide
|
||||
FindPeersRouter: routinghelpers.Null{},
|
||||
FindProvidersRouter: httpRouter,
|
||||
}
|
||||
|
||||
routers = append(routers, &routinghelpers.ParallelRouter{
|
||||
Router: r,
|
||||
IgnoreError: true, // https://github.com/ipfs/kubo/pull/9475#discussion_r1042507387
|
||||
Timeout: 15 * time.Second, // 5x server value from https://github.com/ipfs/kubo/pull/9475#discussion_r1042428529
|
||||
ExecuteAfter: 0,
|
||||
})
|
||||
}
|
||||
|
||||
routing := routinghelpers.NewComposableParallel(routers)
|
||||
return routing, nil
|
||||
}
|
||||
}
|
||||
|
||||
// constructDHTRouting is used when Routing.Type = "dht"
|
||||
func constructDHTRouting(mode dht.ModeOpt) func(
|
||||
ctx context.Context,
|
||||
host host.Host,
|
||||
@ -49,6 +125,7 @@ func constructDHTRouting(mode dht.ModeOpt) func(
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructDelegatedRouting is used when Routing.Type = "custom"
|
||||
func ConstructDelegatedRouting(routers config.Routers, methods config.Methods, peerID string, addrs []string, privKey string) func(
|
||||
ctx context.Context,
|
||||
host host.Host,
|
||||
|
||||
@ -19,7 +19,6 @@ func yamuxTransport() network.Multiplexer {
|
||||
if os.Getenv("YAMUX_DEBUG") != "" {
|
||||
tpt.LogOutput = os.Stderr
|
||||
}
|
||||
|
||||
return &tpt
|
||||
}
|
||||
|
||||
@ -27,9 +26,6 @@ func makeSmuxTransportOption(tptConfig config.Transports) (libp2p.Option, error)
|
||||
const yamuxID = "/yamux/1.0.0"
|
||||
const mplexID = "/mplex/6.7.0"
|
||||
|
||||
ymxtpt := *yamux.DefaultTransport
|
||||
ymxtpt.AcceptBacklog = 512
|
||||
|
||||
if prefs := os.Getenv("LIBP2P_MUX_PREFS"); prefs != "" {
|
||||
// Using legacy LIBP2P_MUX_PREFS variable.
|
||||
log.Error("LIBP2P_MUX_PREFS is now deprecated.")
|
||||
@ -47,7 +43,7 @@ func makeSmuxTransportOption(tptConfig config.Transports) (libp2p.Option, error)
|
||||
}
|
||||
switch tpt {
|
||||
case yamuxID:
|
||||
opts = append(opts, libp2p.Muxer(tpt, yamuxTransport))
|
||||
opts = append(opts, libp2p.Muxer(tpt, yamuxTransport()))
|
||||
case mplexID:
|
||||
opts = append(opts, libp2p.Muxer(tpt, mplex.DefaultTransport))
|
||||
default:
|
||||
@ -59,7 +55,7 @@ func makeSmuxTransportOption(tptConfig config.Transports) (libp2p.Option, error)
|
||||
return prioritizeOptions([]priorityOption{{
|
||||
priority: tptConfig.Multiplexers.Yamux,
|
||||
defaultPriority: 100,
|
||||
opt: libp2p.Muxer(yamuxID, yamuxTransport),
|
||||
opt: libp2p.Muxer(yamuxID, yamuxTransport()),
|
||||
}, {
|
||||
priority: tptConfig.Multiplexers.Mplex,
|
||||
defaultPriority: 200,
|
||||
|
||||
@ -36,12 +36,10 @@ func Transports(tptConfig config.Transports) interface{} {
|
||||
"QUIC transport does not support private networks, please disable Swarm.Transports.Network.QUIC",
|
||||
)
|
||||
}
|
||||
// TODO(9290): Make WithMetrics configurable
|
||||
opts.Opts = append(opts.Opts, libp2p.Transport(quic.NewTransport, quic.WithMetrics()))
|
||||
opts.Opts = append(opts.Opts, libp2p.Transport(quic.NewTransport))
|
||||
}
|
||||
|
||||
// TODO(9292): Remove the false && to allows it enabled by default
|
||||
if tptConfig.Network.WebTransport.WithDefault(false && !privateNetworkEnabled) {
|
||||
if tptConfig.Network.WebTransport.WithDefault(!privateNetworkEnabled) {
|
||||
if privateNetworkEnabled {
|
||||
return opts, fmt.Errorf(
|
||||
"WebTransport transport does not support private networks, please disable Swarm.Transports.Network.WebTransport",
|
||||
|
||||
@ -18,8 +18,6 @@ import (
|
||||
irouting "github.com/ipfs/kubo/routing"
|
||||
)
|
||||
|
||||
const kReprovideFrequency = time.Hour * 12
|
||||
|
||||
// SIMPLE
|
||||
|
||||
// ProviderQueue creates new datastore backed provider queue
|
||||
@ -61,20 +59,10 @@ func SimpleProviderSys(isOnline bool) interface{} {
|
||||
}
|
||||
|
||||
// BatchedProviderSys creates new provider system
|
||||
func BatchedProviderSys(isOnline bool, reprovideInterval string) interface{} {
|
||||
func BatchedProviderSys(isOnline bool, reprovideInterval time.Duration) interface{} {
|
||||
return func(lc fx.Lifecycle, cr irouting.ProvideManyRouter, q *q.Queue, keyProvider simple.KeyChanFunc, repo repo.Repo) (provider.System, error) {
|
||||
reprovideIntervalDuration := kReprovideFrequency
|
||||
if reprovideInterval != "" {
|
||||
dur, err := time.ParseDuration(reprovideInterval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reprovideIntervalDuration = dur
|
||||
}
|
||||
|
||||
sys, err := batched.New(cr, q,
|
||||
batched.ReproviderInterval(reprovideIntervalDuration),
|
||||
batched.ReproviderInterval(reprovideInterval),
|
||||
batched.Datastore(repo.Datastore()),
|
||||
batched.KeyProvider(keyProvider))
|
||||
if err != nil {
|
||||
@ -100,7 +88,7 @@ func BatchedProviderSys(isOnline bool, reprovideInterval string) interface{} {
|
||||
// ONLINE/OFFLINE
|
||||
|
||||
// OnlineProviders groups units managing provider routing records online
|
||||
func OnlineProviders(useStrategicProviding bool, useBatchedProviding bool, reprovideStrategy string, reprovideInterval string) fx.Option {
|
||||
func OnlineProviders(useStrategicProviding bool, useBatchedProviding bool, reprovideStrategy string, reprovideInterval time.Duration) fx.Option {
|
||||
if useStrategicProviding {
|
||||
return fx.Provide(provider.NewOfflineProvider)
|
||||
}
|
||||
@ -113,7 +101,7 @@ func OnlineProviders(useStrategicProviding bool, useBatchedProviding bool, repro
|
||||
}
|
||||
|
||||
// OfflineProviders groups units managing provider routing records offline
|
||||
func OfflineProviders(useStrategicProviding bool, useBatchedProviding bool, reprovideStrategy string, reprovideInterval string) fx.Option {
|
||||
func OfflineProviders(useStrategicProviding bool, useBatchedProviding bool, reprovideStrategy string, reprovideInterval time.Duration) fx.Option {
|
||||
if useStrategicProviding {
|
||||
return fx.Provide(provider.NewOfflineProvider)
|
||||
}
|
||||
@ -126,17 +114,7 @@ func OfflineProviders(useStrategicProviding bool, useBatchedProviding bool, repr
|
||||
}
|
||||
|
||||
// SimpleProviders creates the simple provider/reprovider dependencies
|
||||
func SimpleProviders(reprovideStrategy string, reprovideInterval string) fx.Option {
|
||||
reproviderInterval := kReprovideFrequency
|
||||
if reprovideInterval != "" {
|
||||
dur, err := time.ParseDuration(reprovideInterval)
|
||||
if err != nil {
|
||||
return fx.Error(err)
|
||||
}
|
||||
|
||||
reproviderInterval = dur
|
||||
}
|
||||
|
||||
func SimpleProviders(reprovideStrategy string, reproviderInterval time.Duration) fx.Option {
|
||||
var keyProvider fx.Option
|
||||
switch reprovideStrategy {
|
||||
case "all":
|
||||
|
||||
@ -2,7 +2,7 @@ include mk/header.mk
|
||||
|
||||
GOCC ?= go
|
||||
|
||||
$(d)/coverage_deps: $$(DEPS_GO)
|
||||
$(d)/coverage_deps: $$(DEPS_GO) cmd/ipfs/ipfs
|
||||
rm -rf $(@D)/unitcover && mkdir $(@D)/unitcover
|
||||
rm -rf $(@D)/sharnesscover && mkdir $(@D)/sharnesscover
|
||||
|
||||
@ -46,7 +46,7 @@ endif
|
||||
export IPFS_COVER_DIR:= $(realpath $(d))/sharnesscover/
|
||||
|
||||
$(d)/sharness_tests.coverprofile: export TEST_NO_PLUGIN=1
|
||||
$(d)/sharness_tests.coverprofile: $(d)/ipfs cmd/ipfs/ipfs-test-cover $(d)/coverage_deps test_sharness_short
|
||||
$(d)/sharness_tests.coverprofile: $(d)/ipfs cmd/ipfs/ipfs-test-cover $(d)/coverage_deps test_sharness
|
||||
(cd $(@D)/sharnesscover && find . -type f | gocovmerge -list -) > $@
|
||||
|
||||
|
||||
|
||||
@ -27,7 +27,6 @@ We will ask early testers to participate at two points in the process:
|
||||
- [ ] Textile (@sanderpick)
|
||||
- [ ] Pinata (@obo20)
|
||||
- [ ] RTrade (@postables)
|
||||
- [ ] QRI (@b5)
|
||||
- [ ] Siderus (@koalalorenzo)
|
||||
- [ ] Charity Engine (@rytiss, @tristanolive)
|
||||
- [ ] Fission (@bmann)
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
# Developer Documentation and Guides
|
||||
|
||||
If you are looking for User Documentation & Guides, please visit [docs.ipfs.tech](https://docs.ipfs.tech/).
|
||||
If you are looking for User Documentation & Guides, please visit [docs.ipfs.tech](https://docs.ipfs.tech/) or check [General Documentation](#general-documentation).
|
||||
|
||||
If you’re experiencing an issue with IPFS, **please follow [our issue guide](github-issue-guide.md) when filing an issue!**
|
||||
|
||||
Otherwise, check out the following guides to using and developing IPFS:
|
||||
|
||||
## General Documentation
|
||||
|
||||
- [Configuration reference](config.md)
|
||||
- [Datastore configuration](datastores.md)
|
||||
- [Experimental features](experimental-features.md)
|
||||
|
||||
## Developing `kubo`
|
||||
|
||||
- First, please read the Contributing Guidelines [for IPFS projects](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) and then the Contributing Guidelines for [Go code specifically](https://github.com/ipfs/community/blob/master/CONTRIBUTING_GO.md)
|
||||
@ -22,9 +28,6 @@ Otherwise, check out the following guides to using and developing IPFS:
|
||||
## Advanced User Guides
|
||||
|
||||
- [Transferring a File Over IPFS](file-transfer.md)
|
||||
- [Configuration reference](config.md)
|
||||
- [Datastore configuration](datastores.md)
|
||||
- [Experimental features](experimental-features.md)
|
||||
- [Installing command completion](command-completion.md)
|
||||
- [Mounting IPFS with FUSE](fuse.md)
|
||||
- [Installing plugins](plugins.md)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user