Заметки в категории «JavaScript» (страница 2)

Автоматический вход на сайт с использованием Credential Management API

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

Credential Management API позволяет разработчикам сайта сохранять и получать данные для аутентификации. Обязательным условием работы этого API является безопасное соединение с сервером.


if (window.PasswordCredential) {
  navigator.credentials
    .get({
      password: true,
      mediation: 'optional'
    })
    .then(credential => {
      if (!credential) {
        return;
      }

      console.log(`Username: ${credential.id}`);
      console.log(`Password: ${credential.password}`);
    });
}

Если выполнить этот фрагмент кода на любом сайте, где у вас есть сохранённые пароли, то браузер напечатает их в консоли. Дальнейшие действия с этими данными ложатся на плечи разработчика сайта. Их можно подставить в существующие поля ввода и инициализировать отправку формы на сервер.

Свойство mediation у параметра метода .get() определяет сценарий запроса подтверждения у пользователя. Допустимые значения:

  • required — браузер всегда будет запрашивать разрешение;
  • optional — браузер будет запрашивать разрешение, если пользователь до этого момента не дал его;
  • silent — браузер не будет явно запрашивать разрешение.

Sign in with your account saved with Google Chrome

Чтобы отозвать ранее выданное разрешение (например, когда пользователь выходит из системы) нужно вызвать метод .preventSilentAccess().


if (navigator.credentials && navigator.credentials.preventSilentAccess) {
  navigator.credentials.preventSilentAccess();
}
Комментарии к заметке: 2

Способы создания реактивного потока данных

В реактивном программировании класс Observable является базовым и все манипуляции, как правило, происходят с его экземплярами.


const observable = Rx.Observable.create(
  function subscribe(observer) {
    observer.next(1);
    observer.next(2);
    observer.next(3);
    observer.complete();
  }
);

observable.subscribe(n => console.log(n));

В этом примере данные генерируются только внутри функции subscribe . Повлиять на поток данных извне никак нельзя. Чтобы управлять потоком данных, объект должен реализовывать методы класса Observer, т.е. методы next() , error() и complete().

Когда нужно генерировать данные в другом месте кода, то чаще всего используют класс Subject. Он представляет собой комбинацию Observable и Observer.


const subject = new Rx.Subject();

subject.subscribe(n => console.log(n));

subject.next(1);
subject.next(2);
subject.next(3);
subject.complete();

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


const delta$ = new Rx.Subject();

function handleNextTrack() {
  delta$.next(1);
}
function handlePrevTrack() {
  delta$.next(-1);
}

const playlist = (
  <Playlist
    onNextTrack={handleNextTrack}
    onPrevTrack={handlePrevTrack}
  />
);

Экземпляр класса Subject уведомляет своих подписчиков только когда появляются новые данные. Это напоминает как работают события — если вы подписались после того как событие произошло, то уведомление вы получите лишь в момент наступления следующего события. По этой причине для экземпляров Subject бесполезно применять операторы типа .startWith().

В RxJS есть специальный класс BehaviorSubject . Экземпляр этого класса хранит последнее событие и как только появляется очередной подписчик, он сразу получает последнее значение. На примере плейлиста, поток направления воспроизведения — это экземпляр Subject, а поток позиций в плейлисте — это экземпляр BehaviorSubject.

С другой стороны, поток позиции может быть получен из потока направления через свёртку методом .scan().


const position$ = Rx.Observable.from(delta$)
  .startWith(0)
  .scan((position, delta) => position + delta);
Оставте свой комментарий

CSRF-атака на ваш API

Иван Гришаев в своей статье Руководство по кросс-доменным запросам (CORS) подробно рассказываем про механизм осуществления запроса на другой сервер. В частности, он описывает «простые» и «сложные» запросы.

Простым считается запрос методами:

  • HEAD
  • GET
  • POST

и заголовками:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type, но только со значениями:
    — application/x-www-form-urlencoded
    — multipart/form-data
    — text/plain

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

Вот такие простые запросы злоумышленники могут эксплуатировать для CSRF-атак.

fetch('https://jsfiddle.net/echo/html/', {
  method: 'POST',
  credentials: 'include',
  body: new URLSearchParams({
    html: 'My test response'
  })
});

Даже если ответ не был принят браузером, тем не менее сервер уже совершил какие-то операции. А так как вместе с запросом автоматически передаются cookie, то эти операции могу быть выполнены от лица авторизованного пользователя или администратора.

Как защититься?

  1. Принимаем только запросы с заголовком Content-Type: application/json.

    Это автоматически делаем запрос «сложным». Он будет выполнен в 2 этапа: сначала браузер проверит какие заголовки разрешены для CORS-запроса, а затем выполнит основной запрос.

  2. Правильно выставляем заголовки Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methods и Access-Control-Max-Age.

  3. Авторизацию пользователя выполняем только по значению в заголовке, а не по cookie.

    Стоит отметить, что хранить авторизационный токен в cookie не опасно. Но использовать его на сервере нельзя так как все cookie для домена, на который выполняется запрос, как я уже упомянул, пересылаются автоматически.

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

Оставте свой комментарий

util.promisify() для замены колбэков на промисы

Промисы на практике оказались удобнее колбэков из-за того, что не требуют постоянного увеличения вложенности при работе с цепочками асинхронных функций. Более того, на базе промисов строится работа async function.

Все асинхронные функции node.js и подавляющее большинство асинхронных функций внешних модулей на данный момент всё же используют колбэки, чтобы вернуть результат работы или сообщить об ошибке.

Технически, не сложно обернуть асинхронную функцию в промиз. Но, к сожалению, это не одна строчка кода.


const fs = require('fs');

function reader(path) {
    return new Promise((resolve, reject) => {
        fs.readdir(path, (err, list) => {
            if (err) {
                reject(err);
                return;
            }

            resolve(list);
        });
    });
}

reader('.')
    .then(list => {
        console.log(list);
        process.exit(0);
    })
    .catch(err => {
        console.error(err);
        process.exit(1);
    });

В стандартной библиотеке node.js 8 util появилась функция promisify(), которая как раз решает эту задачу.


const fs = require('fs');
const util = require('util');

const reader = util.promisify(fs.readdir);

reader('.')
    .then(list => {
        console.log(list);
        process.exit(0);
    })
    .catch(err => {
        console.error(err);
        process.exit(1);
    });

Функция promisify() придерживается соглашения:

  1. колбек в параметрах исходной функции всегда идет последним;
  2. первым параметром колбека приходит объект ошибки, если она есть.

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


function Timer(delay) {
    this._delay = delay;
}

Timer.prototype.start = function (callback) {
    setTimeout(callback, this._delay);
};

const timer = new Timer(1000);
timer.start(() => {
    console.log('Finish');
});

Из метода start() я хочу сделать новую функцию, возвращающую мне промиз.


const start = util.promisify(timer.start);
start().then(() => {
    console.log('Finish');
});

Но этот фрагмент кода перестаёт работать. Он начинает выдавать ошибку:

TypeError: Cannot read property '_delay' of undefined

Проблема кроется в том, что метод внутри себя использует ссылку на экземпляр класса Timer — в частности, он вытаскиваем величину задержки. А так как метод был вызван вне контекста экземпляра таймера, то this оказалось не определено.

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


const start = timer.start;
start(() => {
    console.log('Finish');
});

Исправить ситуацию можно несколькими путями.

Связать функцию с экземпляром

const start = util.promisify(timer.start).bind(timer);

Теперь не важно как мы вызовем функцию start() . Она всегда будет иметь экземпляр таймера в качестве контекста исполнения.

Сохранить функцию в экземпляр класса


timer.start = util.promisify(timer.start);
timer.start().then(() => {
    console.log('Finish');
});

Для данного экземпляра мы подменили реализацию метода из прототипа на новую функцию. Контекст исполнения будет передаваться естественным образом.

Развивая этот вариант дальше, новый метод можно сохранить в прототипе класса.

Timer.prototype.start = util.promisify(Timer.prototype.start);

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

Оставте свой комментарий

Шаблон тестирования Page Object

Функциональные тесты большинством воспринимаются как линейные сценарии: открой страничку, нажми эту кнопку, введи такой-то текст в поле ввода, нажми другую кнопку. Сценарии похожи на то, как действует реальный пользователь. Однако они плохи тем, что требуется повторять много однотипных действий в разных тестах, чтобы попасть в нужное исходное состояние. Если меняется содержимое станицы (например, разметка или классы у элементов), то нужно во все тестах внести соответствующие изменения.

В документации к Selenium описан другой подход — Page Object. Его особенность заключается в том, что страница представлена в виде модели с которой взаимодействует тест. Это уменьшает количество повторения кода. Доступ к элементам страницы осуществляется только в одном месте. Если поменяется разметка, то потребуется минимум изменений в коде Page Object, а тесты не нужно будет менять вообще.

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

Смотрите пример — https://gist.github.com/mistakster/597f110631fe8a5cde6b. Инфраструктура для запуска этого теста описана в заметке Node.js + mocha + selenium-webdriver.

Я тестирую главную страницу Яндекса. Она должна удовлетворять следующей спецификации:


Yandex home page
  √ should be valid (4857ms)
  √ should have at least 9 tabs above the search (4358ms)
  √ should have a weather widget (1562ms)

Сам тест выглядит очень просто и понятно. Вся магия скрыта в объекте Page и двух компонентах, которые должны быть на странице.

Когда мы открываем страницу, то не должны знать её адреса. Для разных окружений (локальная разработка, стейджинг, продакшин) он может быть разным. В тесте у Page Object просто вызывается соответствующий метод open(). Мы так же должны быть уверены, что страница открылась и на ней правильное содержимое. За это отвечает метод validate() . Итак, всю логику по взаимодействию с драйвером мы прячем в Page Object за компактным фасадом из методов.

Методы Page Object должны возвращать объекты других страниц или объекты компонент. Так осуществляются переход между страницами или в тесте можно получить доступ к отдельным блокам на странице. Например, в тесте можно получить список всех пунктов меню, которое расположено над поисковой строкой, или текущую погоду в вашем городе.

Комментарии к заметке: 2