├── .ci ├── install.ps1 └── versions.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── release.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── golangci-lint.yml │ └── tests.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── RELEASE.md ├── _examples ├── client │ └── client.go ├── consumer │ └── consumer.go ├── producer │ └── producer.go └── pubsub │ └── pubsub.go ├── allocator.go ├── allocator_test.go ├── auth.go ├── auth_test.go ├── certs.sh ├── change_version.sh ├── channel.go ├── client_test.go ├── confirms.go ├── confirms_test.go ├── connection.go ├── connection_test.go ├── consumers.go ├── consumers_test.go ├── delivery.go ├── delivery_test.go ├── doc.go ├── example_client_test.go ├── examples_test.go ├── fuzz.go ├── gen.ps1 ├── gen.sh ├── go.mod ├── go.sum ├── integration_test.go ├── log.go ├── rabbitmq-confs └── tls │ └── 90-tls.conf ├── read.go ├── read_test.go ├── reconnect_test.go ├── return.go ├── shared_test.go ├── spec ├── amqp0-9-1.stripped.extended.xml └── gen.go ├── spec091.go ├── tls_test.go ├── types.go ├── types_test.go ├── uri.go ├── uri_test.go └── write.go /.ci/install.ps1: -------------------------------------------------------------------------------- 1 | $ProgressPreference = 'Continue' 2 | $ErrorActionPreference = 'Stop' 3 | Set-StrictMode -Version 2.0 4 | 5 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 'Tls12' 6 | 7 | $versions_path = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath '.ci' | Join-Path -ChildPath 'versions.json' 8 | $versions = Get-Content $versions_path | ConvertFrom-Json 9 | Write-Host "[INFO] versions: $versions" 10 | $erlang_ver = $versions.erlang 11 | $rabbitmq_ver = $versions.rabbitmq 12 | 13 | $base_installers_dir = Join-Path -Path $HOME -ChildPath 'installers' 14 | if (-Not (Test-Path $base_installers_dir)) 15 | { 16 | New-Item -Verbose -ItemType Directory $base_installers_dir 17 | } 18 | 19 | $erlang_download_url = "https://github.com/erlang/otp/releases/download/OTP-$erlang_ver/otp_win64_$erlang_ver.exe" 20 | $erlang_installer_path = Join-Path -Path $base_installers_dir -ChildPath "otp_win64_$erlang_ver.exe" 21 | $erlang_install_dir = Join-Path -Path $HOME -ChildPath 'erlang' 22 | 23 | Write-Host '[INFO] Downloading Erlang...' 24 | 25 | if (-Not (Test-Path $erlang_installer_path)) 26 | { 27 | Invoke-WebRequest -UseBasicParsing -Uri $erlang_download_url -OutFile $erlang_installer_path 28 | } 29 | else 30 | { 31 | Write-Host "[INFO] Found '$erlang_installer_path' in cache!" 32 | } 33 | 34 | Write-Host "[INFO] Installing Erlang to $erlang_install_dir..." 35 | & $erlang_installer_path '/S' "/D=$erlang_install_dir" | Out-Null 36 | 37 | $rabbitmq_installer_download_url = "https://github.com/rabbitmq/rabbitmq-server/releases/download/v$rabbitmq_ver/rabbitmq-server-$rabbitmq_ver.exe" 38 | $rabbitmq_installer_path = Join-Path -Path $base_installers_dir -ChildPath "rabbitmq-server-$rabbitmq_ver.exe" 39 | Write-Host "[INFO] rabbitmq installer path $rabbitmq_installer_path" 40 | 41 | $erlang_reg_path = 'HKLM:\SOFTWARE\Ericsson\Erlang' 42 | if (Test-Path 'HKLM:\SOFTWARE\WOW6432Node\') 43 | { 44 | $erlang_reg_path = 'HKLM:\SOFTWARE\WOW6432Node\Ericsson\Erlang' 45 | } 46 | $erlang_erts_version = Get-ChildItem -Path $erlang_reg_path -Name 47 | $erlang_home = (Get-ItemProperty -LiteralPath $erlang_reg_path\$erlang_erts_version).'(default)' 48 | 49 | Write-Host "[INFO] Setting ERLANG_HOME to '$erlang_home'..." 50 | $env:ERLANG_HOME = $erlang_home 51 | [Environment]::SetEnvironmentVariable('ERLANG_HOME', $erlang_home, 'Machine') 52 | 53 | Write-Host "[INFO] Setting RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS..." 54 | $env:RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS = '-rabbitmq_stream advertised_host localhost' 55 | [Environment]::SetEnvironmentVariable('RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS', '-rabbitmq_stream advertised_host localhost', 'Machine') 56 | 57 | Write-Host '[INFO] Downloading RabbitMQ...' 58 | 59 | if (-Not (Test-Path $rabbitmq_installer_path)) 60 | { 61 | Invoke-WebRequest -UseBasicParsing -Uri $rabbitmq_installer_download_url -OutFile $rabbitmq_installer_path 62 | } 63 | else 64 | { 65 | Write-Host "[INFO] Found '$rabbitmq_installer_path' in cache!" 66 | } 67 | 68 | Write-Host "[INFO] Installer dir '$base_installers_dir' contents:" 69 | Get-ChildItem -Verbose -Path $base_installers_dir 70 | 71 | Write-Host '[INFO] Creating Erlang cookie files...' 72 | 73 | function Set-ErlangCookie { 74 | Param($Path, $Value = 'RABBITMQ-COOKIE') 75 | Remove-Item -Force $Path -ErrorAction SilentlyContinue 76 | [System.IO.File]::WriteAllText($Path, $Value, [System.Text.Encoding]::ASCII) 77 | } 78 | 79 | $erlang_cookie_user = Join-Path -Path $HOME -ChildPath '.erlang.cookie' 80 | $erlang_cookie_system = Join-Path -Path $env:SystemRoot -ChildPath 'System32\config\systemprofile\.erlang.cookie' 81 | 82 | Set-ErlangCookie -Path $erlang_cookie_user 83 | Set-ErlangCookie -Path $erlang_cookie_system 84 | 85 | Write-Host '[INFO] Installing and starting RabbitMQ with default config...' 86 | 87 | & $rabbitmq_installer_path '/S' | Out-Null 88 | (Get-Service -Name RabbitMQ).Status 89 | 90 | $rabbitmq_base_path = (Get-ItemProperty -Name Install_Dir -Path 'HKLM:\SOFTWARE\WOW6432Node\VMware, Inc.\RabbitMQ Server').Install_Dir 91 | $regPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\RabbitMQ' 92 | if (Test-Path 'HKLM:\SOFTWARE\WOW6432Node\') 93 | { 94 | $regPath = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\RabbitMQ' 95 | } 96 | $rabbitmq_version = (Get-ItemProperty $regPath 'DisplayVersion').DisplayVersion 97 | Write-Host "[INFO] RabbitMQ version path: $rabbitmq_base_path and version: $rabbitmq_version" 98 | 99 | $rabbitmq_home = Join-Path -Path $rabbitmq_base_path -ChildPath "rabbitmq_server-$rabbitmq_version" 100 | Write-Host "[INFO] Setting RABBITMQ_HOME to '$rabbitmq_home'..." 101 | [Environment]::SetEnvironmentVariable('RABBITMQ_HOME', $rabbitmq_home, 'Machine') 102 | $env:RABBITMQ_HOME = $rabbitmq_home 103 | 104 | $rabbitmqctl_path = Join-Path -Path $rabbitmq_base_path -ChildPath "rabbitmq_server-$rabbitmq_version" | Join-Path -ChildPath 'sbin' | Join-Path -ChildPath 'rabbitmqctl.bat' 105 | $rabbitmq_plugins_path = Join-Path -Path $rabbitmq_base_path -ChildPath "rabbitmq_server-$rabbitmq_version" | Join-Path -ChildPath 'sbin' | Join-Path -ChildPath 'rabbitmq-plugins.bat' 106 | 107 | Write-Host "[INFO] Setting RABBITMQ_RABBITMQCTL_PATH to '$rabbitmqctl_path'..." 108 | $env:RABBITMQ_RABBITMQCTL_PATH = $rabbitmqctl_path 109 | [Environment]::SetEnvironmentVariable('RABBITMQ_RABBITMQCTL_PATH', $rabbitmqctl_path, 'Machine') 110 | 111 | $epmd_running = $false 112 | [int]$count = 1 113 | 114 | $epmd_exe = Join-Path -Path $erlang_home -ChildPath "erts-$erlang_erts_version" | Join-Path -ChildPath 'bin' | Join-Path -ChildPath 'epmd.exe' 115 | 116 | Write-Host "[INFO] Waiting for epmd ($epmd_exe) to report that RabbitMQ has started..." 117 | 118 | Do { 119 | $epmd_running = & $epmd_exe -names | Select-String -CaseSensitive -SimpleMatch -Quiet -Pattern 'name rabbit at port' 120 | if ($epmd_running -eq $true) { 121 | Write-Host '[INFO] epmd reports that RabbitMQ is running!' 122 | break 123 | } 124 | 125 | if ($count -gt 60) { 126 | throw '[ERROR] too many tries waiting for epmd to report RabbitMQ running!' 127 | } 128 | 129 | Write-Host "[INFO] epmd NOT reporting yet that RabbitMQ is running, count: '$count'..." 130 | $count = $count + 1 131 | Start-Sleep -Seconds 5 132 | 133 | } While ($true) 134 | 135 | [int]$count = 1 136 | 137 | Do { 138 | $proc_id = (Get-Process -Name erl).Id 139 | if (-Not ($proc_id -is [array])) { 140 | & $rabbitmqctl_path wait -t 300000 -P $proc_id 141 | if ($LASTEXITCODE -ne 0) { 142 | throw "[ERROR] rabbitmqctl wait returned error: $LASTEXITCODE" 143 | } 144 | break 145 | } 146 | 147 | if ($count -gt 120) { 148 | throw '[ERROR] too many tries waiting for just one erl process to be running!' 149 | } 150 | 151 | Write-Host '[INFO] multiple erl instances running still...' 152 | $count = $count + 1 153 | Start-Sleep -Seconds 5 154 | 155 | } While ($true) 156 | 157 | $ErrorActionPreference = 'Continue' 158 | Write-Host '[INFO] Getting RabbitMQ status...' 159 | & $rabbitmqctl_path status 160 | 161 | $ErrorActionPreference = 'Continue' 162 | Write-Host '[INFO] Enabling plugins...' 163 | & $rabbitmq_plugins_path enable rabbitmq_management rabbitmq_stream rabbitmq_stream_management rabbitmq_amqp1_0 164 | -------------------------------------------------------------------------------- /.ci/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "erlang": "26.2.5.1", 3 | "rabbitmq": "3.13.4" 4 | } 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/release.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | about: Do not use this template. This template is meant to be used by RabbitMQ maintainers. 4 | title: Release x.y.z 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] Update change log using `github_changelog_generator` 11 | - [ ] Update version in `connection.go` using `change_version.sh` 12 | - [ ] Create a tag 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '43 5 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # 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 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v3 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 50 | 51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 52 | # If this step fails, then you should remove it and run the build manually (see below) 53 | - name: Autobuild 54 | uses: github/codeql-action/autobuild@v3 55 | 56 | # ℹ️ Command-line programs to run using the OS shell. 57 | # 📚 https://git.io/JvXDl 58 | 59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 60 | # and modify them (or add more) to build your code if your project 61 | # uses a compiled language 62 | 63 | #- run: | 64 | # make bootstrap 65 | # make release 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v3 69 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - "*" 9 | 10 | permissions: 11 | contents: read 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: 'stable' 20 | check-latest: true 21 | - uses: actions/checkout@v4 22 | - uses: golangci/golangci-lint-action@v4 23 | with: 24 | version: latest 25 | only-new-issues: false 26 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: amqp091-go 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build-win32: 11 | name: build/test on windows-latest 12 | runs-on: windows-latest 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | go-version: ['oldstable', 'stable'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | check-latest: true 23 | - name: Cache installers 24 | uses: actions/cache@v3 25 | with: 26 | # Note: the cache path is relative to the workspace directory 27 | # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#using-the-cache-action 28 | path: ~/installers 29 | key: ${{ runner.os }}-v0-${{ hashFiles('.ci/versions.json') }} 30 | - name: Install and start RabbitMQ 31 | run: ./.ci/install.ps1 32 | - name: Tests 33 | run: go test -cpu 1,2 -race -v -tags integration 34 | build-linux: 35 | name: build/test on ubuntu-latest 36 | runs-on: ubuntu-latest 37 | strategy: 38 | matrix: 39 | go-version: ['oldstable', 'stable'] 40 | services: 41 | rabbitmq: 42 | image: rabbitmq 43 | ports: 44 | - 5672:5672 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-go@v5 48 | with: 49 | go-version: ${{ matrix.go-version }} 50 | check-latest: true 51 | - name: Tests 52 | env: 53 | RABBITMQ_RABBITMQCTL_PATH: DOCKER:${{ job.services.rabbitmq.id }} 54 | run: make check-fmt tests 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | certs/* 2 | spec/spec 3 | examples/simple-consumer/simple-consumer 4 | examples/simple-producer/simple-producer 5 | 6 | .idea/ 7 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | build-tags: 3 | - integration 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in RabbitMQ Operator project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at oss-coc@vmware.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Workflow 4 | 5 | Here is the recommended workflow: 6 | 7 | 1. Fork this repository, **github.com/rabbitmq/amqp091-go** 8 | 1. Create your feature branch (`git checkout -b my-new-feature`) 9 | 1. Run Static Checks 10 | 1. Run integration tests (see below) 11 | 1. **Implement tests** 12 | 1. Implement fixes 13 | 1. Commit your changes. Use a [good, descriptive, commit message][good-commit]. 14 | 1. Push to a branch (`git push -u origin my-new-feature`) 15 | 1. Submit a pull request 16 | 17 | [good-commit]: https://cbea.ms/git-commit/ 18 | 19 | ## Running Static Checks 20 | 21 | golangci-lint must be installed to run the static checks. See [installation 22 | docs](https://golangci-lint.run/usage/install/) for more information. 23 | 24 | The static checks can be run via: 25 | 26 | ```shell 27 | make checks 28 | ``` 29 | 30 | ## Running Tests 31 | 32 | ### Integration Tests 33 | 34 | Running the Integration tests require: 35 | 36 | * A running RabbitMQ node with all defaults: 37 | [https://www.rabbitmq.com/download.html](https://www.rabbitmq.com/download.html) 38 | * That the server is either reachable via `amqp://guest:guest@127.0.0.1:5672/` 39 | or the environment variable `AMQP_URL` set to it's URL 40 | (e.g.: `export AMQP_URL="amqp://guest:verysecretpasswd@rabbitmq-host:5772/`) 41 | 42 | The integration tests can be run via: 43 | 44 | ```shell 45 | make tests 46 | ``` 47 | 48 | Some tests require access to `rabbitmqctl` CLI. Use the environment variable 49 | `RABBITMQ_RABBITMQCTL_PATH=/some/path/to/rabbitmqctl` to run those tests. 50 | 51 | If you have Docker available in your machine, you can run: 52 | 53 | ```shell 54 | make tests-docker 55 | ``` 56 | 57 | This target will start a RabbitMQ container, run the test suite with the environment 58 | variable setup, and stop RabbitMQ container after a successful run. 59 | 60 | All integration tests should use the `integrationConnection(...)` test 61 | helpers defined in `integration_test.go` to setup the integration environment 62 | and logging. 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | AMQP 0-9-1 Go Client 2 | Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | Redistributions in binary form must reproduce the above copyright notice, this 13 | list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := list 2 | 3 | # Insert a comment starting with '##' after a target, and it will be printed by 'make' and 'make list' 4 | .PHONY: list 5 | list: ## list Makefile targets 6 | @echo "The most used targets: \n" 7 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' 8 | 9 | .PHONY: check-fmt 10 | check-fmt: ## Ensure code is formatted 11 | gofmt -l -d . # For the sake of debugging 12 | test -z "$$(gofmt -l .)" 13 | 14 | .PHONY: fmt 15 | fmt: ## Run go fmt against code 16 | go fmt ./... 17 | 18 | .PHONY: tests 19 | tests: ## Run all tests and requires a running rabbitmq-server. Use GO_TEST_FLAGS to add extra flags to go test 20 | go test -race -v -tags integration $(GO_TEST_FLAGS) 21 | 22 | .PHONY: tests-docker 23 | tests-docker: rabbitmq-server 24 | RABBITMQ_RABBITMQCTL_PATH="DOCKER:$(CONTAINER_NAME)" go test -race -v -tags integration $(GO_TEST_FLAGS) 25 | $(MAKE) stop-rabbitmq-server 26 | 27 | .PHONY: check 28 | check: 29 | golangci-lint run ./... 30 | 31 | CONTAINER_NAME ?= amqp091-go-rabbitmq 32 | 33 | .PHONY: rabbitmq-server 34 | rabbitmq-server: ## Start a RabbitMQ server using Docker. Container name can be customised with CONTAINER_NAME=some-rabbit 35 | docker run --detach --rm --name $(CONTAINER_NAME) \ 36 | --publish 5672:5672 --publish 15672:15672 \ 37 | --pull always rabbitmq:3-management 38 | 39 | .PHONY: stop-rabbitmq-server 40 | stop-rabbitmq-server: ## Stop a RabbitMQ server using Docker. Container name can be customised with CONTAINER_NAME=some-rabbit 41 | docker stop $(CONTAINER_NAME) 42 | 43 | certs: 44 | ./certs.sh 45 | 46 | .PHONY: certs-rm 47 | certs-rm: 48 | rm -r ./certs/ 49 | 50 | .PHONY: rabbitmq-server-tls 51 | rabbitmq-server-tls: | certs ## Start a RabbitMQ server using Docker. Container name can be customised with CONTAINER_NAME=some-rabbit 52 | docker run --detach --rm --name $(CONTAINER_NAME) \ 53 | --publish 5672:5672 --publish 5671:5671 --publish 15672:15672 \ 54 | --mount type=bind,src=./certs/server,dst=/certs \ 55 | --mount type=bind,src=./certs/ca/cacert.pem,dst=/certs/cacert.pem,readonly \ 56 | --mount type=bind,src=./rabbitmq-confs/tls/90-tls.conf,dst=/etc/rabbitmq/conf.d/90-tls.conf \ 57 | --pull always rabbitmq:3-management 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go RabbitMQ Client Library 2 | 3 | [![amqp091-go](https://github.com/rabbitmq/amqp091-go/actions/workflows/tests.yml/badge.svg)](https://github.com/rabbitmq/amqp091-go/actions/workflows/tests.yml) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/rabbitmq/amqp091-go.svg)](https://pkg.go.dev/github.com/rabbitmq/amqp091-go) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/rabbitmq/amqp091-go)](https://goreportcard.com/report/github.com/rabbitmq/amqp091-go) 6 | 7 | This is a Go AMQP 0.9.1 client maintained by the [RabbitMQ core team](https://github.com/rabbitmq). 8 | It was [originally developed by Sean Treadway](https://github.com/streadway/amqp). 9 | 10 | ## Differences from streadway/amqp 11 | 12 | Some things are different compared to the original client, 13 | others haven't changed. 14 | 15 | ### Package Name 16 | 17 | This library uses a different package name. If moving from `streadway/amqp`, 18 | using an alias may reduce the number of changes needed: 19 | 20 | ``` go 21 | amqp "github.com/rabbitmq/amqp091-go" 22 | ``` 23 | 24 | ### License 25 | 26 | This client uses the same 2-clause BSD license as the original project. 27 | 28 | ### Public API Evolution 29 | 30 | This client retains key API elements as practically possible. 31 | It is, however, open to reasonable breaking public API changes suggested by the community. 32 | We don't have the "no breaking public API changes ever" rule and fully recognize 33 | that a good client API evolves over time. 34 | 35 | 36 | ## Project Maturity 37 | 38 | This project is based on a mature Go client that's been around for over a decade. 39 | 40 | 41 | ## Supported Go Versions 42 | 43 | This client supports two most recent Go release series. 44 | 45 | 46 | ## Supported RabbitMQ Versions 47 | 48 | This project supports RabbitMQ versions starting with `2.0` but primarily tested 49 | against [currently supported RabbitMQ release series](https://www.rabbitmq.com/versions.html). 50 | 51 | Some features and behaviours may be server version-specific. 52 | 53 | ## Goals 54 | 55 | Provide a functional interface that closely represents the AMQP 0.9.1 model 56 | targeted to RabbitMQ as a server. This includes the minimum necessary to 57 | interact the semantics of the protocol. 58 | 59 | ## Non-goals 60 | 61 | Things not intended to be supported. 62 | 63 | * Auto reconnect and re-synchronization of client and server topologies. 64 | * Reconnection would require understanding the error paths when the 65 | topology cannot be declared on reconnect. This would require a new set 66 | of types and code paths that are best suited at the call-site of this 67 | package. AMQP has a dynamic topology that needs all peers to agree. If 68 | this doesn't happen, the behavior is undefined. Instead of producing a 69 | possible interface with undefined behavior, this package is designed to 70 | be simple for the caller to implement the necessary connection-time 71 | topology declaration so that reconnection is trivial and encapsulated in 72 | the caller's application code. 73 | * AMQP Protocol negotiation for forward or backward compatibility. 74 | * 0.9.1 is stable and widely deployed. AMQP 1.0 is a divergent 75 | specification (a different protocol) and belongs to a different library. 76 | * Anything other than PLAIN and EXTERNAL authentication mechanisms. 77 | * Keeping the mechanisms interface modular makes it possible to extend 78 | outside of this package. If other mechanisms prove to be popular, then 79 | we would accept patches to include them in this package. 80 | * Support for [`basic.return` and `basic.ack` frame ordering](https://www.rabbitmq.com/confirms.html#when-publishes-are-confirmed). 81 | This client uses Go channels for certain protocol events and ordering between 82 | events sent to two different channels generally cannot be guaranteed. 83 | 84 | ## Usage 85 | 86 | See the [_examples](_examples) subdirectory for simple producers and consumers executables. 87 | If you have a use-case in mind which isn't well-represented by the examples, 88 | please file an issue. 89 | 90 | ## Documentation 91 | 92 | * [Godoc API reference](http://godoc.org/github.com/rabbitmq/amqp091-go) 93 | * [RabbitMQ tutorials in Go](https://github.com/rabbitmq/rabbitmq-tutorials/tree/master/go) 94 | 95 | ## Contributing 96 | 97 | Pull requests are very much welcomed. Create your pull request on a non-main 98 | branch, make sure a test or example is included that covers your change, and 99 | your commits represent coherent changes that include a reason for the change. 100 | 101 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more information. 102 | 103 | ## License 104 | 105 | BSD 2 clause, see LICENSE for more details. 106 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Guide to release a new version 2 | 3 | 1. Update the `buildVersion` constant in [connection.go](https://github.com/rabbitmq/amqp091-go/blob/4886c35d10b273bd374e3ed2356144ad41d27940/connection.go#L31) 4 | 2. Commit and push. Include the version in the commit message e.g. [this commit](https://github.com/rabbitmq/amqp091-go/commit/52ce2efd03c53dcf77d5496977da46840e9abd24) 5 | 3. Create a new [GitHub Release](https://github.com/rabbitmq/amqp091-go/releases). Create a new tag as `v..` 6 | 1. Use auto-generate release notes feature in GitHub 7 | 4. Generate the change log, see [Changelog Generation](#changelog-generation) 8 | 5. Review the changelog. Watch out for issues closed as "not-fixed" or without a PR 9 | 6. Commit and Push. Pro-tip: include `[skip ci]` in the commit message to skip the CI run, since it's only documentation 10 | 7. Send an announcement to the mailing list. Take inspiration from [this message](https://groups.google.com/g/rabbitmq-users/c/EBGYGOWiSgs/m/0sSFuAGICwAJ) 11 | 12 | ## Changelog Generation 13 | 14 | ``` 15 | github_changelog_generator --token GITHUB-TOKEN -u rabbitmq -p amqp091-go --no-unreleased --release-branch main 16 | ``` 17 | -------------------------------------------------------------------------------- /_examples/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | package main 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "log" 11 | "os" 12 | "sync" 13 | "time" 14 | 15 | amqp "github.com/rabbitmq/amqp091-go" 16 | ) 17 | 18 | // This exports a Client object that wraps this library. It 19 | // automatically reconnects when the connection fails, and 20 | // blocks all pushes until the connection succeeds. It also 21 | // confirms every outgoing message, so none are lost. 22 | // It doesn't automatically ack each message, but leaves that 23 | // to the parent process, since it is usage-dependent. 24 | // 25 | // Try running this in one terminal, and rabbitmq-server in another. 26 | // 27 | // Stop & restart RabbitMQ to see how the queue reacts. 28 | func publish(done chan struct{}) { 29 | queueName := "job_queue" 30 | addr := "amqp://guest:guest@localhost:5672/" 31 | queue := New(queueName, addr) 32 | message := []byte("message") 33 | 34 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*20)) 35 | defer cancel() 36 | loop: 37 | for { 38 | select { 39 | // Attempt to push a message every 2 seconds 40 | case <-time.After(time.Second * 2): 41 | if err := queue.Push(message); err != nil { 42 | queue.errlog.Printf("push failed: %s\n", err) 43 | } else { 44 | queue.infolog.Println("push succeeded") 45 | } 46 | case <-ctx.Done(): 47 | if err := queue.Close(); err != nil { 48 | queue.errlog.Printf("close failed: %s\n", err) 49 | } 50 | break loop 51 | } 52 | } 53 | 54 | close(done) 55 | } 56 | 57 | func consume(done chan struct{}) { 58 | queueName := "job_queue" 59 | addr := "amqp://guest:guest@localhost:5672/" 60 | queue := New(queueName, addr) 61 | 62 | // Give the connection sometime to set up 63 | <-time.After(time.Second) 64 | 65 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*25) 66 | defer cancel() 67 | 68 | deliveries, err := queue.Consume() 69 | if err != nil { 70 | queue.errlog.Printf("could not start consuming: %s\n", err) 71 | return 72 | } 73 | 74 | // This channel will receive a notification when a channel closed event 75 | // happens. This must be different from Client.notifyChanClose because the 76 | // library sends only one notification and Client.notifyChanClose already has 77 | // a receiver in handleReconnect(). 78 | // Recommended to make it buffered to avoid deadlocks 79 | chClosedCh := make(chan *amqp.Error, 1) 80 | queue.channel.NotifyClose(chClosedCh) 81 | 82 | loop: 83 | for { 84 | select { 85 | case <-ctx.Done(): 86 | err := queue.Close() 87 | if err != nil { 88 | queue.errlog.Printf("close failed: %s\n", err) 89 | } 90 | break loop 91 | 92 | case amqErr := <-chClosedCh: 93 | // This case handles the event of closed channel e.g. abnormal shutdown 94 | queue.errlog.Printf("AMQP Channel closed due to: %s\n", amqErr) 95 | 96 | deliveries, err = queue.Consume() 97 | if err != nil { 98 | // If the AMQP channel is not ready, it will continue the loop. Next 99 | // iteration will enter this case because chClosedCh is closed by the 100 | // library 101 | queue.errlog.Println("error trying to consume, will try again") 102 | continue 103 | } 104 | 105 | // Re-set channel to receive notifications 106 | // The library closes this channel after abnormal shutdown 107 | chClosedCh = make(chan *amqp.Error, 1) 108 | queue.channel.NotifyClose(chClosedCh) 109 | 110 | case delivery := <-deliveries: 111 | // Ack a message every 2 seconds 112 | queue.infolog.Printf("received message: %s\n", delivery.Body) 113 | if err := delivery.Ack(false); err != nil { 114 | queue.errlog.Printf("error acknowledging message: %s\n", err) 115 | } 116 | <-time.After(time.Second * 2) 117 | } 118 | } 119 | 120 | close(done) 121 | } 122 | 123 | // Client is the base struct for handling connection recovery, consumption and 124 | // publishing. Note that this struct has an internal mutex to safeguard against 125 | // data races. As you develop and iterate over this example, you may need to add 126 | // further locks, or safeguards, to keep your application safe from data races 127 | type Client struct { 128 | m *sync.Mutex 129 | queueName string 130 | infolog *log.Logger 131 | errlog *log.Logger 132 | connection *amqp.Connection 133 | channel *amqp.Channel 134 | done chan bool 135 | notifyConnClose chan *amqp.Error 136 | notifyChanClose chan *amqp.Error 137 | notifyConfirm chan amqp.Confirmation 138 | isReady bool 139 | } 140 | 141 | const ( 142 | // When reconnecting to the server after connection failure 143 | reconnectDelay = 5 * time.Second 144 | 145 | // When setting up the channel after a channel exception 146 | reInitDelay = 2 * time.Second 147 | 148 | // When resending messages the server didn't confirm 149 | resendDelay = 5 * time.Second 150 | ) 151 | 152 | var ( 153 | errNotConnected = errors.New("not connected to a server") 154 | errAlreadyClosed = errors.New("already closed: not connected to the server") 155 | errShutdown = errors.New("client is shutting down") 156 | ) 157 | 158 | // New creates a new consumer state instance, and automatically 159 | // attempts to connect to the server. 160 | func New(queueName, addr string) *Client { 161 | client := Client{ 162 | m: &sync.Mutex{}, 163 | infolog: log.New(os.Stdout, "[INFO] ", log.LstdFlags|log.Lmsgprefix), 164 | errlog: log.New(os.Stderr, "[ERROR] ", log.LstdFlags|log.Lmsgprefix), 165 | queueName: queueName, 166 | done: make(chan bool), 167 | } 168 | go client.handleReconnect(addr) 169 | return &client 170 | } 171 | 172 | // handleReconnect will wait for a connection error on 173 | // notifyConnClose, and then continuously attempt to reconnect. 174 | func (client *Client) handleReconnect(addr string) { 175 | for { 176 | client.m.Lock() 177 | client.isReady = false 178 | client.m.Unlock() 179 | 180 | client.infolog.Println("attempting to connect") 181 | 182 | conn, err := client.connect(addr) 183 | if err != nil { 184 | client.errlog.Println("failed to connect. Retrying...") 185 | 186 | select { 187 | case <-client.done: 188 | return 189 | case <-time.After(reconnectDelay): 190 | } 191 | continue 192 | } 193 | 194 | if done := client.handleReInit(conn); done { 195 | break 196 | } 197 | } 198 | } 199 | 200 | // connect will create a new AMQP connection 201 | func (client *Client) connect(addr string) (*amqp.Connection, error) { 202 | conn, err := amqp.Dial(addr) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | client.changeConnection(conn) 208 | client.infolog.Println("connected") 209 | return conn, nil 210 | } 211 | 212 | // handleReInit will wait for a channel error 213 | // and then continuously attempt to re-initialize both channels 214 | func (client *Client) handleReInit(conn *amqp.Connection) bool { 215 | for { 216 | client.m.Lock() 217 | client.isReady = false 218 | client.m.Unlock() 219 | 220 | err := client.init(conn) 221 | if err != nil { 222 | client.errlog.Println("failed to initialize channel, retrying...") 223 | 224 | select { 225 | case <-client.done: 226 | return true 227 | case <-client.notifyConnClose: 228 | client.infolog.Println("connection closed, reconnecting...") 229 | return false 230 | case <-time.After(reInitDelay): 231 | } 232 | continue 233 | } 234 | 235 | select { 236 | case <-client.done: 237 | return true 238 | case <-client.notifyConnClose: 239 | client.infolog.Println("connection closed, reconnecting...") 240 | return false 241 | case <-client.notifyChanClose: 242 | client.infolog.Println("channel closed, re-running init...") 243 | } 244 | } 245 | } 246 | 247 | // init will initialize channel & declare queue 248 | func (client *Client) init(conn *amqp.Connection) error { 249 | ch, err := conn.Channel() 250 | if err != nil { 251 | return err 252 | } 253 | 254 | err = ch.Confirm(false) 255 | if err != nil { 256 | return err 257 | } 258 | _, err = ch.QueueDeclare( 259 | client.queueName, 260 | false, // Durable 261 | false, // Delete when unused 262 | false, // Exclusive 263 | false, // No-wait 264 | nil, // Arguments 265 | ) 266 | if err != nil { 267 | return err 268 | } 269 | 270 | client.changeChannel(ch) 271 | client.m.Lock() 272 | client.isReady = true 273 | client.m.Unlock() 274 | client.infolog.Println("client init done") 275 | 276 | return nil 277 | } 278 | 279 | // changeConnection takes a new connection to the queue, 280 | // and updates the close listener to reflect this. 281 | func (client *Client) changeConnection(connection *amqp.Connection) { 282 | client.connection = connection 283 | client.notifyConnClose = make(chan *amqp.Error, 1) 284 | client.connection.NotifyClose(client.notifyConnClose) 285 | } 286 | 287 | // changeChannel takes a new channel to the queue, 288 | // and updates the channel listeners to reflect this. 289 | func (client *Client) changeChannel(channel *amqp.Channel) { 290 | client.channel = channel 291 | client.notifyChanClose = make(chan *amqp.Error, 1) 292 | client.notifyConfirm = make(chan amqp.Confirmation, 1) 293 | client.channel.NotifyClose(client.notifyChanClose) 294 | client.channel.NotifyPublish(client.notifyConfirm) 295 | } 296 | 297 | // Push will push data onto the queue, and wait for a confirmation. 298 | // This will block until the server sends a confirmation. Errors are 299 | // only returned if the push action itself fails, see UnsafePush. 300 | func (client *Client) Push(data []byte) error { 301 | client.m.Lock() 302 | if !client.isReady { 303 | client.m.Unlock() 304 | return errNotConnected 305 | } 306 | client.m.Unlock() 307 | for { 308 | err := client.UnsafePush(data) 309 | if err != nil { 310 | client.errlog.Println("push failed. Retrying...") 311 | select { 312 | case <-client.done: 313 | return errShutdown 314 | case <-time.After(resendDelay): 315 | } 316 | continue 317 | } 318 | confirm := <-client.notifyConfirm 319 | if confirm.Ack { 320 | client.infolog.Printf("push confirmed [%d]", confirm.DeliveryTag) 321 | return nil 322 | } 323 | } 324 | } 325 | 326 | // UnsafePush will push to the queue without checking for 327 | // confirmation. It returns an error if it fails to connect. 328 | // No guarantees are provided for whether the server will 329 | // receive the message. 330 | func (client *Client) UnsafePush(data []byte) error { 331 | client.m.Lock() 332 | if !client.isReady { 333 | client.m.Unlock() 334 | return errNotConnected 335 | } 336 | client.m.Unlock() 337 | 338 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 339 | defer cancel() 340 | 341 | return client.channel.PublishWithContext( 342 | ctx, 343 | "", // Exchange 344 | client.queueName, // Routing key 345 | false, // Mandatory 346 | false, // Immediate 347 | amqp.Publishing{ 348 | ContentType: "text/plain", 349 | Body: data, 350 | }, 351 | ) 352 | } 353 | 354 | // Consume will continuously put queue items on the channel. 355 | // It is required to call delivery.Ack when it has been 356 | // successfully processed, or delivery.Nack when it fails. 357 | // Ignoring this will cause data to build up on the server. 358 | func (client *Client) Consume() (<-chan amqp.Delivery, error) { 359 | client.m.Lock() 360 | if !client.isReady { 361 | client.m.Unlock() 362 | return nil, errNotConnected 363 | } 364 | client.m.Unlock() 365 | 366 | if err := client.channel.Qos( 367 | 1, // prefetchCount 368 | 0, // prefetchSize 369 | false, // global 370 | ); err != nil { 371 | return nil, err 372 | } 373 | 374 | return client.channel.Consume( 375 | client.queueName, 376 | "", // Consumer 377 | false, // Auto-Ack 378 | false, // Exclusive 379 | false, // No-local 380 | false, // No-Wait 381 | nil, // Args 382 | ) 383 | } 384 | 385 | // Close will cleanly shut down the channel and connection. 386 | func (client *Client) Close() error { 387 | client.m.Lock() 388 | // we read and write isReady in two locations, so we grab the lock and hold onto 389 | // it until we are finished 390 | defer client.m.Unlock() 391 | 392 | if !client.isReady { 393 | return errAlreadyClosed 394 | } 395 | close(client.done) 396 | err := client.channel.Close() 397 | if err != nil { 398 | return err 399 | } 400 | err = client.connection.Close() 401 | if err != nil { 402 | return err 403 | } 404 | 405 | client.isReady = false 406 | return nil 407 | } 408 | 409 | func main() { 410 | publishDone := make(chan struct{}) 411 | consumeDone := make(chan struct{}) 412 | 413 | go publish(publishDone) 414 | go consume(consumeDone) 415 | 416 | select { 417 | case <-publishDone: 418 | log.Println("publishing is done") 419 | } 420 | 421 | select { 422 | case <-consumeDone: 423 | log.Println("consuming is done") 424 | } 425 | 426 | log.Println("exiting") 427 | } 428 | -------------------------------------------------------------------------------- /_examples/consumer/consumer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | // This example declares a durable Exchange, an ephemeral (auto-delete) Queue, 7 | // binds the Queue to the Exchange with a binding key, and consumes every 8 | // message published to that Exchange with that routing key. 9 | package main 10 | 11 | import ( 12 | "flag" 13 | "fmt" 14 | "log" 15 | "os" 16 | "os/signal" 17 | "syscall" 18 | "time" 19 | 20 | amqp "github.com/rabbitmq/amqp091-go" 21 | ) 22 | 23 | var ( 24 | uri = flag.String("uri", "amqp://guest:guest@localhost:5672/", "AMQP URI") 25 | exchange = flag.String("exchange", "test-exchange", "Durable, non-auto-deleted AMQP exchange name") 26 | exchangeType = flag.String("exchange-type", "direct", "Exchange type - direct|fanout|topic|x-custom") 27 | queue = flag.String("queue", "test-queue", "Ephemeral AMQP queue name") 28 | bindingKey = flag.String("key", "test-key", "AMQP binding key") 29 | consumerTag = flag.String("consumer-tag", "simple-consumer", "AMQP consumer tag (should not be blank)") 30 | lifetime = flag.Duration("lifetime", 5*time.Second, "lifetime of process before shutdown (0s=infinite)") 31 | verbose = flag.Bool("verbose", true, "enable verbose output of message data") 32 | autoAck = flag.Bool("auto_ack", false, "enable message auto-ack") 33 | ErrLog = log.New(os.Stderr, "[ERROR] ", log.LstdFlags|log.Lmsgprefix) 34 | Log = log.New(os.Stdout, "[INFO] ", log.LstdFlags|log.Lmsgprefix) 35 | deliveryCount int = 0 36 | ) 37 | 38 | func init() { 39 | flag.Parse() 40 | } 41 | 42 | func main() { 43 | c, err := NewConsumer(*uri, *exchange, *exchangeType, *queue, *bindingKey, *consumerTag) 44 | if err != nil { 45 | ErrLog.Fatalf("%s", err) 46 | } 47 | 48 | SetupCloseHandler(c) 49 | 50 | if *lifetime > 0 { 51 | Log.Printf("running for %s", *lifetime) 52 | time.Sleep(*lifetime) 53 | } else { 54 | Log.Printf("running until Consumer is done") 55 | <-c.done 56 | } 57 | 58 | Log.Printf("shutting down") 59 | 60 | if err := c.Shutdown(); err != nil { 61 | ErrLog.Fatalf("error during shutdown: %s", err) 62 | } 63 | } 64 | 65 | type Consumer struct { 66 | conn *amqp.Connection 67 | channel *amqp.Channel 68 | tag string 69 | done chan error 70 | } 71 | 72 | func SetupCloseHandler(consumer *Consumer) { 73 | c := make(chan os.Signal, 2) 74 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 75 | go func() { 76 | <-c 77 | Log.Printf("Ctrl+C pressed in Terminal") 78 | if err := consumer.Shutdown(); err != nil { 79 | ErrLog.Fatalf("error during shutdown: %s", err) 80 | } 81 | os.Exit(0) 82 | }() 83 | } 84 | 85 | func NewConsumer(amqpURI, exchange, exchangeType, queueName, key, ctag string) (*Consumer, error) { 86 | c := &Consumer{ 87 | conn: nil, 88 | channel: nil, 89 | tag: ctag, 90 | done: make(chan error), 91 | } 92 | 93 | var err error 94 | 95 | config := amqp.Config{Properties: amqp.NewConnectionProperties()} 96 | config.Properties.SetClientConnectionName("sample-consumer") 97 | Log.Printf("dialing %q", amqpURI) 98 | c.conn, err = amqp.DialConfig(amqpURI, config) 99 | if err != nil { 100 | return nil, fmt.Errorf("Dial: %s", err) 101 | } 102 | 103 | go func() { 104 | Log.Printf("closing: %s", <-c.conn.NotifyClose(make(chan *amqp.Error))) 105 | }() 106 | 107 | Log.Printf("got Connection, getting Channel") 108 | c.channel, err = c.conn.Channel() 109 | if err != nil { 110 | return nil, fmt.Errorf("Channel: %s", err) 111 | } 112 | 113 | Log.Printf("got Channel, declaring Exchange (%q)", exchange) 114 | if err = c.channel.ExchangeDeclare( 115 | exchange, // name of the exchange 116 | exchangeType, // type 117 | true, // durable 118 | false, // delete when complete 119 | false, // internal 120 | false, // noWait 121 | nil, // arguments 122 | ); err != nil { 123 | return nil, fmt.Errorf("Exchange Declare: %s", err) 124 | } 125 | 126 | Log.Printf("declared Exchange, declaring Queue %q", queueName) 127 | queue, err := c.channel.QueueDeclare( 128 | queueName, // name of the queue 129 | true, // durable 130 | false, // delete when unused 131 | false, // exclusive 132 | false, // noWait 133 | nil, // arguments 134 | ) 135 | if err != nil { 136 | return nil, fmt.Errorf("Queue Declare: %s", err) 137 | } 138 | 139 | Log.Printf("declared Queue (%q %d messages, %d consumers), binding to Exchange (key %q)", 140 | queue.Name, queue.Messages, queue.Consumers, key) 141 | 142 | if err = c.channel.QueueBind( 143 | queue.Name, // name of the queue 144 | key, // bindingKey 145 | exchange, // sourceExchange 146 | false, // noWait 147 | nil, // arguments 148 | ); err != nil { 149 | return nil, fmt.Errorf("Queue Bind: %s", err) 150 | } 151 | 152 | Log.Printf("Queue bound to Exchange, starting Consume (consumer tag %q)", c.tag) 153 | deliveries, err := c.channel.Consume( 154 | queue.Name, // name 155 | c.tag, // consumerTag, 156 | *autoAck, // autoAck 157 | false, // exclusive 158 | false, // noLocal 159 | false, // noWait 160 | nil, // arguments 161 | ) 162 | if err != nil { 163 | return nil, fmt.Errorf("Queue Consume: %s", err) 164 | } 165 | 166 | go handle(deliveries, c.done) 167 | 168 | return c, nil 169 | } 170 | 171 | func (c *Consumer) Shutdown() error { 172 | // will close() the deliveries channel 173 | if err := c.channel.Cancel(c.tag, true); err != nil { 174 | return fmt.Errorf("Consumer cancel failed: %s", err) 175 | } 176 | 177 | if err := c.conn.Close(); err != nil { 178 | return fmt.Errorf("AMQP connection close error: %s", err) 179 | } 180 | 181 | defer Log.Printf("AMQP shutdown OK") 182 | 183 | // wait for handle() to exit 184 | return <-c.done 185 | } 186 | 187 | func handle(deliveries <-chan amqp.Delivery, done chan error) { 188 | cleanup := func() { 189 | Log.Printf("handle: deliveries channel closed") 190 | done <- nil 191 | } 192 | 193 | defer cleanup() 194 | 195 | for d := range deliveries { 196 | deliveryCount++ 197 | if *verbose == true { 198 | Log.Printf( 199 | "got %dB delivery: [%v] %q", 200 | len(d.Body), 201 | d.DeliveryTag, 202 | d.Body, 203 | ) 204 | } else { 205 | if deliveryCount%65536 == 0 { 206 | Log.Printf("delivery count %d", deliveryCount) 207 | } 208 | } 209 | if *autoAck == false { 210 | d.Ack(false) 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /_examples/producer/producer.go: -------------------------------------------------------------------------------- 1 | // This example declares a durable exchange, and publishes one messages to that 2 | // exchange. This example allows up to 8 outstanding publisher confirmations 3 | // before blocking publishing. 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | amqp "github.com/rabbitmq/amqp091-go" 15 | ) 16 | 17 | var ( 18 | uri = flag.String("uri", "amqp://guest:guest@localhost:5672/", "AMQP URI") 19 | exchange = flag.String("exchange", "test-exchange", "Durable AMQP exchange name") 20 | exchangeType = flag.String("exchange-type", "direct", "Exchange type - direct|fanout|topic|x-custom") 21 | queue = flag.String("queue", "test-queue", "Ephemeral AMQP queue name") 22 | routingKey = flag.String("key", "test-key", "AMQP routing key") 23 | body = flag.String("body", "foobar", "Body of message") 24 | continuous = flag.Bool("continuous", false, "Keep publishing messages at a 1msg/sec rate") 25 | WarnLog = log.New(os.Stderr, "[WARNING] ", log.LstdFlags|log.Lmsgprefix) 26 | ErrLog = log.New(os.Stderr, "[ERROR] ", log.LstdFlags|log.Lmsgprefix) 27 | Log = log.New(os.Stdout, "[INFO] ", log.LstdFlags|log.Lmsgprefix) 28 | ) 29 | 30 | func init() { 31 | flag.Parse() 32 | } 33 | 34 | func main() { 35 | exitCh := make(chan struct{}) 36 | confirmsCh := make(chan *amqp.DeferredConfirmation) 37 | confirmsDoneCh := make(chan struct{}) 38 | // Note: this is a buffered channel so that indicating OK to 39 | // publish does not block the confirm handler 40 | publishOkCh := make(chan struct{}, 1) 41 | 42 | setupCloseHandler(exitCh) 43 | 44 | startConfirmHandler(publishOkCh, confirmsCh, confirmsDoneCh, exitCh) 45 | 46 | publish(publishOkCh, confirmsCh, confirmsDoneCh, exitCh) 47 | } 48 | 49 | func setupCloseHandler(exitCh chan struct{}) { 50 | c := make(chan os.Signal, 2) 51 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 52 | go func() { 53 | <-c 54 | Log.Printf("close handler: Ctrl+C pressed in Terminal") 55 | close(exitCh) 56 | }() 57 | } 58 | 59 | func publish(publishOkCh <-chan struct{}, confirmsCh chan<- *amqp.DeferredConfirmation, confirmsDoneCh <-chan struct{}, exitCh chan struct{}) { 60 | config := amqp.Config{ 61 | Vhost: "/", 62 | Properties: amqp.NewConnectionProperties(), 63 | } 64 | config.Properties.SetClientConnectionName("producer-with-confirms") 65 | 66 | Log.Printf("producer: dialing %s", *uri) 67 | conn, err := amqp.DialConfig(*uri, config) 68 | if err != nil { 69 | ErrLog.Fatalf("producer: error in dial: %s", err) 70 | } 71 | defer conn.Close() 72 | 73 | Log.Println("producer: got Connection, getting Channel") 74 | channel, err := conn.Channel() 75 | if err != nil { 76 | ErrLog.Fatalf("error getting a channel: %s", err) 77 | } 78 | defer channel.Close() 79 | 80 | Log.Printf("producer: declaring exchange") 81 | if err := channel.ExchangeDeclare( 82 | *exchange, // name 83 | *exchangeType, // type 84 | true, // durable 85 | false, // auto-delete 86 | false, // internal 87 | false, // noWait 88 | nil, // arguments 89 | ); err != nil { 90 | ErrLog.Fatalf("producer: Exchange Declare: %s", err) 91 | } 92 | 93 | Log.Printf("producer: declaring queue '%s'", *queue) 94 | queue, err := channel.QueueDeclare( 95 | *queue, // name of the queue 96 | true, // durable 97 | false, // delete when unused 98 | false, // exclusive 99 | false, // noWait 100 | nil, // arguments 101 | ) 102 | if err == nil { 103 | Log.Printf("producer: declared queue (%q %d messages, %d consumers), binding to Exchange (key %q)", 104 | queue.Name, queue.Messages, queue.Consumers, *routingKey) 105 | } else { 106 | ErrLog.Fatalf("producer: Queue Declare: %s", err) 107 | } 108 | 109 | Log.Printf("producer: declaring binding") 110 | if err := channel.QueueBind(queue.Name, *routingKey, *exchange, false, nil); err != nil { 111 | ErrLog.Fatalf("producer: Queue Bind: %s", err) 112 | } 113 | 114 | // Reliable publisher confirms require confirm.select support from the 115 | // connection. 116 | Log.Printf("producer: enabling publisher confirms.") 117 | if err := channel.Confirm(false); err != nil { 118 | ErrLog.Fatalf("producer: channel could not be put into confirm mode: %s", err) 119 | } 120 | 121 | for { 122 | canPublish := false 123 | Log.Println("producer: waiting on the OK to publish...") 124 | for { 125 | select { 126 | case <-confirmsDoneCh: 127 | Log.Println("producer: stopping, all confirms seen") 128 | return 129 | case <-publishOkCh: 130 | Log.Println("producer: got the OK to publish") 131 | canPublish = true 132 | break 133 | case <-time.After(time.Second): 134 | WarnLog.Println("producer: still waiting on the OK to publish...") 135 | continue 136 | } 137 | if canPublish { 138 | break 139 | } 140 | } 141 | 142 | Log.Printf("producer: publishing %dB body (%q)", len(*body), *body) 143 | dConfirmation, err := channel.PublishWithDeferredConfirm( 144 | *exchange, 145 | *routingKey, 146 | true, 147 | false, 148 | amqp.Publishing{ 149 | Headers: amqp.Table{}, 150 | ContentType: "text/plain", 151 | ContentEncoding: "", 152 | DeliveryMode: amqp.Persistent, 153 | Priority: 0, 154 | AppId: "sequential-producer", 155 | Body: []byte(*body), 156 | }, 157 | ) 158 | if err != nil { 159 | ErrLog.Fatalf("producer: error in publish: %s", err) 160 | } 161 | 162 | select { 163 | case <-confirmsDoneCh: 164 | Log.Println("producer: stopping, all confirms seen") 165 | return 166 | case confirmsCh <- dConfirmation: 167 | Log.Println("producer: delivered deferred confirm to handler") 168 | break 169 | } 170 | 171 | select { 172 | case <-confirmsDoneCh: 173 | Log.Println("producer: stopping, all confirms seen") 174 | return 175 | case <-time.After(time.Millisecond * 250): 176 | if *continuous { 177 | continue 178 | } else { 179 | Log.Println("producer: initiating stop") 180 | close(exitCh) 181 | select { 182 | case <-confirmsDoneCh: 183 | Log.Println("producer: stopping, all confirms seen") 184 | return 185 | case <-time.After(time.Second * 10): 186 | WarnLog.Println("producer: may be stopping with outstanding confirmations") 187 | return 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | func startConfirmHandler(publishOkCh chan<- struct{}, confirmsCh <-chan *amqp.DeferredConfirmation, confirmsDoneCh chan struct{}, exitCh <-chan struct{}) { 195 | go func() { 196 | confirms := make(map[uint64]*amqp.DeferredConfirmation) 197 | 198 | for { 199 | select { 200 | case <-exitCh: 201 | exitConfirmHandler(confirms, confirmsDoneCh) 202 | return 203 | default: 204 | break 205 | } 206 | 207 | outstandingConfirmationCount := len(confirms) 208 | 209 | // Note: 8 is arbitrary, you may wish to allow more outstanding confirms before blocking publish 210 | if outstandingConfirmationCount <= 8 { 211 | select { 212 | case publishOkCh <- struct{}{}: 213 | Log.Println("confirm handler: sent OK to publish") 214 | case <-time.After(time.Second * 5): 215 | WarnLog.Println("confirm handler: timeout indicating OK to publish (this should never happen!)") 216 | } 217 | } else { 218 | WarnLog.Printf("confirm handler: waiting on %d outstanding confirmations, blocking publish", outstandingConfirmationCount) 219 | } 220 | 221 | select { 222 | case confirmation := <-confirmsCh: 223 | dtag := confirmation.DeliveryTag 224 | confirms[dtag] = confirmation 225 | case <-exitCh: 226 | exitConfirmHandler(confirms, confirmsDoneCh) 227 | return 228 | } 229 | 230 | checkConfirmations(confirms) 231 | } 232 | }() 233 | } 234 | 235 | func exitConfirmHandler(confirms map[uint64]*amqp.DeferredConfirmation, confirmsDoneCh chan struct{}) { 236 | Log.Println("confirm handler: exit requested") 237 | waitConfirmations(confirms) 238 | close(confirmsDoneCh) 239 | Log.Println("confirm handler: exiting") 240 | } 241 | 242 | func checkConfirmations(confirms map[uint64]*amqp.DeferredConfirmation) { 243 | Log.Printf("confirm handler: checking %d outstanding confirmations", len(confirms)) 244 | for k, v := range confirms { 245 | if v.Acked() { 246 | Log.Printf("confirm handler: confirmed delivery with tag: %d", k) 247 | delete(confirms, k) 248 | } 249 | } 250 | } 251 | 252 | func waitConfirmations(confirms map[uint64]*amqp.DeferredConfirmation) { 253 | Log.Printf("confirm handler: waiting on %d outstanding confirmations", len(confirms)) 254 | 255 | checkConfirmations(confirms) 256 | 257 | for k, v := range confirms { 258 | select { 259 | case <-v.Done(): 260 | Log.Printf("confirm handler: confirmed delivery with tag: %d", k) 261 | delete(confirms, k) 262 | case <-time.After(time.Second): 263 | WarnLog.Printf("confirm handler: did not receive confirmation for tag %d", k) 264 | } 265 | } 266 | 267 | outstandingConfirmationCount := len(confirms) 268 | if outstandingConfirmationCount > 0 { 269 | ErrLog.Printf("confirm handler: exiting with %d outstanding confirmations", outstandingConfirmationCount) 270 | } else { 271 | Log.Println("confirm handler: done waiting on outstanding confirmations") 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /_examples/pubsub/pubsub.go: -------------------------------------------------------------------------------- 1 | // Command pubsub is an example of a fanout exchange with dynamic reliable 2 | // membership, reading from stdin, writing to stdout. 3 | // 4 | // This example shows how to implement reconnect logic independent from a 5 | // publish/subscribe loop with bridges to application types. 6 | 7 | package main 8 | 9 | import ( 10 | "bufio" 11 | "context" 12 | "crypto/sha1" 13 | "flag" 14 | "fmt" 15 | "io" 16 | "log" 17 | "os" 18 | 19 | amqp "github.com/rabbitmq/amqp091-go" 20 | ) 21 | 22 | var url = flag.String("url", "amqp:///", "AMQP url for both the publisher and subscriber") 23 | 24 | // exchange binds the publishers to the subscribers 25 | const exchange = "pubsub" 26 | 27 | // message is the application type for a message. This can contain identity, 28 | // or a reference to the receiver chan for further demuxing. 29 | type message []byte 30 | 31 | // session composes an amqp.Connection with an amqp.Channel 32 | type session struct { 33 | *amqp.Connection 34 | *amqp.Channel 35 | } 36 | 37 | // Close tears the connection down, taking the channel with it. 38 | func (s session) Close() error { 39 | if s.Connection == nil { 40 | return nil 41 | } 42 | return s.Connection.Close() 43 | } 44 | 45 | // redial continually connects to the URL, exiting the program when no longer possible 46 | func redial(ctx context.Context, url string) chan chan session { 47 | sessions := make(chan chan session) 48 | 49 | go func() { 50 | sess := make(chan session) 51 | defer close(sessions) 52 | 53 | for { 54 | select { 55 | case sessions <- sess: 56 | case <-ctx.Done(): 57 | log.Println("shutting down session factory") 58 | return 59 | } 60 | 61 | conn, err := amqp.Dial(url) 62 | if err != nil { 63 | log.Fatalf("cannot (re)dial: %v: %q", err, url) 64 | } 65 | 66 | ch, err := conn.Channel() 67 | if err != nil { 68 | log.Fatalf("cannot create channel: %v", err) 69 | } 70 | 71 | if err := ch.ExchangeDeclare(exchange, "fanout", false, true, false, false, nil); err != nil { 72 | log.Fatalf("cannot declare fanout exchange: %v", err) 73 | } 74 | 75 | select { 76 | case sess <- session{conn, ch}: 77 | case <-ctx.Done(): 78 | log.Println("shutting down new session") 79 | return 80 | } 81 | } 82 | }() 83 | 84 | return sessions 85 | } 86 | 87 | // publish publishes messages to a reconnecting session to a fanout exchange. 88 | // It receives from the application specific source of messages. 89 | func publish(sessions chan chan session, messages <-chan message) { 90 | pending := make(chan message, 1) 91 | 92 | for session := range sessions { 93 | var ( 94 | running bool 95 | reading = messages 96 | confirm = make(chan amqp.Confirmation, 1) 97 | ) 98 | 99 | pub := <-session 100 | 101 | // publisher confirms for this channel/connection 102 | if err := pub.Confirm(false); err != nil { 103 | log.Printf("publisher confirms not supported") 104 | close(confirm) // confirms not supported, simulate by always nacking 105 | } else { 106 | pub.NotifyPublish(confirm) 107 | } 108 | 109 | log.Printf("publishing...") 110 | 111 | Publish: 112 | for { 113 | var body message 114 | select { 115 | case confirmed, ok := <-confirm: 116 | if !ok { 117 | pub.Close() 118 | break Publish 119 | } 120 | if !confirmed.Ack { 121 | log.Printf("nack message %d, body: %q", confirmed.DeliveryTag, string(body)) 122 | } 123 | reading = messages 124 | 125 | case body = <-pending: 126 | routingKey := "ignored for fanout exchanges, application dependent for other exchanges" 127 | err := pub.Publish(exchange, routingKey, false, false, amqp.Publishing{ 128 | Body: body, 129 | }) 130 | // Retry failed delivery on the next session 131 | if err != nil { 132 | pending <- body 133 | pub.Close() 134 | break Publish 135 | } 136 | 137 | case body, running = <-reading: 138 | // all messages consumed 139 | if !running { 140 | return 141 | } 142 | // work on pending delivery until ack'd 143 | pending <- body 144 | reading = nil 145 | } 146 | } 147 | } 148 | } 149 | 150 | // identity returns the same host/process unique string for the lifetime of 151 | // this process so that subscriber reconnections reuse the same queue name. 152 | func identity() string { 153 | hostname, err := os.Hostname() 154 | h := sha1.New() 155 | fmt.Fprint(h, hostname) 156 | fmt.Fprint(h, err) 157 | fmt.Fprint(h, os.Getpid()) 158 | return fmt.Sprintf("%x", h.Sum(nil)) 159 | } 160 | 161 | // subscribe consumes deliveries from an exclusive queue from a fanout exchange and sends to the application specific messages chan. 162 | func subscribe(sessions chan chan session, messages chan<- message) { 163 | queue := identity() 164 | 165 | for session := range sessions { 166 | sub := <-session 167 | 168 | if _, err := sub.QueueDeclare(queue, false, true, true, false, nil); err != nil { 169 | log.Printf("cannot consume from exclusive queue: %q, %v", queue, err) 170 | return 171 | } 172 | 173 | routingKey := "application specific routing key for fancy topologies" 174 | if err := sub.QueueBind(queue, routingKey, exchange, false, nil); err != nil { 175 | log.Printf("cannot consume without a binding to exchange: %q, %v", exchange, err) 176 | return 177 | } 178 | 179 | deliveries, err := sub.Consume(queue, "", false, true, false, false, nil) 180 | if err != nil { 181 | log.Printf("cannot consume from: %q, %v", queue, err) 182 | return 183 | } 184 | 185 | log.Printf("subscribed...") 186 | 187 | for msg := range deliveries { 188 | messages <- msg.Body 189 | sub.Ack(msg.DeliveryTag, false) 190 | } 191 | sub.Close() 192 | } 193 | } 194 | 195 | // read is this application's translation to the message format, scanning from 196 | // stdin. 197 | func read(r io.Reader) <-chan message { 198 | lines := make(chan message) 199 | go func() { 200 | defer close(lines) 201 | scan := bufio.NewScanner(r) 202 | for scan.Scan() { 203 | lines <- scan.Bytes() 204 | } 205 | }() 206 | return lines 207 | } 208 | 209 | // write is this application's subscriber of application messages, printing to 210 | // stdout. 211 | func write(w io.Writer) chan<- message { 212 | lines := make(chan message) 213 | go func() { 214 | for line := range lines { 215 | fmt.Fprintln(w, string(line)) 216 | } 217 | }() 218 | return lines 219 | } 220 | 221 | func main() { 222 | flag.Parse() 223 | 224 | ctx, done := context.WithCancel(context.Background()) 225 | 226 | go func() { 227 | publish(redial(ctx, *url), read(os.Stdin)) 228 | done() 229 | }() 230 | 231 | go func() { 232 | subscribe(redial(ctx, *url), write(os.Stdout)) 233 | done() 234 | }() 235 | 236 | <-ctx.Done() 237 | } 238 | -------------------------------------------------------------------------------- /allocator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "fmt" 10 | "math/big" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | free = 0 16 | allocated = 1 17 | ) 18 | 19 | // allocator maintains a bitset of allocated numbers. 20 | type allocator struct { 21 | pool *big.Int 22 | follow int 23 | low int 24 | high int 25 | } 26 | 27 | // NewAllocator reserves and frees integers out of a range between low and 28 | // high. 29 | // 30 | // O(N) worst case space used, where N is maximum allocated, divided by 31 | // sizeof(big.Word) 32 | func newAllocator(low, high int) *allocator { 33 | return &allocator{ 34 | pool: big.NewInt(0), 35 | follow: low, 36 | low: low, 37 | high: high, 38 | } 39 | } 40 | 41 | // String returns a string describing the contents of the allocator like 42 | // "allocator[low..high] reserved..until" 43 | // 44 | // O(N) where N is high-low 45 | func (a *allocator) String() string { 46 | var b strings.Builder 47 | fmt.Fprintf(&b, "allocator[%d..%d]", a.low, a.high) 48 | 49 | for low := a.low; low <= a.high; low++ { 50 | high := low 51 | for a.reserved(high) && high <= a.high { 52 | high++ 53 | } 54 | 55 | if high > low+1 { 56 | fmt.Fprintf(&b, " %d..%d", low, high-1) 57 | } else if high > low { 58 | fmt.Fprintf(&b, " %d", high-1) 59 | } 60 | 61 | low = high 62 | } 63 | return b.String() 64 | } 65 | 66 | // Next reserves and returns the next available number out of the range between 67 | // low and high. If no number is available, false is returned. 68 | // 69 | // O(N) worst case runtime where N is allocated, but usually O(1) due to a 70 | // rolling index into the oldest allocation. 71 | func (a *allocator) next() (int, bool) { 72 | wrapped := a.follow 73 | defer func() { 74 | // make a.follow point to next value 75 | if a.follow == a.high { 76 | a.follow = a.low 77 | } else { 78 | a.follow += 1 79 | } 80 | }() 81 | 82 | // Find trailing bit 83 | for ; a.follow <= a.high; a.follow++ { 84 | if a.reserve(a.follow) { 85 | return a.follow, true 86 | } 87 | } 88 | 89 | // Find preceding free'd pool 90 | a.follow = a.low 91 | 92 | for ; a.follow < wrapped; a.follow++ { 93 | if a.reserve(a.follow) { 94 | return a.follow, true 95 | } 96 | } 97 | 98 | return 0, false 99 | } 100 | 101 | // reserve claims the bit if it is not already claimed, returning true if 102 | // successfully claimed. 103 | func (a *allocator) reserve(n int) bool { 104 | if a.reserved(n) { 105 | return false 106 | } 107 | a.pool.SetBit(a.pool, n-a.low, allocated) 108 | return true 109 | } 110 | 111 | // reserved returns true if the integer has been allocated 112 | func (a *allocator) reserved(n int) bool { 113 | return a.pool.Bit(n-a.low) == allocated 114 | } 115 | 116 | // release frees the use of the number for another allocation 117 | func (a *allocator) release(n int) { 118 | a.pool.SetBit(a.pool, n-a.low, free) 119 | } 120 | -------------------------------------------------------------------------------- /allocator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "math/rand" 10 | "testing" 11 | ) 12 | 13 | func TestAllocatorFirstShouldBeTheLow(t *testing.T) { 14 | n, ok := newAllocator(1, 2).next() 15 | if !ok { 16 | t.Fatalf("expected to allocate between 1 and 2") 17 | } 18 | 19 | if want, got := 1, n; want != got { 20 | t.Fatalf("expected to first allocation to be 1") 21 | } 22 | } 23 | 24 | func TestAllocatorShouldBeBoundByHigh(t *testing.T) { 25 | a := newAllocator(1, 2) 26 | 27 | if n, ok := a.next(); n != 1 || !ok { 28 | t.Fatalf("expected to allocate between 1 and 2, got %d, %v", n, ok) 29 | } 30 | if n, ok := a.next(); n != 2 || !ok { 31 | t.Fatalf("expected to allocate between 1 and 2, got %d, %v", n, ok) 32 | } 33 | if _, ok := a.next(); ok { 34 | t.Fatalf("expected not to allocate outside of 1 and 2") 35 | } 36 | } 37 | 38 | func TestAllocatorStringShouldIncludeAllocatedRanges(t *testing.T) { 39 | a := newAllocator(1, 10) 40 | a.reserve(1) 41 | a.reserve(2) 42 | a.reserve(3) 43 | a.reserve(5) 44 | a.reserve(6) 45 | a.reserve(8) 46 | a.reserve(10) 47 | 48 | if want, got := "allocator[1..10] 1..3 5..6 8 10", a.String(); want != got { 49 | t.Fatalf("expected String of %q, got %q", want, got) 50 | } 51 | } 52 | 53 | func TestAllocatorShouldReuseReleased(t *testing.T) { 54 | a := newAllocator(1, 2) 55 | 56 | first, _ := a.next() 57 | if want, got := 1, first; want != got { 58 | t.Fatalf("expected allocation to be %d, got: %d", want, got) 59 | } 60 | 61 | second, _ := a.next() 62 | if want, got := 2, second; want != got { 63 | t.Fatalf("expected allocation to be %d, got: %d", want, got) 64 | } 65 | 66 | a.release(first) 67 | 68 | third, _ := a.next() 69 | if want, got := first, third; want != got { 70 | t.Fatalf("expected third allocation to be %d, got: %d", want, got) 71 | } 72 | 73 | _, ok := a.next() 74 | if want, got := false, ok; want != got { 75 | t.Fatalf("expected fourth allocation to saturate the pool") 76 | } 77 | } 78 | 79 | func TestAllocatorShouldNotReuseEarly(t *testing.T) { 80 | a := newAllocator(1, 2) 81 | 82 | first, _ := a.next() 83 | if want, got := 1, first; want != got { 84 | t.Fatalf("expected allocation to be %d, got: %d", want, got) 85 | } 86 | 87 | a.release(first) 88 | 89 | second, _ := a.next() 90 | if want, got := 2, second; want != got { 91 | t.Fatalf("expected second allocation to be %d, got: %d", want, got) 92 | } 93 | 94 | third, _ := a.next() 95 | if want, got := first, third; want != got { 96 | t.Fatalf("expected third allocation to be %d, got: %d", want, got) 97 | } 98 | } 99 | 100 | func TestAllocatorReleasesKeepUpWithAllocationsForAllSizes(t *testing.T) { 101 | if testing.Short() { 102 | t.Skip() 103 | } 104 | 105 | const runs = 5 106 | const max = 13 107 | 108 | for lim := 1; lim < 2<= lim { // fills the allocator 113 | a.release(int(rand.Int63n(int64(lim)))) 114 | } 115 | if _, ok := a.next(); !ok { 116 | t.Fatalf("expected %d runs of random release of size %d not to fail on allocation %d", runs, lim, i) 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "bytes" 10 | "fmt" 11 | ) 12 | 13 | // Authentication interface provides a means for different SASL authentication 14 | // mechanisms to be used during connection tuning. 15 | type Authentication interface { 16 | Mechanism() string 17 | Response() string 18 | } 19 | 20 | // PlainAuth is a similar to Basic Auth in HTTP. 21 | type PlainAuth struct { 22 | Username string 23 | Password string 24 | } 25 | 26 | // Mechanism returns "PLAIN" 27 | func (auth *PlainAuth) Mechanism() string { 28 | return "PLAIN" 29 | } 30 | 31 | // Response returns the null character delimited encoding for the SASL PLAIN Mechanism. 32 | func (auth *PlainAuth) Response() string { 33 | return fmt.Sprintf("\000%s\000%s", auth.Username, auth.Password) 34 | } 35 | 36 | // AMQPlainAuth is similar to PlainAuth 37 | type AMQPlainAuth struct { 38 | Username string 39 | Password string 40 | } 41 | 42 | // Mechanism returns "AMQPLAIN" 43 | func (auth *AMQPlainAuth) Mechanism() string { 44 | return "AMQPLAIN" 45 | } 46 | 47 | // Response returns an AMQP encoded credentials table, without the field table size. 48 | func (auth *AMQPlainAuth) Response() string { 49 | var buf bytes.Buffer 50 | table := Table{"LOGIN": auth.Username, "PASSWORD": auth.Password} 51 | if err := writeTable(&buf, table); err != nil { 52 | return "" 53 | } 54 | return buf.String()[4:] 55 | } 56 | 57 | // ExternalAuth for RabbitMQ-auth-mechanism-ssl. 58 | type ExternalAuth struct{} 59 | 60 | // Mechanism returns "EXTERNAL" 61 | func (*ExternalAuth) Mechanism() string { 62 | return "EXTERNAL" 63 | } 64 | 65 | // Response returns an AMQP encoded credentials table, without the field table size. 66 | func (*ExternalAuth) Response() string { 67 | return "\000*\000*" 68 | } 69 | 70 | // Finds the first mechanism preferred by the client that the server supports. 71 | func pickSASLMechanism(client []Authentication, serverMechanisms []string) (auth Authentication, ok bool) { 72 | for _, auth = range client { 73 | for _, mech := range serverMechanisms { 74 | if auth.Mechanism() == mech { 75 | return auth, true 76 | } 77 | } 78 | } 79 | 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package amqp091 2 | 3 | import "testing" 4 | 5 | func TestPlainAuth(t *testing.T) { 6 | auth := &PlainAuth{ 7 | Username: "user", 8 | Password: "pass", 9 | } 10 | 11 | if auth.Mechanism() != "PLAIN" { 12 | t.Errorf("Expected PLAIN, got %s", auth.Mechanism()) 13 | } 14 | 15 | expectedResponse := "\000user\000pass" 16 | if auth.Response() != expectedResponse { 17 | t.Errorf("Expected %s, got %s", expectedResponse, auth.Response()) 18 | } 19 | } 20 | 21 | func TestExternalAuth(t *testing.T) { 22 | auth := &ExternalAuth{} 23 | 24 | if auth.Mechanism() != "EXTERNAL" { 25 | t.Errorf("Expected EXTERNAL, got %s", auth.Mechanism()) 26 | } 27 | 28 | if auth.Response() != "\000*\000*" { 29 | t.Errorf("Expected \000*\000*, got %s", auth.Response()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Creates the CA, server and client certs to be used by tls_test.go 4 | # http://www.rabbitmq.com/ssl.html 5 | # 6 | # Copy stdout into the const section of tls_test.go or use for RabbitMQ 7 | # 8 | root=$PWD/certs 9 | 10 | if [ -f $root/ca/serial ]; then 11 | echo >&2 "Previous installation found" 12 | echo >&2 "Remove $root/ca and rerun to overwrite" 13 | exit 1 14 | fi 15 | 16 | mkdir -p $root/ca/private 17 | mkdir -p $root/ca/certs 18 | mkdir -p $root/server 19 | mkdir -p $root/client 20 | 21 | cd $root/ca 22 | 23 | chmod 700 private 24 | touch index.txt 25 | echo 'unique_subject = no' > index.txt.attr 26 | echo '01' > serial 27 | echo >openssl.cnf ' 28 | [ ca ] 29 | default_ca = testca 30 | 31 | [ testca ] 32 | dir = . 33 | certificate = $dir/cacert.pem 34 | database = $dir/index.txt 35 | new_certs_dir = $dir/certs 36 | private_key = $dir/private/cakey.pem 37 | serial = $dir/serial 38 | 39 | default_crl_days = 7 40 | default_days = 3650 41 | default_md = sha256 42 | 43 | policy = testca_policy 44 | x509_extensions = certificate_extensions 45 | 46 | [ testca_policy ] 47 | commonName = supplied 48 | stateOrProvinceName = optional 49 | countryName = optional 50 | emailAddress = optional 51 | organizationName = optional 52 | organizationalUnitName = optional 53 | 54 | [ certificate_extensions ] 55 | basicConstraints = CA:false 56 | 57 | [ req ] 58 | default_bits = 2048 59 | default_keyfile = ./private/cakey.pem 60 | default_md = sha256 61 | prompt = yes 62 | distinguished_name = root_ca_distinguished_name 63 | x509_extensions = root_ca_extensions 64 | 65 | [ root_ca_distinguished_name ] 66 | commonName = hostname 67 | 68 | [ root_ca_extensions ] 69 | basicConstraints = CA:true 70 | keyUsage = keyCertSign, cRLSign 71 | 72 | [ client_ca_extensions ] 73 | basicConstraints = CA:false 74 | keyUsage = keyEncipherment,digitalSignature 75 | extendedKeyUsage = 1.3.6.1.5.5.7.3.2 76 | 77 | [ server_ca_extensions ] 78 | basicConstraints = CA:false 79 | keyUsage = keyEncipherment,digitalSignature 80 | extendedKeyUsage = 1.3.6.1.5.5.7.3.1 81 | subjectAltName = @alt_names 82 | 83 | [ alt_names ] 84 | IP.1 = 127.0.0.1 85 | ' 86 | 87 | openssl req \ 88 | -x509 \ 89 | -nodes \ 90 | -config openssl.cnf \ 91 | -newkey rsa:2048 \ 92 | -days 3650 \ 93 | -subj "/CN=MyTestCA/" \ 94 | -out cacert.pem \ 95 | -outform PEM 96 | 97 | openssl x509 \ 98 | -in cacert.pem \ 99 | -out cacert.cer \ 100 | -outform DER 101 | 102 | openssl genrsa -out $root/server/key.pem 2048 103 | openssl genrsa -out $root/client/key.pem 2048 104 | 105 | openssl req \ 106 | -new \ 107 | -nodes \ 108 | -config openssl.cnf \ 109 | -subj "/CN=localhost/O=server/" \ 110 | -key $root/server/key.pem \ 111 | -out $root/server/req.pem \ 112 | -outform PEM 113 | 114 | openssl req \ 115 | -new \ 116 | -nodes \ 117 | -config openssl.cnf \ 118 | -subj "/CN=localhost/O=client/" \ 119 | -key $root/client/key.pem \ 120 | -out $root/client/req.pem \ 121 | -outform PEM 122 | 123 | openssl ca \ 124 | -config openssl.cnf \ 125 | -in $root/server/req.pem \ 126 | -out $root/server/cert.pem \ 127 | -notext \ 128 | -batch \ 129 | -extensions server_ca_extensions 130 | 131 | openssl ca \ 132 | -config openssl.cnf \ 133 | -in $root/client/req.pem \ 134 | -out $root/client/cert.pem \ 135 | -notext \ 136 | -batch \ 137 | -extensions client_ca_extensions 138 | 139 | cat <<-END 140 | const caCert = \` 141 | `cat $root/ca/cacert.pem` 142 | \` 143 | 144 | const serverCert = \` 145 | `cat $root/server/cert.pem` 146 | \` 147 | 148 | const serverKey = \` 149 | `cat $root/server/key.pem` 150 | \` 151 | 152 | const clientCert = \` 153 | `cat $root/client/cert.pem` 154 | \` 155 | 156 | const clientKey = \` 157 | `cat $root/client/key.pem` 158 | \` 159 | END 160 | -------------------------------------------------------------------------------- /change_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo $1 > VERSION 3 | sed -i -e "s/.*buildVersion = \"*.*/buildVersion = \"$1\"/" ./connection.go 4 | go fmt ./... 5 | -------------------------------------------------------------------------------- /confirms.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "context" 10 | "sync" 11 | ) 12 | 13 | // confirms resequences and notifies one or multiple publisher confirmation listeners 14 | type confirms struct { 15 | m sync.Mutex 16 | listeners []chan Confirmation 17 | sequencer map[uint64]Confirmation 18 | deferredConfirmations *deferredConfirmations 19 | published uint64 20 | publishedMut sync.Mutex 21 | expecting uint64 22 | } 23 | 24 | // newConfirms allocates a confirms 25 | func newConfirms() *confirms { 26 | return &confirms{ 27 | sequencer: map[uint64]Confirmation{}, 28 | deferredConfirmations: newDeferredConfirmations(), 29 | published: 0, 30 | expecting: 1, 31 | } 32 | } 33 | 34 | func (c *confirms) Listen(l chan Confirmation) { 35 | c.m.Lock() 36 | defer c.m.Unlock() 37 | 38 | c.listeners = append(c.listeners, l) 39 | } 40 | 41 | // Publish increments the publishing counter 42 | func (c *confirms) publish() *DeferredConfirmation { 43 | c.publishedMut.Lock() 44 | defer c.publishedMut.Unlock() 45 | 46 | c.published++ 47 | return c.deferredConfirmations.Add(c.published) 48 | } 49 | 50 | // unpublish decrements the publishing counter and removes the 51 | // DeferredConfirmation. It must be called immediately after a publish fails. 52 | func (c *confirms) unpublish() { 53 | c.publishedMut.Lock() 54 | defer c.publishedMut.Unlock() 55 | c.deferredConfirmations.remove(c.published) 56 | c.published-- 57 | } 58 | 59 | // confirm confirms one publishing, increments the expecting delivery tag, and 60 | // removes bookkeeping for that delivery tag. 61 | func (c *confirms) confirm(confirmation Confirmation) { 62 | delete(c.sequencer, c.expecting) 63 | c.expecting++ 64 | for _, l := range c.listeners { 65 | l <- confirmation 66 | } 67 | } 68 | 69 | // resequence confirms any out of order delivered confirmations 70 | func (c *confirms) resequence() { 71 | c.publishedMut.Lock() 72 | defer c.publishedMut.Unlock() 73 | 74 | for c.expecting <= c.published { 75 | sequenced, found := c.sequencer[c.expecting] 76 | if !found { 77 | return 78 | } 79 | c.confirm(sequenced) 80 | } 81 | } 82 | 83 | // One confirms one publishing and all following in the publishing sequence 84 | func (c *confirms) One(confirmed Confirmation) { 85 | c.m.Lock() 86 | defer c.m.Unlock() 87 | 88 | c.deferredConfirmations.Confirm(confirmed) 89 | 90 | if c.expecting == confirmed.DeliveryTag { 91 | c.confirm(confirmed) 92 | } else { 93 | c.sequencer[confirmed.DeliveryTag] = confirmed 94 | } 95 | c.resequence() 96 | } 97 | 98 | // Multiple confirms all publishings up until the delivery tag 99 | func (c *confirms) Multiple(confirmed Confirmation) { 100 | c.m.Lock() 101 | defer c.m.Unlock() 102 | 103 | c.deferredConfirmations.ConfirmMultiple(confirmed) 104 | 105 | for c.expecting <= confirmed.DeliveryTag { 106 | c.confirm(Confirmation{c.expecting, confirmed.Ack}) 107 | } 108 | c.resequence() 109 | } 110 | 111 | // Cleans up the confirms struct and its dependencies. 112 | // Closes all listeners, discarding any out of sequence confirmations 113 | func (c *confirms) Close() error { 114 | c.m.Lock() 115 | defer c.m.Unlock() 116 | 117 | c.deferredConfirmations.Close() 118 | 119 | for _, l := range c.listeners { 120 | close(l) 121 | } 122 | c.listeners = nil 123 | return nil 124 | } 125 | 126 | type deferredConfirmations struct { 127 | m sync.Mutex 128 | confirmations map[uint64]*DeferredConfirmation 129 | } 130 | 131 | func newDeferredConfirmations() *deferredConfirmations { 132 | return &deferredConfirmations{ 133 | confirmations: map[uint64]*DeferredConfirmation{}, 134 | } 135 | } 136 | 137 | func (d *deferredConfirmations) Add(tag uint64) *DeferredConfirmation { 138 | d.m.Lock() 139 | defer d.m.Unlock() 140 | 141 | dc := &DeferredConfirmation{DeliveryTag: tag} 142 | dc.done = make(chan struct{}) 143 | d.confirmations[tag] = dc 144 | return dc 145 | } 146 | 147 | // remove is only used to drop a tag whose publish failed 148 | func (d *deferredConfirmations) remove(tag uint64) { 149 | d.m.Lock() 150 | defer d.m.Unlock() 151 | dc, found := d.confirmations[tag] 152 | if !found { 153 | return 154 | } 155 | close(dc.done) 156 | delete(d.confirmations, tag) 157 | } 158 | 159 | func (d *deferredConfirmations) Confirm(confirmation Confirmation) { 160 | d.m.Lock() 161 | defer d.m.Unlock() 162 | 163 | dc, found := d.confirmations[confirmation.DeliveryTag] 164 | if !found { 165 | // We should never receive a confirmation for a tag that hasn't 166 | // been published, but a test causes this to happen. 167 | return 168 | } 169 | dc.setAck(confirmation.Ack) 170 | delete(d.confirmations, confirmation.DeliveryTag) 171 | } 172 | 173 | func (d *deferredConfirmations) ConfirmMultiple(confirmation Confirmation) { 174 | d.m.Lock() 175 | defer d.m.Unlock() 176 | 177 | for k, v := range d.confirmations { 178 | if k <= confirmation.DeliveryTag { 179 | v.setAck(confirmation.Ack) 180 | delete(d.confirmations, k) 181 | } 182 | } 183 | } 184 | 185 | // Close nacks all pending DeferredConfirmations being blocked by dc.Wait(). 186 | func (d *deferredConfirmations) Close() { 187 | d.m.Lock() 188 | defer d.m.Unlock() 189 | 190 | for k, v := range d.confirmations { 191 | v.setAck(false) 192 | delete(d.confirmations, k) 193 | } 194 | } 195 | 196 | // setAck sets the acknowledgement status of the confirmation. Note that it must 197 | // not be called more than once. 198 | func (d *DeferredConfirmation) setAck(ack bool) { 199 | d.ack = ack 200 | close(d.done) 201 | } 202 | 203 | // Done returns the channel that can be used to wait for the publisher 204 | // confirmation. 205 | func (d *DeferredConfirmation) Done() <-chan struct{} { 206 | return d.done 207 | } 208 | 209 | // Acked returns the publisher confirmation in a non-blocking manner. It returns 210 | // false if the confirmation was not acknowledged yet or received negative 211 | // acknowledgement. 212 | func (d *DeferredConfirmation) Acked() bool { 213 | select { 214 | case <-d.done: 215 | default: 216 | return false 217 | } 218 | return d.ack 219 | } 220 | 221 | // Wait blocks until the publisher confirmation. It returns true if the server 222 | // successfully received the publishing. 223 | func (d *DeferredConfirmation) Wait() bool { 224 | <-d.done 225 | return d.ack 226 | } 227 | 228 | // WaitContext waits until the publisher confirmation. It returns true if the 229 | // server successfully received the publishing. If the context expires before 230 | // that, ctx.Err() is returned. 231 | func (d *DeferredConfirmation) WaitContext(ctx context.Context) (bool, error) { 232 | select { 233 | case <-ctx.Done(): 234 | return false, ctx.Err() 235 | case <-d.done: 236 | } 237 | return d.ack, nil 238 | } 239 | -------------------------------------------------------------------------------- /confirms_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "context" 10 | "sync" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestConfirmOneResequences(t *testing.T) { 16 | var ( 17 | fixtures = []Confirmation{ 18 | {1, true}, 19 | {2, false}, 20 | {3, true}, 21 | } 22 | c = newConfirms() 23 | l = make(chan Confirmation, len(fixtures)) 24 | ) 25 | 26 | c.Listen(l) 27 | 28 | for i := range fixtures { 29 | if want, got := uint64(i+1), c.publish(); want != got.DeliveryTag { 30 | t.Fatalf("expected publish to return the 1 based delivery tag published, want: %d, got: %d", want, got.DeliveryTag) 31 | } 32 | } 33 | 34 | c.One(fixtures[1]) 35 | c.One(fixtures[2]) 36 | 37 | select { 38 | case confirm := <-l: 39 | t.Fatalf("expected to wait in order to properly resequence results, got: %+v", confirm) 40 | default: 41 | } 42 | 43 | c.One(fixtures[0]) 44 | 45 | for i, fix := range fixtures { 46 | if want, got := fix, <-l; want != got { 47 | t.Fatalf("expected to return confirmations in sequence for %d, want: %+v, got: %+v", i, want, got) 48 | } 49 | } 50 | } 51 | 52 | func TestConfirmAndPublishDoNotDeadlock(t *testing.T) { 53 | var ( 54 | c = newConfirms() 55 | l = make(chan Confirmation) 56 | iterations = 10 57 | ) 58 | c.Listen(l) 59 | 60 | go func() { 61 | for i := 0; i < iterations; i++ { 62 | c.One(Confirmation{uint64(i + 1), true}) 63 | } 64 | }() 65 | 66 | for i := 0; i < iterations; i++ { 67 | c.publish() 68 | <-l 69 | } 70 | } 71 | 72 | func TestConfirmMixedResequences(t *testing.T) { 73 | var ( 74 | fixtures = []Confirmation{ 75 | {1, true}, 76 | {2, true}, 77 | {3, true}, 78 | } 79 | c = newConfirms() 80 | l = make(chan Confirmation, len(fixtures)) 81 | ) 82 | c.Listen(l) 83 | 84 | for range fixtures { 85 | c.publish() 86 | } 87 | 88 | c.One(fixtures[0]) 89 | c.One(fixtures[2]) 90 | c.Multiple(fixtures[1]) 91 | 92 | for i, fix := range fixtures { 93 | want := fix 94 | var got Confirmation 95 | select { 96 | case got = <-l: 97 | case <-time.After(1 * time.Second): 98 | t.Fatalf("timeout on reading confirmations") 99 | } 100 | if want != got { 101 | t.Fatalf("expected to confirm in sequence for %d, want: %+v, got: %+v", i, want, got) 102 | } 103 | } 104 | } 105 | 106 | func TestConfirmMultipleResequences(t *testing.T) { 107 | var ( 108 | fixtures = []Confirmation{ 109 | {1, true}, 110 | {2, true}, 111 | {3, true}, 112 | {4, true}, 113 | } 114 | c = newConfirms() 115 | l = make(chan Confirmation, len(fixtures)) 116 | ) 117 | c.Listen(l) 118 | 119 | for range fixtures { 120 | c.publish() 121 | } 122 | 123 | c.Multiple(fixtures[len(fixtures)-1]) 124 | 125 | for i, fix := range fixtures { 126 | if want, got := fix, <-l; want != got { 127 | t.Fatalf("expected to confirm multiple in sequence for %d, want: %+v, got: %+v", i, want, got) 128 | } 129 | } 130 | } 131 | 132 | func BenchmarkSequentialBufferedConfirms(t *testing.B) { 133 | var ( 134 | c = newConfirms() 135 | l = make(chan Confirmation, 10) 136 | ) 137 | 138 | c.Listen(l) 139 | 140 | for i := 0; i < t.N; i++ { 141 | if i > cap(l)-1 { 142 | <-l 143 | } 144 | c.One(Confirmation{c.publish().DeliveryTag, true}) 145 | } 146 | } 147 | 148 | func TestConfirmsIsThreadSafe(t *testing.T) { 149 | const count = 1000 150 | const timeout = 5 * time.Second 151 | var ( 152 | c = newConfirms() 153 | l = make(chan Confirmation) 154 | pub = make(chan Confirmation) 155 | done = make(chan Confirmation) 156 | late = time.After(timeout) 157 | ) 158 | 159 | c.Listen(l) 160 | 161 | for i := 0; i < count; i++ { 162 | go func() { pub <- Confirmation{c.publish().DeliveryTag, true} }() 163 | } 164 | 165 | for i := 0; i < count; i++ { 166 | go func() { c.One(<-pub) }() 167 | } 168 | 169 | for i := 0; i < count; i++ { 170 | go func() { done <- <-l }() 171 | } 172 | 173 | for i := 0; i < count; i++ { 174 | select { 175 | case <-done: 176 | case <-late: 177 | t.Fatalf("expected all publish/confirms to finish after %s", timeout) 178 | } 179 | } 180 | } 181 | 182 | func TestDeferredConfirmationsConfirm(t *testing.T) { 183 | dcs := newDeferredConfirmations() 184 | var wg sync.WaitGroup 185 | for i, ack := range []bool{true, false} { 186 | var result bool 187 | deliveryTag := uint64(i + 1) 188 | dc := dcs.Add(deliveryTag) 189 | wg.Add(1) 190 | go func() { 191 | result = dc.Wait() 192 | wg.Done() 193 | }() 194 | dcs.Confirm(Confirmation{deliveryTag, ack}) 195 | wg.Wait() 196 | if result != ack { 197 | t.Fatalf("expected to receive matching ack got %v", result) 198 | } 199 | } 200 | } 201 | 202 | func TestDeferredConfirmationsConfirmMultiple(t *testing.T) { 203 | dcs := newDeferredConfirmations() 204 | var wg sync.WaitGroup 205 | var result bool 206 | dc1 := dcs.Add(1) 207 | dc2 := dcs.Add(2) 208 | dc3 := dcs.Add(3) 209 | wg.Add(1) 210 | go func() { 211 | result = dc1.Wait() && dc2.Wait() && dc3.Wait() 212 | wg.Done() 213 | }() 214 | dcs.ConfirmMultiple(Confirmation{4, true}) 215 | wg.Wait() 216 | if !result { 217 | t.Fatal("expected to receive true for result, received false") 218 | } 219 | } 220 | 221 | func TestDeferredConfirmationsClose(t *testing.T) { 222 | dcs := newDeferredConfirmations() 223 | var wg sync.WaitGroup 224 | var result bool 225 | dc1 := dcs.Add(1) 226 | dc2 := dcs.Add(2) 227 | dc3 := dcs.Add(3) 228 | wg.Add(1) 229 | go func() { 230 | result = !dc1.Wait() && !dc2.Wait() && !dc3.Wait() 231 | wg.Done() 232 | }() 233 | dcs.Close() 234 | wg.Wait() 235 | if !result { 236 | t.Fatal("expected to receive false for nacked confirmations, received true") 237 | } 238 | } 239 | 240 | func TestDeferredConfirmationsDoneAcked(t *testing.T) { 241 | dcs := newDeferredConfirmations() 242 | dc := dcs.Add(1) 243 | 244 | if dc.Acked() { 245 | t.Fatal("expected to receive false for pending confirmations, received true") 246 | } 247 | 248 | // Confirm twice to ensure that setAck is called once. 249 | for i := 0; i < 2; i++ { 250 | dcs.Confirm(Confirmation{dc.DeliveryTag, true}) 251 | } 252 | 253 | <-dc.Done() 254 | 255 | if !dc.Acked() { 256 | t.Fatal("expected to receive true for acked confirmations, received false") 257 | } 258 | } 259 | 260 | func TestDeferredConfirmationsWaitContextNack(t *testing.T) { 261 | dcs := newDeferredConfirmations() 262 | dc := dcs.Add(1) 263 | 264 | dcs.Confirm(Confirmation{dc.DeliveryTag, false}) 265 | 266 | ack, err := dc.WaitContext(context.Background()) 267 | if err != nil { 268 | t.Fatalf("expected to receive nil, got %v", err) 269 | } 270 | if ack { 271 | t.Fatal("expected to receive false for nacked confirmations, received true") 272 | } 273 | } 274 | 275 | func TestDeferredConfirmationsWaitContextCancel(t *testing.T) { 276 | dc := newDeferredConfirmations().Add(1) 277 | 278 | ctx, cancel := context.WithCancel(context.Background()) 279 | cancel() 280 | 281 | ack, err := dc.WaitContext(ctx) 282 | if err == nil { 283 | t.Fatal("expected to receive context error, got nil") 284 | } 285 | if ack { 286 | t.Fatal("expected to receive false for pending confirmations, received true") 287 | } 288 | } 289 | 290 | func TestDeferredConfirmationsConcurrency(t *testing.T) { 291 | dcs := newDeferredConfirmations() 292 | var wg sync.WaitGroup 293 | var result bool 294 | dc1 := dcs.Add(1) 295 | dc2 := dcs.Add(2) 296 | dc3 := dcs.Add(3) 297 | wg.Add(1) 298 | 299 | go func() { 300 | defer wg.Done() 301 | result = dc1.Wait() && dc2.Wait() && dc3.Wait() 302 | }() 303 | dcs.ConfirmMultiple(Confirmation{4, true}) 304 | wg.Wait() 305 | if !result { 306 | t.Fatal("expected to receive true for concurrent confirmations, received false") 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /connection_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | //go:build integration 7 | // +build integration 8 | 9 | package amqp091 10 | 11 | import ( 12 | "context" 13 | "crypto/tls" 14 | "net" 15 | "os" 16 | "os/exec" 17 | "regexp" 18 | "strings" 19 | "sync" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | const rabbitmqctlEnvKey = "RABBITMQ_RABBITMQCTL_PATH" 25 | 26 | func TestRequiredServerLocale(t *testing.T) { 27 | conn := integrationConnection(t, "AMQP 0-9-1 required server locale") 28 | t.Cleanup(func() { conn.Close() }) 29 | requiredServerLocale := defaultLocale 30 | 31 | for _, locale := range conn.Locales { 32 | if locale == requiredServerLocale { 33 | return 34 | } 35 | } 36 | 37 | t.Fatalf("AMQP 0-9-1 server must support at least the %s locale, server sent the following locales: %#v", requiredServerLocale, conn.Locales) 38 | } 39 | 40 | func TestDefaultConnectionLocale(t *testing.T) { 41 | conn := integrationConnection(t, "client default locale") 42 | t.Cleanup(func() { conn.Close() }) 43 | 44 | if conn.Config.Locale != defaultLocale { 45 | t.Fatalf("Expected default connection locale to be %s, is was: %s", defaultLocale, conn.Config.Locale) 46 | } 47 | } 48 | 49 | func TestChannelOpenOnAClosedConnectionFails(t *testing.T) { 50 | conn := integrationConnection(t, "channel on close") 51 | 52 | conn.Close() 53 | 54 | if _, err := conn.Channel(); err != ErrClosed { 55 | t.Fatalf("channel.open on a closed connection %#v is expected to fail", conn) 56 | } 57 | } 58 | 59 | // TestChannelOpenOnAClosedConnectionFails_ReleasesAllocatedChannel ensures the 60 | // channel allocated is released if opening the channel fails. 61 | func TestChannelOpenOnAClosedConnectionFails_ReleasesAllocatedChannel(t *testing.T) { 62 | conn := integrationConnection(t, "releases channel allocation") 63 | conn.Close() 64 | 65 | before := len(conn.channels) 66 | 67 | if _, err := conn.Channel(); err != ErrClosed { 68 | t.Fatalf("channel.open on a closed connection %#v is expected to fail", conn) 69 | } 70 | 71 | if len(conn.channels) != before { 72 | t.Fatalf("channel.open failed, but the allocated channel was not released") 73 | } 74 | } 75 | 76 | // TestRaceBetweenChannelAndConnectionClose ensures allocating a new channel 77 | // does not race with shutting the connection down. 78 | // 79 | // See https://github.com/streadway/amqp/issues/251 - thanks to jmalloc for the 80 | // test case. 81 | func TestRaceBetweenChannelAndConnectionClose(t *testing.T) { 82 | defer time.AfterFunc(10*time.Second, func() { t.Fatalf("Close deadlock") }).Stop() 83 | 84 | conn := integrationConnection(t, "allocation/shutdown race") 85 | 86 | go conn.Close() 87 | for i := 0; i < 10; i++ { 88 | go func() { 89 | ch, err := conn.Channel() 90 | if err == nil { 91 | ch.Close() 92 | } 93 | }() 94 | } 95 | } 96 | 97 | // TestRaceBetweenChannelShutdownAndSend ensures closing a channel 98 | // (channel.shutdown) does not race with calling channel.send() from any other 99 | // goroutines. 100 | // 101 | // See https://github.com/streadway/amqp/pull/253#issuecomment-292464811 for 102 | // more details - thanks to jmalloc again. 103 | func TestRaceBetweenChannelShutdownAndSend(t *testing.T) { 104 | const concurrency = 10 105 | defer time.AfterFunc(10*time.Second, func() { t.Fatalf("Close deadlock") }).Stop() 106 | 107 | conn := integrationConnection(t, "channel close/send race") 108 | defer conn.Close() 109 | 110 | ch, _ := conn.Channel() 111 | go ch.Close() 112 | 113 | errs := make(chan error, concurrency) 114 | wg := sync.WaitGroup{} 115 | wg.Add(concurrency) 116 | 117 | for i := 0; i < concurrency; i++ { 118 | go func() { 119 | defer wg.Done() 120 | // ch.Ack calls ch.send() internally. 121 | if err := ch.Ack(42, false); err != nil { 122 | errs <- err 123 | } 124 | }() 125 | } 126 | 127 | wg.Wait() 128 | close(errs) 129 | 130 | for err := range errs { 131 | if err != nil { 132 | t.Logf("[INFO] %#v (%s) of type %T", err, err, err) 133 | } 134 | } 135 | } 136 | 137 | func TestQueueDeclareOnAClosedConnectionFails(t *testing.T) { 138 | conn := integrationConnection(t, "queue declare on close") 139 | ch, _ := conn.Channel() 140 | 141 | conn.Close() 142 | 143 | if _, err := ch.QueueDeclare("an example", false, false, false, false, nil); err != ErrClosed { 144 | t.Fatalf("queue.declare on a closed connection %#v is expected to return ErrClosed, returned: %#v", conn, err) 145 | } 146 | } 147 | 148 | func TestConcurrentClose(t *testing.T) { 149 | const concurrency = 32 150 | 151 | conn := integrationConnection(t, "concurrent close") 152 | defer conn.Close() 153 | 154 | errs := make(chan error, concurrency) 155 | wg := sync.WaitGroup{} 156 | wg.Add(concurrency) 157 | 158 | for i := 0; i < concurrency; i++ { 159 | go func() { 160 | defer wg.Done() 161 | 162 | err := conn.Close() 163 | 164 | if err == nil { 165 | t.Log("first concurrent close was successful") 166 | return 167 | } 168 | 169 | if err == ErrClosed { 170 | t.Log("later concurrent close were successful and returned ErrClosed") 171 | return 172 | } 173 | 174 | // BUG(st) is this really acceptable? we got a net.OpError before the 175 | // connection was marked as closed means a race condition between the 176 | // network connection and handshake state. It should be a package error 177 | // returned. 178 | if _, neterr := err.(*net.OpError); neterr { 179 | t.Logf("unknown net.OpError during close, ignoring: %+v", err) 180 | return 181 | } 182 | 183 | // A different/protocol error occurred indicating a race or missed condition 184 | if _, other := err.(*Error); other { 185 | errs <- err 186 | } 187 | }() 188 | } 189 | 190 | wg.Wait() 191 | close(errs) 192 | 193 | for err := range errs { 194 | if err != nil { 195 | t.Fatalf("Expected no error, or ErrClosed, or a net.OpError from conn.Close(), got %#v (%s) of type %T", err, err, err) 196 | } 197 | } 198 | } 199 | 200 | // TestPlaintextDialTLS ensures amqp:// connections succeed when using DialTLS. 201 | func TestPlaintextDialTLS(t *testing.T) { 202 | uri, err := ParseURI(amqpURL) 203 | if err != nil { 204 | t.Fatalf("parse URI error: %s", err) 205 | } 206 | 207 | // We can only test when we have a plaintext listener 208 | if uri.Scheme != "amqp" { 209 | t.Skip("requires server listening for plaintext connections") 210 | } 211 | 212 | conn, err := DialTLS(uri.String(), &tls.Config{MinVersion: tls.VersionTLS12}) 213 | if err != nil { 214 | t.Fatalf("unexpected dial error, got %v", err) 215 | } 216 | conn.Close() 217 | } 218 | 219 | // TestIsClosed will test the public method IsClosed on a connection. 220 | func TestIsClosed(t *testing.T) { 221 | conn := integrationConnection(t, "public IsClosed()") 222 | 223 | if conn.IsClosed() { 224 | t.Fatalf("connection expected to not be marked as closed") 225 | } 226 | 227 | conn.Close() 228 | 229 | if !conn.IsClosed() { 230 | t.Fatal("connection expected to be marked as closed") 231 | } 232 | } 233 | 234 | // TestChannelIsClosed will test the public method IsClosed on a channel. 235 | func TestChannelIsClosed(t *testing.T) { 236 | conn := integrationConnection(t, "public channel.IsClosed()") 237 | t.Cleanup(func() { conn.Close() }) 238 | ch, _ := conn.Channel() 239 | 240 | if ch.IsClosed() { 241 | t.Fatalf("channel expected to not be marked as closed") 242 | } 243 | 244 | ch.Close() 245 | 246 | if !ch.IsClosed() { 247 | t.Fatal("channel expected to be marked as closed") 248 | } 249 | } 250 | 251 | // TestReaderGoRoutineTerminatesWhenMsgIsProcessedDuringClose tests the issue 252 | // described in https://github.com/rabbitmq/amqp091-go/issues/69. 253 | func TestReaderGoRoutineTerminatesWhenMsgIsProcessedDuringClose(t *testing.T) { 254 | const routines = 10 255 | c := integrationConnection(t, t.Name()) 256 | 257 | var wg sync.WaitGroup 258 | startSigCh := make(chan interface{}) 259 | 260 | for i := 0; i < routines; i++ { 261 | wg.Add(1) 262 | go func(id int) { 263 | defer wg.Done() 264 | 265 | <-startSigCh 266 | 267 | err := c.Close() 268 | if err != nil { 269 | t.Logf("close failed in routine %d: %s", id, err.Error()) 270 | } 271 | }(i) 272 | } 273 | close(startSigCh) 274 | 275 | t.Log("waiting for go-routines to terminate") 276 | wg.Wait() 277 | } 278 | 279 | func TestConnectionConfigPropertiesWithClientProvidedConnectionName(t *testing.T) { 280 | const expectedConnectionName = "amqp091-go-test" 281 | 282 | connectionProperties := NewConnectionProperties() 283 | connectionProperties.SetClientConnectionName(expectedConnectionName) 284 | 285 | currentConnectionName, ok := connectionProperties["connection_name"] 286 | if !ok { 287 | t.Fatal("Connection name was not set by Table.SetClientConnectionName") 288 | } 289 | if currentConnectionName != expectedConnectionName { 290 | t.Fatalf("Connection name is set to: %s. Expected: %s", 291 | currentConnectionName, 292 | expectedConnectionName) 293 | } 294 | } 295 | 296 | func TestNewConnectionProperties_HasDefaultProperties(t *testing.T) { 297 | expectedProductName := defaultProduct 298 | expectedPlatform := platform 299 | 300 | props := NewConnectionProperties() 301 | 302 | productName, ok := props["product"] 303 | if !ok { 304 | t.Fatal("Product name was not set by NewConnectionProperties") 305 | } 306 | if productName != expectedProductName { 307 | t.Fatalf("Product name is set to: %s. Expected: %s", 308 | productName, 309 | expectedProductName, 310 | ) 311 | } 312 | 313 | platform, ok := props["platform"] 314 | if !ok { 315 | t.Fatal("Platform was not set by NewConnectionProperties") 316 | } 317 | if platform != expectedPlatform { 318 | t.Fatalf("Platform is set to: %s. Expected: %s", 319 | platform, 320 | expectedPlatform, 321 | ) 322 | } 323 | 324 | versionUntyped, ok := props["version"] 325 | if !ok { 326 | t.Fatal("Version was not set by NewConnectionProperties") 327 | } 328 | 329 | version, ok := versionUntyped.(string) 330 | if !ok { 331 | t.Fatalf("Version in NewConnectionProperties should be string. Type given was: %T", versionUntyped) 332 | } 333 | 334 | // semver regexp as specified by https://semver.org/ 335 | semverRegexp := regexp.MustCompile(`^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) 336 | 337 | if !semverRegexp.MatchString(version) { 338 | t.Fatalf("Version in NewConnectionProperties is not a valid semver value: %s", version) 339 | } 340 | } 341 | 342 | // Connection and channels should be closeable when a memory alarm is active. 343 | // https://github.com/rabbitmq/amqp091-go/issues/178 344 | func TestConnection_Close_WhenMemoryAlarmIsActive(t *testing.T) { 345 | err := rabbitmqctl(t, "set_vm_memory_high_watermark", "0.0001") 346 | if err != nil { 347 | t.Fatal(err) 348 | } 349 | t.Cleanup(func() { 350 | _ = rabbitmqctl(t, "set_vm_memory_high_watermark", "0.4") 351 | conn, ch := integrationQueue(t, t.Name()) 352 | integrationQueueDelete(t, ch, t.Name()) 353 | _ = ch.Close() 354 | _ = conn.Close() 355 | }) 356 | 357 | conn, ch := integrationQueue(t, t.Name()) 358 | 359 | go func() { 360 | // simulate a producer 361 | // required to block the connection 362 | _ = ch.PublishWithContext(context.Background(), "", t.Name(), false, false, Publishing{ 363 | Body: []byte("this is a test"), 364 | }) 365 | }() 366 | <-time.After(time.Second * 1) 367 | 368 | err = conn.CloseDeadline(time.Now().Add(time.Second * 2)) 369 | if err == nil { 370 | t.Fatal("expected error, got nil") 371 | } 372 | if !conn.IsClosed() { 373 | t.Fatal("expected connection to be closed") 374 | } 375 | } 376 | 377 | func rabbitmqctl(t *testing.T, args ...string) error { 378 | t.Helper() 379 | 380 | rabbitmqctlPath, found := os.LookupEnv(rabbitmqctlEnvKey) 381 | if !found { 382 | t.Skipf("variable for %s for rabbitmqctl not found, skipping", rabbitmqctlEnvKey) 383 | } 384 | 385 | var cmd *exec.Cmd 386 | if strings.HasPrefix(rabbitmqctlPath, "DOCKER:") { 387 | containerName := strings.Split(rabbitmqctlPath, ":")[1] 388 | cmd = exec.Command("docker", "exec", containerName, "rabbitmqctl") 389 | cmd.Args = append(cmd.Args, args...) 390 | } else { 391 | cmd = exec.Command(rabbitmqctlPath, args...) 392 | } 393 | 394 | return cmd.Run() 395 | } 396 | -------------------------------------------------------------------------------- /consumers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "os" 10 | "strconv" 11 | "sync" 12 | "sync/atomic" 13 | ) 14 | 15 | var consumerSeq uint64 16 | 17 | const consumerTagLengthMax = 0xFF // see writeShortstr 18 | 19 | func uniqueConsumerTag() string { 20 | return commandNameBasedUniqueConsumerTag(os.Args[0]) 21 | } 22 | 23 | func commandNameBasedUniqueConsumerTag(commandName string) string { 24 | tagPrefix := "ctag-" 25 | tagInfix := commandName 26 | tagSuffix := "-" + strconv.FormatUint(atomic.AddUint64(&consumerSeq, 1), 10) 27 | 28 | if len(tagPrefix)+len(tagInfix)+len(tagSuffix) > consumerTagLengthMax { 29 | tagInfix = "streadway/amqp" 30 | } 31 | 32 | return tagPrefix + tagInfix + tagSuffix 33 | } 34 | 35 | type consumerBuffers map[string]chan *Delivery 36 | 37 | // Concurrent type that manages the consumerTag -> 38 | // ingress consumerBuffer mapping 39 | type consumers struct { 40 | sync.WaitGroup // one for buffer 41 | closed chan struct{} // signal buffer 42 | 43 | sync.Mutex // protects below 44 | chans consumerBuffers 45 | } 46 | 47 | func makeConsumers() *consumers { 48 | return &consumers{ 49 | closed: make(chan struct{}), 50 | chans: make(consumerBuffers), 51 | } 52 | } 53 | 54 | func (subs *consumers) buffer(in chan *Delivery, out chan Delivery) { 55 | defer close(out) 56 | defer subs.Done() 57 | 58 | inflight := in 59 | var queue []*Delivery 60 | 61 | for delivery := range in { 62 | queue = append(queue, delivery) 63 | 64 | for len(queue) > 0 { 65 | select { 66 | case <-subs.closed: 67 | // closed before drained, drop in-flight 68 | return 69 | 70 | case delivery, consuming := <-inflight: 71 | if consuming { 72 | queue = append(queue, delivery) 73 | } else { 74 | inflight = nil 75 | } 76 | 77 | case out <- *queue[0]: 78 | /* 79 | * https://github.com/rabbitmq/amqp091-go/issues/179 80 | * https://github.com/rabbitmq/amqp091-go/pull/180 81 | * 82 | * Comment from @lars-t-hansen: 83 | * 84 | * Given Go's slice semantics, and barring any information 85 | * available to the compiler that proves that queue is the only 86 | * pointer to the memory it references, the only meaning that 87 | * queue = queue[1:] can have is basically queue += sizeof(queue 88 | * element), ie, it bumps a pointer. Looking at the generated 89 | * code for a simple example (on ARM64 in this case) bears this 90 | * out. So what we're left with is an array that we have a 91 | * pointer into the middle of. When the GC traces this pointer, 92 | * it too does not know whether the array has multiple 93 | * referents, and so its only sensible choice is to find the 94 | * beginning of the array, and if the array is not already 95 | * visited, mark every element in it, including the "dead" 96 | * pointer. 97 | * 98 | * (Depending on the program dynamics, an element may eventually 99 | * be appended to the queue when the queue is at capacity, and 100 | * in this case the live elements are copied into a new array 101 | * and the old array is left to be GC'd eventually, along with 102 | * the dead object. But that can take time.) 103 | */ 104 | queue[0] = nil 105 | queue = queue[1:] 106 | } 107 | } 108 | } 109 | } 110 | 111 | // On key conflict, close the previous channel. 112 | func (subs *consumers) add(tag string, consumer chan Delivery) { 113 | subs.Lock() 114 | defer subs.Unlock() 115 | 116 | if prev, found := subs.chans[tag]; found { 117 | close(prev) 118 | } 119 | 120 | in := make(chan *Delivery) 121 | subs.chans[tag] = in 122 | 123 | subs.Add(1) 124 | go subs.buffer(in, consumer) 125 | } 126 | 127 | func (subs *consumers) cancel(tag string) (found bool) { 128 | subs.Lock() 129 | defer subs.Unlock() 130 | 131 | ch, found := subs.chans[tag] 132 | 133 | if found { 134 | delete(subs.chans, tag) 135 | close(ch) 136 | } 137 | 138 | return found 139 | } 140 | 141 | func (subs *consumers) close() { 142 | subs.Lock() 143 | defer subs.Unlock() 144 | 145 | close(subs.closed) 146 | 147 | for tag, ch := range subs.chans { 148 | delete(subs.chans, tag) 149 | close(ch) 150 | } 151 | 152 | subs.Wait() 153 | } 154 | 155 | // Sends a delivery to a the consumer identified by `tag`. 156 | // If unbuffered channels are used for Consume this method 157 | // could block all deliveries until the consumer 158 | // receives on the other end of the channel. 159 | func (subs *consumers) send(tag string, msg *Delivery) bool { 160 | subs.Lock() 161 | defer subs.Unlock() 162 | 163 | buffer, found := subs.chans[tag] 164 | if found { 165 | buffer <- msg 166 | } 167 | 168 | return found 169 | } 170 | -------------------------------------------------------------------------------- /consumers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestGeneratedUniqueConsumerTagDoesNotExceedMaxLength(t *testing.T) { 14 | assertCorrectLength := func(commandName string) { 15 | tag := commandNameBasedUniqueConsumerTag(commandName) 16 | if len(tag) > consumerTagLengthMax { 17 | t.Error("Generated unique consumer tag exceeds maximum length:", tag) 18 | } 19 | } 20 | 21 | assertCorrectLength("test") 22 | assertCorrectLength(strings.Repeat("z", 249)) 23 | assertCorrectLength(strings.Repeat("z", 256)) 24 | assertCorrectLength(strings.Repeat("z", 1024)) 25 | } 26 | -------------------------------------------------------------------------------- /delivery.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "errors" 10 | "time" 11 | ) 12 | 13 | var ErrDeliveryNotInitialized = errors.New("delivery not initialized. Channel is probably closed") 14 | 15 | // Acknowledger notifies the server of successful or failed consumption of 16 | // deliveries via identifier found in the Delivery.DeliveryTag field. 17 | // 18 | // Applications can provide mock implementations in tests of Delivery handlers. 19 | type Acknowledger interface { 20 | Ack(tag uint64, multiple bool) error 21 | Nack(tag uint64, multiple, requeue bool) error 22 | Reject(tag uint64, requeue bool) error 23 | } 24 | 25 | // Delivery captures the fields for a previously delivered message resident in 26 | // a queue to be delivered by the server to a consumer from Channel.Consume or 27 | // Channel.Get. 28 | type Delivery struct { 29 | Acknowledger Acknowledger // the channel from which this delivery arrived 30 | 31 | Headers Table // Application or header exchange table 32 | 33 | // Properties 34 | ContentType string // MIME content type 35 | ContentEncoding string // MIME content encoding 36 | DeliveryMode uint8 // queue implementation use - non-persistent (1) or persistent (2) 37 | Priority uint8 // queue implementation use - 0 to 9 38 | CorrelationId string // application use - correlation identifier 39 | ReplyTo string // application use - address to reply to (ex: RPC) 40 | Expiration string // implementation use - message expiration spec 41 | MessageId string // application use - message identifier 42 | Timestamp time.Time // application use - message timestamp 43 | Type string // application use - message type name 44 | UserId string // application use - creating user - should be authenticated user 45 | AppId string // application use - creating application id 46 | 47 | // Valid only with Channel.Consume 48 | ConsumerTag string 49 | 50 | // Valid only with Channel.Get 51 | MessageCount uint32 52 | 53 | DeliveryTag uint64 54 | Redelivered bool 55 | Exchange string // basic.publish exchange 56 | RoutingKey string // basic.publish routing key 57 | 58 | Body []byte 59 | } 60 | 61 | func newDelivery(channel *Channel, msg messageWithContent) *Delivery { 62 | props, body := msg.getContent() 63 | 64 | delivery := Delivery{ 65 | Acknowledger: channel, 66 | 67 | Headers: props.Headers, 68 | ContentType: props.ContentType, 69 | ContentEncoding: props.ContentEncoding, 70 | DeliveryMode: props.DeliveryMode, 71 | Priority: props.Priority, 72 | CorrelationId: props.CorrelationId, 73 | ReplyTo: props.ReplyTo, 74 | Expiration: props.Expiration, 75 | MessageId: props.MessageId, 76 | Timestamp: props.Timestamp, 77 | Type: props.Type, 78 | UserId: props.UserId, 79 | AppId: props.AppId, 80 | 81 | Body: body, 82 | } 83 | 84 | // Properties for the delivery types 85 | switch m := msg.(type) { 86 | case *basicDeliver: 87 | delivery.ConsumerTag = m.ConsumerTag 88 | delivery.DeliveryTag = m.DeliveryTag 89 | delivery.Redelivered = m.Redelivered 90 | delivery.Exchange = m.Exchange 91 | delivery.RoutingKey = m.RoutingKey 92 | 93 | case *basicGetOk: 94 | delivery.MessageCount = m.MessageCount 95 | delivery.DeliveryTag = m.DeliveryTag 96 | delivery.Redelivered = m.Redelivered 97 | delivery.Exchange = m.Exchange 98 | delivery.RoutingKey = m.RoutingKey 99 | } 100 | 101 | return &delivery 102 | } 103 | 104 | /* 105 | Ack delegates an acknowledgement through the Acknowledger interface that the 106 | client or server has finished work on a delivery. 107 | 108 | All deliveries in AMQP must be acknowledged. If you called Channel.Consume 109 | with autoAck true then the server will be automatically ack each message and 110 | this method should not be called. Otherwise, you must call Delivery.Ack after 111 | you have successfully processed this delivery. 112 | 113 | When multiple is true, this delivery and all prior unacknowledged deliveries 114 | on the same channel will be acknowledged. This is useful for batch processing 115 | of deliveries. 116 | 117 | An error will indicate that the acknowledge could not be delivered to the 118 | channel it was sent from. 119 | 120 | Either Delivery.Ack, Delivery.Reject or Delivery.Nack must be called for every 121 | delivery that is not automatically acknowledged. 122 | */ 123 | func (d Delivery) Ack(multiple bool) error { 124 | if d.Acknowledger == nil { 125 | return ErrDeliveryNotInitialized 126 | } 127 | return d.Acknowledger.Ack(d.DeliveryTag, multiple) 128 | } 129 | 130 | /* 131 | Reject delegates a negatively acknowledgement through the Acknowledger interface. 132 | 133 | When requeue is true, queue this message to be delivered to a consumer on a 134 | different channel. When requeue is false or the server is unable to queue this 135 | message, it will be dropped. 136 | 137 | If you are batch processing deliveries, and your server supports it, prefer 138 | Delivery.Nack. 139 | 140 | Either Delivery.Ack, Delivery.Reject or Delivery.Nack must be called for every 141 | delivery that is not automatically acknowledged. 142 | */ 143 | func (d Delivery) Reject(requeue bool) error { 144 | if d.Acknowledger == nil { 145 | return ErrDeliveryNotInitialized 146 | } 147 | return d.Acknowledger.Reject(d.DeliveryTag, requeue) 148 | } 149 | 150 | /* 151 | Nack negatively acknowledge the delivery of message(s) identified by the 152 | delivery tag from either the client or server. 153 | 154 | When multiple is true, nack messages up to and including delivered messages up 155 | until the delivery tag delivered on the same channel. 156 | 157 | When requeue is true, request the server to deliver this message to a different 158 | consumer. If it is not possible or requeue is false, the message will be 159 | dropped or delivered to a server configured dead-letter queue. 160 | 161 | This method must not be used to select or requeue messages the client wishes 162 | not to handle, rather it is to inform the server that the client is incapable 163 | of handling this message at this time. 164 | 165 | Either Delivery.Ack, Delivery.Reject or Delivery.Nack must be called for every 166 | delivery that is not automatically acknowledged. 167 | */ 168 | func (d Delivery) Nack(multiple, requeue bool) error { 169 | if d.Acknowledger == nil { 170 | return ErrDeliveryNotInitialized 171 | } 172 | return d.Acknowledger.Nack(d.DeliveryTag, multiple, requeue) 173 | } 174 | -------------------------------------------------------------------------------- /delivery_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "errors" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func shouldNotPanic(t *testing.T) { 15 | if err := recover(); err != nil { 16 | t.Fatalf("should not panic, got: %s", err) 17 | } 18 | } 19 | 20 | // A closed delivery chan could produce zero value. Ack/Nack/Reject on these 21 | // deliveries can produce a nil pointer panic. Instead, return an error when 22 | // the method can never be successful. 23 | func TestAckZeroValueAcknowledgerDoesNotPanic(t *testing.T) { 24 | defer shouldNotPanic(t) 25 | err := (Delivery{}).Ack(false) 26 | if err == nil { 27 | t.Fatalf("expected Delivery{}.Ack to error") 28 | } 29 | if !errors.Is(err, ErrDeliveryNotInitialized) { 30 | t.Fatalf("expected '%v' got '%v'", ErrDeliveryNotInitialized, err) 31 | } 32 | expectedErrMessage := "delivery not initialized. Channel is probably closed" 33 | if !strings.EqualFold(err.Error(), expectedErrMessage) { 34 | t.Errorf("expected '%s' got '%s'", expectedErrMessage, err) 35 | } 36 | } 37 | 38 | func TestNackZeroValueAcknowledgerDoesNotPanic(t *testing.T) { 39 | defer shouldNotPanic(t) 40 | err := (Delivery{}).Nack(false, false) 41 | if err == nil { 42 | t.Fatalf("expected Delivery{}.Nack to error") 43 | } 44 | if !errors.Is(err, ErrDeliveryNotInitialized) { 45 | t.Fatalf("expected '%v' got '%v'", ErrDeliveryNotInitialized, err) 46 | } 47 | expectedErrMessage := "delivery not initialized. Channel is probably closed" 48 | if !strings.EqualFold(err.Error(), expectedErrMessage) { 49 | t.Errorf("expected '%s' got '%s'", expectedErrMessage, err) 50 | } 51 | } 52 | 53 | func TestRejectZeroValueAcknowledgerDoesNotPanic(t *testing.T) { 54 | defer shouldNotPanic(t) 55 | err := (Delivery{}).Reject(false) 56 | if err == nil { 57 | t.Fatalf("expected Delivery{}.Reject to error") 58 | } 59 | if !errors.Is(err, ErrDeliveryNotInitialized) { 60 | t.Fatalf("expected '%v' got '%v'", ErrDeliveryNotInitialized, err) 61 | } 62 | expectedErrMessage := "delivery not initialized. Channel is probably closed" 63 | if !strings.EqualFold(err.Error(), expectedErrMessage) { 64 | t.Errorf("expected '%s' got '%s'", expectedErrMessage, err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | /* 7 | Package amqp091 is an AMQP 0.9.1 client with RabbitMQ extensions 8 | 9 | Understand the AMQP 0.9.1 messaging model by reviewing these links first. Much 10 | of the terminology in this library directly relates to AMQP concepts. 11 | 12 | Resources 13 | 14 | http://www.rabbitmq.com/tutorials/amqp-concepts.html 15 | http://www.rabbitmq.com/getstarted.html 16 | http://www.rabbitmq.com/amqp-0-9-1-reference.html 17 | 18 | # Design 19 | 20 | Most other broker clients publish to queues, but in AMQP, clients publish 21 | Exchanges instead. AMQP is programmable, meaning that both the producers and 22 | consumers agree on the configuration of the broker, instead of requiring an 23 | operator or system configuration that declares the logical topology in the 24 | broker. The routing between producers and consumer queues is via Bindings. 25 | These bindings form the logical topology of the broker. 26 | 27 | In this library, a message sent from publisher is called a "Publishing" and a 28 | message received to a consumer is called a "Delivery". The fields of 29 | Publishings and Deliveries are close but not exact mappings to the underlying 30 | wire format to maintain stronger types. Many other libraries will combine 31 | message properties with message headers. In this library, the message well 32 | known properties are strongly typed fields on the Publishings and Deliveries, 33 | whereas the user defined headers are in the Headers field. 34 | 35 | The method naming closely matches the protocol's method name with positional 36 | parameters mapping to named protocol message fields. The motivation here is to 37 | present a comprehensive view over all possible interactions with the server. 38 | 39 | Generally, methods that map to protocol methods of the "basic" class will be 40 | elided in this interface, and "select" methods of various channel mode selectors 41 | will be elided for example Channel.Confirm and Channel.Tx. 42 | 43 | The library is intentionally designed to be synchronous, where responses for 44 | each protocol message are required to be received in an RPC manner. Some 45 | methods have a noWait parameter like Channel.QueueDeclare, and some methods are 46 | asynchronous like Channel.Publish. The error values should still be checked for 47 | these methods as they will indicate IO failures like when the underlying 48 | connection closes. 49 | 50 | # Asynchronous Events 51 | 52 | Clients of this library may be interested in receiving some of the protocol 53 | messages other than Deliveries like basic.ack methods while a channel is in 54 | confirm mode. 55 | 56 | The Notify* methods with Connection and Channel receivers model the pattern of 57 | asynchronous events like closes due to exceptions, or messages that are sent out 58 | of band from an RPC call like basic.ack or basic.flow. 59 | 60 | Any asynchronous events, including Deliveries and Publishings must always have 61 | a receiver until the corresponding chans are closed. Without asynchronous 62 | receivers, the synchronous methods will block. 63 | 64 | # Use Case 65 | 66 | It's important as a client to an AMQP topology to ensure the state of the 67 | broker matches your expectations. For both publish and consume use cases, 68 | make sure you declare the queues, exchanges and bindings you expect to exist 69 | prior to calling [Channel.PublishWithContext] or [Channel.Consume]. 70 | 71 | // Connections start with amqp.Dial() typically from a command line argument 72 | // or environment variable. 73 | connection, err := amqp.Dial(os.Getenv("AMQP_URL")) 74 | 75 | // To cleanly shutdown by flushing kernel buffers, make sure to close and 76 | // wait for the response. 77 | defer connection.Close() 78 | 79 | // Most operations happen on a channel. If any error is returned on a 80 | // channel, the channel will no longer be valid, throw it away and try with 81 | // a different channel. If you use many channels, it's useful for the 82 | // server to 83 | channel, err := connection.Channel() 84 | 85 | // Declare your topology here, if it doesn't exist, it will be created, if 86 | // it existed already and is not what you expect, then that's considered an 87 | // error. 88 | 89 | // Use your connection on this topology with either Publish or Consume, or 90 | // inspect your queues with QueueInspect. It's unwise to mix Publish and 91 | // Consume to let TCP do its job well. 92 | 93 | # SSL/TLS - Secure connections 94 | 95 | When Dial encounters an amqps:// scheme, it will use the zero value of a 96 | tls.Config. This will only perform server certificate and host verification. 97 | 98 | Use DialTLS when you wish to provide a client certificate (recommended), include 99 | a private certificate authority's certificate in the cert chain for server 100 | validity, or run insecure by not verifying the server certificate. DialTLS will 101 | use the provided tls.Config when it encounters an amqps:// scheme and will dial 102 | a plain connection when it encounters an amqp:// scheme. 103 | 104 | SSL/TLS in RabbitMQ is documented here: http://www.rabbitmq.com/ssl.html 105 | 106 | # Best practises for Connection and Channel notifications: 107 | 108 | In order to be notified when a connection or channel gets closed, both 109 | structures offer the possibility to register channels using 110 | [Channel.NotifyClose] and [Connection.NotifyClose] functions: 111 | 112 | notifyConnCloseCh := conn.NotifyClose(make(chan *amqp.Error, 1)) 113 | 114 | No errors will be sent in case of a graceful connection close. In case of a 115 | non-graceful closure due to e.g. network issue, or forced connection closure 116 | from the Management UI, the error will be notified synchronously by the library. 117 | 118 | The library sends to notification channels just once. After sending a 119 | notification to all channels, the library closes all registered notification 120 | channels. After receiving a notification, the application should create and 121 | register a new channel. To avoid deadlocks in the library, it is necessary to 122 | consume from the channels. This could be done inside a different goroutine with 123 | a select listening on the two channels inside a for loop like: 124 | 125 | go func() { 126 | for notifyConnClose != nil || notifyChanClose != nil { 127 | select { 128 | case err, ok := <-notifyConnClose: 129 | if !ok { 130 | notifyConnClose = nil 131 | } else { 132 | fmt.Printf("connection closed, error %s", err) 133 | } 134 | case err, ok := <-notifyChanClose: 135 | if !ok { 136 | notifyChanClose = nil 137 | } else { 138 | fmt.Printf("channel closed, error %s", err) 139 | } 140 | } 141 | } 142 | }() 143 | 144 | It is strongly recommended to use buffered channels to avoid deadlocks inside 145 | the library. 146 | 147 | # Best practises for NotifyPublish notifications: 148 | 149 | Using [Channel.NotifyPublish] allows the caller of the library to be notified, 150 | through a go channel, when a message has been received and confirmed by the 151 | broker. It's advisable to wait for all Confirmations to arrive before calling 152 | [Channel.Close] or [Connection.Close]. It is also necessary to consume from this 153 | channel until it gets closed. The library sends synchronously to the registered channel. 154 | It is advisable to use a buffered channel, with capacity set to the maximum acceptable 155 | number of unconfirmed messages. 156 | 157 | It is important to consume from the confirmation channel at all times, in order to avoid 158 | deadlocks in the library. 159 | */ 160 | package amqp091 161 | -------------------------------------------------------------------------------- /example_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091_test 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "log" 12 | "os" 13 | "sync" 14 | "time" 15 | 16 | amqp "github.com/rabbitmq/amqp091-go" 17 | ) 18 | 19 | // This exports a Client object that wraps this library. It 20 | // automatically reconnects when the connection fails, and 21 | // blocks all pushes until the connection succeeds. It also 22 | // confirms every outgoing message, so none are lost. 23 | // It doesn't automatically ack each message, but leaves that 24 | // to the parent process, since it is usage-dependent. 25 | // 26 | // Try running this in one terminal, and rabbitmq-server in another. 27 | // 28 | // Stop & restart RabbitMQ to see how the queue reacts. 29 | func Example_publish() { 30 | queueName := "job_queue" 31 | addr := "amqp://guest:guest@localhost:5672/" 32 | queue := New(queueName, addr) 33 | message := []byte("message") 34 | 35 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*20)) 36 | defer cancel() 37 | loop: 38 | for { 39 | select { 40 | // Attempt to push a message every 2 seconds 41 | case <-time.After(time.Second * 2): 42 | if err := queue.Push(message); err != nil { 43 | log.Printf("Push failed: %s\n", err) 44 | } else { 45 | log.Println("Push succeeded!") 46 | } 47 | case <-ctx.Done(): 48 | if err := queue.Close(); err != nil { 49 | log.Printf("Close failed: %s\n", err) 50 | } 51 | break loop 52 | } 53 | } 54 | } 55 | 56 | func Example_consume() { 57 | queueName := "job_queue" 58 | addr := "amqp://guest:guest@localhost:5672/" 59 | queue := New(queueName, addr) 60 | 61 | // Give the connection sometime to set up 62 | <-time.After(time.Second) 63 | 64 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) 65 | defer cancel() 66 | 67 | deliveries, err := queue.Consume() 68 | if err != nil { 69 | log.Printf("Could not start consuming: %s\n", err) 70 | return 71 | } 72 | 73 | // This channel will receive a notification when a channel closed event 74 | // happens. This must be different from Client.notifyChanClose because the 75 | // library sends only one notification and Client.notifyChanClose already has 76 | // a receiver in handleReconnect(). 77 | // Recommended to make it buffered to avoid deadlocks 78 | chClosedCh := make(chan *amqp.Error, 1) 79 | queue.channel.NotifyClose(chClosedCh) 80 | 81 | for { 82 | select { 83 | case <-ctx.Done(): 84 | err := queue.Close() 85 | if err != nil { 86 | log.Printf("Close failed: %s\n", err) 87 | } 88 | return 89 | 90 | case amqErr := <-chClosedCh: 91 | // This case handles the event of closed channel e.g. abnormal shutdown 92 | log.Printf("AMQP Channel closed due to: %s\n", amqErr) 93 | 94 | deliveries, err = queue.Consume() 95 | if err != nil { 96 | // If the AMQP channel is not ready, it will continue the loop. Next 97 | // iteration will enter this case because chClosedCh is closed by the 98 | // library 99 | log.Println("Error trying to consume, will try again") 100 | continue 101 | } 102 | 103 | // Re-set channel to receive notifications 104 | // The library closes this channel after abnormal shutdown 105 | chClosedCh = make(chan *amqp.Error, 1) 106 | queue.channel.NotifyClose(chClosedCh) 107 | 108 | case delivery := <-deliveries: 109 | // Ack a message every 2 seconds 110 | log.Printf("Received message: %s\n", delivery.Body) 111 | if err := delivery.Ack(false); err != nil { 112 | log.Printf("Error acknowledging message: %s\n", err) 113 | } 114 | <-time.After(time.Second * 2) 115 | } 116 | } 117 | } 118 | 119 | // Client is the base struct for handling connection recovery, consumption and 120 | // publishing. Note that this struct has an internal mutex to safeguard against 121 | // data races. As you develop and iterate over this example, you may need to add 122 | // further locks, or safeguards, to keep your application safe from data races 123 | type Client struct { 124 | m *sync.Mutex 125 | queueName string 126 | logger *log.Logger 127 | connection *amqp.Connection 128 | channel *amqp.Channel 129 | done chan bool 130 | notifyConnClose chan *amqp.Error 131 | notifyChanClose chan *amqp.Error 132 | notifyConfirm chan amqp.Confirmation 133 | isReady bool 134 | } 135 | 136 | const ( 137 | // When reconnecting to the server after connection failure 138 | reconnectDelay = 5 * time.Second 139 | 140 | // When setting up the channel after a channel exception 141 | reInitDelay = 2 * time.Second 142 | 143 | // When resending messages the server didn't confirm 144 | resendDelay = 5 * time.Second 145 | ) 146 | 147 | var ( 148 | errNotConnected = errors.New("not connected to a server") 149 | errAlreadyClosed = errors.New("already closed: not connected to the server") 150 | errShutdown = errors.New("client is shutting down") 151 | ) 152 | 153 | // New creates a new consumer state instance, and automatically 154 | // attempts to connect to the server. 155 | func New(queueName, addr string) *Client { 156 | client := Client{ 157 | m: &sync.Mutex{}, 158 | logger: log.New(os.Stdout, "", log.LstdFlags), 159 | queueName: queueName, 160 | done: make(chan bool), 161 | } 162 | go client.handleReconnect(addr) 163 | return &client 164 | } 165 | 166 | // handleReconnect will wait for a connection error on 167 | // notifyConnClose, and then continuously attempt to reconnect. 168 | func (client *Client) handleReconnect(addr string) { 169 | for { 170 | client.m.Lock() 171 | client.isReady = false 172 | client.m.Unlock() 173 | 174 | client.logger.Println("Attempting to connect") 175 | 176 | conn, err := client.connect(addr) 177 | if err != nil { 178 | client.logger.Println("Failed to connect. Retrying...") 179 | 180 | select { 181 | case <-client.done: 182 | return 183 | case <-time.After(reconnectDelay): 184 | } 185 | continue 186 | } 187 | 188 | if done := client.handleReInit(conn); done { 189 | break 190 | } 191 | } 192 | } 193 | 194 | // connect will create a new AMQP connection 195 | func (client *Client) connect(addr string) (*amqp.Connection, error) { 196 | conn, err := amqp.Dial(addr) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | client.changeConnection(conn) 202 | client.logger.Println("Connected!") 203 | return conn, nil 204 | } 205 | 206 | // handleReInit will wait for a channel error 207 | // and then continuously attempt to re-initialize both channels 208 | func (client *Client) handleReInit(conn *amqp.Connection) bool { 209 | for { 210 | client.m.Lock() 211 | client.isReady = false 212 | client.m.Unlock() 213 | 214 | err := client.init(conn) 215 | if err != nil { 216 | client.logger.Println("Failed to initialize channel. Retrying...") 217 | 218 | select { 219 | case <-client.done: 220 | return true 221 | case <-client.notifyConnClose: 222 | client.logger.Println("Connection closed. Reconnecting...") 223 | return false 224 | case <-time.After(reInitDelay): 225 | } 226 | continue 227 | } 228 | 229 | select { 230 | case <-client.done: 231 | return true 232 | case <-client.notifyConnClose: 233 | client.logger.Println("Connection closed. Reconnecting...") 234 | return false 235 | case <-client.notifyChanClose: 236 | client.logger.Println("Channel closed. Re-running init...") 237 | } 238 | } 239 | } 240 | 241 | // init will initialize channel & declare queue 242 | func (client *Client) init(conn *amqp.Connection) error { 243 | ch, err := conn.Channel() 244 | if err != nil { 245 | return err 246 | } 247 | 248 | err = ch.Confirm(false) 249 | if err != nil { 250 | return err 251 | } 252 | _, err = ch.QueueDeclare( 253 | client.queueName, 254 | false, // Durable 255 | false, // Delete when unused 256 | false, // Exclusive 257 | false, // No-wait 258 | nil, // Arguments 259 | ) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | client.changeChannel(ch) 265 | client.m.Lock() 266 | client.isReady = true 267 | client.m.Unlock() 268 | client.logger.Println("Setup!") 269 | 270 | return nil 271 | } 272 | 273 | // changeConnection takes a new connection to the queue, 274 | // and updates the close listener to reflect this. 275 | func (client *Client) changeConnection(connection *amqp.Connection) { 276 | client.connection = connection 277 | client.notifyConnClose = make(chan *amqp.Error, 1) 278 | client.connection.NotifyClose(client.notifyConnClose) 279 | } 280 | 281 | // changeChannel takes a new channel to the queue, 282 | // and updates the channel listeners to reflect this. 283 | func (client *Client) changeChannel(channel *amqp.Channel) { 284 | client.channel = channel 285 | client.notifyChanClose = make(chan *amqp.Error, 1) 286 | client.notifyConfirm = make(chan amqp.Confirmation, 1) 287 | client.channel.NotifyClose(client.notifyChanClose) 288 | client.channel.NotifyPublish(client.notifyConfirm) 289 | } 290 | 291 | // Push will push data onto the queue, and wait for a confirmation. 292 | // This will block until the server sends a confirmation. Errors are 293 | // only returned if the push action itself fails, see UnsafePush. 294 | func (client *Client) Push(data []byte) error { 295 | client.m.Lock() 296 | if !client.isReady { 297 | client.m.Unlock() 298 | return errors.New("failed to push: not connected") 299 | } 300 | client.m.Unlock() 301 | for { 302 | err := client.UnsafePush(data) 303 | if err != nil { 304 | client.logger.Println("Push failed. Retrying...") 305 | select { 306 | case <-client.done: 307 | return errShutdown 308 | case <-time.After(resendDelay): 309 | } 310 | continue 311 | } 312 | confirm := <-client.notifyConfirm 313 | if confirm.Ack { 314 | client.logger.Printf("Push confirmed [%d]!", confirm.DeliveryTag) 315 | return nil 316 | } 317 | } 318 | } 319 | 320 | // UnsafePush will push to the queue without checking for 321 | // confirmation. It returns an error if it fails to connect. 322 | // No guarantees are provided for whether the server will 323 | // receive the message. 324 | func (client *Client) UnsafePush(data []byte) error { 325 | client.m.Lock() 326 | if !client.isReady { 327 | client.m.Unlock() 328 | return errNotConnected 329 | } 330 | client.m.Unlock() 331 | 332 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 333 | defer cancel() 334 | 335 | return client.channel.PublishWithContext( 336 | ctx, 337 | "", // Exchange 338 | client.queueName, // Routing key 339 | false, // Mandatory 340 | false, // Immediate 341 | amqp.Publishing{ 342 | ContentType: "text/plain", 343 | Body: data, 344 | }, 345 | ) 346 | } 347 | 348 | // Consume will continuously put queue items on the channel. 349 | // It is required to call delivery.Ack when it has been 350 | // successfully processed, or delivery.Nack when it fails. 351 | // Ignoring this will cause data to build up on the server. 352 | func (client *Client) Consume() (<-chan amqp.Delivery, error) { 353 | client.m.Lock() 354 | if !client.isReady { 355 | client.m.Unlock() 356 | return nil, errNotConnected 357 | } 358 | client.m.Unlock() 359 | 360 | if err := client.channel.Qos( 361 | 1, // prefetchCount 362 | 0, // prefetchSize 363 | false, // global 364 | ); err != nil { 365 | return nil, err 366 | } 367 | 368 | return client.channel.Consume( 369 | client.queueName, 370 | "", // Consumer 371 | false, // Auto-Ack 372 | false, // Exclusive 373 | false, // No-local 374 | false, // No-Wait 375 | nil, // Args 376 | ) 377 | } 378 | 379 | // Close will cleanly shut down the channel and connection. 380 | func (client *Client) Close() error { 381 | client.m.Lock() 382 | // we read and write isReady in two locations, so we grab the lock and hold onto 383 | // it until we are finished 384 | defer client.m.Unlock() 385 | 386 | if !client.isReady { 387 | return errAlreadyClosed 388 | } 389 | close(client.done) 390 | err := client.channel.Close() 391 | if err != nil { 392 | return err 393 | } 394 | err = client.connection.Close() 395 | if err != nil { 396 | return err 397 | } 398 | 399 | client.isReady = false 400 | return nil 401 | } 402 | -------------------------------------------------------------------------------- /fuzz.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | //go:build gofuzz 7 | // +build gofuzz 8 | 9 | package amqp091 10 | 11 | import "bytes" 12 | 13 | func Fuzz(data []byte) int { 14 | r := reader{bytes.NewReader(data)} 15 | frame, err := r.ReadFrame() 16 | if err != nil { 17 | if frame != nil { 18 | panic("frame is not nil") 19 | } 20 | return 0 21 | } 22 | return 1 23 | } 24 | -------------------------------------------------------------------------------- /gen.ps1: -------------------------------------------------------------------------------- 1 | $DebugPreference = 'Continue' 2 | $ErrorActionPreference = 'Stop' 3 | 4 | Set-PSDebug -Off 5 | Set-StrictMode -Version 'Latest' -ErrorAction 'Stop' -Verbose 6 | 7 | New-Variable -Name curdir -Option Constant -Value $PSScriptRoot 8 | 9 | $specDir = Resolve-Path -LiteralPath (Join-Path -Path $curdir -ChildPath 'spec') 10 | $amqpSpecXml = Resolve-Path -LiteralPath (Join-Path -Path $specDir -ChildPath 'amqp0-9-1.stripped.extended.xml') 11 | $gen = Resolve-Path -LiteralPath (Join-Path -Path $specDir -ChildPath 'gen.go') 12 | $spec091 = Resolve-Path -LiteralPath (Join-Path -Path $curdir -ChildPath 'spec091.go') 13 | 14 | Get-Content -LiteralPath $amqpSpecXml | go run $gen | gofmt | Set-Content -Force -Path $spec091 15 | -------------------------------------------------------------------------------- /gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | go run spec/gen.go < spec/amqp0-9-1.stripped.extended.xml | gofmt > spec091.go 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rabbitmq/amqp091-go 2 | 3 | go 1.20 4 | 5 | require go.uber.org/goleak v1.3.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 3 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 4 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 5 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 6 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 7 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package amqp091 6 | 7 | type Logging interface { 8 | Printf(format string, v ...interface{}) 9 | } 10 | 11 | var Logger Logging = NullLogger{} 12 | 13 | // Enables logging using a custom Logging instance. Note that this is 14 | // not thread safe and should be called at application start 15 | func SetLogger(logger Logging) { 16 | Logger = logger 17 | } 18 | 19 | type NullLogger struct{} 20 | 21 | func (l NullLogger) Printf(format string, v ...interface{}) { 22 | } 23 | -------------------------------------------------------------------------------- /rabbitmq-confs/tls/90-tls.conf: -------------------------------------------------------------------------------- 1 | listeners.ssl.default = 5671 2 | 3 | ssl_options.cacertfile = /certs/cacert.pem 4 | ssl_options.certfile = /certs/cert.pem 5 | ssl_options.keyfile = /certs/key.pem 6 | ssl_options.depth = 2 7 | ssl_options.verify = verify_none 8 | ssl_options.fail_if_no_peer_cert = false 9 | -------------------------------------------------------------------------------- /read.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "bytes" 10 | "encoding/binary" 11 | "errors" 12 | "io" 13 | "time" 14 | ) 15 | 16 | /* 17 | ReadFrame reads a frame from an input stream and returns an interface that can be cast into 18 | one of the following: 19 | 20 | methodFrame 21 | PropertiesFrame 22 | bodyFrame 23 | heartbeatFrame 24 | 25 | 2.3.5 frame Details 26 | 27 | All frames consist of a header (7 octets), a payload of arbitrary size, and a 28 | 'frame-end' octet that detects malformed frames: 29 | 30 | 0 1 3 7 size+7 size+8 31 | +------+---------+-------------+ +------------+ +-----------+ 32 | | type | channel | size | | payload | | frame-end | 33 | +------+---------+-------------+ +------------+ +-----------+ 34 | octet short long size octets octet 35 | 36 | To read a frame, we: 37 | 1. Read the header and check the frame type and channel. 38 | 2. Depending on the frame type, we read the payload and process it. 39 | 3. Read the frame end octet. 40 | 41 | In realistic implementations where performance is a concern, we would use 42 | “read-ahead buffering” or 43 | 44 | “gathering reads” to avoid doing three separate system calls to read a frame. 45 | */ 46 | func (r *reader) ReadFrame() (frame frame, err error) { 47 | var scratch [7]byte 48 | 49 | if _, err = io.ReadFull(r.r, scratch[:7]); err != nil { 50 | return 51 | } 52 | 53 | typ := scratch[0] 54 | channel := binary.BigEndian.Uint16(scratch[1:3]) 55 | size := binary.BigEndian.Uint32(scratch[3:7]) 56 | 57 | switch typ { 58 | case frameMethod: 59 | if frame, err = r.parseMethodFrame(channel, size); err != nil { 60 | return 61 | } 62 | 63 | case frameHeader: 64 | if frame, err = r.parseHeaderFrame(channel, size); err != nil { 65 | return 66 | } 67 | 68 | case frameBody: 69 | if frame, err = r.parseBodyFrame(channel, size); err != nil { 70 | return nil, err 71 | } 72 | 73 | case frameHeartbeat: 74 | if frame, err = r.parseHeartbeatFrame(channel, size); err != nil { 75 | return 76 | } 77 | 78 | default: 79 | return nil, ErrFrame 80 | } 81 | 82 | if _, err = io.ReadFull(r.r, scratch[:1]); err != nil { 83 | return nil, err 84 | } 85 | 86 | if scratch[0] != frameEnd { 87 | return nil, ErrFrame 88 | } 89 | 90 | return 91 | } 92 | 93 | func readShortstr(r io.Reader) (v string, err error) { 94 | var length uint8 95 | if err = binary.Read(r, binary.BigEndian, &length); err != nil { 96 | return 97 | } 98 | 99 | bytes := make([]byte, length) 100 | if _, err = io.ReadFull(r, bytes); err != nil { 101 | return 102 | } 103 | return string(bytes), nil 104 | } 105 | 106 | func readLongstr(r io.Reader) (v string, err error) { 107 | var length uint32 108 | if err = binary.Read(r, binary.BigEndian, &length); err != nil { 109 | return 110 | } 111 | 112 | // slices can't be longer than max int32 value 113 | if length > (^uint32(0) >> 1) { 114 | return 115 | } 116 | 117 | bytes := make([]byte, length) 118 | if _, err = io.ReadFull(r, bytes); err != nil { 119 | return 120 | } 121 | return string(bytes), nil 122 | } 123 | 124 | func readDecimal(r io.Reader) (v Decimal, err error) { 125 | if err = binary.Read(r, binary.BigEndian, &v.Scale); err != nil { 126 | return 127 | } 128 | if err = binary.Read(r, binary.BigEndian, &v.Value); err != nil { 129 | return 130 | } 131 | return 132 | } 133 | 134 | func readTimestamp(r io.Reader) (v time.Time, err error) { 135 | var sec int64 136 | if err = binary.Read(r, binary.BigEndian, &sec); err != nil { 137 | return 138 | } 139 | return time.Unix(sec, 0), nil 140 | } 141 | 142 | /* 143 | 'A': []interface{} 144 | 'D': Decimal 145 | 'F': Table 146 | 'I': int32 147 | 'S': string 148 | 'T': time.Time 149 | 'V': nil 150 | 'b': int8 151 | 'B': byte 152 | 'd': float64 153 | 'f': float32 154 | 'l': int64 155 | 's': int16 156 | 't': bool 157 | 'x': []byte 158 | */ 159 | func readField(r io.Reader) (v interface{}, err error) { 160 | var typ byte 161 | if err = binary.Read(r, binary.BigEndian, &typ); err != nil { 162 | return 163 | } 164 | 165 | switch typ { 166 | case 't': 167 | var value uint8 168 | if err = binary.Read(r, binary.BigEndian, &value); err != nil { 169 | return 170 | } 171 | return value != 0, nil 172 | 173 | case 'B': 174 | var value [1]byte 175 | if _, err = io.ReadFull(r, value[0:1]); err != nil { 176 | return 177 | } 178 | return value[0], nil 179 | 180 | case 'b': 181 | var value int8 182 | if err = binary.Read(r, binary.BigEndian, &value); err != nil { 183 | return 184 | } 185 | return value, nil 186 | 187 | case 's': 188 | var value int16 189 | if err = binary.Read(r, binary.BigEndian, &value); err != nil { 190 | return 191 | } 192 | return value, nil 193 | 194 | case 'I': 195 | var value int32 196 | if err = binary.Read(r, binary.BigEndian, &value); err != nil { 197 | return 198 | } 199 | return value, nil 200 | 201 | case 'l': 202 | var value int64 203 | if err = binary.Read(r, binary.BigEndian, &value); err != nil { 204 | return 205 | } 206 | return value, nil 207 | 208 | case 'f': 209 | var value float32 210 | if err = binary.Read(r, binary.BigEndian, &value); err != nil { 211 | return 212 | } 213 | return value, nil 214 | 215 | case 'd': 216 | var value float64 217 | if err = binary.Read(r, binary.BigEndian, &value); err != nil { 218 | return 219 | } 220 | return value, nil 221 | 222 | case 'D': 223 | return readDecimal(r) 224 | 225 | case 'S': 226 | return readLongstr(r) 227 | 228 | case 'A': 229 | return readArray(r) 230 | 231 | case 'T': 232 | return readTimestamp(r) 233 | 234 | case 'F': 235 | return readTable(r) 236 | 237 | case 'x': 238 | var len int32 239 | if err = binary.Read(r, binary.BigEndian, &len); err != nil { 240 | return nil, err 241 | } 242 | 243 | value := make([]byte, len) 244 | if _, err = io.ReadFull(r, value); err != nil { 245 | return nil, err 246 | } 247 | return value, err 248 | 249 | case 'V': 250 | return nil, nil 251 | } 252 | 253 | return nil, ErrSyntax 254 | } 255 | 256 | /* 257 | Field tables are long strings that contain packed name-value pairs. The 258 | name-value pairs are encoded as short string defining the name, and octet 259 | defining the values type and then the value itself. The valid field types for 260 | tables are an extension of the native integer, bit, string, and timestamp 261 | types, and are shown in the grammar. Multi-octet integer fields are always 262 | held in network byte order. 263 | */ 264 | func readTable(r io.Reader) (table Table, err error) { 265 | var nested bytes.Buffer 266 | var str string 267 | 268 | if str, err = readLongstr(r); err != nil { 269 | return 270 | } 271 | 272 | nested.WriteString(str) 273 | 274 | table = make(Table) 275 | 276 | for nested.Len() > 0 { 277 | var key string 278 | var value interface{} 279 | 280 | if key, err = readShortstr(&nested); err != nil { 281 | return 282 | } 283 | 284 | if value, err = readField(&nested); err != nil { 285 | return 286 | } 287 | 288 | table[key] = value 289 | } 290 | 291 | return 292 | } 293 | 294 | func readArray(r io.Reader) (arr []interface{}, err error) { 295 | var size uint32 296 | 297 | if err = binary.Read(r, binary.BigEndian, &size); err != nil { 298 | return nil, err 299 | } 300 | 301 | var ( 302 | lim = &io.LimitedReader{R: r, N: int64(size)} 303 | field interface{} 304 | ) 305 | 306 | for { 307 | if field, err = readField(lim); err != nil { 308 | if err == io.EOF { 309 | break 310 | } 311 | return nil, err 312 | } 313 | arr = append(arr, field) 314 | } 315 | 316 | return arr, nil 317 | } 318 | 319 | // Checks if this bit mask matches the flags bitset 320 | func hasProperty(mask uint16, prop int) bool { 321 | return int(mask)&prop > 0 322 | } 323 | 324 | func (r *reader) parseHeaderFrame(channel uint16, size uint32) (frame frame, err error) { 325 | hf := &headerFrame{ 326 | ChannelId: channel, 327 | } 328 | 329 | if err = binary.Read(r.r, binary.BigEndian, &hf.ClassId); err != nil { 330 | return 331 | } 332 | 333 | if err = binary.Read(r.r, binary.BigEndian, &hf.weight); err != nil { 334 | return 335 | } 336 | 337 | if err = binary.Read(r.r, binary.BigEndian, &hf.Size); err != nil { 338 | return 339 | } 340 | 341 | var flags uint16 342 | 343 | if err = binary.Read(r.r, binary.BigEndian, &flags); err != nil { 344 | return 345 | } 346 | 347 | if hasProperty(flags, flagContentType) { 348 | if hf.Properties.ContentType, err = readShortstr(r.r); err != nil { 349 | return 350 | } 351 | } 352 | if hasProperty(flags, flagContentEncoding) { 353 | if hf.Properties.ContentEncoding, err = readShortstr(r.r); err != nil { 354 | return 355 | } 356 | } 357 | if hasProperty(flags, flagHeaders) { 358 | if hf.Properties.Headers, err = readTable(r.r); err != nil { 359 | return 360 | } 361 | } 362 | if hasProperty(flags, flagDeliveryMode) { 363 | if err = binary.Read(r.r, binary.BigEndian, &hf.Properties.DeliveryMode); err != nil { 364 | return 365 | } 366 | } 367 | if hasProperty(flags, flagPriority) { 368 | if err = binary.Read(r.r, binary.BigEndian, &hf.Properties.Priority); err != nil { 369 | return 370 | } 371 | } 372 | if hasProperty(flags, flagCorrelationId) { 373 | if hf.Properties.CorrelationId, err = readShortstr(r.r); err != nil { 374 | return 375 | } 376 | } 377 | if hasProperty(flags, flagReplyTo) { 378 | if hf.Properties.ReplyTo, err = readShortstr(r.r); err != nil { 379 | return 380 | } 381 | } 382 | if hasProperty(flags, flagExpiration) { 383 | if hf.Properties.Expiration, err = readShortstr(r.r); err != nil { 384 | return 385 | } 386 | } 387 | if hasProperty(flags, flagMessageId) { 388 | if hf.Properties.MessageId, err = readShortstr(r.r); err != nil { 389 | return 390 | } 391 | } 392 | if hasProperty(flags, flagTimestamp) { 393 | if hf.Properties.Timestamp, err = readTimestamp(r.r); err != nil { 394 | return 395 | } 396 | } 397 | if hasProperty(flags, flagType) { 398 | if hf.Properties.Type, err = readShortstr(r.r); err != nil { 399 | return 400 | } 401 | } 402 | if hasProperty(flags, flagUserId) { 403 | if hf.Properties.UserId, err = readShortstr(r.r); err != nil { 404 | return 405 | } 406 | } 407 | if hasProperty(flags, flagAppId) { 408 | if hf.Properties.AppId, err = readShortstr(r.r); err != nil { 409 | return 410 | } 411 | } 412 | if hasProperty(flags, flagReserved1) { 413 | if hf.Properties.reserved1, err = readShortstr(r.r); err != nil { 414 | return 415 | } 416 | } 417 | 418 | return hf, nil 419 | } 420 | 421 | func (r *reader) parseBodyFrame(channel uint16, size uint32) (frame frame, err error) { 422 | bf := &bodyFrame{ 423 | ChannelId: channel, 424 | Body: make([]byte, size), 425 | } 426 | 427 | if _, err = io.ReadFull(r.r, bf.Body); err != nil { 428 | return nil, err 429 | } 430 | 431 | return bf, nil 432 | } 433 | 434 | var errHeartbeatPayload = errors.New("Heartbeats should not have a payload") 435 | 436 | func (r *reader) parseHeartbeatFrame(channel uint16, size uint32) (frame frame, err error) { 437 | hf := &heartbeatFrame{ 438 | ChannelId: channel, 439 | } 440 | 441 | if size > 0 { 442 | return nil, errHeartbeatPayload 443 | } 444 | 445 | return hf, nil 446 | } 447 | -------------------------------------------------------------------------------- /read_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestGoFuzzCrashers(t *testing.T) { 14 | if testing.Short() { 15 | t.Skip("excessive allocation") 16 | } 17 | 18 | testData := []string{ 19 | "\b000000", 20 | "\x02\x16\x10�[��\t\xbdui�" + "\x10\x01\x00\xff\xbf\xef\xbfサn\x99\x00\x10r", 21 | "\x0300\x00\x00\x00\x040000", 22 | } 23 | 24 | for idx, testStr := range testData { 25 | r := reader{strings.NewReader(testStr)} 26 | frame, err := r.ReadFrame() 27 | if err != nil && frame != nil { 28 | t.Errorf("%d. frame is not nil: %#v err = %v", idx, frame, err) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /reconnect_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091_test 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "os" 12 | 13 | amqp "github.com/rabbitmq/amqp091-go" 14 | ) 15 | 16 | // Every connection should declare the topology they expect 17 | func setup(url, queue string) (*amqp.Connection, *amqp.Channel, error) { 18 | conn, err := amqp.Dial(url) 19 | if err != nil { 20 | return nil, nil, err 21 | } 22 | 23 | ch, err := conn.Channel() 24 | if err != nil { 25 | return nil, nil, err 26 | } 27 | 28 | if _, err := ch.QueueDeclare(queue, false, true, false, false, nil); err != nil { 29 | return nil, nil, err 30 | } 31 | 32 | return conn, ch, nil 33 | } 34 | 35 | func consume(url, queue string) (*amqp.Connection, <-chan amqp.Delivery, error) { 36 | conn, ch, err := setup(url, queue) 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | 41 | // Indicate we only want 1 message to acknowledge at a time. 42 | if err := ch.Qos(1, 0, false); err != nil { 43 | return nil, nil, err 44 | } 45 | 46 | // Exclusive consumer 47 | deliveries, err := ch.Consume(queue, "", false, true, false, false, nil) 48 | 49 | return conn, deliveries, err 50 | } 51 | 52 | func ExampleConnection_reconnect() { 53 | if url := os.Getenv("AMQP_URL"); url != "" { 54 | queue := "example.reconnect" 55 | 56 | // The connection/channel for publishing to interleave the ingress messages 57 | // between reconnects, shares the same topology as the consumer. If we rather 58 | // sent all messages up front, the first consumer would receive every message. 59 | // We would rather show how the messages are not lost between reconnects. 60 | con, pub, err := setup(url, queue) 61 | if err != nil { 62 | fmt.Println("err publisher setup:", err) 63 | return 64 | } 65 | defer con.Close() 66 | 67 | // Purge the queue from the publisher side to establish initial state 68 | if _, err := pub.QueuePurge(queue, false); err != nil { 69 | fmt.Println("err purge:", err) 70 | return 71 | } 72 | 73 | // Reconnect simulation, should be for { ... } in production 74 | for i := 1; i <= 3; i++ { 75 | fmt.Println("connect") 76 | 77 | conn, deliveries, err := consume(url, queue) 78 | if err != nil { 79 | fmt.Println("err consume:", err) 80 | return 81 | } 82 | 83 | // Simulate a producer on a different connection showing that consumers 84 | // continue where they were left off after each reconnect. 85 | if err := pub.PublishWithContext(context.TODO(), "", queue, false, false, amqp.Publishing{ 86 | Body: []byte(fmt.Sprintf("%d", i)), 87 | }); err != nil { 88 | fmt.Println("err publish:", err) 89 | return 90 | } 91 | 92 | // Simulates a consumer that when the range finishes, will setup a new 93 | // session and begin ranging over the deliveries again. 94 | for msg := range deliveries { 95 | fmt.Println(string(msg.Body)) 96 | if e := msg.Ack(false); e != nil { 97 | fmt.Println("ack error: ", e) 98 | } 99 | 100 | // Simulate an error like a server restart, loss of route or operator 101 | // intervention that results in the connection terminating 102 | go conn.Close() 103 | } 104 | } 105 | } else { 106 | // pass with expected output when not running in an integration 107 | // environment. 108 | fmt.Println("connect") 109 | fmt.Println("1") 110 | fmt.Println("connect") 111 | fmt.Println("2") 112 | fmt.Println("connect") 113 | fmt.Println("3") 114 | } 115 | 116 | // Output: 117 | // connect 118 | // 1 119 | // connect 120 | // 2 121 | // connect 122 | // 3 123 | } 124 | -------------------------------------------------------------------------------- /return.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "time" 10 | ) 11 | 12 | // Return captures a flattened struct of fields returned by the server when a 13 | // Publishing is unable to be delivered either due to the `mandatory` flag set 14 | // and no route found, or `immediate` flag set and no free consumer. 15 | type Return struct { 16 | ReplyCode uint16 // reason 17 | ReplyText string // description 18 | Exchange string // basic.publish exchange 19 | RoutingKey string // basic.publish routing key 20 | 21 | // Properties 22 | ContentType string // MIME content type 23 | ContentEncoding string // MIME content encoding 24 | Headers Table // Application or header exchange table 25 | DeliveryMode uint8 // queue implementation use - non-persistent (1) or persistent (2) 26 | Priority uint8 // queue implementation use - 0 to 9 27 | CorrelationId string // application use - correlation identifier 28 | ReplyTo string // application use - address to to reply to (ex: RPC) 29 | Expiration string // implementation use - message expiration spec 30 | MessageId string // application use - message identifier 31 | Timestamp time.Time // application use - message timestamp 32 | Type string // application use - message type name 33 | UserId string // application use - creating user id 34 | AppId string // application use - creating application 35 | 36 | Body []byte 37 | } 38 | 39 | func newReturn(msg basicReturn) *Return { 40 | props, body := msg.getContent() 41 | 42 | return &Return{ 43 | ReplyCode: msg.ReplyCode, 44 | ReplyText: msg.ReplyText, 45 | Exchange: msg.Exchange, 46 | RoutingKey: msg.RoutingKey, 47 | 48 | Headers: props.Headers, 49 | ContentType: props.ContentType, 50 | ContentEncoding: props.ContentEncoding, 51 | DeliveryMode: props.DeliveryMode, 52 | Priority: props.Priority, 53 | CorrelationId: props.CorrelationId, 54 | ReplyTo: props.ReplyTo, 55 | Expiration: props.Expiration, 56 | MessageId: props.MessageId, 57 | Timestamp: props.Timestamp, 58 | Type: props.Type, 59 | UserId: props.UserId, 60 | AppId: props.AppId, 61 | 62 | Body: body, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /shared_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "io" 10 | "testing" 11 | ) 12 | 13 | type pipe struct { 14 | r *io.PipeReader 15 | w *io.PipeWriter 16 | } 17 | 18 | func (p pipe) Read(b []byte) (int, error) { 19 | return p.r.Read(b) 20 | } 21 | 22 | func (p pipe) Write(b []byte) (int, error) { 23 | return p.w.Write(b) 24 | } 25 | 26 | func (p pipe) Close() error { 27 | p.r.Close() 28 | p.w.Close() 29 | return nil 30 | } 31 | 32 | type logIO struct { 33 | t *testing.T 34 | prefix string 35 | proxy io.ReadWriteCloser 36 | } 37 | 38 | func (log *logIO) Read(p []byte) (n int, err error) { 39 | return log.proxy.Read(p) 40 | } 41 | 42 | func (log *logIO) Write(p []byte) (n int, err error) { 43 | return log.proxy.Write(p) 44 | } 45 | 46 | func (log *logIO) Close() (err error) { 47 | return log.proxy.Close() 48 | } 49 | -------------------------------------------------------------------------------- /spec/gen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | //go:build ignore 7 | // +build ignore 8 | 9 | package main 10 | 11 | import ( 12 | "bytes" 13 | "encoding/xml" 14 | "errors" 15 | "fmt" 16 | "io" 17 | "log" 18 | "os" 19 | "regexp" 20 | "strings" 21 | "text/template" 22 | ) 23 | 24 | var ( 25 | ErrUnknownType = errors.New("unknown field type in gen") 26 | ErrUnknownDomain = errors.New("unknown domain type in gen") 27 | ) 28 | 29 | var amqpTypeToNative = map[string]string{ 30 | "bit": "bool", 31 | "octet": "byte", 32 | "shortshort": "uint8", 33 | "short": "uint16", 34 | "long": "uint32", 35 | "longlong": "uint64", 36 | "timestamp": "time.Time", 37 | "table": "Table", 38 | "shortstr": "string", 39 | "longstr": "string", 40 | } 41 | 42 | type Rule struct { 43 | Name string `xml:"name,attr"` 44 | Docs []string `xml:"doc"` 45 | } 46 | 47 | type Doc struct { 48 | Type string `xml:"type,attr"` 49 | Body string `xml:",innerxml"` 50 | } 51 | 52 | type Chassis struct { 53 | Name string `xml:"name,attr"` 54 | Implement string `xml:"implement,attr"` 55 | } 56 | 57 | type Assert struct { 58 | Check string `xml:"check,attr"` 59 | Value string `xml:"value,attr"` 60 | Method string `xml:"method,attr"` 61 | } 62 | 63 | type Field struct { 64 | Name string `xml:"name,attr"` 65 | Domain string `xml:"domain,attr"` 66 | Type string `xml:"type,attr"` 67 | Label string `xml:"label,attr"` 68 | Reserved bool `xml:"reserved,attr"` 69 | Docs []Doc `xml:"doc"` 70 | Asserts []Assert `xml:"assert"` 71 | } 72 | 73 | type Response struct { 74 | Name string `xml:"name,attr"` 75 | } 76 | 77 | type Method struct { 78 | Name string `xml:"name,attr"` 79 | Response Response `xml:"response"` 80 | Synchronous bool `xml:"synchronous,attr"` 81 | Content bool `xml:"content,attr"` 82 | Index string `xml:"index,attr"` 83 | Label string `xml:"label,attr"` 84 | Docs []Doc `xml:"doc"` 85 | Rules []Rule `xml:"rule"` 86 | Fields []Field `xml:"field"` 87 | Chassis []Chassis `xml:"chassis"` 88 | } 89 | 90 | type Class struct { 91 | Name string `xml:"name,attr"` 92 | Handler string `xml:"handler,attr"` 93 | Index string `xml:"index,attr"` 94 | Label string `xml:"label,attr"` 95 | Docs []Doc `xml:"doc"` 96 | Methods []Method `xml:"method"` 97 | Chassis []Chassis `xml:"chassis"` 98 | } 99 | 100 | type Domain struct { 101 | Name string `xml:"name,attr"` 102 | Type string `xml:"type,attr"` 103 | Label string `xml:"label,attr"` 104 | Rules []Rule `xml:"rule"` 105 | Docs []Doc `xml:"doc"` 106 | } 107 | 108 | type Constant struct { 109 | Name string `xml:"name,attr"` 110 | Value int `xml:"value,attr"` 111 | Class string `xml:"class,attr"` 112 | Doc string `xml:"doc"` 113 | } 114 | 115 | type Amqp struct { 116 | Major int `xml:"major,attr"` 117 | Minor int `xml:"minor,attr"` 118 | Port int `xml:"port,attr"` 119 | Comment string `xml:"comment,attr"` 120 | 121 | Constants []Constant `xml:"constant"` 122 | Domains []Domain `xml:"domain"` 123 | Classes []Class `xml:"class"` 124 | } 125 | 126 | type renderer struct { 127 | Root Amqp 128 | bitcounter int 129 | } 130 | 131 | type fieldset struct { 132 | AmqpType string 133 | NativeType string 134 | Fields []Field 135 | *renderer 136 | } 137 | 138 | var ( 139 | helpers = template.FuncMap{ 140 | "public": public, 141 | "private": private, 142 | "clean": clean, 143 | } 144 | 145 | packageTemplate = template.Must(template.New("package").Funcs(helpers).Parse(` 146 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 147 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 148 | // Use of this source code is governed by a BSD-style 149 | // license that can be found in the LICENSE file. 150 | 151 | /* GENERATED FILE - DO NOT EDIT */ 152 | /* Rebuild from the spec/gen.go tool */ 153 | 154 | {{with .Root}} 155 | package amqp091 156 | 157 | import ( 158 | "fmt" 159 | "encoding/binary" 160 | "io" 161 | ) 162 | 163 | // Error codes that can be sent from the server during a connection or 164 | // channel exception or used by the client to indicate a class of error like 165 | // ErrCredentials. The text of the error is likely more interesting than 166 | // these constants. 167 | const ( 168 | {{range $c := .Constants}} 169 | {{if $c.IsError}}{{.Name | public}}{{else}}{{.Name | private}}{{end}} = {{.Value}}{{end}} 170 | ) 171 | 172 | func isSoftExceptionCode(code int) bool { 173 | switch code { 174 | {{range $c := .Constants}} {{if $c.IsSoftError}} case {{$c.Value}}: 175 | return true 176 | {{end}}{{end}} 177 | } 178 | return false 179 | } 180 | 181 | {{range .Classes}} 182 | {{$class := .}} 183 | {{range .Methods}} 184 | {{$method := .}} 185 | {{$struct := $.StructName $class.Name $method.Name}} 186 | {{if .Docs}}/* {{range .Docs}} {{.Body | clean}} {{end}} */{{end}} 187 | type {{$struct}} struct { 188 | {{range .Fields}} 189 | {{$.FieldName .}} {{$.FieldType . | $.NativeType}} {{if .Label}}// {{.Label}}{{end}}{{end}} 190 | {{if .Content}}Properties properties 191 | Body []byte{{end}} 192 | } 193 | 194 | func (msg *{{$struct}}) id() (uint16, uint16) { 195 | return {{$class.Index}}, {{$method.Index}} 196 | } 197 | 198 | func (msg *{{$struct}}) wait() (bool) { 199 | return {{.Synchronous}}{{if $.HasField "NoWait" .}} && !msg.NoWait{{end}} 200 | } 201 | 202 | {{if .Content}} 203 | func (msg *{{$struct}}) getContent() (properties, []byte) { 204 | return msg.Properties, msg.Body 205 | } 206 | 207 | func (msg *{{$struct}}) setContent(props properties, body []byte) { 208 | msg.Properties, msg.Body = props, body 209 | } 210 | {{end}} 211 | func (msg *{{$struct}}) write(w io.Writer) (err error) { 212 | {{if $.HasType "bit" $method}}var bits byte{{end}} 213 | {{.Fields | $.Fieldsets | $.Partial "enc-"}} 214 | return 215 | } 216 | 217 | func (msg *{{$struct}}) read(r io.Reader) (err error) { 218 | {{if $.HasType "bit" $method}}var bits byte{{end}} 219 | {{.Fields | $.Fieldsets | $.Partial "dec-"}} 220 | return 221 | } 222 | {{end}} 223 | {{end}} 224 | 225 | func (r *reader) parseMethodFrame(channel uint16, size uint32) (f frame, err error) { 226 | mf := &methodFrame { 227 | ChannelId: channel, 228 | } 229 | 230 | if err = binary.Read(r.r, binary.BigEndian, &mf.ClassId); err != nil { 231 | return 232 | } 233 | 234 | if err = binary.Read(r.r, binary.BigEndian, &mf.MethodId); err != nil { 235 | return 236 | } 237 | 238 | switch mf.ClassId { 239 | {{range .Classes}} 240 | {{$class := .}} 241 | case {{.Index}}: // {{.Name}} 242 | switch mf.MethodId { 243 | {{range .Methods}} 244 | case {{.Index}}: // {{$class.Name}} {{.Name}} 245 | // fmt.Println("NextMethod: class:{{$class.Index}} method:{{.Index}}") 246 | method := &{{$.StructName $class.Name .Name}}{} 247 | if err = method.read(r.r); err != nil { 248 | return 249 | } 250 | mf.Method = method 251 | {{end}} 252 | default: 253 | return nil, fmt.Errorf("Bad method frame, unknown method %d for class %d", mf.MethodId, mf.ClassId) 254 | } 255 | {{end}} 256 | default: 257 | return nil, fmt.Errorf("Bad method frame, unknown class %d", mf.ClassId) 258 | } 259 | 260 | return mf, nil 261 | } 262 | {{end}} 263 | 264 | {{define "enc-bit"}} 265 | {{range $off, $field := .Fields}} 266 | if msg.{{$field | $.FieldName}} { bits |= 1 << {{$off}} } 267 | {{end}} 268 | if err = binary.Write(w, binary.BigEndian, bits); err != nil { return } 269 | {{end}} 270 | {{define "enc-octet"}} 271 | {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return } 272 | {{end}} 273 | {{end}} 274 | {{define "enc-shortshort"}} 275 | {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return } 276 | {{end}} 277 | {{end}} 278 | {{define "enc-short"}} 279 | {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return } 280 | {{end}} 281 | {{end}} 282 | {{define "enc-long"}} 283 | {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return } 284 | {{end}} 285 | {{end}} 286 | {{define "enc-longlong"}} 287 | {{range .Fields}} if err = binary.Write(w, binary.BigEndian, msg.{{. | $.FieldName}}); err != nil { return } 288 | {{end}} 289 | {{end}} 290 | {{define "enc-timestamp"}} 291 | {{range .Fields}} if err = writeTimestamp(w, msg.{{. | $.FieldName}}); err != nil { return } 292 | {{end}} 293 | {{end}} 294 | {{define "enc-shortstr"}} 295 | {{range .Fields}} if err = writeShortstr(w, msg.{{. | $.FieldName}}); err != nil { return } 296 | {{end}} 297 | {{end}} 298 | {{define "enc-longstr"}} 299 | {{range .Fields}} if err = writeLongstr(w, msg.{{. | $.FieldName}}); err != nil { return } 300 | {{end}} 301 | {{end}} 302 | {{define "enc-table"}} 303 | {{range .Fields}} if err = writeTable(w, msg.{{. | $.FieldName}}); err != nil { return } 304 | {{end}} 305 | {{end}} 306 | 307 | {{define "dec-bit"}} 308 | if err = binary.Read(r, binary.BigEndian, &bits); err != nil { 309 | return 310 | } 311 | {{range $off, $field := .Fields}} msg.{{$field | $.FieldName}} = (bits & (1 << {{$off}}) > 0) 312 | {{end}} 313 | {{end}} 314 | {{define "dec-octet"}} 315 | {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return } 316 | {{end}} 317 | {{end}} 318 | {{define "dec-shortshort"}} 319 | {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return } 320 | {{end}} 321 | {{end}} 322 | {{define "dec-short"}} 323 | {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return } 324 | {{end}} 325 | {{end}} 326 | {{define "dec-long"}} 327 | {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return } 328 | {{end}} 329 | {{end}} 330 | {{define "dec-longlong"}} 331 | {{range .Fields}} if err = binary.Read(r, binary.BigEndian, &msg.{{. | $.FieldName}}); err != nil { return } 332 | {{end}} 333 | {{end}} 334 | {{define "dec-timestamp"}} 335 | {{range .Fields}} if msg.{{. | $.FieldName}}, err = readTimestamp(r); err != nil { return } 336 | {{end}} 337 | {{end}} 338 | {{define "dec-shortstr"}} 339 | {{range .Fields}} if msg.{{. | $.FieldName}}, err = readShortstr(r); err != nil { return } 340 | {{end}} 341 | {{end}} 342 | {{define "dec-longstr"}} 343 | {{range .Fields}} if msg.{{. | $.FieldName}}, err = readLongstr(r); err != nil { return } 344 | {{end}} 345 | {{end}} 346 | {{define "dec-table"}} 347 | {{range .Fields}} if msg.{{. | $.FieldName}}, err = readTable(r); err != nil { return } 348 | {{end}} 349 | {{end}} 350 | 351 | `)) 352 | ) 353 | 354 | func (c *Constant) IsError() bool { 355 | return strings.Contains(c.Class, "error") 356 | } 357 | 358 | func (c *Constant) IsSoftError() bool { 359 | return c.Class == "soft-error" 360 | } 361 | 362 | func (renderer *renderer) Partial(prefix string, fields []fieldset) (s string, err error) { 363 | var buf bytes.Buffer 364 | for _, set := range fields { 365 | name := prefix + set.AmqpType 366 | t := packageTemplate.Lookup(name) 367 | if t == nil { 368 | return "", errors.New(fmt.Sprintf("Missing template: %s", name)) 369 | } 370 | if err = t.Execute(&buf, set); err != nil { 371 | return 372 | } 373 | } 374 | return string(buf.Bytes()), nil 375 | } 376 | 377 | // Groups the fields so that the right encoder/decoder can be called 378 | func (renderer *renderer) Fieldsets(fields []Field) (f []fieldset, err error) { 379 | if len(fields) > 0 { 380 | for _, field := range fields { 381 | cur := fieldset{} 382 | cur.AmqpType, err = renderer.FieldType(field) 383 | if err != nil { 384 | return 385 | } 386 | 387 | cur.NativeType, err = renderer.NativeType(cur.AmqpType) 388 | if err != nil { 389 | return 390 | } 391 | cur.Fields = append(cur.Fields, field) 392 | f = append(f, cur) 393 | } 394 | 395 | i, j := 0, 1 396 | for j < len(f) { 397 | if f[i].AmqpType == f[j].AmqpType { 398 | f[i].Fields = append(f[i].Fields, f[j].Fields...) 399 | } else { 400 | i++ 401 | f[i] = f[j] 402 | } 403 | j++ 404 | } 405 | return f[:i+1], nil 406 | } 407 | 408 | return 409 | } 410 | 411 | func (renderer *renderer) HasType(typ string, method Method) bool { 412 | for _, f := range method.Fields { 413 | name, _ := renderer.FieldType(f) 414 | if name == typ { 415 | return true 416 | } 417 | } 418 | return false 419 | } 420 | 421 | func (renderer *renderer) HasField(field string, method Method) bool { 422 | for _, f := range method.Fields { 423 | name := renderer.FieldName(f) 424 | if name == field { 425 | return true 426 | } 427 | } 428 | return false 429 | } 430 | 431 | func (renderer *renderer) Domain(field Field) (domain Domain, err error) { 432 | for _, domain = range renderer.Root.Domains { 433 | if field.Domain == domain.Name { 434 | return 435 | } 436 | } 437 | return domain, nil 438 | // return domain, ErrUnknownDomain 439 | } 440 | 441 | func (renderer *renderer) FieldName(field Field) (t string) { 442 | t = public(field.Name) 443 | 444 | if field.Reserved { 445 | t = strings.ToLower(t) 446 | } 447 | 448 | return 449 | } 450 | 451 | func (renderer *renderer) FieldType(field Field) (t string, err error) { 452 | t = field.Type 453 | 454 | if t == "" { 455 | var domain Domain 456 | domain, err = renderer.Domain(field) 457 | if err != nil { 458 | return "", err 459 | } 460 | t = domain.Type 461 | } 462 | 463 | return 464 | } 465 | 466 | func (renderer *renderer) NativeType(amqpType string) (t string, err error) { 467 | if t, ok := amqpTypeToNative[amqpType]; ok { 468 | return t, nil 469 | } 470 | return "", ErrUnknownType 471 | } 472 | 473 | func (renderer *renderer) Tag(d Domain) string { 474 | label := "`" 475 | 476 | label += `domain:"` + d.Name + `"` 477 | 478 | if len(d.Type) > 0 { 479 | label += `,type:"` + d.Type + `"` 480 | } 481 | 482 | label += "`" 483 | 484 | return label 485 | } 486 | 487 | func (renderer *renderer) StructName(parts ...string) string { 488 | return parts[0] + public(parts[1:]...) 489 | } 490 | 491 | func clean(body string) (res string) { 492 | return strings.Replace(body, "\r", "", -1) 493 | } 494 | 495 | func private(parts ...string) string { 496 | return export(regexp.MustCompile(`[-_]\w`), parts...) 497 | } 498 | 499 | func public(parts ...string) string { 500 | return export(regexp.MustCompile(`^\w|[-_]\w`), parts...) 501 | } 502 | 503 | func export(delim *regexp.Regexp, parts ...string) (res string) { 504 | for _, in := range parts { 505 | res += delim.ReplaceAllStringFunc(in, func(match string) string { 506 | switch len(match) { 507 | case 1: 508 | return strings.ToUpper(match) 509 | case 2: 510 | return strings.ToUpper(match[1:]) 511 | } 512 | panic("unreachable") 513 | }) 514 | } 515 | 516 | return 517 | } 518 | 519 | func main() { 520 | var r renderer 521 | 522 | spec, err := io.ReadAll(os.Stdin) 523 | if err != nil { 524 | log.Fatalln("Please pass spec on stdin", err) 525 | } 526 | 527 | err = xml.Unmarshal(spec, &r.Root) 528 | if err != nil { 529 | log.Fatalln("Could not parse XML:", err) 530 | } 531 | 532 | if err = packageTemplate.Execute(os.Stdout, &r); err != nil { 533 | log.Fatalln("Generate error: ", err) 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /tls_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "crypto/tls" 10 | "crypto/x509" 11 | "errors" 12 | "fmt" 13 | "net" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | func tlsServerConfig(t *testing.T) *tls.Config { 19 | t.Helper() 20 | 21 | cfg := new(tls.Config) 22 | 23 | cfg.ClientCAs = x509.NewCertPool() 24 | cfg.ClientCAs.AppendCertsFromPEM([]byte(caCert)) 25 | 26 | cert, err := tls.X509KeyPair([]byte(serverCert), []byte(serverKey)) 27 | if err != nil { 28 | t.Fatalf("TLS server config error: %+v", err) 29 | } 30 | 31 | cfg.Certificates = append(cfg.Certificates, cert) 32 | cfg.ClientAuth = tls.RequireAndVerifyClientCert 33 | 34 | return cfg 35 | } 36 | 37 | func tlsClientConfig(t *testing.T) *tls.Config { 38 | t.Helper() 39 | 40 | cfg := new(tls.Config) 41 | cfg.RootCAs = x509.NewCertPool() 42 | cfg.RootCAs.AppendCertsFromPEM([]byte(caCert)) 43 | 44 | cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey)) 45 | if err != nil { 46 | t.Fatalf("TLS client config error: %+v", err) 47 | } 48 | 49 | cfg.Certificates = append(cfg.Certificates, cert) 50 | 51 | return cfg 52 | } 53 | 54 | type tlsServer struct { 55 | net.Listener 56 | URL string 57 | Config *tls.Config 58 | Sessions chan *server 59 | } 60 | 61 | // Captures the header for each accepted connection 62 | func (s *tlsServer) Serve(t *testing.T) { 63 | t.Helper() 64 | 65 | for { 66 | c, err := s.Accept() 67 | if err != nil { 68 | return 69 | } 70 | s.Sessions <- newServer(t, c, c) 71 | } 72 | } 73 | 74 | func startTLSServer(t *testing.T, cfg *tls.Config) tlsServer { 75 | t.Helper() 76 | 77 | l, err := tls.Listen("tcp", "127.0.0.1:3456", cfg) 78 | if err != nil { 79 | t.Fatalf("TLS server Listen error: %+v", err) 80 | } 81 | 82 | s := tlsServer{ 83 | Listener: l, 84 | Config: cfg, 85 | URL: fmt.Sprintf("amqps://%s/", l.Addr().String()), 86 | Sessions: make(chan *server), 87 | } 88 | go s.Serve(t) 89 | 90 | return s 91 | } 92 | 93 | func TestTlsConfigFromUriPushdownServerNameIndication(t *testing.T) { 94 | uri := "amqps://user:pass@example.com:5671?server_name_indication=another-hostname.com" 95 | parsedUri, err := ParseURI(uri) 96 | if err != nil { 97 | t.Fatalf("expected to parse URI successfully, got error: %s", err) 98 | } 99 | 100 | tlsConf, err := tlsConfigFromURI(parsedUri) 101 | if err != nil { 102 | t.Fatalf("expected tlsConfigFromURI to succeed, got error: %s", err) 103 | } 104 | 105 | const expectedServerName = "another-hostname.com" 106 | if tlsConf.ServerName != expectedServerName { 107 | t.Fatalf("expected tlsConf server name to equal Uri servername: want %s, got %s", expectedServerName, tlsConf.ServerName) 108 | } 109 | } 110 | 111 | // Tests opening a connection of a TLS enabled socket server 112 | func TestTLSHandshake(t *testing.T) { 113 | srv := startTLSServer(t, tlsServerConfig(t)) 114 | defer srv.Close() 115 | 116 | success := make(chan bool) 117 | errs := make(chan error, 3) 118 | 119 | go func() { 120 | select { 121 | case <-time.After(10 * time.Millisecond): 122 | errs <- errors.New("server timeout waiting for TLS handshake from client") 123 | case session := <-srv.Sessions: 124 | session.connectionOpen() 125 | session.connectionClose() 126 | session.S.Close() 127 | } 128 | }() 129 | 130 | go func() { 131 | c, err := DialTLS(srv.URL, tlsClientConfig(t)) 132 | if err != nil { 133 | errs <- fmt.Errorf("expected to open a TLS connection, got err: %v", err) 134 | return 135 | } 136 | defer c.Close() 137 | 138 | if st := c.ConnectionState(); !st.HandshakeComplete { 139 | errs <- fmt.Errorf("expected to complete a TLS handshake, TLS connection state: %+v", st) 140 | } 141 | 142 | success <- true 143 | }() 144 | 145 | select { 146 | case err := <-errs: 147 | t.Fatalf("TLS server saw error: %+v", err) 148 | case <-success: 149 | return 150 | } 151 | } 152 | 153 | const caCert = ` 154 | -----BEGIN CERTIFICATE----- 155 | MIIC0TCCAbmgAwIBAgIUW418AvO6YD2WD5X/coo9geXvauEwDQYJKoZIhvcNAQEL 156 | BQAwEzERMA8GA1UEAwwITXlUZXN0Q0EwHhcNMjIwNDA0MDMxNjIxWhcNMzIwNDAx 157 | MDMxNjIxWjATMREwDwYDVQQDDAhNeVRlc3RDQTCCASIwDQYJKoZIhvcNAQEBBQAD 158 | ggEPADCCAQoCggEBAOU1K7mS1z7U9mdDaxpmaj/JwDjtIquqGv1IE4hi9RU/jkse 159 | 2GYUuOf5Viu7LMaKCS4I5qwyXT/yPUviw5b1bjEviNURCmQScvXKJg9/P31JlZPc 160 | RBV6Hnrku7nZeJcDfiAo8YDA3QPdr1uhTjjnIo4x2SYJfKDgBvsLfxXETUkFsECQ 161 | uuBLl3VL8jeU8y9wAg0y4gmcOZUyKPlcr9dsmDKftzkas+Zd4JR6e327U+VvVxv4 162 | hDMQGxx/yirKxMwH2+pwkYOWwOTb1FGi8iVjdAwgjlKsLqUR9eSN2AXW/v41E79h 163 | Sb9uEGeIfscGOrgVfVtVRZWNPDzM1/DpYSpsP2ECAwEAAaMdMBswDAYDVR0TBAUw 164 | AwEB/zALBgNVHQ8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAJWlbsQqPQPX/lOF 165 | CUnta6OHpt6OlPnN1lUhvbNQVpt0KdmuHQKwSyGiq6fVDt//2RcxPwCYO9AKUZZn 166 | /T48+4haGFEKLRVQrDX7/CQoGbHwWbbgcNpJiQ3XPhGvUrLy6VoahAYK81D23XT0 167 | b4TobCYY+8ny5qhyQgBZ7Jme2jDk0MXt8yjZFGcyA0fRy54ql1AxIXdx5/yvq2CI 168 | nLGEZQLsYhz/r+4gooIkLOIFD2JKoW9p56T52XiRxmz0tAs4WFW38Z6VrzB48Lpq 169 | dBH0sqzECTVQmX8er1taHa+Tg29tuXyqNkBn0tB9qq7G24SDaQFqpW7+J1kNnAY2 170 | KEcj4Ic= 171 | -----END CERTIFICATE----- 172 | ` 173 | 174 | const serverCert = ` 175 | -----BEGIN CERTIFICATE----- 176 | MIIC8zCCAdugAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhNeVRl 177 | c3RDQTAeFw0yMjA0MDQwMzE2MjFaFw0zMjA0MDEwMzE2MjFaMCUxEjAQBgNVBAMM 178 | CTEyNy4wLjAuMTEPMA0GA1UECgwGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOC 179 | AQ8AMIIBCgKCAQEArH6lT2bGHiGLaWh+eXLXeAcbXq2bGDyeS1zsRGT/+D6YBzFo 180 | 7FAUMbN8iJtjWNBlRRdVyiUJn5TUYHrXdCeoAs5JrdzNefax+3toDu6I+nJG4dO3 181 | nbZ9zo8q7uBZtCi/Oph28CtwQxMjNuf3pHlSSKZG5/br4w6HhgSnuLBxyxpr3CrU 182 | 2kdHZGIGjX4sEGJ/aDCKIUem/jEXp5i1n13+p5QmPfZc6KX2+sUmN6todqlZpsEV 183 | WZAYLXshHJvViLMTrCJQ3KfqoF7o4PrwQJoQ3jlNct2ZF68H0Iz/bT/VQfPS+4CL 184 | t71xHhU/mYn+kIt1w0APhCMMkYPOhijmGlDAbwIDAQABo0AwPjAJBgNVHRMEAjAA 185 | MAsGA1UdDwQEAwIFIDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/ 186 | AAABMA0GCSqGSIb3DQEBCwUAA4IBAQA8dM07fsOfxU7oVWQN3Aey0g8/wc2eL0ZY 187 | oxxvKJqZjOSamNEiwb+lTTmUGg78wiIpFK4ChpPbMpURcRVrxTJuWRg4qagFWn18 188 | RGNqNVnucFSOPe3e+zZqnjWc22PDbBt4B3agdpJsHq4hpdOdJreAh/7MMrWyFMZI 189 | PZ5WFjThFpeQw+vArYUq6BMD3H86G++gfWlSv0yPxWG+Q44+tfZSyiiM6k0+p3nw 190 | 1k0eBIA+BibcZ+dQu0m2Vq7XKxfOeN67LY8aElGNSRmH/YAyo5mSrrvzbbZKpr4/ 191 | cn4f79jDcBEtbMeCav0eHXzSwWWpteZvdLOm6rxnHd3jngRPqRj+ 192 | -----END CERTIFICATE----- 193 | ` 194 | 195 | const serverKey = ` 196 | -----BEGIN RSA PRIVATE KEY----- 197 | MIIEowIBAAKCAQEArH6lT2bGHiGLaWh+eXLXeAcbXq2bGDyeS1zsRGT/+D6YBzFo 198 | 7FAUMbN8iJtjWNBlRRdVyiUJn5TUYHrXdCeoAs5JrdzNefax+3toDu6I+nJG4dO3 199 | nbZ9zo8q7uBZtCi/Oph28CtwQxMjNuf3pHlSSKZG5/br4w6HhgSnuLBxyxpr3CrU 200 | 2kdHZGIGjX4sEGJ/aDCKIUem/jEXp5i1n13+p5QmPfZc6KX2+sUmN6todqlZpsEV 201 | WZAYLXshHJvViLMTrCJQ3KfqoF7o4PrwQJoQ3jlNct2ZF68H0Iz/bT/VQfPS+4CL 202 | t71xHhU/mYn+kIt1w0APhCMMkYPOhijmGlDAbwIDAQABAoIBAQCFfEw5MfNHBfZ4 203 | z+Bv46tSu00262oGS4LEF1jPZMmhNe84QchMd3vpKljI7lbnN/3mhbRiBl94Gxhu 204 | wSFSRg4CfdkOrrxkEcCSOGHCjF18UksAH3MMnVimLKywxvUkMhQqKCqCmVr6zSiH 205 | KOO/aBOBHQvqHm9U+r1tvNR+XCzzWmz0OavKquXhdTbLyHWeR7rS4mn53Up0hQIe 206 | la8PzWpw+mcNz00UbeeK84tUro4+wGV6aFHh23F+RqpXqW8VNnBS0zVtqYc7x7a8 207 | WGcxlLt1hWeiFgEJUuui2i1YxyHDb3IyhGckVG+lSzTpgjl9Pisl7jjlVBRAkiWw 208 | fRJfWuyBAoGBAOERpoRSpXCBum3T7ZwcD3pWHgNY1za9zqsgZayumlhqvxmia77o 209 | HvXyroj+8DTUZ+NiKn0HnF8Hni/2fhAWs9XPlELM9BENQP5kcJzcBSalUOa23C6V 210 | Iba30BB8I1v8AhYLdCFdpio+aQnd1S4VH4sTRoQjsspOSesxDWa1snJ/AoGBAMQz 211 | VadEoBR8ZHsZjks7JaU2G1h2tdU3hfynD8DPq0SFnbS3NhVphmYAdfbOrea9ilmQ 212 | AyO/TJD6xmK7IELWYNtS4DCqAaQKxfOsS8wF97lJ1/XKDNQND8PfbVwzLF8NcbMT 213 | G15Vq9dkJpiGBe0YYxdjtPtwM+FSriK37YIyidoRAoGAQqGBFKeLBvXBBYa6T38X 214 | LfaUyBTjEfe7WXor36WJWCeyD5rAHzKFB/ciqLgg0OMZJn4HaiB4sMGGmVh2FblC 215 | 4Eel8ujOUMYFucpudGHGvJwwiT0VjkzkQD3GwTqfFTpUO8aESOR6rwLvAdbEp/Hk 216 | 9r1sIO6Ynb/zrkdFWmTsQW0CgYBvXrhjH2hC2K1s1v/XonZnBoSVPaVPp5nN5cLi 217 | br9IQRRZLZpsox7gLajIdV9vV+39kurFUuSSc1dDWfchGXGXbb7GwOn3hQoCnK3V 218 | 3RlWOx10bsHDaLqnM99u87lfJ1GAFft2G+lUdYwXDhS1Fh/Beh6Uj4dTgsxH9uHC 219 | AxAPEQKBgCkOBQBfPg4mZdYKAaNoRvNhJr1XdruQnFsBCdvCm2x8DAkNGme7Ax8H 220 | QYB+jH1eC/uInPUnW3kh7KPqgdGVNbYeZGT1FwuwoJEubR6+rBiRm3HsjBJlR+Tv 221 | RK8SwjKS0ZkCb8eujkyBakXdF2tAY9Du8HODID+CuuxhGD1YsF2q 222 | -----END RSA PRIVATE KEY----- 223 | ` 224 | 225 | const clientCert = ` 226 | -----BEGIN CERTIFICATE----- 227 | MIIC4jCCAcqgAwIBAgIBAjANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhNeVRl 228 | c3RDQTAeFw0yMjA0MDQwMzE2MjFaFw0zMjA0MDEwMzE2MjFaMCUxEjAQBgNVBAMM 229 | CTEyNy4wLjAuMTEPMA0GA1UECgwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOC 230 | AQ8AMIIBCgKCAQEAtL3BQX/ubTjO+m4WMDe7OiPquHkyE/G8zHNoFXH/pjcf0iYq 231 | lBWD8+bmoPxzOy2RtRawZIR5ZtHkPH8YZues3nwnnUPYQfUGlqlSc4TOyO7vn0Eq 232 | ufJZCVF3+DWTHaV41K20/cpLuCs308KKVt7XosDmj4iwc1t7NcjS8TFI4/SpBIYg 233 | TzII+RzFXhT3FFXuo5ZkOoti0IyUXxALX/Ba+ubR9vrJC4BS7VtoSQQ/uRr5x9MB 234 | /yZWUDTcOOJNjh1mKe+9vEFAjCgRFOwaZzph38Av/L2e4uPTsjTX6MsMQZK1nl5h 235 | 03HoQZ3FRJLVKTHVszihIODgNQ5ReM3YBxrqiwIDAQABoy8wLTAJBgNVHRMEAjAA 236 | MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG9w0BAQsF 237 | AAOCAQEAxMJjHzZ6BiSwzieH+Ujz70fU+qa4xdoY8tt32gnBJt9d+dsgjaAIeGZR 238 | veAA3Ak0aM+1zoCGc2792oEv7gOz946brbkJLNC5e43ZlDFHYbj1OYcMtxL4CCyW 239 | 2QImY/CgoXfVkth1SSRl+wGEwMlS8XuriczwU4Sl/faJHE+mwwJNlNBNKjv1GoUb 240 | 0T3lnfuUNoXzH1SstR/6+/eD0CNvTsTvkC9eXJULt2V93jTFNGlM6KcVvF5E7BDm 241 | iSYtaQ8vtPF64LKM+dF3ymzJDoT6EyYU6t1X5RPQ6fQTRbCvZ74NsWoqWzA4VB20 242 | rgNKRkWBHGxKqNMrF1YQvm2hWxw6pA== 243 | -----END CERTIFICATE----- 244 | ` 245 | 246 | const clientKey = ` 247 | -----BEGIN RSA PRIVATE KEY----- 248 | MIIEpAIBAAKCAQEAtL3BQX/ubTjO+m4WMDe7OiPquHkyE/G8zHNoFXH/pjcf0iYq 249 | lBWD8+bmoPxzOy2RtRawZIR5ZtHkPH8YZues3nwnnUPYQfUGlqlSc4TOyO7vn0Eq 250 | ufJZCVF3+DWTHaV41K20/cpLuCs308KKVt7XosDmj4iwc1t7NcjS8TFI4/SpBIYg 251 | TzII+RzFXhT3FFXuo5ZkOoti0IyUXxALX/Ba+ubR9vrJC4BS7VtoSQQ/uRr5x9MB 252 | /yZWUDTcOOJNjh1mKe+9vEFAjCgRFOwaZzph38Av/L2e4uPTsjTX6MsMQZK1nl5h 253 | 03HoQZ3FRJLVKTHVszihIODgNQ5ReM3YBxrqiwIDAQABAoIBAQCmfc2R2pj1P8lZ 254 | 4yLJU+1CB2fmeq3otVvnMcAFUTfgExNa8BF0y8T7Xg3A6gvzzWxVVgsy7N0wG9SU 255 | 7ba6xFr3r4KGWcLSLzXcfykWhJY/fep51vvWwinGbaeHm0JjotQFheYdisXpZtZM 256 | WP46O5iDshIw0gdInFKJHu9BgtbUNDRAAI9xsmDcITSXf7ZblH5CCLJ6QLFn1Olg 257 | O11k6ABYmIxKSXfLylFDzUxfnGhXkrv8/sQwj3B7XxBYVoAF24uw2ufZ+qIlcxUy 258 | m/T/IU1lOLNmfWwDk2+t7zslOpXAEZtrni7u2GF54wIbMr/Z0unGC1B0AUXDwp8y 259 | MNODQdcBAoGBAOe+p5neqgoUL8kBlRvp/dp5kIsVPNBN7+vbVYVzeIEBNixfYe9S 260 | 0dixQHiJr29sWn/4yC2KkPQewyDVBf7iNXlcXfkjbEjU0cZaMJao4sKki/N/GF64 261 | IpC8HRM1BSvaSnrJZtZ6aWljpvWRa8bGRdQWb397ufChNpstYEALMMZLAoGBAMeo 262 | hGZJcWIEtl1oJIHsKFKf7jwftFidzyy2ShVLX52n3HkbCYMdwg5DIjQ6Vj9GMy8E 263 | AvrcQduhLw4otdPL+X270xX8WKbxRAvlDnGfmOX1+z/lJy2OLi/HpZeUyM6Oe/Uy 264 | ijaGTKOqRSi9xpJOdDR17e/fcYKzvPejNxZcDMTBAoGAeH/VLBfweI8oja8J9lrE 265 | CX7eXsNrPLDZyNziaiKxjPqxTX9HMCbzQGZiLIsDMr+3iwU0KSH830LDmWXK2U6M 266 | GY+iuXHm0zP949Jvo1crmaPvtWvnoxDBwFpgD+Woy7WUtqXUmD9MYmVToiq8TL45 267 | /t6vmS0fcPSSrTt56bMn6GMCgYEAvRDLL8FkaRllR9aSm6VyGavxAWZUdYYa5ZBJ 268 | XxjdFoIauWPtAghv9umDvklv2sMzPNZjrAJfKwfbc2EBrep9+56dKTipCo11jn39 269 | y4MCWuEwZzUsgGsfOYepO31dGpy6rVqKn09Vy7Y1f3sWSv2X9QWnp3rEFqz1yNr6 270 | E2ZfgQECgYA943tAbNFmAZW3WdS82cpM+HXOkKuYkPfFsBq/eoKoi1p0WLtueQZ3 271 | cC39vheaWdRJPI/dKjhgifY6pBcVLzGIf9gD6VKVbIrcx6U3uMULEQb3GqvdIOtU 272 | d5WpU0bwFMa+vYfrlAjngXlDW/tGqK8ietb6n+xW15M1w4mrEFcAng== 273 | -----END RSA PRIVATE KEY----- 274 | ` 275 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package amqp091 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestNewError(t *testing.T) { 10 | testCases := []struct { 11 | code uint16 12 | text string 13 | expectedServer bool 14 | recoverable bool 15 | temporary bool 16 | }{ 17 | // Just three basics samples 18 | {404, "Not Found", true, true, false}, 19 | {500, "Internal Server Error", true, false, false}, 20 | {403, "Forbidden", true, true, false}, 21 | {311, "Content Too Large", true, true, true}, 22 | } 23 | 24 | for _, tc := range testCases { 25 | aerr := newError(tc.code, tc.text) 26 | if aerr.Code != int(tc.code) { 27 | t.Errorf("expected Code %d, got %d", tc.code, aerr.Code) 28 | } 29 | if aerr.Reason != tc.text { 30 | t.Errorf("expected Reason %s, got %s", tc.text, aerr.Reason) 31 | } 32 | if aerr.Server != tc.expectedServer { 33 | t.Errorf("expected Server to be %v", tc.expectedServer) 34 | } 35 | if aerr.Recover != tc.recoverable { 36 | t.Errorf("expected Recover to be %v", tc.recoverable) 37 | } 38 | 39 | if ok := aerr.Recoverable(); ok != tc.recoverable { 40 | t.Errorf("expected err to be temporary %v", tc.recoverable) 41 | } 42 | 43 | if ok := aerr.Temporary(); ok != tc.temporary { 44 | t.Errorf("expected err to be retriable %v", tc.recoverable) 45 | } 46 | } 47 | } 48 | 49 | func TestErrorMessage(t *testing.T) { 50 | var err error = newError(404, "Not Found") 51 | 52 | expected := `Exception (404) Reason: "Not Found"` 53 | 54 | if got := err.Error(); expected != got { 55 | t.Errorf("expected Error %q, got %q", expected, got) 56 | } 57 | 58 | expected = `Exception=404, Reason="Not Found", Recover=true, Server=true` 59 | 60 | if got := fmt.Sprintf("%#v", err); expected != got { 61 | t.Errorf("expected go string %q, got %q", expected, got) 62 | } 63 | } 64 | 65 | func TestValidateField(t *testing.T) { 66 | // Test case for simple types 67 | simpleTypes := []interface{}{ 68 | nil, true, byte(1), int8(1), 10, int16(10), int32(10), int64(10), 69 | float32(1.0), float64(1.0), "string", []byte("byte slice"), 70 | Decimal{Scale: 2, Value: 12345}, 71 | time.Now(), 72 | } 73 | for _, v := range simpleTypes { 74 | if err := validateField(v); err != nil { 75 | t.Errorf("validateField failed for simple type %T: %s", v, err) 76 | } 77 | } 78 | 79 | // Test case for []interface{} 80 | sliceTypes := []interface{}{ 81 | "string", 10, float64(1.0), Decimal{Scale: 2, Value: 12345}, 82 | } 83 | if err := validateField(sliceTypes); err != nil { 84 | t.Errorf("validateField failed for []interface{}: %s", err) 85 | } 86 | 87 | // Test case for Table 88 | tableType := Table{ 89 | "key1": "value1", 90 | "key2": 10, 91 | "key3": []interface{}{"nested string", 20}, 92 | } 93 | if err := validateField(tableType); err != nil { 94 | t.Errorf("validateField failed for Table: %s", err) 95 | } 96 | 97 | // Test case for unsupported type 98 | unsupportedType := struct{}{} 99 | if err := validateField(unsupportedType); err == nil { 100 | t.Error("validateField should fail for unsupported type but it didn't") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /uri.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "net" 12 | "net/url" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | var ( 18 | errURIScheme = errors.New("AMQP scheme must be either 'amqp://' or 'amqps://'") 19 | errURIWhitespace = errors.New("URI must not contain whitespace") 20 | ) 21 | 22 | var schemePorts = map[string]int{ 23 | "amqp": 5672, 24 | "amqps": 5671, 25 | } 26 | 27 | var defaultURI = URI{ 28 | Scheme: "amqp", 29 | Host: "localhost", 30 | Port: 5672, 31 | Username: "guest", 32 | Password: "guest", 33 | Vhost: "/", 34 | } 35 | 36 | // URI represents a parsed AMQP URI string. 37 | type URI struct { 38 | Scheme string 39 | Host string 40 | Port int 41 | Username string 42 | Password string 43 | Vhost string 44 | CertFile string // client TLS auth - path to certificate (PEM) 45 | CACertFile string // client TLS auth - path to CA certificate (PEM) 46 | KeyFile string // client TLS auth - path to private key (PEM) 47 | ServerName string // client TLS auth - server name 48 | AuthMechanism []string 49 | Heartbeat heartbeatDuration 50 | ConnectionTimeout int 51 | ChannelMax uint16 52 | } 53 | 54 | // ParseURI attempts to parse the given AMQP URI according to the spec. 55 | // See http://www.rabbitmq.com/uri-spec.html. 56 | // 57 | // Default values for the fields are: 58 | // 59 | // Scheme: amqp 60 | // Host: localhost 61 | // Port: 5672 62 | // Username: guest 63 | // Password: guest 64 | // Vhost: / 65 | // 66 | // Supports TLS query parameters. See https://www.rabbitmq.com/uri-query-parameters.html 67 | // 68 | // certfile: 69 | // keyfile: 70 | // cacertfile: 71 | // server_name_indication: 72 | // auth_mechanism: 73 | // heartbeat: 74 | // connection_timeout: 75 | // channel_max: 76 | // 77 | // If cacertfile is not provided, system CA certificates will be used. 78 | // Mutual TLS (client auth) will be enabled only in case keyfile AND certfile provided. 79 | // 80 | // If Config.TLSClientConfig is set, TLS parameters from URI will be ignored. 81 | func ParseURI(uri string) (URI, error) { 82 | builder := defaultURI 83 | 84 | if strings.Contains(uri, " ") { 85 | return builder, errURIWhitespace 86 | } 87 | 88 | u, err := url.Parse(uri) 89 | if err != nil { 90 | return builder, err 91 | } 92 | 93 | defaultPort, okScheme := schemePorts[u.Scheme] 94 | 95 | if okScheme { 96 | builder.Scheme = u.Scheme 97 | } else { 98 | return builder, errURIScheme 99 | } 100 | 101 | host := u.Hostname() 102 | port := u.Port() 103 | 104 | if host != "" { 105 | builder.Host = host 106 | } 107 | 108 | if port != "" { 109 | port32, err := strconv.ParseInt(port, 10, 32) 110 | if err != nil { 111 | return builder, err 112 | } 113 | builder.Port = int(port32) 114 | } else { 115 | builder.Port = defaultPort 116 | } 117 | 118 | if u.User != nil { 119 | builder.Username = u.User.Username() 120 | if password, ok := u.User.Password(); ok { 121 | builder.Password = password 122 | } 123 | } 124 | 125 | if u.Path != "" { 126 | if strings.HasPrefix(u.Path, "/") { 127 | if u.Host == "" && strings.HasPrefix(u.Path, "///") { 128 | // net/url doesn't handle local context authorities and leaves that up 129 | // to the scheme handler. In our case, we translate amqp:/// into the 130 | // default host and whatever the vhost should be 131 | if len(u.Path) > 3 { 132 | builder.Vhost = u.Path[3:] 133 | } 134 | } else if len(u.Path) > 1 { 135 | builder.Vhost = u.Path[1:] 136 | } 137 | } else { 138 | builder.Vhost = u.Path 139 | } 140 | } 141 | 142 | // see https://www.rabbitmq.com/uri-query-parameters.html 143 | params := u.Query() 144 | builder.CertFile = params.Get("certfile") 145 | builder.KeyFile = params.Get("keyfile") 146 | builder.CACertFile = params.Get("cacertfile") 147 | builder.ServerName = params.Get("server_name_indication") 148 | builder.AuthMechanism = params["auth_mechanism"] 149 | 150 | if params.Has("heartbeat") { 151 | value, err := strconv.Atoi(params.Get("heartbeat")) 152 | if err != nil { 153 | return builder, fmt.Errorf("heartbeat is not an integer: %v", err) 154 | } 155 | builder.Heartbeat = newHeartbeatDurationFromSeconds(value) 156 | } 157 | 158 | if params.Has("connection_timeout") { 159 | value, err := strconv.Atoi(params.Get("connection_timeout")) 160 | if err != nil { 161 | return builder, fmt.Errorf("connection_timeout is not an integer: %v", err) 162 | } 163 | builder.ConnectionTimeout = value 164 | } 165 | 166 | if params.Has("channel_max") { 167 | value, err := strconv.ParseUint(params.Get("channel_max"), 10, 16) 168 | if err != nil { 169 | return builder, fmt.Errorf("channel_max is not an uint16: %v", err) 170 | } 171 | builder.ChannelMax = uint16(value) 172 | } 173 | 174 | return builder, nil 175 | } 176 | 177 | // PlainAuth returns a PlainAuth structure based on the parsed URI's 178 | // Username and Password fields. 179 | func (uri URI) PlainAuth() *PlainAuth { 180 | return &PlainAuth{ 181 | Username: uri.Username, 182 | Password: uri.Password, 183 | } 184 | } 185 | 186 | // AMQPlainAuth returns a PlainAuth structure based on the parsed URI's 187 | // Username and Password fields. 188 | func (uri URI) AMQPlainAuth() *AMQPlainAuth { 189 | return &AMQPlainAuth{ 190 | Username: uri.Username, 191 | Password: uri.Password, 192 | } 193 | } 194 | 195 | func (uri URI) String() string { 196 | authority, err := url.Parse("") 197 | if err != nil { 198 | return err.Error() 199 | } 200 | 201 | authority.Scheme = uri.Scheme 202 | 203 | if uri.Username != defaultURI.Username || uri.Password != defaultURI.Password { 204 | authority.User = url.User(uri.Username) 205 | 206 | if uri.Password != defaultURI.Password { 207 | authority.User = url.UserPassword(uri.Username, uri.Password) 208 | } 209 | } 210 | 211 | if defaultPort, found := schemePorts[uri.Scheme]; !found || defaultPort != uri.Port { 212 | authority.Host = net.JoinHostPort(uri.Host, strconv.Itoa(uri.Port)) 213 | } else { 214 | // JoinHostPort() automatically add brackets to the host if it's 215 | // an IPv6 address. 216 | // 217 | // If not port is specified, JoinHostPort() return an IP address in the 218 | // form of "[::1]:", so we use TrimSuffix() to remove the extra ":". 219 | authority.Host = strings.TrimSuffix(net.JoinHostPort(uri.Host, ""), ":") 220 | } 221 | 222 | if uri.Vhost != defaultURI.Vhost { 223 | // Make sure net/url does not double escape, e.g. 224 | // "%2F" does not become "%252F". 225 | authority.Path = uri.Vhost 226 | authority.RawPath = url.QueryEscape(uri.Vhost) 227 | } else { 228 | authority.Path = "/" 229 | } 230 | 231 | if uri.CertFile != "" || uri.KeyFile != "" || uri.CACertFile != "" || uri.ServerName != "" { 232 | rawQuery := strings.Builder{} 233 | if uri.CertFile != "" { 234 | rawQuery.WriteString("certfile=") 235 | rawQuery.WriteString(uri.CertFile) 236 | rawQuery.WriteRune('&') 237 | } 238 | if uri.KeyFile != "" { 239 | rawQuery.WriteString("keyfile=") 240 | rawQuery.WriteString(uri.KeyFile) 241 | rawQuery.WriteRune('&') 242 | } 243 | if uri.CACertFile != "" { 244 | rawQuery.WriteString("cacertfile=") 245 | rawQuery.WriteString(uri.CACertFile) 246 | rawQuery.WriteRune('&') 247 | } 248 | if uri.ServerName != "" { 249 | rawQuery.WriteString("server_name_indication=") 250 | rawQuery.WriteString(uri.ServerName) 251 | } 252 | authority.RawQuery = rawQuery.String() 253 | } 254 | 255 | return authority.String() 256 | } 257 | -------------------------------------------------------------------------------- /uri_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "reflect" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // Test matrix defined on http://www.rabbitmq.com/uri-spec.html 15 | type testURI struct { 16 | url string 17 | username string 18 | password string 19 | host string 20 | port int 21 | vhost string 22 | canon string 23 | } 24 | 25 | var uriTests = []testURI{ 26 | { 27 | url: "amqp://user:pass@host:10000/vhost", 28 | username: "user", 29 | password: "pass", 30 | host: "host", 31 | port: 10000, 32 | vhost: "vhost", 33 | canon: "amqp://user:pass@host:10000/vhost", 34 | }, 35 | 36 | { 37 | url: "amqp://", 38 | username: defaultURI.Username, 39 | password: defaultURI.Password, 40 | host: defaultURI.Host, 41 | port: defaultURI.Port, 42 | vhost: defaultURI.Vhost, 43 | canon: "amqp://localhost/", 44 | }, 45 | 46 | { 47 | url: "amqp://:@/", 48 | username: "", 49 | password: "", 50 | host: defaultURI.Host, 51 | port: defaultURI.Port, 52 | vhost: defaultURI.Vhost, 53 | canon: "amqp://:@localhost/", 54 | }, 55 | 56 | { 57 | url: "amqp://user@", 58 | username: "user", 59 | password: defaultURI.Password, 60 | host: defaultURI.Host, 61 | port: defaultURI.Port, 62 | vhost: defaultURI.Vhost, 63 | canon: "amqp://user@localhost/", 64 | }, 65 | 66 | { 67 | url: "amqp://user:pass@", 68 | username: "user", 69 | password: "pass", 70 | host: defaultURI.Host, 71 | port: defaultURI.Port, 72 | vhost: defaultURI.Vhost, 73 | canon: "amqp://user:pass@localhost/", 74 | }, 75 | 76 | { 77 | url: "amqp://guest:pass@", 78 | username: "guest", 79 | password: "pass", 80 | host: defaultURI.Host, 81 | port: defaultURI.Port, 82 | vhost: defaultURI.Vhost, 83 | canon: "amqp://guest:pass@localhost/", 84 | }, 85 | 86 | { 87 | url: "amqp://host", 88 | username: defaultURI.Username, 89 | password: defaultURI.Password, 90 | host: "host", 91 | port: defaultURI.Port, 92 | vhost: defaultURI.Vhost, 93 | canon: "amqp://host/", 94 | }, 95 | 96 | { 97 | url: "amqp://:10000", 98 | username: defaultURI.Username, 99 | password: defaultURI.Password, 100 | host: defaultURI.Host, 101 | port: 10000, 102 | vhost: defaultURI.Vhost, 103 | canon: "amqp://localhost:10000/", 104 | }, 105 | 106 | { 107 | url: "amqp:///vhost", 108 | username: defaultURI.Username, 109 | password: defaultURI.Password, 110 | host: defaultURI.Host, 111 | port: defaultURI.Port, 112 | vhost: "vhost", 113 | canon: "amqp://localhost/vhost", 114 | }, 115 | 116 | { 117 | url: "amqp://host/", 118 | username: defaultURI.Username, 119 | password: defaultURI.Password, 120 | host: "host", 121 | port: defaultURI.Port, 122 | vhost: defaultURI.Vhost, 123 | canon: "amqp://host/", 124 | }, 125 | 126 | { 127 | url: "amqp://host/%2F", 128 | username: defaultURI.Username, 129 | password: defaultURI.Password, 130 | host: "host", 131 | port: defaultURI.Port, 132 | vhost: "/", 133 | canon: "amqp://host/", 134 | }, 135 | 136 | { 137 | url: "amqp://host/%2F%2F", 138 | username: defaultURI.Username, 139 | password: defaultURI.Password, 140 | host: "host", 141 | port: defaultURI.Port, 142 | vhost: "//", 143 | canon: "amqp://host/%2F%2F", 144 | }, 145 | 146 | { 147 | url: "amqp://host/%2Fslash%2F", 148 | username: defaultURI.Username, 149 | password: defaultURI.Password, 150 | host: "host", 151 | port: defaultURI.Port, 152 | vhost: "/slash/", 153 | canon: "amqp://host/%2Fslash%2F", 154 | }, 155 | 156 | { 157 | url: "amqp://192.168.1.1:1000/", 158 | username: defaultURI.Username, 159 | password: defaultURI.Password, 160 | host: "192.168.1.1", 161 | port: 1000, 162 | vhost: defaultURI.Vhost, 163 | canon: "amqp://192.168.1.1:1000/", 164 | }, 165 | 166 | { 167 | url: "amqp://[::1]", 168 | username: defaultURI.Username, 169 | password: defaultURI.Password, 170 | host: "::1", 171 | port: defaultURI.Port, 172 | vhost: defaultURI.Vhost, 173 | canon: "amqp://[::1]/", 174 | }, 175 | 176 | { 177 | url: "amqp://[::1]:1000", 178 | username: defaultURI.Username, 179 | password: defaultURI.Password, 180 | host: "::1", 181 | port: 1000, 182 | vhost: defaultURI.Vhost, 183 | canon: "amqp://[::1]:1000/", 184 | }, 185 | 186 | { 187 | url: "amqp://[fe80::1]", 188 | username: defaultURI.Username, 189 | password: defaultURI.Password, 190 | host: "fe80::1", 191 | port: defaultURI.Port, 192 | vhost: defaultURI.Vhost, 193 | canon: "amqp://[fe80::1]/", 194 | }, 195 | 196 | { 197 | url: "amqp://[fe80::1]", 198 | username: defaultURI.Username, 199 | password: defaultURI.Password, 200 | host: "fe80::1", 201 | port: defaultURI.Port, 202 | vhost: defaultURI.Vhost, 203 | canon: "amqp://[fe80::1]/", 204 | }, 205 | 206 | { 207 | url: "amqp://[fe80::1%25en0]", 208 | username: defaultURI.Username, 209 | password: defaultURI.Password, 210 | host: "fe80::1%en0", 211 | port: defaultURI.Port, 212 | vhost: defaultURI.Vhost, 213 | canon: "amqp://[fe80::1%25en0]/", 214 | }, 215 | 216 | { 217 | url: "amqp://[fe80::1]:5671", 218 | username: defaultURI.Username, 219 | password: defaultURI.Password, 220 | host: "fe80::1", 221 | port: 5671, 222 | vhost: defaultURI.Vhost, 223 | canon: "amqp://[fe80::1]:5671/", 224 | }, 225 | 226 | { 227 | url: "amqps:///", 228 | username: defaultURI.Username, 229 | password: defaultURI.Password, 230 | host: defaultURI.Host, 231 | port: schemePorts["amqps"], 232 | vhost: defaultURI.Vhost, 233 | canon: "amqps://localhost/", 234 | }, 235 | 236 | { 237 | url: "amqps://host:1000/", 238 | username: defaultURI.Username, 239 | password: defaultURI.Password, 240 | host: "host", 241 | port: 1000, 242 | vhost: defaultURI.Vhost, 243 | canon: "amqps://host:1000/", 244 | }, 245 | } 246 | 247 | func TestURISpec(t *testing.T) { 248 | for _, test := range uriTests { 249 | u, err := ParseURI(test.url) 250 | if err != nil { 251 | t.Fatal("Could not parse spec URI: ", test.url, " err: ", err) 252 | } 253 | 254 | if test.username != u.Username { 255 | t.Error("For: ", test.url, " usernames do not match. want: ", test.username, " got: ", u.Username) 256 | } 257 | 258 | if test.password != u.Password { 259 | t.Error("For: ", test.url, " passwords do not match. want: ", test.password, " got: ", u.Password) 260 | } 261 | 262 | if test.host != u.Host { 263 | t.Error("For: ", test.url, " hosts do not match. want: ", test.host, " got: ", u.Host) 264 | } 265 | 266 | if test.port != u.Port { 267 | t.Error("For: ", test.url, " ports do not match. want: ", test.port, " got: ", u.Port) 268 | } 269 | 270 | if test.vhost != u.Vhost { 271 | t.Error("For: ", test.url, " vhosts do not match. want: ", test.vhost, " got: ", u.Vhost) 272 | } 273 | 274 | if test.canon != u.String() { 275 | t.Error("For: ", test.url, " canonical string does not match. want: ", test.canon, " got: ", u.String()) 276 | } 277 | } 278 | } 279 | 280 | func TestURIUnknownScheme(t *testing.T) { 281 | if _, err := ParseURI("http://example.com/"); err == nil { 282 | t.Fatal("Expected error when parsing non-amqp scheme") 283 | } 284 | } 285 | 286 | func TestURIScheme(t *testing.T) { 287 | if _, err := ParseURI("amqp://example.com/"); err != nil { 288 | t.Fatalf("Expected to parse amqp scheme, got %v", err) 289 | } 290 | 291 | if _, err := ParseURI("amqps://example.com/"); err != nil { 292 | t.Fatalf("Expected to parse amqps scheme, got %v", err) 293 | } 294 | } 295 | 296 | func TestURIWhitespace(t *testing.T) { 297 | if _, err := ParseURI("amqp://admin:PASSWORD@rabbitmq-service/ -http_port=8080"); err == nil { 298 | t.Fatal("Expected to fail if URI contains whitespace") 299 | } 300 | } 301 | 302 | func TestURIDefaults(t *testing.T) { 303 | url := "amqp://" 304 | uri, err := ParseURI(url) 305 | if err != nil { 306 | t.Fatal("Could not parse") 307 | } 308 | 309 | if uri.String() != "amqp://localhost/" { 310 | t.Fatal("Defaults not encoded properly got:", uri.String()) 311 | } 312 | } 313 | 314 | func TestURIComplete(t *testing.T) { 315 | url := "amqp://bob:dobbs@foo.bar:5678/private" 316 | uri, err := ParseURI(url) 317 | if err != nil { 318 | t.Fatal("Could not parse") 319 | } 320 | 321 | if uri.String() != url { 322 | t.Fatal("Defaults not encoded properly want:", url, " got:", uri.String()) 323 | } 324 | } 325 | 326 | func TestURIDefaultPortAmqpNotIncluded(t *testing.T) { 327 | url := "amqp://foo.bar:5672/" 328 | uri, err := ParseURI(url) 329 | if err != nil { 330 | t.Fatal("Could not parse") 331 | } 332 | 333 | if uri.String() != "amqp://foo.bar/" { 334 | t.Fatal("Defaults not encoded properly got:", uri.String()) 335 | } 336 | } 337 | 338 | func TestURIDefaultPortAmqp(t *testing.T) { 339 | url := "amqp://foo.bar/" 340 | uri, err := ParseURI(url) 341 | if err != nil { 342 | t.Fatal("Could not parse") 343 | } 344 | 345 | if uri.Port != 5672 { 346 | t.Fatal("Default port not correct for amqp, got:", uri.Port) 347 | } 348 | } 349 | 350 | func TestURIDefaultPortAmqpsNotIncludedInString(t *testing.T) { 351 | url := "amqps://foo.bar:5671/" 352 | uri, err := ParseURI(url) 353 | if err != nil { 354 | t.Fatal("Could not parse") 355 | } 356 | 357 | if uri.String() != "amqps://foo.bar/" { 358 | t.Fatal("Defaults not encoded properly got:", uri.String()) 359 | } 360 | } 361 | 362 | func TestURIDefaultPortAmqps(t *testing.T) { 363 | url := "amqps://foo.bar/" 364 | uri, err := ParseURI(url) 365 | if err != nil { 366 | t.Fatal("Could not parse") 367 | } 368 | 369 | if uri.Port != 5671 { 370 | t.Fatal("Default port not correct for amqps, got:", uri.Port) 371 | } 372 | } 373 | 374 | func TestURITLSConfig(t *testing.T) { 375 | url := "amqps://foo.bar/?certfile=/foo/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82/cert.pem&keyfile=/foo/%E4%BD%A0%E5%A5%BD/key.pem&cacertfile=C:%5Ccerts%5Cca.pem&server_name_indication=example.com" 376 | uri, err := ParseURI(url) 377 | if err != nil { 378 | t.Fatal("Could not parse") 379 | } 380 | if uri.CertFile != "/foo/привет/cert.pem" { 381 | t.Fatal("Certfile not set") 382 | } 383 | if uri.CACertFile != "C:\\certs\\ca.pem" { 384 | t.Fatal("CA not set") 385 | } 386 | if uri.KeyFile != "/foo/你好/key.pem" { 387 | t.Fatal("Key not set") 388 | } 389 | if uri.ServerName != "example.com" { 390 | t.Fatal("Server name not set") 391 | } 392 | } 393 | 394 | func TestURIParameters(t *testing.T) { 395 | url := "amqps://foo.bar/?auth_mechanism=plain&auth_mechanism=amqpplain&heartbeat=2&connection_timeout=5000&channel_max=8" 396 | uri, err := ParseURI(url) 397 | if err != nil { 398 | t.Fatal("Could not parse") 399 | } 400 | if !reflect.DeepEqual(uri.AuthMechanism, []string{"plain", "amqpplain"}) { 401 | t.Fatal("AuthMechanism not set") 402 | } 403 | if !uri.Heartbeat.hasValue { 404 | t.Fatal("Heartbeat not set") 405 | } 406 | if uri.Heartbeat.value != time.Duration(2)*time.Second { 407 | t.Fatal("Heartbeat not set") 408 | } 409 | if uri.ConnectionTimeout != 5000 { 410 | t.Fatal("ConnectionTimeout not set") 411 | } 412 | if uri.ChannelMax != 8 { 413 | t.Fatal("ChannelMax name not set") 414 | } 415 | } 416 | 417 | func TestURI_ParseUriToString(t *testing.T) { 418 | tests := []struct { 419 | name string 420 | uri string 421 | want string 422 | }{ 423 | {name: "virtual host is set", uri: "amqp://example.com/foobar", want: "amqp://example.com/foobar"}, 424 | {name: "non-default port", uri: "amqp://foo.bar:1234/example", want: "amqp://foo.bar:1234/example"}, 425 | { 426 | name: "TLS with URI parameters", 427 | uri: "amqps://some-host.com/foobar?certfile=/foo/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82/cert.pem&keyfile=/foo/%E4%BD%A0%E5%A5%BD/key.pem&cacertfile=C:%5Ccerts%5Cca.pem&server_name_indication=example.com", 428 | want: "amqps://some-host.com/foobar?certfile=/foo/привет/cert.pem&keyfile=/foo/你好/key.pem&cacertfile=C:\\certs\\ca.pem&server_name_indication=example.com", 429 | }, 430 | {name: "only server name indication", uri: "amqps://foo.bar?server_name_indication=example.com", want: "amqps://foo.bar/?server_name_indication=example.com"}, 431 | } 432 | for _, tt := range tests { 433 | t.Run(tt.name, func(t *testing.T) { 434 | amqpUri, err := ParseURI(tt.uri) 435 | if err != nil { 436 | t.Errorf("ParseURI() error = %v", err) 437 | } 438 | if got := amqpUri.String(); got != tt.want { 439 | t.Errorf("String() = %v, want %v", got, tt.want) 440 | } 441 | }) 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /write.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 VMware, Inc. or its affiliates. All Rights Reserved. 2 | // Copyright (c) 2012-2021, Sean Treadway, SoundCloud Ltd. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package amqp091 7 | 8 | import ( 9 | "bufio" 10 | "bytes" 11 | "encoding/binary" 12 | "errors" 13 | "fmt" 14 | "io" 15 | "math" 16 | "time" 17 | ) 18 | 19 | func (w *writer) WriteFrameNoFlush(frame frame) (err error) { 20 | err = frame.write(w.w) 21 | return 22 | } 23 | 24 | func (w *writer) WriteFrame(frame frame) (err error) { 25 | if err = frame.write(w.w); err != nil { 26 | return 27 | } 28 | 29 | if buf, ok := w.w.(*bufio.Writer); ok { 30 | err = buf.Flush() 31 | } 32 | 33 | return 34 | } 35 | 36 | func (f *methodFrame) write(w io.Writer) (err error) { 37 | var payload bytes.Buffer 38 | 39 | if f.Method == nil { 40 | return errors.New("malformed frame: missing method") 41 | } 42 | 43 | class, method := f.Method.id() 44 | 45 | if err = binary.Write(&payload, binary.BigEndian, class); err != nil { 46 | return 47 | } 48 | 49 | if err = binary.Write(&payload, binary.BigEndian, method); err != nil { 50 | return 51 | } 52 | 53 | if err = f.Method.write(&payload); err != nil { 54 | return 55 | } 56 | 57 | return writeFrame(w, frameMethod, f.ChannelId, payload.Bytes()) 58 | } 59 | 60 | // Heartbeat 61 | // 62 | // Payload is empty 63 | func (f *heartbeatFrame) write(w io.Writer) (err error) { 64 | return writeFrame(w, frameHeartbeat, f.ChannelId, []byte{}) 65 | } 66 | 67 | // CONTENT HEADER 68 | // 0 2 4 12 14 69 | // +----------+--------+-----------+----------------+------------- - - 70 | // | class-id | weight | body size | property flags | property list... 71 | // +----------+--------+-----------+----------------+------------- - - 72 | // 73 | // short short long long short remainder... 74 | func (f *headerFrame) write(w io.Writer) (err error) { 75 | var payload bytes.Buffer 76 | 77 | if err = binary.Write(&payload, binary.BigEndian, f.ClassId); err != nil { 78 | return 79 | } 80 | 81 | if err = binary.Write(&payload, binary.BigEndian, f.weight); err != nil { 82 | return 83 | } 84 | 85 | if err = binary.Write(&payload, binary.BigEndian, f.Size); err != nil { 86 | return 87 | } 88 | 89 | // First pass will build the mask to be serialized, second pass will serialize 90 | // each of the fields that appear in the mask. 91 | 92 | var mask uint16 93 | 94 | if f.Properties.ContentType != "" { 95 | mask |= flagContentType 96 | } 97 | if f.Properties.ContentEncoding != "" { 98 | mask |= flagContentEncoding 99 | } 100 | if len(f.Properties.Headers) > 0 { 101 | mask |= flagHeaders 102 | } 103 | if f.Properties.DeliveryMode > 0 { 104 | mask |= flagDeliveryMode 105 | } 106 | if f.Properties.Priority > 0 { 107 | mask |= flagPriority 108 | } 109 | if f.Properties.CorrelationId != "" { 110 | mask |= flagCorrelationId 111 | } 112 | if f.Properties.ReplyTo != "" { 113 | mask |= flagReplyTo 114 | } 115 | if f.Properties.Expiration != "" { 116 | mask |= flagExpiration 117 | } 118 | if f.Properties.MessageId != "" { 119 | mask |= flagMessageId 120 | } 121 | if !f.Properties.Timestamp.IsZero() { 122 | mask |= flagTimestamp 123 | } 124 | if f.Properties.Type != "" { 125 | mask |= flagType 126 | } 127 | if f.Properties.UserId != "" { 128 | mask |= flagUserId 129 | } 130 | if f.Properties.AppId != "" { 131 | mask |= flagAppId 132 | } 133 | 134 | if err = binary.Write(&payload, binary.BigEndian, mask); err != nil { 135 | return 136 | } 137 | 138 | if hasProperty(mask, flagContentType) { 139 | if err = writeShortstr(&payload, f.Properties.ContentType); err != nil { 140 | return 141 | } 142 | } 143 | if hasProperty(mask, flagContentEncoding) { 144 | if err = writeShortstr(&payload, f.Properties.ContentEncoding); err != nil { 145 | return 146 | } 147 | } 148 | if hasProperty(mask, flagHeaders) { 149 | if err = writeTable(&payload, f.Properties.Headers); err != nil { 150 | return 151 | } 152 | } 153 | if hasProperty(mask, flagDeliveryMode) { 154 | if err = binary.Write(&payload, binary.BigEndian, f.Properties.DeliveryMode); err != nil { 155 | return 156 | } 157 | } 158 | if hasProperty(mask, flagPriority) { 159 | if err = binary.Write(&payload, binary.BigEndian, f.Properties.Priority); err != nil { 160 | return 161 | } 162 | } 163 | if hasProperty(mask, flagCorrelationId) { 164 | if err = writeShortstr(&payload, f.Properties.CorrelationId); err != nil { 165 | return 166 | } 167 | } 168 | if hasProperty(mask, flagReplyTo) { 169 | if err = writeShortstr(&payload, f.Properties.ReplyTo); err != nil { 170 | return 171 | } 172 | } 173 | if hasProperty(mask, flagExpiration) { 174 | if err = writeShortstr(&payload, f.Properties.Expiration); err != nil { 175 | return 176 | } 177 | } 178 | if hasProperty(mask, flagMessageId) { 179 | if err = writeShortstr(&payload, f.Properties.MessageId); err != nil { 180 | return 181 | } 182 | } 183 | if hasProperty(mask, flagTimestamp) { 184 | if err = binary.Write(&payload, binary.BigEndian, uint64(f.Properties.Timestamp.Unix())); err != nil { 185 | return 186 | } 187 | } 188 | if hasProperty(mask, flagType) { 189 | if err = writeShortstr(&payload, f.Properties.Type); err != nil { 190 | return 191 | } 192 | } 193 | if hasProperty(mask, flagUserId) { 194 | if err = writeShortstr(&payload, f.Properties.UserId); err != nil { 195 | return 196 | } 197 | } 198 | if hasProperty(mask, flagAppId) { 199 | if err = writeShortstr(&payload, f.Properties.AppId); err != nil { 200 | return 201 | } 202 | } 203 | 204 | return writeFrame(w, frameHeader, f.ChannelId, payload.Bytes()) 205 | } 206 | 207 | // Body 208 | // 209 | // Payload is one byterange from the full body who's size is declared in the 210 | // Header frame 211 | func (f *bodyFrame) write(w io.Writer) (err error) { 212 | return writeFrame(w, frameBody, f.ChannelId, f.Body) 213 | } 214 | 215 | func writeFrame(w io.Writer, typ uint8, channel uint16, payload []byte) (err error) { 216 | end := []byte{frameEnd} 217 | size := uint(len(payload)) 218 | 219 | _, err = w.Write([]byte{ 220 | typ, 221 | byte((channel & 0xff00) >> 8), 222 | byte((channel & 0x00ff) >> 0), 223 | byte((size & 0xff000000) >> 24), 224 | byte((size & 0x00ff0000) >> 16), 225 | byte((size & 0x0000ff00) >> 8), 226 | byte((size & 0x000000ff) >> 0), 227 | }) 228 | if err != nil { 229 | return 230 | } 231 | 232 | if _, err = w.Write(payload); err != nil { 233 | return 234 | } 235 | 236 | if _, err = w.Write(end); err != nil { 237 | return 238 | } 239 | 240 | return 241 | } 242 | 243 | func writeShortstr(w io.Writer, s string) (err error) { 244 | b := []byte(s) 245 | 246 | length := uint8(len(b)) 247 | 248 | if err = binary.Write(w, binary.BigEndian, length); err != nil { 249 | return 250 | } 251 | 252 | if _, err = w.Write(b[:length]); err != nil { 253 | return 254 | } 255 | 256 | return 257 | } 258 | 259 | func writeLongstr(w io.Writer, s string) (err error) { 260 | b := []byte(s) 261 | 262 | length := uint32(len(b)) 263 | 264 | if err = binary.Write(w, binary.BigEndian, length); err != nil { 265 | return 266 | } 267 | 268 | if _, err = w.Write(b[:length]); err != nil { 269 | return 270 | } 271 | 272 | return 273 | } 274 | 275 | /* 276 | 'A': []interface{} 277 | 'D': Decimal 278 | 'F': Table 279 | 'I': int32 280 | 'S': string 281 | 'T': time.Time 282 | 'V': nil 283 | 'b': int8 284 | 'B': byte 285 | 'd': float64 286 | 'f': float32 287 | 'l': int64 288 | 's': int16 289 | 't': bool 290 | 'x': []byte 291 | */ 292 | func writeField(w io.Writer, value interface{}) (err error) { 293 | var buf [9]byte 294 | var enc []byte 295 | 296 | switch v := value.(type) { 297 | case bool: 298 | buf[0] = 't' 299 | if v { 300 | buf[1] = byte(1) 301 | } else { 302 | buf[1] = byte(0) 303 | } 304 | enc = buf[:2] 305 | 306 | case byte: 307 | buf[0] = 'B' 308 | buf[1] = v 309 | enc = buf[:2] 310 | 311 | case int8: 312 | buf[0] = 'b' 313 | buf[1] = uint8(v) 314 | enc = buf[:2] 315 | 316 | case int16: 317 | buf[0] = 's' 318 | binary.BigEndian.PutUint16(buf[1:3], uint16(v)) 319 | enc = buf[:3] 320 | 321 | case int: 322 | buf[0] = 'I' 323 | binary.BigEndian.PutUint32(buf[1:5], uint32(v)) 324 | enc = buf[:5] 325 | 326 | case int32: 327 | buf[0] = 'I' 328 | binary.BigEndian.PutUint32(buf[1:5], uint32(v)) 329 | enc = buf[:5] 330 | 331 | case int64: 332 | buf[0] = 'l' 333 | binary.BigEndian.PutUint64(buf[1:9], uint64(v)) 334 | enc = buf[:9] 335 | 336 | case float32: 337 | buf[0] = 'f' 338 | binary.BigEndian.PutUint32(buf[1:5], math.Float32bits(v)) 339 | enc = buf[:5] 340 | 341 | case float64: 342 | buf[0] = 'd' 343 | binary.BigEndian.PutUint64(buf[1:9], math.Float64bits(v)) 344 | enc = buf[:9] 345 | 346 | case Decimal: 347 | buf[0] = 'D' 348 | buf[1] = v.Scale 349 | binary.BigEndian.PutUint32(buf[2:6], uint32(v.Value)) 350 | enc = buf[:6] 351 | 352 | case string: 353 | buf[0] = 'S' 354 | binary.BigEndian.PutUint32(buf[1:5], uint32(len(v))) 355 | enc = append(buf[:5], []byte(v)...) 356 | 357 | case []interface{}: // field-array 358 | buf[0] = 'A' 359 | 360 | sec := new(bytes.Buffer) 361 | for _, val := range v { 362 | if err = writeField(sec, val); err != nil { 363 | return 364 | } 365 | } 366 | 367 | binary.BigEndian.PutUint32(buf[1:5], uint32(sec.Len())) 368 | if _, err = w.Write(buf[:5]); err != nil { 369 | return 370 | } 371 | 372 | if _, err = w.Write(sec.Bytes()); err != nil { 373 | return 374 | } 375 | 376 | return 377 | 378 | case time.Time: 379 | buf[0] = 'T' 380 | binary.BigEndian.PutUint64(buf[1:9], uint64(v.Unix())) 381 | enc = buf[:9] 382 | 383 | case Table: 384 | if _, err = w.Write([]byte{'F'}); err != nil { 385 | return 386 | } 387 | return writeTable(w, v) 388 | 389 | case []byte: 390 | buf[0] = 'x' 391 | binary.BigEndian.PutUint32(buf[1:5], uint32(len(v))) 392 | if _, err = w.Write(buf[0:5]); err != nil { 393 | return 394 | } 395 | if _, err = w.Write(v); err != nil { 396 | return 397 | } 398 | return 399 | 400 | case nil: 401 | buf[0] = 'V' 402 | enc = buf[:1] 403 | 404 | default: 405 | return ErrFieldType 406 | } 407 | 408 | _, err = w.Write(enc) 409 | 410 | return 411 | } 412 | 413 | // writeTable serializes a Table to the given writer. 414 | // It writes each key-value pair and returns the serialized data as a longstr. 415 | func writeTable(w io.Writer, table Table) (err error) { 416 | var buf bytes.Buffer 417 | 418 | for key, val := range table { 419 | if err = writeShortstr(&buf, key); err != nil { 420 | return fmt.Errorf("writing key %q: %w", key, err) 421 | } 422 | if err = writeField(&buf, val); err != nil { 423 | return fmt.Errorf("writing value for key %q: %w", key, err) 424 | } 425 | } 426 | 427 | if err := writeLongstr(w, buf.String()); err != nil { 428 | return fmt.Errorf("writing final long string: %w", err) 429 | } 430 | 431 | return nil 432 | } 433 | --------------------------------------------------------------------------------