diff --git a/README.md b/README.md index 49a140fe..fa01a89b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/sip-to-webrtc/README.md b/sip-to-webrtc/README.md new file mode 100644 index 00000000..8ab13d3b --- /dev/null +++ b/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 + 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` diff --git a/sip-to-webrtc/main.go b/sip-to-webrtc/main.go new file mode 100644 index 00000000..73a2a607 --- /dev/null +++ b/sip-to-webrtc/main.go @@ -0,0 +1,122 @@ +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 websocket is available on") + port = flag.String("port", "5066", "Port that websocket is available on") +) + +func main() { + flag.Parse() + + if *host == "" || *port == "" || *password == "" { + panic("-host -port and -password are required") + } + + conn := softphone.NewSoftPhone(softphone.SIPInfoResponse{ + Username: *username, + AuthorizationID: *username, + Password: *password, + Domain: *host, + Transport: "ws", + OutboundProxy: *host + ":" + *port, + }) + + 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) +} diff --git a/sip-to-webrtc/output.ogg b/sip-to-webrtc/output.ogg new file mode 100644 index 00000000..6865540b Binary files /dev/null and b/sip-to-webrtc/output.ogg differ diff --git a/sip-to-webrtc/softphone/constants.go b/sip-to-webrtc/softphone/constants.go new file mode 100644 index 00000000..4e7419d0 --- /dev/null +++ b/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", +} diff --git a/sip-to-webrtc/softphone/inboundcall.go b/sip-to-webrtc/softphone/inboundcall.go new file mode 100644 index 00000000..fa4bd877 --- /dev/null +++ b/sip-to-webrtc/softphone/inboundcall.go @@ -0,0 +1,54 @@ +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(``, 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(";tag=%s", softphone.sipInfo.Username, softphone.sipInfo.Domain, softphone.fromTag) + sipMessage.headers["To"] = fmt.Sprintf("", msg.Hdr.From) + sipMessage.headers["Content-Type"] = "x-rc/agent" + sipMessage.addCseq(softphone).addCallID(*softphone).addUserAgent() + sipMessage.Body = fmt.Sprintf(``, msg.Hdr.SID, msg.Hdr.Req, msg.Hdr.To, msg.Hdr.From, softphone.sipInfo.AuthorizationID) + 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) + } + }) +} diff --git a/sip-to-webrtc/softphone/invite.go b/sip-to-webrtc/softphone/invite.go new file mode 100644 index 00000000..9fccbb8b --- /dev/null +++ b/sip-to-webrtc/softphone/invite.go @@ -0,0 +1,37 @@ +package softphone + +import ( + "fmt" + "regexp" +) + +// 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(";expires=200", softphone.FakeEmail) + sipMessage.headers["To"] = fmt.Sprintf("", extension, softphone.sipInfo.Domain) + sipMessage.headers["Via"] = fmt.Sprintf("SIP/2.0/WS %s;branch=%s", softphone.fakeDomain, branch()) + sipMessage.headers["From"] = fmt.Sprintf(";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 { + authenticateHeader := SIPMessage{}.FromString(message).headers["Proxy-Authenticate"] + regex := regexp.MustCompile(`, nonce="(.+?)"`) + nonce := regex.FindStringSubmatch(authenticateHeader)[1] + + sipMessage.addProxyAuthorization(*softphone, nonce, extension, "INVITE").addCseq(softphone).newViaBranch() + softphone.request(sipMessage, func(msg string) bool { + return false + }) + + return true + }) +} diff --git a/sip-to-webrtc/softphone/rcmessage.go b/sip-to-webrtc/softphone/rcmessage.go new file mode 100644 index 00000000..c592fcc7 --- /dev/null +++ b/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"` +} diff --git a/sip-to-webrtc/softphone/register.go b/sip-to-webrtc/softphone/register.go new file mode 100644 index 00000000..9a388c1d --- /dev/null +++ b/sip-to-webrtc/softphone/register.go @@ -0,0 +1,74 @@ +package softphone + +import ( + "context" + "crypto/tls" + "fmt" + "log" + "net/url" + "regexp" + "strings" + + "github.com/gorilla/websocket" +) + +func (softphone *Softphone) register() { + url := url.URL{Scheme: strings.ToLower(softphone.sipInfo.Transport), Host: softphone.sipInfo.OutboundProxy, Path: ""} + dialer := websocket.DefaultDialer + dialer.Subprotocols = []string{"sip"} + dialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint + + conn, _, err := dialer.Dial(url.String(), nil) + if err != nil { + log.Fatal(err) + } + + softphone.wsConn = conn + + go func() { + for { + _, bytes, err := conn.ReadMessage() + if err != nil { + log.Fatal(err) + } + + message := string(bytes) + // log.Print("↓↓↓\n", message) + + for _, ml := range softphone.messageListeners { + go ml(message) + } + } + }() + + sipMessage := SIPMessage{} + sipMessage.method = "REGISTER" + sipMessage.address = softphone.sipInfo.Domain + sipMessage.headers = make(map[string]string) + sipMessage.headers["Contact"] = fmt.Sprintf(";expires=200", softphone.FakeEmail) + sipMessage.headers["Via"] = fmt.Sprintf("SIP/2.0/WS %s;branch=%s", softphone.fakeDomain, branch()) + sipMessage.headers["From"] = fmt.Sprintf(";tag=%s", softphone.sipInfo.Username, softphone.sipInfo.Domain, softphone.fromTag) + sipMessage.headers["To"] = fmt.Sprintf("", softphone.sipInfo.Username, softphone.sipInfo.Domain) + sipMessage.headers["Organization"] = "ACE MEDIAS TOOLS" + sipMessage.headers["Supported"] = "path,ice" + sipMessage.addCseq(softphone).addCallID(*softphone).addUserAgent() + + registered, registeredFunc := context.WithCancel(context.Background()) + + softphone.request(sipMessage, func(message string) bool { + authenticateHeader := SIPMessage{}.FromString(message).headers["WWW-Authenticate"] + regex := regexp.MustCompile(`, nonce="(.+?)"`) + nonce := regex.FindStringSubmatch(authenticateHeader)[1] + + sipMessage.addAuthorization(*softphone, nonce, "REGISTER").addCseq(softphone).newViaBranch() + softphone.request(sipMessage, func(msg string) bool { + registeredFunc() + + return false + }) + + return true + }) + + <-registered.Done() +} diff --git a/sip-to-webrtc/softphone/sipmessage.go b/sip-to-webrtc/softphone/sipmessage.go new file mode 100644 index 00000000..5b80f8a6 --- /dev/null +++ b/sip-to-webrtc/softphone/sipmessage.go @@ -0,0 +1,99 @@ +package softphone + +import ( + "fmt" + "regexp" + "strings" +) + +// SIPMessage ... +type SIPMessage struct { + method string + address string + subject string + headers map[string]string + Body string +} + +func (sm *SIPMessage) addAuthorization(softphone Softphone, nonce, method string) *SIPMessage { + sm.headers["Authorization"] = generateAuthorization(softphone.sipInfo, method, nonce) + + return sm +} + +func (sm *SIPMessage) addProxyAuthorization(softphone Softphone, nonce, user, method string) *SIPMessage { + sm.headers["Proxy-Authorization"] = generateProxyAuthorization(softphone.sipInfo, method, user, nonce) + + return sm +} + +func (sm *SIPMessage) newViaBranch() { + if val, ok := sm.headers["Via"]; ok { + sm.headers["Via"] = regexp.MustCompile(";branch=z9hG4bK.+?$").ReplaceAllString(val, ";branch="+branch()) + } +} + +func (sm *SIPMessage) addCseq(softphone *Softphone) *SIPMessage { + sm.headers["CSeq"] = fmt.Sprintf("%d %s", softphone.cseq, sm.method) + softphone.cseq++ + + return sm +} + +func (sm *SIPMessage) addCallID(softphone Softphone) *SIPMessage { + sm.headers["Call-ID"] = softphone.callID + + return sm +} + +func (sm *SIPMessage) addUserAgent() { + sm.headers["User-Agent"] = "Pion WebRTC SIP Client" +} + +// ToString ... +func (sm SIPMessage) ToString() string { + arr := []string{fmt.Sprintf("%s sip:%s SIP/2.0", sm.method, sm.address)} + for k, v := range sm.headers { + arr = append(arr, fmt.Sprintf("%s: %s", k, v)) + } + + arr = append(arr, fmt.Sprintf("Content-Length: %d", len(sm.Body))) + arr = append(arr, "Max-Forwards: 70") + arr = append(arr, "", sm.Body) + + return strings.Join(arr, "\r\n") +} + +// FromString ... +func (sm SIPMessage) FromString(s string) SIPMessage { + parts := strings.Split(s, "\r\n\r\n") + sm.Body = strings.Join(parts[1:], "\r\n\r\n") + parts = strings.Split(parts[0], "\r\n") + sm.subject = parts[0] + sm.headers = make(map[string]string) + + for _, line := range parts[1:] { + tokens := strings.Split(line, ": ") + sm.headers[tokens[0]] = tokens[1] + } + + return sm +} + +// Response ... +func (sm SIPMessage) Response(softphone Softphone, statusCode int, headers map[string]string, body string) string { + arr := []string{fmt.Sprintf("SIP/2.0 %d %s", statusCode, responseCodes[statusCode])} + for _, key := range []string{"Via", "From", "Call-ID", "CSeq"} { + arr = append(arr, fmt.Sprintf("%s: %s", key, sm.headers[key])) + } + + for k, v := range headers { + arr = append(arr, fmt.Sprintf("%s: %s", k, v)) + } + + arr = append(arr, "Supported: outbound", fmt.Sprintf("To: %s;tag=%s", sm.headers["To"], softphone.toTag)) + arr = append(arr, fmt.Sprintf("Content-Length: %d", len(body))) + arr = append(arr, "", body) + + return strings.Join(arr, "\r\n") +} diff --git a/sip-to-webrtc/softphone/softphone.go b/sip-to-webrtc/softphone/softphone.go new file mode 100644 index 00000000..327a2c6a --- /dev/null +++ b/sip-to-webrtc/softphone/softphone.go @@ -0,0 +1,100 @@ +// Package softphone provides abstractions for SIP over Websocket +package softphone + +import ( + "fmt" + "log" + "math/rand" + "strings" + + "github.com/google/uuid" + "github.com/gorilla/websocket" +) + +// Softphone ... +type Softphone struct { + OnInvite func(inviteMessage SIPMessage) // sipmessage.go + sipInfo SIPInfoResponse // util.go + wsConn *websocket.Conn + fakeDomain string + FakeEmail string + fromTag string + toTag string + callID string + cseq int + messageListeners map[string]func(string) + inviteKey string + messages chan string +} + +// NewSoftPhone ... +func NewSoftPhone(sipInfo SIPInfoResponse) *Softphone { + softphone := Softphone{} + softphone.OnInvite = func(inviteMessage SIPMessage) {} + softphone.fakeDomain = uuid.New().String() + ".invalid" + softphone.FakeEmail = uuid.New().String() + "@" + softphone.fakeDomain + softphone.fromTag = uuid.New().String() + softphone.toTag = uuid.New().String() + softphone.callID = uuid.New().String() + softphone.cseq = rand.Intn(10000) + 1 //nolint + softphone.messageListeners = make(map[string]func(string)) + softphone.sipInfo = sipInfo + softphone.register() + + return &softphone +} + +func (softphone *Softphone) addMessageListener(messageListener func(string)) string { + key := uuid.New().String() + softphone.messageListeners[key] = messageListener + + return key +} + +func (softphone *Softphone) removeMessageListener(key string) { + delete(softphone.messageListeners, key) +} + +func (softphone Softphone) request2(sipMessage SIPMessage, expectedResp string) string { + fmt.Println(sipMessage.ToString()) + + if err := softphone.wsConn.WriteMessage(1, []byte(sipMessage.ToString())); err != nil { + log.Panic(err) + } + + if expectedResp != "" { + for { + message := <-softphone.messages + if strings.Contains(message, expectedResp) { + return message + } + } + } + + return "" +} + +func (softphone *Softphone) request(sipMessage SIPMessage, responseHandler func(string) bool) { + // log.Print("↑↑↑\n", sipMessage.ToString()) + if responseHandler != nil { + var key string + key = softphone.addMessageListener(func(message string) { + done := responseHandler(message) + if done { + softphone.removeMessageListener(key) + } + }) + } + + if err := softphone.wsConn.WriteMessage(1, []byte(sipMessage.ToString())); err != nil { + log.Fatal(err) + } +} + +func (softphone *Softphone) response(message string) { + log.Print("↑↑↑\n", message) + + if err := softphone.wsConn.WriteMessage(1, []byte(message)); err != nil { + log.Fatal(err) + } +} diff --git a/sip-to-webrtc/softphone/utils.go b/sip-to-webrtc/softphone/utils.go new file mode 100644 index 00000000..4220c1d9 --- /dev/null +++ b/sip-to-webrtc/softphone/utils.go @@ -0,0 +1,48 @@ +package softphone + +import ( + "crypto/md5" //nolint + "fmt" + + "github.com/google/uuid" +) + +// SIPInfoResponse ... +type SIPInfoResponse struct { + Username string `json:"username"` + Password string `json:"password"` + AuthorizationID string `json:"authorizationId"` + Domain string `json:"domain"` + OutboundProxy string `json:"outboundProxy"` + Transport string `json:"transport"` + Certificate string `json:"certificate"` + SwitchBackInterval int `json:"switchBackInterval"` +} + +func generateResponse(username, password, realm, method, uri, nonce string) string { // ONLY REGISTRATION WITH QOP=AUTH ! + ha1 := md5.Sum([]byte(fmt.Sprintf("%s:%s:%s", username, realm, password))) //nolint + ha2 := md5.Sum([]byte(fmt.Sprintf("%s:%s", method, uri))) //nolint + response := md5.Sum([]byte(fmt.Sprintf("%x:%s:00000001:%s:auth:%x", ha1, nonce, "0e6758e1adfccffbd0ad9ffdde3ef655", ha2))) //nolint + + return fmt.Sprintf("%x", response) +} + +func generateAuthorization(sipInfo SIPInfoResponse, method, nonce string) string { + return fmt.Sprintf( + `Digest username="%s",realm="%s",nonce="%s",uri="sip:%s",response="%s",algorithm=MD5,cnonce="%s",qop=auth,nc=00000001`, + sipInfo.Username, sipInfo.Domain, nonce, sipInfo.Domain, + generateResponse(sipInfo.Username, sipInfo.Password, sipInfo.Domain, method, "sip:"+sipInfo.Domain, nonce), "0e6758e1adfccffbd0ad9ffdde3ef655", + ) +} + +func generateProxyAuthorization(sipInfo SIPInfoResponse, method, targetUser, nonce string) string { + return fmt.Sprintf( + `Digest username="%s", realm="%s", nonce="%s", uri="sip:%s@%s", response="%s",algorithm=MD5,cnonce="%s",qop=auth,nc=00000001`, + sipInfo.AuthorizationID, sipInfo.Domain, nonce, targetUser, sipInfo.Domain, + generateResponse(sipInfo.AuthorizationID, sipInfo.Password, sipInfo.Domain, method, "sip:"+targetUser+"@"+sipInfo.Domain, nonce), "0e6758e1adfccffbd0ad9ffdde3ef655", + ) +} + +func branch() string { + return "z9hG4bK" + uuid.New().String() +}