WebRTC - Bảo mật
Trong chương này, chúng tôi sẽ thêm các tính năng bảo mật vào máy chủ báo hiệu mà chúng tôi đã tạo trong chương “Báo hiệu WebRTC”. Sẽ có hai cải tiến -
- Xác thực người dùng bằng cơ sở dữ liệu Redis
- Bật kết nối ổ cắm an toàn
Đầu tiên, bạn nên cài đặt Redis.
Tải xuống bản phát hành ổn định mới nhất tại http://redis.io/download(3.05 trong trường hợp của tôi)
Giải nén nó
Bên trong thư mục tải xuống chạy sudo thực hiện cài đặt
Sau khi cài đặt xong, hãy chạy thực hiện kiểm tra để kiểm tra xem mọi thứ có hoạt động chính xác hay không.
Redis có hai lệnh thực thi -
redis-cli - giao diện dòng lệnh cho Redis (phần khách hàng)
redis-server - Kho dữ liệu Redis
Để chạy máy chủ Redis, hãy nhập redis-server trong bảng điều khiển đầu cuối. Bạn sẽ thấy những điều sau:
Bây giờ, hãy mở một cửa sổ đầu cuối mới và chạy redis-cli để mở một ứng dụng khách.
Về cơ bản, Redis là một cơ sở dữ liệu khóa-giá trị. Để tạo khóa có giá trị chuỗi, bạn nên sử dụng lệnh SET. Để đọc giá trị khóa, bạn nên sử dụng lệnh GET. Hãy thêm hai người dùng và mật khẩu cho họ. Các khóa sẽ là tên người dùng và giá trị của các khóa này sẽ là mật khẩu tương ứng.
Bây giờ chúng ta nên sửa đổi máy chủ báo hiệu của mình để thêm xác thực người dùng. Thêm mã sau vào đầu tệp server.js -
//require the redis library in Node.js
var redis = require("redis");
//creating the redis client object
var redisClient = redis.createClient();
Trong đoạn mã trên, chúng tôi yêu cầu thư viện Redis cho Node.js và tạo một ứng dụng khách redis cho máy chủ của chúng tôi.
Để thêm xác thực, hãy sửa đổi trình xử lý thông báo trên đối tượng kết nối -
//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*******
Trong đoạn mã trên, nếu người dùng cố gắng đăng nhập, chúng tôi nhận được từ Redis mật khẩu của họ, hãy kiểm tra xem mật khẩu đó có khớp với mật khẩu được lưu trữ hay không và nếu thành công, chúng tôi lưu trữ tên người dùng của họ trên máy chủ. Chúng tôi cũng thêm cờ isAuth vào kết nối để kiểm tra xem người dùng có được xác thực hay không. Lưu ý mã này -
//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;
}
}
Nếu người dùng chưa được xác thực cố gắng gửi phiếu mua hàng hoặc rời khỏi kết nối, chúng tôi chỉ gửi lại lỗi.
Bước tiếp theo là kích hoạt kết nối ổ cắm an toàn. Nó rất được khuyến khích cho các ứng dụng WebRTC. PKI (Cơ sở hạ tầng khóa công khai) là chữ ký số từ CA (Cơ quan cấp chứng chỉ). Sau đó, người dùng kiểm tra xem khóa riêng tư được sử dụng để ký chứng chỉ có khớp với khóa công khai của chứng chỉ CA không. Vì mục đích phát triển. chúng tôi sẽ sử dụng chứng chỉ bảo mật tự ký.
Chúng tôi sẽ sử dụng openssl. Nó là một công cụ mã nguồn mở triển khai các giao thức SSL (Lớp cổng bảo mật) và TLS (Bảo mật lớp truyền tải). Nó thường được cài đặt theo mặc định trên hệ thống Unix. Chạy phiên bản openssl -a để kiểm tra xem nó đã được cài đặt chưa.
Để tạo khóa chứng chỉ bảo mật công khai và riêng tư, bạn nên làm theo các bước dưới đây:
Generate a temporary server password key
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
Generate a server private key
openssl rsa -passin pass:12345 -in server.pass.key -out server.key
Generate a signing request. You will be asked additional questions about your company. Just hit the “Enter” button all the time.
openssl req -new -key server.key -out server.csr
Generate the certificate
openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt
Bây giờ bạn có hai tệp, chứng chỉ (server.crt) và khóa riêng (server.key). Sao chép chúng vào thư mục gốc của máy chủ báo hiệu.
Để kích hoạt kết nối ổ cắm an toàn, hãy sửa đổi máy chủ báo hiệu của chúng tôi.
//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
Trong đoạn mã trên, chúng tôi yêu cầu thư viện fs đọc khóa cá nhân và chứng chỉ, tạo đối tượng cfg với cổng ràng buộc và đường dẫn cho khóa riêng và chứng chỉ. Sau đó, chúng tôi tạo một máy chủ HTTPS với các khóa của chúng tôi cùng với máy chủ WebSocket trên cổng 9090.
Bây giờ mở https://localhost:9090trong Opera. Bạn sẽ thấy những điều sau:
Nhấp vào nút “vẫn tiếp tục”. Bạn sẽ thấy thông báo “OK”.
Để kiểm tra máy chủ báo hiệu an toàn của chúng tôi, chúng tôi sẽ sửa đổi ứng dụng trò chuyện mà chúng tôi đã tạo trong hướng dẫn “WebRTC Text Demo”. Chúng tôi chỉ cần thêm một trường mật khẩu. Sau đây là toàn bộ tệp 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>
Chúng ta cũng cần kích hoạt kết nối socket an toàn trong tệp client.js thông qua dòng này var conn = new WebSocket ('wss: // localhost: 9090'); . Lưu ý giao thức wss . Sau đó, trình xử lý nút đăng nhập phải sửa đổi để gửi mật khẩu cùng với tên người dùng -
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
var pwd = passwordInput.value;
if (name.length > 0) {
send({
type: "login",
name: name,
password: pwd
});
}
});
Sau đây là toàn bộ tệp 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 = "";
});
Bây giờ chạy máy chủ báo hiệu an toàn của chúng tôi thông qua máy chủ nút . Chạy nút tĩnh bên trong thư mục demo trò chuyện đã sửa đổi. Mởlocalhost:8080trong hai tab trình duyệt. Cố gắng đăng nhập. Hãy nhớ chỉ “user1” với “password1” và “user2” với “password2” mới được phép đăng nhập. Sau đó, thiết lập RTCPeerConnection (gọi người dùng khác) và cố gắng gửi tin nhắn.
Sau đây là toàn bộ mã của máy chủ báo hiệu an toàn của chúng tôi -
//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));
}
Tóm lược
Trong chương này, chúng tôi đã thêm xác thực người dùng vào máy chủ báo hiệu của chúng tôi. Chúng tôi cũng đã học cách tạo chứng chỉ SSL tự ký và sử dụng chúng trong phạm vi ứng dụng WebRTC.