diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d05545a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +version: 2 + +updates: + - package-ecosystem: gomod # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: daily + reviewers: + - "rustatian" + assignees: + - "rustatian" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly + reviewers: + - "rustatian" + assignees: + - "rustatian" \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6fa7b35 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,24 @@ +# Reason for This PR + +`[Author TODO: add issue # or explain reasoning.]` + +## Description of Changes + +`[Author TODO: add description of changes.]` + +## License Acceptance + +By submitting this pull request, I confirm that my contribution is made under +the terms of the MIT license. + +## PR Checklist + +`[Author TODO: Meet these criteria.]` +`[Reviewer TODO: Verify that these criteria are met. Request changes if not]` + +- [ ] All commits in this PR are signed (`git commit -s`). +- [ ] The reason for this PR is clearly provided (issue no. or explanation). +- [ ] The description of changes is clear and encompassing. +- [ ] Any required documentation changes (code and docs) are included in this PR. +- [ ] Any user-facing changes are mentioned in `CHANGELOG.md`. +- [ ] All added/changed functionality is tested. \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..38cc298 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,73 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '0 15 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: [ 'go' ] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # Initializes the Golang environment for the CodeQL tools. + # https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 \ No newline at end of file diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 0000000..c4fbadf --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,23 @@ +name: Linters + +on: [push, pull_request] + +jobs: + golangci-lint: + name: Golang-CI (lint) + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 # action page: + with: + go-version: stable + + - name: Run linter + uses: golangci/golangci-lint-action@v4.0.0 # Action page: + with: + version: v1.56 # without patch version + only-new-issues: false # show only new issues if it's a pull request + args: --timeout=10m --build-tags=race ./... \ No newline at end of file diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..69fb49f --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,97 @@ +name: sendremotefile + +on: + push: + branches: + - master + - stable + pull_request: + branches: + - master + - stable + +jobs: + sendremotefile_test: + name: Sendremotefile plugin (Go ${{ matrix.go }}, PHP ${{ matrix.php }}, OS ${{matrix.os}}) + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + matrix: + php: [ "8.3" ] + go: [ stable ] + os: [ "ubuntu-latest" ] + steps: + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@v5 # action page: + with: + go-version: ${{ matrix.go }} + + - name: Set up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 # action page: + with: + php-version: ${{ matrix.php }} + extensions: sockets + + - name: Check out code + uses: actions/checkout@v4 + + - name: Get Composer Cache Directory + id: composer-cache + run: | + cd tests/php_test_files + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Init Composer Cache # Docs: + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: cd tests/php_test_files && composer update --prefer-dist --no-progress --ansi + + - name: Init Go modules Cache # Docs: + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-go- + + - name: Install Go dependencies + run: go mod download + + - name: Run golang tests with coverage + run: | + cd tests + mkdir ./coverage-ci + + go test -timeout 20m -v -race -cover -tags=debug -failfast -coverpkg=$(cat pkgs.txt) -coverprofile=./coverage-ci/sendremotefile.out -covermode=atomic plugin_test.go + + - name: Archive code coverage results + uses: actions/upload-artifact@v4 + with: + name: coverage + path: ./tests/coverage-ci/sendremotefile.out + + codecov: + name: Upload codecov + runs-on: ubuntu-latest + needs: + - sendremotefile_test + + timeout-minutes: 60 + steps: + - name: Download code coverage results + uses: actions/download-artifact@v4 + - run: | + cd coverage + echo 'mode: atomic' > summary.txt + tail -q -n +2 *.out >> summary.txt + sed -i '2,${/roadrunner/!d}' summary.txt + + - name: upload to codecov + uses: codecov/codecov-action@v4 # Docs: + with: + file: ./coverage/summary.txt + fail_ci_if_error: false \ No newline at end of file diff --git a/.github/workflows/linux_durability.yml b/.github/workflows/linux_durability.yml new file mode 100644 index 0000000..9eac432 --- /dev/null +++ b/.github/workflows/linux_durability.yml @@ -0,0 +1,101 @@ +name: sendremotefile durability + +on: + push: + branches: + - master + - stable + pull_request: + branches: + - master + - stable + +jobs: + sendremotefile_durability_test: + name: Sendremotefile plugin durability (Go ${{ matrix.go }}, PHP ${{ matrix.php }}, OS ${{matrix.os}}) + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + matrix: + php: [ "8.3" ] + go: [ stable ] + os: [ "ubuntu-latest" ] + steps: + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@v5 # action page: + with: + go-version: ${{ matrix.go }} + + - name: Set up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 # action page: + with: + php-version: ${{ matrix.php }} + extensions: sockets + + - name: Check out code + uses: actions/checkout@v4 + + - name: Get Composer Cache Directory + id: composer-cache + run: | + cd tests/php_test_files + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Init Composer Cache # Docs: + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: cd tests/php_test_files && composer update --prefer-dist --no-progress --ansi + + - name: Init Go modules Cache # Docs: + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-go- + + - name: Install Go dependencies + run: go mod download + + - name: Run golang tests with coverage + run: | + cd tests + docker compose -f docker-compose.yml up -d + sleep 30 + + mkdir ./coverage-ci + + go test -timeout 20m -v -race -cover -tags=debug -failfast -coverpkg=$(cat pkgs.txt) -coverprofile=./coverage-ci/sendremotefile.out -covermode=atomic plugin_durability_test.go + docker compose -f docker-compose.yml down + + - name: Archive code coverage results + uses: actions/upload-artifact@v4 + with: + name: coverage + path: ./tests/coverage-ci/sendremotefile.out + + codecov: + name: Upload codecov + runs-on: ubuntu-latest + needs: + - sendremotefile_durability_test + + timeout-minutes: 60 + steps: + - name: Download code coverage results + uses: actions/download-artifact@v4 + - run: | + cd coverage + echo 'mode: atomic' > summary.txt + tail -q -n +2 *.out >> summary.txt + sed -i '2,${/roadrunner/!d}' summary.txt + + - name: upload to codecov + uses: codecov/codecov-action@v4 # Docs: + with: + file: ./coverage/summary.txt + fail_ci_if_error: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3b735ec..f63c105 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ +composer.lock -# Go workspace file -go.work +tests/minio-data \ No newline at end of file diff --git a/bpool.go b/bpool.go new file mode 100644 index 0000000..e3c5985 --- /dev/null +++ b/bpool.go @@ -0,0 +1,83 @@ +package sendremotefile + +import ( + "sync" +) + +const ( + OneMB uint = 1024 * 1024 * 1 + FiveMB uint = 1024 * 1024 * 5 + TenMB uint = 1024 * 1024 * 10 +) + +type bpool struct { + *sync.Map +} + +func NewBytePool() *bpool { + var frameChunkedPool = &sync.Map{} + var preallocate = &sync.Once{} + preallocate.Do(internalAllocate(frameChunkedPool)) + + return &bpool{ + frameChunkedPool, + } +} + +func internalAllocate(frameChunkedPool *sync.Map) func() { + return func() { + pool1 := &sync.Pool{ + New: func() any { + data := make([]byte, OneMB) + return &data + }, + } + pool5 := &sync.Pool{ + New: func() any { + data := make([]byte, FiveMB) + return &data + }, + } + pool10 := &sync.Pool{ + New: func() any { + data := make([]byte, TenMB) + return &data + }, + } + + frameChunkedPool.Store(OneMB, pool1) + frameChunkedPool.Store(FiveMB, pool5) + frameChunkedPool.Store(TenMB, pool10) + } +} + +func (bpool *bpool) get(size uint) *[]byte { + switch { + case size <= OneMB: + val, _ := bpool.Load(OneMB) + return val.(*sync.Pool).Get().(*[]byte) + case size <= FiveMB: + val, _ := bpool.Load(FiveMB) + return val.(*sync.Pool).Get().(*[]byte) + default: + val, _ := bpool.Load(TenMB) + return val.(*sync.Pool).Get().(*[]byte) + } +} + +func (bpool *bpool) put(size uint, data *[]byte) { + switch { + case size <= OneMB: + pool, _ := bpool.Load(OneMB) + pool.(*sync.Pool).Put(data) + return + case size <= FiveMB: + pool, _ := bpool.Load(FiveMB) + pool.(*sync.Pool).Put(data) + return + default: + pool, _ := bpool.Load(TenMB) + pool.(*sync.Pool).Put(data) + return + } +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..5be83b2 --- /dev/null +++ b/client.go @@ -0,0 +1,59 @@ +package sendremotefile + +import ( + "context" + "net" + "net/http" + "time" +) + +type client struct { + inner *http.Client + url string +} + +type connection struct { + net.Conn + timeout time.Duration +} + +func NewClient(url string, timeout time.Duration) *client { + return &client{ + inner: &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := (&net.Dialer{Timeout: timeout}).DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + + return &connection{ + Conn: conn, + timeout: timeout, + }, nil + }, + TLSHandshakeTimeout: timeout, + ResponseHeaderTimeout: timeout, + }, + }, + url: url, + } +} + +func (c *client) Request() (*http.Response, error) { + req, err := http.NewRequest(http.MethodGet, c.url, nil) + if err != nil { + return nil, err + } + + return c.inner.Do(req) +} + +func (c *connection) Read(b []byte) (int, error) { + err := c.Conn.SetReadDeadline(time.Now().Add(c.timeout)) + if err != nil { + return 0, err + } + + return c.Conn.Read(b) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9d5d1ec --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/roadrunner-server/sendremotefile/v4 + +go 1.22.0 + +require ( + github.com/roadrunner-server/errors v1.4.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/testify v1.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b5e1227 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/roadrunner-server/errors v1.4.0 h1:Odjg3VZrj1q5Y8ILwoN+JgERyv0pkhrWPNOM4h68iQ8= +github.com/roadrunner-server/errors v1.4.0/go.mod h1:78PvraAFj+Sxy5nDmo0S+h6rEMLFIDszWZxA3B0sPAs= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work new file mode 100644 index 0000000..6c7b2dd --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.22.1 + +use ( + . + ./tests +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..32c8cda --- /dev/null +++ b/go.work.sum @@ -0,0 +1,80 @@ +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= +github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.temporal.io/api v1.28.0/go.mod h1:sAtVCXkwNaCtHVMP6B/FlK8PcEnaDjJ+KHCwS/ufscI= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..fc07d7e --- /dev/null +++ b/plugin.go @@ -0,0 +1,170 @@ +package sendremotefile + +import ( + "errors" + "io" + "maps" + "net" + "net/http" + "strings" + "time" + + rrErrors "github.com/roadrunner-server/errors" + "go.uber.org/zap" +) + +const ( + rootPluginName string = "http" + pluginName string = "sendremotefile" + responseContentTypeKey string = "Content-Type" + responseContentTypeVal string = "application/octet-stream" + responseStatusCode int = http.StatusOK + xSendRemoteHeader string = "X-Sendremotefile" + defaultBufferSize uint = TenMB + timeout time.Duration = 5 * time.Second +) + +type Configurer interface { + UnmarshalKey(name string, out any) error + Has(name string) bool +} + +type Logger interface { + NamedLogger(name string) *zap.Logger +} + +type Plugin struct { + log *zap.Logger + bytesPool *bpool + writersPool *wpool +} + +func (p *Plugin) Init(cfg Configurer, log Logger) error { + const op = rrErrors.Op("sendremotefile_plugin_init") + + if !cfg.Has(rootPluginName) { + return rrErrors.E(op, rrErrors.Disabled) + } + + p.log = log.NamedLogger(pluginName) + p.bytesPool = NewBytePool() + p.writersPool = NewWriterPool() + + return nil +} + +func (p *Plugin) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rrWriter := p.writersPool.get() + defer func() { + p.writersPool.put(rrWriter) + _ = r.Body.Close() + }() + + next.ServeHTTP(rrWriter, r) + + // if there is no X-Sendremotefile header from the PHP worker, just return + if url := rrWriter.Header().Get(xSendRemoteHeader); url == "" { + // re-add original headers, status code and body + maps.Copy(w.Header(), rrWriter.Header()) + w.WriteHeader(rrWriter.code) + if len(rrWriter.data) > 0 { + // write a body if exists + _, err := w.Write(rrWriter.data) + if err != nil { + p.log.Error("failed to write data to the response", zap.Error(err)) + } + } + + return + } + + // we already checked that that header exists + url := rrWriter.Header().Get(xSendRemoteHeader) + // delete the original X-Sendremotefile header + rrWriter.Header().Del(xSendRemoteHeader) + + if !strings.HasPrefix(url, "http") { + p.log.Error("header value must start with http") + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + resp, err := NewClient(url, timeout).Request() + if err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + http.Error(w, http.StatusText(http.StatusRequestTimeout), http.StatusRequestTimeout) + return + } + + p.log.Error("failed to request from the upstream", zap.Error(err)) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + defer func() { + err = resp.Body.Close() + if err != nil { + p.log.Error("failed to close upstream response body", zap.Error(err)) + } + }() + + if resp.StatusCode != http.StatusOK { + p.log.Error("invalid upstream response status code", zap.Int("rr_response_code", rrWriter.code), zap.Int("remotefile_response_code", resp.StatusCode)) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + var pl = defaultBufferSize + if cl := resp.ContentLength; cl > 0 { + pl = uint(cl) + } + + pb := p.bytesPool.get(pl) + + // re-add original headers + maps.Copy(w.Header(), rrWriter.Header()) + // overwrite content-type header + w.Header().Set(responseContentTypeKey, responseContentTypeVal) + w.WriteHeader(responseStatusCode) + + rc := http.NewResponseController(w) + + for { + nr, er := resp.Body.Read(*pb) + + if nr > 0 { + nw, ew := w.Write((*pb)[:nr]) + + if nw > 0 { + if ef := rc.Flush(); ef != nil { + p.log.Error("failed to flush data to the downstream response", zap.Error(ef)) + break + } + } + + if ew != nil { + p.log.Error("failed to write data to the downstream response", zap.Error(ew)) + break + } + } + + if er == io.EOF { + break + } + + if er != nil { + p.log.Error("failed to read data from the upstream response", zap.Error(er)) + break + } + } + + p.bytesPool.put(pl, pb) + }) +} + +// Middleware/plugin name. +func (p *Plugin) Name() string { + return pluginName +} diff --git a/tests/configs/.rr-with-disabled-sendremotefile.yaml b/tests/configs/.rr-with-disabled-sendremotefile.yaml new file mode 100644 index 0000000..0e6d5be --- /dev/null +++ b/tests/configs/.rr-with-disabled-sendremotefile.yaml @@ -0,0 +1,19 @@ +version: '3' + +server: + command: "php php_test_files/psr-worker.php" + relay: "pipes" + relay_timeout: "20s" + +http: + address: 127.0.0.1:18953 + middleware: [] + pool: + num_workers: 2 + max_jobs: 0 + allocate_timeout: 60s + destroy_timeout: 60s + +logs: + mode: development + level: error diff --git a/tests/configs/.rr-with-sendremotefile.yaml b/tests/configs/.rr-with-sendremotefile.yaml new file mode 100644 index 0000000..e6e073c --- /dev/null +++ b/tests/configs/.rr-with-sendremotefile.yaml @@ -0,0 +1,19 @@ +version: '3' + +server: + command: "php php_test_files/psr-worker.php" + relay: "pipes" + relay_timeout: "20s" + +http: + address: 127.0.0.1:18953 + middleware: [ "sendremotefile" ] + pool: + num_workers: 2 + max_jobs: 0 + allocate_timeout: 60s + destroy_timeout: 60s + +logs: + mode: development + level: error diff --git a/tests/data/1MB.jpg b/tests/data/1MB.jpg new file mode 100644 index 0000000..682b3ac Binary files /dev/null and b/tests/data/1MB.jpg differ diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..eb0aa7f --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3.8' + +services: + minio: + image: quay.io/minio/minio + volumes: + - ./minio-data:/data + ports: + - 9000:9000 + - 9001:9001 + environment: + MINIO_ROOT_USER: 'minio_user' + MINIO_ROOT_PASSWORD: 'minio_password' + MINIO_ADDRESS: ':9000' + MINIO_CONSOLE_ADDRESS: ':9001' + command: minio server /data + + sync-bucket-data: + image: quay.io/minio/mc + volumes: + - ./data:/fs-data + depends_on: + - minio + entrypoint: > + /bin/sh -c " + /usr/bin/mc alias set rr http://minio:9000 minio_user minio_password; + /usr/bin/mc mb rr/bucket; + /usr/bin/mc anonymous set public rr/bucket; + /usr/bin/mc cp --recursive /fs-data rr/bucket; + exit 0; + " + + toxicproxy: + image: shopify/toxiproxy:latest + network_mode: "host" diff --git a/tests/go.mod b/tests/go.mod new file mode 100644 index 0000000..4bad0b0 --- /dev/null +++ b/tests/go.mod @@ -0,0 +1,91 @@ +module tests + +go 1.22.1 + +require ( + github.com/Shopify/toxiproxy v2.1.4+incompatible + github.com/roadrunner-server/config/v4 v4.6.9 + github.com/roadrunner-server/endure/v2 v2.4.3 + github.com/roadrunner-server/http/v4 v4.5.3 + github.com/roadrunner-server/logger/v4 v4.2.16 + github.com/roadrunner-server/sendremotefile/v4 v4.0.0-00010101000000-000000000000 + github.com/roadrunner-server/server/v4 v4.5.9 + github.com/stretchr/testify v1.8.4 + go.uber.org/zap v1.27.0 +) + +replace github.com/roadrunner-server/sendremotefile/v4 => ../ + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/caddyserver/certmagic v0.20.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/libdns/libdns v0.2.1 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mholt/acmez v1.2.0 // indirect + github.com/miekg/dns v1.1.58 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/onsi/ginkgo/v2 v2.15.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.19.0 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.50.0 // indirect + github.com/prometheus/procfs v0.13.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/quic-go v0.41.0 // indirect + github.com/roadrunner-server/api/v4 v4.11.1 // indirect + github.com/roadrunner-server/errors v1.4.0 // indirect + github.com/roadrunner-server/goridge/v3 v3.8.1 // indirect + github.com/roadrunner-server/sdk/v4 v4.6.0 // indirect + github.com/roadrunner-server/tcplisten v1.4.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zeebo/blake3 v0.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.24.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.uber.org/mock v0.4.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.18.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/tests/go.sum b/tests/go.sum new file mode 100644 index 0000000..d7db1a2 --- /dev/null +++ b/tests/go.sum @@ -0,0 +1,199 @@ +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= +github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= +github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= +github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= +github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k= +github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA= +github.com/roadrunner-server/api/v4 v4.11.1 h1:QDcAg+2YL6z1dBWUZi32qyFUaJAAF6LsIO8hqATegZU= +github.com/roadrunner-server/api/v4 v4.11.1/go.mod h1:YA8GSWngFSJMtloWsuqJg6GO9a2O9Lcz/743PxaCwd0= +github.com/roadrunner-server/config/v4 v4.6.9 h1:yNs8xHelu3yBVqUuQH25v93Be2/yv7Teie4Tl3kqeUc= +github.com/roadrunner-server/config/v4 v4.6.9/go.mod h1:Dj76Yq641BAI2sxKDt9XvJoPiURrFzBGmDPclwi4K9k= +github.com/roadrunner-server/endure/v2 v2.4.3 h1:R9DdsLiLjtSFivZ1HKk/1eDZ0TYaKHQzakVwz9D2hto= +github.com/roadrunner-server/endure/v2 v2.4.3/go.mod h1:4n3PdwZ3h/IRL2enDGvEVXtaQgqRnZ74VOyZtOJq528= +github.com/roadrunner-server/errors v1.4.0 h1:Odjg3VZrj1q5Y8ILwoN+JgERyv0pkhrWPNOM4h68iQ8= +github.com/roadrunner-server/errors v1.4.0/go.mod h1:78PvraAFj+Sxy5nDmo0S+h6rEMLFIDszWZxA3B0sPAs= +github.com/roadrunner-server/goridge/v3 v3.8.1 h1:mdS5lDKQwPuVJ2jwW7l5cngJNJiie7xEGwpgw7a6CuQ= +github.com/roadrunner-server/goridge/v3 v3.8.1/go.mod h1:L5UkNzD8aKLz6TzpqmmiHOJ6EnsadsWEYNoqK/4qoK0= +github.com/roadrunner-server/http/v4 v4.5.3 h1:0pEilNRSpbj6DWVuPK8mk27q+QG3gqF6s6x2HCRvnjo= +github.com/roadrunner-server/http/v4 v4.5.3/go.mod h1:tOvt7MmUNTs1IQR4GBwSl5a/vrn3KCG+755wTfeIFz8= +github.com/roadrunner-server/logger/v4 v4.2.16 h1:4mGVR5t9t/jGzJQEM/fiLpzrXV93sfhtVvgn5k1iW0E= +github.com/roadrunner-server/logger/v4 v4.2.16/go.mod h1:HM0OwRLsC52C1bSDEe+55JNRt2j4pnaQRyLtQRpE6fg= +github.com/roadrunner-server/sdk/v4 v4.6.0 h1:dXMN7V8+VKjQAZamhKBizPGSqrpfAfVG6r4OCF66hNY= +github.com/roadrunner-server/sdk/v4 v4.6.0/go.mod h1:YzRn2S947MqcnBcOuwu04CpAhsQGf6JBf+xY+njqu5o= +github.com/roadrunner-server/server/v4 v4.5.9 h1:m7JCbrjU97EeQjBTPhR3x5j3dGmkv52eoaJo0DuKUV8= +github.com/roadrunner-server/server/v4 v4.5.9/go.mod h1:YmisW+/CQ59+ucX56KJ9E6e2rM27Z5gkv3apIWIVurA= +github.com/roadrunner-server/tcplisten v1.4.0 h1:yWo09zktv/CSV6VywLfw4pwNcUchgTiIrW4uIICtO5M= +github.com/roadrunner-server/tcplisten v1.4.0/go.mod h1:A6+VSnW2ETGnN/e/CMdP63ZXqQDaC0UDMU6QmyuB0yM= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= +github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= +github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= +github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A= +github.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= +github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/contrib/propagators/jaeger v1.24.0 h1:CKtIfwSgDvJmaWsZROcHzONZgmQdMYn9mVYWypOWT5o= +go.opentelemetry.io/contrib/propagators/jaeger v1.24.0/go.mod h1:Q5JA/Cfdy/ta+5VeEhrMJRWGyS6UNRwFbl+yS3W1h5I= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tests/mock/logger.go b/tests/mock/logger.go new file mode 100644 index 0000000..9bc251f --- /dev/null +++ b/tests/mock/logger.go @@ -0,0 +1,64 @@ +package mocklogger + +import ( + "github.com/roadrunner-server/endure/v2/dep" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type ZapLoggerMock struct { + l *zap.Logger +} + +type Logger interface { + NamedLogger(string) *zap.Logger +} + +func ZapTestLogger(enab zapcore.LevelEnabler) (*ZapLoggerMock, *ObservedLogs) { + core, logs := New(enab) + obsLog := zap.New(core, zap.Development()) + + return &ZapLoggerMock{ + l: obsLog, + }, logs +} + +func (z *ZapLoggerMock) Init() error { + return nil +} + +func (z *ZapLoggerMock) Serve() chan error { + return make(chan error, 1) +} + +func (z *ZapLoggerMock) Stop() error { + return z.l.Sync() +} + +func (z *ZapLoggerMock) Provides() []*dep.Out { + return []*dep.Out{ + dep.Bind((*Logger)(nil), z.ProvideLogger), + } +} + +func (z *ZapLoggerMock) Weight() uint { + return 100 +} + +func (z *ZapLoggerMock) ProvideLogger() *Log { + return NewLogger(z.l) +} + +type Log struct { + base *zap.Logger +} + +func NewLogger(log *zap.Logger) *Log { + return &Log{ + base: log, + } +} + +func (l *Log) NamedLogger(string) *zap.Logger { + return l.base +} diff --git a/tests/mock/observer.go b/tests/mock/observer.go new file mode 100644 index 0000000..061cec7 --- /dev/null +++ b/tests/mock/observer.go @@ -0,0 +1,199 @@ +package mocklogger + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import ( + "strings" + "sync" + "time" + + "go.uber.org/zap/zapcore" +) + +// An LoggedEntry is an encoding-agnostic representation of a log message. +// Field availability is context dependant. +type LoggedEntry struct { + zapcore.Entry + Context []zapcore.Field +} + +// ContextMap returns a map for all fields in Context. +func (e LoggedEntry) ContextMap() map[string]any { + encoder := zapcore.NewMapObjectEncoder() + for _, f := range e.Context { + f.AddTo(encoder) + } + return encoder.Fields +} + +// ObservedLogs is a concurrency-safe, ordered collection of observed logs. +type ObservedLogs struct { + mu sync.RWMutex + logs []LoggedEntry +} + +// Len returns the number of items in the collection. +func (o *ObservedLogs) Len() int { + o.mu.RLock() + n := len(o.logs) + o.mu.RUnlock() + return n +} + +// All returns a copy of all the observed logs. +func (o *ObservedLogs) All() []LoggedEntry { + o.mu.RLock() + ret := make([]LoggedEntry, len(o.logs)) + copy(ret, o.logs) + o.mu.RUnlock() + return ret +} + +// TakeAll returns a copy of all the observed logs, and truncates the observed +// slice. +func (o *ObservedLogs) TakeAll() []LoggedEntry { + o.mu.Lock() + ret := o.logs + o.logs = nil + o.mu.Unlock() + return ret +} + +// AllUntimed returns a copy of all the observed logs, but overwrites the +// observed timestamps with time.Time's zero value. This is useful when making +// assertions in tests. +func (o *ObservedLogs) AllUntimed() []LoggedEntry { + ret := o.All() + for i := range ret { + ret[i].Time = time.Time{} + } + return ret +} + +// FilterLevelExact filters entries to those logged at exactly the given level. +func (o *ObservedLogs) FilterLevelExact(level zapcore.Level) *ObservedLogs { + return o.Filter(func(e LoggedEntry) bool { + return e.Level == level + }) +} + +// FilterMessage filters entries to those that have the specified message. +func (o *ObservedLogs) FilterMessage(msg string) *ObservedLogs { + return o.Filter(func(e LoggedEntry) bool { + return e.Message == msg + }) +} + +// FilterMessageSnippet filters entries to those that have a message containing the specified snippet. +func (o *ObservedLogs) FilterMessageSnippet(snippet string) *ObservedLogs { + return o.Filter(func(e LoggedEntry) bool { + return strings.Contains(e.Message, snippet) + }) +} + +// FilterField filters entries to those that have the specified field. +func (o *ObservedLogs) FilterField(field zapcore.Field) *ObservedLogs { + return o.Filter(func(e LoggedEntry) bool { + for _, ctxField := range e.Context { + if ctxField.Equals(field) { + return true + } + } + return false + }) +} + +// FilterFieldKey filters entries to those that have the specified key. +func (o *ObservedLogs) FilterFieldKey(key string) *ObservedLogs { + return o.Filter(func(e LoggedEntry) bool { + for _, ctxField := range e.Context { + if ctxField.Key == key { + return true + } + } + return false + }) +} + +// Filter returns a copy of this ObservedLogs containing only those entries +// for which the provided function returns true. +func (o *ObservedLogs) Filter(keep func(LoggedEntry) bool) *ObservedLogs { + o.mu.RLock() + defer o.mu.RUnlock() + + var filtered []LoggedEntry + for _, entry := range o.logs { + if keep(entry) { + filtered = append(filtered, entry) + } + } + return &ObservedLogs{logs: filtered} +} + +func (o *ObservedLogs) add(log LoggedEntry) { + o.mu.Lock() + o.logs = append(o.logs, log) + o.mu.Unlock() +} + +// New creates a new Core that buffers logs in memory (without any encoding). +// It's particularly useful in tests. +func New(enab zapcore.LevelEnabler) (zapcore.Core, *ObservedLogs) { + ol := &ObservedLogs{} + return &contextObserver{ + LevelEnabler: enab, + logs: ol, + }, ol +} + +type contextObserver struct { + zapcore.LevelEnabler + logs *ObservedLogs + context []zapcore.Field +} + +func (co *contextObserver) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if co.Enabled(ent.Level) { + return ce.AddCore(ent, co) + } + return ce +} + +func (co *contextObserver) With(fields []zapcore.Field) zapcore.Core { + return &contextObserver{ + LevelEnabler: co.LevelEnabler, + logs: co.logs, + context: append(co.context[:len(co.context):len(co.context)], fields...), + } +} + +func (co *contextObserver) Write(ent zapcore.Entry, fields []zapcore.Field) error { + all := make([]zapcore.Field, 0, len(fields)+len(co.context)) + all = append(all, co.context...) + all = append(all, fields...) + co.logs.add(LoggedEntry{ent, all}) + + return nil +} + +func (co *contextObserver) Sync() error { + return nil +} diff --git a/tests/php_test_files/composer.json b/tests/php_test_files/composer.json new file mode 100644 index 0000000..aafab07 --- /dev/null +++ b/tests/php_test_files/composer.json @@ -0,0 +1,11 @@ +{ + "name": "roadrunner/sendremotefile-tests", + "minimum-stability": "stable", + "require": { + "nyholm/psr7": "^1.8", + "spiral/roadrunner": "^2023.3", + "spiral/roadrunner-http": "^3.4", + "spiral/roadrunner-worker": "^3.4", + "spiral/goridge": "^4.1" + } +} diff --git a/tests/php_test_files/psr-worker.php b/tests/php_test_files/psr-worker.php new file mode 100644 index 0000000..b5ee617 --- /dev/null +++ b/tests/php_test_files/psr-worker.php @@ -0,0 +1,56 @@ +waitRequest()) { + try { + switch ($req->getUri()->getPath()) { + case "/local-file": + $resp = new Response(200, ["X-Sendremotefile" => "/../sample/2k24.mp4"]); + break; + + case "/remote-file": + $resp = new Response(200, ["X-Sendremotefile" => "http://127.0.0.1:18953/file", "Content-Disposition" => "attachment; filename=1MB.jpg"]); + break; + + case "/remote-file-not-found": + $resp = new Response(200, ["X-Sendremotefile" => "http://127.0.0.1:18953/file-missing"]); + break; + + case "/remote-file-timeout": + $resp = new Response(200, ["X-Sendremotefile" => "http://127.0.0.1:18953/file-timeout"]); + break; + + case "/file": + $resp = new Response(200, ["Content-Type" => "image/jpeg"], $psr17Factory->createStreamFromFile(__DIR__ . "/../data/1MB.jpg")); + break; + + case "/file-timeout": + usleep(5_500_000); + $resp = new Response(200, ["Content-Type" => "image/jpeg"], $psr17Factory->createStreamFromFile(__DIR__ . "/../data/1MB.jpg")); + break; + + case "/minio-file": + $resp = new Response(200, ["X-Sendremotefile" => "http://127.0.0.1:26379/bucket/fs-data/1MB.jpg"]); + break; + + default: + $resp = new Response(404); + break; + } + + $psr7->respond($resp); + unset($resp); + } catch (\Throwable $e) { + $psr7->getWorker()->error((string)$e); + } +} diff --git a/tests/pkgs.txt b/tests/pkgs.txt new file mode 100644 index 0000000..93a2ac5 --- /dev/null +++ b/tests/pkgs.txt @@ -0,0 +1 @@ +github.com/roadrunner-server/amqp/v4,github.com/roadrunner-server/app-logger/v4,github.com/roadrunner-server/beanstalk/v4,github.com/roadrunner-server/boltdb/v4,github.com/roadrunner-server/centrifuge/v4,github.com/roadrunner-server/config/v4,github.com/roadrunner-server/fileserver/v4,github.com/roadrunner-server/grpc/v4,github.com/roadrunner-server/gzip/v4,github.com/roadrunner-server/headers/v4,github.com/roadrunner-server/http/v4,github.com/roadrunner-server/informer/v4,github.com/roadrunner-server/jobs/v4,github.com/roadrunner-server/kafka/v4,github.com/roadrunner-server/kv/v4,github.com/roadrunner-server/logger/v4,github.com/roadrunner-server/lock/v4,github.com/roadrunner-server/memcached/v4,github.com/roadrunner-server/memory/v4,github.com/roadrunner-server/metrics/v4,github.com/roadrunner-server/nats/v4,github.com/roadrunner-server/otel/v4,github.com/roadrunner-server/prometheus/v4,github.com/roadrunner-server/proxy_ip_parser/v4,github.com/roadrunner-server/redis/v4,github.com/roadrunner-server/resetter/v4,github.com/roadrunner-server/rpc/v4,github.com/roadrunner-server/send/v4,github.com/roadrunner-server/sendremotefile/v4,github.com/roadrunner-server/server/v4,github.com/roadrunner-server/service/v4,github.com/roadrunner-server/sqs/v4,github.com/roadrunner-server/static/v4,github.com/roadrunner-server/status/v4,github.com/roadrunner-server/tcp/v4 \ No newline at end of file diff --git a/tests/plugin_durability_test.go b/tests/plugin_durability_test.go new file mode 100644 index 0000000..014a743 --- /dev/null +++ b/tests/plugin_durability_test.go @@ -0,0 +1,213 @@ +package sendremotefile + +import ( + "io" + "log/slog" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + mocklogger "tests/mock" + + toxiproxy "github.com/Shopify/toxiproxy/client" + "github.com/roadrunner-server/config/v4" + "github.com/roadrunner-server/endure/v2" + httpPlugin "github.com/roadrunner-server/http/v4" + "github.com/roadrunner-server/sendremotefile/v4" + "github.com/roadrunner-server/server/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +var proxy *toxiproxy.Proxy + +func init() { + client := toxiproxy.NewClient("127.0.0.1:8474") + var err error + proxy, err = client.CreateProxy("TestStorageDown", "localhost:26379", "localhost:9000") + if err != nil { + panic(err) + } +} + +func TestStorageDown(t *testing.T) { + cont := endure.New(slog.LevelDebug) + + cfg := &config.Plugin{ + Version: "2023.3.0", + Path: "configs/.rr-with-sendremotefile.yaml", + Prefix: "rr", + } + + l, oLogger := mocklogger.ZapTestLogger(zap.DebugLevel) + + err := cont.RegisterAll( + cfg, + l, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &sendremotefile.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + require.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + + proxy.Disable() + + r, err := http.DefaultClient.Get("http://127.0.0.1:18953/minio-file") + require.NoError(t, err) + + assert.Equal(t, 500, r.StatusCode) + assert.Equal(t, 1, oLogger.FilterMessageSnippet("failed to request from the upstream").Len()) + + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + + assert.Equal(t, "Internal Server Error\n", string(b)) + + err = r.Body.Close() + require.NoError(t, err) + + stopCh <- struct{}{} + wg.Wait() +} + +func TestStorageSlowConnectionDown(t *testing.T) { + cont := endure.New(slog.LevelDebug) + + cfg := &config.Plugin{ + Version: "2023.3.0", + Path: "configs/.rr-with-sendremotefile.yaml", + Prefix: "rr", + } + + l, oLogger := mocklogger.ZapTestLogger(zap.DebugLevel) + + err := cont.RegisterAll( + cfg, + l, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &sendremotefile.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + require.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + + proxy.Enable() + proxy.AddToxic("slow_bandwidth", "bandwidth", "downstream", 1.0, toxiproxy.Attributes{ + "rate": 10, + }) + + go func() { + time.Sleep(time.Second * 2) + proxy.Disable() + }() + + time.Sleep(time.Second * 1) + + r, err := http.DefaultClient.Get("http://127.0.0.1:18953/minio-file") + require.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + + assert.Less(t, len(b), 100000) + + assert.Equal(t, 1, oLogger.FilterMessageSnippet("failed to read data from the upstream response").Len()) + + err = r.Body.Close() + require.NoError(t, err) + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugin_test.go b/tests/plugin_test.go new file mode 100644 index 0000000..894b429 --- /dev/null +++ b/tests/plugin_test.go @@ -0,0 +1,303 @@ +package sendremotefile + +import ( + "io" + "log/slog" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + mocklogger "tests/mock" + + "github.com/roadrunner-server/config/v4" + "github.com/roadrunner-server/endure/v2" + httpPlugin "github.com/roadrunner-server/http/v4" + "github.com/roadrunner-server/logger/v4" + "github.com/roadrunner-server/sendremotefile/v4" + "github.com/roadrunner-server/server/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestSendremotefileInit(t *testing.T) { + cont := endure.New(slog.LevelDebug) + + cfg := &config.Plugin{ + Version: "2023.3.0", + Path: "configs/.rr-with-sendremotefile.yaml", + Prefix: "rr", + } + + err := cont.RegisterAll( + cfg, + &logger.Plugin{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &sendremotefile.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + require.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + stopCh <- struct{}{} + wg.Wait() +} + +func TestSendremotefileDisabled(t *testing.T) { + cont := endure.New(slog.LevelDebug) + + cfg := &config.Plugin{ + Version: "2023.3.0", + Path: "configs/.rr-with-disabled-sendremotefile.yaml", + Prefix: "rr", + } + + err := cont.RegisterAll( + cfg, + &logger.Plugin{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &sendremotefile.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + require.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("middlewareDisabledCheck", middlewareDisabledCheck) + + stopCh <- struct{}{} + wg.Wait() +} + +func middlewareDisabledCheck(t *testing.T) { + r, err := http.DefaultClient.Get("http://127.0.0.1:18953/remote-file") + require.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "http://127.0.0.1:18953/file", r.Header.Get("X-Sendremotefile")) + + err = r.Body.Close() + require.NoError(t, err) +} + +func TestSendremotefileFileStream(t *testing.T) { + cont := endure.New(slog.LevelDebug) + + cfg := &config.Plugin{ + Version: "2023.3.0", + Path: "configs/.rr-with-sendremotefile.yaml", + Prefix: "rr", + } + + l, oLogger := mocklogger.ZapTestLogger(zap.DebugLevel) + + err := cont.RegisterAll( + cfg, + l, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &sendremotefile.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + require.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("remoteFileCheck", remoteFileCheck) + t.Run("localFileCheck", localFileCheck(oLogger)) + t.Run("remoteFileNotFoundCheck", remoteFileNotFoundCheck(oLogger)) + t.Run("remoteFileTimeoutCheck", remoteFileTimeoutCheck(oLogger)) + + stopCh <- struct{}{} + wg.Wait() +} + +func remoteFileCheck(t *testing.T) { + r, err := http.DefaultClient.Get("http://127.0.0.1:18953/remote-file") + require.NoError(t, err) + + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + + file, err := os.Open("./data/1MB.jpg") + require.NoError(t, err) + defer file.Close() + fs, err := file.Stat() + require.NoError(t, err) + + assert.Equal(t, int(fs.Size()), len(b)) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "", r.Header.Get("X-Sendremotefile")) + assert.Equal(t, "attachment; filename=1MB.jpg", r.Header.Get("Content-Disposition")) + assert.Equal(t, "application/octet-stream", r.Header.Get("Content-Type")) + + err = r.Body.Close() + require.NoError(t, err) +} + +func localFileCheck(oLogger *mocklogger.ObservedLogs) func(t *testing.T) { + return func(t *testing.T) { + r, err := http.DefaultClient.Get("http://127.0.0.1:18953/local-file") + require.NoError(t, err) + defer r.Body.Close() + + assert.Equal(t, 404, r.StatusCode) + assert.Equal(t, "", r.Header.Get("X-Sendremotefile")) + assert.Equal(t, 1, oLogger.FilterMessageSnippet("header value must start with http").Len()) + + err = r.Body.Close() + require.NoError(t, err) + } +} + +func remoteFileNotFoundCheck(oLogger *mocklogger.ObservedLogs) func(t *testing.T) { + return func(t *testing.T) { + r, err := http.DefaultClient.Get("http://127.0.0.1:18953/remote-file-not-found") + require.NoError(t, err) + defer r.Body.Close() + + assert.Equal(t, 400, r.StatusCode) + assert.Equal(t, "", r.Header.Get("X-Sendremotefile")) + assert.Equal(t, 1, oLogger.FilterMessageSnippet("invalid upstream response status code").Len()) + + err = r.Body.Close() + require.NoError(t, err) + } +} + +func remoteFileTimeoutCheck(oLogger *mocklogger.ObservedLogs) func(t *testing.T) { + return func(t *testing.T) { + r, err := http.DefaultClient.Get("http://127.0.0.1:18953/remote-file-timeout") + require.NoError(t, err) + + assert.Equal(t, 408, r.StatusCode) + assert.Equal(t, "", r.Header.Get("X-Sendremotefile")) + + err = r.Body.Close() + require.NoError(t, err) + } +} diff --git a/wpool.go b/wpool.go new file mode 100644 index 0000000..9eda5b2 --- /dev/null +++ b/wpool.go @@ -0,0 +1,39 @@ +package sendremotefile + +import ( + "net/http" + "sync" +) + +type wpool struct { + *sync.Pool +} + +func NewWriterPool() *wpool { + return &wpool{ + &sync.Pool{ + New: func() any { + wr := new(writer) + wr.code = http.StatusOK + wr.data = make([]byte, 0, 10) + wr.hdrToSend = make(map[string][]string, 2) + return wr + }, + }, + } +} + +func (wp *wpool) get() *writer { + return wp.Get().(*writer) +} + +func (wp *wpool) put(w *writer) { + w.code = http.StatusOK + w.data = make([]byte, 0, 10) + + for k := range w.hdrToSend { + delete(w.hdrToSend, k) + } + + wp.Put(w) +} diff --git a/writer.go b/writer.go new file mode 100644 index 0000000..e9b099b --- /dev/null +++ b/writer.go @@ -0,0 +1,24 @@ +package sendremotefile + +import ( + "net/http" +) + +type writer struct { + code int + data []byte + hdrToSend map[string][]string +} + +func (w *writer) WriteHeader(code int) { + w.code = code +} + +func (w *writer) Write(b []byte) (int, error) { + w.data = append(w.data, b...) + return len(b), nil +} + +func (w *writer) Header() http.Header { + return w.hdrToSend +}