emiago / sipgo

SIP library for writing fast SIP services in GO
BSD 2-Clause "Simplified" License
438 stars 44 forks source link

[QUESTION] - Cannot connect from JS to Server using WS #94

Open sujit-baniya opened 3 weeks ago

sujit-baniya commented 3 weeks ago

I'm trying to use following code

server.go

package main

import (
    "context"
    "crypto/tls"
    "flag"
    "fmt"
    "os"
    "strings"
    "time"

    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"

    "github.com/emiago/sipgo"
    "github.com/emiago/sipgo/sip"
    "github.com/emiago/sipgo/utils"

    "github.com/icholy/digest"
)

func main() {
    defIP := utils.LocalIp()
    extIP := flag.String("ip", defIP+":5060", "My external ip")
    creds := flag.String("u", "alice:alice,bob:bob", "Coma seperated username:password list")
    tran := flag.String("t", "ws", "Transport")
    tlskey := flag.String("tlskey", "", "TLS key path")
    tlscrt := flag.String("tlscrt", "", "TLS crt path")
    flag.Parse()

    // Make SIP Debugging available
    sip.SIPDebug = os.Getenv("SIP_DEBUG") != ""

    zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMicro
    log.Logger = zerolog.New(zerolog.ConsoleWriter{
        Out:        os.Stdout,
        TimeFormat: time.StampMicro,
    }).With().Timestamp().Logger().Level(zerolog.InfoLevel)

    if lvl, err := zerolog.ParseLevel(os.Getenv("LOG_LEVEL")); err == nil && lvl != zerolog.NoLevel {
        log.Logger = log.Logger.Level(lvl)
    }

    registry := make(map[string]string)
    for _, c := range strings.Split(*creds, ",") {
        arr := strings.Split(c, ":")
        registry[arr[0]] = arr[1]
    }

    ua, err := sipgo.NewUA(
        sipgo.WithUserAgent("SIPGO"),
        // sipgo.WithUserAgentIP(*extIP),
    )
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to setup user agent")
    }

    srv, err := sipgo.NewServer(ua)
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to setup server handle")
    }

    ctx := context.TODO()

    // NOTE: This server only supports 1 REGISTRATION/Chalenge
    // This needs to be rewritten in better way
    var chal digest.Challenge
    srv.OnRegister(func(req *sip.Request, tx sip.ServerTransaction) {
        // https://www.rfc-editor.org/rfc/rfc2617#page-6
        h := req.GetHeader("Authorization")
        if h == nil {
            chal = digest.Challenge{
                Realm:     "sipgo-server",
                Nonce:     fmt.Sprintf("%d", time.Now().UnixMicro()),
                Opaque:    "sipgo",
                Algorithm: "MD5",
            }

            res := sip.NewResponseFromRequest(req, 401, "Unathorized", nil)
            res.AppendHeader(sip.NewHeader("WWW-Authenticate", chal.String()))

            tx.Respond(res)
            return
        }

        cred, err := digest.ParseCredentials(h.Value())
        if err != nil {
            log.Error().Err(err).Msg("parsing creds failed")
            tx.Respond(sip.NewResponseFromRequest(req, 401, "Bad credentials", nil))
            return
        }

        // Check registry
        passwd, exists := registry[cred.Username]
        if !exists {
            tx.Respond(sip.NewResponseFromRequest(req, 404, "Bad authorization header", nil))
            return
        }

        // Make digest and compare response
        digCred, err := digest.Digest(&chal, digest.Options{
            Method:   "REGISTER",
            URI:      cred.URI,
            Username: cred.Username,
            Password: passwd,
        })

        if err != nil {
            log.Error().Err(err).Msg("Calc digest failed")
            tx.Respond(sip.NewResponseFromRequest(req, 401, "Bad credentials", nil))
            return
        }

        if cred.Response != digCred.Response {
            tx.Respond(sip.NewResponseFromRequest(req, 401, "Unathorized", nil))
            return
        }
        log.Info().Str("username", cred.Username).Str("source", req.Source()).Msg("New client registered")
        tx.Respond(sip.NewResponseFromRequest(req, 200, "OK", nil))
    })

    log.Info().Str("addr", *extIP).Msg("Listening on")

    switch *tran {
    case "tls", "wss":

        cert, err := tls.LoadX509KeyPair(*tlscrt, *tlskey)
        if err != nil {

            log.Fatal().Err(err).Msg("Fail to load  x509 key and crt")
        }
        if err := srv.ListenAndServeTLS(ctx, *tran, *extIP, &tls.Config{Certificates: []tls.Certificate{cert}}); err != nil {
            log.Info().Err(err).Msg("Listening stop")
        }
        return
    }

    srv.ListenAndServe(ctx, *tran, *extIP)
}

client.go

package main

import (
    "context"
    "flag"
    "fmt"
    stdLog "log"
    "net/http"
    "os"
    "strings"
    "time"

    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"

    "github.com/emiago/sipgo"
    "github.com/emiago/sipgo/sip"
    "github.com/emiago/sipgo/utils"

    "github.com/icholy/digest"
)

func runWeb() {
    // Define the directory where static files are stored
    staticDir := "./static"

    // Check if the static directory exists
    if _, err := os.Stat(staticDir); os.IsNotExist(err) {
        stdLog.Fatalf("Static directory %s does not exist", staticDir)
    }

    // Serve static files from the defined directory
    fs := http.FileServer(http.Dir(staticDir))
    http.Handle("/", fs)

    // Start the HTTP server on port 8080
    stdLog.Println("Serving on http://localhost:8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        stdLog.Fatalf("Failed to start server: %v", err)
    }
}

func main() {
    defIP := utils.LocalIp()
    dst := flag.String("ip", defIP+":5060", "My external ip")
    inter := flag.String("h", "localhost", "My interface ip or hostname")
    tran := flag.String("t", "ws", "Transport")
    username := flag.String("u", "alice", "SIP Username")
    password := flag.String("p", "alice", "Password")
    flag.Parse()

    // Make SIP Debugging available
    sip.SIPDebug = os.Getenv("SIP_DEBUG") != ""

    zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMicro
    log.Logger = zerolog.New(zerolog.ConsoleWriter{
        Out:        os.Stdout,
        TimeFormat: time.StampMicro,
    }).With().Timestamp().Logger().Level(zerolog.InfoLevel)

    if lvl, err := zerolog.ParseLevel(os.Getenv("LOG_LEVEL")); err == nil && lvl != zerolog.NoLevel {
        log.Logger = log.Logger.Level(lvl)
    }

    // Setup UAC
    ua, err := sipgo.NewUA(
        sipgo.WithUserAgent(*username),
    )
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to setup user agent")
    }

    client, err := sipgo.NewClient(ua, sipgo.WithClientHostname(*inter))
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to setup client handle")
    }
    defer client.Close()

    // Create basic REGISTER request structure
    recipient := &sip.Uri{}
    sip.ParseUri(fmt.Sprintf("sip:%s@%s", *username, *dst), recipient)
    req := sip.NewRequest(sip.REGISTER, *recipient)
    req.AppendHeader(
        sip.NewHeader("Contact", fmt.Sprintf("<sip:%s@%s>", *username, *inter)),
    )
    req.SetTransport(strings.ToUpper(*tran))

    // Send request and parse response
    // req.SetDestination(*dst)
    log.Info().Msg(req.StartLine())
    ctx := context.Background()
    tx, err := client.TransactionRequest(ctx, req)
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to create transaction")
    }
    defer tx.Terminate()

    res, err := getResponse(tx)
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to get response")
    }

    log.Info().Int("status", int(res.StatusCode)).Msg("Received status")
    if res.StatusCode == 401 {
        // Get WwW-Authenticate
        wwwAuth := res.GetHeader("WWW-Authenticate")
        chal, err := digest.ParseChallenge(wwwAuth.Value())
        if err != nil {
            log.Fatal().Str("wwwauth", wwwAuth.Value()).Err(err).Msg("Fail to parse challenge")
        }

        // Reply with digest
        cred, _ := digest.Digest(chal, digest.Options{
            Method:   req.Method.String(),
            URI:      recipient.Host,
            Username: *username,
            Password: *password,
        })

        newReq := req.Clone()
        newReq.RemoveHeader("Via") // Must be regenerated by tranport layer
        newReq.AppendHeader(sip.NewHeader("Authorization", cred.String()))

        ctx := context.Background()
        tx, err := client.TransactionRequest(ctx, newReq, sipgo.ClientRequestAddVia)
        if err != nil {
            log.Fatal().Err(err).Msg("Fail to create transaction")
        }
        defer tx.Terminate()

        res, err = getResponse(tx)
        if err != nil {
            log.Fatal().Err(err).Msg("Fail to get response")
        }
    }

    if res.StatusCode != 200 {
        log.Fatal().Msg("Fail to register")
    }

    log.Info().Msg("Client registered")
    runWeb()
}

func getResponse(tx sip.ClientTransaction) (*sip.Response, error) {
    select {
    case <-tx.Done():
        return nil, fmt.Errorf("transaction died")
    case res := <-tx.Responses():
        return res, nil
    }
}

index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="https:////cdnjs.cloudflare.com/ajax/libs/jssip/0.7.23/jssip.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script>
        var configuration = {
            'uri': 'sip:alice@192.168.18.8:5060', // FILL SIP URI HERE like sip:sip-user@your-domain.bwapp.bwsip.io
            'password': 'alice', // FILL PASSWORD HERE,
            'ws_servers': 'ws://192.168.18.8:5060'
        };

        var incomingCallAudio = new window.Audio('/incoming_alert.wav');
        incomingCallAudio.loop = true;
        var remoteAudio = new window.Audio();
        remoteAudio.autoplay = true;

        var callOptions = {
            mediaConstraints: {audio: true, video: false}
        };

        var phone;
        if(configuration.uri && configuration.password){
            JsSIP.debug.enable('JsSIP:*'); // more detailed debug output
            phone = new JsSIP.UA(configuration);
            phone.on('registrationFailed', function(ev){
                alert('Registering on SIP server failed with error: ' + ev.cause);
                configuration.uri = null;
                configuration.password = null;
                updateUI();
            });
            phone.on('newRTCSession',function(ev){
                var newSession = ev.session;
                if(session){ // hangup any existing call
                    session.terminate();
                }
                session = newSession;
                var completeSession = function(){
                    session = null;
                    updateUI();
                };
                session.on('ended', completeSession);
                session.on('failed', completeSession);
                session.on('accepted',updateUI);
                session.on('confirmed',function(){
                    var localStream = session.connection.getLocalStreams()[0];
                    var dtmfSender = session.connection.createDTMFSender(localStream.getAudioTracks()[0])
                    session.sendDTMF = function(tone){
                        dtmfSender.insertDTMF(tone);
                    };
                    updateUI();
                });
                session.on('addstream', function(e){
                    incomingCallAudio.pause();
                    remoteAudio.src = window.URL.createObjectURL(e.stream);
                });
                if(session.direction === 'incoming'){
                    incomingCallAudio.play();
                }
                updateUI();
            });
            phone.start();
        }

        var session;
        updateUI();

        $('#connectCall').click(function () {
            var dest = $('#toField').val();
            phone.call(dest, callOptions);
            updateUI();
        });

        $('#answer').click(function(){
            session.answer(callOptions);
        });

        var hangup = function(){
            session.terminate();
        };

        $('#hangUp').click(hangup);
        $('#reject').click(hangup);

        $('#mute').click(function(){
            console.log('MUTE CLICKED');
            if(session.isMuted().audio){
                session.unmute({audio: true});
            }else{
                session.mute({audio: true});
            }
            updateUI();
        });
        $('#toField').keypress(function(e){
            if(e.which === 13){//enter
                $('#connectCall').click();
            }
        });
        $('#inCallButtons').on('click', '.dialpad-char', function (e) {
            var $target = $(e.target);
            var value = $target.data('value');
            session.sendDTMF(value.toString());
        });
        function updateUI(){
            if(configuration.uri && configuration.password){
                $('#errorMessage').hide();
                $('#wrapper').show();
                if(session){
                    if(session.isInProgress()){
                        if(session.direction === 'incoming'){
                            $('#incomingCallNumber').html(session.remote_identity.uri);
                            $('#incomingCall').show();
                            $('#callControl').hide()
                            $('#incomingCall').show();
                        }else{
                            $('#callInfoText').html('Ringing...');
                            $('#callInfoNumber').html(session.remote_identity.uri.user);
                            $('#callStatus').show();
                        }

                    }else if(session.isEstablished()){
                        $('#callStatus').show();
                        $('#incomingCall').hide();
                        $('#callInfoText').html('In Call');
                        $('#callInfoNumber').html(session.remote_identity.uri.user);
                        $('#inCallButtons').show();
                        incomingCallAudio.pause();
                    }
                    $('#callControl').hide();
                }else{
                    $('#incomingCall').hide();
                    $('#callControl').show();
                    $('#callStatus').hide();
                    $('#inCallButtons').hide();
                    incomingCallAudio.pause();
                }
                //microphone mute icon
                if(session && session.isMuted().audio){
                    $('#muteIcon').addClass('fa-microphone-slash');
                    $('#muteIcon').removeClass('fa-microphone');
                }else{
                    $('#muteIcon').removeClass('fa-microphone-slash');
                    $('#muteIcon').addClass('fa-microphone');
                }
            }else{
                $('#wrapper').hide();
                $('#errorMessage').show();
            }
        }

    </script>
    <style>
        /* CSS Document */
        html, body {
            margin: 0;
            padding: 0;
            font-family:'Open Sans', sans-serif;
            color: #494949;
        }
        #wrapper {
            width: 300px;
            margin: 0 auto;
        }
        .callInfo {
            width: 190px;
            height: 70px;
            float: left;
        }
        #connectedCall .callInfo {
            width: 240px;
        }
        #dialPad div{
            -moz-user-select: -moz-none;
            -khtml-user-select: none;
            -webkit-user-select: none;

            /*
            Introduced in IE 10.
            See http://ie.microsoft.com/testdrive/HTML5/msUserSelect/
            */
            -ms-user-select: none;
            user-select: none;
        }
        .callInfo h3 {
            color: #389400;
            margin: 10px 0 10px 0;
        }
        .callInfo p {
            margin: 0px 25px 0px 0px;
            float: left;
        }
        #answer, #hangUp, #reject, #connectCall, #mute {
            color: #FFF;
            background-color: #389400;
            width: 50px;
            height: 50px;
            float: right;
            text-align: center;
            font-size: 30px;
            margin: 10px 0px 10px 0px;
            border-radius: 25px 25px 25px 25px;
            -moz-border-radius: 25px 25px 25px 25px;
            -webkit-border-radius: 25px 25px 25px 25px;
            cursor: pointer;
            cursor: hand;
        }
        #mute:active, #connectCall:active, #reject:active, #hangUp:active, #answer:active {
            background-color: #B6B6B6;
            -webkit-box-shadow: 0px 1px 7px 2px rgba(0, 0, 0, 0.45);
            -moz-box-shadow: 0px 1px 7px 2px rgba(0, 0, 0, 0.45);
            box-shadow: 0px 1px 7px 2px rgba(0, 0, 0, 0.45);
            border: none;
        }
        #hangUp, #reject {
            -ms-transform: rotate(135deg);
            /* IE 9 */
            -webkit-transform: rotate(135deg);
            /* Chrome, Safari, Opera */
            transform: rotate(135deg);
        }
        #hangUp {
            background-color: #A90002;
        }
        #reject {
            background-color: #FFF;
            color: #A90002;
            margin-right: 10px;
        }
        #connectCall, #mute {
            color: #FFF;
            background-color: #389400;
            width:260px;
            height: 50px;
            font-size: 30px;
            text-align: center;
            margin: 20px 20px 0px 20px;
            border-radius: 5px 5px 5px 5px;
            -moz-border-radius: 5px 5px 5px 5px;
            -webkit-border-radius: 5px 5px 5px 5px;
            cursor: pointer;
            cursor: hand;
        }
        #mute {
            color: #545454;
            margin: 0 auto;
            width: 50px;
            height: 50px;
            margin: 15px 125px 10px 20px;
            border-radius: 25px 25px 25px 25px;
            -moz-border-radius: 25px 25px 25px 25px;
            -webkit-border-radius: 25px 25px 25px 25px;
            background-color: #FFF;
        }
        .muteActive {
            color: #FFF;
            background-color:#389400;
        }
        #to {
            border-bottom: 2px solid #BBBBBB;
        }
        #toField {
            margin-top: 20px;
            padding-left: 10px;
            font-size: 1em;
            font-family:'Open Sans', sans-serif;
            width: 300px;
            height: 40px;
            border-radius: 2px 2px 2px 2px;
            -moz-border-radius: 2px 2px 2px 2px;
            -webkit-border-radius: 2px 2px 2px 2px;
            border: 0px solid #B8B8B8;
        }
        #dialPad {
            width: 240px;
            height: 308px;
            margin:0 auto;
        }
        #dialPad div {
            float: left;
            width: 50px;
            height: 50px;
            text-align: center;
            font-size: 26px;
            margin: 25px 15px 0px 15px;
            box-sizing:border-box;
            -moz-box-sizing:border-box;
            -webkit-box-sizing:border-box;
            border-radius: 25px 25px 25px 25px;
            -moz-border-radius: 25px 25px 25px 25px;
            -webkit-border-radius: 25px 25px 25px 25px;
            border: 1px solid #E8E8E8;
            padding-top: 5px;
        }
        #dialPad div:hover {
            background-color: #389400;
            color: #FFF;
            cursor: pointer;
            cursor: hand;
        }
        #dialPad div:active {
            background-color: #B2B2B2;
            -webkit-box-shadow: 0px 1px 7px 2px rgba(0, 0, 0, 0.45);
            -moz-box-shadow: 0px 1px 7px 2px rgba(0, 0, 0, 0.45);
            box-shadow: 0px 1px 7px 2px rgba(0, 0, 0, 0.45);
            border: none;
        }
        .fa {
            margin-top: 11px;
        }
        #answer .fa {
            -webkit-animation: callp 1s;
            animation: callp 1s;
            -webkit-animation-iteration-count: infinite;
            -webkit-animation-direction: alternate;
            -webkit-animation-play-state: running;
            animation-iteration-count: infinite;
            animation-direction: alternate;
            animation-play-state: running;
        }
        @-webkit-keyframes callp {
            from {
                color: #FFF;
            }
            to {
                color: #389400;
            }
        }
        @keyframes callp {
            from {
                color: #FFF;
            }
            to {
                color: #389400;
            }
        }
        #answer {
            -webkit-animation: call 1s;
            animation: call 1s;
            -webkit-animation-iteration-count: infinite;
            -webkit-animation-direction: alternate;
            -webkit-animation-play-state: running;
            animation-iteration-count: infinite;
            animation-direction: alternate;
            animation-play-state: running;
        }
        @-webkit-keyframes call {
            from {
                background: #389400;
            }
            to {
                background: #FFF;
            }
        }
        @keyframes call {
            from {
                background: #389400;
            }
            to {
                background: #FFF;
            }
        }
    </style>
    <title>Document</title>
</head>
<body>
<div id="errorMessage">must set sip uri/password</div>
<div id="wrapper">
    <div id="incomingCall" style="display: none">
        <div class="callInfo">
            <h3>Incoming Call</h3>
            <p id="incomingCallNumber">Unknown</p>
        </div>
        <div id="answer"> <i class="fa fa-phone"></i></div>
        <div id="reject"> <i class="fa fa-phone"></i></div>
    </div>
    <div id="callStatus" style="display: none">
        <div class="callInfo">
            <h3 id="callInfoText">info text goes here</h3>
            <p id="callInfoNumber">info number goes here</p>
        </div>
        <div id="hangUp"> <i class="fa fa-phone"></i>
        </div>
    </div>
    <!---------TO FIELD---------------------------------------------------->
    <!---------DIALPAD---------------------------------------------------->
    <div id="inCallButtons" style="display: none">
        <div id="dialPad">

            <div class="dialpad-char" data-value="1" unselectable="on">1</div>
            <div class="dialpad-char" data-value="2" unselectable="on">2</div>
            <div class="dialpad-char" data-value="3" unselectable="on">3</div>
            <div class="dialpad-char" data-value="4" unselectable="on">4</div>
            <div class="dialpad-char" data-value="5" unselectable="on">5</div>
            <div class="dialpad-char" data-value="6" unselectable="on">6</div>
            <div class="dialpad-char" data-value="7" unselectable="on">7</div>
            <div class="dialpad-char" data-value="8" unselectable="on">8</div>
            <div class="dialpad-char" data-value="9" unselectable="on">9</div>
            <div class="dialpad-char" data-value="*" unselectable="on">*</div>
            <div class="dialpad-char" data-value="0" unselectable="on">0</div>
            <div class="dialpad-char" data-value="#" unselectable="on">#</div>
        </div>
        <div id="mute">
            <i id="muteIcon" class="fa fa-microphone"></i>
        </div>
    </div>

    <!---------DIAL CONTROLS-------------------------------------------->
    <div id="callControl">
        <div id="to">
            <input id="toField" type="text" placeholder="Enter number here"/>
        </div>
        <div id="connectCall"> <i class="fa fa-phone"></i>

        </div>
    </div>
</div>
</body>
</html>

But I'm not able to connect to ws server. I would really appreciate if you could point out the issue or anything I'm missing?

emiago commented 3 weeks ago

@sujit-baniya I will guess you are running this in browser as static. Read about CSP. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

If this is not issue, then you need to provide some logs with debug level, but first check are you even reaching go websocket

sujit-baniya commented 3 weeks ago

@emiago In client.go, I tried adding the CSP

package main

import (
    "context"
    "crypto/rand"
    "encoding/base64"
    "flag"
    "fmt"
    "html/template"
    "net/http"
    "os"
    "strings"
    "time"

    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"

    "github.com/emiago/sipgo"
    "github.com/emiago/sipgo/sip"
    "github.com/emiago/sipgo/utils"

    "github.com/icholy/digest"
)

// Static files directory
const staticDir = "./static"

// generateNonce generates a random nonce
func generateNonce() (string, error) {
    nonce := make([]byte, 16)
    _, err := rand.Read(nonce)
    if err != nil {
        return "", err
    }
    return base64.StdEncoding.EncodeToString(nonce), nil
}

// cspMiddleware is a middleware function that adds a CSP header to the response
func cspMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        nonce, err := generateNonce()
        if err != nil {
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            return
        }

        // Add CSP header with nonce for both scripts and styles
        csp := "default-src 'self' 'unsafe-inline'; script-src 'self' https://cdnjs.cloudflare.com  'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://192.168.18.8:8080 ws://192.168.18.8:5060;"
        w.Header().Set("Content-Security-Policy", csp)

        // Store the nonce in the request context
        ctx := context.WithValue(r.Context(), "nonce", nonce)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// nonceHandler is a helper function to get the nonce from the request context
func nonceHandler(r *http.Request) string {
    if nonce, ok := r.Context().Value("nonce").(string); ok {
        return nonce
    }
    return ""
}

// HTML template
var tmpl = template.Must(template.ParseFiles("./static/index.html"))

// indexHandler serves the main page
func indexHandler(w http.ResponseWriter, r *http.Request) {
    nonce := nonceHandler(r)
    data := struct {
        Nonce string
    }{
        Nonce: nonce,
    }
    tmpl.Execute(w, data)
}

func runWeb() {
    // Serve static files from the defined directory
    fs := http.FileServer(http.Dir(staticDir))

    // Wrap the file server with the CSP middleware
    http.Handle("/", cspMiddleware(fs))

    // Handle the main page
    http.HandleFunc("/index", indexHandler)

    // Start the HTTP server
    http.ListenAndServe(":8080", nil)
}

func main() {
    defIP := utils.LocalIp()
    dst := flag.String("ip", defIP+":5060", "My external ip")
    inter := flag.String("h", "localhost", "My interface ip or hostname")
    tran := flag.String("t", "ws", "Transport")
    username := flag.String("u", "alice", "SIP Username")
    password := flag.String("p", "alice", "Password")
    flag.Parse()

    // Make SIP Debugging available
    sip.SIPDebug = os.Getenv("SIP_DEBUG") != ""

    zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMicro
    log.Logger = zerolog.New(zerolog.ConsoleWriter{
        Out:        os.Stdout,
        TimeFormat: time.StampMicro,
    }).With().Timestamp().Logger().Level(zerolog.InfoLevel)

    if lvl, err := zerolog.ParseLevel(os.Getenv("LOG_LEVEL")); err == nil && lvl != zerolog.NoLevel {
        log.Logger = log.Logger.Level(lvl)
    }

    // Setup UAC
    ua, err := sipgo.NewUA(
        sipgo.WithUserAgent(*username),
    )
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to setup user agent")
    }

    client, err := sipgo.NewClient(ua, sipgo.WithClientHostname(*inter))
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to setup client handle")
    }
    defer client.Close()

    // Create basic REGISTER request structure
    recipient := &sip.Uri{}
    sip.ParseUri(fmt.Sprintf("sip:%s@%s", *username, *dst), recipient)
    req := sip.NewRequest(sip.REGISTER, *recipient)
    req.AppendHeader(
        sip.NewHeader("Contact", fmt.Sprintf("<sip:%s@%s>", *username, *inter)),
    )
    req.SetTransport(strings.ToUpper(*tran))

    // Send request and parse response
    // req.SetDestination(*dst)
    log.Info().Msg(req.StartLine())
    ctx := context.Background()
    tx, err := client.TransactionRequest(ctx, req)
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to create transaction")
    }
    defer tx.Terminate()

    res, err := getResponse(tx)
    if err != nil {
        log.Fatal().Err(err).Msg("Fail to get response")
    }

    log.Info().Int("status", int(res.StatusCode)).Msg("Received status")
    if res.StatusCode == 401 {
        // Get WwW-Authenticate
        wwwAuth := res.GetHeader("WWW-Authenticate")
        chal, err := digest.ParseChallenge(wwwAuth.Value())
        if err != nil {
            log.Fatal().Str("wwwauth", wwwAuth.Value()).Err(err).Msg("Fail to parse challenge")
        }

        // Reply with digest
        cred, _ := digest.Digest(chal, digest.Options{
            Method:   req.Method.String(),
            URI:      recipient.Host,
            Username: *username,
            Password: *password,
        })

        newReq := req.Clone()
        newReq.RemoveHeader("Via") // Must be regenerated by tranport layer
        newReq.AppendHeader(sip.NewHeader("Authorization", cred.String()))

        ctx := context.Background()
        tx, err := client.TransactionRequest(ctx, newReq, sipgo.ClientRequestAddVia)
        if err != nil {
            log.Fatal().Err(err).Msg("Fail to create transaction")
        }
        defer tx.Terminate()

        res, err = getResponse(tx)
        if err != nil {
            log.Fatal().Err(err).Msg("Fail to get response")
        }
    }

    if res.StatusCode != 200 {
        log.Fatal().Msg("Fail to register")
    }

    log.Info().Msg("Client registered")
    runWeb()
}

func getResponse(tx sip.ClientTransaction) (*sip.Response, error) {
    select {
    case <-tx.Done():
        return nil, fmt.Errorf("transaction died")
    case res := <-tx.Responses():
        return res, nil
    }
}

But it didn't connect to server. Do I need to set CSP on server as well?

emiago commented 3 weeks ago

@sujit-baniya OK you got me confused with web client. Fine for code, but I would need some logs. I am not sure how you are executing.

Set sip.SIPDebug = true and zerolog logger log = log.SetLevel(...) to debug

I suggest running tcpdump on your ports and maybe take some more simple example, just to have connection working.