├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── command ├── api.go ├── api_test.go ├── auth.go ├── auth_test.go ├── call │ ├── execute.go │ ├── execute_test.go │ ├── hangup.go │ ├── hangup_test.go │ ├── nomedia.go │ ├── nomedia_test.go │ ├── transfer.go │ ├── transfer_test.go │ ├── unicast.go │ └── unicast_test.go ├── command.go ├── connect.go ├── connect_test.go ├── event.go ├── event_test.go ├── exit.go ├── exit_test.go ├── filter.go ├── filter_test.go ├── linger.go ├── linger_test.go ├── log.go ├── log_test.go └── sendmsg.go ├── connection.go ├── connection_test.go ├── event.go ├── event_test.go ├── example ├── events │ └── events.go ├── inbound │ └── inbound.go └── outbound │ └── outbound.go ├── go.mod ├── go.sum ├── helper.go ├── helper_call.go ├── inbound.go ├── logger.go ├── outbound.go ├── response.go ├── utils.go └── utils_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.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 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [v1] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [v1] 14 | schedule: 15 | - cron: '0 14 * * 3' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['go'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ v1 ] 6 | pull_request: 7 | branches: [ v1 ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.14 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Vet 26 | run: go vet ./... 27 | 28 | - name: Test 29 | run: go test -v ./... 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## IntelliJ 2 | .idea/ 3 | *.iml 4 | 5 | ## Linux 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ## macOS 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ## Windows 49 | # Windows thumbnail cache files 50 | Thumbs.db 51 | Thumbs.db:encryptable 52 | ehthumbs.db 53 | ehthumbs_vista.db 54 | 55 | # Dump file 56 | *.stackdump 57 | 58 | # Folder config file 59 | [Dd]esktop.ini 60 | 61 | # Recycle Bin used on file shares 62 | $RECYCLE.BIN/ 63 | 64 | # Windows Installer files 65 | *.cab 66 | *.msi 67 | *.msix 68 | *.msm 69 | *.msp 70 | 71 | # Windows shortcuts 72 | *.lnk 73 | 74 | ## GoLang 75 | # Binaries for programs and plugins 76 | *.exe 77 | *.exe~ 78 | *.dll 79 | *.so 80 | *.dylib 81 | 82 | # Test binary, built with `go test -c` 83 | *.test 84 | 85 | # Output of the go coverage tool, specifically when used with LiteIDE 86 | *.out 87 | 88 | # Dependency directories (remove the comment below to include it) 89 | vendor/ 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslgo 2 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/percipia/eslgo)](https://pkg.go.dev/github.com/percipia/eslgo) 3 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/percipia/eslgo/Go) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/percipia/eslgo)](https://goreportcard.com/report/github.com/percipia/eslgo) 5 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/percipia/eslgo.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/percipia/eslgo/alerts/) 6 | [![GitHub license](https://img.shields.io/github/license/percipia/eslgo)](https://github.com/percipia/eslgo/blob/v1/LICENSE) 7 | 8 | eslgo is a [FreeSWITCH™](https://freeswitch.com/) ESL library for GoLang. 9 | eslgo was written from the ground up in idiomatic Go for use in our production products tested handling thousands of calls per second. 10 | 11 | ## Install 12 | ``` 13 | go get github.com/percipia/eslgo 14 | ``` 15 | ``` 16 | github.com/percipia/eslgo v1.4.1 17 | ``` 18 | 19 | ## Overview 20 | - Inbound ESL Connection 21 | - Outbound ESL Server 22 | - Event listeners by UUID or All events 23 | - Unique-Id 24 | - Application-UUID 25 | - Job-UUID 26 | - Context support for canceling requests 27 | - All command types abstracted out 28 | - You can also send custom data by implementing the `Command` interface 29 | - `BuildMessage() string` 30 | - Basic Helpers for common tasks 31 | - DTMF 32 | - Call origination 33 | - Call answer/hangup 34 | - Audio playback 35 | 36 | ## Examples 37 | There are some buildable examples under the `example` directory as well 38 | ### Outbound ESL Server 39 | ```go 40 | package main 41 | 42 | import ( 43 | "context" 44 | "fmt" 45 | "github.com/percipia/eslgo" 46 | "log" 47 | ) 48 | 49 | func main() { 50 | // Start listening, this is a blocking function 51 | log.Fatalln(eslgo.ListenAndServe(":8084", handleConnection)) 52 | } 53 | 54 | func handleConnection(ctx context.Context, conn *eslgo.Conn, response *eslgo.RawResponse) { 55 | fmt.Printf("Got connection! %#v\n", response) 56 | 57 | // Place the call in the foreground(api) to user 100 and playback an audio file as the bLeg and no exported variables 58 | response, err := conn.OriginateCall(ctx, false, eslgo.Leg{CallURL: "user/100"}, eslgo.Leg{CallURL: "&playback(misc/ivr-to_hear_screaming_monkeys.wav)"}, map[string]string{}) 59 | fmt.Println("Call Originated: ", response, err) 60 | } 61 | ``` 62 | ## Inbound ESL Client 63 | ```go 64 | package main 65 | 66 | import ( 67 | "context" 68 | "fmt" 69 | "github.com/percipia/eslgo" 70 | "time" 71 | ) 72 | 73 | func main() { 74 | // Connect to FreeSWITCH 75 | conn, err := eslgo.Dial("127.0.0.1:8021", "ClueCon", func() { 76 | fmt.Println("Inbound Connection Disconnected") 77 | }) 78 | if err != nil { 79 | fmt.Println("Error connecting", err) 80 | return 81 | } 82 | 83 | // Create a basic context 84 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 85 | defer cancel() 86 | 87 | // Place the call in the background(bgapi) to user 100 and playback an audio file as the bLeg and no exported variables 88 | response, err := conn.OriginateCall(ctx, true, eslgo.Leg{CallURL: "user/100"}, eslgo.Leg{CallURL: "&playback(misc/ivr-to_hear_screaming_monkeys.wav)"}, map[string]string{}) 89 | fmt.Println("Call Originated: ", response, err) 90 | 91 | // Close the connection after sleeping for a bit 92 | time.Sleep(60 * time.Second) 93 | conn.ExitAndClose() 94 | } 95 | ``` -------------------------------------------------------------------------------- /command/api.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import "fmt" 14 | 15 | type API struct { 16 | Command string 17 | Arguments string 18 | Background bool 19 | } 20 | 21 | func (api API) BuildMessage() string { 22 | if api.Background { 23 | return fmt.Sprintf("bgapi %s %s", api.Command, api.Arguments) 24 | } 25 | return fmt.Sprintf("api %s %s", api.Command, api.Arguments) 26 | } 27 | -------------------------------------------------------------------------------- /command/api_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "testing" 16 | ) 17 | 18 | const ( 19 | TestAPIMessage = `api originate user/100 &park()` 20 | TestBGAPIMessage = `bgapi originate user/100 &park()` 21 | ) 22 | 23 | func TestAPI_BuildMessage(t *testing.T) { 24 | api := API{ 25 | Command: "originate", 26 | Arguments: "user/100 &park()", 27 | } 28 | assert.Equal(t, TestAPIMessage, api.BuildMessage()) 29 | } 30 | 31 | func TestAPI_BuildMessage_BG(t *testing.T) { 32 | api := API{ 33 | Command: "originate", 34 | Arguments: "user/100 &park()", 35 | Background: true, 36 | } 37 | assert.Equal(t, TestBGAPIMessage, api.BuildMessage()) 38 | } 39 | -------------------------------------------------------------------------------- /command/auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import "fmt" 14 | 15 | type Auth struct { 16 | User string 17 | Password string 18 | } 19 | 20 | func (auth Auth) BuildMessage() string { 21 | if len(auth.User) > 0 { 22 | return fmt.Sprintf("userauth %s:%s", auth.User, auth.Password) 23 | } 24 | return fmt.Sprintf("auth %s", auth.Password) 25 | } 26 | -------------------------------------------------------------------------------- /command/auth_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "testing" 16 | ) 17 | 18 | const ( 19 | TestAuthMessage = `auth testing123` 20 | TestUserAuthMessage = `userauth testuser:testing123` 21 | ) 22 | 23 | func TestAuth_BuildMessage(t *testing.T) { 24 | auth := Auth{ 25 | Password: "testing123", 26 | } 27 | assert.Equal(t, TestAuthMessage, auth.BuildMessage()) 28 | } 29 | 30 | func TestAuth_BuildMessage_User(t *testing.T) { 31 | auth := Auth{ 32 | User: "testuser", 33 | Password: "testing123", 34 | } 35 | assert.Equal(t, TestUserAuthMessage, auth.BuildMessage()) 36 | } 37 | -------------------------------------------------------------------------------- /command/call/execute.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "fmt" 15 | "github.com/percipia/eslgo/command" 16 | "net/textproto" 17 | "strconv" 18 | ) 19 | 20 | type Execute struct { 21 | UUID string 22 | AppName string 23 | AppArgs string 24 | AppUUID string 25 | Loops int 26 | Sync bool 27 | SyncPri bool 28 | ForceBody bool 29 | } 30 | 31 | // Helper to call Execute with Set since it is commonly used 32 | type Set struct { 33 | UUID string 34 | Key string 35 | Value string 36 | Sync bool 37 | SyncPri bool 38 | } 39 | 40 | // Helper to call Execute with Export since it is commonly used 41 | type Export Set 42 | 43 | // Helper to call Execute with Push since it is commonly used 44 | type Push Set 45 | 46 | func (s Set) buildMessage(app string) string { 47 | e := Execute{ 48 | UUID: s.UUID, 49 | AppName: app, 50 | AppArgs: fmt.Sprintf("%s=%s", s.Key, s.Value), 51 | Sync: s.Sync, 52 | SyncPri: s.SyncPri, 53 | ForceBody: true, 54 | } 55 | return e.BuildMessage() 56 | } 57 | 58 | func (s Set) BuildMessage() string { 59 | return s.buildMessage("set") 60 | } 61 | 62 | func (e Export) BuildMessage() string { 63 | return Set(e).buildMessage("export") 64 | } 65 | 66 | func (p Push) BuildMessage() string { 67 | return Set(p).buildMessage("push") 68 | } 69 | 70 | func (e *Execute) BuildMessage() string { 71 | if e.Loops == 0 { 72 | e.Loops = 1 73 | } 74 | sendMsg := command.SendMessage{ 75 | UUID: e.UUID, 76 | Headers: make(textproto.MIMEHeader), 77 | Sync: e.Sync, 78 | SyncPri: e.SyncPri, 79 | } 80 | sendMsg.Headers.Set("call-command", "execute") 81 | sendMsg.Headers.Set("execute-app-name", e.AppName) 82 | sendMsg.Headers.Set("loops", strconv.Itoa(e.Loops)) 83 | // This allows us to track when application execution completes via the Application-UUID header in events. 84 | if e.AppUUID != "" { 85 | sendMsg.Headers.Set("Event-UUID", e.AppUUID) 86 | } 87 | 88 | // According to documentation that is the max header length 89 | if len(e.AppArgs) > 2048 || e.ForceBody { 90 | sendMsg.Headers.Set("content-type", "text/plain") 91 | sendMsg.Headers.Set("content-length", strconv.Itoa(len(e.AppArgs))) 92 | sendMsg.Body = e.AppArgs 93 | } else { 94 | sendMsg.Headers.Set("execute-app-arg", e.AppArgs) 95 | } 96 | 97 | return sendMsg.BuildMessage() 98 | } 99 | -------------------------------------------------------------------------------- /command/call/execute_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "strings" 16 | "testing" 17 | ) 18 | 19 | var ( 20 | TestExecMessage = strings.ReplaceAll(`sendmsg none 21 | Call-Command: execute 22 | Execute-App-Arg: /tmp/test.wav 23 | Execute-App-Name: playback 24 | Loops: 1`, "\n", "\r\n") 25 | TestSetMessage = strings.ReplaceAll(`sendmsg none 26 | Call-Command: execute 27 | Content-Length: 11 28 | Content-Type: text/plain 29 | Execute-App-Name: set 30 | Loops: 1 31 | 32 | hello=world`, "\n", "\r\n") 33 | TestExportMessage = strings.ReplaceAll(`sendmsg none 34 | Call-Command: execute 35 | Content-Length: 11 36 | Content-Type: text/plain 37 | Execute-App-Name: export 38 | Loops: 1 39 | 40 | hello=world`, "\n", "\r\n") 41 | TestPushMessage = strings.ReplaceAll(`sendmsg none 42 | Call-Command: execute 43 | Content-Length: 11 44 | Content-Type: text/plain 45 | Execute-App-Name: push 46 | Loops: 1 47 | 48 | hello=world`, "\n", "\r\n") 49 | ) 50 | 51 | func TestExecute_BuildMessage(t *testing.T) { 52 | exec := Execute{ 53 | UUID: "none", 54 | AppName: "playback", 55 | AppArgs: "/tmp/test.wav", 56 | } 57 | assert.Equal(t, TestExecMessage, exec.BuildMessage()) 58 | } 59 | 60 | func TestSet_BuildMessage(t *testing.T) { 61 | set := Set{ 62 | UUID: "none", 63 | Key: "hello", 64 | Value: "world", 65 | } 66 | assert.Equal(t, TestSetMessage, set.BuildMessage()) 67 | } 68 | 69 | func TestExport_BuildMessage(t *testing.T) { 70 | export := Export{ 71 | UUID: "none", 72 | Key: "hello", 73 | Value: "world", 74 | } 75 | assert.Equal(t, TestExportMessage, export.BuildMessage()) 76 | } 77 | 78 | func TestPush_BuildMessage(t *testing.T) { 79 | push := Push{ 80 | UUID: "none", 81 | Key: "hello", 82 | Value: "world", 83 | } 84 | assert.Equal(t, TestPushMessage, push.BuildMessage()) 85 | } 86 | -------------------------------------------------------------------------------- /command/call/hangup.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "github.com/percipia/eslgo/command" 15 | "net/textproto" 16 | ) 17 | 18 | type Hangup struct { 19 | UUID string 20 | Cause string 21 | Sync bool 22 | SyncPri bool 23 | } 24 | 25 | func (h Hangup) BuildMessage() string { 26 | sendMsg := command.SendMessage{ 27 | UUID: h.UUID, 28 | Headers: make(textproto.MIMEHeader), 29 | Sync: h.Sync, 30 | SyncPri: h.SyncPri, 31 | } 32 | sendMsg.Headers.Set("call-command", "hangup") 33 | sendMsg.Headers.Set("hangup-cause", h.Cause) 34 | 35 | return sendMsg.BuildMessage() 36 | } 37 | -------------------------------------------------------------------------------- /command/call/hangup_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "strings" 16 | "testing" 17 | ) 18 | 19 | var TestHangupMessage = strings.ReplaceAll(`sendmsg none 20 | Call-Command: hangup 21 | Hangup-Cause: NORMAL_CLEARING`, "\n", "\r\n") 22 | 23 | func TestHangup_BuildMessage(t *testing.T) { 24 | hangup := Hangup{ 25 | UUID: "none", 26 | Cause: "NORMAL_CLEARING", 27 | } 28 | assert.Equal(t, TestHangupMessage, hangup.BuildMessage()) 29 | } 30 | -------------------------------------------------------------------------------- /command/call/nomedia.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "github.com/percipia/eslgo/command" 15 | "net/textproto" 16 | ) 17 | 18 | type NoMedia struct { 19 | UUID string 20 | NoMediaUUID string 21 | Sync bool 22 | SyncPri bool 23 | } 24 | 25 | func (n NoMedia) BuildMessage() string { 26 | sendMsg := command.SendMessage{ 27 | UUID: n.UUID, 28 | Headers: make(textproto.MIMEHeader), 29 | Sync: n.Sync, 30 | SyncPri: n.SyncPri, 31 | } 32 | sendMsg.Headers.Set("call-command", "nomedia") 33 | sendMsg.Headers.Set("nomedia-uuid", n.NoMediaUUID) 34 | 35 | return sendMsg.BuildMessage() 36 | } 37 | -------------------------------------------------------------------------------- /command/call/nomedia_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "strings" 16 | "testing" 17 | ) 18 | 19 | var TestNoMediaMessage = strings.ReplaceAll(`sendmsg none 20 | Call-Command: nomedia 21 | Nomedia-Uuid: test`, "\n", "\r\n") 22 | 23 | func TestNoMedia_BuildMessage(t *testing.T) { 24 | nomedia := NoMedia{ 25 | UUID: "none", 26 | NoMediaUUID: "test", 27 | } 28 | assert.Equal(t, TestNoMediaMessage, nomedia.BuildMessage()) 29 | } 30 | -------------------------------------------------------------------------------- /command/call/transfer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "github.com/percipia/eslgo/command" 15 | "net/textproto" 16 | ) 17 | 18 | // Documentation is sparse on this, but it looks like it transfers a call to an application? 19 | type Transfer struct { 20 | UUID string 21 | Application string 22 | Sync bool 23 | SyncPri bool 24 | } 25 | 26 | func (t Transfer) BuildMessage() string { 27 | sendMsg := command.SendMessage{ 28 | UUID: t.UUID, 29 | Headers: make(textproto.MIMEHeader), 30 | Sync: t.Sync, 31 | SyncPri: t.SyncPri, 32 | } 33 | sendMsg.Headers.Set("call-command", "xferext") 34 | sendMsg.Headers.Set("application", t.Application) 35 | 36 | return sendMsg.BuildMessage() 37 | } 38 | -------------------------------------------------------------------------------- /command/call/transfer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "testing" 15 | ) 16 | 17 | func TestTransfer_BuildMessage(t *testing.T) { 18 | // No real documentation on this 19 | } 20 | -------------------------------------------------------------------------------- /command/call/unicast.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "github.com/percipia/eslgo/command" 15 | "net" 16 | "net/textproto" 17 | ) 18 | 19 | /* 20 | * unicast is used to hook up mod_spandsp for faxing over a socket. 21 | * Note: 22 | * That is a nice way for a script or app that uses the socket interface to get at the media. 23 | * It's good because then spandsp isn't living inside of FreeSWITCH and it can run on a box sitting next to it. It scales better. 24 | */ 25 | type Unicast struct { 26 | UUID string 27 | Local net.Addr 28 | Remote net.Addr 29 | Flags string 30 | Sync bool 31 | SyncPri bool 32 | } 33 | 34 | func (u Unicast) BuildMessage() string { 35 | sendMsg := command.SendMessage{ 36 | UUID: u.UUID, 37 | Headers: make(textproto.MIMEHeader), 38 | Sync: u.Sync, 39 | SyncPri: u.SyncPri, 40 | } 41 | localHost, localPort, _ := net.SplitHostPort(u.Local.String()) 42 | remoteHost, remotePort, _ := net.SplitHostPort(u.Remote.String()) 43 | sendMsg.Headers.Set("call-command", "unicast") 44 | sendMsg.Headers.Set("local-ip", localHost) 45 | sendMsg.Headers.Set("local-port", localPort) 46 | sendMsg.Headers.Set("remote-ip", remoteHost) 47 | sendMsg.Headers.Set("remote-port", remotePort) 48 | sendMsg.Headers.Set("transport", u.Local.Network()) 49 | if len(u.Flags) > 0 { 50 | sendMsg.Headers.Set("flags", u.Flags) 51 | } 52 | return sendMsg.BuildMessage() 53 | } 54 | -------------------------------------------------------------------------------- /command/call/unicast_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package call 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "net" 16 | "strings" 17 | "testing" 18 | ) 19 | 20 | var TestUnicastMessage = strings.ReplaceAll(`sendmsg none 21 | Call-Command: unicast 22 | Flags: native 23 | Local-Ip: 192.168.1.100 24 | Local-Port: 8025 25 | Remote-Ip: 192.168.1.101 26 | Remote-Port: 8026 27 | Transport: tcp`, "\n", "\r\n") 28 | 29 | func TestUnicast_BuildMessage(t *testing.T) { 30 | testLocal, _ := net.ResolveTCPAddr("tcp", "192.168.1.100:8025") 31 | testRemote, _ := net.ResolveTCPAddr("tcp", "192.168.1.101:8026") 32 | unicast := Unicast{ 33 | UUID: "none", 34 | Local: testLocal, 35 | Remote: testRemote, 36 | Flags: "native", 37 | } 38 | assert.Equal(t, TestUnicastMessage, unicast.BuildMessage()) 39 | } 40 | -------------------------------------------------------------------------------- /command/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "net/textproto" 15 | "sort" 16 | "strings" 17 | ) 18 | 19 | // Command - A basic interface for FreeSWITCH ESL commands. Implement this if you want to send your own raw data to FreeSIWTCH over the ESL connection. Do not add the eslgo.EndOfMessage(\r\n\r\n) marker, eslgo does that for you. 20 | type Command interface { 21 | BuildMessage() string 22 | } 23 | 24 | var crlfToLF = strings.NewReplacer("\r\n", "\n") 25 | 26 | // FormatHeaderString - Writes headers in a FreeSWITCH ESL friendly format. Converts headers containing \r\n to \n 27 | func FormatHeaderString(headers textproto.MIMEHeader) string { 28 | var ws strings.Builder 29 | 30 | keys := make([]string, len(headers)) 31 | i := 0 32 | for key := range headers { 33 | keys[i] = key 34 | i++ 35 | } 36 | sort.Strings(keys) 37 | 38 | for _, key := range keys { 39 | for _, value := range headers[key] { 40 | value = crlfToLF.Replace(value) 41 | value = textproto.TrimString(value) 42 | ws.WriteString(key) 43 | ws.WriteString(": ") 44 | ws.WriteString(value) 45 | ws.WriteString("\r\n") 46 | } 47 | } 48 | // Remove the extra \r\n 49 | return ws.String()[:ws.Len()-2] 50 | } 51 | -------------------------------------------------------------------------------- /command/connect.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | type Connect struct{} 14 | 15 | func (Connect) BuildMessage() string { 16 | return "connect" 17 | } 18 | -------------------------------------------------------------------------------- /command/connect_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "testing" 16 | ) 17 | 18 | func TestConnect_BuildMessage(t *testing.T) { 19 | assert.Equal(t, "connect", Connect{}.BuildMessage()) 20 | } 21 | -------------------------------------------------------------------------------- /command/event.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "fmt" 15 | "net/textproto" 16 | "strconv" 17 | "strings" 18 | ) 19 | 20 | type Event struct { 21 | Ignore bool 22 | Format string 23 | Listen []string 24 | } 25 | 26 | type MyEvents struct { 27 | Format string 28 | UUID string 29 | } 30 | 31 | type DisableEvents struct{} 32 | 33 | // The divert_events command is available to allow events that an embedded script would expect to get in the inputcallback to be diverted to the event socket. 34 | type DivertEvents struct { 35 | Enabled bool 36 | } 37 | 38 | type SendEvent struct { 39 | Name string 40 | Headers textproto.MIMEHeader 41 | Body string 42 | } 43 | 44 | func (e Event) BuildMessage() string { 45 | prefix := "" 46 | if e.Ignore { 47 | prefix = "nix" 48 | } 49 | return fmt.Sprintf("%sevent %s %s", prefix, e.Format, strings.Join(e.Listen, " ")) 50 | } 51 | 52 | func (m MyEvents) BuildMessage() string { 53 | if len(m.UUID) > 0 { 54 | return fmt.Sprintf("myevents %s %s", m.Format, m.UUID) 55 | 56 | } 57 | return fmt.Sprintf("myevents %s", m.Format) 58 | } 59 | 60 | func (DisableEvents) BuildMessage() string { 61 | return "noevents" 62 | } 63 | 64 | func (d DivertEvents) BuildMessage() string { 65 | if d.Enabled { 66 | return "divert_events on" 67 | } 68 | return "divert_events off" 69 | } 70 | 71 | func (s *SendEvent) BuildMessage() string { 72 | // Ensure the correct content length is set in the header 73 | if len(s.Body) > 0 { 74 | s.Headers.Set("Content-Length", strconv.Itoa(len(s.Body))) 75 | } else { 76 | delete(s.Headers, "Content-Length") 77 | } 78 | 79 | // Format the headers 80 | headerString := FormatHeaderString(s.Headers) 81 | if _, ok := s.Headers["Content-Length"]; ok { 82 | return fmt.Sprintf("sendevent %s\r\n%s\r\n\r\n%s", s.Name, headerString, s.Body) 83 | } 84 | return fmt.Sprintf("sendevent %s\r\n%s", s.Name, headerString) 85 | } 86 | -------------------------------------------------------------------------------- /command/event_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "strings" 16 | "testing" 17 | ) 18 | 19 | var TestSendEventMessage = strings.ReplaceAll(`sendevent MESSAGE_WAITING 20 | MWI-Message-Account: 7100@192.168.1.1 21 | MWI-Messages-Waiting: yes 22 | MWI-Voice-Message: 5/5 (1/1)`, "\n", "\r\n") 23 | 24 | func TestDisableEvents_BuildMessage(t *testing.T) { 25 | assert.Equal(t, "noevents", DisableEvents{}.BuildMessage()) 26 | } 27 | 28 | func TestDivertEvents_BuildMessage(t *testing.T) { 29 | assert.Equal(t, "divert_events on", DivertEvents{true}.BuildMessage()) 30 | assert.Equal(t, "divert_events off", DivertEvents{false}.BuildMessage()) 31 | } 32 | 33 | func TestEvent_BuildMessage(t *testing.T) { 34 | assert.Equal(t, "event plain MESSAGE_QUERY", Event{ 35 | Format: "plain", 36 | Listen: []string{"MESSAGE_QUERY"}, 37 | }.BuildMessage()) 38 | assert.Equal(t, "nixevent plain MESSAGE_QUERY", Event{ 39 | Ignore: true, 40 | Format: "plain", 41 | Listen: []string{"MESSAGE_QUERY"}, 42 | }.BuildMessage()) 43 | } 44 | 45 | func TestMyEvents_BuildMessage(t *testing.T) { 46 | assert.Equal(t, "myevents plain none", MyEvents{Format: "plain", UUID: "none"}.BuildMessage()) 47 | } 48 | 49 | func TestSendEvent_BuildMessage(t *testing.T) { 50 | sendEvent := SendEvent{ 51 | Name: "MESSAGE_WAITING", 52 | Headers: map[string][]string{ 53 | "MWI-Messages-Waiting": {"yes"}, 54 | "MWI-Message-Account": {"7100@192.168.1.1"}, 55 | "MWI-Voice-Message": {"5/5 (1/1)"}, 56 | }, 57 | } 58 | assert.Equal(t, TestSendEventMessage, sendEvent.BuildMessage()) 59 | } 60 | -------------------------------------------------------------------------------- /command/exit.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | type Exit struct{} 14 | 15 | func (Exit) BuildMessage() string { 16 | return "exit" 17 | } 18 | -------------------------------------------------------------------------------- /command/exit_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "testing" 16 | ) 17 | 18 | func TestExit_BuildMessage(t *testing.T) { 19 | assert.Equal(t, "exit", Exit{}.BuildMessage()) 20 | } 21 | -------------------------------------------------------------------------------- /command/filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import "fmt" 14 | 15 | type Filter struct { 16 | Delete bool 17 | EventHeader string 18 | FilterValue string 19 | } 20 | 21 | func (f Filter) BuildMessage() string { 22 | if f.Delete { 23 | if len(f.FilterValue) > 0 { 24 | // Clear just the specific header value 25 | return fmt.Sprintf("filter delete %s %s", f.EventHeader, f.FilterValue) 26 | } 27 | // Clears all filters for the header 28 | return fmt.Sprintf("filter delete %s", f.EventHeader) 29 | } 30 | return fmt.Sprintf("filter %s %s", f.EventHeader, f.FilterValue) 31 | } 32 | -------------------------------------------------------------------------------- /command/filter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "testing" 16 | ) 17 | 18 | func TestFilter_BuildMessage(t *testing.T) { 19 | assert.Equal(t, "filter variable_domain_name 192.168.1.1", Filter{ 20 | EventHeader: "variable_domain_name", 21 | FilterValue: "192.168.1.1", 22 | }.BuildMessage()) 23 | assert.Equal(t, "filter delete variable_domain_name 192.168.1.1", Filter{ 24 | Delete: true, 25 | EventHeader: "variable_domain_name", 26 | FilterValue: "192.168.1.1", 27 | }.BuildMessage()) 28 | } 29 | -------------------------------------------------------------------------------- /command/linger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | type Linger struct { 14 | Enabled bool 15 | } 16 | 17 | func (l Linger) BuildMessage() string { 18 | if l.Enabled { 19 | return "linger" 20 | } 21 | return "nolinger" 22 | } 23 | -------------------------------------------------------------------------------- /command/linger_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "testing" 16 | ) 17 | 18 | func TestNoLinger_BuildMessage(t *testing.T) { 19 | assert.Equal(t, "nolinger", Linger{}.BuildMessage()) 20 | } 21 | 22 | func TestLinger_BuildMessage(t *testing.T) { 23 | assert.Equal(t, "linger", Linger{true}.BuildMessage()) 24 | } 25 | -------------------------------------------------------------------------------- /command/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import "fmt" 14 | 15 | type Log struct { 16 | Enabled bool 17 | Level int 18 | } 19 | 20 | func (l Log) BuildMessage() string { 21 | if l.Enabled { 22 | return fmt.Sprintf("log %d", l.Level) 23 | } 24 | return "nolog" 25 | } 26 | -------------------------------------------------------------------------------- /command/log_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "testing" 16 | ) 17 | 18 | func TestLog_BuildMessage(t *testing.T) { 19 | assert.Equal(t, "log 9", Log{Enabled: true, Level: 9}.BuildMessage()) 20 | } 21 | 22 | func TestNoLog_BuildMessage(t *testing.T) { 23 | assert.Equal(t, "nolog", Log{}.BuildMessage()) 24 | } 25 | -------------------------------------------------------------------------------- /command/sendmsg.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package command 12 | 13 | import ( 14 | "fmt" 15 | "net/textproto" 16 | "strconv" 17 | ) 18 | 19 | type SendMessage struct { 20 | UUID string 21 | Headers textproto.MIMEHeader 22 | Body string 23 | Sync bool 24 | SyncPri bool 25 | } 26 | 27 | func (s *SendMessage) BuildMessage() string { 28 | if s.Headers == nil { 29 | s.Headers = make(textproto.MIMEHeader) 30 | } 31 | // Waits for this event to finish before continuing even in async mode 32 | if s.Sync { 33 | s.Headers.Set("event-lock", "true") 34 | } 35 | // No documentation on this flag, I assume it takes priority over the other flag? 36 | if s.SyncPri { 37 | s.Headers.Set("event-lock-pri", "true") 38 | } 39 | 40 | // Ensure the correct content length is set in the header 41 | if len(s.Body) > 0 { 42 | s.Headers.Set("Content-Length", strconv.Itoa(len(s.Body))) 43 | } else { 44 | delete(s.Headers, "Content-Length") 45 | } 46 | 47 | // Format the headers 48 | headerString := FormatHeaderString(s.Headers) 49 | if _, ok := s.Headers["Content-Length"]; ok { 50 | return fmt.Sprintf("sendmsg %s\r\n%s\r\n\r\n%s", s.UUID, headerString, s.Body) 51 | } 52 | return fmt.Sprintf("sendmsg %s\r\n%s", s.UUID, headerString) 53 | } 54 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "bufio" 15 | "context" 16 | "errors" 17 | "github.com/google/uuid" 18 | "github.com/percipia/eslgo/command" 19 | "net" 20 | "net/textproto" 21 | "sync" 22 | "time" 23 | ) 24 | 25 | type Conn struct { 26 | conn net.Conn 27 | reader *bufio.Reader 28 | header *textproto.Reader 29 | writeLock sync.Mutex 30 | runningContext context.Context 31 | stopFunc func() 32 | responseChannels map[string]chan *RawResponse 33 | responseChanMutex sync.RWMutex 34 | eventListenerLock sync.RWMutex 35 | eventListeners map[string]map[string]EventListener 36 | outbound bool 37 | logger Logger 38 | exitTimeout time.Duration 39 | closeOnce sync.Once 40 | } 41 | 42 | // Options - Generic options for an ESL connection, either inbound or outbound 43 | type Options struct { 44 | Context context.Context // This specifies the base running context for the connection. If this context expires all connections will be terminated. 45 | Logger Logger // This specifies the logger to be used for any library internal messages. Can be set to nil to suppress everything. 46 | ExitTimeout time.Duration // How long should we wait for FreeSWITCH to respond to our "exit" command. 5 seconds is a sane default. 47 | } 48 | 49 | // DefaultOptions - The default options used for creating the connection 50 | var DefaultOptions = Options{ 51 | Context: context.Background(), 52 | Logger: NormalLogger{}, 53 | ExitTimeout: 5 * time.Second, 54 | } 55 | 56 | const EndOfMessage = "\r\n\r\n" 57 | 58 | func newConnection(c net.Conn, outbound bool, opts Options) *Conn { 59 | reader := bufio.NewReader(c) 60 | header := textproto.NewReader(reader) 61 | 62 | // If logger is nil, do not actually output anything 63 | if opts.Logger == nil { 64 | opts.Logger = NilLogger{} 65 | } 66 | 67 | runningContext, stop := context.WithCancel(opts.Context) 68 | 69 | instance := &Conn{ 70 | conn: c, 71 | reader: reader, 72 | header: header, 73 | responseChannels: map[string]chan *RawResponse{ 74 | TypeReply: make(chan *RawResponse), 75 | TypeAPIResponse: make(chan *RawResponse), 76 | TypeEventPlain: make(chan *RawResponse), 77 | TypeEventXML: make(chan *RawResponse), 78 | TypeEventJSON: make(chan *RawResponse), 79 | TypeAuthRequest: make(chan *RawResponse, 1), // Buffered to ensure we do not lose the initial auth request before we are setup to respond 80 | TypeDisconnect: make(chan *RawResponse), 81 | }, 82 | runningContext: runningContext, 83 | stopFunc: stop, 84 | eventListeners: make(map[string]map[string]EventListener), 85 | outbound: outbound, 86 | logger: opts.Logger, 87 | exitTimeout: opts.ExitTimeout, 88 | } 89 | go instance.receiveLoop() 90 | go instance.eventLoop() 91 | return instance 92 | } 93 | 94 | // RegisterEventListener - Registers a new event listener for the specified channel UUID(or EventListenAll). Returns the registered listener ID used to remove it. 95 | func (c *Conn) RegisterEventListener(channelUUID string, listener EventListener) string { 96 | c.eventListenerLock.Lock() 97 | defer c.eventListenerLock.Unlock() 98 | 99 | id := uuid.New().String() 100 | if _, ok := c.eventListeners[channelUUID]; ok { 101 | c.eventListeners[channelUUID][id] = listener 102 | } else { 103 | c.eventListeners[channelUUID] = map[string]EventListener{id: listener} 104 | } 105 | return id 106 | } 107 | 108 | // RemoveEventListener - Removes the listener for the specified channel UUID with the listener ID returned from RegisterEventListener 109 | func (c *Conn) RemoveEventListener(channelUUID string, id string) { 110 | c.eventListenerLock.Lock() 111 | defer c.eventListenerLock.Unlock() 112 | 113 | if listeners, ok := c.eventListeners[channelUUID]; ok { 114 | delete(listeners, id) 115 | } 116 | } 117 | 118 | // SendCommand - Sends the specified ESL command to FreeSWITCH with the provided context. Returns the response data and any errors encountered. 119 | func (c *Conn) SendCommand(ctx context.Context, command command.Command) (*RawResponse, error) { 120 | c.writeLock.Lock() 121 | defer c.writeLock.Unlock() 122 | 123 | if deadline, ok := ctx.Deadline(); ok { 124 | _ = c.conn.SetWriteDeadline(deadline) 125 | } 126 | _, err := c.conn.Write([]byte(command.BuildMessage() + EndOfMessage)) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | // Get response 132 | c.responseChanMutex.RLock() 133 | defer c.responseChanMutex.RUnlock() 134 | select { 135 | case response := <-c.responseChannels[TypeReply]: 136 | if response == nil { 137 | // We only get nil here if the channel is closed 138 | return nil, errors.New("connection closed") 139 | } 140 | return response, nil 141 | case response := <-c.responseChannels[TypeAPIResponse]: 142 | if response == nil { 143 | // We only get nil here if the channel is closed 144 | return nil, errors.New("connection closed") 145 | } 146 | return response, nil 147 | case <-ctx.Done(): 148 | return nil, ctx.Err() 149 | } 150 | } 151 | 152 | // ExitAndClose - Attempt to gracefully send FreeSWITCH "exit" over the ESL connection before closing our connection and stopping. Protected by a sync.Once 153 | func (c *Conn) ExitAndClose() { 154 | c.closeOnce.Do(func() { 155 | // Attempt a graceful closing of the connection with FreeSWITCH 156 | ctx, cancel := context.WithTimeout(c.runningContext, c.exitTimeout) 157 | _, _ = c.SendCommand(ctx, command.Exit{}) 158 | cancel() 159 | c.close() 160 | }) 161 | } 162 | 163 | // Close - Close our connection to FreeSWITCH without sending "exit". Protected by a sync.Once 164 | func (c *Conn) Close() { 165 | c.closeOnce.Do(c.close) 166 | } 167 | 168 | func (c *Conn) close() { 169 | // Allow users to do anything they need to do before we tear everything down 170 | c.stopFunc() 171 | c.responseChanMutex.Lock() 172 | defer c.responseChanMutex.Unlock() 173 | for key, responseChan := range c.responseChannels { 174 | close(responseChan) 175 | delete(c.responseChannels, key) 176 | } 177 | 178 | // Close the connection only after we have the response channel lock and we have deleted all response channels to ensure we don't receive on a closed channel 179 | _ = c.conn.Close() 180 | } 181 | 182 | func (c *Conn) callEventListener(event *Event) { 183 | c.eventListenerLock.RLock() 184 | defer c.eventListenerLock.RUnlock() 185 | 186 | // First check if there are any general event listener 187 | if listeners, ok := c.eventListeners[EventListenAll]; ok { 188 | for _, listener := range listeners { 189 | go listener(event) 190 | } 191 | } 192 | 193 | // Next call any listeners for a particular channel 194 | if event.HasHeader("Unique-Id") { 195 | channelUUID := event.GetHeader("Unique-Id") 196 | if listeners, ok := c.eventListeners[channelUUID]; ok { 197 | for _, listener := range listeners { 198 | go listener(event) 199 | } 200 | } 201 | } 202 | 203 | // Next call any listeners for a particular application 204 | if event.HasHeader("Application-UUID") { 205 | appUUID := event.GetHeader("Application-UUID") 206 | if listeners, ok := c.eventListeners[appUUID]; ok { 207 | for _, listener := range listeners { 208 | go listener(event) 209 | } 210 | } 211 | } 212 | 213 | // Next call any listeners for a particular job 214 | if event.HasHeader("Job-UUID") { 215 | jobUUID := event.GetHeader("Job-UUID") 216 | if listeners, ok := c.eventListeners[jobUUID]; ok { 217 | for _, listener := range listeners { 218 | go listener(event) 219 | } 220 | } 221 | } 222 | } 223 | 224 | func (c *Conn) eventLoop() { 225 | for { 226 | var event *Event 227 | var err error 228 | c.responseChanMutex.RLock() 229 | select { 230 | case raw := <-c.responseChannels[TypeEventPlain]: 231 | if raw == nil { 232 | // We only get nil here if the channel is closed 233 | c.responseChanMutex.RUnlock() 234 | return 235 | } 236 | event, err = readPlainEvent(raw.Body) 237 | case raw := <-c.responseChannels[TypeEventXML]: 238 | if raw == nil { 239 | // We only get nil here if the channel is closed 240 | c.responseChanMutex.RUnlock() 241 | return 242 | } 243 | event, err = readXMLEvent(raw.Body) 244 | case raw := <-c.responseChannels[TypeEventJSON]: 245 | if raw == nil { 246 | // We only get nil here if the channel is closed 247 | c.responseChanMutex.RUnlock() 248 | return 249 | } 250 | event, err = readJSONEvent(raw.Body) 251 | case <-c.runningContext.Done(): 252 | c.responseChanMutex.RUnlock() 253 | return 254 | } 255 | c.responseChanMutex.RUnlock() 256 | 257 | if err != nil { 258 | c.logger.Warn("Error parsing event\n%s\n", err.Error()) 259 | continue 260 | } 261 | 262 | c.callEventListener(event) 263 | } 264 | } 265 | 266 | func (c *Conn) receiveLoop() { 267 | for c.runningContext.Err() == nil { 268 | err := c.doMessage() 269 | if err != nil { 270 | c.logger.Warn("Error receiving message: %s\n", err.Error()) 271 | break 272 | } 273 | } 274 | } 275 | 276 | func (c *Conn) doMessage() error { 277 | response, err := c.readResponse() 278 | if err != nil { 279 | return err 280 | } 281 | 282 | c.responseChanMutex.RLock() 283 | defer c.responseChanMutex.RUnlock() 284 | responseChan, ok := c.responseChannels[response.GetHeader("Content-Type")] 285 | if !ok && len(c.responseChannels) <= 0 { 286 | // We must have shutdown! 287 | return errors.New("no response channels") 288 | } 289 | 290 | // We have a handler 291 | if ok { 292 | // Only allow 5 seconds to allow the handler to receive hte message on the channel 293 | ctx, cancel := context.WithTimeout(c.runningContext, 5*time.Second) 294 | defer cancel() 295 | 296 | select { 297 | case responseChan <- response: 298 | case <-c.runningContext.Done(): 299 | // Parent connection context has stopped we most likely shutdown in the middle of waiting for a handler to handle the message 300 | return c.runningContext.Err() 301 | case <-ctx.Done(): 302 | // Do not return an error since this is not fatal but log since it could be a indication of problems 303 | c.logger.Warn("No one to handle response\nIs the connection overloaded or stopping?\n%v\n\n", response) 304 | } 305 | } else { 306 | return errors.New("no response channel for Content-Type: " + response.GetHeader("Content-Type")) 307 | } 308 | return nil 309 | } 310 | -------------------------------------------------------------------------------- /connection_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "bufio" 15 | "context" 16 | "github.com/percipia/eslgo/command" 17 | "github.com/stretchr/testify/assert" 18 | "net" 19 | "sync" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | func TestConn_SendCommand(t *testing.T) { 25 | server, client := net.Pipe() 26 | connection := newConnection(client, false, DefaultOptions) 27 | defer connection.Close() 28 | defer server.Close() 29 | defer client.Close() 30 | 31 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 32 | defer cancel() 33 | 34 | serverReader := bufio.NewReader(server) 35 | defer serverReader.Discard(serverReader.Buffered()) 36 | 37 | var wait sync.WaitGroup 38 | wait.Add(1) 39 | go func() { 40 | response, err := connection.SendCommand(ctx, command.Auth{ 41 | Password: "test1234", 42 | }) 43 | assert.Nil(t, err) 44 | assert.NotNil(t, response) 45 | assert.True(t, response.IsOk()) 46 | assert.Equal(t, "+OK Job-UUID: c7709e9c-1517-11dc-842a-d3a3942d3d63", response.GetHeader("Reply-Text")) 47 | wait.Done() 48 | }() 49 | 50 | // This is sorta lazy, we should be reading until the proper deliminator of \r\n\r\n 51 | incomingCommand, err := serverReader.ReadString('\r') 52 | assert.Nil(t, err) 53 | assert.Equal(t, "auth test1234\r", incomingCommand) 54 | 55 | _, err = server.Write([]byte("Content-Type: command/reply\r\nReply-Text: +OK Job-UUID: c7709e9c-1517-11dc-842a-d3a3942d3d63\r\n\r\n")) 56 | assert.Nil(t, err) 57 | wait.Wait() 58 | } 59 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "bufio" 15 | "bytes" 16 | "fmt" 17 | "io" 18 | "net/textproto" 19 | "net/url" 20 | "strconv" 21 | "strings" 22 | ) 23 | 24 | type EventListener func(event *Event) 25 | 26 | type Event struct { 27 | Headers textproto.MIMEHeader 28 | Body []byte 29 | } 30 | 31 | const ( 32 | EventListenAll = "ALL" 33 | ) 34 | 35 | func readPlainEvent(body []byte) (*Event, error) { 36 | reader := bufio.NewReader(bytes.NewBuffer(body)) 37 | header := textproto.NewReader(reader) 38 | 39 | headers, err := header.ReadMIMEHeader() 40 | if err != nil { 41 | return nil, err 42 | } 43 | event := &Event{ 44 | Headers: headers, 45 | } 46 | 47 | if contentLength := headers.Get("Content-Length"); len(contentLength) > 0 { 48 | length, err := strconv.Atoi(contentLength) 49 | if err != nil { 50 | return event, err 51 | } 52 | event.Body = make([]byte, length) 53 | _, err = io.ReadFull(reader, event.Body) 54 | if err != nil { 55 | return event, err 56 | } 57 | } 58 | 59 | return event, nil 60 | } 61 | 62 | // TODO: Needs processing 63 | func readXMLEvent(body []byte) (*Event, error) { 64 | return &Event{ 65 | Headers: make(textproto.MIMEHeader), 66 | }, nil 67 | } 68 | 69 | // TODO: Needs processing 70 | func readJSONEvent(body []byte) (*Event, error) { 71 | return &Event{ 72 | Headers: make(textproto.MIMEHeader), 73 | }, nil 74 | } 75 | 76 | // GetName Helper function that returns the event name header 77 | func (e Event) GetName() string { 78 | return e.GetHeader("Event-Name") 79 | } 80 | 81 | // HasHeader Helper to check if the Event has a header 82 | func (e Event) HasHeader(header string) bool { 83 | _, ok := e.Headers[textproto.CanonicalMIMEHeaderKey(header)] 84 | return ok 85 | } 86 | 87 | // GetHeader Helper function that calls e.Header.Get 88 | func (e Event) GetHeader(header string) string { 89 | value, _ := url.PathUnescape(e.Headers.Get(header)) 90 | return value 91 | } 92 | 93 | // String Implement the Stringer interface for pretty printing (%v) 94 | func (e Event) String() string { 95 | var builder strings.Builder 96 | builder.WriteString(fmt.Sprintf("%s\n", e.GetName())) 97 | for key, values := range e.Headers { 98 | builder.WriteString(fmt.Sprintf("%s: %#v\n", key, values)) 99 | } 100 | builder.Write(e.Body) 101 | return builder.String() 102 | } 103 | 104 | // GoString Implement the GoStringer interface for pretty printing (%#v) 105 | func (e Event) GoString() string { 106 | return e.String() 107 | } 108 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "net" 16 | "sync" 17 | "testing" 18 | ) 19 | 20 | const TestEventToSend = "Content-Length: 483\r\nContent-Type: text/event-plain\r\n\r\nMessage-Account: sip%3A1006%4010.0.1.250\r\nEvent-Name: MESSAGE_QUERY\r\nCore-UUID: 2130a7d1-c1f7-44cd-8fae-8ed5946f3cec\r\nFreeSWITCH-Hostname: localhost.localdomain\r\nFreeSWITCH-IPv4: 10.0.1.250\r\nFreeSWITCH-IPv6: 127.0.0.1\r\nEvent-Date-Local: 2007-12-16%2022%3A29%3A59\r\nEvent-Date-GMT: Mon,%2017%20Dec%202007%2004%3A29%3A59%20GMT\r\nEvent-Date-timestamp: 1197865799573052\r\nEvent-Calling-File: sofia_reg.c\r\nEvent-Calling-Function: sofia_reg_handle_register\r\nEvent-Calling-Line-Number: 603\r\n\r\n" 21 | 22 | func TestEvent_readPlainEvent(t *testing.T) { 23 | server, client := net.Pipe() 24 | connection := newConnection(client, false, DefaultOptions) 25 | defer connection.Close() 26 | defer server.Close() 27 | defer client.Close() 28 | 29 | var wait sync.WaitGroup 30 | wait.Add(1) 31 | connection.RegisterEventListener(EventListenAll, func(event *Event) { 32 | assert.NotNil(t, event) 33 | assert.Equal(t, "MESSAGE_QUERY", event.GetName()) 34 | assert.Len(t, event.Headers, 12) 35 | wait.Done() 36 | }) 37 | 38 | _, err := server.Write([]byte(TestEventToSend)) 39 | assert.Nil(t, err) 40 | wait.Wait() 41 | } 42 | -------------------------------------------------------------------------------- /example/events/events.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package main 12 | 13 | import ( 14 | "bufio" 15 | "context" 16 | "fmt" 17 | "github.com/percipia/eslgo" 18 | "os" 19 | "time" 20 | ) 21 | 22 | func main() { 23 | // Connect to FreeSWITCH 24 | conn, err := eslgo.Dial("127.0.0.1:8021", "ClueCon", func() { 25 | fmt.Println("Inbound Connection Disconnected") 26 | }) 27 | if err != nil { 28 | fmt.Println("Error connecting", err) 29 | return 30 | } 31 | 32 | // Register an event listener for all events 33 | listenerID := conn.RegisterEventListener(eslgo.EventListenAll, func(event *eslgo.Event) { 34 | fmt.Printf("%#v\n", event) 35 | }) 36 | 37 | // Ensure all events are enabled 38 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 39 | _ = conn.EnableEvents(ctx) 40 | cancel() 41 | 42 | // Wait until enter is pressed to exit 43 | for { 44 | reader := bufio.NewReader(os.Stdin) 45 | text, _ := reader.ReadString('\n') 46 | if text != "" { 47 | break 48 | } 49 | } 50 | 51 | // Remove the listener and close the connection gracefully 52 | conn.RemoveEventListener(eslgo.EventListenAll, listenerID) 53 | conn.ExitAndClose() 54 | } 55 | -------------------------------------------------------------------------------- /example/inbound/inbound.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package main 12 | 13 | import ( 14 | "context" 15 | "fmt" 16 | "github.com/percipia/eslgo" 17 | "time" 18 | ) 19 | 20 | func main() { 21 | // Connect to FreeSWITCH 22 | conn, err := eslgo.Dial("127.0.0.1:8021", "ClueCon", func() { 23 | fmt.Println("Inbound Connection Disconnected") 24 | }) 25 | if err != nil { 26 | fmt.Println("Error connecting", err) 27 | return 28 | } 29 | 30 | // Create a basic context 31 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 32 | defer cancel() 33 | 34 | // Place the call in the background(bgapi) to user 100 and playback an audio file as the bLeg and no exported variables 35 | response, err := conn.OriginateCall(ctx, true, eslgo.Leg{CallURL: "user/100"}, eslgo.Leg{CallURL: "&playback(misc/ivr-to_hear_screaming_monkeys.wav)"}, map[string]string{}) 36 | fmt.Println("Call Originated: ", response, err) 37 | 38 | // Close the connection after sleeping for a bit 39 | time.Sleep(60 * time.Second) 40 | conn.ExitAndClose() 41 | } 42 | -------------------------------------------------------------------------------- /example/outbound/outbound.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package main 12 | 13 | import ( 14 | "context" 15 | "fmt" 16 | "github.com/percipia/eslgo" 17 | "log" 18 | ) 19 | 20 | func main() { 21 | // Start listening, this is a blocking function 22 | log.Fatalln(eslgo.ListenAndServe(":8084", handleConnection)) 23 | } 24 | 25 | func handleConnection(ctx context.Context, conn *eslgo.Conn, response *eslgo.RawResponse) { 26 | fmt.Printf("Got connection! %#v\n", response) 27 | 28 | // Place the call in the foreground(api) to user 100 and playback an audio file as the bLeg and no exported variables 29 | response, err := conn.OriginateCall(ctx, false, eslgo.Leg{CallURL: "user/100"}, eslgo.Leg{CallURL: "&playback(misc/ivr-to_hear_screaming_monkeys.wav)"}, map[string]string{}) 30 | fmt.Println("Call Originated: ", response, err) 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/percipia/eslgo 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | github.com/stretchr/testify v1.7.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 4 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 9 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "context" 15 | "errors" 16 | "fmt" 17 | "github.com/percipia/eslgo/command" 18 | "github.com/percipia/eslgo/command/call" 19 | "io" 20 | "log" 21 | ) 22 | 23 | func (c *Conn) EnableEvents(ctx context.Context) error { 24 | var err error 25 | if c.outbound { 26 | _, err = c.SendCommand(ctx, command.MyEvents{ 27 | Format: "plain", 28 | }) 29 | } else { 30 | _, err = c.SendCommand(ctx, command.Event{ 31 | Format: "plain", 32 | Listen: []string{"all"}, 33 | }) 34 | } 35 | return err 36 | } 37 | 38 | // DebugEvents - A helper that will output all events to a logger 39 | func (c *Conn) DebugEvents(w io.Writer) string { 40 | logger := log.New(w, "EventLog: ", log.LstdFlags|log.Lmsgprefix) 41 | return c.RegisterEventListener(EventListenAll, func(event *Event) { 42 | logger.Println(event) 43 | }) 44 | } 45 | 46 | func (c *Conn) DebugOff(id string) { 47 | c.RemoveEventListener(EventListenAll, id) 48 | } 49 | 50 | // Phrase - Executes the mod_dptools phrase app 51 | func (c *Conn) Phrase(ctx context.Context, uuid, macro string, times int, wait bool) (*RawResponse, error) { 52 | return c.audioCommand(ctx, "phrase", uuid, macro, times, wait) 53 | } 54 | 55 | // PhraseWithArg - Executes the mod_dptools phrase app with arguments 56 | func (c *Conn) PhraseWithArg(ctx context.Context, uuid, macro string, argument interface{}, times int, wait bool) (*RawResponse, error) { 57 | return c.audioCommand(ctx, "phrase", uuid, fmt.Sprintf("%s,%v", macro, argument), times, wait) 58 | } 59 | 60 | // Playback - Executes the mod_dptools playback app 61 | func (c *Conn) Playback(ctx context.Context, uuid, audioArgs string, times int, wait bool) (*RawResponse, error) { 62 | return c.audioCommand(ctx, "playback", uuid, audioArgs, times, wait) 63 | } 64 | 65 | // Say - Executes the mod_dptools say app 66 | func (c *Conn) Say(ctx context.Context, uuid, audioArgs string, times int, wait bool) (*RawResponse, error) { 67 | return c.audioCommand(ctx, "say", uuid, audioArgs, times, wait) 68 | } 69 | 70 | // Speak - Executes the mod_dptools speak app 71 | func (c *Conn) Speak(ctx context.Context, uuid, audioArgs string, times int, wait bool) (*RawResponse, error) { 72 | return c.audioCommand(ctx, "speak", uuid, audioArgs, times, wait) 73 | } 74 | 75 | // WaitForDTMF, waits for a DTMF event. Requires events to be enabled! 76 | func (c *Conn) WaitForDTMF(ctx context.Context, uuid string) (byte, error) { 77 | done := make(chan byte, 1) 78 | listenerID := c.RegisterEventListener(uuid, func(event *Event) { 79 | if event.GetName() == "DTMF" { 80 | dtmf := event.GetHeader("DTMF-Digit") 81 | if len(dtmf) > 0 { 82 | done <- dtmf[0] 83 | } 84 | done <- 0 85 | } 86 | }) 87 | defer func() { 88 | c.RemoveEventListener(uuid, listenerID) 89 | close(done) 90 | }() 91 | 92 | select { 93 | case digit := <-done: 94 | if digit != 0 { 95 | return digit, nil 96 | } 97 | return digit, errors.New("invalid DTMF digit received") 98 | case <-ctx.Done(): 99 | return 0, ctx.Err() 100 | } 101 | } 102 | 103 | // Helper for mod_dptools apps since they are very similar in invocation 104 | func (c *Conn) audioCommand(ctx context.Context, command, uuid, audioArgs string, times int, wait bool) (*RawResponse, error) { 105 | response, err := c.SendCommand(ctx, &call.Execute{ 106 | UUID: uuid, 107 | AppName: command, 108 | AppArgs: audioArgs, 109 | Loops: times, 110 | Sync: wait, 111 | }) 112 | if err != nil { 113 | return response, err 114 | } 115 | if !response.IsOk() { 116 | return response, errors.New(command + " response is not okay") 117 | } 118 | return response, nil 119 | } 120 | -------------------------------------------------------------------------------- /helper_call.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "context" 15 | "errors" 16 | "fmt" 17 | "github.com/percipia/eslgo/command" 18 | "github.com/percipia/eslgo/command/call" 19 | "strings" 20 | ) 21 | 22 | // Leg This struct is used to specify the individual legs of a call for the originate helpers 23 | type Leg struct { 24 | CallURL string 25 | LegVariables map[string]string 26 | } 27 | 28 | // OriginateCall - Calls the originate function in FreeSWITCH. If you want variables for each leg independently set them in the aLeg and bLeg 29 | // Arguments: ctx context.Context for supporting context cancellation, background bool should we wait for the origination to complete 30 | // aLeg, bLeg Leg The aLeg and bLeg of the call respectively 31 | // vars map[string]string, channel variables to be passed to originate for both legs, contained in {} 32 | func (c *Conn) OriginateCall(ctx context.Context, background bool, aLeg, bLeg Leg, vars map[string]string) (*RawResponse, error) { 33 | if vars == nil { 34 | vars = make(map[string]string) 35 | } 36 | 37 | if _, ok := vars["origination_uuid"]; ok { 38 | // We cannot set origination uuid globally 39 | delete(vars, "origination_uuid") 40 | } 41 | 42 | response, err := c.SendCommand(ctx, command.API{ 43 | Command: "originate", 44 | Arguments: fmt.Sprintf("%s%s %s", BuildVars("{%s}", vars), aLeg.String(), bLeg.String()), 45 | Background: background, 46 | }) 47 | 48 | return response, err 49 | } 50 | 51 | // EnterpriseOriginateCall - Calls the originate function in FreeSWITCH using the enterprise method for calling multiple legs ":_:" 52 | // If you want variables for each leg independently set them in the aLeg and bLeg strings 53 | // Arguments: ctx context.Context for supporting context cancellation, background bool should we wait for the origination to complete 54 | // vars map[string]string, channel variables to be passed to originate for both legs, contained in <> 55 | // bLeg string The bLeg of the call 56 | // aLegs ...string variadic argument for each aLeg to call 57 | func (c *Conn) EnterpriseOriginateCall(ctx context.Context, background bool, vars map[string]string, bLeg Leg, aLegs ...Leg) (*RawResponse, error) { 58 | if len(aLegs) == 0 { 59 | return nil, errors.New("no aLeg specified") 60 | } 61 | 62 | if vars == nil { 63 | vars = make(map[string]string) 64 | } 65 | 66 | if _, ok := vars["origination_uuid"]; ok { 67 | // We cannot set origination uuid globally 68 | delete(vars, "origination_uuid") 69 | } 70 | 71 | var aLeg strings.Builder 72 | for i, leg := range aLegs { 73 | if i > 0 { 74 | aLeg.WriteString(":_:") 75 | } 76 | aLeg.WriteString(leg.String()) 77 | } 78 | 79 | response, err := c.SendCommand(ctx, command.API{ 80 | Command: "originate", 81 | Arguments: fmt.Sprintf("%s%s %s", BuildVars("<%s>", vars), aLeg.String(), bLeg.String()), 82 | Background: background, 83 | }) 84 | 85 | return response, err 86 | } 87 | 88 | // HangupCall - A helper to hangup a call asynchronously 89 | func (c *Conn) HangupCall(ctx context.Context, uuid, cause string) error { 90 | _, err := c.SendCommand(ctx, call.Hangup{ 91 | UUID: uuid, 92 | Cause: cause, 93 | Sync: false, 94 | }) 95 | return err 96 | } 97 | 98 | // HangupCall - A helper to answer a call synchronously 99 | func (c *Conn) AnswerCall(ctx context.Context, uuid string) error { 100 | _, err := c.SendCommand(ctx, &call.Execute{ 101 | UUID: uuid, 102 | AppName: "answer", 103 | Sync: true, 104 | }) 105 | return err 106 | } 107 | 108 | // String - Build the Leg string for passing to Bridge/Originate functions 109 | func (l Leg) String() string { 110 | return fmt.Sprintf("%s%s", BuildVars("[%s]", l.LegVariables), l.CallURL) 111 | } 112 | -------------------------------------------------------------------------------- /inbound.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "context" 15 | "fmt" 16 | "github.com/percipia/eslgo/command" 17 | "net" 18 | "time" 19 | ) 20 | 21 | // InboundOptions - Used to dial a new inbound ESL connection to FreeSWITCH 22 | type InboundOptions struct { 23 | Options // Generic common options to both Inbound and Outbound Conn 24 | Network string // The network type to use, should always be tcp, tcp4, tcp6. 25 | Password string // The password used to authenticate with FreeSWITCH. Usually ClueCon 26 | OnDisconnect func() // An optional function to be called with the inbound connection gets disconnected 27 | AuthTimeout time.Duration // How long to wait for authentication to complete 28 | } 29 | 30 | // DefaultOutboundOptions - The default options used for creating the inbound connection 31 | var DefaultInboundOptions = InboundOptions{ 32 | Options: DefaultOptions, 33 | Network: "tcp", 34 | Password: "ClueCon", 35 | AuthTimeout: 5 * time.Second, 36 | } 37 | 38 | // Dial - Connects to FreeSWITCH ESL at the provided address and authenticates with the provided password. onDisconnect is called when the connection is closed either by us, FreeSWITCH, or network error 39 | func Dial(address, password string, onDisconnect func()) (*Conn, error) { 40 | opts := DefaultInboundOptions 41 | opts.Password = password 42 | opts.OnDisconnect = onDisconnect 43 | return opts.Dial(address) 44 | } 45 | 46 | // Dial - Connects to FreeSWITCH ESL on the address with the provided options. Returns the connection and any errors encountered 47 | func (opts InboundOptions) Dial(address string) (*Conn, error) { 48 | c, err := net.Dial(opts.Network, address) 49 | if err != nil { 50 | return nil, err 51 | } 52 | connection := newConnection(c, false, opts.Options) 53 | 54 | // First auth 55 | <-connection.responseChannels[TypeAuthRequest] 56 | authCtx, cancel := context.WithTimeout(connection.runningContext, opts.AuthTimeout) 57 | err = connection.doAuth(authCtx, command.Auth{Password: opts.Password}) 58 | cancel() 59 | if err != nil { 60 | // Try to gracefully disconnect, we have the wrong password. 61 | connection.ExitAndClose() 62 | if opts.OnDisconnect != nil { 63 | go opts.OnDisconnect() 64 | } 65 | return nil, err 66 | } else { 67 | connection.logger.Info("Successfully authenticated %s\n", connection.conn.RemoteAddr()) 68 | } 69 | 70 | // Inbound only handlers 71 | go connection.authLoop(command.Auth{Password: opts.Password}, opts.AuthTimeout) 72 | go connection.disconnectLoop(opts.OnDisconnect) 73 | 74 | return connection, nil 75 | } 76 | 77 | func (c *Conn) disconnectLoop(onDisconnect func()) { 78 | select { 79 | case <-c.responseChannels[TypeDisconnect]: 80 | c.Close() 81 | if onDisconnect != nil { 82 | onDisconnect() 83 | } 84 | return 85 | case <-c.runningContext.Done(): 86 | return 87 | } 88 | } 89 | 90 | func (c *Conn) authLoop(auth command.Auth, authTimeout time.Duration) { 91 | for { 92 | select { 93 | case <-c.responseChannels[TypeAuthRequest]: 94 | authCtx, cancel := context.WithTimeout(c.runningContext, authTimeout) 95 | err := c.doAuth(authCtx, auth) 96 | cancel() 97 | if err != nil { 98 | c.logger.Warn("Failed to auth %e\n", err) 99 | // Close the connection, we have the wrong password 100 | c.ExitAndClose() 101 | return 102 | } else { 103 | c.logger.Info("Successfully authenticated %s\n", c.conn.RemoteAddr()) 104 | } 105 | case <-c.runningContext.Done(): 106 | return 107 | } 108 | } 109 | } 110 | 111 | func (c *Conn) doAuth(ctx context.Context, auth command.Auth) error { 112 | response, err := c.SendCommand(ctx, auth) 113 | if err != nil { 114 | return err 115 | } 116 | if !response.IsOk() { 117 | return fmt.Errorf("failed to auth %#v", response) 118 | } 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package eslgo 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | type Logger interface { 8 | Debug(format string, args ...interface{}) 9 | Info(format string, args ...interface{}) 10 | Warn(format string, args ...interface{}) 11 | Error(format string, args ...interface{}) 12 | } 13 | 14 | type NilLogger struct{} 15 | type NormalLogger struct{} 16 | 17 | func (l NormalLogger) Debug(format string, args ...interface{}) { 18 | log.Print("DEBUG: ") 19 | log.Printf(format, args...) 20 | } 21 | func (l NormalLogger) Info(format string, args ...interface{}) { 22 | log.Print("INFO: ") 23 | log.Printf(format, args...) 24 | } 25 | func (l NormalLogger) Warn(format string, args ...interface{}) { 26 | log.Print("WARN: ") 27 | log.Printf(format, args...) 28 | } 29 | func (l NormalLogger) Error(format string, args ...interface{}) { 30 | log.Print("ERROR: ") 31 | log.Printf(format, args...) 32 | } 33 | 34 | func (l NilLogger) Debug(string, ...interface{}) {} 35 | func (l NilLogger) Info(string, ...interface{}) {} 36 | func (l NilLogger) Warn(string, ...interface{}) {} 37 | func (l NilLogger) Error(string, ...interface{}) {} 38 | -------------------------------------------------------------------------------- /outbound.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "context" 15 | "errors" 16 | "github.com/percipia/eslgo/command" 17 | "net" 18 | "time" 19 | ) 20 | 21 | type OutboundHandler func(ctx context.Context, conn *Conn, connectResponse *RawResponse) 22 | 23 | // OutboundOptions - Used to open a new listener for outbound ESL connections from FreeSWITCH 24 | type OutboundOptions struct { 25 | Options // Generic common options to both Inbound and Outbound Conn 26 | Network string // The network type to listen on, should be tcp, tcp4, or tcp6 27 | ConnectTimeout time.Duration // How long should we wait for FreeSWITCH to respond to our "connect" command. 5 seconds is a sane default. 28 | ConnectionDelay time.Duration // How long should we wait after connection to start sending commands. 25ms is the recommended default otherwise we can close the connection before FreeSWITCH finishes starting it on their end. https://github.com/signalwire/freeswitch/pull/636 29 | } 30 | 31 | // DefaultOutboundOptions - The default options used for creating the outbound connection 32 | var DefaultOutboundOptions = OutboundOptions{ 33 | Options: DefaultOptions, 34 | Network: "tcp", 35 | ConnectTimeout: 5 * time.Second, 36 | ConnectionDelay: 25 * time.Millisecond, 37 | } 38 | 39 | /* 40 | * TODO: Review if we should have a rate limiting facility to prevent DoS attacks 41 | * For our use it should be fine since we only want to listen on localhost 42 | */ 43 | // ListenAndServe - Open a new listener for outbound ESL connections from FreeSWITCH on the specified address with the provided connection handler 44 | func ListenAndServe(address string, handler OutboundHandler) error { 45 | return DefaultOutboundOptions.ListenAndServe(address, handler) 46 | } 47 | 48 | // ListenAndServe - Open a new listener for outbound ESL connections from FreeSWITCH with provided options and handle them with the specified handler 49 | func (opts OutboundOptions) ListenAndServe(address string, handler OutboundHandler) error { 50 | listener, err := net.Listen(opts.Network, address) 51 | if err != nil { 52 | return err 53 | } 54 | if opts.Logger != nil { 55 | opts.Logger.Info("Listening for new ESL connections on %s\n", listener.Addr().String()) 56 | } 57 | for { 58 | c, err := listener.Accept() 59 | if err != nil { 60 | break 61 | } 62 | conn := newConnection(c, true, opts.Options) 63 | 64 | conn.logger.Info("New outbound connection from %s\n", c.RemoteAddr().String()) 65 | go conn.dummyLoop() 66 | // Does not call the handler directly to ensure closing cleanly 67 | go conn.outboundHandle(handler, opts.ConnectionDelay, opts.ConnectTimeout) 68 | } 69 | 70 | if opts.Logger != nil { 71 | opts.Logger.Info("Outbound server shutting down") 72 | } 73 | return errors.New("connection closed") 74 | } 75 | 76 | func (c *Conn) outboundHandle(handler OutboundHandler, connectionDelay, connectTimeout time.Duration) { 77 | ctx, cancel := context.WithTimeout(c.runningContext, connectTimeout) 78 | response, err := c.SendCommand(ctx, command.Connect{}) 79 | cancel() 80 | if err != nil { 81 | c.logger.Warn("Error connecting to %s error %s", c.conn.RemoteAddr().String(), err.Error()) 82 | // Try closing cleanly first 83 | c.Close() // Not ExitAndClose since this error connection is most likely from communication failure 84 | return 85 | } 86 | handler(c.runningContext, c, response) 87 | // XXX This is ugly, the issue with short lived async sockets on our end is if they complete too fast we can actually 88 | // close the connection before FreeSWITCH is in a state to close the connection on their end. 25ms is an magic value 89 | // found by testing to have no failures on my test system. I started at 1 second and reduced as far as I could go. 90 | // TODO This actually may be fixed: https://github.com/signalwire/freeswitch/pull/636 91 | time.Sleep(connectionDelay) 92 | c.ExitAndClose() 93 | } 94 | 95 | func (c *Conn) dummyLoop() { 96 | select { 97 | case <-c.responseChannels[TypeDisconnect]: 98 | c.logger.Info("Disconnect outbound connection", c.conn.RemoteAddr()) 99 | c.Close() 100 | case <-c.responseChannels[TypeAuthRequest]: 101 | c.logger.Debug("Ignoring auth request on outbound connection", c.conn.RemoteAddr()) 102 | case <-c.runningContext.Done(): 103 | return 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "fmt" 15 | "io" 16 | "net/textproto" 17 | "net/url" 18 | "strconv" 19 | "strings" 20 | ) 21 | 22 | const ( 23 | TypeEventPlain = `text/event-plain` 24 | TypeEventJSON = `text/event-json` 25 | TypeEventXML = `text/event-xml` 26 | TypeReply = `command/reply` 27 | TypeAPIResponse = `api/response` 28 | TypeAuthRequest = `auth/request` 29 | TypeDisconnect = `text/disconnect-notice` 30 | ) 31 | 32 | // RawResponse This struct contains all response data from FreeSWITCH 33 | type RawResponse struct { 34 | Headers textproto.MIMEHeader 35 | Body []byte 36 | } 37 | 38 | func (c *Conn) readResponse() (*RawResponse, error) { 39 | header, err := c.header.ReadMIMEHeader() 40 | if err != nil { 41 | return nil, err 42 | } 43 | response := &RawResponse{ 44 | Headers: header, 45 | } 46 | 47 | if contentLength := header.Get("Content-Length"); len(contentLength) > 0 { 48 | length, err := strconv.Atoi(contentLength) 49 | if err != nil { 50 | return response, err 51 | } 52 | response.Body = make([]byte, length) 53 | _, err = io.ReadFull(c.reader, response.Body) 54 | if err != nil { 55 | return response, err 56 | } 57 | } 58 | 59 | return response, nil 60 | } 61 | 62 | // IsOk Helper to check response status, uses the Reply-Text header primarily. Calls GetReply internally 63 | func (r RawResponse) IsOk() bool { 64 | return strings.HasPrefix(r.GetReply(), "+OK") 65 | } 66 | 67 | // GetReply Helper to get the Reply text from FreeSWITCH, uses the Reply-Text header primarily. 68 | // Also will use the body if the Reply-Text header does not exist, this can be the case for TypeAPIResponse 69 | func (r RawResponse) GetReply() string { 70 | if r.HasHeader("Reply-Text") { 71 | return r.GetHeader("Reply-Text") 72 | } 73 | return string(r.Body) 74 | } 75 | 76 | // ChannelUUID Helper to get the channel UUID. Calls GetHeader internally 77 | func (r RawResponse) ChannelUUID() string { 78 | return r.GetHeader("Unique-ID") 79 | } 80 | 81 | // HasHeader Helper to check if the RawResponse has a header 82 | func (r RawResponse) HasHeader(header string) bool { 83 | _, ok := r.Headers[textproto.CanonicalMIMEHeaderKey(header)] 84 | return ok 85 | } 86 | 87 | // GetVariable Helper function to get "Variable_" headers. Calls GetHeader internally 88 | func (r RawResponse) GetVariable(variable string) string { 89 | return r.GetHeader(fmt.Sprintf("Variable_%s", variable)) 90 | } 91 | 92 | // GetHeader Helper function that calls RawResponse.Headers.Get. Result gets passed through url.PathUnescape 93 | func (r RawResponse) GetHeader(header string) string { 94 | value, _ := url.PathUnescape(r.Headers.Get(header)) 95 | return value 96 | } 97 | 98 | // String Implement the Stringer interface for pretty printing 99 | func (r RawResponse) String() string { 100 | var builder strings.Builder 101 | for key, values := range r.Headers { 102 | builder.WriteString(fmt.Sprintf("%s: %#v\n", key, values)) 103 | } 104 | builder.Write(r.Body) 105 | return builder.String() 106 | } 107 | 108 | // GoString Implement the GoStringer interface for pretty printing (%#v) 109 | func (r RawResponse) GoString() string { 110 | return r.String() 111 | } 112 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "fmt" 15 | "strings" 16 | ) 17 | 18 | // BuildVars - A helper that builds channel variable strings to be included in various commands to FreeSWITCH 19 | func BuildVars(format string, vars map[string]string) string { 20 | // No vars do not format 21 | if vars == nil || len(vars) == 0 { 22 | return "" 23 | } 24 | 25 | var builder strings.Builder 26 | for key, value := range vars { 27 | if builder.Len() > 0 { 28 | builder.WriteString(",") 29 | } 30 | builder.WriteString(key) 31 | builder.WriteString("=") 32 | if strings.ContainsAny(value, " ") { 33 | builder.WriteString("'") 34 | builder.WriteString(value) 35 | builder.WriteString("'") 36 | } else { 37 | builder.WriteString(value) 38 | } 39 | } 40 | return fmt.Sprintf(format, builder.String()) 41 | } 42 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Percipia 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | * 8 | * Contributor(s): 9 | * Andrew Querol 10 | */ 11 | package eslgo 12 | 13 | import ( 14 | "github.com/stretchr/testify/assert" 15 | "strings" 16 | "testing" 17 | ) 18 | 19 | func Test_BuildVars(t *testing.T) { 20 | vars := BuildVars("{%s}", map[string]string{ 21 | "origination_caller_name": "test", 22 | "origination_caller_number": "1234", 23 | "origination_callee_name": "John Doe", 24 | "origination_callee_number": "7100", 25 | }) 26 | 27 | // Contains since order is not guaranteed when iterating over maps 28 | assert.Contains(t, vars, "origination_caller_name=test") 29 | assert.Contains(t, vars, "origination_caller_number=1234") 30 | assert.Contains(t, vars, "origination_callee_name='John Doe'") 31 | assert.Contains(t, vars, "origination_callee_number=7100") 32 | 33 | // Ensure the formatting elements are contained in the string 34 | assert.Equal(t, 3, strings.Count(vars, ",")) 35 | assert.True(t, strings.HasPrefix(vars, "{")) 36 | assert.True(t, strings.HasSuffix(vars, "}")) 37 | } 38 | --------------------------------------------------------------------------------