├── .github ├── FUNDING.yml └── workflows │ └── release.yaml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── README.md ├── options-aa-fld.json ├── options-aa-tpl.sip ├── register-aa-fld.json └── register-aa-tpl.sip ├── go.mod ├── go.sum └── wsctl.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [miconda] 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | releases-matrix: 7 | name: Release Go Binary 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | # build and publish in parallel: linux/386, linux/amd64, linux/arm64, windows/386, windows/amd64, darwin/amd64, darwin/arm64 12 | goos: [linux, windows, darwin] 13 | goarch: ["386", amd64, arm64] 14 | exclude: 15 | - goarch: "386" 16 | goos: darwin 17 | - goarch: arm64 18 | goos: windows 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: wangyoucao577/go-release-action@v1.22 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | goos: ${{ matrix.goos }} 25 | goarch: ${{ matrix.goarch }} 26 | extra_files: LICENSE README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | /wsctl 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | # Temporary files 28 | *~ 29 | *.swp 30 | *.swo 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wsctl 2 | WebSocket Command Line Tool 3 | 4 | License: `GPLv2` 5 | 6 | Copyright: Daniel-Constantin Mierla (Asipto, https://www.asipto.com) 7 | 8 | ## Overview 9 | 10 | **wsctl** is a websocket client and basic server to be used from command line. It is written in Go (Golang). 11 | 12 | While the common use case for websocket connections is between web browser and web server, there are situations where is more convenient to use a command line (e.g., testing, monitoring). 13 | 14 | **wsctl** can send data loaded from a template file to a websocket server and it will print the response received from the server. 15 | 16 | It was developed and tested for sending SIP requests over websocket to Kamailio SIP Server (http://www.kamailio.org), but the data can be any format. 17 | 18 | For SIP over websocket, it can do www-digest authentication if the server challenges with a 401/407 response. 19 | 20 | As a webserver socket, **wsctl** offers a few basic services useful for testing, matching on URL path: 21 | 22 | * `/echo` - copy received message back to the websocket connection and write a log with the message 23 | * `/echo-only` - only copy what is received back to the websocket connection 24 | * `/echo-reply` - prefix with `Replying-To: ` what is received and send back to the websocket connection 25 | * `/log` - write a log with the message received on websocket connection 26 | 27 | ## Install 28 | 29 | First install Go (http://golang.org). Once the Go environment is configured, the websocket package must be fetched locally: 30 | 31 | ``` 32 | go get -v golang.org/x/net/websocket 33 | ``` 34 | 35 | Fetch this repository into your Go environment: 36 | 37 | ``` 38 | go get -v -u github.com/miconda/wsctl 39 | ``` 40 | 41 | ### Run 42 | 43 | Navigate to the project folder and run: 44 | 45 | ``` 46 | go run wsctl.go [options] 47 | ``` 48 | 49 | Or install the application: 50 | 51 | ``` 52 | go install github.com/miconda/wsctl 53 | ``` 54 | 55 | And then execute: 56 | 57 | ``` 58 | $GOPATH/bin/wsctl [options] 59 | ``` 60 | 61 | ## Command Line Options 62 | 63 | If run with option `-h` or `--help`, it will print the help message. 64 | 65 | The parameter `--template` (short form `-t`) is used to provide the path to template file. 66 | If it is not provided, `wsctl` uses an internal template and fields data, which 67 | build an SIP OPTIONS requests. More details about template files are provided in the next section. 68 | 69 | The parameter '--url' can be used to set the URL to websocket server, if not provided, its value is 'wss://127.0.0.1:8443'. 70 | 71 | Next is an example of running wsctl by using external template and fields files, to send data to a particular WS server over secure connection: 72 | 73 | ``` 74 | go run wsctl.go \ 75 | --url='wss://myserver.com:8443/ws' \ 76 | --template=examples/options-aa-tpl.sip \ 77 | --fields=examples/options-aa-fld.json 78 | ``` 79 | 80 | To provide username and password for www-digest authentication of SIP requests: 81 | 82 | ``` 83 | go run wsctl.go \ 84 | --url='wss://myserver.com:8443/ws' \ 85 | --template=examples/options-aa-tpl.sip \ 86 | --fields=examples/options-aa-fld.json \ 87 | --auser='test' --apasswd='secret' 88 | ``` 89 | 90 | For websocket secure connections (wss), by default it skips server's TLS certificate verification. To enforce certificate verification add the command line option `--insecure=false`. 91 | 92 | The HTTP URL for Origin header can be set with option `--origin=...`. Its default value is `http://127.0.0.1`. 93 | 94 | The websocket subprotocol can be set with option `--protocol=...`. Default is `sip`. 95 | 96 | ## Data Templates 97 | 98 | The data to be sent via the websocket connection is built from a template file and a fields file. 99 | 100 | The template file can contain any any of the directives supported by Go package `text/template` - for more see: 101 | 102 | * https://golang.org/pkg/text/template/ 103 | 104 | Example: 105 | 106 | ``` 107 | OPTIONS sip:{{.callee}}@{{.domain}} SIP/2.0 108 | Via: SIP/2.0/WSS df7jal23ls0d.invalid;branch=z9hG4bKasudf-3696-24845-1 109 | From: "{{.caller}}" ;tag={{.fromtag}} 110 | To: "{{.callee}}" 111 | Call-ID: {{.callid}} 112 | CSeq: {{.cseqnum}} OPTIONS 113 | Subject: testing 114 | Content-Length: 0 115 | 116 | ``` 117 | 118 | The internal template can be found at the top of `wsctl.go` file. 119 | 120 | ## Data Fields 121 | 122 | The fields file has to contain a JSON document with the fields to be replaced 123 | in the template file. The path to the JSON file is provided via `-f` or `--fields` 124 | parameters. 125 | 126 | Sample template and fields files can be found inside subfolder `examples/`. 127 | 128 | When the `--fields-eval` cli option is provided, `wsctl` evaluates the values of the 129 | fields in the root structure of the JSON document. That means special tokens (expressions) 130 | are replaced if the value of the field is a string matching one of the next: 131 | 132 | * `"$uuid"` - replace with a UUID value 133 | * `"$randseq"` - replace with a random number from `1` to `1 000 000`. 134 | * `"$datefull"` - replace with output of `time.Now().String()` 135 | * `"$daterfc1123"` - replace with output of `time.Now().Format(time.RFC1123)` 136 | * `"$dateansic"` - replace with output of `time.Now().Format(time.ANSIC)` 137 | * `"$dateunix"` - replace with output of `time.Now().Format(time.UnixDate)` 138 | * `"$timestamp"` - replace with output of `time.Now().Unix()` 139 | * `"$cr"` - replace with `\r` 140 | * `"$lf"` - replace with `\n` 141 | 142 | Example: 143 | 144 | ```json 145 | { 146 | "caller": "alice", 147 | "callee": "bob", 148 | "domain": "localhost", 149 | "fromtag": "$uuid", 150 | "callid": "$uuid", 151 | "cseqnum": "$randseq" 152 | } 153 | ``` 154 | 155 | The internal fields data can be found at the top of `wsctl.go` file. 156 | 157 | The values for fields can be also provided using `--field-val` cli parameter, in 158 | format `name:value`, for example: 159 | 160 | ``` 161 | wsctl --field-val="domain:openrcs.com" ... 162 | ``` 163 | 164 | The value provided via `--field-val` overwrites the value provided in the 165 | JSON fields file. 166 | 167 | ## Internals 168 | 169 | Sending data over websocket connection has a timeout of 10 seconds. Receiving data from websocket connection has a timeout of 20 seconds. These values can be changed via command line parameters. 170 | 171 | ## WebSocket Server Mode 172 | 173 | To be started as a WebSocket server, `wsctl` must be given `-http-srv` or `-https-srv` command 174 | line parameter. Their value has to be `:PORT` to listen on all local IP address, 175 | or `LOCALIP:PORT` to listen only on a specific local IP. 176 | 177 | ## Contributions 178 | 179 | Contributions are welcome! Fork and do pull requests on https://github.com/miconda/wsctl . 180 | 181 | ## To-Do 182 | 183 | Just some ideas for now, not all to be implemented: 184 | 185 | * open many websocket connections at once and send data on all of them (tool for stress testing) 186 | * option to specify some of the command line parameters via fields file (e.g., auth username, password) 187 | * support to work with an array of templates and fields files 188 | 189 | Suggestions for what to add are welcome as well! 190 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Sample Templates For Wsctl ## 2 | 3 | ### SIP OPTIONS Requests ### 4 | 5 | Basic SIP OPTIONS request: 6 | * options-aa-tpl.sip - the template file 7 | * options-aa-fld.json - the fields file 8 | -------------------------------------------------------------------------------- /examples/options-aa-fld.json: -------------------------------------------------------------------------------- 1 | { 2 | "caller": "alice", 3 | "callee": "bob", 4 | "domain": "localhost" 5 | } 6 | -------------------------------------------------------------------------------- /examples/options-aa-tpl.sip: -------------------------------------------------------------------------------- 1 | OPTIONS sip:{{.callee}}@{{.domain}} SIP/2.0 2 | Via: SIP/2.0/WSS df7jal23ls0d.invalid;branch=z9hG4bKasudf-3696-24845-1 3 | From: '{{.caller}}' ;tag=d71a60e3-c5f6-4b17-a8b6-d08c6a690ae4 4 | To: '{{.callee}}' 5 | Call-ID: deefd8e1-993e-4552-8a1b-4f3d9390485e 6 | CSeq: 2 OPTIONS 7 | Subject: testing 8 | Content-Length: 0 9 | 10 | -------------------------------------------------------------------------------- /examples/register-aa-fld.json: -------------------------------------------------------------------------------- 1 | { 2 | "caller": "alice", 3 | "domain": "localhost", 4 | "expires": "600" 5 | } 6 | -------------------------------------------------------------------------------- /examples/register-aa-tpl.sip: -------------------------------------------------------------------------------- 1 | REGISTER sip:{{.domain}} SIP/2.0 2 | Via: SIP/2.0/WSS q72jr0k8gorn.invalid;branch=z9hG4bK428540716 3 | Max-Forwards: 69 4 | To: '{{.caller}}' 5 | From: '{{.caller}}' ;tag=syevwlcxem 6 | Call-ID: 69e069a5-5d2e-4514-9c40-2fbad9e3b8ee 7 | CSeq: 1 REGISTER 8 | Contact: 9 | Expires: {{.expires}} 10 | Allow: INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO 11 | Supported: path 12 | User-Agent: wsctl 13 | Content-Length: 0 14 | 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/miconda/wsctl 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | golang.org/x/net v0.23.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 2 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 4 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 5 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 6 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 7 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 8 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 9 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 11 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 12 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 13 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 14 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 15 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 16 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 17 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 18 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 19 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 20 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 21 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 22 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 29 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 31 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 32 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 33 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 34 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 35 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 36 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 37 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 38 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 39 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 40 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 41 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 42 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 43 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 44 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 45 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 46 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 47 | -------------------------------------------------------------------------------- /wsctl.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WebSocket Command Line Tool 3 | * (C) Copyright 2015 Daniel-Constantin Mierla (asipto.com) 4 | * License: GPLv2 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "crypto/md5" 12 | cryptorand "crypto/rand" 13 | "crypto/tls" 14 | "encoding/base64" 15 | "encoding/json" 16 | "flag" 17 | "fmt" 18 | "io" 19 | "io/ioutil" 20 | "log" 21 | mathrand "math/rand" 22 | "net/http" 23 | "net/url" 24 | "os" 25 | "path/filepath" 26 | "sort" 27 | "strconv" 28 | "strings" 29 | "text/template" 30 | "time" 31 | 32 | "github.com/google/uuid" 33 | "golang.org/x/net/websocket" 34 | ) 35 | 36 | const wsctlVersion = "1.2.1" 37 | 38 | var templateDefaultText string = `OPTIONS sip:{{.callee}}@{{.domain}} SIP/2.0 39 | Via: SIP/2.0/WSS df7jal23ls0d.invalid;branch=z9hG4bKasudf-3696-24845-1 40 | From: "{{.caller}}" ;tag={{.fromtag}} 41 | To: "{{.callee}}" 42 | Call-ID: {{.callid}} 43 | CSeq: {{.cseqnum}} OPTIONS 44 | Subject: testing 45 | Date: {{.date}} 46 | Content-Length: 0 47 | 48 | ` 49 | 50 | var templateDefaultJSONFields string = `{ 51 | "caller": "alice", 52 | "callee": "bob", 53 | "domain": "localhost", 54 | "fromtag": "$uuid", 55 | "callid": "$uuid", 56 | "cseqnum": "$randseq", 57 | "date": "$daterfc1123" 58 | }` 59 | 60 | var templateFields = map[string]map[string]interface{}{ 61 | "FIELDS:EMPTY": {}, 62 | } 63 | 64 | type paramFieldsType map[string]string 65 | 66 | func (m paramFieldsType) String() string { 67 | b := new(bytes.Buffer) 68 | for key, value := range m { 69 | fmt.Fprintf(b, "%s:%s\n", key, value) 70 | } 71 | return b.String() 72 | } 73 | 74 | func (m paramFieldsType) Set(value string) error { 75 | z := strings.SplitN(value, ":", 2) 76 | if len(z) > 1 { 77 | m[z[0]] = z[1] 78 | } 79 | return nil 80 | } 81 | 82 | var paramFields = make(paramFieldsType) 83 | 84 | // 85 | // CLIOptions - structure for command line options 86 | type CLIOptions struct { 87 | wsurl string 88 | wsorigin string 89 | wsproto string 90 | wsinsecure bool 91 | wsreceive bool 92 | wstemplate string 93 | wstemplaterun bool 94 | wsfields string 95 | wsfieldseval bool 96 | wscrlf bool 97 | version bool 98 | wsauser string 99 | wsapasswd string 100 | wstimeoutrecv int 101 | wstimeoutsend int 102 | wsoutputfile string 103 | wsuuid bool 104 | wsflagdefaults bool 105 | wstemplatedefaults bool 106 | wsdomainurl bool 107 | wsdomainorigin bool 108 | wshttpdomain string 109 | wshttpsrv string 110 | wshttpssrv string 111 | wshttpspubkey string 112 | wshttpsprvkey string 113 | wshttpsusele bool 114 | } 115 | 116 | var cliops = CLIOptions{ 117 | wsurl: "wss://127.0.0.1:8443", 118 | wsorigin: "http://127.0.0.1", 119 | wsproto: "sip", 120 | wsinsecure: true, 121 | wsreceive: true, 122 | wstemplate: "", 123 | wstemplaterun: false, 124 | wsfields: "", 125 | wsfieldseval: false, 126 | wscrlf: false, 127 | version: false, 128 | wsauser: "", 129 | wsapasswd: "", 130 | wstimeoutrecv: 20000, 131 | wstimeoutsend: 10000, 132 | wsoutputfile: "", 133 | wsuuid: false, 134 | wsflagdefaults: false, 135 | wstemplatedefaults: false, 136 | wsdomainurl: false, 137 | wsdomainorigin: false, 138 | wshttpdomain: "", 139 | wshttpsrv: "", 140 | wshttpssrv: "", 141 | wshttpspubkey: "", 142 | wshttpsprvkey: "", 143 | wshttpsusele: false, 144 | } 145 | 146 | // 147 | var outputFile *os.File 148 | 149 | func printCLIOptions() { 150 | type CLIOptionDef struct { 151 | Ops []string 152 | Usage string 153 | DefValue string 154 | VType string 155 | } 156 | var items []CLIOptionDef 157 | flag.VisitAll(func(f *flag.Flag) { 158 | var found bool = false 159 | for idx, it := range items { 160 | if it.Usage == f.Usage { 161 | found = true 162 | it.Ops = append(it.Ops, f.Name) 163 | items[idx] = it 164 | } 165 | } 166 | if !found { 167 | items = append(items, CLIOptionDef{ 168 | Ops: []string{f.Name}, 169 | Usage: f.Usage, 170 | DefValue: f.DefValue, 171 | VType: fmt.Sprintf("%T", f.Value), 172 | }) 173 | } 174 | }) 175 | sort.Slice(items, func(i, j int) bool { return strings.ToLower(items[i].Ops[0]) < strings.ToLower(items[j].Ops[0]) }) 176 | for _, val := range items { 177 | vtype := val.VType[6 : len(val.VType)-5] 178 | if vtype[len(vtype)-2:] == "64" { 179 | vtype = vtype[:len(vtype)-2] 180 | } 181 | for _, opt := range val.Ops { 182 | if vtype == "bool" { 183 | fmt.Printf(" -%s\n", opt) 184 | } else { 185 | fmt.Printf(" -%s %s\n", opt, vtype) 186 | } 187 | } 188 | if vtype != "bool" && len(val.DefValue) > 0 { 189 | fmt.Printf(" %s [default: %s]\n", val.Usage, val.DefValue) 190 | } else { 191 | fmt.Printf(" %s\n", val.Usage) 192 | } 193 | } 194 | } 195 | 196 | // 197 | // initialize application components 198 | func init() { 199 | // command line arguments 200 | flag.Usage = func() { 201 | fmt.Fprintf(os.Stderr, "Usage of %s (v%s):\n", filepath.Base(os.Args[0]), wsctlVersion) 202 | fmt.Fprintf(os.Stderr, " (some options have short and long version)\n") 203 | printCLIOptions() 204 | fmt.Fprintf(os.Stderr, "\n") 205 | os.Exit(1) 206 | } 207 | flag.StringVar(&cliops.wsauser, "auser", cliops.wsauser, "username to be used for authentication") 208 | flag.StringVar(&cliops.wsapasswd, "apasswd", cliops.wsapasswd, "password to be used for authentication") 209 | flag.BoolVar(&cliops.wscrlf, "crlf", cliops.wscrlf, "replace '\\n' with '\\r\\n' inside the data to be sent (true|false)") 210 | flag.StringVar(&cliops.wsfields, "fields", cliops.wsfields, "path to the json fields file") 211 | flag.StringVar(&cliops.wsfields, "f", cliops.wsfields, "path to the json fields file") 212 | flag.BoolVar(&cliops.wsfieldseval, "fields-eval", cliops.wsfieldseval, "evaluate expression in fields file") 213 | flag.BoolVar(&cliops.wsinsecure, "insecure", cliops.wsinsecure, "skip tls certificate validation for wss (true|false)") 214 | flag.BoolVar(&cliops.wsinsecure, "i", cliops.wsinsecure, "skip tls certificate validation for wss (true|false)") 215 | flag.StringVar(&cliops.wsorigin, "origin", cliops.wsorigin, "origin http url") 216 | flag.StringVar(&cliops.wsorigin, "o", cliops.wsorigin, "origin http url") 217 | flag.StringVar(&cliops.wsproto, "proto", cliops.wsproto, "websocket sub-protocol") 218 | flag.StringVar(&cliops.wsproto, "p", cliops.wsproto, "websocket sub-protocol") 219 | flag.BoolVar(&cliops.wsreceive, "receive", cliops.wsreceive, "wait to receive response from ws server (true|false)") 220 | flag.BoolVar(&cliops.wsreceive, "r", cliops.wsreceive, "wait to receive response from ws server (true|false)") 221 | flag.StringVar(&cliops.wstemplate, "template", cliops.wstemplate, "path to template file") 222 | flag.StringVar(&cliops.wstemplate, "t", cliops.wstemplate, "path to template file") 223 | flag.StringVar(&cliops.wsurl, "url", cliops.wsurl, "websocket url (ws://... or wss://...)") 224 | flag.StringVar(&cliops.wsurl, "u", cliops.wsurl, "websocket url (ws://... or wss://...)") 225 | flag.BoolVar(&cliops.version, "version", cliops.version, "print version") 226 | flag.IntVar(&cliops.wstimeoutrecv, "timeout-recv", cliops.wstimeoutrecv, "timeout waiting to receive data (milliseconds)") 227 | flag.IntVar(&cliops.wstimeoutsend, "timeout-send", cliops.wstimeoutsend, "timeout trying to send data (milliseconds)") 228 | flag.StringVar(&cliops.wsoutputfile, "output-file", cliops.wsoutputfile, "path to the file where to store sent and received messages") 229 | flag.StringVar(&cliops.wsoutputfile, "O", cliops.wsoutputfile, "path to the file where to store sent and received messages") 230 | flag.BoolVar(&cliops.wsuuid, "uuid", cliops.wsuuid, "generate and print a uuid") 231 | flag.BoolVar(&cliops.wstemplaterun, "template-run", cliops.wstemplaterun, "run template execution and print the result") 232 | flag.Var(¶mFields, "field-val", "field value in format 'name:value' (can be provided many times)") 233 | flag.BoolVar(&cliops.wsflagdefaults, "flag-defaults", cliops.wsflagdefaults, "print flag (cli param) default values") 234 | flag.BoolVar(&cliops.wstemplatedefaults, "template-defaults", cliops.wstemplatedefaults, "print default (internal) template data") 235 | flag.BoolVar(&cliops.wsdomainurl, "domain-url", cliops.wsdomainurl, "set domain field value extracting from URL parameter") 236 | flag.BoolVar(&cliops.wsdomainorigin, "domain-origin", cliops.wsdomainorigin, "set domain field value extracting from origin parameter") 237 | flag.StringVar(&cliops.wshttpdomain, "http-domain", cliops.wshttpdomain, "http service domain") 238 | flag.StringVar(&cliops.wshttpsrv, "http-srv", cliops.wshttpsrv, "http server bind address") 239 | flag.StringVar(&cliops.wshttpssrv, "https-srv", cliops.wshttpssrv, "https server bind address") 240 | flag.StringVar(&cliops.wshttpspubkey, "https-pubkey", cliops.wshttpspubkey, "https server public key") 241 | flag.StringVar(&cliops.wshttpsprvkey, "https-prvkey", cliops.wshttpsprvkey, "https server private key") 242 | } 243 | 244 | // Echo-only service with direct copy 245 | func WSServerEchoOnly(ws *websocket.Conn) { 246 | fmt.Printf("echo-only - service requested\n") 247 | io.Copy(ws, ws) 248 | fmt.Println("echo-only - service finished\n") 249 | } 250 | 251 | // Echo service with logging of content 252 | func WSServerEcho(ws *websocket.Conn) { 253 | fmt.Printf("echo - service requested: %#v\n", ws) 254 | for { 255 | var buf string 256 | err := websocket.Message.Receive(ws, &buf) 257 | if err != nil { 258 | fmt.Println(err) 259 | break 260 | } 261 | fmt.Printf("echo - message received: %q\n", buf) 262 | err = websocket.Message.Send(ws, buf) 263 | if err != nil { 264 | fmt.Println(err) 265 | break 266 | } 267 | fmt.Printf("echo - message sent: %q\n", buf) 268 | } 269 | fmt.Printf("echo - service finished: %#v\n", ws) 270 | } 271 | 272 | // Echo service with logging of content 273 | func WSServerEchoReply(ws *websocket.Conn) { 274 | fmt.Printf("echo-reply - service requested: %#v\n", ws) 275 | for { 276 | var buf string 277 | err := websocket.Message.Receive(ws, &buf) 278 | if err != nil { 279 | fmt.Println(err) 280 | break 281 | } 282 | fmt.Printf("echo-reply - message received: %q\n", buf) 283 | err = websocket.Message.Send(ws, "Replying-To: "+buf) 284 | if err != nil { 285 | fmt.Println(err) 286 | break 287 | } 288 | fmt.Printf("echo-reply - message sent: %q\n", buf) 289 | } 290 | fmt.Printf("echo-reply - service finished: %#v\n", ws) 291 | } 292 | 293 | // Log service to print received messages 294 | func WSServerLog(ws *websocket.Conn) { 295 | fmt.Printf("log - service requested: %#v\n", ws) 296 | for { 297 | var buf string 298 | err := websocket.Message.Receive(ws, &buf) 299 | if err != nil { 300 | fmt.Println(err) 301 | break 302 | } 303 | fmt.Printf("log - message received: %q\n", buf) 304 | } 305 | fmt.Printf("log - service finished: %#v\n", ws) 306 | } 307 | 308 | // 309 | // Start http and https services 310 | func startHTTPServices() chan error { 311 | 312 | errchan := make(chan error) 313 | 314 | // starting HTTP server 315 | if len(cliops.wshttpsrv) > 0 { 316 | go func() { 317 | log.Printf("staring HTTP service on: %s ...", cliops.wshttpsrv) 318 | 319 | if err := http.ListenAndServe(cliops.wshttpsrv, nil); err != nil { 320 | errchan <- err 321 | } 322 | 323 | }() 324 | } 325 | 326 | // starting HTTPS server 327 | if len(cliops.wshttpssrv) > 0 && len(cliops.wshttpspubkey) > 0 && len(cliops.wshttpsprvkey) > 0 { 328 | go func() { 329 | log.Printf("Staring HTTPS service on: %s ...", cliops.wshttpssrv) 330 | if err := http.ListenAndServeTLS(cliops.wshttpssrv, cliops.wshttpspubkey, cliops.wshttpsprvkey, nil); err != nil { 331 | errchan <- err 332 | } 333 | }() 334 | } 335 | 336 | return errchan 337 | } 338 | 339 | // 340 | // wsctl application 341 | func main() { 342 | 343 | flag.Parse() 344 | 345 | fmt.Printf("\n") 346 | 347 | if cliops.version { 348 | fmt.Printf("%s v%s\n", filepath.Base(os.Args[0]), wsctlVersion) 349 | os.Exit(1) 350 | } 351 | 352 | if len(cliops.wshttpsrv) > 0 || len(cliops.wshttpssrv) > 0 { 353 | if cliops.wshttpsusele && len(cliops.wshttpdomain) == 0 { 354 | log.Printf("use-letsencrypt requires http domain parameter\n") 355 | os.Exit(1) 356 | } 357 | if cliops.wshttpsusele && len(cliops.wshttpssrv) > 0 && len(cliops.wshttpdomain) > 0 { 358 | cliops.wshttpspubkey = "/etc/letsencrypt/live/" + cliops.wshttpdomain + "/fullchain.pem" 359 | cliops.wshttpsprvkey = "/etc/letsencrypt/live/" + cliops.wshttpdomain + "/privkey.pem" 360 | } 361 | http.Handle("/echo-only", websocket.Handler(WSServerEchoOnly)) 362 | http.Handle("/echo-reply", websocket.Handler(WSServerEchoReply)) 363 | http.Handle("/echo", websocket.Handler(WSServerEcho)) 364 | http.Handle("/log", websocket.Handler(WSServerLog)) 365 | errchan := startHTTPServices() 366 | select { 367 | case err := <-errchan: 368 | log.Printf("unable to start http services due to (error: %v)", err) 369 | } 370 | os.Exit(1) 371 | } 372 | if cliops.wstemplatedefaults { 373 | fmt.Println("Default template:\n") 374 | fmt.Println(templateDefaultText) 375 | fmt.Println("Default fields:\n") 376 | fmt.Println(templateDefaultJSONFields) 377 | os.Exit(1) 378 | } 379 | if cliops.wsflagdefaults { 380 | flag.PrintDefaults() 381 | os.Exit(1) 382 | } 383 | if cliops.wsuuid { 384 | uuidVal := uuid.New() 385 | fmt.Println(uuidVal) 386 | os.Exit(1) 387 | } 388 | 389 | // enable file name and line numbers in logging 390 | log.SetFlags(log.LstdFlags | log.Lshortfile) 391 | 392 | // options for ws connections 393 | urlp, err := url.Parse(cliops.wsurl) 394 | if err != nil { 395 | log.Fatal(err) 396 | } 397 | orgp, err := url.Parse(cliops.wsorigin) 398 | if err != nil { 399 | log.Fatal(err) 400 | } 401 | 402 | // buffer to send over ws connection 403 | var buf bytes.Buffer 404 | var tplstr = "" 405 | if len(cliops.wstemplate) > 0 { 406 | tpldata, err1 := ioutil.ReadFile(cliops.wstemplate) 407 | if err1 != nil { 408 | log.Fatal(err1) 409 | } 410 | tplstr = string(tpldata) 411 | } else if len(templateDefaultText) > 0 { 412 | tplstr = templateDefaultText 413 | } else { 414 | log.Fatal("missing data template file ('-t' or '--template' parameter must be provided)") 415 | } 416 | 417 | tplfields := make(map[string]interface{}) 418 | if len(cliops.wsfields) > 0 { 419 | fieldsdata, err1 := ioutil.ReadFile(cliops.wsfields) 420 | if err1 != nil { 421 | log.Fatal(err1) 422 | } 423 | err = json.Unmarshal(fieldsdata, &tplfields) 424 | if err != nil { 425 | log.Fatal(err) 426 | } 427 | } else if len(templateDefaultJSONFields) > 0 { 428 | err = json.Unmarshal([]byte(templateDefaultJSONFields), &tplfields) 429 | if err != nil { 430 | log.Fatal(err) 431 | } 432 | cliops.wsfieldseval = true 433 | } else { 434 | tplfields = templateFields["FIELDS:EMPTY"] 435 | } 436 | if cliops.wsfieldseval { 437 | for k := range tplfields { 438 | switch tplfields[k].(type) { 439 | case string: 440 | if tplfields[k] == "$uuid" { 441 | tplfields[k] = uuid.New().String() 442 | } else if tplfields[k] == "$randseq" { 443 | mathrand.Seed(time.Now().Unix()) 444 | tplfields[k] = strconv.Itoa(1 + mathrand.Intn(999999)) 445 | } else if tplfields[k] == "$datefull" { 446 | tplfields[k] = time.Now().String() 447 | } else if tplfields[k] == "$daterfc1123" { 448 | tplfields[k] = time.Now().Format(time.RFC1123) 449 | } else if tplfields[k] == "$dateunix" { 450 | tplfields[k] = time.Now().Format(time.UnixDate) 451 | } else if tplfields[k] == "$dateansic" { 452 | tplfields[k] = time.Now().Format(time.ANSIC) 453 | } else if tplfields[k] == "$timestamp" { 454 | tplfields[k] = strconv.FormatInt(time.Now().Unix(), 10) 455 | } else if tplfields[k] == "$cr" { 456 | tplfields[k] = "\r" 457 | } else if tplfields[k] == "$lf" { 458 | tplfields[k] = "\n" 459 | } 460 | break 461 | } 462 | } 463 | } 464 | if len(paramFields) > 0 { 465 | for k := range paramFields { 466 | tplfields[k] = paramFields[k] 467 | } 468 | } 469 | if cliops.wsdomainurl { 470 | tplfields["domain"] = urlp.Hostname() 471 | } 472 | if cliops.wsdomainorigin { 473 | tplfields["domain"] = orgp.Hostname() 474 | } 475 | 476 | var tpl = template.Must(template.New("wsout").Parse(tplstr)) 477 | tpl.Execute(&buf, tplfields) 478 | 479 | var wmsg []byte 480 | if cliops.wscrlf { 481 | wmsg = []byte(strings.Replace(buf.String(), "\n", "\r\n", -1)) 482 | } else { 483 | wmsg = buf.Bytes() 484 | } 485 | 486 | if cliops.wstemplaterun { 487 | fmt.Println(string(wmsg)) 488 | os.Exit(1) 489 | } 490 | 491 | tlc := tls.Config{ 492 | InsecureSkipVerify: false, 493 | } 494 | if cliops.wsinsecure { 495 | tlc.InsecureSkipVerify = true 496 | } 497 | 498 | if cliops.wsoutputfile != "" { 499 | outputFile, err = os.Create(cliops.wsoutputfile) 500 | if err != nil { 501 | log.Fatal("Cannot create file", err) 502 | } 503 | defer outputFile.Close() 504 | } 505 | 506 | // open ws connection 507 | // ws, err := websocket.Dial(wsurl, "", wsorigin) 508 | ws, err := websocket.DialConfig(&websocket.Config{ 509 | Location: urlp, 510 | Origin: orgp, 511 | Protocol: []string{cliops.wsproto}, 512 | Version: 13, 513 | TlsConfig: &tlc, 514 | Header: http.Header{"User-Agent": {"wsctl"}}, 515 | }) 516 | if err != nil { 517 | log.Fatal(err) 518 | } 519 | 520 | // send data to ws server 521 | err = ws.SetWriteDeadline(time.Now().Add(time.Duration(cliops.wstimeoutsend) * time.Millisecond)) 522 | if err != nil { 523 | log.Fatal(err) 524 | } 525 | _, err = ws.Write(wmsg) 526 | if err != nil { 527 | log.Fatal(err) 528 | } 529 | localAddr := ws.LocalAddr() 530 | remoteAddr := ws.RemoteAddr() 531 | fmt.Printf("[%s] ** snd (%d bytes)\n -- %s => %s --\n%s\n", time.Now(), len(wmsg), localAddr.String(), remoteAddr.String(), wmsg) 532 | if cliops.wsoutputfile != "" { 533 | fmt.Fprintf(outputFile, "[%s] ** snd (%d bytes)\n -- %s => %s --\n%s\n\n", time.Now(), len(wmsg), localAddr.String(), remoteAddr.String(), wmsg) 534 | } 535 | 536 | // receive data from ws server 537 | if cliops.wsreceive { 538 | var rmsg = make([]byte, 8192) 539 | err = ws.SetReadDeadline(time.Now().Add(time.Duration(cliops.wstimeoutrecv) * time.Millisecond)) 540 | if err != nil { 541 | log.Fatal(err) 542 | } 543 | n, err := ws.Read(rmsg) 544 | if err != nil { 545 | log.Fatal(err) 546 | } 547 | fmt.Printf("[%s] ** rcv (%d bytes)\n -- %s => %s --\n%s\n", time.Now(), n, remoteAddr.String(), localAddr.String(), rmsg) 548 | if cliops.wsoutputfile != "" { 549 | fmt.Fprintf(outputFile, "[%s] ** rcv (%d bytes)\n -- %s => %s --\n%s\n\n", time.Now(), n, remoteAddr.String(), localAddr.String(), rmsg) 550 | } 551 | if n > 24 && cliops.wsproto == "sip" { 552 | ManageSIPResponse(ws, wmsg, rmsg) 553 | } 554 | } 555 | } 556 | 557 | // 558 | // ParseAuthHeader - parse www/proxy-authenticate header body. 559 | // Return a map of parameters or nil if the header is not Digest auth header. 560 | func ParseAuthHeader(hbody []byte) map[string]string { 561 | s := strings.SplitN(strings.Trim(string(hbody), " "), " ", 2) 562 | if len(s) != 2 || s[0] != "Digest" { 563 | return nil 564 | } 565 | 566 | params := map[string]string{} 567 | for _, kv := range strings.Split(s[1], ",") { 568 | parts := strings.SplitN(kv, "=", 2) 569 | if len(parts) != 2 { 570 | continue 571 | } 572 | params[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ") 573 | } 574 | return params 575 | } 576 | 577 | // 578 | // BuildAuthResponseHeader - return the body for auth header in response 579 | func BuildAuthResponseHeader(username string, password string, hparams map[string]string) string { 580 | // https://en.wikipedia.org/wiki/Digest_access_authentication 581 | // HA1 582 | h := md5.New() 583 | A1 := fmt.Sprintf("%s:%s:%s", username, hparams["realm"], password) 584 | io.WriteString(h, A1) 585 | HA1 := fmt.Sprintf("%x", h.Sum(nil)) 586 | 587 | // HA2 588 | h = md5.New() 589 | A2 := fmt.Sprintf("%s:%s", hparams["method"], hparams["uri"]) 590 | io.WriteString(h, A2) 591 | HA2 := fmt.Sprintf("%x", h.Sum(nil)) 592 | 593 | var AuthHeader string 594 | if _, ok := hparams["qop"]; !ok { 595 | // build digest response 596 | response := HMD5(strings.Join([]string{HA1, hparams["nonce"], HA2}, ":")) 597 | // build header body 598 | AuthHeader = fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=MD5, response="%s"`, 599 | username, hparams["realm"], hparams["nonce"], hparams["uri"], response) 600 | } else { 601 | // build digest response 602 | cnonce := RandomKey() 603 | response := HMD5(strings.Join([]string{HA1, hparams["nonce"], "00000001", cnonce, hparams["qop"], HA2}, ":")) 604 | // build header body 605 | AuthHeader = fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc=00000001, qop=%s, opaque="%s", algorithm=MD5, response="%s"`, 606 | username, hparams["realm"], hparams["nonce"], hparams["uri"], cnonce, hparams["qop"], hparams["opaque"], response) 607 | } 608 | return AuthHeader 609 | } 610 | 611 | // 612 | // RandomKey - return random key (used for cnonce) 613 | func RandomKey() string { 614 | key := make([]byte, 12) 615 | for b := 0; b < len(key); { 616 | n, err := cryptorand.Read(key[b:]) 617 | if err != nil { 618 | panic("failed to get random bytes") 619 | } 620 | b += n 621 | } 622 | return base64.StdEncoding.EncodeToString(key) 623 | } 624 | 625 | // 626 | // HMD5 - return a lower-case hex MD5 digest of the parameter 627 | func HMD5(data string) string { 628 | md5d := md5.New() 629 | md5d.Write([]byte(data)) 630 | return fmt.Sprintf("%x", md5d.Sum(nil)) 631 | } 632 | 633 | // 634 | // ManageSIPResponse - process a SIP response 635 | // - if was a 401/407, follow up with authentication request 636 | func ManageSIPResponse(ws *websocket.Conn, wmsg []byte, rmsg []byte) bool { 637 | if cliops.wsapasswd == "" { 638 | return false 639 | } 640 | // www or proxy authentication 641 | hname := "" 642 | if bytes.HasPrefix(rmsg, []byte("SIP/2.0 401 ")) { 643 | hname = "WWW-Authenticate:" 644 | } else if bytes.HasPrefix(rmsg, []byte("SIP/2.0 407 ")) { 645 | hname = "Proxy-Authenticate:" 646 | } 647 | n := bytes.Index(rmsg, []byte(hname)) 648 | if n < 0 { 649 | return false 650 | } 651 | hbody := bytes.Trim(rmsg[n:n+bytes.Index(rmsg[n:], []byte("\n"))], " \t\r") 652 | hparams := ParseAuthHeader(hbody[len(hname):]) 653 | if hparams == nil { 654 | return false 655 | } 656 | auser := "test" 657 | if cliops.wsauser != "" { 658 | auser = cliops.wsauser 659 | } 660 | 661 | s := strings.SplitN(string(wmsg), " ", 3) 662 | if len(s) != 3 { 663 | return false 664 | } 665 | 666 | hparams["method"] = s[0] 667 | hparams["uri"] = s[1] 668 | fmt.Printf("\nAuth params map:\n %+v\n\n", hparams) 669 | authResponse := BuildAuthResponseHeader(auser, cliops.wsapasswd, hparams) 670 | 671 | // build new request - increase CSeq and insert auth header 672 | n = bytes.Index(wmsg, []byte("CSeq:")) 673 | if n < 0 { 674 | n = bytes.Index(wmsg, []byte("s:")) 675 | if n < 0 { 676 | return false 677 | } 678 | } 679 | hbody = bytes.Trim(wmsg[n:n+bytes.Index(wmsg[n:], []byte("\n"))], " \t\r") 680 | var obuf bytes.Buffer 681 | obuf.Write(wmsg[:n]) 682 | s = strings.SplitN(string(hbody), " ", 3) 683 | if len(s) != 3 { 684 | return false 685 | } 686 | csn, _ := strconv.Atoi(s[1]) 687 | cs := strconv.Itoa(1 + csn) 688 | 689 | obuf.WriteString("CSeq: " + cs + " " + s[2] + "\r\n") 690 | if hname[0] == 'W' { 691 | obuf.WriteString("Authorization: ") 692 | } else { 693 | obuf.WriteString("Proxy-Authorization: ") 694 | } 695 | obuf.WriteString(authResponse) 696 | obuf.WriteString("\r\n") 697 | obuf.Write(wmsg[1+n+bytes.Index(wmsg[n:], []byte("\n")):]) 698 | 699 | // sending data to ws server 700 | _, err := ws.Write(obuf.Bytes()) 701 | if err != nil { 702 | log.Fatal(err) 703 | } 704 | localAddr := ws.LocalAddr() 705 | remoteAddr := ws.RemoteAddr() 706 | fmt.Printf("[%s] ** snd (%d bytes)\n -- %s => %s --\n%s\n", time.Now(), obuf.Len(), localAddr.String(), remoteAddr.String(), obuf.Bytes()) 707 | if cliops.wsoutputfile != "" { 708 | fmt.Fprintf(outputFile, "[%s] ** snd (%d bytes)\n -- %s => %s --\n%s\n\n", time.Now(), obuf.Len(), localAddr.String(), remoteAddr.String(), obuf.Bytes()) 709 | } 710 | 711 | // receive data from ws server 712 | if cliops.wsreceive { 713 | var imsg = make([]byte, 8192) 714 | n, err := ws.Read(imsg) 715 | if err != nil { 716 | log.Fatal(err) 717 | } 718 | fmt.Printf("[%s] ** rcv (%d bytes)\n -- %s => %s --\n%s\n", time.Now(), n, remoteAddr.String(), localAddr.String(), imsg) 719 | if cliops.wsoutputfile != "" { 720 | fmt.Fprintf(outputFile, "[%s] ** rcv (%d bytes)\n -- %s => %s --\n%s\n\n", time.Now(), n, remoteAddr.String(), localAddr.String(), imsg) 721 | } 722 | } 723 | 724 | return true 725 | } 726 | --------------------------------------------------------------------------------