├── .gitignore ├── README.md ├── go.mod ├── go.sum ├── sip_message.go ├── sip_message_test.go ├── softphone.go ├── softphone_test.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.wav 2 | *.raw 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RingCentral Softphone SDK for GoLang 2 | 3 | 4 | ## Usage 5 | 6 | Check [softphone_test.go](./softphone_test.go). 7 | 8 | 9 | ## Known issues 10 | 11 | - `SIP/2.0 603 Too Many Contacts` if you create too many softphone instances in very short time 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ringcentral/ringcentral-softphone-go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/google/uuid v1.2.0 7 | github.com/gorilla/websocket v1.4.2 8 | github.com/joho/godotenv v1.3.0 9 | github.com/pion/webrtc/v3 v3.0.11 10 | github.com/ringcentral/ringcentral-go v0.8.0 11 | github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 5 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 6 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 7 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 8 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 9 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 10 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 11 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 12 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 13 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 14 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 15 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 16 | github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 18 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 19 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 20 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 21 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 22 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 23 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 24 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 25 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 28 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 29 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 30 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 31 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 32 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 33 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 34 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 35 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 36 | github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= 37 | github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= 38 | github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI= 39 | github.com/pion/dtls/v2 v2.0.7 h1:PNcUs/G1l9hb4jzMEorgFMxIBdp7fRN4LIApOTMtCYs= 40 | github.com/pion/dtls/v2 v2.0.7/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10= 41 | github.com/pion/ice/v2 v2.0.15 h1:KZrwa2ciL9od8+TUVJiYTNsCW9J5lktBjGwW1MacEnQ= 42 | github.com/pion/ice/v2 v2.0.15/go.mod h1:ZIiVGevpgAxF/cXiIVmuIUtCb3Xs4gCzCbXB6+nFkSI= 43 | github.com/pion/interceptor v0.0.9 h1:fk5hTdyLO3KURQsf/+RjMpEm4NE3yeTY9Kh97b5BvwA= 44 | github.com/pion/interceptor v0.0.9/go.mod h1:dHgEP5dtxOTf21MObuBAjJeAayPxLUAZjerGH8Xr07c= 45 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 46 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 47 | github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= 48 | github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= 49 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 50 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 51 | github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo= 52 | github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= 53 | github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U= 54 | github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 55 | github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= 56 | github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE= 57 | github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= 58 | github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8= 59 | github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= 60 | github.com/pion/srtp/v2 v2.0.1 h1:kgfh65ob3EcnFYA4kUBvU/menCp9u7qaJLXwWgpobzs= 61 | github.com/pion/srtp/v2 v2.0.1/go.mod h1:c8NWHhhkFf/drmHTAblkdu8++lsISEBBdAuiyxgqIsE= 62 | github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= 63 | github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= 64 | github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= 65 | github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= 66 | github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= 67 | github.com/pion/transport v0.12.1/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= 68 | github.com/pion/transport v0.12.2 h1:WYEjhloRHt1R86LhUKjC5y+P52Y11/QqEUalvtzVoys= 69 | github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= 70 | github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= 71 | github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= 72 | github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= 73 | github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= 74 | github.com/pion/webrtc/v3 v3.0.11 h1:RIxUbkWJn6YvLVmHZSzc30yQLyME5vGDkpqrV7EHxz4= 75 | github.com/pion/webrtc/v3 v3.0.11/go.mod h1:WEvXneGTeqNmiR59v5jTsxMc4yXQyOQcRsrdAbNwSEU= 76 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 77 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/ringcentral/ringcentral-go v0.8.0 h1:z/595K14KpM6hDt5VnFDzdIO6lpgLXivN8R7nYeXnvc= 81 | github.com/ringcentral/ringcentral-go v0.8.0/go.mod h1:vqC8XImjaXva337Lxd3QDzIVdMXhYAvrbT4ABZ3Pl/I= 82 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 83 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 84 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 85 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 86 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 87 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 88 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 89 | github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b h1:QqixIpc5WFIqTLxB3Hq8qs0qImAgBdq0p6rq2Qdl634= 90 | github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8= 91 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 92 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 93 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 94 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 95 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 96 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 97 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 98 | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 99 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 100 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 101 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 102 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 103 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 104 | golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 105 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= 106 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 107 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 109 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 119 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 121 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 122 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 123 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 124 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 125 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 126 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 127 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 128 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 130 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 131 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 132 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 133 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 134 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 137 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 138 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 139 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 140 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 141 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 142 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 143 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 144 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 145 | -------------------------------------------------------------------------------- /sip_message.go: -------------------------------------------------------------------------------- 1 | package softphone 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // SipMessage SIP message 11 | type SipMessage struct { 12 | Subject string 13 | Headers map[string]string 14 | Body string 15 | } 16 | 17 | // ToString from SipMessage to string message 18 | func (sipMessage SipMessage) ToString() (message string) { 19 | if _, ok := sipMessage.Headers["Content-Length"]; !ok { 20 | sipMessage.Headers["Content-Length"] = fmt.Sprintf("%d", len(sipMessage.Body)) 21 | } 22 | sipMessage.Headers["User-Agent"] = "github.com/ringcentral/ringcentral-softphone-go" 23 | list := []string{} 24 | list = append(list, sipMessage.Subject) 25 | for key, value := range sipMessage.Headers { 26 | list = append(list, fmt.Sprintf("%s: %s", key, value)) 27 | } 28 | list = append(list, "") 29 | list = append(list, sipMessage.Body) 30 | return strings.Join(list, "\r\n") 31 | } 32 | 33 | // IncreaseSeq increase CSeq 34 | func (sipMessage *SipMessage) IncreaseSeq() { 35 | if value, ok := sipMessage.Headers["CSeq"]; ok { 36 | tokens := strings.Split(value, " ") 37 | i, err := strconv.Atoi(tokens[0]) 38 | if err != nil { 39 | log.Fatal("CSeq doesn't start with an integer") 40 | } 41 | tokens[0] = fmt.Sprintf("%d", i+1) 42 | sipMessage.Headers["CSeq"] = strings.Join(tokens, " ") 43 | } 44 | } 45 | 46 | // FromStringToSipMessage from string message to SipMessage 47 | func FromStringToSipMessage(message string) (sipMessage SipMessage) { 48 | paragraphs := strings.Split(message, "\r\n\r\n") 49 | body := strings.Join(paragraphs[1:], "\r\n\r\n") 50 | paragraphs = strings.Split(paragraphs[0], "\r\n") 51 | subject := paragraphs[0] 52 | headers := make(map[string]string) 53 | for _, line := range paragraphs[1:] { 54 | tokens := strings.Split(line, ": ") 55 | headers[tokens[0]] = tokens[1] 56 | } 57 | return SipMessage{ 58 | Subject: subject, 59 | Headers: headers, 60 | Body: body, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sip_message_test.go: -------------------------------------------------------------------------------- 1 | package softphone 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestSipMessage(t *testing.T) { 10 | loadDotEnv() 11 | sipMessage := SipMessage{ 12 | Subject: "SIP/2.0 100 Trying", 13 | Headers: map[string]string{ 14 | "CSeq": "8082 REGISTER", 15 | "Call-ID": "21ee3d44-98d6-4bde-b541-fdc4dce63b13", 16 | }, 17 | Body: "", 18 | } 19 | 20 | sipMessage2 := FromStringToSipMessage(sipMessage.ToString()) 21 | 22 | if sipMessage.Subject != sipMessage2.Subject { 23 | t.Error("SipMessage Subject was changed during transformation") 24 | } 25 | if sipMessage.Body != sipMessage2.Body { 26 | t.Error("SipMessage Body was changed during transformation") 27 | } 28 | sipMessage.Headers["Content-Length"] = fmt.Sprintf("%d", len(sipMessage.Body)) 29 | if !reflect.DeepEqual(sipMessage.Headers, sipMessage2.Headers) { 30 | t.Error("SipMessage Headers was changed during transformation") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /softphone.go: -------------------------------------------------------------------------------- 1 | package softphone 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/google/uuid" 12 | "github.com/gorilla/websocket" 13 | "github.com/pion/webrtc/v3" 14 | "github.com/ringcentral/ringcentral-go" 15 | ) 16 | 17 | // Softphone softphone 18 | type Softphone struct { 19 | CreateSipRegistrationResponse ringcentral.CreateSipRegistrationResponse 20 | MessageListeners map[string]func(string) 21 | Conn *websocket.Conn 22 | OnInvite func(inviteMessage SipMessage) 23 | OnTrack func(track *webrtc.TrackRemote) 24 | } 25 | 26 | var fakeDomain = fmt.Sprintf("%s.invalid", uuid.New().String()) 27 | var fakeEmail = fmt.Sprintf("%s@%s", uuid.New().String(), fakeDomain) 28 | 29 | // Register register the softphone 30 | func (softphone *Softphone) Register() { 31 | softphone.MessageListeners = make(map[string]func(string)) 32 | sipInfo := softphone.CreateSipRegistrationResponse.SipInfo[0] 33 | url := url.URL{Scheme: strings.ToLower(sipInfo.Transport), Host: sipInfo.OutboundProxy, Path: ""} 34 | dialer := websocket.DefaultDialer 35 | dialer.Subprotocols = []string{"sip"} 36 | dialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 37 | conn, _, err := dialer.Dial(url.String(), nil) 38 | if err != nil { 39 | panic(err) 40 | } 41 | softphone.Conn = conn 42 | go func() { 43 | for { 44 | _, bytes, err := softphone.Conn.ReadMessage() 45 | if err != nil { 46 | panic(err) 47 | } 48 | message := string(bytes) 49 | log.Println("↓↓↓\n", message) 50 | for _, messageListener := range softphone.MessageListeners { 51 | go messageListener(message) 52 | } 53 | } 54 | }() 55 | 56 | registerMessage := SipMessage{ 57 | Subject: fmt.Sprintf("REGISTER sip:%s SIP/2.0", sipInfo.Domain), 58 | Headers: map[string]string{ 59 | "Call-ID": uuid.New().String(), 60 | "Contact": fmt.Sprintf(";expires=600", fakeEmail), 61 | "Via": fmt.Sprintf("SIP/2.0/WSS %s;branch=z9hG4bK%s", fakeDomain, uuid.New().String()), 62 | "From": fmt.Sprintf(";tag=%s", sipInfo.Username, sipInfo.Domain, uuid.New().String()), 63 | "To": fmt.Sprintf("", sipInfo.Username, sipInfo.Domain), 64 | "CSeq": "8082 REGISTER", 65 | }, 66 | Body: "", 67 | } 68 | softphone.Send(registerMessage, func(strMessage string) bool { 69 | if strings.Contains(strMessage, "SIP/2.0 401 Unauthorized") { 70 | unAuthMessage := FromStringToSipMessage(strMessage) 71 | authHeader := unAuthMessage.Headers["WWW-Authenticate"] 72 | regex := regexp.MustCompile(", nonce=\"(.+?)\"") 73 | match := regex.FindStringSubmatch(authHeader) 74 | nonce := match[1] 75 | 76 | registerMessage.Headers["Authorization"] = GenerateAuthorization(sipInfo, "REGISTER", nonce) 77 | registerMessage.IncreaseSeq() 78 | registerMessage.Headers["Via"] = fmt.Sprintf("SIP/2.0/TCP %s;branch=z9hG4bK%s", fakeDomain, uuid.New().String()) 79 | softphone.Send(registerMessage, nil) 80 | 81 | return true 82 | } 83 | return false 84 | }) 85 | 86 | softphone.addMessageListener(func(strMessage string) { 87 | if strings.Contains(strMessage, "INVITE sip:") { 88 | softphone.OnInvite(FromStringToSipMessage(strMessage)) 89 | } 90 | }) 91 | } 92 | 93 | // Answer answer an incoming call 94 | func (softphone *Softphone) Answer(inviteMessage SipMessage) { 95 | mediaEngine := webrtc.MediaEngine{} 96 | if err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{ 97 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/pcmu", ClockRate: 48000, Channels: 1, SDPFmtpLine: "", RTCPFeedback: nil}, 98 | PayloadType: 111, 99 | }, webrtc.RTPCodecTypeAudio); err != nil { 100 | panic(err) 101 | } 102 | api := webrtc.NewAPI(webrtc.WithMediaEngine(&mediaEngine)) 103 | config := webrtc.Configuration{ 104 | ICEServers: []webrtc.ICEServer{ 105 | { 106 | URLs: []string{"stun:74.125.194.127:19302"}, 107 | }, 108 | }, 109 | } 110 | peerConnection, err := api.NewPeerConnection(config) 111 | if err != nil { 112 | panic(err) 113 | } 114 | peerConnection.CreateDataChannel("audio", nil) 115 | if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil { 116 | panic(err) 117 | } 118 | 119 | var re = regexp.MustCompile(`\r\na=rtpmap:111 OPUS/48000/2\r\n`) 120 | // to workaround a pion/webrtc bug: https://github.com/pion/webrtc/issues/879 121 | sdp := re.ReplaceAllString(inviteMessage.Body, "\r\na=rtpmap:111 OPUS/48000/2\r\na=mid:0\r\n") 122 | offer := webrtc.SessionDescription{ 123 | Type: webrtc.SDPTypeOffer, 124 | SDP: sdp, 125 | } 126 | err = peerConnection.SetRemoteDescription(offer) 127 | if err != nil { 128 | panic(err) 129 | } 130 | 131 | answer, err := peerConnection.CreateAnswer(nil) 132 | if err != nil { 133 | panic(err) 134 | } 135 | gatherComplete := webrtc.GatheringCompletePromise(peerConnection) 136 | err = peerConnection.SetLocalDescription(answer) 137 | if err != nil { 138 | panic(err) 139 | } 140 | <-gatherComplete 141 | 142 | peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { 143 | log.Println("OnTrack") 144 | if softphone.OnTrack != nil { 145 | softphone.OnTrack(track) 146 | } 147 | }) 148 | 149 | responseMessage := SipMessage{ 150 | Subject: "SIP/2.0 200 OK", 151 | Headers: map[string]string{ 152 | "Contact": fmt.Sprintf("", fakeEmail), 153 | "Content-Type": "application/sdp", 154 | "Via": inviteMessage.Headers["Via"], 155 | "From": inviteMessage.Headers["From"], 156 | "CSeq": inviteMessage.Headers["CSeq"], 157 | "Call-Id": inviteMessage.Headers["Call-Id"], 158 | "To": fmt.Sprintf("%s;tag=%s", inviteMessage.Headers["To"], uuid.New().String()), 159 | }, 160 | Body: peerConnection.LocalDescription().SDP, 161 | } 162 | 163 | softphone.Send(responseMessage, nil) 164 | } 165 | -------------------------------------------------------------------------------- /softphone_test.go: -------------------------------------------------------------------------------- 1 | package softphone 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "os/user" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/joho/godotenv" 12 | "github.com/pion/webrtc/v3" 13 | "github.com/ringcentral/ringcentral-go" 14 | "github.com/zaf/g711" 15 | ) 16 | 17 | func loadDotEnv() { 18 | usr, err := user.Current() 19 | if err == nil { 20 | // ignore error because we fallback to sys env vars 21 | godotenv.Overload(usr.HomeDir + "/.env.prod") 22 | } 23 | } 24 | 25 | func TestSoftphone(t *testing.T) { 26 | loadDotEnv() 27 | rc := ringcentral.RestClient{ 28 | ClientID: os.Getenv("RINGCENTRAL_CLIENT_ID"), 29 | ClientSecret: os.Getenv("RINGCENTRAL_CLIENT_SECRET"), 30 | Server: os.Getenv("RINGCENTRAL_SERVER_URL"), 31 | } 32 | 33 | rc.Authorize(ringcentral.GetTokenRequest{ 34 | GrantType: "password", 35 | Username: os.Getenv("RINGCENTRAL_USERNAME"), 36 | Extension: os.Getenv("RINGCENTRAL_EXTENSION"), 37 | Password: os.Getenv("RINGCENTRAL_PASSWORD"), 38 | }) 39 | 40 | bytes := rc.Post("/restapi/v1.0/client-info/sip-provision", strings.NewReader(`{"sipInfo":[{"transport":"WSS"}]}`)) 41 | var createSipRegistrationResponse ringcentral.CreateSipRegistrationResponse 42 | json.Unmarshal(bytes, &createSipRegistrationResponse) 43 | 44 | if len(createSipRegistrationResponse.SipInfo) <= 0 { 45 | t.Error("No SipInfo") 46 | } 47 | 48 | softphone := Softphone{ 49 | CreateSipRegistrationResponse: createSipRegistrationResponse, 50 | } 51 | softphone.Register() 52 | 53 | softphone.OnInvite = func(inviteMessage SipMessage) { 54 | softphone.Answer(inviteMessage) 55 | } 56 | 57 | softphone.OnTrack = func(track *webrtc.TrackRemote) { 58 | fileName := "temp.raw" 59 | os.Remove(fileName) 60 | f, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) 61 | if err != nil { 62 | panic(err) 63 | } 64 | defer f.Close() 65 | for { 66 | rtp, _, err := track.ReadRTP() 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | // g711.DecodeUlaw(...): change u-law PCM to LPCM 71 | f.Write(g711.DecodeUlaw(rtp.Payload)) 72 | } 73 | } 74 | 75 | rc.Revoke() 76 | 77 | select {} //block forever 78 | } 79 | 80 | // to play the audio: play -b 16 -e signed -c 1 -r 8000 temp.raw 81 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package softphone 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/google/uuid" 9 | "github.com/ringcentral/ringcentral-go" 10 | ) 11 | 12 | // GenerateResponse generate response field in the authorization header 13 | func GenerateResponse(username string, password string, realm string, method string, uri string, nonce string) string { 14 | ha1 := md5.Sum([]byte(fmt.Sprintf("%s:%s:%s", username, realm, password))) 15 | ha2 := md5.Sum([]byte(fmt.Sprintf("%s:%s", method, uri))) 16 | response := md5.Sum([]byte(fmt.Sprintf("%x:%s:%x", ha1, nonce, ha2))) 17 | return fmt.Sprintf("%x", response) 18 | } 19 | 20 | // GenerateAuthorization generate the authorization header 21 | func GenerateAuthorization(sipInfo ringcentral.SIPInfoResponse, method string, nonce string) string { 22 | return fmt.Sprintf( 23 | `Digest algorithm=MD5, username="%s", realm="%s", nonce="%s", uri="sip:%s", response="%s"`, 24 | sipInfo.AuthorizationId, sipInfo.Domain, nonce, sipInfo.Domain, 25 | GenerateResponse(sipInfo.AuthorizationId, sipInfo.Password, sipInfo.Domain, method, "sip:"+sipInfo.Domain, nonce), 26 | ) 27 | } 28 | 29 | // Send send message via WebSocket 30 | func (softphone *Softphone) Send(sipMessage SipMessage, responseHandler func(string) bool) { 31 | stringMessage := sipMessage.ToString() 32 | log.Println("↑↑↑\n", stringMessage) 33 | if responseHandler != nil { 34 | var key string 35 | key = softphone.addMessageListener(func(message string) { 36 | done := responseHandler(message) 37 | if done { 38 | softphone.removeMessageListener(key) 39 | } 40 | }) 41 | } 42 | err := softphone.Conn.WriteMessage(1, []byte(stringMessage)) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | } 47 | func (softphone *Softphone) addMessageListener(messageListener func(string)) string { 48 | key := uuid.New().String() 49 | softphone.MessageListeners[key] = messageListener 50 | return key 51 | } 52 | func (softphone *Softphone) removeMessageListener(key string) { 53 | delete(softphone.MessageListeners, key) 54 | } 55 | --------------------------------------------------------------------------------