├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── examples ├── b2bua │ ├── b2bua │ │ └── b2bua.go │ ├── fcm │ │ └── fcm.go │ ├── main.go │ ├── pushkit │ │ └── pushkit.go │ └── registry │ │ ├── expire.go │ │ ├── mem.go │ │ ├── registry.go │ │ └── rfc8599.go ├── client │ └── main.go ├── mock │ └── sdp.go └── register │ └── main.go ├── go.mod ├── go.sum ├── pkg ├── account │ └── profile.go ├── auth │ ├── client.go │ └── server.go ├── media │ ├── media.go │ ├── rtp.go │ ├── rtp │ │ ├── udp.go │ │ └── udp_test.go │ ├── sdp.go │ └── webrtc │ │ └── webrtc.go ├── session │ ├── session.go │ └── type.go ├── stack │ └── stack.go ├── ua │ ├── register.go │ └── ua.go └── utils │ ├── log.go │ └── util.go └── renovate.json /.gitignore: -------------------------------------------------------------------------------- 1 | # private certs dir 2 | certs 3 | 4 | # Binaries for programs and plugins 5 | .idea 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | bin 22 | 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | {"mode":"full","isActive":false} 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell git describe --tags) 2 | GOFLAGS= 3 | GO_LDFLAGS = -ldflags "-s -w" 4 | 5 | all: server 6 | 7 | clean: 8 | rm -rf bin 9 | 10 | upx: 11 | upx -9 bin/* 12 | 13 | server: 14 | go build -o bin/simple-b2bua $(GO_LDFLAGS) examples/b2bua/main.go 15 | go build -o bin/simple-client $(GO_LDFLAGS) examples/client/main.go 16 | go build -o bin/simple-register $(GO_LDFLAGS) examples/register/main.go 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-sip-ua 2 | 3 | SIP UA library for client/b2bua using golang 4 | 5 | ## Features 6 | 7 | - [x] Transports UDP/TCP/TLS/WS/WSS. 8 | - [x] Simple pure Go SIP Client. 9 | - [x] Simple pure Go B2BUA, support RFC8599, Google FCM/Apple PushKit. 10 | - [ ] RTP relay (UDP<-->UDP, WebRTC/ICE<->UDP) 11 | - [ ] WebRTC2SIP Gateway. 12 | 13 | ## Running the examples 14 | 15 | ```bash 16 | git clone https://github.com/cloudwebrtc/go-sip-ua 17 | cd go-sip-ua 18 | ``` 19 | 20 | ### Client 21 | 22 | ```bash 23 | # run client 24 | go run examples/client/main.go 25 | ``` 26 | 27 | ### B2BUA 28 | 29 | B2BUA is a minimal SIP call switch, it registers and calls, and supports UDP/TCP/TLS/WebSockets. 30 | 31 | When you need a quick test for TLS/WSS, you can use [mkcert](https://github.com/FiloSottile/mkcert) to create a local self-signed certificate. 32 | 33 | ```bash 34 | mkdir -p certs 35 | mkcert -key-file certs/key.pem -cert-file certs/cert.pem localhost 127.0.0.1 ::1 example.com 36 | ``` 37 | 38 | Run the mini b2bua. 39 | 40 | ```bash 41 | # run b2bua 42 | go run examples/b2bua/main.go -c 43 | ``` 44 | 45 | You can use [dart-sip-ua](https://github.com/flutter-webrtc/dart-sip-ua) or [linphone](https://www.linphone.org/) or [jssip](https://tryit.jssip.net/) to test call or registration, built-in test account 100~400 46 | 47 | ``` 48 | WebSocket: wss://127.0.0.1:5081 49 | SIP URI: 100@127.0.0.1 50 | Authorization User: 100 51 | Password: 100 52 | Display Name: Flutter SIP Client 53 | ``` 54 | 55 | ## Dependencies 56 | 57 | - [ghettovoice/gosip](https://github.com/ghettovoice/gosip) SIP stack 58 | - [c-bata/go-prompt](https://github.com/c-bata/go-prompt) Console for b2bua 59 | - [pixelbender/go-sdp](https://github.com/pixelbender/go-sdp) SDP 60 | -------------------------------------------------------------------------------- /examples/b2bua/b2bua/b2bua.go: -------------------------------------------------------------------------------- 1 | package b2bua 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudwebrtc/go-sip-ua/examples/b2bua/fcm" 7 | "github.com/cloudwebrtc/go-sip-ua/examples/b2bua/pushkit" 8 | "github.com/cloudwebrtc/go-sip-ua/examples/b2bua/registry" 9 | 10 | "github.com/cloudwebrtc/go-sip-ua/pkg/account" 11 | "github.com/cloudwebrtc/go-sip-ua/pkg/auth" 12 | "github.com/cloudwebrtc/go-sip-ua/pkg/session" 13 | "github.com/cloudwebrtc/go-sip-ua/pkg/stack" 14 | "github.com/cloudwebrtc/go-sip-ua/pkg/ua" 15 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 16 | "github.com/ghettovoice/gosip/log" 17 | "github.com/ghettovoice/gosip/sip" 18 | "github.com/ghettovoice/gosip/sip/parser" 19 | "github.com/ghettovoice/gosip/transport" 20 | ) 21 | 22 | type B2BCall struct { 23 | src *session.Session 24 | //TODO: Add support for forked calls 25 | dest *session.Session 26 | } 27 | 28 | func (b *B2BCall) ToString() string { 29 | return b.src.Contact() + " => " + b.dest.Contact() 30 | } 31 | 32 | func pushCallback(pn *registry.PNParams, payload map[string]string) error { 33 | fmt.Printf("Handle Push Request:\nprovider=%v\nparam=%v\nprid=%v\npayload=%v", pn.Provider, pn.Param, pn.PRID, payload) 34 | switch pn.Provider { 35 | case "apns": 36 | go pushkit.DoPushKit("./voip-callkeep.p12", pn.PRID, payload) 37 | return nil 38 | case "fcm": 39 | go fcm.FCMPush("service-account.json", pn.PRID, payload) 40 | return nil 41 | } 42 | return fmt.Errorf("%v provider not found", pn.Provider) 43 | } 44 | 45 | // B2BUA . 46 | type B2BUA struct { 47 | stack *stack.SipStack 48 | ua *ua.UserAgent 49 | accounts map[string]string 50 | registry registry.Registry 51 | domains []string 52 | calls []*B2BCall 53 | rfc8599 *registry.RFC8599 54 | } 55 | 56 | var ( 57 | logger log.Logger 58 | ) 59 | 60 | func init() { 61 | logger = utils.NewLogrusLogger(log.InfoLevel, "B2BUA", nil) 62 | } 63 | 64 | //NewB2BUA . 65 | func NewB2BUA(disableAuth bool, enableTLS bool) *B2BUA { 66 | b := &B2BUA{ 67 | registry: registry.Registry(registry.NewMemoryRegistry()), 68 | accounts: make(map[string]string), 69 | rfc8599: registry.NewRFC8599(pushCallback), 70 | } 71 | 72 | var authenticator *auth.ServerAuthorizer = nil 73 | 74 | if !disableAuth { 75 | authenticator = auth.NewServerAuthorizer(b.requestCredential, "b2bua", false) 76 | } 77 | 78 | stack := stack.NewSipStack(&stack.SipStackConfig{ 79 | UserAgent: "Go B2BUA/1.0.0", 80 | Extensions: []string{"replaces", "outbound"}, 81 | Dns: "8.8.8.8", 82 | ServerAuthManager: stack.ServerAuthManager{ 83 | Authenticator: authenticator, 84 | RequiresChallenge: b.requiresChallenge, 85 | }, 86 | }) 87 | 88 | stack.OnConnectionError(b.handleConnectionError) 89 | 90 | if err := stack.Listen("udp", "0.0.0.0:5060"); err != nil { 91 | logger.Panic(err) 92 | } 93 | 94 | if err := stack.Listen("tcp", "0.0.0.0:5060"); err != nil { 95 | logger.Panic(err) 96 | } 97 | 98 | if enableTLS { 99 | tlsOptions := &transport.TLSConfig{Cert: "certs/cert.pem", Key: "certs/key.pem"} 100 | 101 | if err := stack.ListenTLS("tls", "0.0.0.0:5061", tlsOptions); err != nil { 102 | logger.Panic(err) 103 | } 104 | 105 | if err := stack.ListenTLS("wss", "0.0.0.0:5081", tlsOptions); err != nil { 106 | logger.Panic(err) 107 | } 108 | } 109 | 110 | ua := ua.NewUserAgent(&ua.UserAgentConfig{ 111 | 112 | SipStack: stack, 113 | }) 114 | 115 | ua.InviteStateHandler = func(sess *session.Session, req *sip.Request, resp *sip.Response, state session.Status) { 116 | logger.Infof("InviteStateHandler: state => %v, type => %s", state, sess.Direction()) 117 | 118 | switch state { 119 | // Handle incoming call. 120 | case session.InviteReceived: 121 | to, _ := (*req).To() 122 | from, _ := (*req).From() 123 | caller := from.Address 124 | called := to.Address 125 | 126 | doInvite := func(instance *registry.ContactInstance) { 127 | displayName := "" 128 | if from.DisplayName != nil { 129 | displayName = from.DisplayName.String() 130 | } 131 | 132 | // Create a temporary profile. In the future, it will support reading profiles from files or data 133 | // For example: use a specific ip or sip account as outbound trunk 134 | profile := account.NewProfile(caller, displayName, nil, 0, stack) 135 | 136 | recipient, err2 := parser.ParseSipUri("sip:" + called.User().String() + "@" + instance.Source + ";transport=" + instance.Transport) 137 | if err2 != nil { 138 | logger.Error(err2) 139 | } 140 | 141 | offer := sess.RemoteSdp() 142 | dest, err := ua.Invite(profile, called, recipient, &offer) 143 | if err != nil { 144 | logger.Errorf("B-Leg session error: %v", err) 145 | return 146 | } 147 | b.calls = append(b.calls, &B2BCall{src: sess, dest: dest}) 148 | } 149 | 150 | // Try to find online contact records. 151 | if contacts, found := b.registry.GetContacts(called); found { 152 | sess.Provisional(100, "Trying") 153 | for _, instance := range *contacts { 154 | doInvite(instance) 155 | } 156 | return 157 | } 158 | 159 | // Pushable: try to find pn-params in contact records. 160 | // Try to push the UA and wait for it to wake up. 161 | pusher, ok := b.rfc8599.TryPush(called, from) 162 | if ok { 163 | sess.Provisional(100, "Trying") 164 | instance, err := pusher.WaitContactOnline() 165 | if err != nil { 166 | logger.Errorf("Push failed, error: %v", err) 167 | sess.Reject(500, fmt.Sprint("Push failed")) 168 | return 169 | } 170 | doInvite(instance) 171 | return 172 | } 173 | 174 | // Could not found any records 175 | sess.Reject(404, fmt.Sprintf("%v Not found", called)) 176 | 177 | // Handle re-INVITE or UPDATE. 178 | case session.ReInviteReceived: 179 | logger.Infof("re-INVITE") 180 | switch sess.Direction() { 181 | case session.Incoming: 182 | sess.Accept(200) 183 | case session.Outgoing: 184 | //TODO: Need to provide correct answer. 185 | } 186 | 187 | // Handle 1XX 188 | case session.EarlyMedia: 189 | fallthrough 190 | case session.Provisional: 191 | call := b.findCall(sess) 192 | if call != nil && call.dest == sess { 193 | answer := call.dest.RemoteSdp() 194 | call.src.ProvideAnswer(answer) 195 | call.src.Provisional((*resp).StatusCode(), (*resp).Reason()) 196 | } 197 | 198 | // Handle 200OK or ACK 199 | case session.Confirmed: 200 | //TODO: Add support for forked calls 201 | call := b.findCall(sess) 202 | if call != nil && call.dest == sess { 203 | answer := call.dest.RemoteSdp() 204 | call.src.ProvideAnswer(answer) 205 | call.src.Accept(200) 206 | } 207 | 208 | // Handle 4XX+ 209 | case session.Failure: 210 | fallthrough 211 | case session.Canceled: 212 | fallthrough 213 | case session.Terminated: 214 | //TODO: Add support for forked calls 215 | call := b.findCall(sess) 216 | if call != nil { 217 | if call.src == sess { 218 | call.dest.End() 219 | } else if call.dest == sess { 220 | call.src.End() 221 | } 222 | } 223 | b.removeCall(sess) 224 | 225 | } 226 | } 227 | 228 | ua.RegisterStateHandler = func(state account.RegisterState) { 229 | logger.Infof("RegisterStateHandler: state => %v", state) 230 | } 231 | 232 | stack.OnRequest(sip.REGISTER, b.handleRegister) 233 | b.stack = stack 234 | b.ua = ua 235 | return b 236 | } 237 | 238 | func (b *B2BUA) Calls() []*B2BCall { 239 | return b.calls 240 | } 241 | 242 | func (b *B2BUA) findCall(sess *session.Session) *B2BCall { 243 | for _, call := range b.calls { 244 | if call.src == sess || call.dest == sess { 245 | return call 246 | } 247 | } 248 | return nil 249 | } 250 | 251 | func (b *B2BUA) removeCall(sess *session.Session) { 252 | for idx, call := range b.calls { 253 | if call.src == sess || call.dest == sess { 254 | b.calls = append(b.calls[:idx], b.calls[idx+1:]...) 255 | return 256 | } 257 | } 258 | } 259 | 260 | //Shutdown . 261 | func (b *B2BUA) Shutdown() { 262 | b.ua.Shutdown() 263 | } 264 | 265 | func (b *B2BUA) requiresChallenge(req sip.Request) bool { 266 | switch req.Method() { 267 | //case sip.UPDATE: 268 | case sip.REGISTER: 269 | return true 270 | case sip.INVITE: 271 | return true 272 | //case sip.RREFER: 273 | // return false 274 | case sip.CANCEL: 275 | return false 276 | case sip.OPTIONS: 277 | return false 278 | case sip.INFO: 279 | return false 280 | case sip.BYE: 281 | { 282 | // Allow locally initiated dialogs 283 | // Return false if call-id in sessions. 284 | return false 285 | } 286 | } 287 | return false 288 | } 289 | 290 | //AddAccount . 291 | func (b *B2BUA) AddAccount(username string, password string) { 292 | b.accounts[username] = password 293 | } 294 | 295 | //GetAccounts . 296 | func (b *B2BUA) GetAccounts() map[string]string { 297 | return b.accounts 298 | } 299 | 300 | //GetRegistry . 301 | func (b *B2BUA) GetRegistry() registry.Registry { 302 | return b.registry 303 | } 304 | 305 | //GetRFC8599 . 306 | func (b *B2BUA) GetRFC8599() *registry.RFC8599 { 307 | return b.rfc8599 308 | } 309 | 310 | func (b *B2BUA) requestCredential(username string) (string, string, error) { 311 | if password, found := b.accounts[username]; found { 312 | logger.Infof("Found user %s", username) 313 | return password, "", nil 314 | } 315 | return "", "", fmt.Errorf("username [%s] not found", username) 316 | } 317 | 318 | func (b *B2BUA) handleRegister(request sip.Request, tx sip.ServerTransaction) { 319 | headers := request.GetHeaders("Expires") 320 | to, _ := request.To() 321 | aor := to.Address.Clone() 322 | var expires sip.Expires = 0 323 | if len(headers) > 0 { 324 | expires = *headers[0].(*sip.Expires) 325 | } 326 | 327 | reason := "" 328 | if len(headers) > 0 && expires != sip.Expires(0) { 329 | instance := registry.NewContactInstanceForRequest(request) 330 | logger.Infof("Registered [%v] expires [%d] source %s", to, expires, request.Source()) 331 | reason = "Registered" 332 | b.registry.AddAor(aor, instance) 333 | b.rfc8599.HandleContactInstance(aor, instance) 334 | } else { 335 | logger.Infof("Logged out [%v] expires [%d] ", to, expires) 336 | reason = "UnRegistered" 337 | instance := registry.NewContactInstanceForRequest(request) 338 | b.registry.RemoveContact(aor, instance) 339 | b.rfc8599.HandleContactInstance(aor, instance) 340 | } 341 | 342 | resp := sip.NewResponseFromRequest(request.MessageID(), request, 200, reason, "") 343 | sip.CopyHeaders("Expires", request, resp) 344 | utils.BuildContactHeader("Contact", request, resp, &expires) 345 | tx.Respond(resp) 346 | 347 | } 348 | 349 | func (b *B2BUA) handleConnectionError(connError *transport.ConnectionError) { 350 | logger.Debugf("Handle Connection Lost: Source: %v, Dest: %v, Network: %v", connError.Source, connError.Dest, connError.Net) 351 | b.registry.HandleConnectionError(connError) 352 | } 353 | 354 | func (b *B2BUA) SetLogLevel(level log.Level) { 355 | utils.SetLogLevel("B2BUA", level) 356 | } 357 | -------------------------------------------------------------------------------- /examples/b2bua/fcm/fcm.go: -------------------------------------------------------------------------------- 1 | package fcm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "context" 7 | 8 | firebase "firebase.google.com/go" 9 | "firebase.google.com/go/messaging" 10 | 11 | "google.golang.org/api/option" 12 | ) 13 | 14 | func FCMPush(fcmCert string, token string, payload map[string]string) (string, error) { 15 | opt := option.WithCredentialsFile(fcmCert) 16 | app, err := firebase.NewApp(context.Background(), nil, opt) 17 | 18 | // Obtain a messaging.Client from the App. 19 | ctx := context.Background() 20 | client, err := app.Messaging(ctx) 21 | 22 | // See documentation on defining a message payload. 23 | message := &messaging.Message{ 24 | Data: payload, 25 | Token: token, 26 | } 27 | 28 | // Send a message to the device corresponding to the provided 29 | // registration token. 30 | response, err := client.Send(ctx, message) 31 | if err != nil { 32 | fmt.Println(err) 33 | return "", err 34 | } 35 | // Response is a message ID string. 36 | fmt.Println("Successfully sent message:", response) 37 | return response, err 38 | } 39 | -------------------------------------------------------------------------------- /examples/b2bua/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | _ "net/http/pprof" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "github.com/c-bata/go-prompt" 13 | "github.com/cloudwebrtc/go-sip-ua/examples/b2bua/b2bua" 14 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 15 | "github.com/ghettovoice/gosip/log" 16 | ) 17 | 18 | func completer(d prompt.Document) []prompt.Suggest { 19 | s := []prompt.Suggest{ 20 | {Text: "users", Description: "Show sip accounts"}, 21 | {Text: "onlines", Description: "Show online sip devices"}, 22 | {Text: "calls", Description: "Show active calls"}, 23 | {Text: "set debug on", Description: "Show debug msg in console"}, 24 | {Text: "set debug off", Description: "Turn off debug msg in console"}, 25 | {Text: "show loggers", Description: "Print Loggers"}, 26 | {Text: "exit", Description: "Exit"}, 27 | } 28 | return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) 29 | } 30 | 31 | func usage() { 32 | fmt.Fprintf(os.Stderr, `go pbx version: go-pbx/1.10.0 33 | Usage: server [-nc] 34 | 35 | Options: 36 | `) 37 | flag.PrintDefaults() 38 | } 39 | 40 | func consoleLoop(b2bua *b2bua.B2BUA) { 41 | 42 | fmt.Println("Please select command.") 43 | for { 44 | t := prompt.Input("CLI> ", completer, 45 | prompt.OptionTitle("GO B2BUA 1.0.0"), 46 | prompt.OptionHistory([]string{"calls", "users", "onlines"}), 47 | prompt.OptionPrefixTextColor(prompt.Yellow), 48 | prompt.OptionPreviewSuggestionTextColor(prompt.Blue), 49 | prompt.OptionSelectedSuggestionBGColor(prompt.LightGray), 50 | prompt.OptionSuggestionBGColor(prompt.DarkGray)) 51 | 52 | switch t { 53 | case "show loggers": 54 | loggers := utils.GetLoggers() 55 | for prefix, log := range loggers { 56 | fmt.Printf("%v => %v\n", prefix, log.Level()) 57 | } 58 | case "set debug on": 59 | b2bua.SetLogLevel(log.DebugLevel) 60 | fmt.Printf("Set Log level to debug\n") 61 | case "set debug off": 62 | b2bua.SetLogLevel(log.InfoLevel) 63 | fmt.Printf("Set Log level to info\n") 64 | case "users": 65 | fallthrough 66 | case "ul": /* user list*/ 67 | accounts := b2bua.GetAccounts() 68 | if len(accounts) > 0 { 69 | fmt.Printf("Users:\n") 70 | fmt.Printf("Username \t Password\n") 71 | for user, pass := range accounts { 72 | fmt.Printf("%v \t\t %v\n", user, pass) 73 | } 74 | } else { 75 | fmt.Printf("No users\n") 76 | } 77 | case "calls": 78 | fallthrough 79 | case "cl": /* call list*/ 80 | calls := b2bua.Calls() 81 | if len(calls) > 0 { 82 | fmt.Printf("Calls:\n") 83 | for _, call := range calls { 84 | fmt.Printf("%v:\n", call.ToString()) 85 | } 86 | } else { 87 | fmt.Printf("No active calls\n") 88 | } 89 | case "onlines": 90 | fallthrough 91 | case "rr": /* register records*/ 92 | aors := b2bua.GetRegistry().GetAllContacts() 93 | if len(aors) > 0 { 94 | for aor, instances := range aors { 95 | fmt.Printf("AOR: %v:\n", aor) 96 | for _, instance := range instances { 97 | fmt.Printf("\t%v, Expires: %d, Source: %v, Transport: %v\n", 98 | (*instance).UserAgent, 99 | (*instance).RegExpires, 100 | (*instance).Source, 101 | (*instance).Transport) 102 | } 103 | } 104 | } else { 105 | fmt.Printf("No online devices\n") 106 | } 107 | case "pr": /* pn records*/ 108 | pnrs := b2bua.GetRFC8599().PNRecords() 109 | if len(pnrs) > 0 { 110 | fmt.Printf("PN Records:\n") 111 | for pn, aor := range pnrs { 112 | fmt.Printf("AOR: %v => pn-provider=%v, pn-param=%v, pn-prid=%v\n", aor, pn.Provider, pn.Param, pn.PRID) 113 | } 114 | } else { 115 | fmt.Printf("No pn records\n") 116 | } 117 | case "exit": 118 | fmt.Println("Exit now.") 119 | b2bua.Shutdown() 120 | return 121 | } 122 | } 123 | } 124 | 125 | func main() { 126 | noconsole := false 127 | disableAuth := false 128 | enableTLS := false 129 | h := false 130 | flag.BoolVar(&h, "h", false, "this help") 131 | flag.BoolVar(&noconsole, "nc", false, "no console mode") 132 | flag.BoolVar(&disableAuth, "da", false, "disable auth mode") 133 | flag.BoolVar(&enableTLS, "tls", false, "enable TLS") 134 | flag.Usage = usage 135 | 136 | flag.Parse() 137 | 138 | if h { 139 | flag.Usage() 140 | return 141 | } 142 | 143 | stop := make(chan os.Signal, 1) 144 | signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) 145 | 146 | go func() { 147 | fmt.Print("Start pprof on :6658\n") 148 | http.ListenAndServe(":6658", nil) 149 | }() 150 | 151 | b2bua := b2bua.NewB2BUA(disableAuth, enableTLS) 152 | 153 | // Add sample accounts. 154 | b2bua.AddAccount("100", "100") 155 | b2bua.AddAccount("200", "200") 156 | b2bua.AddAccount("300", "300") 157 | b2bua.AddAccount("400", "400") 158 | 159 | if !noconsole { 160 | consoleLoop(b2bua) 161 | return 162 | } 163 | 164 | <-stop 165 | b2bua.Shutdown() 166 | } 167 | -------------------------------------------------------------------------------- /examples/b2bua/pushkit/pushkit.go: -------------------------------------------------------------------------------- 1 | package pushkit 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "net" 14 | "strings" 15 | "time" 16 | 17 | "golang.org/x/crypto/pkcs12" 18 | ) 19 | 20 | var ( 21 | ErrExpired = errors.New("certificate has expired or is not yet valid") 22 | ) 23 | 24 | const ( 25 | Development = "gateway.sandbox.push.apple.com:2195" 26 | Production = "gateway.push.apple.com:2195" 27 | ) 28 | 29 | type PushKit struct { 30 | FileName string 31 | Pwd string 32 | serverName string 33 | cert tls.Certificate 34 | hostUrl string 35 | isDebug bool 36 | } 37 | 38 | func InitPushKit(filename, pwd string, isDebug bool) (*PushKit, error) { 39 | var ( 40 | err error 41 | cert tls.Certificate 42 | pushkit = &PushKit{} 43 | ) 44 | if cert, err = Load(filename, pwd); err != nil { 45 | return nil, err 46 | } 47 | pushkit.cert = cert 48 | pushkit.FileName = filename 49 | pushkit.isDebug = isDebug 50 | if isDebug { 51 | pushkit.hostUrl = Development 52 | pushkit.serverName = strings.Split(Development, ":")[0] 53 | } else { 54 | pushkit.serverName = strings.Split(Production, ":")[0] 55 | pushkit.hostUrl = Production 56 | } 57 | return pushkit, nil 58 | } 59 | 60 | func (p *PushKit) Push(token string, data []byte) error { 61 | conf := &tls.Config{ 62 | Certificates: []tls.Certificate{p.cert}, 63 | ServerName: p.serverName, 64 | } 65 | 66 | conn, err := net.Dial("tcp", p.hostUrl) 67 | if err != nil { 68 | return errors.New(fmt.Sprintf("error Dial: %s", err.Error())) 69 | } 70 | tlsconn := tls.Client(conn, conf) 71 | 72 | err = tlsconn.Handshake() 73 | if err != nil { 74 | return errors.New(fmt.Sprintf("error Handshake: %s", err.Error())) 75 | } 76 | 77 | btoken, err := hex.DecodeString(token) 78 | if err != nil { 79 | return errors.New(fmt.Sprintf("tls error DecodeString: %s", err.Error())) 80 | } 81 | buffer := bytes.NewBuffer([]byte{}) 82 | binary.Write(buffer, binary.BigEndian, uint8(1)) 83 | binary.Write(buffer, binary.BigEndian, uint32(1)) 84 | binary.Write(buffer, binary.BigEndian, uint32(time.Now().Unix()+60*60*24)) 85 | binary.Write(buffer, binary.BigEndian, uint16(len(btoken))) 86 | binary.Write(buffer, binary.BigEndian, btoken) 87 | binary.Write(buffer, binary.BigEndian, uint16(len(data))) 88 | binary.Write(buffer, binary.BigEndian, data) 89 | pdu := buffer.Bytes() 90 | _, err = tlsconn.Write(pdu) 91 | if err != nil { 92 | return errors.New(fmt.Sprintf("tls error Write: %s", err.Error())) 93 | } 94 | tlsconn.Close() 95 | return nil 96 | } 97 | 98 | // Load a .p12 certificate from disk. 99 | func Load(filename, password string) (tls.Certificate, error) { 100 | p12, err := ioutil.ReadFile(filename) 101 | if err != nil { 102 | return tls.Certificate{}, fmt.Errorf("Unable to load %s: %v", filename, err) 103 | } 104 | return Decode(p12, password) 105 | } 106 | 107 | // Decode and verify an in memory .p12 certificate (DER binary format). 108 | func Decode(p12 []byte, password string) (tls.Certificate, error) { 109 | // decode an x509.Certificate to verify 110 | privateKey, cert, err := pkcs12.Decode(p12, password) 111 | if err != nil { 112 | return tls.Certificate{}, err 113 | } 114 | if err := verify(cert); err != nil { 115 | return tls.Certificate{}, err 116 | } 117 | // wraps x509 certificate as a tls.Certificate: 118 | return tls.Certificate{ 119 | Certificate: [][]byte{cert.Raw}, 120 | PrivateKey: privateKey, 121 | Leaf: cert, 122 | }, nil 123 | } 124 | 125 | // verify checks if a certificate has expired 126 | func verify(cert *x509.Certificate) error { 127 | _, err := cert.Verify(x509.VerifyOptions{}) 128 | if err == nil { 129 | return nil 130 | } 131 | switch e := err.(type) { 132 | case x509.CertificateInvalidError: 133 | switch e.Reason { 134 | case x509.Expired: 135 | return ErrExpired 136 | default: 137 | return err 138 | } 139 | case x509.UnknownAuthorityError: 140 | return nil 141 | default: 142 | return err 143 | } 144 | } 145 | 146 | func DoPushKit(p12Cert string, token string, payload map[string]string) { 147 | push, err := InitPushKit(p12Cert, "", true) 148 | if err != nil { 149 | fmt.Println(err) 150 | } 151 | data, _ := json.Marshal(payload) 152 | fmt.Println(push.Push(token, data)) 153 | } 154 | -------------------------------------------------------------------------------- /examples/b2bua/registry/expire.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | ) 9 | 10 | type val struct { 11 | data interface{} 12 | expiredTime int64 13 | } 14 | 15 | const delChannelCap = 100 16 | 17 | type ExpiredMap struct { 18 | m map[interface{}]*val 19 | timeMap map[int64][]interface{} 20 | lck *sync.Mutex 21 | stop chan struct{} 22 | needStop int32 23 | } 24 | 25 | func NewExpiredMap() *ExpiredMap { 26 | e := ExpiredMap{ 27 | m: make(map[interface{}]*val), 28 | lck: new(sync.Mutex), 29 | timeMap: make(map[int64][]interface{}), 30 | stop: make(chan struct{}), 31 | } 32 | atomic.StoreInt32(&e.needStop, 0) 33 | go e.run(time.Now().Unix()) 34 | return &e 35 | } 36 | 37 | type delMsg struct { 38 | keys []interface{} 39 | t int64 40 | } 41 | 42 | func (e *ExpiredMap) run(now int64) { 43 | t := time.NewTicker(time.Second * 1) 44 | delCh := make(chan *delMsg, delChannelCap) 45 | go func() { 46 | for v := range delCh { 47 | if atomic.LoadInt32(&e.needStop) == 1 { 48 | log.Printf("---del stop---") 49 | return 50 | } 51 | e.multiDelete(v.keys, v.t) 52 | } 53 | }() 54 | for { 55 | select { 56 | case <-t.C: 57 | now++ 58 | if keys, found := e.timeMap[now]; found { 59 | delCh <- &delMsg{keys: keys, t: now} 60 | } 61 | case <-e.stop: 62 | log.Printf("=== STOP ===") 63 | atomic.StoreInt32(&e.needStop, 1) 64 | delCh <- &delMsg{keys: []interface{}{}, t: 0} 65 | return 66 | } 67 | } 68 | } 69 | 70 | func (e *ExpiredMap) Set(key, value interface{}, expireSeconds int64) { 71 | if expireSeconds <= 0 { 72 | return 73 | } 74 | log.Printf("ExpiredMap: Set %s ttl[%d] => %v", key, expireSeconds, value) 75 | e.lck.Lock() 76 | defer e.lck.Unlock() 77 | expiredTime := time.Now().Unix() + expireSeconds 78 | e.m[key] = &val{ 79 | data: value, 80 | expiredTime: expiredTime, 81 | } 82 | e.timeMap[expiredTime] = append(e.timeMap[expiredTime], key) 83 | } 84 | 85 | func (e *ExpiredMap) Get(key interface{}) (found bool, value interface{}) { 86 | e.lck.Lock() 87 | defer e.lck.Unlock() 88 | if found = e.checkDeleteKey(key); !found { 89 | return 90 | } 91 | value = e.m[key].data 92 | return 93 | } 94 | 95 | func (e *ExpiredMap) Delete(key interface{}) { 96 | e.lck.Lock() 97 | delete(e.m, key) 98 | e.lck.Unlock() 99 | } 100 | 101 | func (e *ExpiredMap) Remove(key interface{}) { 102 | e.Delete(key) 103 | } 104 | 105 | func (e *ExpiredMap) multiDelete(keys []interface{}, t int64) { 106 | e.lck.Lock() 107 | defer e.lck.Unlock() 108 | delete(e.timeMap, t) 109 | for _, key := range keys { 110 | delete(e.m, key) 111 | } 112 | } 113 | 114 | func (e *ExpiredMap) Length() int { 115 | e.lck.Lock() 116 | defer e.lck.Unlock() 117 | return len(e.m) 118 | } 119 | 120 | func (e *ExpiredMap) Size() int { 121 | return e.Length() 122 | } 123 | 124 | func (e *ExpiredMap) TTL(key interface{}) int64 { 125 | e.lck.Lock() 126 | defer e.lck.Unlock() 127 | if !e.checkDeleteKey(key) { 128 | return -1 129 | } 130 | return e.m[key].expiredTime - time.Now().Unix() 131 | } 132 | 133 | func (e *ExpiredMap) Clear() { 134 | e.lck.Lock() 135 | defer e.lck.Unlock() 136 | e.m = make(map[interface{}]*val) 137 | e.timeMap = make(map[int64][]interface{}) 138 | } 139 | 140 | func (e *ExpiredMap) Close() { 141 | e.lck.Lock() 142 | defer e.lck.Unlock() 143 | e.stop <- struct{}{} 144 | } 145 | 146 | func (e *ExpiredMap) Stop() { 147 | e.Close() 148 | } 149 | 150 | func (e *ExpiredMap) DoForEach(handler func(interface{}, interface{})) { 151 | e.lck.Lock() 152 | defer e.lck.Unlock() 153 | for k, v := range e.m { 154 | if !e.checkDeleteKey(k) { 155 | continue 156 | } 157 | handler(k, v) 158 | } 159 | } 160 | 161 | func (e *ExpiredMap) DoForEachWithBreak(handler func(interface{}, interface{}) bool) { 162 | e.lck.Lock() 163 | defer e.lck.Unlock() 164 | for k, v := range e.m { 165 | if !e.checkDeleteKey(k) { 166 | continue 167 | } 168 | if handler(k, v) { 169 | break 170 | } 171 | } 172 | } 173 | 174 | func (e *ExpiredMap) checkDeleteKey(key interface{}) bool { 175 | if val, found := e.m[key]; found { 176 | if val.expiredTime <= time.Now().Unix() { 177 | delete(e.m, key) 178 | return false 179 | } 180 | return true 181 | } 182 | return false 183 | } 184 | -------------------------------------------------------------------------------- /examples/b2bua/registry/mem.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/ghettovoice/gosip/sip" 8 | "github.com/ghettovoice/gosip/transport" 9 | ) 10 | 11 | // MemoryRegistry Address-of-Record registry using memory. 12 | type MemoryRegistry struct { 13 | mutex *sync.Mutex 14 | aors map[sip.Uri]map[string]*ContactInstance 15 | } 16 | 17 | func NewMemoryRegistry() *MemoryRegistry { 18 | mr := &MemoryRegistry{ 19 | aors: make(map[sip.Uri]map[string]*ContactInstance), 20 | mutex: new(sync.Mutex), 21 | } 22 | return mr 23 | } 24 | 25 | func (mr *MemoryRegistry) AddAor(aor sip.Uri, instance *ContactInstance) error { 26 | mr.mutex.Lock() 27 | defer mr.mutex.Unlock() 28 | instances, _ := findInstances(mr.aors, aor) 29 | if instances != nil { 30 | (*instances)[instance.Source] = instance 31 | return nil 32 | } else { 33 | mr.aors[aor] = make(map[string]*ContactInstance) 34 | } 35 | mr.aors[aor][instance.Source] = instance 36 | return nil 37 | } 38 | 39 | func (mr *MemoryRegistry) RemoveAor(aor sip.Uri) error { 40 | mr.mutex.Lock() 41 | defer mr.mutex.Unlock() 42 | for key := range mr.aors { 43 | if key.Equals(aor) { 44 | delete(mr.aors, key) 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | func (mr *MemoryRegistry) AorIsRegistered(aor sip.Uri) bool { 51 | mr.mutex.Lock() 52 | defer mr.mutex.Unlock() 53 | _, ok := mr.aors[aor] 54 | return ok 55 | } 56 | 57 | func (mr *MemoryRegistry) UpdateContact(aor sip.Uri, instance *ContactInstance) error { 58 | mr.mutex.Lock() 59 | defer mr.mutex.Unlock() 60 | instances, err := findInstances(mr.aors, aor) 61 | if err != nil { 62 | return err 63 | } 64 | (*instances)[instance.Source] = instance 65 | return nil 66 | } 67 | 68 | func (mr *MemoryRegistry) RemoveContact(aor sip.Uri, instance *ContactInstance) error { 69 | mr.mutex.Lock() 70 | defer mr.mutex.Unlock() 71 | instances, err := findInstances(mr.aors, aor) 72 | if instances != nil { 73 | delete(*instances, instance.Source) 74 | if len(*instances) == 0 { 75 | for key := range mr.aors { 76 | if key.Equals(aor) { 77 | delete(mr.aors, key) 78 | } 79 | } 80 | } 81 | return nil 82 | } 83 | return err 84 | } 85 | 86 | func (mr *MemoryRegistry) HandleConnectionError(connError *transport.ConnectionError) bool { 87 | mr.mutex.Lock() 88 | defer mr.mutex.Unlock() 89 | result := false 90 | for aor, cis := range mr.aors { 91 | for source := range cis { 92 | if source == connError.Source { 93 | delete(cis, source) 94 | result = true 95 | break 96 | } 97 | } 98 | if len(cis) == 0 { 99 | delete(mr.aors, aor) 100 | break 101 | } 102 | } 103 | return result 104 | } 105 | 106 | func (mr *MemoryRegistry) GetContacts(aor sip.Uri) (*map[string]*ContactInstance, bool) { 107 | mr.mutex.Lock() 108 | defer mr.mutex.Unlock() 109 | instance, err := findInstances(mr.aors, aor) 110 | if err != nil { 111 | return nil, false 112 | } 113 | 114 | return instance, true 115 | } 116 | 117 | func (mr *MemoryRegistry) GetAllContacts() map[sip.Uri]map[string]*ContactInstance { 118 | mr.mutex.Lock() 119 | defer mr.mutex.Unlock() 120 | return mr.aors 121 | } 122 | 123 | func findInstances(aors map[sip.Uri]map[string]*ContactInstance, aor sip.Uri) (*map[string]*ContactInstance, error) { 124 | for key, instances := range aors { 125 | if key.User() == aor.User() { 126 | return &instances, nil 127 | } 128 | } 129 | return nil, fmt.Errorf("Not found instances for %v", aor) 130 | } 131 | -------------------------------------------------------------------------------- /examples/b2bua/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "github.com/ghettovoice/gosip/sip" 5 | "github.com/ghettovoice/gosip/transport" 6 | ) 7 | 8 | type ContactInstance struct { 9 | Contact *sip.ContactHeader 10 | RegExpires uint32 11 | LastUpdated uint32 12 | Source string 13 | UserAgent string 14 | Transport string 15 | } 16 | 17 | func (c *ContactInstance) GetPNParams() *PNParams { 18 | params := c.Contact.Address.UriParams() 19 | if provider, ok := params.Get("pn-provider"); ok { 20 | param, _ := params.Get("pn-param") 21 | prid, found := params.Get("pn-prid") 22 | if !found || prid == nil { 23 | prid = sip.String{Str: ""} 24 | } 25 | pn := &PNParams{ 26 | Provider: provider.String(), 27 | Param: param.String(), 28 | PRID: prid.String(), 29 | } 30 | return pn 31 | } 32 | return nil 33 | } 34 | 35 | func NewContactInstanceForRequest(request sip.Request) *ContactInstance { 36 | headers := request.GetHeaders("Expires") 37 | var expires sip.Expires = 0 38 | if len(headers) > 0 { 39 | expires = *headers[0].(*sip.Expires) 40 | } 41 | contacts, _ := request.Contact() 42 | userAgent := request.GetHeaders("User-Agent")[0].(*sip.UserAgentHeader) 43 | instance := &ContactInstance{ 44 | Source: request.Source(), 45 | RegExpires: uint32(expires), 46 | Contact: contacts.Clone().(*sip.ContactHeader), 47 | UserAgent: userAgent.String(), 48 | Transport: request.Transport(), 49 | } 50 | return instance 51 | } 52 | 53 | // Registry Address-of-Record registry 54 | type Registry interface { 55 | AddAor(aor sip.Uri, instance *ContactInstance) error 56 | RemoveAor(aor sip.Uri) error 57 | AorIsRegistered(aor sip.Uri) bool 58 | UpdateContact(aor sip.Uri, instance *ContactInstance) error 59 | RemoveContact(aor sip.Uri, instance *ContactInstance) error 60 | GetContacts(aor sip.Uri) (*map[string]*ContactInstance, bool) 61 | GetAllContacts() map[sip.Uri]map[string]*ContactInstance 62 | HandleConnectionError(connError *transport.ConnectionError) bool 63 | } 64 | -------------------------------------------------------------------------------- /examples/b2bua/registry/rfc8599.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/ghettovoice/gosip/sip" 9 | ) 10 | 11 | const ( 12 | DefaultPNTimeout = 30 // s 13 | ) 14 | 15 | type PNParams struct { 16 | Provider string // PNS Provider (apns|fcm|other) 17 | Param string 18 | PRID string 19 | PURR string 20 | Expires uint32 //TODO: 21 | } 22 | 23 | func (p *PNParams) String() string { 24 | return "pn-provider: " + p.Provider + ", pn-param: " + p.Param + ", pn-prid: " + p.PRID 25 | } 26 | 27 | func (p *PNParams) Equals(other *PNParams) bool { 28 | return p.Provider == other.Provider && p.Param == other.Param && p.PRID == other.PRID 29 | } 30 | 31 | //Disabled https://tools.ietf.org/html/rfc8599#section-4.1.2 32 | func (p *PNParams) Disabled() bool { 33 | return len(p.PRID) == 0 34 | } 35 | 36 | type PushCallback func(pn *PNParams, payload map[string]string) error 37 | 38 | type RFC8599 struct { 39 | PushCallback PushCallback 40 | records map[PNParams]sip.Uri 41 | pushers map[PNParams]*Pusher 42 | } 43 | 44 | func NewRFC8599(callback PushCallback) *RFC8599 { 45 | rfc := &RFC8599{ 46 | PushCallback: callback, 47 | records: make(map[PNParams]sip.Uri), 48 | pushers: make(map[PNParams]*Pusher), 49 | } 50 | return rfc 51 | } 52 | 53 | func (r *RFC8599) PNRecords() map[PNParams]sip.Uri { 54 | return r.records 55 | } 56 | 57 | func (r *RFC8599) HandleContactInstance(aor sip.Uri, instance *ContactInstance) { 58 | pn := instance.GetPNParams() 59 | if pn != nil { 60 | disable := pn.Disabled() 61 | if disable { 62 | //Remove pn record. 63 | for params, uri := range r.records { 64 | if uri.User() == aor.User() { 65 | delete(r.records, params) 66 | } 67 | } 68 | 69 | return 70 | } 71 | 72 | // Add pn record. 73 | if _, ok := r.records[*pn]; !ok { 74 | r.records[*pn] = aor 75 | } 76 | 77 | //for params, aor := range r.records { 78 | // fmt.Printf("\n\n\naor %v => params %v\n\n\n", aor, params.String()) 79 | //} 80 | 81 | for params, pusher := range r.pushers { 82 | if params.Equals(pn) { 83 | pusher.CH <- instance 84 | delete(r.pushers, params) 85 | break 86 | } 87 | } 88 | } 89 | } 90 | 91 | func (r *RFC8599) TryPush(aor sip.Uri, from *sip.FromHeader) (*Pusher, bool) { 92 | for params, uri := range r.records { 93 | 94 | if uri.User() == aor.User() { 95 | displayName := "" 96 | if from.DisplayName != nil { 97 | displayName = from.DisplayName.String() 98 | } 99 | payload := map[string]string{ 100 | "caller_id": from.Address.User().String(), 101 | "caller_name": displayName, 102 | "caller_id_type": "number", 103 | "has_video": "false", 104 | } 105 | 106 | if err := r.PushCallback(¶ms, payload); err != nil { 107 | //push failed,. 108 | log.Printf("Push failed: %v", err) 109 | return nil, false 110 | } 111 | pusher := NewPusher() 112 | r.pushers[params] = pusher 113 | return pusher, true 114 | } 115 | } 116 | return nil, false 117 | } 118 | 119 | type Pusher struct { 120 | CH chan *ContactInstance 121 | abort chan int 122 | } 123 | 124 | func NewPusher() *Pusher { 125 | pn := &Pusher{ 126 | CH: make(chan *ContactInstance, 1), 127 | abort: make(chan int, 1), 128 | } 129 | return pn 130 | } 131 | 132 | func (pn *Pusher) WaitContactOnline() (*ContactInstance, error) { 133 | t := time.NewTicker(time.Second * time.Duration(DefaultPNTimeout)) 134 | for { 135 | select { 136 | case <-pn.abort: 137 | return nil, fmt.Errorf("Abort") 138 | case <-t.C: 139 | return nil, fmt.Errorf("Timeout") 140 | case instance := <-pn.CH: 141 | return instance, nil 142 | } 143 | } 144 | } 145 | 146 | //Abort caller cancelled the call 147 | func (pn *Pusher) Abort() { 148 | pn.abort <- 1 149 | } 150 | -------------------------------------------------------------------------------- /examples/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/cloudwebrtc/go-sip-ua/examples/mock" 11 | "github.com/cloudwebrtc/go-sip-ua/pkg/account" 12 | "github.com/cloudwebrtc/go-sip-ua/pkg/media/rtp" 13 | "github.com/cloudwebrtc/go-sip-ua/pkg/session" 14 | "github.com/cloudwebrtc/go-sip-ua/pkg/stack" 15 | "github.com/cloudwebrtc/go-sip-ua/pkg/ua" 16 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 17 | "github.com/ghettovoice/gosip/log" 18 | "github.com/ghettovoice/gosip/sip" 19 | "github.com/ghettovoice/gosip/sip/parser" 20 | ) 21 | 22 | var ( 23 | logger log.Logger 24 | udp *rtp.RtpUDPStream 25 | ) 26 | 27 | func init() { 28 | logger = utils.NewLogrusLogger(log.DebugLevel, "Client", nil) 29 | } 30 | 31 | func createUdp() *rtp.RtpUDPStream { 32 | 33 | udp = rtp.NewRtpUDPStream("127.0.0.1", rtp.DefaultPortMin, rtp.DefaultPortMax, func(data []byte, raddr net.Addr) { 34 | logger.Infof("Rtp recevied: %v, laddr %s : raddr %s", len(data), udp.LocalAddr().String(), raddr) 35 | dest, _ := net.ResolveUDPAddr(raddr.Network(), raddr.String()) 36 | logger.Infof("Echo rtp to %v", raddr) 37 | udp.Send(data, dest) 38 | }) 39 | 40 | go udp.Read() 41 | 42 | return udp 43 | } 44 | 45 | func main() { 46 | stop := make(chan os.Signal, 1) 47 | signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) 48 | stack := stack.NewSipStack(&stack.SipStackConfig{ 49 | UserAgent: "Go Sip Client/example-client", 50 | Extensions: []string{"replaces", "outbound"}, 51 | Dns: "8.8.8.8"}) 52 | 53 | listen := "0.0.0.0:5080" 54 | logger.Infof("Listen => %s", listen) 55 | 56 | if err := stack.Listen("udp", listen); err != nil { 57 | logger.Panic(err) 58 | } 59 | 60 | if err := stack.Listen("tcp", listen); err != nil { 61 | logger.Panic(err) 62 | } 63 | 64 | if err := stack.ListenTLS("wss", "0.0.0.0:5091", nil); err != nil { 65 | logger.Panic(err) 66 | } 67 | 68 | ua := ua.NewUserAgent(&ua.UserAgentConfig{ 69 | SipStack: stack, 70 | }) 71 | 72 | ua.InviteStateHandler = func(sess *session.Session, req *sip.Request, resp *sip.Response, state session.Status) { 73 | logger.Infof("InviteStateHandler: state => %v, type => %s", state, sess.Direction()) 74 | 75 | switch state { 76 | case session.InviteReceived: 77 | udp = createUdp() 78 | udpLaddr := udp.LocalAddr() 79 | sdp := mock.BuildLocalSdp(udpLaddr.IP.String(), udpLaddr.Port) 80 | sess.ProvideAnswer(sdp) 81 | sess.Accept(200) 82 | case session.Canceled: 83 | fallthrough 84 | case session.Failure: 85 | fallthrough 86 | case session.Terminated: 87 | udp.Close() 88 | } 89 | } 90 | 91 | ua.RegisterStateHandler = func(state account.RegisterState) { 92 | logger.Infof("RegisterStateHandler: user => %s, state => %v, expires => %v", state.Account.AuthInfo.AuthUser, state.StatusCode, state.Expiration) 93 | } 94 | 95 | uri, err := parser.ParseUri("sip:100@127.0.0.1") 96 | if err != nil { 97 | logger.Error(err) 98 | } 99 | 100 | profile := account.NewProfile(uri.Clone(), "goSIP/example-client", 101 | &account.AuthInfo{ 102 | AuthUser: "100", 103 | Password: "100", 104 | Realm: "", 105 | }, 106 | 1800, 107 | stack, 108 | ) 109 | 110 | recipient, err := parser.ParseSipUri("sip:100@127.0.0.1:5081;transport=wss") 111 | if err != nil { 112 | logger.Error(err) 113 | } 114 | 115 | register, _ := ua.SendRegister(profile, recipient, profile.Expires, nil) 116 | time.Sleep(time.Second * 3) 117 | 118 | udp = createUdp() 119 | udpLaddr := udp.LocalAddr() 120 | sdp := mock.BuildLocalSdp(udpLaddr.IP.String(), udpLaddr.Port) 121 | 122 | called, err2 := parser.ParseUri("sip:400@127.0.0.1") 123 | if err2 != nil { 124 | logger.Error(err) 125 | } 126 | 127 | recipient, err = parser.ParseSipUri("sip:400@127.0.0.1:5081;transport=wss") 128 | if err != nil { 129 | logger.Error(err) 130 | } 131 | 132 | go ua.Invite(profile, called, recipient, &sdp) 133 | 134 | <-stop 135 | 136 | register.SendRegister(0) 137 | 138 | ua.Shutdown() 139 | } 140 | -------------------------------------------------------------------------------- /examples/mock/sdp.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pixelbender/go-sdp/sdp" 7 | ) 8 | 9 | var ( 10 | host = "127.0.0.1" 11 | Offer *sdp.Session 12 | Answer *sdp.Session 13 | ) 14 | 15 | func init() { 16 | Offer = &sdp.Session{ 17 | Origin: &sdp.Origin{ 18 | Username: "-", 19 | Address: host, 20 | SessionID: time.Now().UnixNano() / 1e6, 21 | SessionVersion: time.Now().UnixNano() / 1e6, 22 | }, 23 | Timing: &sdp.Timing{Start: time.Time{}, Stop: time.Time{}}, 24 | //Name: "Example", 25 | Connection: &sdp.Connection{ 26 | Address: host, 27 | }, 28 | //Bandwidth: []*sdp.Bandwidth{{Type: "AS", Value: 117}}, 29 | Media: []*sdp.Media{ 30 | { 31 | //Bandwidth: []*sdp.Bandwidth{{Type: "TIAS", Value: 96000}}, 32 | Connection: []*sdp.Connection{{Address: host}}, 33 | Mode: sdp.SendRecv, 34 | Type: "audio", 35 | Port: 4008, 36 | Proto: "RTP/AVP", 37 | Format: []*sdp.Format{ 38 | {Payload: 8, Name: "PCMA", ClockRate: 8000}, 39 | {Payload: 18, Name: "G729", ClockRate: 8000, Params: []string{"annexb=yes"}}, 40 | {Payload: 106, Name: "telephone-event", ClockRate: 8000, Params: []string{"0-16"}}, 41 | }, 42 | }, 43 | }, 44 | } 45 | Answer = Offer 46 | } 47 | 48 | func BuildLocalSdp(host string, port int) string { 49 | sdp := &sdp.Session{ 50 | Origin: &sdp.Origin{ 51 | Username: "-", 52 | Address: host, 53 | SessionID: time.Now().UnixNano() / 1e6, 54 | SessionVersion: time.Now().UnixNano() / 1e6, 55 | }, 56 | Timing: &sdp.Timing{Start: time.Time{}, Stop: time.Time{}}, 57 | //Name: "Example", 58 | Connection: &sdp.Connection{ 59 | Address: host, 60 | }, 61 | //Bandwidth: []*sdp.Bandwidth{{Type: "AS", Value: 117}}, 62 | Media: []*sdp.Media{ 63 | { 64 | //Bandwidth: []*sdp.Bandwidth{{Type: "TIAS", Value: 96000}}, 65 | Connection: []*sdp.Connection{{Address: host}}, 66 | Mode: sdp.SendRecv, 67 | Type: "audio", 68 | Port: port, 69 | Proto: "RTP/AVP", 70 | Format: []*sdp.Format{ 71 | {Payload: 0, Name: "PCMU", ClockRate: 8000}, 72 | {Payload: 8, Name: "PCMA", ClockRate: 8000}, 73 | //{Payload: 18, Name: "G729", ClockRate: 8000, Params: []string{"annexb=yes"}}, 74 | {Payload: 106, Name: "telephone-event", ClockRate: 8000, Params: []string{"0-16"}}, 75 | }, 76 | }, 77 | }, 78 | } 79 | return sdp.String() 80 | } 81 | 82 | func GetRemoteIpPort(sdp *sdp.Session) (string, int) { 83 | return sdp.Connection.Address, sdp.Media[0].Port 84 | } 85 | -------------------------------------------------------------------------------- /examples/register/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | "time" 8 | 9 | "github.com/cloudwebrtc/go-sip-ua/pkg/account" 10 | "github.com/cloudwebrtc/go-sip-ua/pkg/media/rtp" 11 | "github.com/cloudwebrtc/go-sip-ua/pkg/stack" 12 | "github.com/cloudwebrtc/go-sip-ua/pkg/ua" 13 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 14 | "github.com/ghettovoice/gosip/log" 15 | "github.com/ghettovoice/gosip/sip/parser" 16 | ) 17 | 18 | var ( 19 | logger log.Logger 20 | udp *rtp.RtpUDPStream 21 | ) 22 | 23 | func init() { 24 | logger = utils.NewLogrusLogger(log.DebugLevel, "Register", nil) 25 | } 26 | 27 | func main() { 28 | 29 | stop := make(chan os.Signal, 1) 30 | signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) 31 | stack := stack.NewSipStack(&stack.SipStackConfig{ 32 | UserAgent: "Go Sip Client/example-register", 33 | Extensions: []string{"replaces", "outbound"}, 34 | Dns: "8.8.8.8"}) 35 | 36 | if err := stack.Listen("udp", "0.0.0.0:5066"); err != nil { 37 | logger.Panic(err) 38 | } 39 | 40 | ua := ua.NewUserAgent(&ua.UserAgentConfig{ 41 | SipStack: stack, 42 | }) 43 | 44 | ua.RegisterStateHandler = func(state account.RegisterState) { 45 | logger.Infof("RegisterStateHandler: user => %s, state => %v, expires => %v, reason => %v", state.Account.AuthInfo.AuthUser, state.StatusCode, state.Expiration, state.Reason) 46 | } 47 | 48 | uri, err := parser.ParseUri("sip:100@127.0.0.1") // this acts as an identifier, not connection info 49 | if err != nil { 50 | logger.Error(err) 51 | } 52 | 53 | profile := account.NewProfile(uri.Clone(), "goSIP", 54 | &account.AuthInfo{ 55 | AuthUser: "100", 56 | Password: "100", 57 | Realm: "b2bua", 58 | }, 59 | 1800, 60 | stack, 61 | ) 62 | 63 | recipient, err := parser.ParseSipUri("sip:100@127.0.0.1;transport=udp") // this is the remote address 64 | if err != nil { 65 | logger.Error(err) 66 | } 67 | 68 | register, err := ua.SendRegister(profile, recipient, profile.Expires, nil) 69 | if err != nil { 70 | logger.Error(err) 71 | } 72 | 73 | time.Sleep(time.Second * 5) 74 | 75 | register.SendRegister(0) 76 | 77 | time.Sleep(time.Second * 5) 78 | 79 | register.SendRegister(300) 80 | 81 | time.Sleep(time.Second * 5) 82 | 83 | register.SendRegister(0) 84 | 85 | <-stop 86 | 87 | ua.Shutdown() 88 | } 89 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudwebrtc/go-sip-ua 2 | 3 | go 1.13 4 | 5 | require ( 6 | firebase.google.com/go v3.13.0+incompatible 7 | github.com/c-bata/go-prompt v0.2.6 8 | github.com/ghettovoice/gosip v0.0.0-20230322091832-d77de1c97f89 9 | github.com/google/uuid v1.3.0 10 | github.com/pixelbender/go-sdp v1.1.0 11 | github.com/sirupsen/logrus v1.9.0 12 | github.com/tevino/abool v1.2.0 13 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 14 | golang.org/x/crypto v0.7.0 15 | google.golang.org/api v0.114.0 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/account/profile.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudwebrtc/go-sip-ua/pkg/stack" 7 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 8 | "github.com/ghettovoice/gosip/log" 9 | "github.com/ghettovoice/gosip/sip" 10 | "github.com/ghettovoice/gosip/sip/parser" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | var ( 15 | logger log.Logger 16 | ) 17 | 18 | func init() { 19 | logger = utils.NewLogrusLogger(log.DebugLevel, "UserAgent", nil) 20 | } 21 | 22 | //AuthInfo . 23 | type AuthInfo struct { 24 | AuthUser string 25 | Realm string 26 | Password string 27 | Ha1 string 28 | } 29 | 30 | // Profile . 31 | type Profile struct { 32 | URI sip.Uri 33 | DisplayName string 34 | AuthInfo *AuthInfo 35 | Expires uint32 36 | InstanceID string 37 | Routes []sip.Uri 38 | ContactURI sip.Uri 39 | ContactParams map[string]string 40 | } 41 | 42 | // Contact . 43 | func (p *Profile) Contact() *sip.Address { 44 | var uri sip.Uri 45 | if p.ContactURI != nil { 46 | uri = p.ContactURI 47 | } else { 48 | uri = p.URI.Clone() 49 | } 50 | 51 | contact := &sip.Address{ 52 | Uri: uri, 53 | Params: sip.NewParams(), 54 | } 55 | if p.InstanceID != "nil" { 56 | contact.Params.Add("+sip.instance", sip.String{Str: p.InstanceID}) 57 | } 58 | 59 | for key, value := range p.ContactParams { 60 | contact.Params.Add(key, sip.String{Str: value}) 61 | } 62 | 63 | //TODO: Add more necessary parameters. 64 | //etc: ip:port, transport=udp|tcp, +sip.ice, +sip.instance, +sip.pnsreg, 65 | 66 | return contact 67 | } 68 | 69 | //NewProfile . 70 | func NewProfile( 71 | uri sip.Uri, 72 | displayName string, 73 | authInfo *AuthInfo, 74 | expires uint32, 75 | stack *stack.SipStack, 76 | ) *Profile { 77 | p := &Profile{ 78 | URI: uri, 79 | DisplayName: displayName, 80 | AuthInfo: authInfo, 81 | Expires: expires, 82 | } 83 | if stack != nil { // populate the Contact field 84 | var transport string 85 | if tp, ok := uri.UriParams().Get("transport"); ok { 86 | transport = tp.String() 87 | } else { 88 | transport = "udp" 89 | } 90 | addr := stack.GetNetworkInfo(transport) 91 | uri, err := parser.ParseUri(fmt.Sprintf("sip:%s@%s;transport=%s", p.URI.User(), addr.Addr(), transport)) 92 | if err == nil { 93 | p.ContactURI = uri 94 | } else { 95 | logger.Errorf("Error parsing contact URI: %s", err) 96 | } 97 | } 98 | 99 | uid, err := uuid.NewUUID() 100 | if err != nil { 101 | logger.Errorf("could not create UUID: %v", err) 102 | } 103 | p.InstanceID = fmt.Sprintf(`"<%s>"`, uid.URN()) 104 | return p 105 | } 106 | 107 | //RegisterState . 108 | type RegisterState struct { 109 | Account *Profile 110 | StatusCode sip.StatusCode 111 | Reason string 112 | Expiration uint32 113 | Response sip.Response 114 | UserData interface{} 115 | } 116 | -------------------------------------------------------------------------------- /pkg/auth/client.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/ghettovoice/gosip/sip" 9 | ) 10 | 11 | // currently only Digest and MD5 12 | type Authorization struct { 13 | realm string 14 | qop string 15 | nonce string 16 | cnonce string 17 | opaque string 18 | algorithm string 19 | username string 20 | password string 21 | uri string 22 | response string 23 | method string 24 | stale string 25 | nc int 26 | ncHex string 27 | domain string 28 | other map[string]string 29 | } 30 | 31 | func AuthFromValue(value string) *Authorization { 32 | auth := &Authorization{ 33 | algorithm: "MD5", 34 | cnonce: generateNonce(12), 35 | opaque: "", 36 | stale: "", 37 | qop: "", 38 | nc: 0, 39 | ncHex: "00000000", 40 | domain: "", 41 | other: make(map[string]string), 42 | } 43 | 44 | re := regexp.MustCompile(`([\w]+)=("([^"]+)"|([\w]+))`) 45 | matches := re.FindAllStringSubmatch(value, -1) 46 | for _, match := range matches { 47 | value2 := strings.Replace(match[2], "\"", "", -1) 48 | switch match[1] { 49 | case "qop": 50 | auth.qop = strings.Split(value2, ",")[0] 51 | case "realm": 52 | auth.realm = value2 53 | case "algorithm": 54 | auth.algorithm = value2 55 | case "opaque": 56 | auth.opaque = value2 57 | case "nonce": 58 | auth.nonce = value2 59 | case "stale": 60 | auth.stale = value2 61 | case "domain": 62 | auth.domain = value2 63 | default: 64 | auth.other[match[1]] = value2 65 | } 66 | } 67 | 68 | return auth 69 | } 70 | 71 | func (auth *Authorization) SetUsername(username string) *Authorization { 72 | auth.username = username 73 | 74 | return auth 75 | } 76 | 77 | func (auth *Authorization) SetUri(uri string) *Authorization { 78 | auth.uri = uri 79 | 80 | return auth 81 | } 82 | 83 | func (auth *Authorization) SetMethod(method string) *Authorization { 84 | auth.method = method 85 | 86 | return auth 87 | } 88 | 89 | func (auth *Authorization) SetPassword(password string) *Authorization { 90 | auth.password = password 91 | 92 | return auth 93 | } 94 | 95 | // calculates Authorization response https://www.ietf.org/rfc/rfc2617.txt 96 | func (auth *Authorization) CalcResponse(request sip.Request) *Authorization { 97 | auth.nc += 1 98 | hex := fmt.Sprintf("%x", auth.nc) 99 | ncHex := "00000000" 100 | auth.ncHex = ncHex[:len(ncHex)-len(hex)] + hex 101 | // Nc-value = 8LHEX. Max value = 'FFFFFFFF'. 102 | if temp_nc := int64(auth.nc); temp_nc == 4294967296 { 103 | auth.nc = 1 104 | auth.ncHex = "00000001" 105 | } 106 | // HA1 = MD5(A1) = MD5(username:realm:password). 107 | ha1 := md5Hex(auth.username + ":" + auth.realm + ":" + auth.password) 108 | if auth.qop == "auth" { 109 | // HA2 = MD5(A2) = MD5(method:digestURI). 110 | ha2 := md5Hex(auth.method + ":" + auth.uri) 111 | // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2). 112 | auth.response = md5Hex(ha1 + ":" + auth.nonce + ":" + auth.ncHex + ":" + auth.cnonce + ":auth:" + ha2) 113 | } else if auth.qop == "auth-int" { 114 | // HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody)). 115 | ha2 := md5Hex(auth.method + ":" + auth.uri + ":" + md5Hex(request.Body())) 116 | // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2). 117 | auth.response = md5Hex(ha1 + ":" + auth.nonce + ":" + auth.ncHex + ":" + auth.cnonce + ":auth-int:" + ha2) 118 | } else { 119 | // HA2 = MD5(A2) = MD5(method:digestURI). 120 | ha2 := md5Hex(auth.method + ":" + auth.uri) 121 | // Response = MD5(HA1:nonce:HA2). 122 | auth.response = md5Hex(ha1 + ":" + auth.nonce + ":" + ha2) 123 | } 124 | return auth 125 | } 126 | 127 | func (auth *Authorization) String() string { 128 | digest := fmt.Sprintf( 129 | `Digest realm="%s",algorithm=%s,nonce="%s",username="%s",uri="%s",response="%s"`, 130 | auth.realm, 131 | auth.algorithm, 132 | auth.nonce, 133 | auth.username, 134 | auth.uri, 135 | auth.response, 136 | ) 137 | 138 | if auth.domain != "" { 139 | digest += fmt.Sprintf(`,domain="%s"`, auth.domain) 140 | } 141 | 142 | if auth.opaque != "" { 143 | digest += fmt.Sprintf(`,opaque="%s"`, auth.opaque) 144 | } 145 | 146 | if auth.qop != "" { 147 | digest += fmt.Sprintf(`,qop="%s"`, auth.qop) 148 | digest += fmt.Sprintf(`,cnonce="%s"`, auth.cnonce) 149 | digest += fmt.Sprintf(`,nc="%s"`, auth.ncHex) 150 | } 151 | 152 | if len(auth.stale) > 0 { 153 | digest += fmt.Sprintf(`,stale=%s`, auth.stale) 154 | } 155 | 156 | return digest 157 | } 158 | 159 | func AuthorizeRequest(request sip.Request, response sip.Response, user, password sip.MaybeString) error { 160 | if user == nil { 161 | return fmt.Errorf("authorize request: user is nil") 162 | } 163 | 164 | var authenticateHeaderName, authorizeHeaderName string 165 | if response.StatusCode() == 401 { 166 | // on 401 Unauthorized increase request seq num, add Authorization header and send once again 167 | authenticateHeaderName = "WWW-Authenticate" 168 | authorizeHeaderName = "Authorization" 169 | } else { 170 | // 407 Proxy authentication 171 | authenticateHeaderName = "Proxy-Authenticate" 172 | authorizeHeaderName = "Proxy-Authorization" 173 | } 174 | 175 | if hdrs := response.GetHeaders(authenticateHeaderName); len(hdrs) > 0 { 176 | authenticateHeader := hdrs[0].(*sip.GenericHeader) 177 | auth := AuthFromValue(authenticateHeader.Contents). 178 | SetMethod(string(request.Method())). 179 | SetUri(request.Recipient().String()). 180 | SetUsername(user.String()) 181 | 182 | if password != nil { 183 | auth.SetPassword(password.String()) 184 | } 185 | 186 | auth.CalcResponse(request) 187 | 188 | var authorizationHeader *sip.GenericHeader 189 | hdrs = request.GetHeaders(authorizeHeaderName) 190 | if len(hdrs) > 0 { 191 | authorizationHeader = hdrs[0].(*sip.GenericHeader) 192 | authorizationHeader.Contents = auth.String() 193 | } else { 194 | authorizationHeader = &sip.GenericHeader{ 195 | HeaderName: authorizeHeaderName, 196 | Contents: auth.String(), 197 | } 198 | request.AppendHeader(authorizationHeader) 199 | } 200 | } else { 201 | return fmt.Errorf("authorize request: header '%s' not found in response", authenticateHeaderName) 202 | } 203 | 204 | if viaHop, ok := request.ViaHop(); ok { 205 | viaHop.Params.Add("branch", sip.String{Str: sip.GenerateBranch()}) 206 | } 207 | 208 | if cseq, ok := request.CSeq(); ok { 209 | cseq.SeqNo++ 210 | } 211 | 212 | return nil 213 | } 214 | 215 | type Authorizer interface { 216 | AuthorizeRequest(request sip.Request, response sip.Response) error 217 | } 218 | 219 | type ClientAuthorizer struct { 220 | user sip.MaybeString 221 | password sip.MaybeString 222 | } 223 | 224 | func NewClientAuthorizer(u string, p string) *ClientAuthorizer { 225 | auth := &ClientAuthorizer{ 226 | user: sip.String{Str: u}, 227 | password: sip.String{Str: p}, 228 | } 229 | return auth 230 | } 231 | 232 | func (auth *ClientAuthorizer) AuthorizeRequest(request sip.Request, response sip.Response) error { 233 | return AuthorizeRequest(request, response, auth.user, auth.password) 234 | } 235 | -------------------------------------------------------------------------------- /pkg/auth/server.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "math/rand" 7 | "regexp" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 13 | "github.com/ghettovoice/gosip/log" 14 | "github.com/ghettovoice/gosip/sip" 15 | ) 16 | 17 | const ( 18 | NonceExpire = 180 * time.Second 19 | ) 20 | 21 | var ( 22 | logger log.Logger 23 | ) 24 | 25 | // AuthSession . 26 | type AuthSession struct { 27 | nonce string 28 | created time.Time 29 | } 30 | 31 | type RequestCredentialCallback func(username string) (password string, ha1 string, err error) 32 | 33 | // ServerAuthorizer Proxy-Authorization | WWW-Authenticate 34 | type ServerAuthorizer struct { 35 | // a map[call id]authSession pair 36 | sessions map[string]AuthSession 37 | requestCredential RequestCredentialCallback 38 | useAuthInt bool 39 | realm string 40 | log log.Logger 41 | 42 | mx sync.RWMutex 43 | } 44 | 45 | // NewServerAuthorizer . 46 | func NewServerAuthorizer(callback RequestCredentialCallback, realm string, authInt bool) *ServerAuthorizer { 47 | auth := &ServerAuthorizer{ 48 | sessions: make(map[string]AuthSession), 49 | requestCredential: callback, 50 | useAuthInt: authInt, 51 | realm: realm, 52 | } 53 | auth.log = utils.NewLogrusLogger(log.DebugLevel, "ServerAuthorizer", nil) 54 | go func() { 55 | for now := range time.Tick(NonceExpire) { 56 | auth.mx.Lock() 57 | for k, v := range auth.sessions { 58 | if now.After(v.created.Add(180 * time.Second)) { 59 | delete(auth.sessions, k) 60 | } 61 | } 62 | auth.mx.Unlock() 63 | } 64 | }() 65 | return auth 66 | } 67 | 68 | // ServerAuthorizer handles Authenticate requests. 69 | func (auth *ServerAuthorizer) Authenticate(request sip.Request, tx sip.ServerTransaction) (string, bool) { 70 | logger := auth.log 71 | logger.Debugf("Request => %s", request.Short()) 72 | 73 | from, _ := request.From() 74 | 75 | /* 76 | TODO: check domain 77 | to, _ := request.To() 78 | if to.Address.Host() != from.Address.Host() { 79 | sendResponse(request, tx, 400, "User in To and From fields do not match.") 80 | return "", false 81 | } 82 | */ 83 | 84 | hdrs := request.GetHeaders("Authorization") 85 | if len(hdrs) == 0 { 86 | auth.requestAuthentication(request, tx, from) 87 | return "", false 88 | } 89 | 90 | authenticateHeader := hdrs[0].(*sip.GenericHeader) 91 | authArgs := parseAuthHeader(authenticateHeader.Contents) 92 | return auth.checkAuthorization(request, tx, authArgs, from) 93 | } 94 | 95 | func (auth *ServerAuthorizer) requestAuthentication(request sip.Request, tx sip.ServerTransaction, from *sip.FromHeader) { 96 | callID, ok := request.CallID() 97 | if !ok { 98 | sendResponse(request, tx, 400, "Missing required Call-ID header.") 99 | return 100 | } 101 | 102 | response := sip.NewResponseFromRequest(request.MessageID(), request, 401, "Unauthorized", "") 103 | nonce := generateNonce(8) 104 | opaque := generateNonce(4) 105 | 106 | digest := sip.NewParams() 107 | digest.Add("realm", sip.String{Str: "\"" + auth.realm + "\""}) 108 | if auth.useAuthInt { 109 | digest.Add("qop", sip.String{Str: "\"auth,auth-int\""}) 110 | } else { 111 | digest.Add("qop", sip.String{Str: "\"auth\""}) 112 | } 113 | digest.Add("nonce", sip.String{Str: "\"" + nonce + "\""}) 114 | digest.Add("opaque", sip.String{Str: "\"" + opaque + "\""}) 115 | digest.Add("stale", sip.String{Str: "\"false\""}) 116 | digest.Add("algorithm", sip.String{Str: "\"md5\""}) 117 | 118 | response.AppendHeader(&sip.GenericHeader{ 119 | HeaderName: "WWW-Authenticate", 120 | Contents: "Digest " + digest.ToString(','), 121 | }) 122 | 123 | from.Params.Add("tag", sip.String{Str: generateNonce(8)}) 124 | auth.mx.Lock() 125 | auth.sessions[callID.String()] = AuthSession{ 126 | nonce: nonce, 127 | created: time.Now(), 128 | } 129 | auth.mx.Unlock() 130 | response.SetBody("", true) 131 | tx.Respond(response) 132 | } 133 | 134 | func (auth *ServerAuthorizer) checkAuthorization(request sip.Request, tx sip.ServerTransaction, 135 | authArgs sip.Params, from *sip.FromHeader) (string, bool) { 136 | callID, ok := request.CallID() 137 | if !ok { 138 | sendResponse(request, tx, 400, "Missing required Call-ID header.") 139 | return "", false 140 | } 141 | 142 | auth.mx.RLock() 143 | session, found := auth.sessions[callID.String()] 144 | auth.mx.RUnlock() 145 | if !found { 146 | auth.requestAuthentication(request, tx, from) 147 | return "", false 148 | } 149 | 150 | if time.Now().After(session.created.Add(NonceExpire)) { 151 | auth.requestAuthentication(request, tx, from) 152 | return "", false 153 | } 154 | 155 | if username, ok := authArgs.Get("username"); ok && username.String() != from.Address.User().String() { 156 | auth.requestAuthentication(request, tx, from) 157 | return "", false 158 | } 159 | 160 | if nonce, ok := authArgs.Get("nonce"); ok && nonce.String() != session.nonce { 161 | auth.requestAuthentication(request, tx, from) 162 | return "", false 163 | } 164 | 165 | username := from.Address.User().String() 166 | password, ha1, err := auth.requestCredential(username) 167 | if err != nil { 168 | sendResponse(request, tx, 404, "User not found") 169 | return "", false 170 | } 171 | 172 | uri, _ := authArgs.Get("uri") 173 | nc, _ := authArgs.Get("nc") 174 | cnonce, _ := authArgs.Get("cnonce") 175 | response, _ := authArgs.Get("response") 176 | qop, _ := authArgs.Get("qop") 177 | realm, _ := authArgs.Get("realm") 178 | 179 | result := "" 180 | 181 | // HA1 = MD5(A1) = MD5(username:realm:password). 182 | if len(ha1) == 0 { 183 | ha1 = md5Hex(username + ":" + realm.String() + ":" + password) 184 | } 185 | 186 | if qop != nil && qop.String() == "auth" { 187 | // HA2 = MD5(A2) = MD5(method:digestURI). 188 | ha2 := md5Hex(string(request.Method()) + ":" + uri.String()) 189 | 190 | // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2). 191 | result = md5Hex(ha1 + ":" + session.nonce + ":" + nc.String() + 192 | ":" + cnonce.String() + ":auth:" + ha2) 193 | } else if qop != nil && qop.String() == "auth-int" { 194 | // HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody)). 195 | ha2 := md5Hex(string(request.Method()) + ":" + uri.String() + ":" + md5Hex(request.Body())) 196 | 197 | // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2). 198 | result = md5Hex(ha1 + ":" + session.nonce + ":" + nc.String() + 199 | ":" + cnonce.String() + ":auth-int:" + ha2) 200 | } else { 201 | // HA2 = MD5(A2) = MD5(method:digestURI). 202 | ha2 := md5Hex(string(request.Method()) + ":" + uri.String()) 203 | 204 | // Response = MD5(HA1:nonce:HA2). 205 | result = md5Hex(ha1 + ":" + session.nonce + ":" + ha2) 206 | } 207 | 208 | if result != response.String() { 209 | sendResponse(request, tx, 403, "Forbidden (Bad auth)") 210 | return "", false 211 | } 212 | 213 | return username, true 214 | } 215 | 216 | // parseAuthHeader . 217 | func parseAuthHeader(value string) sip.Params { 218 | authArgs := sip.NewParams() 219 | re := regexp.MustCompile(`([\w]+)=("([^"]+)"|([\w]+))`) 220 | matches := re.FindAllStringSubmatch(value, -1) 221 | for _, match := range matches { 222 | authArgs.Add(match[1], sip.String{Str: strings.Replace(match[2], "\"", "", -1)}) 223 | } 224 | return authArgs 225 | } 226 | 227 | func generateNonce(size int) string { 228 | bytes := make([]byte, size) 229 | _, err := rand.Read(bytes) 230 | if err != nil { 231 | panic(err) 232 | } 233 | return hex.EncodeToString(bytes) 234 | } 235 | 236 | func md5Hex(data string) string { 237 | sum := md5.Sum([]byte(data)) 238 | return hex.EncodeToString(sum[:]) 239 | } 240 | 241 | // sendResponse . 242 | func sendResponse(request sip.Request, tx sip.ServerTransaction, statusCode sip.StatusCode, reason string) { 243 | response := sip.NewResponseFromRequest(request.MessageID(), request, statusCode, reason, "") 244 | tx.Respond(response) 245 | } 246 | -------------------------------------------------------------------------------- /pkg/media/media.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | //Media interface 4 | type Media interface { 5 | CreateOffer() (*Description, error) 6 | CreateAnswer() (*Description, error) 7 | SetLocalDescription(desc *Description) error 8 | SetRemoteDescription(desc *Description) error 9 | Tracks() map[string]*Track 10 | OnTrack(func(*Track)) 11 | } 12 | 13 | //Description sdp 14 | type Description struct { 15 | Type string `json:"type"` 16 | SDP string `json:"sdp"` 17 | } 18 | -------------------------------------------------------------------------------- /pkg/media/rtp.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | type Track interface { 4 | Name() string 5 | WriteRTP(buf []byte) 6 | WriteRTCP(buf []byte) 7 | ReadRTP() <-chan []byte 8 | ReadRTCP() <-chan []byte 9 | } 10 | -------------------------------------------------------------------------------- /pkg/media/rtp/udp.go: -------------------------------------------------------------------------------- 1 | package rtp 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 7 | "github.com/ghettovoice/gosip/log" 8 | ) 9 | 10 | const ( 11 | DefaultPortMin = 30000 12 | DefaultPortMax = 65530 13 | ) 14 | 15 | type RtpUDPStream struct { 16 | conn *net.UDPConn 17 | stop bool 18 | onPacket func(pkt []byte, raddr net.Addr) 19 | laddr *net.UDPAddr 20 | raddr *net.UDPAddr 21 | logger log.Logger 22 | } 23 | 24 | func NewRtpUDPStream(bind string, portMin, portMax int, callback func(pkt []byte, raddr net.Addr)) *RtpUDPStream { 25 | 26 | logger := utils.NewLogrusLogger(log.DebugLevel, "Media", nil) 27 | 28 | lAddr := &net.UDPAddr{IP: net.ParseIP(bind), Port: 0} 29 | var err error 30 | conn, err := utils.ListenUDPInPortRange(portMin, portMax, lAddr) 31 | if err != nil { 32 | logger.Errorf("ListenUDP: err => %v", err) 33 | return nil 34 | } 35 | 36 | return &RtpUDPStream{ 37 | conn: conn, 38 | stop: false, 39 | onPacket: callback, 40 | laddr: lAddr, 41 | logger: logger, 42 | } 43 | } 44 | 45 | func (r *RtpUDPStream) Log() log.Logger { 46 | return r.logger 47 | } 48 | 49 | func (r *RtpUDPStream) RemoteAddr() *net.UDPAddr { 50 | return r.raddr 51 | } 52 | 53 | func (r *RtpUDPStream) LocalAddr() *net.UDPAddr { 54 | return r.laddr 55 | } 56 | 57 | func (r *RtpUDPStream) Close() { 58 | r.stop = true 59 | r.conn.Close() 60 | } 61 | 62 | func (r *RtpUDPStream) Send(pkt []byte, raddr *net.UDPAddr) (int, error) { 63 | r.Log().Debugf("Send to %v, length %d", raddr.String(), len(pkt)) 64 | r.raddr = raddr 65 | return r.conn.WriteToUDP(pkt, raddr) 66 | } 67 | 68 | func (r *RtpUDPStream) Read() { 69 | 70 | r.Log().Infof("Read") 71 | 72 | buf := make([]byte, 1500) 73 | for { 74 | if r.stop { 75 | r.Log().Infof("Terminate: stop rtp conn now!") 76 | return 77 | } 78 | n, raddr, err := r.conn.ReadFrom(buf) 79 | if err != nil { 80 | r.Log().Warnf("RTP Conn [%v] refused, err: %v, stop now!", raddr, err) 81 | return 82 | } 83 | 84 | r.Log().Tracef("Read rtp from: %v, length: %d", raddr.String(), n) 85 | 86 | if !r.stop { 87 | r.onPacket(buf[0:n], raddr) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pkg/media/rtp/udp_test.go: -------------------------------------------------------------------------------- 1 | package rtp_test 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/cloudwebrtc/go-sip-ua/pkg/media/rtp" 9 | 10 | "github.com/ghettovoice/gosip/log" 11 | "github.com/sirupsen/logrus" 12 | prefixed "github.com/x-cray/logrus-prefixed-formatter" 13 | ) 14 | 15 | var ( 16 | logger *log.LogrusLogger 17 | wg = new(sync.WaitGroup) 18 | ) 19 | 20 | func init() { 21 | logrusNew := logrus.New() 22 | logrusNew.Formatter = &prefixed.TextFormatter{ 23 | FullTimestamp: true, 24 | TimestampFormat: "2006-01-02 15:04:05.000", 25 | ForceColors: true, 26 | ForceFormatting: true, 27 | } 28 | logrusNew.SetLevel(logrus.DebugLevel) 29 | logger = log.NewLogrusLogger(logrusNew, "rtp_test", nil) 30 | } 31 | 32 | func TestUdpStream(t *testing.T) { 33 | udp := rtp.NewRtpUDPStream("127.0.0.1", rtp.DefaultPortMin, rtp.DefaultPortMax, func(data []byte, raddr net.Addr) { 34 | wg.Done() 35 | 36 | got := string(data) 37 | if got != "hello" { 38 | t.Errorf("onpkt = %s; want hello", got) 39 | } 40 | 41 | logger.Debugf("onpkt %v, raddr %v\n", got, raddr.String()) 42 | }) 43 | 44 | logger.Debugf("laddr %v\n", udp.LocalAddr()) 45 | 46 | wg.Add(1) 47 | go udp.Read() 48 | 49 | n, err := udp.Send([]byte("hello"), udp.LocalAddr()) 50 | 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | 55 | if n != 5 { 56 | t.Errorf("Send = %d; want 5", n) 57 | } 58 | 59 | logger.Infof("Send res %v", n) 60 | wg.Wait() 61 | udp.Close() 62 | } 63 | -------------------------------------------------------------------------------- /pkg/media/sdp.go: -------------------------------------------------------------------------------- 1 | package media 2 | -------------------------------------------------------------------------------- /pkg/media/webrtc/webrtc.go: -------------------------------------------------------------------------------- 1 | package webrtc 2 | 3 | type TrackImpl struct { 4 | name string 5 | } 6 | 7 | func (t *TrackImpl) Name() string { 8 | return t.name 9 | } 10 | 11 | func (t *TrackImpl) WriteRTP(buf []byte) { 12 | 13 | } 14 | 15 | func (t *TrackImpl) WriteRTCP(buf []byte) { 16 | 17 | } 18 | 19 | func (t *TrackImpl) ReadRTP() <-chan []byte { 20 | return nil 21 | } 22 | 23 | func (t *TrackImpl) ReadRTCP() <-chan []byte { 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 9 | "github.com/ghettovoice/gosip/log" 10 | "github.com/ghettovoice/gosip/sip" 11 | "github.com/ghettovoice/gosip/util" 12 | ) 13 | 14 | type RequestCallback func(ctx context.Context, request sip.Request, authorizer sip.Authorizer, waitForResult bool, attempt int) (sip.Response, error) 15 | 16 | type Session struct { 17 | lock sync.Mutex 18 | requestCallbck RequestCallback 19 | status Status 20 | callID sip.CallID 21 | offer string 22 | answer string 23 | request sip.Request 24 | response sip.Response 25 | transaction sip.Transaction 26 | direction Direction 27 | uaType string // UAS | UAC 28 | contact *sip.ContactHeader 29 | localURI sip.Address 30 | remoteURI sip.Address 31 | remoteTarget sip.Uri 32 | logger log.Logger 33 | } 34 | 35 | func NewInviteSession(reqcb RequestCallback, uaType string, 36 | contact *sip.ContactHeader, req sip.Request, cid sip.CallID, 37 | tx sip.Transaction, dir Direction, logger log.Logger) *Session { 38 | s := &Session{ 39 | requestCallbck: reqcb, 40 | uaType: uaType, 41 | callID: cid, 42 | transaction: tx, 43 | direction: dir, 44 | offer: "", 45 | answer: "", 46 | contact: contact, 47 | } 48 | 49 | s.logger = utils.NewLogrusLogger(log.DebugLevel, "Session", nil) 50 | 51 | to, _ := req.To() 52 | from, _ := req.From() 53 | 54 | if to.Params != nil && !to.Params.Has("tag") { 55 | to.Params.Add("tag", sip.String{Str: util.RandString(8)}) 56 | req.RemoveHeader("To") 57 | req.AppendHeader(to) 58 | } 59 | 60 | if uaType == "UAS" { 61 | s.localURI = sip.Address{Uri: to.Address, Params: to.Params} 62 | s.remoteURI = sip.Address{Uri: from.Address, Params: from.Params} 63 | s.remoteTarget = contact.Address 64 | s.offer = req.Body() 65 | } else if uaType == "UAC" { 66 | s.localURI = sip.Address{Uri: from.Address, Params: from.Params} 67 | s.remoteURI = sip.Address{Uri: to.Address, Params: to.Params} 68 | s.remoteTarget = req.Recipient() 69 | s.offer = req.Body() 70 | } 71 | 72 | s.request = req 73 | return s 74 | } 75 | 76 | func (s *Session) Log() log.Logger { 77 | return s.logger 78 | } 79 | 80 | func (s *Session) String() string { 81 | return "Local: " + s.localURI.String() + ", Remote: " + s.remoteURI.String() 82 | } 83 | 84 | func (s *Session) LocalURI() sip.Address { 85 | return s.localURI 86 | } 87 | 88 | func (s *Session) RemoteURI() sip.Address { 89 | return s.remoteURI 90 | } 91 | 92 | func (s *Session) LocalSdp() string { 93 | if s.uaType == "UAC" { 94 | return s.offer 95 | } 96 | return s.answer 97 | } 98 | 99 | func (s *Session) RemoteSdp() string { 100 | if s.uaType == "UAS" { 101 | return s.offer 102 | } 103 | return s.answer 104 | } 105 | 106 | func (s *Session) Contact() string { 107 | return s.contact.String() 108 | } 109 | 110 | func (s *Session) CallID() *sip.CallID { 111 | return &s.callID 112 | } 113 | 114 | func (s *Session) Request() sip.Request { 115 | return s.request 116 | } 117 | 118 | func (s *Session) Response() sip.Response { 119 | return s.response 120 | } 121 | 122 | func (s *Session) IsInProgress() bool { 123 | switch s.status { 124 | case InviteSent: 125 | fallthrough 126 | case Provisional: 127 | fallthrough 128 | case EarlyMedia: 129 | fallthrough 130 | case InviteReceived: 131 | fallthrough 132 | case WaitingForAnswer: 133 | return true 134 | default: 135 | return false 136 | } 137 | } 138 | 139 | func (s *Session) IsEstablished() bool { 140 | switch s.status { 141 | case Answered: 142 | fallthrough 143 | case WaitingForACK: 144 | fallthrough 145 | case Confirmed: 146 | return true 147 | default: 148 | return false 149 | } 150 | } 151 | 152 | func (s *Session) IsEnded() bool { 153 | switch s.status { 154 | case Failure: 155 | fallthrough 156 | case Canceled: 157 | fallthrough 158 | case Terminated: 159 | return true 160 | default: 161 | return false 162 | } 163 | } 164 | 165 | func (s *Session) StoreRequest(request sip.Request) { 166 | s.request = request 167 | } 168 | 169 | func (s *Session) StoreResponse(response sip.Response) { 170 | if s.uaType == "UAC" { 171 | to, _ := response.To() 172 | if to.Params != nil && to.Params.Has("tag") { 173 | //Update to URI. 174 | s.remoteURI = sip.Address{Uri: to.Address, Params: to.Params} 175 | } 176 | 177 | sdp := response.Body() 178 | if len(sdp) > 0 { 179 | s.answer = sdp 180 | } 181 | } 182 | s.response = response 183 | } 184 | 185 | func (s *Session) StoreTransaction(tx sip.Transaction) { 186 | if s.transaction != nil { 187 | s.transaction.Done() 188 | } 189 | s.transaction = tx 190 | } 191 | 192 | func (s *Session) SetState(status Status) { 193 | s.lock.Lock() 194 | defer s.lock.Unlock() 195 | s.status = status 196 | } 197 | 198 | func (s *Session) Status() Status { 199 | s.lock.Lock() 200 | defer s.lock.Unlock() 201 | return s.status 202 | } 203 | 204 | func (s *Session) Direction() Direction { 205 | return s.direction 206 | } 207 | 208 | // GetEarlyMedia Get sdp for early media. 209 | func (s *Session) GetEarlyMedia() string { 210 | return s.answer 211 | } 212 | 213 | // ProvideOffer . 214 | func (s *Session) ProvideOffer(sdp string) { 215 | s.offer = sdp 216 | } 217 | 218 | // ProvideAnswer . 219 | func (s *Session) ProvideAnswer(sdp string) { 220 | s.answer = sdp 221 | } 222 | 223 | // Info send SIP INFO 224 | func (s *Session) Info(content string, contentType string) { 225 | method := sip.INFO 226 | req := s.makeRequest(s.uaType, method, sip.MessageID(s.callID), s.request, s.response) 227 | req.SetBody(content, true) 228 | hdr := sip.ContentType(contentType) 229 | req.AppendHeader(&hdr) 230 | s.sendRequest(req) 231 | } 232 | 233 | // ReInvite send re-INVITE 234 | func (s *Session) ReInvite() { 235 | method := sip.INVITE 236 | req := s.makeRequest(s.uaType, method, sip.MessageID(s.callID), s.request, s.response) 237 | req.SetBody(s.offer, true) 238 | hdr := sip.ContentType("application/sdp") 239 | req.AppendHeader(&hdr) 240 | s.sendRequest(req) 241 | } 242 | 243 | // Bye send Bye request. 244 | func (s *Session) Bye() (sip.Response, error) { 245 | req := s.makeRequest(s.uaType, sip.BYE, sip.MessageID(s.callID), s.request, s.response) 246 | return s.sendRequest(req) 247 | } 248 | 249 | func (s *Session) sendRequest(req sip.Request) (sip.Response, error) { 250 | s.Log().Debugf(s.uaType+" send request: %v => \n%v", req.Method(), req) 251 | return s.requestCallbck(context.TODO(), req, nil, false, 1) 252 | } 253 | 254 | // Reject Reject incoming call or for re-INVITE or UPDATE, 255 | func (s *Session) Reject(statusCode sip.StatusCode, reason string) { 256 | tx := (s.transaction.(sip.ServerTransaction)) 257 | request := s.request 258 | s.Log().Debugf("Reject: Request => %s, body => %s", request.Short(), request.Body()) 259 | response := sip.NewResponseFromRequest(request.MessageID(), request, statusCode, reason, "") 260 | response.AppendHeader(s.contact) 261 | tx.Respond(response) 262 | } 263 | 264 | // End end session 265 | func (s *Session) End() error { 266 | 267 | if s.status == Terminated { 268 | err := fmt.Errorf("invalid status: %v", s.status) 269 | s.Log().Errorf("Session::End() %v", err) 270 | return err 271 | } 272 | 273 | switch s.status { 274 | // - UAC - 275 | case InviteSent: 276 | fallthrough 277 | case Provisional: 278 | fallthrough 279 | case EarlyMedia: 280 | s.Log().Info("Canceling session.") 281 | switch s.transaction.(type) { 282 | case sip.ClientTransaction: 283 | s.transaction.(sip.ClientTransaction).Cancel() 284 | case sip.ServerTransaction: 285 | s.transaction.(sip.ServerTransaction).Done() 286 | } 287 | 288 | // - UAS - 289 | case InviteReceived: 290 | fallthrough 291 | case WaitingForAnswer: 292 | fallthrough 293 | case Answered: 294 | s.Log().Info("Rejecting session") 295 | s.Reject(603, "Decline") 296 | 297 | case WaitingForACK: 298 | fallthrough 299 | case Confirmed: 300 | s.Log().Info("Terminating session.") 301 | s.Bye() 302 | } 303 | 304 | return nil 305 | } 306 | 307 | // Accept 200 308 | func (s *Session) Accept(statusCode sip.StatusCode) { 309 | tx := (s.transaction.(sip.ServerTransaction)) 310 | 311 | if len(s.answer) == 0 { 312 | s.Log().Errorf("Answer sdp is nil!") 313 | return 314 | } 315 | request := s.request 316 | response := sip.NewResponseFromRequest(request.MessageID(), request, statusCode, "OK", s.answer) 317 | 318 | hdrs := request.GetHeaders("Content-Type") 319 | if len(hdrs) == 0 { 320 | contentType := sip.ContentType("application/sdp") 321 | response.AppendHeader(&contentType) 322 | } else { 323 | sip.CopyHeaders("Content-Type", request, response) 324 | } 325 | 326 | response.AppendHeader(s.contact) 327 | response.SetBody(s.answer, true) 328 | 329 | s.response = response 330 | tx.Respond(response) 331 | 332 | s.SetState(WaitingForACK) 333 | } 334 | 335 | // Redirect send a 3xx 336 | func (s *Session) Redirect(target sip.Uri, code sip.StatusCode, reason string) { 337 | tx := (s.transaction.(sip.ServerTransaction)) 338 | request := s.request 339 | 340 | s.Log().Debugf("Redirect: Request => %s, body => %s", request.Short(), request.Body()) 341 | 342 | response := sip.NewResponseFromRequest(request.MessageID(), request, code, reason, "") 343 | s.contact.Address = target 344 | response.AppendHeader(s.contact) 345 | 346 | tx.Respond(response) 347 | } 348 | 349 | // Provisional send a provisional code 100|180|183 350 | func (s *Session) Provisional(statusCode sip.StatusCode, reason string) { 351 | tx := (s.transaction.(sip.ServerTransaction)) 352 | request := s.request 353 | var response sip.Response 354 | if len(s.answer) > 0 { 355 | response = sip.NewResponseFromRequest(request.MessageID(), request, statusCode, reason, s.answer) 356 | hdrs := response.GetHeaders("Content-Type") 357 | if len(hdrs) == 0 { 358 | contentType := sip.ContentType("application/sdp") 359 | response.AppendHeader(&contentType) 360 | } else { 361 | sip.CopyHeaders("Content-Type", request, response) 362 | } 363 | response.SetBody(s.answer, true) 364 | } else { 365 | response = sip.NewResponseFromRequest(request.MessageID(), request, statusCode, reason, "") 366 | } 367 | response.AppendHeader(s.contact) 368 | 369 | s.response = response 370 | tx.Respond(response) 371 | } 372 | 373 | func (s *Session) makeRequest(uaType string, method sip.RequestMethod, msgID sip.MessageID, inviteRequest sip.Request, inviteResponse sip.Response) sip.Request { 374 | newRequest := sip.NewRequest( 375 | msgID, 376 | method, 377 | s.remoteTarget, 378 | inviteRequest.SipVersion(), 379 | []sip.Header{}, 380 | "", 381 | inviteRequest.Fields(). 382 | WithFields(log.Fields{ 383 | "invite_request_id": inviteRequest.MessageID(), 384 | }), 385 | ) 386 | 387 | from := s.localURI.Clone().AsFromHeader() 388 | newRequest.AppendHeader(from) 389 | to := s.remoteURI.Clone().AsToHeader() 390 | newRequest.AppendHeader(to) 391 | newRequest.SetRecipient(s.request.Recipient()) 392 | sip.CopyHeaders("Via", inviteRequest, newRequest) 393 | newRequest.AppendHeader(s.contact) 394 | 395 | if uaType == "UAC" { 396 | for _, header := range s.response.Headers() { 397 | if header.Name() == "Record-Route" { 398 | h := header.(*sip.RecordRouteHeader) 399 | rh := &sip.RouteHeader{ 400 | Addresses: h.Addresses, 401 | } 402 | newRequest.AppendHeader(rh) 403 | } 404 | } 405 | if len(inviteRequest.GetHeaders("Route")) > 0 { 406 | sip.CopyHeaders("Route", inviteRequest, newRequest) 407 | } 408 | } else if uaType == "UAS" { 409 | if len(inviteResponse.GetHeaders("Route")) > 0 { 410 | sip.CopyHeaders("Route", inviteResponse, newRequest) 411 | } 412 | newRequest.SetDestination(inviteResponse.Destination()) 413 | newRequest.SetSource(inviteResponse.Source()) 414 | newRequest.SetRecipient(to.Address) 415 | } 416 | 417 | maxForwardsHeader := sip.MaxForwards(70) 418 | newRequest.AppendHeader(&maxForwardsHeader) 419 | sip.CopyHeaders("Call-ID", inviteRequest, newRequest) 420 | sip.CopyHeaders("CSeq", inviteRequest, newRequest) 421 | 422 | cseq, _ := newRequest.CSeq() 423 | cseq.SeqNo++ 424 | cseq.MethodName = method 425 | 426 | return newRequest 427 | } 428 | -------------------------------------------------------------------------------- /pkg/session/type.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | // ReasonPhrase . 4 | var ReasonPhrase = map[uint16]string{ 5 | 100: "Trying", 6 | 180: "Ringing", 7 | 181: "Call Is Being Forwarded", 8 | 182: "Queued", 9 | 183: "Session Progress", 10 | 199: "Early Dialog Terminated", // draft-ietf-sipcore-199 11 | 200: "OK", 12 | 202: "Accepted", // RFC 3265 13 | 204: "No Notification", // RFC 5839 14 | 300: "Multiple Choices", 15 | 301: "Moved Permanently", 16 | 302: "Moved Temporarily", 17 | 305: "Use Proxy", 18 | 380: "Alternative Service", 19 | 400: "Bad Request", 20 | 401: "Unauthorized", 21 | 402: "Payment Required", 22 | 403: "Forbidden", 23 | 404: "Not Found", 24 | 405: "Method Not Allowed", 25 | 406: "Not Acceptable", 26 | 407: "Proxy Authentication Required", 27 | 408: "Request Timeout", 28 | 410: "Gone", 29 | 412: "Conditional Request Failed", // RFC 3903 30 | 413: "Request Entity Too Large", 31 | 414: "Request-URI Too Long", 32 | 415: "Unsupported Media Type", 33 | 416: "Unsupported URI Scheme", 34 | 417: "Unknown Resource-Priority", // RFC 4412 35 | 420: "Bad Extension", 36 | 421: "Extension Required", 37 | 422: "Session Interval Too Small", // RFC 4028 38 | 423: "Interval Too Brief", 39 | 424: "Bad Location Information", // RFC 6442 40 | 428: "Use Identity Header", // RFC 4474 41 | 429: "Provide Referrer Identity", // RFC 3892 42 | 430: "Flow Failed", // RFC 5626 43 | 433: "Anonymity Disallowed", // RFC 5079 44 | 436: "Bad Identity-Info", // RFC 4474 45 | 437: "Unsupported Certificate", // RFC 4744 46 | 438: "Invalid Identity Header", // RFC 4744 47 | 439: "First Hop Lacks Outbound Support", // RFC 5626 48 | 440: "Max-Breadth Exceeded", // RFC 5393 49 | 469: "Bad Info Package", // draft-ietf-sipcore-info-events 50 | 470: "Consent Needed", // RFC 5360 51 | 478: "Unresolvable Destination", // Custom code copied from Kamailio. 52 | 480: "Temporarily Unavailable", 53 | 481: "Call/Transaction Does Not Exist", 54 | 482: "Loop Detected", 55 | 483: "Too Many Hops", 56 | 484: "Address Incomplete", 57 | 485: "Ambiguous", 58 | 486: "Busy Here", 59 | 487: "Request Terminated", 60 | 488: "Not Acceptable Here", 61 | 489: "Bad Event", // RFC 3265 62 | 491: "Request Pending", 63 | 493: "Undecipherable", 64 | 494: "Security Agreement Required", // RFC 3329 65 | 500: "DartSIP Internal Error", 66 | 501: "Not Implemented", 67 | 502: "Bad Gateway", 68 | 503: "Service Unavailable", 69 | 504: "Server Time-out", 70 | 505: "Version Not Supported", 71 | 513: "Message Too Large", 72 | 580: "Precondition Failure", // RFC 3312 73 | 600: "Busy Everywhere", 74 | 603: "Decline", 75 | 604: "Does Not Exist Anywhere", 76 | 606: "Not Acceptable", 77 | } 78 | 79 | const ( 80 | AllowedMethods = "INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO" 81 | AcceptedBody = "application/sdp, application/dtmf-relay" 82 | MaxForwards = 69 83 | SessionExpires = 90 84 | MinSessionExpires = 60 85 | ) 86 | 87 | type Status string 88 | 89 | const ( 90 | InviteSent Status = "InviteSent" /**< After INVITE s sent */ 91 | InviteReceived Status = "InviteReceived" /**< After INVITE s received. */ 92 | ReInviteReceived Status = "ReInviteReceived" /**< After re-INVITE/UPDATE s received */ 93 | //Answer Status = "Answer" /**< After response for re-INVITE/UPDATE. */ 94 | Provisional Status = "Provisional" /**< After response for 1XX. */ 95 | EarlyMedia Status = "EarlyMedia" /**< After response 1XX with sdp. */ 96 | WaitingForAnswer Status = "WaitingForAnswer" 97 | WaitingForACK Status = "WaitingForACK" /**< After 2xx s sent/received. */ 98 | Answered Status = "Answered" 99 | Canceled Status = "Canceled" 100 | Confirmed Status = "Confirmed" /**< After ACK s sent/received. */ 101 | Failure Status = "Failure" /**< Session s rejected or canceled. */ 102 | Terminated Status = "Terminated" /**< Session s terminated. */ 103 | ) 104 | 105 | type Direction string 106 | 107 | const ( 108 | Outgoing Direction = "Outgoing" 109 | Incoming Direction = "Incoming" 110 | ) 111 | -------------------------------------------------------------------------------- /pkg/stack/stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/cloudwebrtc/go-sip-ua/pkg/auth" 14 | "github.com/tevino/abool" 15 | 16 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 17 | "github.com/ghettovoice/gosip/log" 18 | "github.com/ghettovoice/gosip/sip" 19 | "github.com/ghettovoice/gosip/transaction" 20 | "github.com/ghettovoice/gosip/transport" 21 | "github.com/ghettovoice/gosip/util" 22 | ) 23 | 24 | const ( 25 | // DefaultUserAgent . 26 | DefaultUserAgent = "Go SipStack/1.0.0" 27 | ) 28 | 29 | // RequestHandler is a callback that will be called on the incoming request 30 | // of the certain method 31 | // tx argument can be nil for 2xx ACK request 32 | type RequestHandler func(req sip.Request, tx sip.ServerTransaction) 33 | 34 | // RequiresChallengeHandler will check if each request requires 401/407 authentication. 35 | type RequiresChallengeHandler func(req sip.Request) bool 36 | 37 | // ServerAuthManager . 38 | type ServerAuthManager struct { 39 | Authenticator *auth.ServerAuthorizer 40 | RequiresChallenge RequiresChallengeHandler 41 | } 42 | 43 | // SipStackConfig describes available options 44 | type SipStackConfig struct { 45 | // Public IP address or domain name, if empty auto resolved IP will be used. 46 | Host string 47 | // Dns is an address of the public DNS server to use in SRV lookup. 48 | Dns string 49 | Extensions []string 50 | MsgMapper sip.MessageMapper 51 | ServerAuthManager ServerAuthManager 52 | UserAgent string 53 | } 54 | 55 | // SipStack a golang SIP Stack 56 | type SipStack struct { 57 | running abool.AtomicBool 58 | config *SipStackConfig 59 | listenPorts map[string]*sip.Port 60 | tp transport.Layer 61 | tx transaction.Layer 62 | host string 63 | ip net.IP 64 | hwg *sync.WaitGroup 65 | hmu *sync.RWMutex 66 | requestHandlers map[sip.RequestMethod]RequestHandler 67 | handleConnectionError func(err *transport.ConnectionError) 68 | extensions []string 69 | invites map[transaction.TxKey]sip.Request 70 | invitesLock *sync.RWMutex 71 | authenticator *ServerAuthManager 72 | log log.Logger 73 | } 74 | 75 | // NewSipStack creates new instance of SipStack. 76 | func NewSipStack(config *SipStackConfig) *SipStack { 77 | if config == nil { 78 | config = &SipStackConfig{} 79 | } 80 | 81 | logger := utils.NewLogrusLogger(log.DebugLevel, "SipStack", nil) 82 | 83 | var host string 84 | var ip net.IP 85 | if config.Host != "" { 86 | host = config.Host 87 | if addr, err := net.ResolveIPAddr("ip", host); err == nil { 88 | ip = addr.IP 89 | } else { 90 | logger.Panicf("resolve host IP failed: %s", err) 91 | } 92 | } else { 93 | if v, err := util.ResolveSelfIP(); err == nil { 94 | ip = v 95 | host = v.String() 96 | } else { 97 | logger.Panicf("resolve host IP failed: %s", err) 98 | } 99 | } 100 | 101 | var dnsResolver *net.Resolver 102 | if config.Dns != "" { 103 | dnsResolver = &net.Resolver{ 104 | PreferGo: true, 105 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 106 | d := net.Dialer{} 107 | return d.DialContext(ctx, "udp", config.Dns) 108 | }, 109 | } 110 | } else { 111 | dnsResolver = net.DefaultResolver 112 | } 113 | 114 | var extensions []string 115 | if config.Extensions != nil { 116 | extensions = config.Extensions 117 | } 118 | 119 | s := &SipStack{ 120 | config: config, 121 | listenPorts: make(map[string]*sip.Port), 122 | host: host, 123 | ip: ip, 124 | hwg: new(sync.WaitGroup), 125 | hmu: new(sync.RWMutex), 126 | requestHandlers: make(map[sip.RequestMethod]RequestHandler), 127 | extensions: extensions, 128 | invites: make(map[transaction.TxKey]sip.Request), 129 | invitesLock: new(sync.RWMutex), 130 | } 131 | 132 | if config.ServerAuthManager.Authenticator != nil { 133 | s.authenticator = &config.ServerAuthManager 134 | } 135 | 136 | s.log = logger 137 | s.tp = transport.NewLayer(ip, dnsResolver, config.MsgMapper, utils.NewLogrusLogger(log.DebugLevel, "transport.Layer", nil)) 138 | sipTp := &sipTransport{ 139 | tpl: s.tp, 140 | s: s, 141 | } 142 | s.tx = transaction.NewLayer(sipTp, utils.NewLogrusLogger(log.DebugLevel, "transaction.Layer", nil)) 143 | 144 | s.running.Set() 145 | go s.serve() 146 | 147 | return s 148 | } 149 | 150 | // Log . 151 | func (s *SipStack) Log() log.Logger { 152 | return s.log 153 | } 154 | 155 | // ListenTLS starts serving listeners on the provided address 156 | func (s *SipStack) ListenTLS(protocol string, listenAddr string, options *transport.TLSConfig) error { 157 | var err error 158 | network := strings.ToUpper(protocol) 159 | if options != nil { 160 | err = s.tp.Listen(network, listenAddr, options) 161 | } else { 162 | err = s.tp.Listen(network, listenAddr) 163 | } 164 | if err == nil { 165 | target, err := transport.NewTargetFromAddr(listenAddr) 166 | if err != nil { 167 | return err 168 | } 169 | target = transport.FillTargetHostAndPort(network, target) 170 | if _, ok := s.listenPorts[network]; !ok { 171 | s.listenPorts[network] = target.Port 172 | } 173 | } 174 | return err 175 | } 176 | 177 | func (s *SipStack) Listen(protocol string, listenAddr string) error { 178 | return s.ListenTLS(protocol, listenAddr, nil) 179 | } 180 | 181 | func (s *SipStack) serve() { 182 | defer s.Shutdown() 183 | 184 | for { 185 | select { 186 | case tx, ok := <-s.tx.Requests(): 187 | if !ok { 188 | return 189 | } 190 | s.hwg.Add(1) 191 | go s.handleRequest(tx.Origin(), tx) 192 | case ack, ok := <-s.tx.Acks(): 193 | if !ok { 194 | return 195 | } 196 | s.hwg.Add(1) 197 | go s.handleRequest(ack, nil) 198 | case response, ok := <-s.tx.Responses(): 199 | if !ok { 200 | return 201 | } 202 | logger := s.Log().WithFields(map[string]interface{}{ 203 | "sip_response": response.Short(), 204 | }) 205 | logger.Warn("received not matched response") 206 | if key, err := transaction.MakeClientTxKey(response); err == nil { 207 | s.invitesLock.RLock() 208 | inviteRequest, ok := s.invites[key] 209 | s.invitesLock.RUnlock() 210 | if ok { 211 | go s.AckInviteRequest(inviteRequest, response) 212 | } 213 | } 214 | case err, ok := <-s.tx.Errors(): 215 | if !ok { 216 | return 217 | } 218 | s.Log().Errorf("received SIP transaction error: %s", err) 219 | case err, ok := <-s.tp.Errors(): 220 | if !ok { 221 | return 222 | } 223 | 224 | var ferr *sip.MalformedMessageError 225 | if errors.Is(err, io.EOF) { 226 | s.Log().Debugf("received SIP transport error: %s", err) 227 | } else if errors.As(err, &ferr) { 228 | s.Log().Warnf("received SIP transport error: %s", err) 229 | } else { 230 | s.Log().Errorf("received SIP transport error: %s", err) 231 | } 232 | 233 | if connError, ok := err.(*transport.ConnectionError); ok { 234 | if s.handleConnectionError != nil { 235 | s.handleConnectionError(connError) 236 | } 237 | } 238 | } 239 | } 240 | } 241 | 242 | func (s *SipStack) handleRequest(req sip.Request, tx sip.ServerTransaction) { 243 | defer s.hwg.Done() 244 | 245 | logger := s.Log().WithFields(req.Fields()) 246 | logger.Debugf("routing incoming SIP request...") 247 | 248 | s.hmu.RLock() 249 | handler, ok := s.requestHandlers[req.Method()] 250 | s.hmu.RUnlock() 251 | 252 | if !ok { 253 | logger.Warnf("SIP request %v handler not found", req.Method()) 254 | 255 | go func(tx sip.ServerTransaction, logger log.Logger) { 256 | for { 257 | select { 258 | case <-s.tx.Done(): 259 | return 260 | case err, ok := <-tx.Errors(): 261 | if !ok { 262 | return 263 | } 264 | 265 | logger.Warnf("error from SIP server transaction %s: %s", tx, err) 266 | } 267 | } 268 | }(tx, logger) 269 | 270 | res := sip.NewResponseFromRequest("", req, 405, "Method Not Allowed", "") 271 | if _, err := s.Respond(res); err != nil { 272 | logger.Errorf("respond '405 Method Not Allowed' failed: %s", err) 273 | } 274 | 275 | return 276 | } 277 | 278 | if s.authenticator != nil { 279 | authenticator := s.authenticator.Authenticator 280 | requiresChallenge := s.authenticator.RequiresChallenge 281 | if requiresChallenge(req) { 282 | go func() { 283 | if _, ok := authenticator.Authenticate(req, tx); ok { 284 | handler(req, tx) 285 | } 286 | }() 287 | return 288 | } 289 | } 290 | 291 | go handler(req, tx) 292 | } 293 | 294 | // Request Send SIP message 295 | func (s *SipStack) Request(req sip.Request) (sip.ClientTransaction, error) { 296 | if !s.running.IsSet() { 297 | return nil, fmt.Errorf("can not send through stopped server") 298 | } 299 | return s.tx.Request(s.prepareRequest(req)) 300 | } 301 | 302 | func (s *SipStack) GetNetworkInfo(protocol string) *transport.Target { 303 | logger := s.Log() 304 | 305 | var target transport.Target 306 | if s.host != "" { 307 | target.Host = s.host 308 | } else if v, err := util.ResolveSelfIP(); err == nil { 309 | target.Host = v.String() 310 | } else { 311 | logger.Panicf("resolve host IP failed: %s", err) 312 | } 313 | 314 | network := strings.ToUpper(protocol) 315 | if p, ok := s.listenPorts[network]; ok { 316 | target.Port = p 317 | } else { 318 | defPort := sip.DefaultPort(network) 319 | target.Port = &defPort 320 | } 321 | return &target 322 | } 323 | 324 | func (s *SipStack) RememberInviteRequest(request sip.Request) { 325 | if key, err := transaction.MakeClientTxKey(request); err == nil { 326 | s.invitesLock.Lock() 327 | s.invites[key] = request 328 | s.invitesLock.Unlock() 329 | 330 | time.AfterFunc(time.Minute, func() { 331 | s.invitesLock.Lock() 332 | delete(s.invites, key) 333 | s.invitesLock.Unlock() 334 | }) 335 | } else { 336 | s.Log().WithFields(map[string]interface{}{ 337 | "sip_request": request.Short(), 338 | }).Errorf("remember of the request failed: %s", err) 339 | } 340 | } 341 | 342 | func (s *SipStack) AckInviteRequest(request sip.Request, response sip.Response) { 343 | ackRequest := sip.NewAckRequest("", request, response, "", log.Fields{ 344 | "sent_at": time.Now(), 345 | }) 346 | if err := s.Send(ackRequest); err != nil { 347 | s.Log().WithFields(map[string]interface{}{ 348 | "invite_request": request.Short(), 349 | "invite_response": response.Short(), 350 | "ack_request": ackRequest.Short(), 351 | }).Errorf("send ACK request failed: %s", err) 352 | } 353 | } 354 | 355 | func (s *SipStack) CancelRequest(request sip.Request, response sip.Response) { 356 | cancelRequest := sip.NewCancelRequest("", request, log.Fields{ 357 | "sent_at": time.Now(), 358 | }) 359 | if err := s.Send(cancelRequest); err != nil { 360 | s.Log().WithFields(map[string]interface{}{ 361 | "invite_request": request.Short(), 362 | "invite_response": response.Short(), 363 | "cancel_request": cancelRequest.Short(), 364 | }).Errorf("send CANCEL request failed: %s", err) 365 | } 366 | } 367 | 368 | func (s *SipStack) prepareRequest(req sip.Request) sip.Request { 369 | if viaHop, ok := req.ViaHop(); ok { 370 | if viaHop.Params == nil { 371 | viaHop.Params = sip.NewParams() 372 | } 373 | if !viaHop.Params.Has("branch") { 374 | viaHop.Params.Add("branch", sip.String{Str: sip.GenerateBranch()}) 375 | } 376 | } else { 377 | viaHop = &sip.ViaHop{ 378 | ProtocolName: "SIP", 379 | ProtocolVersion: "2.0", 380 | Params: sip.NewParams(). 381 | Add("rport", nil). 382 | Add("branch", sip.String{Str: sip.GenerateBranch()}), 383 | } 384 | 385 | req.PrependHeaderAfter(sip.ViaHeader{ 386 | viaHop, 387 | }, "Route") 388 | } 389 | 390 | s.appendAutoHeaders(req) 391 | 392 | return req 393 | } 394 | 395 | // Respond . 396 | func (s *SipStack) Respond(res sip.Response) (sip.ServerTransaction, error) { 397 | if !s.running.IsSet() { 398 | return nil, fmt.Errorf("can not send through stopped server") 399 | } 400 | 401 | return s.tx.Respond(s.prepareResponse(res)) 402 | } 403 | 404 | func (s *SipStack) RespondOnRequest( 405 | request sip.Request, 406 | status sip.StatusCode, 407 | reason, body string, 408 | headers []sip.Header, 409 | ) (sip.ServerTransaction, error) { 410 | response := sip.NewResponseFromRequest("", request, status, reason, body) 411 | for _, header := range headers { 412 | response.AppendHeader(header) 413 | } 414 | 415 | tx, err := s.Respond(response) 416 | if err != nil { 417 | return nil, fmt.Errorf("respond '%d %s' failed: %w", response.StatusCode(), response.Reason(), err) 418 | } 419 | 420 | return tx, nil 421 | } 422 | 423 | // Send . 424 | func (s *SipStack) Send(msg sip.Message) error { 425 | if !s.running.IsSet() { 426 | return fmt.Errorf("can not send through stopped server") 427 | } 428 | 429 | switch m := msg.(type) { 430 | case sip.Request: 431 | msg = s.prepareRequest(m) 432 | case sip.Response: 433 | msg = s.prepareResponse(m) 434 | } 435 | 436 | return s.tp.Send(msg) 437 | } 438 | 439 | func (s *SipStack) prepareResponse(res sip.Response) sip.Response { 440 | s.appendAutoHeaders(res) 441 | return res 442 | } 443 | 444 | // Shutdown gracefully shutdowns SIP server 445 | func (s *SipStack) Shutdown() { 446 | if !s.running.IsSet() { 447 | return 448 | } 449 | s.running.UnSet() 450 | // stop transaction layer 451 | s.tx.Cancel() 452 | <-s.tx.Done() 453 | // stop transport layer 454 | s.tp.Cancel() 455 | <-s.tp.Done() 456 | // wait for handlers 457 | s.hwg.Wait() 458 | } 459 | 460 | // OnRequest registers new request callback 461 | func (s *SipStack) OnRequest(method sip.RequestMethod, handler RequestHandler) error { 462 | s.hmu.Lock() 463 | s.requestHandlers[method] = handler 464 | s.hmu.Unlock() 465 | 466 | return nil 467 | } 468 | 469 | func (s *SipStack) OnConnectionError(handler func(err *transport.ConnectionError)) { 470 | s.hmu.Lock() 471 | s.handleConnectionError = handler 472 | s.hmu.Unlock() 473 | } 474 | 475 | func (s *SipStack) appendAutoHeaders(msg sip.Message) { 476 | autoAppendMethods := map[sip.RequestMethod]bool{ 477 | sip.INVITE: true, 478 | sip.REGISTER: true, 479 | sip.OPTIONS: true, 480 | sip.REFER: true, 481 | sip.NOTIFY: true, 482 | } 483 | 484 | var msgMethod sip.RequestMethod 485 | switch m := msg.(type) { 486 | case sip.Request: 487 | msgMethod = m.Method() 488 | case sip.Response: 489 | if cseq, ok := m.CSeq(); ok && !m.IsProvisional() { 490 | msgMethod = cseq.MethodName 491 | } 492 | } 493 | if len(msgMethod) > 0 { 494 | if _, ok := autoAppendMethods[msgMethod]; ok { 495 | hdrs := msg.GetHeaders("Allow") 496 | if len(hdrs) == 0 { 497 | allow := make(sip.AllowHeader, 0) 498 | for _, method := range s.getAllowedMethods() { 499 | allow = append(allow, method) 500 | } 501 | 502 | msg.AppendHeader(allow) 503 | } 504 | 505 | hdrs = msg.GetHeaders("Supported") 506 | if len(hdrs) == 0 { 507 | msg.AppendHeader(&sip.SupportedHeader{ 508 | Options: s.extensions, 509 | }) 510 | } 511 | } 512 | } 513 | 514 | if hdrs := msg.GetHeaders("User-Agent"); len(hdrs) == 0 { 515 | userAgent := DefaultUserAgent 516 | if len(s.config.UserAgent) > 0 { 517 | userAgent = s.config.UserAgent 518 | } 519 | userAgentHeader := sip.UserAgentHeader(userAgent) 520 | msg.AppendHeader(&userAgentHeader) 521 | } else if len(s.config.UserAgent) > 0 { 522 | msg.RemoveHeader("User-Agent") 523 | userAgentHeader := sip.UserAgentHeader(s.config.UserAgent) 524 | msg.AppendHeader(&userAgentHeader) 525 | } 526 | 527 | if hdrs := msg.GetHeaders("Content-Length"); len(hdrs) == 0 { 528 | msg.SetBody(msg.Body(), true) 529 | } 530 | } 531 | 532 | func (s *SipStack) getAllowedMethods() []sip.RequestMethod { 533 | methods := []sip.RequestMethod{ 534 | sip.INVITE, 535 | sip.ACK, 536 | sip.BYE, 537 | sip.CANCEL, 538 | sip.INFO, 539 | sip.OPTIONS, 540 | } 541 | added := map[sip.RequestMethod]bool{ 542 | sip.INVITE: true, 543 | sip.ACK: true, 544 | sip.BYE: true, 545 | sip.CANCEL: true, 546 | sip.INFO: true, 547 | sip.OPTIONS: true, 548 | } 549 | 550 | s.hmu.RLock() 551 | for method := range s.requestHandlers { 552 | if _, ok := added[method]; !ok { 553 | methods = append(methods, method) 554 | } 555 | } 556 | s.hmu.RUnlock() 557 | 558 | return methods 559 | } 560 | 561 | type sipTransport struct { 562 | tpl transport.Layer 563 | s *SipStack 564 | } 565 | 566 | func (tp *sipTransport) Messages() <-chan sip.Message { 567 | return tp.tpl.Messages() 568 | } 569 | 570 | func (tp *sipTransport) Send(msg sip.Message) error { 571 | return tp.s.Send(msg) 572 | } 573 | 574 | func (tp *sipTransport) IsReliable(network string) bool { 575 | return tp.tpl.IsReliable(network) 576 | } 577 | 578 | func (tp *sipTransport) IsStreamed(network string) bool { 579 | return tp.tpl.IsStreamed(network) 580 | } 581 | -------------------------------------------------------------------------------- /pkg/ua/register.go: -------------------------------------------------------------------------------- 1 | package ua 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/cloudwebrtc/go-sip-ua/pkg/account" 9 | "github.com/cloudwebrtc/go-sip-ua/pkg/auth" 10 | "github.com/ghettovoice/gosip/sip" 11 | "github.com/ghettovoice/gosip/util" 12 | ) 13 | 14 | type Register struct { 15 | ua *UserAgent 16 | timer *time.Timer 17 | profile *account.Profile 18 | authorizer *auth.ClientAuthorizer 19 | recipient sip.SipUri 20 | request *sip.Request 21 | ctx context.Context 22 | cancel context.CancelFunc 23 | data interface{} 24 | } 25 | 26 | func NewRegister(ua *UserAgent, profile *account.Profile, recipient sip.SipUri, data interface{}) *Register { 27 | r := &Register{ 28 | ua: ua, 29 | profile: profile, 30 | recipient: recipient, 31 | request: nil, 32 | data: data, 33 | } 34 | r.ctx, r.cancel = context.WithCancel(context.Background()) 35 | return r 36 | } 37 | 38 | func (r *Register) SendRegister(expires uint32) error { 39 | 40 | ua := r.ua 41 | profile := r.profile 42 | recipient := r.recipient 43 | 44 | from := &sip.Address{ 45 | Uri: profile.URI, 46 | Params: sip.NewParams().Add("tag", sip.String{Str: util.RandString(8)}), 47 | } 48 | 49 | to := &sip.Address{ 50 | Uri: profile.URI, 51 | } 52 | 53 | contact := profile.Contact() 54 | 55 | if r.request == nil || expires == 0 { 56 | request, err := ua.buildRequest(sip.REGISTER, from, to, contact, recipient, profile.Routes, nil) 57 | if err != nil { 58 | ua.Log().Errorf("Register: err = %v", err) 59 | return err 60 | } 61 | expiresHeader := sip.Expires(expires) 62 | (*request).AppendHeader(&expiresHeader) 63 | r.request = request 64 | } else { 65 | cseq, _ := (*r.request).CSeq() 66 | cseq.SeqNo++ 67 | cseq.MethodName = sip.REGISTER 68 | 69 | (*r.request).RemoveHeader("Expires") 70 | // replace Expires header. 71 | expiresHeader := sip.Expires(expires) 72 | (*r.request).AppendHeader(&expiresHeader) 73 | } 74 | 75 | if profile.AuthInfo != nil && r.authorizer == nil { 76 | r.authorizer = auth.NewClientAuthorizer(profile.AuthInfo.AuthUser, profile.AuthInfo.Password) 77 | } 78 | resp, err := ua.RequestWithContext(r.ctx, *r.request, r.authorizer, true, 1) 79 | 80 | if err != nil { 81 | ua.Log().Errorf("Request [%s] failed, err => %v", sip.REGISTER, err) 82 | 83 | var code sip.StatusCode 84 | var reason string 85 | if _, ok := err.(*sip.RequestError); ok { 86 | reqErr := err.(*sip.RequestError) 87 | code = sip.StatusCode(reqErr.Code) 88 | reason = reqErr.Reason 89 | } else { 90 | code = 500 91 | reason = err.Error() 92 | } 93 | 94 | state := account.RegisterState{ 95 | Account: profile, 96 | Response: nil, 97 | StatusCode: sip.StatusCode(code), 98 | Reason: reason, 99 | Expiration: 0, 100 | UserData: r.data, 101 | } 102 | 103 | ua.Log().Debugf("Request [%s], has error %v, state => %v", sip.REGISTER, err, state) 104 | 105 | if ua.RegisterStateHandler != nil { 106 | ua.RegisterStateHandler(state) 107 | } 108 | } 109 | if resp != nil { 110 | stateCode := resp.StatusCode() 111 | ua.Log().Debugf("%s resp %d => %s", sip.REGISTER, stateCode, resp.String()) 112 | 113 | var expires uint32 = 0 114 | hdrs := resp.GetHeaders("Expires") 115 | if len(hdrs) > 0 { 116 | expires = uint32(*(hdrs[0]).(*sip.Expires)) 117 | } else { 118 | hdrs = resp.GetHeaders("Contact") 119 | if len(hdrs) > 0 { 120 | if cexpires, cexpirescok := (hdrs[0].(*sip.ContactHeader)).Params.Get("expires"); cexpirescok { 121 | cexpiresint, _ := strconv.Atoi(cexpires.String()) 122 | expires = uint32(cexpiresint) 123 | } 124 | } 125 | } 126 | state := account.RegisterState{ 127 | Account: profile, 128 | Response: resp, 129 | StatusCode: resp.StatusCode(), 130 | Reason: resp.Reason(), 131 | Expiration: expires, 132 | UserData: r.data, 133 | } 134 | if expires > 0 { 135 | go func() { 136 | if r.timer == nil { 137 | r.timer = time.NewTimer(time.Second * time.Duration(expires-10)) 138 | } else { 139 | r.timer.Reset(time.Second * time.Duration(expires-10)) 140 | } 141 | select { 142 | case <-r.timer.C: 143 | r.SendRegister(expires) 144 | case <-r.ctx.Done(): 145 | return 146 | } 147 | }() 148 | } else if expires == 0 { 149 | if r.timer != nil { 150 | r.timer.Stop() 151 | r.timer = nil 152 | } 153 | r.request = nil 154 | } 155 | 156 | ua.Log().Debugf("Request [%s], response: state => %v", sip.REGISTER, state) 157 | 158 | if ua.RegisterStateHandler != nil { 159 | ua.RegisterStateHandler(state) 160 | } 161 | } 162 | 163 | return nil 164 | } 165 | 166 | func (r *Register) Stop() { 167 | if r.timer != nil { 168 | r.timer.Stop() 169 | r.timer = nil 170 | } 171 | r.cancel() 172 | } 173 | -------------------------------------------------------------------------------- /pkg/ua/ua.go: -------------------------------------------------------------------------------- 1 | package ua 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "sync" 8 | 9 | "github.com/cloudwebrtc/go-sip-ua/pkg/account" 10 | "github.com/cloudwebrtc/go-sip-ua/pkg/auth" 11 | "github.com/cloudwebrtc/go-sip-ua/pkg/session" 12 | "github.com/cloudwebrtc/go-sip-ua/pkg/stack" 13 | 14 | "github.com/ghettovoice/gosip/log" 15 | "github.com/ghettovoice/gosip/sip" 16 | "github.com/ghettovoice/gosip/transaction" 17 | "github.com/ghettovoice/gosip/util" 18 | 19 | "github.com/cloudwebrtc/go-sip-ua/pkg/utils" 20 | ) 21 | 22 | // SessionKey - Session Key for Session Storage 23 | type SessionKey struct { 24 | CallID sip.CallID 25 | TagID sip.MaybeString 26 | } 27 | 28 | // NewSessionKey - Build a Session Key quickly 29 | func NewSessionKey(callID sip.CallID, tagID sip.MaybeString) SessionKey { 30 | return SessionKey{ 31 | CallID: callID, 32 | TagID: tagID, 33 | } 34 | } 35 | 36 | // UserAgentConfig . 37 | type UserAgentConfig struct { 38 | SipStack *stack.SipStack 39 | } 40 | 41 | //InviteSessionHandler . 42 | type InviteSessionHandler func(s *session.Session, req *sip.Request, resp *sip.Response, status session.Status) 43 | 44 | //RegisterHandler . 45 | type RegisterHandler func(regState account.RegisterState) 46 | 47 | //UserAgent . 48 | type UserAgent struct { 49 | InviteStateHandler InviteSessionHandler 50 | RegisterStateHandler RegisterHandler 51 | config *UserAgentConfig 52 | iss sync.Map /*Invite Session*/ 53 | log log.Logger 54 | } 55 | 56 | //NewUserAgent . 57 | func NewUserAgent(config *UserAgentConfig) *UserAgent { 58 | ua := &UserAgent{ 59 | config: config, 60 | iss: sync.Map{}, 61 | InviteStateHandler: nil, 62 | RegisterStateHandler: nil, 63 | log: utils.NewLogrusLogger(log.DebugLevel, "UserAgent", nil), 64 | } 65 | stack := config.SipStack 66 | stack.OnRequest(sip.INVITE, ua.handleInvite) 67 | stack.OnRequest(sip.ACK, ua.handleACK) 68 | stack.OnRequest(sip.BYE, ua.handleBye) 69 | stack.OnRequest(sip.CANCEL, ua.handleCancel) 70 | stack.OnRequest(sip.UPDATE, ua.handleUpdate) 71 | return ua 72 | } 73 | 74 | func (ua *UserAgent) Log() log.Logger { 75 | return ua.log 76 | } 77 | 78 | func (ua *UserAgent) handleInviteState(is *session.Session, request *sip.Request, response *sip.Response, state session.Status, tx *sip.Transaction) { 79 | if request != nil && *request != nil { 80 | is.StoreRequest(*request) 81 | } 82 | 83 | if response != nil && *response != nil { 84 | is.StoreResponse(*response) 85 | } 86 | 87 | if tx != nil { 88 | is.StoreTransaction(*tx) 89 | } 90 | 91 | is.SetState(state) 92 | 93 | if ua.InviteStateHandler != nil { 94 | ua.InviteStateHandler(is, request, response, state) 95 | } 96 | } 97 | 98 | func (ua *UserAgent) buildRequest( 99 | method sip.RequestMethod, 100 | from *sip.Address, 101 | to *sip.Address, 102 | contact *sip.Address, 103 | recipient sip.SipUri, 104 | routes []sip.Uri, 105 | callID *sip.CallID) (*sip.Request, error) { 106 | 107 | builder := sip.NewRequestBuilder() 108 | 109 | builder.SetMethod(method) 110 | builder.SetFrom(from) 111 | builder.SetTo(to) 112 | builder.SetContact(contact) 113 | builder.SetRecipient(recipient.Clone()) 114 | 115 | if len(routes) > 0 { 116 | builder.SetRoutes(routes) 117 | } 118 | 119 | if callID != nil { 120 | builder.SetCallID(callID) 121 | } 122 | 123 | req, err := builder.Build() 124 | if err != nil { 125 | ua.Log().Errorf("err => %v", err) 126 | return nil, err 127 | } 128 | 129 | //ua.Log().Infof("buildRequest %s => \n%v", method, req) 130 | return &req, nil 131 | } 132 | 133 | func (ua *UserAgent) SendRegister(profile *account.Profile, recipient sip.SipUri, expires uint32, userdata interface{}) (*Register, error) { 134 | register := NewRegister(ua, profile, recipient, userdata) 135 | err := register.SendRegister(expires) 136 | if err != nil { 137 | ua.Log().Errorf("SendRegister failed, err => %v", err) 138 | return nil, err 139 | } 140 | return register, nil 141 | } 142 | 143 | func (ua *UserAgent) Invite(profile *account.Profile, target sip.Uri, recipient sip.SipUri, body *string) (*session.Session, error) { 144 | return ua.InviteWithContext(context.TODO(), profile, target, recipient, body) 145 | } 146 | 147 | func (ua *UserAgent) InviteWithContext(ctx context.Context, profile *account.Profile, target sip.Uri, recipient sip.SipUri, body *string) (*session.Session, error) { 148 | 149 | from := &sip.Address{ 150 | DisplayName: sip.String{Str: profile.DisplayName}, 151 | Uri: profile.URI, 152 | Params: sip.NewParams().Add("tag", sip.String{Str: util.RandString(8)}), 153 | } 154 | 155 | contact := profile.Contact() 156 | 157 | to := &sip.Address{ 158 | Uri: target, 159 | } 160 | 161 | request, err := ua.buildRequest(sip.INVITE, from, to, contact, recipient, profile.Routes, nil) 162 | if err != nil { 163 | ua.Log().Errorf("INVITE: err = %v", err) 164 | return nil, err 165 | } 166 | 167 | if body != nil { 168 | (*request).SetBody(*body, true) 169 | contentType := sip.ContentType("application/sdp") 170 | (*request).AppendHeader(&contentType) 171 | } 172 | 173 | var authorizer *auth.ClientAuthorizer = nil 174 | if profile.AuthInfo != nil { 175 | authorizer = auth.NewClientAuthorizer(profile.AuthInfo.AuthUser, profile.AuthInfo.Password) 176 | } 177 | 178 | resp, err := ua.RequestWithContext(ctx, *request, authorizer, false, 1) 179 | if err != nil { 180 | ua.Log().Errorf("INVITE: Request [INVITE] failed, err => %v", err) 181 | return nil, err 182 | } 183 | 184 | if resp != nil { 185 | stateCode := resp.StatusCode() 186 | ua.Log().Debugf("INVITE: resp %d => %s", stateCode, resp.String()) 187 | return nil, fmt.Errorf("Invite session is unsuccessful, code: %d, reason: %s", stateCode, resp.String()) 188 | } 189 | 190 | callID, ok := (*request).CallID() 191 | fromHeader, ok2 := (*request).From() 192 | if ok && ok2 { 193 | fromTag, _ := fromHeader.Params.Get("tag") 194 | if v, found := ua.iss.Load(NewSessionKey(*callID, fromTag)); found { 195 | return v.(*session.Session), nil 196 | } 197 | } 198 | 199 | return nil, fmt.Errorf("invite session not found, unknown errors") 200 | } 201 | 202 | func (ua *UserAgent) Request(req *sip.Request) (sip.ClientTransaction, error) { 203 | return ua.config.SipStack.Request(*req) 204 | } 205 | 206 | func (ua *UserAgent) handleBye(request sip.Request, tx sip.ServerTransaction) { 207 | ua.Log().Debugf("handleBye: Request => %s, body => %s", request.Short(), request.Body()) 208 | response := sip.NewResponseFromRequest(request.MessageID(), request, 200, "OK", "") 209 | tx.Respond(response) 210 | callID, ok := request.CallID() 211 | fromHeader, ok2 := request.From() 212 | if ok && ok2 { 213 | fromTag, _ := fromHeader.Params.Get("tag") 214 | if v, found := ua.iss.Load(NewSessionKey(*callID, fromTag)); found { 215 | is := v.(*session.Session) 216 | ua.iss.Delete(NewSessionKey(*callID, fromTag)) 217 | var transaction sip.Transaction = tx.(sip.Transaction) 218 | ua.handleInviteState(is, &request, &response, session.Terminated, &transaction) 219 | } 220 | } 221 | } 222 | 223 | func (ua *UserAgent) handleCancel(request sip.Request, tx sip.ServerTransaction) { 224 | 225 | ua.Log().Debugf("handleCancel: Request => %s, body => %s", request.Short(), request.Body()) 226 | response := sip.NewResponseFromRequest(request.MessageID(), request, 200, "OK", "") 227 | tx.Respond(response) 228 | 229 | callID, ok := request.CallID() 230 | fromHeader, ok2 := request.From() 231 | if ok && ok2 { 232 | fromTag, _ := fromHeader.Params.Get("tag") 233 | if v, found := ua.iss.Load(NewSessionKey(*callID, fromTag)); found { 234 | is := v.(*session.Session) 235 | ua.iss.Delete(NewSessionKey(*callID, fromTag)) 236 | var transaction sip.Transaction = tx.(sip.Transaction) 237 | is.SetState(session.Canceled) 238 | ua.handleInviteState(is, &request, nil, session.Canceled, &transaction) 239 | } 240 | } 241 | } 242 | 243 | func (ua *UserAgent) handleACK(request sip.Request, tx sip.ServerTransaction) { 244 | ua.Log().Debugf("handleACK => %s, body => %s", request.Short(), request.Body()) 245 | callID, ok := request.CallID() 246 | fromHeader, ok2 := request.From() 247 | if ok && ok2 { 248 | fromTag, _ := fromHeader.Params.Get("tag") 249 | if v, found := ua.iss.Load(NewSessionKey(*callID, fromTag)); found { 250 | // handle Ringing or Processing with sdp 251 | is := v.(*session.Session) 252 | is.SetState(session.Confirmed) 253 | ua.handleInviteState(is, &request, nil, session.Confirmed, nil) 254 | } 255 | } 256 | } 257 | 258 | func (ua *UserAgent) handleInvite(request sip.Request, tx sip.ServerTransaction) { 259 | 260 | ua.Log().Debugf("handleInvite => %s, body => %s", request.Short(), request.Body()) 261 | 262 | callID, ok := request.CallID() 263 | fromHeader, ok2 := request.From() 264 | if ok && ok2 { 265 | fromTag, _ := fromHeader.Params.Get("tag") 266 | var transaction sip.Transaction = tx.(sip.Transaction) 267 | v, found := ua.iss.Load(NewSessionKey(*callID, fromTag)) 268 | if toHdr, ok := request.To(); ok && toHdr.Params.Has("tag") { 269 | if found { 270 | is := v.(*session.Session) 271 | is.SetState(session.ReInviteReceived) 272 | ua.handleInviteState(is, &request, nil, session.ReInviteReceived, &transaction) 273 | } else { 274 | // reinvite for transaction we have no record of; reject it 275 | response := sip.NewResponseFromRequest(request.MessageID(), request, sip.StatusCode(481), "Call/Transaction does not exist", "") 276 | tx.Respond(response) 277 | } 278 | } else { 279 | if found { 280 | // retransmission; reject it 281 | response := sip.NewResponseFromRequest(request.MessageID(), request, sip.StatusCode(482), "Loop Detected", "") 282 | tx.Respond(response) 283 | } else { 284 | contactHdr, _ := request.Contact() 285 | contactAddr := ua.updateContact2UAAddr(request.Transport(), contactHdr.Address) 286 | contactHdr.Address = contactAddr 287 | 288 | is := session.NewInviteSession(ua.RequestWithContext, "UAS", contactHdr, request, *callID, transaction, session.Incoming, ua.Log()) 289 | ua.iss.Store(NewSessionKey(*callID, fromTag), is) 290 | is.SetState(session.InviteReceived) 291 | ua.handleInviteState(is, &request, nil, session.InviteReceived, &transaction) 292 | is.SetState(session.WaitingForAnswer) 293 | } 294 | } 295 | } 296 | 297 | go func() { 298 | cancel := <-tx.Cancels() 299 | if cancel != nil { 300 | ua.Log().Debugf("Cancel => %s, body => %s", cancel.Short(), cancel.Body()) 301 | response := sip.NewResponseFromRequest(cancel.MessageID(), cancel, 200, "OK", "") 302 | callID, ok := response.CallID() 303 | fromHeader, ok2 := request.From() 304 | if ok && ok2 { 305 | fromTag, _ := fromHeader.Params.Get("tag") 306 | if v, found := ua.iss.Load(NewSessionKey(*callID, fromTag)); found { 307 | ua.iss.Delete(NewSessionKey(*callID, fromTag)) 308 | is := v.(*session.Session) 309 | is.SetState(session.Canceled) 310 | ua.handleInviteState(is, &request, &response, session.Canceled, nil) 311 | } 312 | } 313 | 314 | tx.Respond(response) 315 | } 316 | }() 317 | 318 | go func() { 319 | ack := <-tx.Acks() 320 | if ack != nil { 321 | ua.Log().Debugf("ack => %v", ack) 322 | } 323 | }() 324 | } 325 | 326 | func (ua *UserAgent) handleUpdate(request sip.Request, tx sip.ServerTransaction) { 327 | ua.Log().Debugf("handleUpdate: Request => %s", request.Short()) 328 | response := sip.NewResponseFromRequest(request.MessageID(), request, 200, "OK", "") 329 | tx.Respond(response) 330 | } 331 | 332 | // RequestWithContext . 333 | func (ua *UserAgent) RequestWithContext(ctx context.Context, request sip.Request, authorizer sip.Authorizer, waitForResult bool, attempt int) (sip.Response, error) { 334 | s := ua.config.SipStack 335 | tx, err := s.Request(request) 336 | if err != nil { 337 | return nil, err 338 | } 339 | var cts sip.Transaction = tx.(sip.Transaction) 340 | 341 | if request.IsInvite() { 342 | 343 | callID, ok := request.CallID() 344 | fromHeader, ok2 := request.From() 345 | if ok && ok2 { 346 | fromTag, _ := fromHeader.Params.Get("tag") 347 | if _, found := ua.iss.Load(NewSessionKey(*callID, fromTag)); !found { 348 | contactHdr, _ := request.Contact() 349 | contactAddr := ua.updateContact2UAAddr(request.Transport(), contactHdr.Address) 350 | contactHdr.Address = contactAddr 351 | is := session.NewInviteSession(ua.RequestWithContext, "UAC", contactHdr, request, *callID, cts, session.Outgoing, ua.Log()) 352 | ua.iss.Store(NewSessionKey(*callID, fromTag), is) 353 | is.ProvideOffer(request.Body()) 354 | is.SetState(session.InviteSent) 355 | ua.handleInviteState(is, &request, nil, session.InviteSent, &cts) 356 | } 357 | } 358 | } 359 | 360 | responses := make(chan sip.Response) 361 | provisionals := make(chan sip.Response) 362 | errs := make(chan error) 363 | go func() { 364 | var lastResponse sip.Response 365 | 366 | previousResponses := make([]sip.Response, 0) 367 | previousResponsesStatuses := make(map[sip.StatusCode]bool) 368 | 369 | for { 370 | select { 371 | case <-ctx.Done(): 372 | if lastResponse != nil && lastResponse.IsProvisional() { 373 | s.CancelRequest(request, lastResponse) 374 | } 375 | if lastResponse != nil { 376 | lastResponse.SetPrevious(previousResponses) 377 | } 378 | errs <- sip.NewRequestError(487, "Request Terminated", request, lastResponse) 379 | // pull out later possible transaction responses and errors 380 | go func() { 381 | for { 382 | select { 383 | case <-tx.Done(): 384 | return 385 | case <-tx.Errors(): 386 | case <-tx.Responses(): 387 | } 388 | } 389 | }() 390 | return 391 | case err, ok := <-tx.Errors(): 392 | if !ok { 393 | if lastResponse != nil { 394 | lastResponse.SetPrevious(previousResponses) 395 | } 396 | errs <- sip.NewRequestError(487, "Request Terminated", request, lastResponse) 397 | return 398 | } 399 | 400 | switch err.(type) { 401 | case *transaction.TxTimeoutError: 402 | { 403 | errs <- sip.NewRequestError(408, "Request Timeout", request, lastResponse) 404 | return 405 | } 406 | } 407 | 408 | //errs <- err 409 | return 410 | case response, ok := <-tx.Responses(): 411 | if !ok { 412 | if lastResponse != nil { 413 | lastResponse.SetPrevious(previousResponses) 414 | } 415 | errs <- sip.NewRequestError(487, "Request Terminated", request, lastResponse) 416 | return 417 | } 418 | 419 | response = sip.CopyResponse(response) 420 | lastResponse = response 421 | 422 | if response.IsProvisional() { 423 | if _, ok := previousResponsesStatuses[response.StatusCode()]; !ok { 424 | previousResponses = append(previousResponses, response) 425 | } 426 | provisionals <- response 427 | continue 428 | } 429 | 430 | // success 431 | if response.IsSuccess() { 432 | response.SetPrevious(previousResponses) 433 | 434 | if request.IsInvite() { 435 | s.AckInviteRequest(request, response) 436 | s.RememberInviteRequest(request) 437 | go func() { 438 | for response := range tx.Responses() { 439 | s.AckInviteRequest(request, response) 440 | } 441 | }() 442 | } 443 | responses <- response 444 | tx.Done() 445 | return 446 | } 447 | 448 | // unauth request 449 | needAuth := (response.StatusCode() == 401 || response.StatusCode() == 407) && attempt < 2 450 | if needAuth && authorizer != nil { 451 | if err := authorizer.AuthorizeRequest(request, response); err != nil { 452 | errs <- err 453 | return 454 | } 455 | if response, err := ua.RequestWithContext(ctx, request, authorizer, true, attempt+1); err == nil { 456 | responses <- response 457 | } else { 458 | errs <- err 459 | } 460 | return 461 | } 462 | 463 | // failed request 464 | if lastResponse != nil { 465 | lastResponse.SetPrevious(previousResponses) 466 | } 467 | errs <- sip.NewRequestError(uint(response.StatusCode()), response.Reason(), request, lastResponse) 468 | return 469 | } 470 | } 471 | }() 472 | 473 | waitForResponse := func(cts *sip.Transaction) (sip.Response, error) { 474 | for { 475 | select { 476 | case provisional := <-provisionals: 477 | callID, ok := provisional.CallID() 478 | fromHeader, ok2 := provisional.From() 479 | if ok && ok2 { 480 | fromTag, _ := fromHeader.Params.Get("tag") 481 | if v, found := ua.iss.Load(NewSessionKey(*callID, fromTag)); found { 482 | is := v.(*session.Session) 483 | is.StoreResponse(provisional) 484 | // handle Ringing or Processing with sdp 485 | ua.handleInviteState(is, &request, &provisional, session.Provisional, cts) 486 | if len(provisional.Body()) > 0 { 487 | is.SetState(session.EarlyMedia) 488 | ua.handleInviteState(is, &request, &provisional, session.EarlyMedia, cts) 489 | } 490 | } 491 | } 492 | case err := <-errs: 493 | //TODO: error type switch transaction.TxTimeoutError 494 | switch err.(type) { 495 | case *transaction.TxTimeoutError: 496 | //errs <- sip.NewRequestError(408, "Request Timeout", nil, nil) 497 | return nil, err 498 | } 499 | request := (err.(*sip.RequestError)).Request 500 | response := (err.(*sip.RequestError)).Response 501 | callID, ok := request.CallID() 502 | fromHeader, ok2 := request.From() 503 | if ok && ok2 { 504 | fromTag, _ := fromHeader.Params.Get("tag") 505 | if v, found := ua.iss.Load(NewSessionKey(*callID, fromTag)); found { 506 | is := v.(*session.Session) 507 | ua.iss.Delete(NewSessionKey(*callID, fromTag)) 508 | is.SetState(session.Failure) 509 | ua.handleInviteState(is, &request, &response, session.Failure, nil) 510 | } 511 | } 512 | return nil, err 513 | case response := <-responses: 514 | callID, ok := response.CallID() 515 | fromHeader, ok2 := request.From() 516 | if ok && ok2 { 517 | fromTag, _ := fromHeader.Params.Get("tag") 518 | if v, found := ua.iss.Load(NewSessionKey(*callID, fromTag)); found { 519 | if request.IsInvite() { 520 | is := v.(*session.Session) 521 | is.SetState(session.Confirmed) 522 | ua.handleInviteState(is, &request, &response, session.Confirmed, nil) 523 | } else if request.Method() == sip.BYE { 524 | is := v.(*session.Session) 525 | ua.iss.Delete(NewSessionKey(*callID, fromTag)) 526 | is.SetState(session.Terminated) 527 | ua.handleInviteState(is, &request, &response, session.Terminated, nil) 528 | } 529 | } 530 | } 531 | return response, nil 532 | } 533 | } 534 | } 535 | 536 | if !waitForResult { 537 | go waitForResponse(&cts) 538 | return nil, err 539 | } 540 | return waitForResponse(&cts) 541 | } 542 | 543 | func (ua *UserAgent) Shutdown() { 544 | ua.config.SipStack.Shutdown() 545 | } 546 | 547 | func (ua *UserAgent) updateContact2UAAddr(transport string, from sip.ContactUri) sip.ContactUri { 548 | stackAddr := ua.config.SipStack.GetNetworkInfo(transport) 549 | ret := from.Clone() 550 | ret.SetHost(stackAddr.Host) 551 | ret.SetPort(stackAddr.Port) 552 | return ret 553 | } 554 | -------------------------------------------------------------------------------- /pkg/utils/log.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ghettovoice/gosip/log" 7 | "github.com/sirupsen/logrus" 8 | prefixed "github.com/x-cray/logrus-prefixed-formatter" 9 | ) 10 | 11 | type MyLogger struct { 12 | Logger *log.LogrusLogger 13 | level log.Level 14 | } 15 | 16 | func (ml *MyLogger) Level() string { 17 | switch ml.level { 18 | case log.PanicLevel: 19 | return "Panic" 20 | case log.FatalLevel: 21 | return "Fatal" 22 | case log.ErrorLevel: 23 | return "Error" 24 | case log.WarnLevel: 25 | return "Warn" 26 | case log.InfoLevel: 27 | return "Info" 28 | case log.DebugLevel: 29 | return "Debug" 30 | case log.TraceLevel: 31 | return "Trace" 32 | } 33 | return "Unkown" 34 | } 35 | 36 | var ( 37 | loggers map[string]*MyLogger 38 | ) 39 | 40 | func init() { 41 | loggers = make(map[string]*MyLogger) 42 | } 43 | 44 | func NewLogrusLogger(level log.Level, prefix string, fields log.Fields) log.Logger { 45 | if logger, found := loggers[prefix]; found { 46 | return logger.Logger.WithPrefix(prefix) 47 | } 48 | l := logrus.New() 49 | l.Level = logrus.ErrorLevel 50 | l.Formatter = &prefixed.TextFormatter{ 51 | FullTimestamp: true, 52 | TimestampFormat: "2006-01-02 15:04:05.000", 53 | ForceColors: true, 54 | ForceFormatting: true, 55 | } 56 | l.SetReportCaller(true) 57 | logger := log.NewLogrusLogger(l, "main", fields) 58 | loggers[prefix] = &MyLogger{ 59 | Logger: logger, 60 | level: level, 61 | } 62 | logger.SetLevel(level) 63 | return logger.WithPrefix(prefix) 64 | } 65 | 66 | func SetLogLevel(prefix string, level log.Level) error { 67 | if logger, found := loggers[prefix]; found { 68 | logger.level = level 69 | logger.Logger.SetLevel(level) 70 | return nil 71 | } 72 | return fmt.Errorf("logger [%v] not found", prefix) 73 | } 74 | 75 | func GetLoggers() map[string]*MyLogger { 76 | return loggers 77 | } 78 | -------------------------------------------------------------------------------- /pkg/utils/util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math/rand" 7 | "net" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/ghettovoice/gosip/sip" 12 | ) 13 | 14 | var ( 15 | localIPPrefix = [...]string{"192.168", "10.0", "169.254", "172.16"} 16 | ErrPort = errors.New("invalid port") 17 | ) 18 | 19 | func GetBranchID(msg sip.Message) sip.MaybeString { 20 | if viaHop, ok := msg.ViaHop(); ok { 21 | if branch, ok := viaHop.Params.Get("branch"); ok { 22 | return branch 23 | } 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func GetIP(addr string) string { 30 | if strings.Contains(addr, ":") { 31 | return strings.Split(addr, ":")[0] 32 | } 33 | return "" 34 | } 35 | 36 | func GetPort(addr string) string { 37 | if strings.Contains(addr, ":") { 38 | return strings.Split(addr, ":")[1] 39 | } 40 | return "" 41 | } 42 | 43 | func StrToUint16(str string) uint16 { 44 | i, _ := strconv.ParseUint(str, 10, 16) 45 | return uint16(i) 46 | } 47 | 48 | func BuildContactHeader(name string, from, to sip.Message, expires *sip.Expires) { 49 | name = strings.ToLower(name) 50 | for _, h := range from.GetHeaders(name) { 51 | AddParamsToContact(h.(*sip.ContactHeader), expires) 52 | to.AppendHeader(h.Clone()) 53 | } 54 | } 55 | 56 | func AddParamsToContact(contact *sip.ContactHeader, expires *sip.Expires) { 57 | if urn, ok := contact.Params.Get("+sip.instance"); ok { 58 | contact.Params.Add("+sip.instance", sip.String{Str: fmt.Sprintf(`"%s"`, urn)}) 59 | } 60 | if expires != nil { 61 | contact.Params.Add("expires", sip.String{Str: fmt.Sprintf("%d", int(*expires))}) 62 | } 63 | } 64 | 65 | func ListenUDPInPortRange(portMin, portMax int, laddr *net.UDPAddr) (*net.UDPConn, error) { 66 | if (laddr.Port != 0) || ((portMin == 0) && (portMax == 0)) { 67 | return net.ListenUDP("udp", laddr) 68 | } 69 | var i, j int 70 | i = portMin 71 | if i == 0 { 72 | i = 1 73 | } 74 | j = portMax 75 | if j == 0 { 76 | j = 0xFFFF 77 | } 78 | if i > j { 79 | return nil, ErrPort 80 | } 81 | portStart := rand.Intn(j-i+1) + i 82 | portCurrent := portStart 83 | for { 84 | *laddr = net.UDPAddr{IP: laddr.IP, Port: portCurrent} 85 | c, e := net.ListenUDP("udp", laddr) 86 | if e == nil { 87 | return c, e 88 | } 89 | portCurrent++ 90 | if portCurrent > j { 91 | portCurrent = i 92 | } 93 | 94 | fmt.Printf("failed to listen %s: %v, try next port %d", laddr.String(), e, portCurrent) 95 | 96 | if portCurrent == portStart { 97 | break 98 | } 99 | } 100 | return nil, ErrPort 101 | } 102 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | --------------------------------------------------------------------------------