WebRTC - 安全

  • 简述

    在本章中,我们将为在“WebRTC 信令”一章中创建的信令服务器添加安全功能。将有两个增强功能 -
    • 使用 Redis 数据库进行用户身份验证
    • 启用安全套接字连接
    首先,你应该安装Redis。
    • 在http://redis.io/download下载最新的稳定版本(在我的例子中是 3.05)
    • 打开包装
    • 在下载的文件夹中运行sudo make install
    • 安装完成后,运行make test检查是否一切正常。
    Redis 有两个可执行命令 -
    • redis-cli - Redis 的命令行界面(客户端部分)
    • redis-server - Redis 数据存储
    要运行 Redis 服务器,请在终端控制台中键入redis-server 。你应该看到以下内容 -
    Redis 服务器
    现在打开一个新的终端窗口并运行redis-cli来打开一个客户端应用程序。
    Redis-cli
    基本上,Redis 是一个键值数据库。要创建具有字符串值的键,您应该使用 SET 命令。要读取键值,您应该使用 GET 命令。让我们为他们添加两个用户和密码。键将是用户名,这些键的值将是相应的密码。
    添加用户和密码
    现在我们应该修改我们的信令服务器以添加用户身份验证。将以下代码添加到server.js文件的顶部 -
    
    //require the redis library in Node.js 
    var redis = require("redis");
     
    //creating the redis client object 
    var redisClient = redis.createClient();
    
    在上面的代码中,我们需要 Node.js 的 Redis 库并为我们的服务器创建一个 Redis 客户端。
    要添加身份验证,请修改连接对象上的消息处理程序 -
    
    //when a user connects to our sever 
    wss.on('connection', function(connection) { 
       console.log("user connected");
         
       //when server gets a message from a connected user 
       connection.on('message', function(message) { 
         
          var data; 
          //accepting only JSON messages 
          try { 
             data = JSON.parse(message); 
          } catch (e) { 
             console.log("Invalid JSON"); 
             data = {}; 
          }
              
          //check whether a user is authenticated 
          if(data.type != "login") { 
              
             //if user is not authenticated 
             if(!connection.isAuth) { 
                sendTo(connection, { 
                   type: "error", 
                   message: "You are not authenticated" 
                }); 
                return; 
             } 
          } 
              
          //switching type of the user message 
          switch (data.type) { 
             //when a user tries to login 
             case "login": 
                console.log("User logged:", data.name); 
                //get password for this username from redis database 
                        
                redisClient.get(data.name, function(err, reply) {  
                   //check if password matches with the one stored in redis 
                   var loginSuccess = reply === data.password;
                        
                   //if anyone is logged in with this username or incorrect password 
                      then refuse 
                   if(users[data.name] || !loginSuccess) { 
                      sendTo(connection, { 
                         type: "login", 
                         success: false 
                      }); 
                   } else { 
                      //save user connection on the server 
                      users[data.name] = connection; 
                      connection.name = data.name;
                      connection.isAuth = true; 
                                  
                      sendTo(connection, { 
                         type: "login", 
                         success: true 
                      }); 
                   }  
                }); 
                        
                break;
          }
       });
         
    }         
              
    //... 
    //*****other handlers*******
    
    在上面的代码中,如果用户尝试登录,我们从 Redis 获取他的密码,检查它是否与存储的密码匹配,如果成功,我们将他的用户名存储在服务器上。我们还将isAuth标志添加到连接以检查用户是否已通过身份验证。注意这段代码 -
    
    //check whether a user is authenticated 
    if(data.type != "login") { 
       //if user is not authenticated 
       if(!connection.isAuth) { 
          sendTo(connection, { 
             type: "error", 
             message: "You are not authenticated" 
          });
              
          return; 
       } 
    }
    
    如果未经身份验证的用户试图发送报价或离开连接,我们只需返回一个错误。
    下一步是启用安全套接字连接。强烈推荐用于 WebRTC 应用程序。PKI (Public Key Infrastructure) 是来自 CA (Certificate Authority) 的数字签名。然后用户检查用于签署证书的私钥是否与 CA 证书的公钥相匹配。用于开发目的。我们将使用自签名安全证书。
    我们将使用 openssl。它是一个实现 SSL(安全套接字层)和 TLS(传输层安全)协议的开源工具。它通常默认安装在 Unix 系统上。运行openssl version -a查看是否安装。
    使用 OpenSSL
    要生成公共和私人安全证书密钥,您应该按照以下步骤操作 -
    • 生成临时服务器密码密钥
    
    openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
    
    临时服务器密码密钥
    • 生成服务器私钥
    
    openssl rsa -passin pass:12345 -in server.pass.key -out server.key
    
    服务器私钥
    • 生成签名请求。您将被问及有关贵公司的其他问题。只需一直点击“输入”按钮即可。
    
    openssl req -new -key server.key -out server.csr
    
    生成签名请求
    • 生成证书
    
    openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt
    
    生成证书
    现在您有两个文件,证书 (server.crt) 和私钥 (server.key)。将它们复制到信令服务器根文件夹中。
    要启用安全套接字连接,请修改我们的信令服务器。
    
    //require file system module 
    var fs = require('fs'); 
    var httpServ = require('https');
      
    //https://github.com/visionmedia/superagent/issues/205 
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
      
    //out secure server will bind to the port 9090 
    var cfg = { 
       port: 9090, 
       ssl_key: 'server.key', 
       ssl_cert: 'server.crt' 
    };
      
    //in case of http request just send back "OK" 
    var processRequest = function(req, res) { 
       res.writeHead(200); 
       res.end("OK"); 
    };
      
    //create our server with SSL enabled 
    var app = httpServ.createServer({ 
       key: fs.readFileSync(cfg.ssl_key), 
       cert: fs.readFileSync(cfg.ssl_cert) 
    }, processRequest).listen(cfg.port);
         
    //require our websocket library 
    var WebSocketServer = require('ws').Server; 
    //creating a websocket server at port 9090 
    var wss = new WebSocketServer({server: app}); 
    //all connected to the server users 
    var users = {};
      
    //require the redis library in Node.js
    var redis = require("redis"); 
    //creating the redis client object 
    var redisClient = redis.createClient(); 
     
    //when a user connects to our sever 
    wss.on('connection', function(connection){ 
    //...other code
    
    在上面的代码中,我们要求fs库读取私钥和证书,使用私钥和证书的绑定端口和路径创建cfg对象。然后,我们使用我们的密钥和端口 9090 上的 WebSocket 服务器创建一个 HTTPS 服务器。
    现在在 Opera 中打开https://localhost:9090 。你应该看到以下内容 -
    证书无效
    单击“仍然继续”按钮。您应该看到“确定”消息。
    为了测试我们的安全信令服务器,我们将修改我们在“WebRTC 文本演示”教程中创建的聊天应用程序。我们只需要添加一个密码字段。以下是整个index.html文件 -
    
    <html>
      
       <head> 
          <title>WebRTC Text Demo</title>  
          <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>  
       </head> 
       <style>  
          body { 
             background: #eee; 
             padding: 5% 0; 
          }  
       </style>
         
       <body>  
          <div id = "loginPage" class = "container text-center"> 
              
             <div class = "row"> 
                <div class = "col-md-4 col-md-offset-4">  
                   <h2>WebRTC Text Demo. Please sign in</h2> 
                   <label for = "usernameInput" class = "sr-only">Login</label> 
                   <input type = "email" id = "usernameInput" 
                      class = "form-control formgroup" placeholder = "Login" 
                      required = "" autofocus = ""> 
                   <input type = "text" id = "passwordInput" 
                      class = "form-control form-group" placeholder = "Password"
                      required = "" autofocus = ""> 
                   <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock"
                      >Sign in</button>  
                </div> 
             </div> 
                   
          </div> 
              
          <div id = "callPage" class = "call-page container">
              
             <div class = "row"> 
                <div class = "col-md-4 col-md-offset-4 text-center"> 
                   <div class = "panel panel-primary"> 
                      <div class = "panel-heading">Text chat</div> 
                      <div id = "chatarea" class = "panel-body text-left"></div> 
                   </div> 
                </div> 
             </div>
                   
             <div class = "row text-center form-group"> 
                <div class = "col-md-12"> 
                   <input id = "callToUsernameInput" type = "text" 
                      placeholder = "username to call" /> 
                   <button id = "callBtn" class = "btn-success btn">Call</button> 
                   <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> 
                </div> 
             </div>
                   
             <div class = "row text-center"> 
                <div class = "col-md-12"> 
                   <input id = "msgInput" type = "text" placeholder = "message" /> 
                   <button id = "sendMsgBtn" class = "btn-success btn">Send</button> 
                </div> 
             </div>
                   
          </div>  
              
          <script src = "client.js"></script> 
      
       </body> 
         
    </html>
    
    我们还需要通过此行在client.js文件中启用安全套接字连接var conn = new WebSocket('wss://localhost:9090'); . 注意wss协议。然后,必须修改登录按钮处理程序以发送密码和用户名 -
    
    loginBtn.addEventListener("click", function (event) { 
       name = usernameInput.value; 
       var pwd = passwordInput.value;
         
       if (name.length > 0) { 
          send({ 
             type: "login", 
             name: name, 
             password: pwd 
          }); 
       } 
         
    });
    
    以下是整个client.js文件 -
    
    //our username 
    var name; 
    var connectedUser;
      
    //connecting to our signaling server 
    var conn = new WebSocket('wss://localhost:9090');
      
    conn.onopen = function () { 
       console.log("Connected to the signaling server"); 
    };
      
    //when we got a message from a signaling server 
    conn.onmessage = function (msg) { 
       console.log("Got message", msg.data);
         
       var data = JSON.parse(msg.data);
         
       switch(data.type) { 
          case "login": 
             handleLogin(data.success); 
             break; 
          //when somebody wants to call us 
          case "offer":
             handleOffer(data.offer, data.name); 
             break; 
          case "answer": 
             handleAnswer(data.answer); 
             break; 
          //when a remote peer sends an ice candidate to us 
          case "candidate": 
             handleCandidate(data.candidate); 
             break; 
          case "leave": 
             handleLeave(); 
             break; 
          default: 
             break; 
       } 
    };
      
    conn.onerror = function (err) { 
       console.log("Got error", err); 
    };  
    //alias for sending JSON encoded messages 
    function send(message) { 
       //attach the other peer username to our messages 
       if (connectedUser) { 
          message.name = connectedUser; 
       } 
         
       conn.send(JSON.stringify(message)); 
    }; 
     
    //****** 
    //UI selectors block 
    //******
    var loginPage = document.querySelector('#loginPage'); 
    var usernameInput = document.querySelector('#usernameInput'); 
    var passwordInput = document.querySelector('#passwordInput'); 
    var loginBtn = document.querySelector('#loginBtn'); 
    var callPage = document.querySelector('#callPage'); 
    var callToUsernameInput = document.querySelector('#callToUsernameInput');
    var callBtn = document.querySelector('#callBtn'); 
    var hangUpBtn = document.querySelector('#hangUpBtn');
      
    var msgInput = document.querySelector('#msgInput'); 
    var sendMsgBtn = document.querySelector('#sendMsgBtn'); 
    var chatArea = document.querySelector('#chatarea'); 
    var yourConn; 
    var dataChannel;
      
    callPage.style.display = "none";
      
    // Login when the user clicks the button 
    loginBtn.addEventListener("click", function (event) { 
       name = usernameInput.value; 
       var pwd = passwordInput.value;  
         
       if (name.length > 0) { 
          send({ 
             type: "login", 
             name: name, 
             password: pwd 
          }); 
       } 
         
    }); 
     
    function handleLogin(success) { 
       if (success === false) {
          alert("Ooops...incorrect username or password"); 
       } else { 
          loginPage.style.display = "none"; 
          callPage.style.display = "block";
              
          //********************** 
          //Starting a peer connection 
          //********************** 
              
          //using Google public stun server 
          var configuration = { 
             "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] 
          }; 
              
          yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]}); 
              
          // Setup ice handling 
          yourConn.onicecandidate = function (event) { 
             if (event.candidate) { 
                send({ 
                   type: "candidate", 
                   candidate: event.candidate 
                }); 
             } 
          };
              
          //creating data channel 
          dataChannel = yourConn.createDataChannel("channel1", {reliable:true}); 
              
          dataChannel.onerror = function (error) { 
             console.log("Ooops...error:", error); 
          };
              
          //when we receive a message from the other peer, display it on the screen 
          dataChannel.onmessage = function (event) { 
             chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; 
          }; 
              
          dataChannel.onclose = function () { 
             console.log("data channel is closed"); 
          };  
       } 
         
    };
      
    //initiating a call 
    callBtn.addEventListener("click", function () { 
       var callToUsername = callToUsernameInput.value;
         
       if (callToUsername.length > 0) {
         
          connectedUser = callToUsername;
              
          // create an offer 
          yourConn.createOffer(function (offer) { 
             send({ 
                type: "offer", 
                offer: offer 
             }); 
                   
             yourConn.setLocalDescription(offer); 
                   
          }, function (error) { 
             alert("Error when creating an offer"); 
          });  
       } 
    });
     
    //when somebody sends us an offer 
    function handleOffer(offer, name) { 
       connectedUser = name; 
       yourConn.setRemoteDescription(new RTCSessionDescription(offer));
         
       //create an answer to an offer 
       yourConn.createAnswer(function (answer) { 
          yourConn.setLocalDescription(answer); 
              
          send({ 
             type: "answer", 
             answer: answer 
          }); 
              
       }, function (error) { 
          alert("Error when creating an answer"); 
       }); 
         
    };
      
    //when we got an answer from a remote user 
    function handleAnswer(answer) { 
       yourConn.setRemoteDescription(new RTCSessionDescription(answer)); 
    };
      
    //when we got an ice candidate from a remote user 
    function handleCandidate(candidate) { 
       yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
    };
       
    //hang up 
    hangUpBtn.addEventListener("click", function () { 
       send({ 
          type: "leave"
       }); 
         
       handleLeave(); 
    });
      
    function handleLeave() { 
       connectedUser = null; 
       yourConn.close(); 
       yourConn.onicecandidate = null; 
    }; 
     
    //when user clicks the "send message" button 
    sendMsgBtn.addEventListener("click", function (event) { 
       var val = msgInput.value; 
       chatArea.innerHTML += name + ": " + val + "<br />"; 
         
       //sending a message to a connected peer 
       dataChannel.send(val); 
       msgInput.value = ""; 
    });
    
    现在通过节点服务器运行我们的安全信令服务器。在修改后的聊天演示文件夹中运行静态节点。在两个浏览器选项卡中打开localhost:8080 。尝试登录。请记住,只允许使用“password1”的“user1”和使用“password2”的“user2”登录。然后建立 RTCPeerConnection(调用另一个用户)并尝试发送消息。
    建立 RTCPeerConnection
    以下是我们安全信令服务器的全部代码 -
    
    //require file system module 
    var fs = require('fs'); 
    var httpServ = require('https');
    //https://github.com/visionmedia/superagent/issues/205 
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
      
    //out secure server will bind to the port 9090 
    var cfg = { 
       port: 9090, 
       ssl_key: 'server.key', 
       ssl_cert: 'server.crt' 
    };
      
    //in case of http request just send back "OK" 
    var processRequest = function(req, res){ 
       res.writeHead(200); 
       res.end("OK"); 
    };
      
    //create our server with SSL enabled 
    var app = httpServ.createServer({ 
       key: fs.readFileSync(cfg.ssl_key), 
       cert: fs.readFileSync(cfg.ssl_cert) 
    }, processRequest).listen(cfg.port);
      
    //require our websocket library 
    var WebSocketServer = require('ws').Server; 
    //creating a websocket server at port 9090 
    var wss = new WebSocketServer({server: app}); 
    //all connected to the server users 
    var users = {};
      
    //require the redis library in Node.js 
    var redis = require("redis"); 
    //creating the redis client object 
    var redisClient = redis.createClient();
    //when a user connects to our sever 
    wss.on('connection', function(connection) { 
       console.log("user connected"); 
         
       //when server gets a message from a connected user 
       connection.on('message', function(message) {  
         
          var data; 
          //accepting only JSON messages 
          try { 
             data = JSON.parse(message); 
          } catch (e) { 
             console.log("Invalid JSON"); 
             data = {}; 
          } 
              
          //check whether a user is authenticated 
          if(data.type != "login") { 
             //if user is not authenticated 
             if(!connection.isAuth) { 
                sendTo(connection, { 
                   type: "error", 
                   message: "You are not authenticated" 
                }); 
                        
                return; 
             } 
          }
              
          //switching type of the user message 
          switch (data.type) { 
             //when a user tries to login 
             case "login":
                console.log("User logged:", data.name); 
                //get password for this username from redis database 
                redisClient.get(data.name, function(err, reply) {
                     
                   //check if password matches with the one stored in redis 
                   var loginSuccess = reply === data.password;
                          
                   //if anyone is logged in with this username or incorrect password 
                      then refuse 
                   if(users[data.name] || !loginSuccess) { 
                      sendTo(connection, { 
                         type: "login", 
                         success: false 
                      }); 
                   } else { 
                      //save user connection on the server 
                      users[data.name] = connection; 
                      connection.name = data.name; 
                      connection.isAuth = true; 
                                  
                      sendTo(connection, { 
                         type: "login", 
                         success: true 
                      }); 
                   }  
                }); 
                        
                break;
                        
             case "offer": 
                //for ex. UserA wants to call UserB 
                console.log("Sending offer to: ", data.name); 
                        
                //if UserB exists then send him offer details 
                var conn = users[data.name];
                        
                if(conn != null) { 
                   //setting that UserA connected with UserB 
                   connection.otherName = data.name;
                             
                   sendTo(conn, { 
                      type: "offer", 
                      offer: data.offer, 
                      name: connection.name 
                   }); 
                } 
                        
                break;
                        
             case "answer": 
                console.log("Sending answer to: ", data.name); 
                //for ex. UserB answers UserA 
                var conn = users[data.name]; 
                        
                if(conn != null) { 
                   connection.otherName = data.name;
                             
                   sendTo(conn, { 
                      type: "answer", 
                      answer: data.answer 
                   }); 
                } 
                        
                break;
                        
             case "candidate": 
                console.log("Sending candidate to:",data.name); 
                var conn = users[data.name];
                        
                if(conn != null) { 
                   sendTo(conn, { 
                      type: "candidate", 
                      candidate: data.candidate 
                   });
                } 
                        
                break;
                        
             case "leave": 
                console.log("Disconnecting from", data.name); 
                var conn = users[data.name]; 
                conn.otherName = null; 
                        
                //notify the other user so he can disconnect his peer connection 
                if(conn != null) { 
                   sendTo(conn, { 
                      type: "leave" 
                   }); 
                }  
                        
                break;
                        
             connection.on("close", function() {
                   
                if(connection.name) { 
                   delete users[connection.name]; 
                             
                   if(connection.otherName) { 
                      console.log("Disconnecting from ", connection.otherName); 
                      var conn = users[connection.otherName]; 
                      conn.otherName = null;  
                                  
                      if(conn != null) { 
                         sendTo(conn, { 
                            type: "leave" 
                        }); 
                      } 
                                  
                   } 
                } 
             });
                   
             default: 
                sendTo(connection, { 
                   type: "error", 
                   message: "Command no found: " + data.type 
                }); 
                        
                break; 
          }  
       });
         
       //when user exits, for example closes a browser window 
       //this may help if we are still in "offer","answer" or "candidate" state 
       connection.on("close", function() { 
          if(connection.name) { 
             delete users[connection.name]; 
          } 
       });
         
       connection.send("Hello from server"); 
    });
      
    function sendTo(connection, message) { 
       connection.send(JSON.stringify(message)); 
    }
    
  • 概括

    在本章中,我们向信令服务器添加了用户身份验证。我们还学习了如何创建自签名 SSL 证书并在 WebRTC 应用程序范围内使用它们。