Fork me on GitHub

Серверная архитектура DebugMail

debugmail-logo

На днях мы запустили DebugMail - сервис, который помогает упростить тестирование email-рассылок на этапе разработки сайта/приложения/etc. Сервис позволяет сразу после регистрации получить свой виртуальный smtp-сервер, просматривать отправляемые письма, совместно работать над рассылками вместе с коллегами. В этом посте я расскажу про то, как разрабатывался backend сервиса, какие технологии использовались и какие проблемы приходилось решать. Думаю пост будет интересен прежде всего тем, кто работает с Node.js.

Основные используемые технологии:

  • Node.js для запуска нашего js на сервере.
  • CoffeeScript, чтобы добавить немного сахара к нашему js.
  • MongoDB для хранения всех перманентных данных. Mongoose - ODM для работы с MongoDB из под Node.js.
  • Redis, как хранилище для сессий, кэш и для поддержки работы сервера real-time обновлений (pub/sub).
  • Faye система для обмена real-time сообщениями между клиентом и сервером при помощи паттерна pub/sub.

Backend сервиса состоит 3 основных компонентов:

  • SMTP-сервер, который сохраняет все полученные письма и умеет отличать проект к которому относится письмо.
  • REST Api который позволяет выполнять операции по работе с проектами, пользователями и письмами.
  • Сервер для поддержки real-time обновлений при получении новых писем.

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

debugmail-design

SMTP-сервер

Необходимо было разработать SMTP-север, который бы не осуществлял доставку письма на целевой хост, а только сохранял его в базе данных. В этом нам отлично помогли модули simplesmtp и mailparser, которые покрывали все базовые функции необходимые для работы smtp-сервера и парсинга писем. Оставалось только прописать нужные обработчики событий и другой специфичный для задачи код.

В сервисе предполагается, что все письма относятся к определенному проекту. При создании проекта пользователь получает параметры подключения к smtp-серверу среди которых есть пара login-password. На основе этих данных smtp-сервер определяет к какому проекту относится то или иное полученное письмо.

REST API

Для взаимодействия клиентского приложения (см. скоро пост про клиентскую часть) с backend было разработано REST API на базе фреймворка express. Выбран был именно этот фреймворк из-за его простоты, скорости работы и гибкости. При помощи поддержки middleware, которые пришли из connect, можно очень просто встраиваться в процесс обработки запросов. Несколько примеров разработанных middleware:

  • контроль доступа,
  • content negotiation,
  • защита от CSRF,
  • инициализация сессии и тд.

Content negotiation

Для того, чтобы максимально распараллелить работу с front-end разработчиком, было принято решение реализовывать сервис в виде SPA (single page application) и REST API. Далее возник вопрос, как различать запросы, которые отправляет клиентское приложение, от запросов, которые генерируются браузером автоматически (при загрузке страниц) при том, то URL-адреса REST API и клиентского приложения пересекаются. Самым корректным решением нам показалось использование content negotiation. При поступлении запроса срабатывает middleware, который проверяет какой формат ответа является предпочтительным для клиента, на основании значения из заголовка запроса Accept. Клиентское приложение отправляет значение данного заголовка как application/json, что сообщает серверу о том, что нужно обрабатывать запрос, как запрос к REST API и вернуть результат в виде JSON-документа. Во всех остальных случаях предполагается, что запрос приходит от браузера и происходит рендеринг основного шаблона с подключенным клиентским приложением.

Маршрутизация

Изначально было принято решение использовать модуль express-resource, но он оказался недостаточно гибким. Например, нельзя задавать кастомные middleware для определенных маршрутов. Pull Request для решения описанной проблемы висит уже больше года, в итоге от express-resource пришлось отказаться в пользу стандартного способа задания маршрутов в express.

Real-time updates

Одна из фич сервиса предполагает, что при получении нового письма оно должна сразу появляться в интерфейсе. Для реализации таких real-time обновлений существует несколько популярных решений:

Мы остановились на Faye, т.к. он:

  • имеет достаточно удобное и простое API,
  • из коробки поддерживаются server-side clients, что важно, т.к. сообщения генерируются из SMTP-сервера,
  • достаточно активно разрабатывается.

Аутентификация пользователей

Основной сложностью было реализовать аутентификацию клиентов, т.к. сессии, которые используются в основном приложении и REST API, здесь не доступны. Для решения этой проблемы была введена аутентификация при подписке на обновления проекта. Предпологается, что при инициализации подписки на уведомления клиент передает user id, session id и token. token является результатом выполнения хэш-функции к параметрам user id, session id и некоторого закрытого ключа. Чтобы реализовать такой механизм отлично подходят, т.н. extensions, которые позволяют перехватывать сообщения и выполнять дополнительные действия (проверки, дополнительную обработку данных и тд.). В итоге получается примерно такой код:

client.addExtension({
  outgoing: function(message, callback) {
    if (message.channel !== '/meta/subscribe')
      return callback(message);

    message.ext = message.ext || {};
    message.ext.uid = USER_ID;
    message.ext.sid = SESSION_ID;
    message.ext.token = PRIVATE_TOKEN;
    callback(message);
  }
});

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

Автоматическое тестирование

Зачем нужно писать тесты я думаю объяснять не нужно. В данном проекте мы хотели добиться максимального покрытия кода тестами. Для этого практически весь код тестируется сразу на 2х уровнях, на функциональном и на модульном.

Писать тесты на node.js - одно удовольствие. В основном это так благодаря большому количеству удобных инструментов и модулей в npm. Также помогают особенностям javascript и node.js, которые предоставляют все необходимые возможности для удобного создания моков. Рассмотрим, какие модули использовались и с какой целью:

  • mocha - многофункциональный тестовый фреймворк, содержит все необходимые функции для структурирования, поиска, запуска, подсчета метрик, просмотра результатов выполнения тестов и тд. Поддерживает как синхронные так и асинхронные тесты и имеет широкие возможности для кастомизации процесса тестирования.
  • supertest - модуль для выполнения функционального тестирования. Все что нужно для начала работы с supertest - передать ему экземпляр приложения express, после этого можно выполнять запросы к приложению и тестировать полученные ответы. Также supertest предоставляет удобный chaining api, типичный тест выглядит так:
request(app)
  .get('/user')
  .set('Accept', 'application/json')
  .expect(200)
  .end(function(err, res){
    if (err) return done(err);

    // ... check something here ...

    done()
  });
  • should.js - это библиотека для удобного выполнения assertions. При инициализации библиотека перегружает встроенные объекты javascript, что позволяет использовать красивые конструкции вроде user.should.have.property('company', 'wbtech'). Предоставляет большое количество полезных фич, что делает написание тестов более приятным и быстрым процессом.
  • sandboxed-module - модуль для dependency injection. Позволяет загрузить модуль с подменой указанных модулей, а также локальных и глобальных переменных.
  • nock - позволяет перехватывать запросы, которые делает код, через http протокол. Можно переопределять все запросы или по определенным признакам (например, url запроса, заголовки или тело запроса) и задавать ответ, который будет возвращаться в вызываемый код. Есть много дополнительных плюшек, вроде задания задержек, chaining api и тд.

Стоит отметить, что тесты выполняются очень быстро. На данный момент 320 тестов выполняются примерно за 17с (в один поток).

Некоторые используемые модули

Список некоторых полезных модулей, о которых не упоминал выше:

  • async, lodash - незаменимые модули, полезны практически в каждом проекте.
    • async предоставляет очень простой способ для выполнения асинхронного кода.
    • lodash модуль для упрощения работы с различными структурами данных, также содержит методы для упрощения функционального программирования и многое другое.
  • ect - очень быстрый и в тоже время гибкий шаблонизатор, который работает с синтаксисом CoffeeScript.
  • nodemailer - для отправки писем, есть все что нужно, кастомизируется.
  • passport - все что нужно для аутентификации через сторонние ресурсы. Поддерживает множество провайдеров через сторонние модули.
  • raven-node - node.js клент к Sentry.

ссылки

онлайн