Русский
Русский
English
Статистика
Реклама

Socket.io

Перевод Создание видеочата с помощью Node.js Socket.io WebRTC

06.04.2021 18:11:06 | Автор: admin

Сегодня, специально к старту нового потока по веб-разработке, поделимся с вами туториалом, из которого вы узнаете, как создать видеочат с помощью JavaScript и NodeJS. Также вы научитесь использовать PeerJS, WebRTC и Socket.io.


Здесь вы можете увидеть живой пример приложения, которое вы будете создавать.

Подготовка к проекту

Вот что вам понадобится:

  • NodeJS: посетите официальный веб-сайт Node.js, чтобы загрузить и установить Node;

  • NPM: программа NPM устанавливается на ваш компьютер при установке Node.js.

Настройка проекта

Весь код этого проекта можно найти в репозитории GitHub.

  1. Создайте пустой каталог с именем video-chat-app.

  2. Откройте консоль, перейдите в наш новый каталог и запустите npm init.

  3. Заполните необходимую информацию для инициализации нашего проекта.

  4. Запустите npm install express ejs socket.io uuid peer. Команда установит все зависимости, необходимые для создания этого приложения.

  5. А также в качестве dev-зависимости установите Nodemon. Нужно выполнить npm install-dev nodemon. Это установит nodemon как dev-зависимость.

  6. Создайте файл server.js в этом файле будет храниться вся ваша серверная логика.

Теперь, когда у вас настроен наш проект, вы можете приступить к созданию приложения!

Создание сервера (с Express JS)

Первое, что вам нужно сделать, это запустить ваш сервер. Мы собираемся использовать для этого Express. Express это минималистичный веб-фреймворк для Node.js. Express позволяет очень легко создавать и запускать веб-сервер с помощью Node.

Давайте создадим шаблонный файл начального приложения Express.

// server.jsconst express = require(express);const app = express();const server = require(http).Server(app);app.get(/, (req, res) => {    res.status(200).send(Hello World);});server.listen(3030);

Теперь ваш сервер запущен, вы можете протестировать его, запустив:

> nodemon server.js

Теперь откройте свой браузер и перейдите по адресу: localhost:3000, вы должны увидеть Hello World.

Создание первой страницы

Вместо того чтобы выводить текст, когда кто-то посещает ваш корневой маршрут, вы хотели бы присылать HTML. Для этого нужно использовать EJS (встроенный JavaScript). EJS это язык шаблонов.

Чтобы использовать EJS в Express, вам нужно настроить ваш шаблонизатор. Для настройки добавьте эту строку кода в файл server.js.

app.set(view engine, ejs)

Доступ к EJS по умолчанию осуществляется в каталоге views. Теперь создайте новую папку views в каталоге. В этой папке добавьте файл с именем room.ejs. Пока что думайте о нашем файле room.ejs как о HTML-файле.

Вот как выглядит ваша файловая структура:

|-- video-chat-app   |-- views      |-- room.ejs   |-- package.json   |-- server.js

Теперь добавьте HTML-код в файл room.ejs.

Как только вы скопируете приведённый выше код, нужно немного поменять app.js:

app.get(/, function (req, res) { // OLD CODE res.status(200).send("Hello World");})

Выше приведён старый код, в котором вы отправляете клиенту текст Hello World!. Вместо этого вы хотите отправить файл room.ejs:

app.get(/, function (req, res) { // NEW CODE res.render(room);})

Теперь откройте браузер и перейдите по адресу: localhost:3030, и вы увидите, что отображается файл room.ejs!

Добавление CSS

Выглядит не очень хорошо, правда? Это потому, что в вашем проекте нет стилей. Итак, добавьте немного CSS.

Нам нужно будет добавить новую папку в проект под названием public. В этой папке создайте файлы style.css и script.js. Вот ваша новая файловая структура:

|-- weather-app   |-- views      |-- index.ejs   |-- public      |-- style.css      |-- script.js   |-- package.json   |-- server.js

Express не даёт доступа к этому файлу по умолчанию, поэтому вам нужно открыть его с помощью следующей строки кода:

app.use(express.static(public));

Этот код позволяет вам получить доступ ко всем статическим файлам в папке public. Наконец, вам нужен CSS. Поскольку это не курс по CSS, я не буду вдаваться в подробности, но если вы хотите использовать мои стили, вы можете скопировать их отсюда.

После того как вы добавили CSS, вы можете посетить: localhost:3030. Вы заметите, что приложение выглядит немного лучше.

Настройка комнат

К настоящему моменту ваш файл server.js должен выглядеть так:

У вас есть один GET-роут и запуск сервера. Однако, чтобы ваше приложение работало, нужно всякий раз, когда новый пользователь посещает ваш роут по умолчанию, перенаправлять его на уникальный URL-адрес. Следует использовать библиотеку uuid для создания случайного уникального URL-адреса для каждой комнаты.

UUID это библиотека javascript, которая позволяет вам создавать уникальные идентификаторы. В вашем приложении вы будете использовать uuid версии 4 для создания уникального URL. Но сначала импортируйте uuid в server.js.

const { v4: uuidv4 } = require("uuid");

Теперь нужно использовать uuid для создания случайного уникального идентификатора для каждой комнаты и перенаправлять пользователя в эту комнату.

app.get(/, (req, res) => {    res.redirect(`/${uuidv4()}`);});

И, прежде чем вы протестируете это, я также хотел добавить страницу для каждой уникальной комнаты, и вы передадите текущий URL этой странице.

app.get(/:room, (req, res) => {    res.render(room, { roomId: req.param.room });});

Вы передали roomId в room.ejs на этом закончили настройку ваших комнат. А теперь, если вы посетите localhost:3030, вы будете перенаправлены на уникальный URL.

Добавление видео пользователя

Вы будете работать с файлом script.js, который вы создали ранее. script.js будет содержать весь клиентский код приложения.

Итак, вот что необходимо сделать: нужно получить видеопоток, а затем добавить этот поток в элемент видео.

let myVideoStream;const videoGrid = document.getElementById("video-grid");const myVideo = document.createElement("video");myVideo.muted = true;navigator.mediaDevices.getUserMedia({    audio: true,    video: true,}).then((stream) => {    myVideoStream = stream;    addVideoStream(myVideo, stream);});

Теперь создайтем функцию addVideoStream, которая добавит поток к видеоэлементу.

const addVideoStream = (video, stream) => {    video.srcObject = stream;    video.addEventListener("loadedmetadata", () => {       video.play();       videoGrid.append(video);    });};

Этот код добавит пользовательский поток к видеоэлементу. Вы можете проверить это, посетив localhost:3030, и вы увидите всплывающее окно с видео

Добавление возможности разрешить другим пользователям транслировать свои видео в потоковом режиме.

Пришло время использовать Socket.io и PeerJS. Для тех, кто не знает, Socket.io позволяет взаимодействовать серверу и клиенту в режиме реального времени. PeerJS позволяют реализовать WebRTC.

Сначала импортируйте socket.io и peerjs в server.js и прослушайте событие соединения.

// server.jsconst express = require(express);const app = express();const server = require(http).Server(app);const { v4: uuidv4 } = require(uuid);app.set(view engine, ejs);const io = require(socket.io)(server);const { ExpressPeerServer } = require(peer);const peerServer = ExpressPeerServer(server, {    debug: true,});app.use(/peerjs, peerServer);app.use(express.static(public));app.get(/, (req, res) => {    res.redirect(`/${uuidv4()}`);});app.get(/:room, (req, res) => {    res.render(room, { roomId: req.param.room });});io.on(connection, (socket) => {    socket.on(join-room, (roomId, userId) => {    socket.join(roomId);    socket.to(roomId).broadcast.emit(user-connected, userId);    });});server.listen(3030);

Теперь ваш сервер прослушивает событие присоединения к комнате. Далее настройте ваш script.js.

// public/script.jsconst socket = io(/);const videoGrid = document.getElementById(video-grid);const myVideo = document.createElement(video);myVideo.muted = true;var peer = new Peer(undefined, {  path: /peerjs,  host: /,  port: 3030,});let myVideoStream;navigator.mediaDevices  .getUserMedia({  audio: true,  video: true,}).then((stream) => {  myVideoStream = stream;  addVideoStream(myVideo, stream);  peer.on(call, (call) => {    call.answer(stream);    const video = document.createElement(video);    call.on(stream, (userVideoStream) => {      addVideoStream(video, userVideoStream);      });    });  socket.on(user-connected, (userId) => {      connectToNewUser(userId, stream);  });});const connectToNewUser = (userId, stream) => {  const call = peer.call(userId, stream);  const video = document.createElement(video);  call.on(stream, (userVideoStream) => {      addVideoStream(video, userVideoStream);  });};peer.on(open, (id) => {    socket.emit(join-room, ROOM_ID, id);});const addVideoStream = (video, stream) => {  video.srcObject = stream;  video.addEventListener(loadedmetadata, () => {    video.play();    videoGrid.append(video);  });};

Теперь, если в комнату войдёт новый пользователь, вы увидите его видео.

Создание пользовательского интерфейса

С видеочастью закончили. А теперь займитесь стилизацией. Но сначала добавьте контент в файл room.ejs. (Добавьте CDN font-awesome внутри тега head.)

// views/room.ejs<body>  <div class="header">    <div class="logo">      <h3>Video Chat</h2>    </div>  </div>  <div class="main">    <div class="main__left">      <div class="videos__group">        <div id="video-grid"></div>      </div>      <div class="options">        <div class="options__left">          <div class="options__button">            <i class="fa fa-video-camera" aria-hidden="true"></i>          </div>        <div class="options__button">            <i class="fa fa-microphone" aria-hidden="true"></i>        </div>       </div>       <div class="options__right">         <div class="options__button background__red">           <i class="fa fa-phone" aria-hidden="true"></i>         </div>       </div>     </div>    </div>    <div class="main__right">      <div class="main__chat_window">        <ul class="messages"></ul>     </div>     <div class="main__message_container">       <input id="chat_message" type="text" placeholder="Type message here...">       <div class="options__button">         <i class="fa fa-plus" aria-hidden="true"></i>       </div>     </div>    </div>  </div></body>

Затем откройте файл style.css и добавьте немного CSS.

@import url(http://personeltest.ru/aways/fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap");:root {   main-darklg: #1d2635;   main-dark: #161d29;   primary-color: #2f80ec;   main-light: #eeeeee;  font-family: Poppins, sans-serif;}* {  margin: 0;  padding: 0;}.header {  display: flex;  justify-content: center;  align-items: center;  height: 8vh;  width: 100%;  background-color: var(  main-darklg);}.logo > h3 {    color: var(  main-light);}.main {  overflow: hidden;  height: 92vh;  display: flex;}.main__left {  flex: 0.7;  display: flex;  flex-direction: column;}.videos__group {  flex-grow: 1;  display: flex;  justify-content: center;  align-items: center;  padding: 1rem;  background-color: var(  main-dark);}video {  height: 300px;  border-radius: 1rem;  margin: 0.5rem;  width: 400px;  object-fit: cover;  transform: rotateY(180deg);  -webkit-transform: rotateY(180deg);  -moz-transform: rotateY(180deg);}.options {  padding: 1rem;  display: flex;  background-color: var(  main-darklg);}.options__left {    display: flex;}.options__right {    margin-left: auto;}.options__button {  display: flex;  justify-content: center;  align-items: center;  background-color: var(  primary-color);  height: 50px;  border-radius: 5px;  color: var(  main-light);  font-size: 1.2rem;  width: 50px;  margin: 0 0.5rem;}.background__red {    background-color: #f6484a;}.main__right {flex: 0.3;    background-color: #242f41;}.main__chat_window {    flex-grow: 1;}.main__message_container {  padding: 1rem;  display: flex;  align-items: center;  justify-content: center;}.main__message_container > input {  height: 50px;  flex: 1;  border-radius: 5px;  padding-left: 20px;  border: none;}#video-grid {  display: flex;  justify-content: center;  flex-wrap: wrap;}

Вот и всё! Поздравляем, вы успешно создали видеочат! Теперь вы можете развернуть его на Heroku и показать его всему миру. Демо и исходный код.

Это лишь небольшой пример того, какие вещи может делать веб-разработчик, причем в одиночку. Сделать just for fun экспорт музыки в Spotify и получить известность, пока огромная компания долго думает над решением задачи без проблем. За один вечер набросать и выкатить расширение для браузера, которое упростит жизнь миллионам пользователей тоже по силам. На что еще способна веб-разработка зависит только от фантазии программиста. Приходите учиться, чтобы освоить дзюцу веб-разработки и стать настоящим самураем интернета.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Авторизация и аутентификация на NodeJs и Socket.io и проблемы вокруг

19.01.2021 12:23:47 | Автор: admin

На текущий момент я работаю в компании МегаФон тимлидом фронта. С начала 2020 года мы в команде МегаФона разрабатываем собственную платформу Интернета вещей. Так как в таком процессе нагрузка на бэк-энд разработчиков стала колоссальной, а фронт не так активно задействован, внутри отдела было принято решение отдать всю веб-часть в руки моей команды. Очевидно, что мы взяли NodeJs с ExpressJS, и занялись построением серверной архитектуры.

Для корректного доступа к собранным с устройств данным нужна была авторизация, чтобы понимать, кто и что может получить/сделать. Про очевидный путь с passportJs, думаю, нет смысла рассказывать. По логину и паролю мы стали отдавать jwt токен, и изначально на этом успокоились.

После этого нам потребовалось хранить в сессии данные, специфичные для каждого пользователя. Логичным решением было бы использовать сам jwt токен, хранить информацию в нем и гонять от клиента к серверу. Однако, данное решение не подходило нам из-за использования веб-сокетов (в нашем случае мы взяли socket.io), так как в данном протоколе передача хедера Authorization с jwt токеном невозможна (в соответствии со стандартом). Единственный вариант - передавать хедер в параметрах url. Но это не очень здорово - токены будут легко видны во всех логах всех прокси-серверов. Хорошим решением оказалось использование сессии, которая хранится полностью на серверной стороне, и по сети ходит лишь id этой сессии. Мы выбрали - express-session.

Объединенная сессия

Отдельной проблемой стала необходимость получения актуального состояния сессии и возможность его изменения в событиях веб-сокетов. Для этого идеально подошел пакет - express-socket.io-session. Правда, пришлось поколдовать над её подключением:

Изменили подключение сессии и настройки кук:

this.store = new pgSession({          pool: pgPool,          tableName: SESSION_TABLE      });this.session = expressSession({    name: SESSION_KEY,    secret: SESSION.secret,    resave: false, // важно, для того, чтобы сессия не перезаписывалась на каждый чих    rolling: true,    saveUninitialized: true, // нужно для выдачи куки даже неавторизированному пользователю    proxy: true,    cookie: {        secure: true, // обязывает производить передачу по ssl        maxAge: SESSION_DURATION,        sameSite: 'none' // чтобы можно было отдавать на разные поддомены    }    store: this.store});

Мы написали обработчики сессии таким образом, чтобы сессия подгружалась до начала обработки события, и сохранялась, если необходимо:

const asyncHandlerExtended = (fn, socket) => (data) => {    const cb = async () => {        await reloadSession(socket.handshake.session);        await fn({ socket, data });        await saveSession(socket.handshake.session);    };    return Promise.resolve(cb()).catch((err) => {        socket.emit('error', err);    });};

Собрали все вместе при настройке сокетов:

import sharedSession from 'express-socket.io-session';import io from 'socket.io';const resultSocket = nameSpace ? this.io.of(nameSpace) : this.io;resultSocket.use(sharedSession(session, { autoSave: true }));

Разделение сокетов по ролям

Дальше нам нужно понимание того, кому и какие события можно получать на сокетах, а какие - нет. Для этого отлично подходит механизм комнат в socket.io. Он позволяет серверу формировать пространства, в которые можно "запускать" пользователей и эмитить в них разные события. Мы выделили под каждую из ролей пользователей отдельную комнату (например комната adminRoom - пространство для событий, которые могут идти/поступать только для администраторов), а общее пространство теперь у нас считается "публичным" и доступно для всех подключенных, но не авторизованных пользователей. Таким образом, процесс получения доступов на сокетах выглядит так:

  1. Клиент аутентифицируется по http, по паре логин/пароль, получает в ответ jwt токен и куку с id сессии.

  2. Далее юзер коннектится к нашей точке входа для socket.io (например: localhost:8080/sockets). Теперь у него есть доступ до публичных событий на наших сокетах.

  3. Если он хочет получить доступ до всех наших событий, которые ему доступны по роли, то он отправляет событие auth_login по сокетам, с jwt токеном, который он получил от http авторизации.

  4. Система проверяет токен и по результатам проверки генерирует одно из двух событий.

    1. auth_loginFailed - пользователю не будут предоставлены доступы, так как токен кривой или просрочен

    2. auth_loginSuccess - все хорошо, можно продолжать

  5. Если проверка прошла успешно, то сервер добавляет пользователя во все пространства предоставленные ему по его роли.

  6. Пользователю теперь доступны аутентифицированные и скрытые ранее события.

  7. Когда у токена проходит "срок годности", сокеты генерируют событие auth_expire, говорящее, что более пользователю недоступны ранее предоставленные комнаты.

Token steal

Вишенкой на торте в данной картине механизма авторизации/аутентификации стал результат изучения проблемы кражи токенов. Ради минимизации рисков от попадания в такую ситуацию, было решено улучшить механизмы работы с авторизационным токеном. Во время исследования данной темы наткнулся на статью на хабре - Зачем нужен Refresh Token, если есть Access Token?. Очень советую ознакомиться, но если кратко, то вот результирующая цитата:

Таким образом, схема refresh + access токен ограничивает время, на которое атакующий может получить доступ к сервису. По сравнению с одним токеном, которым злоумышленник может пользоваться неделями и никто об этом не узнает.

Однако, у нас уже есть два токена:

  • id серверной сессии от express-session, который ходит в куках, всегда

  • jwt токен, который генерируется после логина пользователя

Поэтому было бы логично реализовать похожий механизм, как и в OAuth2. Для этого мы стали хранить jwt токен в сессии и сравнивать его с полученным от клиента при проверке аутентификации. Остается только одна проблема - несколько токенов в одной сессии. Необходимо это для того, чтобы иметь возможность войти и в админку, и на фронт. Для этого считаем, что клиент передаст свое "название" при логине, и под этим названием мы и сохраним токен в сессию, а также записываем само название внутрь токена для дальнейшей проверки. За счет вышеописанного получается следующая схема взаимодействия:

  1. Клиент отправляет пару логин и пароль, плюс к этому уникальный ключ - название себя (то есть имя приложения).

  2. Система, если пара логин и пароль найдена, генерирует jwt токен, включая в него название клиента (ключ), отправляет его клиенту и записывает в сессию.

  3. При последующем обращении клиента по роуту, который закрыт проверкой аутентификации, проверяется наличие токена, его валидность, просрочен ли он. Далее сервер вытаскивает из токена название клиента и смотрит, есть ли такой токен с таким ключом в текущей сессии.

  4. Если вдруг токен и куку украл злоумышленник, то при первом же рефреше с любой из сторон (клиент или злоумышленник), сторона, которая попытается обратиться со старой парой токен + кука, получит негативный результат. Система поймет, что токен не соответствует тому, что хранит сессия и очистит сессию полностью, что привет к выбросу и клиента и злоумышленника, а факт кражи будет зафиксирован в системе.

  5. Если же все хорошо, то само собой мы отдадим данные =)

Таким образом, мы получили многосессионный механизм (один пользователь может зайти сразу с нескольких браузеров в одно приложение) аутентификации, который работает еще и несколько приложений сразу.

В заключение

Надеюсь, описанная выше логика работы поможет читателям реализовать достаточно защищенные веб-сервера. На текущий момент в статье приведено мало снипетов кода, так как логика разбросана по разным файлам и частям кода, и собрать ее воедино будет проблематично. Но если будет проявлен интерес в комментариях к решению той или иной проблемы, я постараюсь дополнить статью тем или иным куском кода, примером и т.п.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru