From bf7ba757f4978594e0d3347879f6337b37042d84 Mon Sep 17 00:00:00 2001 From: Michael Farb Date: Wed, 15 Jan 2020 16:56:07 -0500 Subject: [PATCH] [webapp] Chat WIP --- webapp/dependencies.md | 1 + webapp/web/static/js/chat.js | 351 ++++++++++++++++++++++++++++++++ webapp/web/static/js/netcat.js | 63 ++++++ webapp/web/static/js/webapp.js | 1 + webapp/web/template/chat.html | 143 +++++++++++++ webapp/web/template/header.html | 1 + webapp/webapp.go | 198 ++++++++++++++++++ 7 files changed, 758 insertions(+) create mode 100644 webapp/web/static/js/chat.js create mode 100644 webapp/web/static/js/netcat.js create mode 100644 webapp/web/template/chat.html diff --git a/webapp/dependencies.md b/webapp/dependencies.md index 7fb6e911..d6904635 100644 --- a/webapp/dependencies.md +++ b/webapp/dependencies.md @@ -32,6 +32,7 @@ webapp -h - $sabin/bwtestclient - $sabin/imagefetcher - $sabin/sensorfetcher +- $sabin/netcat ## Scripts/Directories Used/Checked - $sroot/scion.sh (deprecated) diff --git a/webapp/web/static/js/chat.js b/webapp/web/static/js/chat.js new file mode 100644 index 00000000..5ebdf74d --- /dev/null +++ b/webapp/web/static/js/chat.js @@ -0,0 +1,351 @@ +// Copyright 2019 ETH Zurich +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.package main + +// signaling server +var firebaseConfig = { + authDomain : "wip-scion-webrtc.firebaseapp.com", + databaseURL : "https://wip-scion-webrtc.firebaseio.com", + projectId : "wip-scion-webrtc", + storageBucket : "wip-scion-webrtc.appspot.com", +}; + +var iceServers = { + 'iceServers' : [ { + 'urls' : 'stun:stun.services.mozilla.com' + }, { + 'urls' : 'stun:stun.l.google.com:19302' + } ] +}; + +const regexIpAddr = /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/; + +var pc = new RTCPeerConnection(iceServers); +var database; + +var yourVideo; +var friendsVideo; + +var yourId = Math.floor(Math.random() * 1000000000); +var yourIa; +var yourAddr; +var yourChatPort = randomPort(); +var yourAudioPort = randomPort(); +var yourVideoPort = randomPort(); + +var friendsIa; +var friendsAddr; +var friendsChatPort; +var friendsAudioPort; +var friendsVideoPort; + +window.onload = function(event) { + $("#videoout").empty(); + debugLog("Loaded page"); + yourVideo = document.getElementById("yourVideo"); + friendsVideo = document.getElementById("friendsVideo"); + showMyIa(yourIa); + debugLog("yourId: " + yourId); + debugLog("yourChatPort: " + yourChatPort); + debugLog("yourAudioPort: " + yourAudioPort); + debugLog("yourVideoPort: " + yourVideoPort); + ajaxVizConfig(); + ajaxAsTopo(); + ajaxChatConfig(); +}; + +function debugLog(msg) { + console.log(msg); + $("#videoout").append("\n" + msg + "."); + $(".stdout").scrollTop($(".stdout")[0].scrollHeight); +} + +function ajaxChatConfig() { + return $.ajax({ + url : 'chatcfg', + type : 'post', + timeout : 10000, + success : isChatConfigComplete, + error : function(jqXHR, textStatus, errorThrown) { + showError(this.url + ' ' + textStatus + ': ' + errorThrown); + }, + }); +} + +function isChatConfigComplete(data, textStatus, jqXHR) { + console.debug(data); +} + +function ajaxAsTopo() { + return $.ajax({ + url : 'getastopo', + type : 'post', + dataType : "json", + data : { + "src" : yourIa + }, + timeout : 10000, + success : isAsTopoComplete, + error : function(jqXHR, textStatus, errorThrown) { + showError(this.url + ' ' + textStatus + ': ' + errorThrown); + }, + }); +} + +function isAsTopoComplete(data, textStatus, jqXHR) { + console.debug(data); + + yourAddr = ipv4Raw2Read(data.if_info.RawEntries[0].HostInfo.Addrs.Ipv4); + debugLog("yourAddr: " + yourAddr); +} + +function ajaxVizConfig() { + return $.ajax({ + url : 'config', + type : 'get', + dataType : "json", + timeout : 30000, + success : isRTCConfigComplete, + error : function(jqXHR, textStatus, errorThrown) { + debugLog(this.url + ' ' + textStatus + ': ' + errorThrown); + }, + }); +} + +function isRTCConfigComplete(data, textStatus, jqXHR) { + debugLog(this.url + ' ' + textStatus); + debugLog('firebaseConfig.apiKey = ' + data.webrtc_apiKey); + debugLog('firebaseConfig.messagingSenderId = ' + + data.webrtc_messagingSenderId); + debugLog('firebaseConfig.appId = ' + data.webrtc_appId); + firebaseConfig.apiKey = data.webrtc_apiKey; + firebaseConfig.messagingSenderId = data.webrtc_messagingSenderId; + firebaseConfig.appId = data.webrtc_appId; + // Initialize Firebase + firebase.initializeApp(firebaseConfig); + database = firebase.database().ref(); + pc.onicecandidate = function(event) { + if (event.candidate) { + sendMessage(yourId, JSON.stringify({ + 'ice' : event.candidate + })); + } else { + console.log("Sent All Ice"); + + // addresses now complete?, so open netcat chat + + // netcat listen to stdout on local IA yourChatPort + // - on stdout.read(msg), append.txt("friend:"+msg) + + // netcat serve to stdin on remote IA friendChatPort + // - on btn-send(msg), stdin.write(), append.txt("self:"+msg) + if (friendsIa && friendsAddr && friendsChatPort) { + var local = formatScionAddr(yourIa, yourAddr, yourChatPort); + var remote = formatScionAddr(friendsIa, friendsAddr, + friendsChatPort); + openNetcatChatText(local, remote); + openNetcatChatVideo(local, remote); + } + } + }; + pc.onaddstream = function(event) { + friendsVideo.srcObject = event.stream; + }; + database.on('child_added', readMessage); + // when load finished... + showMyFace(); +} + +function formatScionAddr(ia, addr, port) { + return ia + ",[" + addr + "]:" + port; +} + +function randomPort() { + return Math.floor(Math.random() * 10000) + 30000; +} + +function sendMessage(senderId, data) { + debugLog("Called sendMessage()"); + var msg = database.push({ + sender : senderId, + ia : yourIa, + addr : yourAddr, + portC : yourChatPort, + portA : yourAudioPort, + portV : yourVideoPort, + message : data + }); + msg.remove(); + updateAddrs(); +} + +function readMessage(data) { + debugLog("Called readMessage()"); + var msg = JSON.parse(data.val().message); + var sender = data.val().sender; + if (sender != yourId) { + friendsIa = data.val().ia; + friendsAddr = data.val().addr; + friendsChatPort = data.val().portC; + friendsAudioPort = data.val().portA; + friendsVideoPort = data.val().portV; + showPeerIa(friendsIa); + if (msg.ice != undefined) { + pc.addIceCandidate(new RTCIceCandidate(msg.ice)); + } else if (msg.sdp) { + if (msg.sdp.type == "offer") { + // recieved offer, store as remote conn + pc.setRemoteDescription(new RTCSessionDescription(msg.sdp)) + // create answer + .then(function() { + return pc.createAnswer(); + }) + // store answer as local conn + .then(function(answer) { + pc.setLocalDescription(answer); + }) + // send answer + .then(function() { + sendMessage(yourId, JSON.stringify({ + 'sdp' : pc.localDescription + })); + }); + } else if (msg.sdp.type == "answer") { + pc.setRemoteDescription(new RTCSessionDescription(msg.sdp)); + } + } + } + updateAddrs(); +}; + +function updateAddrs() { + if (pc.currentLocalDescription) { + debugLog("yourType: " + pc.currentLocalDescription.type); + showMyAddr(getSdpAddr(pc.currentLocalDescription.sdp)); + showMyAudio(getSdpAudio(pc.currentLocalDescription.sdp)); + showMyVideo(getSdpVideo(pc.currentLocalDescription.sdp)); + } + if (pc.currentRemoteDescription) { + debugLog("friendsType: " + pc.currentRemoteDescription.type); + showPeerAddr(getSdpAddr(pc.currentRemoteDescription.sdp)); + showPeerAudio(getSdpAudio(pc.currentRemoteDescription.sdp)); + showPeerVideo(getSdpVideo(pc.currentRemoteDescription.sdp)); + } +} + +function showMyFace() { + debugLog("Called showMyFace()"); + navigator.mediaDevices.getUserMedia({ + audio : true, + video : true + }) + // place your media in local object + .then(function(stream) { + yourVideo.srcObject = stream; + sendVideoStream(stream); + return stream; + }) + // add your media to stream + .then(function(stream) { + pc.addStream(stream); + }); +} + +function showFriendsFace() { + debugLog("Called showFriendsFace()"); + + pc.createOffer() + // place offer in local conn + .then(function(offer) { + pc.setLocalDescription(offer); + }) + // send the offer + .then(function() { + sendMessage(yourId, JSON.stringify({ + 'sdp' : pc.localDescription + })); + }); +} + +function showMyIa(ia) { + debugLog("yourIaText: " + ia); + $("#yourIaText").html(ia); +} + +function showPeerIa(ia) { + debugLog("friendsIaText: " + ia); + $("#friendsIaText").html(ia); +} + +function showMyAddr(addr) { + debugLog("yourVar1: " + addr); + $("#yourVar1").html(addr); +} + +function showPeerAddr(addr) { + debugLog("friendsVar1: " + addr); + $("#friendsVar1").html(addr); +} + +function showMyAudio(addr) { + debugLog("yourVar2: " + addr); + $("#yourVar2").html(addr); +} + +function showPeerAudio(addr) { + debugLog("friendsVar2: " + addr); + $("#friendsVar2").html(addr); +} + +function showMyVideo(addr) { + debugLog("yourVar3: " + addr); + $("#yourVar3").html(addr); +} + +function showPeerVideo(addr) { + debugLog("friendsVar3: " + addr); + $("#friendsVar3").html(addr); +} + +function getSdpAddr(sdp) { + var ips = []; + if (!sdp) { + return null; + } + ips = sdp.split('\r\n').filter(function(line) { + return line.indexOf('c=') === 0; + }).map(function(ipstr) { + return ipstr.match(regexIpAddr)[0]; + }); + return ips[0]; +} + +function getSdpAudio(sdp) { + var ips = []; + if (sdp) { + ips = sdp.split('\r\n').filter(function(line) { + return line.indexOf('m=audio') === 0; + }); + } + return ips[0]; +} + +function getSdpVideo(sdp) { + var ips = []; + if (sdp) { + ips = sdp.split('\r\n').filter(function(line) { + return line.indexOf('m=video') === 0; + }); + } + return ips[0]; +} diff --git a/webapp/web/static/js/netcat.js b/webapp/web/static/js/netcat.js new file mode 100644 index 00000000..6dbf5794 --- /dev/null +++ b/webapp/web/static/js/netcat.js @@ -0,0 +1,63 @@ +var yourMsg, yourVideo, friendsVideo, logMsgs, socketT, socketV; + +$(document).ready(function() { + yourMsg = document.getElementById("yourMsg"); + yourVideo = document.getElementById("yourVideo"); + friendsVideo = document.getElementById("friendsVideo"); + logMsgs = document.getElementById("logMsgs"); +}); + +function openNetcatChatText(localAddr, remoteAddr) { + + // TODO chrome throws an exception + + socketT = new WebSocket(encodeURI("ws://" + document.location.host + + "/wschat?local=" + localAddr + "&remote=" + remoteAddr)); + + socketT.onopen = function() { + appendChatDisplay("Status: WS text opened\n"); + }; + + socketT.onmessage = function(e) { + appendChatDisplay("Friend: " + e.data + "\n"); + }; +} + +function openNetcatChatVideo(localAddr, remoteAddr) { + socketV = new WebSocket(encodeURI("ws://" + document.location.host + + "/wsvideo?local=" + localAddr + "&remote=" + remoteAddr)); + + socketV.onopen = function() { + debugLog("WS video opened\n"); + }; + + socketV.onmessage = function(e) { + debugLog("WS video incoming...\n"); + friendsVideo.srcObject = e.stream; + }; +} + +function sendTextMsg() { + if (socketT) { + socketT.send(yourMsg.value); + appendChatDisplay("You: " + yourMsg.value + "\n"); + yourMsg.value = ""; + } +} + +function sendVideoStream(stream) { + if (socketV) { + socketV.send(stream); + } +} + +function appendChatDisplay(msg) { + var doScroll = logMsgs.scrollTop > logMsgs.scrollHeight + - logMsgs.clientHeight - 1; + var item = document.createElement("div"); + item.innerHTML = msg; + logMsgs.appendChild(item); + if (doScroll) { + logMsgs.scrollTop = logMsgs.scrollHeight - logMsgs.clientHeight; + } +} diff --git a/webapp/web/static/js/webapp.js b/webapp/web/static/js/webapp.js index 8a4513d9..44bd2d09 100644 --- a/webapp/web/static/js/webapp.js +++ b/webapp/web/static/js/webapp.js @@ -108,6 +108,7 @@ function initBwGraphs() { updateBwInterval(); // charts update on tab switch + handleSwitchTabs(); // init $('a[data-toggle="tab"]').on('shown.bs.tab', function(e) { var name = $(e.target).attr("name"); if (name != "as-graphs" && name != "as-tab-pathtopo") { diff --git a/webapp/web/template/chat.html b/webapp/web/template/chat.html new file mode 100644 index 00000000..ba31a247 --- /dev/null +++ b/webapp/web/template/chat.html @@ -0,0 +1,143 @@ +{{define "chat"}} {{template "header" .}} + + + + + + + + + + + +
+ +
+ +

SCIONLab Chat

+ +
+ +
+ +
+ +
+

Your Video

+

+

+

+
+
+ +
+ +
+

Your Friend's Video

+

+

+

+
+
+ + + +
+ +
+ + +

+
+  
+ +
+ +
+
+
+ +
+ + + + + + + +{{template "footer" .}} {{end}} diff --git a/webapp/web/template/header.html b/webapp/web/template/header.html index c28c6f40..6f14517b 100644 --- a/webapp/web/template/header.html +++ b/webapp/web/template/header.html @@ -49,6 +49,7 @@
  • Health
  • Apps
  • +
  • Chat
  • Monitor
  • Trust
  • Files
  • diff --git a/webapp/webapp.go b/webapp/webapp.go index 090214a7..292eceb6 100644 --- a/webapp/webapp.go +++ b/webapp/webapp.go @@ -18,6 +18,7 @@ package main import ( "bufio" + "bytes" "encoding/json" "errors" "flag" @@ -33,6 +34,7 @@ import ( "strings" "time" + "github.com/gorilla/websocket" log "github.com/inconshreveable/log15" "github.com/kormat/fmt15" _ "github.com/mattn/go-sqlite3" @@ -183,6 +185,7 @@ func main() { appsBuildCheck("sensorapp") appsBuildCheck("echo") appsBuildCheck("traceroute") + appsBuildCheck("netcat") initServeHandlers() log.Info(fmt.Sprintf("Browser access: at http://%s:%d.", browserAddr, *port)) @@ -225,6 +228,10 @@ func initServeHandlers() { http.Handle("/data/", http.StripPrefix("/data/", fsData)) fsFileBrowser := http.FileServer(http.Dir(options.BrowseRoot)) http.Handle("/files/", http.StripPrefix("/files/", fsFileBrowser)) + http.HandleFunc("/chat", chatHandler) + http.HandleFunc("/wschat", chatTextHandler) + http.HandleFunc("/wsvideo", chatVideoHandler) + http.HandleFunc("/chatcfg", chatConfigHandler) http.HandleFunc("/command", commandHandler) http.HandleFunc("/imglast", findImageHandler) @@ -266,6 +273,7 @@ func prepareTemplates(srcpath string) *template.Template { path.Join(srcpath, "template/about.html"), path.Join(srcpath, "template/astopo.html"), path.Join(srcpath, "template/trc.html"), + path.Join(srcpath, "template/chat.html"), )) } @@ -312,6 +320,10 @@ func trcHandler(w http.ResponseWriter, r *http.Request) { display(w, "trc", &Page{Title: "SCIONLab TRC", MyIA: settings.MyIA}) } +func chatHandler(w http.ResponseWriter, r *http.Request) { + display(w, "chat", &Page{Title: "SCIONLab Chat", MyIA: settings.MyIA}) +} + // There're three CmdItem, BwTestItem, EchoItem and TracerouteItem func parseRequest2CmdItem(r *http.Request, appSel string) (model.CmdItem, string) { addlOpt := r.PostFormValue("addl_opt") @@ -593,6 +605,8 @@ func getClientLocationBin(app string) string { binname = path.Join(options.AppsRoot, "bwtestclient") case "echo", "traceroute": binname = path.Join(options.ScionBin, "scmp") + case "netcat": + binname = path.Join(options.AppsRoot, "netcat") } return binname } @@ -773,3 +787,187 @@ func setUserOptionsHandler(w http.ResponseWriter, r *http.Request) { lib.GenClientNodeDefaults(&options, settings.MyIA) log.Info("IA set:", "myIa", settings.MyIA) } + +func chatVideoHandler(w http.ResponseWriter, r *http.Request) { + local := r.FormValue("local") + remote := r.FormValue("remote") + + localAddr := local[:strings.LastIndex(local, ":")] + localPort := local[strings.LastIndex(local, ":")+1:] + remoteAddr := remote[:strings.LastIndex(remote, ":")] + remotePort := remote[strings.LastIndex(remote, ":")+1:] + + // open websocket server connection + conn, err := upgrader.Upgrade(w, r, nil) + defer conn.Close() + if CheckError(err) { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + installpath := getClientLocationBin("netcat") + cmdloc := fmt.Sprintf("-local=%s", localAddr) + keyPath := path.Join(options.StaticRoot, "key.pem") + certPath := path.Join(options.StaticRoot, "cert.pem") + cmdKey := fmt.Sprintf("-tlsKey=%s", keyPath) + cmdCert := fmt.Sprintf("-tlsCert=%s", certPath) + + // serve + serveArgs := []string{installpath, cmdloc, remoteAddr, remotePort} + log.Info("Run NC Video:", "command", strings.Join(serveArgs, " ")) + commandServe := exec.Command(serveArgs[0], serveArgs[1:]...) + // open scion netcat serve to friend and ready stdin... + stdin, err := commandServe.StdinPipe() + CheckError(err) + err = commandServe.Start() + CheckError(err) + // monitor websocket for input + go func() { + for { + // message from browser + _, buf, err := conn.ReadMessage() + CheckError(err) + // pipe buf to netcat stdin... + log.Debug("netcat v send:", "buflen", len(buf)) + stdin.Write(buf) + } + }() + err = commandServe.Wait() + CheckError(err) + + // listen + listenArgs := []string{installpath, "-l", cmdloc, localPort, cmdKey, cmdCert} + log.Info("Run NC Video:", "command", strings.Join(listenArgs, " ")) + commandListen := exec.Command(listenArgs[0], listenArgs[1:]...) + // open scion netcat client listen from friend and ready stdout... + stdout, err := commandListen.StdoutPipe() + CheckError(err) + reader := bufio.NewReader(stdout) + buf := make([]byte, 1024) + go func(reader io.Reader) { + for { + n, err := reader.Read(buf) + fmt.Println(n, err, buf[:n]) + if err == io.EOF { + break + } + // recieved buf on stdin while running netcat... + log.Debug("netcat v recv:", "buflen", len(buf)) + // send buf to browser + err = conn.WriteMessage(websocket.BinaryMessage, buf) + CheckError(err) + } + }(reader) + err = commandListen.Start() + CheckError(err) + err = commandListen.Wait() + CheckError(err) +} + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true // TODO ensure localhost/proxy is maintained + }, +} + +func chatConfigHandler(w http.ResponseWriter, r *http.Request) { + // find TLS cert, or generate if missing + keyPath := path.Join(options.StaticRoot, "key.pem") + certPath := path.Join(options.StaticRoot, "cert.pem") + if _, err := os.Stat(certPath); os.IsNotExist(err) { + certArgs := []string{"openssl", "req", + "-newkey", "rsa:2048", + "-nodes", + "-keyout", keyPath, + "-x509", + "-days", "365", + "-out", certPath, + "-subj", "/CN=localhost"} + log.Info("Executing:", "command", strings.Join(certArgs, " ")) + cmd := exec.Command(certArgs[0], certArgs[1:]...) + + var outb, errb bytes.Buffer + cmd.Stdout = &outb + cmd.Stderr = &errb + err := cmd.Run() + CheckError(err) + log.Info("results:", "out:", outb.String(), "err:", errb.String()) + } +} + +func chatTextHandler(w http.ResponseWriter, r *http.Request) { + local := r.FormValue("local") + remote := r.FormValue("remote") + + localAddr := local[:strings.LastIndex(local, ":")] + localPort := local[strings.LastIndex(local, ":")+1:] + remoteAddr := remote[:strings.LastIndex(remote, ":")] + remotePort := remote[strings.LastIndex(remote, ":")+1:] + + // open websocket server connection + conn, err := upgrader.Upgrade(w, r, nil) + defer conn.Close() + if CheckError(err) { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // use passed in ports for servers/clients here + installpath := getClientLocationBin("netcat") + cmdloc := fmt.Sprintf("-local=%s", localAddr) + keyPath := path.Join(options.StaticRoot, "key.pem") + certPath := path.Join(options.StaticRoot, "cert.pem") + cmdKey := fmt.Sprintf("-tlsKey=%s", keyPath) + cmdCert := fmt.Sprintf("-tlsCert=%s", certPath) + + // TODO: (mwfarb) add reasonable retry logic when handshake timeout occurs + + // serve + serveArgs := []string{installpath, cmdloc, remoteAddr, remotePort} + log.Info("Executing:", "command", strings.Join(serveArgs, " ")) + commandServe := exec.Command(serveArgs[0], serveArgs[1:]...) + // open scion netcat serve to friend and ready stdin... + stdin, err := commandServe.StdinPipe() + CheckError(err) + err = commandServe.Start() + CheckError(err) + // monitor websocket for input + go func() { + for { + // message from browser + _, msg, err := conn.ReadMessage() + CheckError(err) + // pipe message to netcat stdin... + log.Debug("netcat t send:", "msg", string(msg)) + stdin.Write(append(msg, '\n')) + } + }() + err = commandServe.Wait() + CheckError(err) + + // listen + listenArgs := []string{installpath, "-l", cmdloc, localPort, cmdKey, cmdCert} + log.Info("Executing:", "command", strings.Join(listenArgs, " ")) + commandListen := exec.Command(listenArgs[0], listenArgs[1:]...) + // open scion netcat client listen from friend and ready stdout... + stdout, err := commandListen.StdoutPipe() + CheckError(err) + reader := bufio.NewReader(stdout) + go func(reader io.Reader) { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + // recieved text on stdin while running netcat... + msg := scanner.Text() + log.Debug("netcat t recv:", "msg", string(msg)) + // send message to browser + err = conn.WriteMessage(websocket.TextMessage, []byte(msg)) + CheckError(err) + } + }(reader) + err = commandListen.Start() + CheckError(err) + err = commandListen.Wait() + CheckError(err) +}