Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update of the softphone librairy to work with asterisk (Digest Auth updated) #76

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -70,6 +70,7 @@ Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contribu
* [Cameron Elliott](https://github.com/cameronelliott) - *Small race bug fix*
* [Jamie Good](https://github.com/jamiegood) - *Bug fix in jsfiddle example*
* [PhVHoang](https://github.com/PhVHoang)
* [Pascal Benoit](https://github.com/pascal-ace)

### License
MIT License - see [LICENSE](LICENSE) for full text
3 changes: 0 additions & 3 deletions go.mod

This file was deleted.

27 changes: 27 additions & 0 deletions sip-to-webrtc/README.md
@@ -0,0 +1,27 @@
# sip-to-webrtc
sip-to-webrtc demonstrates how you can connect to a SIP over WebRTC endpoint. This example connects to an extension
and saves the audio to a ogg file.

## Instructions
### Setup FreeSWITCH (or SIP over WebSocket Server)
With a fresh install of FreeSWITCH all you need to do is

* Enable `ws-binding`
* Set a `default_password` to something you know

### Run `sip-to-webrtc`
Run `go run *.go -h` to see the arguments of the program. If everything is working
this is the output you will see.

```
$ go run *.go -host 172.17.0.2 -password Aelo1ievoh2oopooTh2paijaeNaidiek -ws_url ws://172.17.0.2:5066
Connection State has changed checking
Connection State has changed connected
Got Opus track, saving to disk as output.ogg
Connection State has changed disconnected
```

### Play the audio file
ffmpeg's in-tree Opus decoder isn't able to play the default audio file from FreeSWITCH. Use the following command to force libopus.

`ffplay -acodec libopus output.ogg`
120 changes: 120 additions & 0 deletions sip-to-webrtc/main2.go
@@ -0,0 +1,120 @@
package main

import (
"flag"
"fmt"

"github.com/pion/example-webrtc-applications/sip-to-webrtc/softphone"
"github.com/pion/sdp/v2"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/pkg/media/oggwriter"
)

var (
username = flag.String("username", "1000", "Extension you wish to register as")
password = flag.String("password", "", "Password for the extension you wish to register as")
extension = flag.String("extension", "9198", "Extension you wish to call")
host = flag.String("host", "", "Host that ipbx is available on")
wsURL = flag.String("ws_url", "", "URL that websocket is available on")
)

func main() {
flag.Parse()

if *host == "" || *wsURL == "" || *password == "" {
panic("-host -port and -password are required")
}

conn := softphone.NewSoftPhone(softphone.SIPInfoResponse{
Username: *username,
Password: *password,
Domain: *host,
WebsocketURL: *wsURL,
})

pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
if err != nil {
panic(err)
}

pc.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
})

oggFile, err := oggwriter.New("output.ogg", 48000, 2)
if err != nil {
panic(err)
}

pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
fmt.Println("Got Opus track, saving to disk as output.ogg")

for {
rtpPacket, _, readErr := track.ReadRTP()
if readErr != nil {
panic(readErr)
}
if readErr := oggFile.WriteRTP(rtpPacket); readErr != nil {
panic(readErr)
}
}
})

if _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil {
panic(err)
}

offer, err := pc.CreateOffer(nil)
if err != nil {
panic(err)
}

if err := pc.SetLocalDescription(offer); err != nil {
panic(err)
}

gotAnswer := false

conn.OnOK(func(okBody string) {
if gotAnswer {
return
}
gotAnswer = true

okBody += "a=mid:0\r\n"
if err := pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: okBody}); err != nil {
panic(err)
}
})
conn.Invite(*extension, rewriteSDP(offer.SDP))

select {}
}

// Apply the following transformations for FreeSWITCH
// * Add fake srflx candidate to each media section
// * Add msid to each media section
// * Make bundle first attribute at session level.
func rewriteSDP(in string) string {
parsed := &sdp.SessionDescription{}
if err := parsed.Unmarshal([]byte(in)); err != nil {
panic(err)
}

// Reverse global attributes
for i, j := 0, len(parsed.Attributes)-1; i < j; i, j = i+1, j-1 {
parsed.Attributes[i], parsed.Attributes[j] = parsed.Attributes[j], parsed.Attributes[i]
}

parsed.MediaDescriptions[0].Attributes = append(parsed.MediaDescriptions[0].Attributes, sdp.Attribute{
Key: "candidate",
Value: "79019993 1 udp 1686052607 1.1.1.1 9 typ srflx",
})

out, err := parsed.Marshal()
if err != nil {
panic(err)
}

return string(out)
}
Binary file added sip-to-webrtc/output.ogg
Binary file not shown.
77 changes: 77 additions & 0 deletions sip-to-webrtc/softphone/constants.go
@@ -0,0 +1,77 @@
package softphone

var responseCodes = map[int]string{
100: "Trying",
180: "Ringing",
181: "Call is Being Forwarded",
182: "Queued",
183: "Session Progress",
199: "Early Dialog Terminated",
200: "OK",
202: "Accepted",
204: "No Notification",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily",
305: "Use Proxy",
380: "Alternative Service",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Conditional Request Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Unsupported URI Scheme",
417: "Unknown Resource-Priority",
420: "Bad Extension",
421: "Extension Required",
422: "Session Interval Too Small",
423: "Interval Too Brief",
424: "Bad Location Information",
428: "Use Identity Header",
429: "Provide Referrer Identity",
433: "Anonymity Disallowed",
436: "Bad Identity-Info",
437: "Unsupported Certificate",
438: "Invalid Identity Header",
439: "First Hop Lacks Outbound Support",
440: "Max-Breadth Exceeded",
469: "Bad Info Package",
470: "Consent Needed",
480: "Temporarily Unavailable",
481: "Call/Transaction Does Not Exist",
482: "Loop Detected",
483: "Too Many Hops",
484: "Address Incomplete",
485: "Ambiguous",
486: "Busy Here",
487: "Request Terminated",
488: "Not Acceptable Here",
489: "Bad Event",
491: "Request Pending",
493: "Undecipherable",
494: "Security Agreement Required",
500: "Server Internal Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Server Time-out",
505: "Version Not Supported",
513: "Message Too Large",
580: "Precondition Failure",
600: "Busy Everywhere",
603: "Decline",
604: "Does Not Exist Anywhere",
606: "Not Acceptable",
607: "Unwanted",
}
55 changes: 55 additions & 0 deletions sip-to-webrtc/softphone/inboundcall.go
@@ -0,0 +1,55 @@
package softphone

import (
"encoding/xml"
"fmt"
"log"
"strings"
)

// OpenToInvite adds a handler that responds to any incoming invites.
func (softphone *Softphone) OpenToInvite() {
softphone.inviteKey = softphone.addMessageListener(func(message string) {
if strings.HasPrefix(message, "INVITE sip:") {
inviteMessage := SIPMessage{}.FromString(message)

dict := map[string]string{"Contact": fmt.Sprintf(`<sip:%s;transport=ws>`, softphone.fakeDomain)}
responseMsg := inviteMessage.Response(*softphone, 180, dict, "")
softphone.response(responseMsg)

var msg Msg
if err := xml.Unmarshal([]byte(inviteMessage.headers["P-rc"]), &msg); err != nil {
log.Panic(err)
}
sipMessage := SIPMessage{}
sipMessage.method = "MESSAGE"
sipMessage.address = msg.Hdr.From
sipMessage.headers = make(map[string]string)
sipMessage.headers["Via"] = fmt.Sprintf("SIP/2.0/WSS %s;branch=%s", softphone.fakeDomain, branch())
sipMessage.headers["From"] = fmt.Sprintf("<sip:%s@%s>;tag=%s", softphone.sipInfo.Username, softphone.sipInfo.Domain, softphone.fromTag)
sipMessage.headers["To"] = fmt.Sprintf("<sip:%s>", msg.Hdr.From)
sipMessage.headers["Content-Type"] = "x-rc/agent"
sipMessage.addCseq(softphone).addCallID(*softphone).addUserAgent()
sipMessage.Body = fmt.Sprintf(`<Msg><Hdr SID="%s" Req="%s" From="%s" To="%s" Cmd="17"/><Bdy Cln="%s"/></Msg>`, msg.Hdr.SID, msg.Hdr.Req, msg.Hdr.To, msg.Hdr.From, softphone.sipInfo.Username)
softphone.request(sipMessage, nil)

softphone.OnInvite(inviteMessage)
}
})
}

// CloseToInvite removes the previously set invite listener.
func (softphone *Softphone) CloseToInvite() {
softphone.removeMessageListener(softphone.inviteKey)
}

// OnOK adds a handler that responds to any incoming ok events.
func (softphone *Softphone) OnOK(hdlr func(string)) {

softphone.addMessageListener(func(message string) {
if strings.HasPrefix(message, "SIP/2.0 200 OK") {
parsed := SIPMessage{}.FromString(message)
hdlr(parsed.Body)
}
})
}
67 changes: 67 additions & 0 deletions sip-to-webrtc/softphone/invite.go
@@ -0,0 +1,67 @@
package softphone

import (
"fmt"
"strings"
)

// Invite ...
func (softphone *Softphone) Invite(extension, offer string) {
sipMessage := SIPMessage{headers: map[string]string{}}

sipMessage.method = "INVITE"
sipMessage.address = softphone.sipInfo.Domain

sipMessage.headers["Contact"] = fmt.Sprintf("<sip:%s;transport=ws>;expires=200", softphone.FakeEmail)
sipMessage.headers["To"] = fmt.Sprintf("<sip:%s@%s>", extension, softphone.sipInfo.Domain)
sipMessage.headers["Via"] = fmt.Sprintf("SIP/2.0/WS %s;branch=%s", softphone.fakeDomain, branch())
sipMessage.headers["From"] = fmt.Sprintf("<sip:%s@%s>;tag=%s", softphone.sipInfo.Username, softphone.sipInfo.Domain, softphone.fromTag)
sipMessage.headers["Supported"] = "replaces, outbound,ice"
sipMessage.addCseq(softphone).addCallID(*softphone).addUserAgent()

sipMessage.headers["Content-Type"] = "application/sdp"
sipMessage.Body = offer

softphone.request(sipMessage, func(message string) bool {
proxyAuthenticateHeader := SIPMessage{}.FromString(message).headers["Proxy-Authenticate"]
authenticateHeader := SIPMessage{}.FromString(message).headers["WWW-Authenticate"]

var ai AuthInfo
if len(authenticateHeader) > 0 { //WWW-Authenticate
ai = GetAuthInfo(authenticateHeader)
ai.AuthType = "Authorization"
ai.Uri = "sip:"+ extension + "@"+ softphone.sipInfo.Domain
ai.Method = "INVITE"
} else if len(proxyAuthenticateHeader) > 0 { //Proxy-Authenticate
ai = GetAuthInfo(proxyAuthenticateHeader)
ai.AuthType = "Proxy-Authorization"
ai.Uri = "sip:"+ extension + "@"+ softphone.sipInfo.Domain
ai.Method = "INVITE"
} else {
panic("FAIL TO SEND INVITE")
}
fmt.Printf("%+v\n", ai)
sipMessage.addAuthorization(*softphone, ai).addCseq(softphone).newViaBranch()
softphone.request(sipMessage, func(msg string) bool {
responseStatus := strings.Split(strings.Split(msg, "\r\n")[0], " ")[1]
textStatus := strings.Split(strings.Split(msg, "\r\n")[0], " ")[2]
switch responseStatus {
case "100":
fmt.Println(textStatus)
return false //Continue the handler
case "183":
fmt.Println(textStatus)
return false //Continue the handler

case "200":
fmt.Println("### INVITE SUCCESS ###")
return true
default: //480
panic("INVITE FAILED : " + responseStatus + " " + textStatus)
}
return true
})

return true
})
}
32 changes: 32 additions & 0 deletions sip-to-webrtc/softphone/rcmessage.go
@@ -0,0 +1,32 @@
package softphone

import "encoding/xml"

// Msg ...
type Msg struct {
XMLName xml.Name `xml:"Msg"`
Hdr Hdr `xml:"Hdr"`
Bdy Bdy `xml:"Bdy"`
}

// Hdr ...
type Hdr struct {
XMLName xml.Name `xml:"Hdr"`
SID string `xml:"SID,attr"`
Req string `xml:"Req,attr"`
From string `xml:"From,attr"`
To string `xml:"To,attr"`
Cmd string `xml:"Cmd,attr"`
}

// Bdy ...
type Bdy struct {
XMLName xml.Name `xml:"Bdy"`
SrvLvl string `xml:"SrvLvl,attr"`
SrvLvlExt string `xml:"SrvLvlExt,attr"`
Phn string `xml:"Phn,attr"`
Nm string `xml:"Nm,attr"`
ToPhn string `xml:"ToPhn,attr"`
ToNm string `xml:"ToNm,attr"`
RecURL string `xml:"RecUrl,attr"`
}