Заметки в категории «JavaScript»

Отменяемые промисы

Промисы не имеют встроенного механизма отмены. Из-за этого разработчики придумывают различные хаки, которые порождают некоторые проблемы.

Дополнительный метод .cancel()

Помимо того, что метод .cancel() делает промис нестандартным, нарушается еще один из главных принципов — только функция, которая создала промис, может изменять его состояние.

Отсутствие очистки после отмены

Некоторые используют Promise.race() в качестве механизма отмены. Однако, в этом случае у функции, создающей промис, нет возможности узнать, что произошла отмена и нужно освободить некоторые ресурсы.

speculation

Эрик Элиот написал маленький модуль speculation, который решает перечисленные выше проблемы.

Сигнатура функций:

speculation(fn: PromiseFunction, shouldCancel: Promise) => Promise

По аналогии с конструктором Promise, первым аргументом передается функция «исполнитель», а вторым аргументом передается промис для отмены.

PromiseFunction(resolve: Function, reject: Function, onCancel: Function) => Void

Функция-исполнитель принимает 3 аргумента и реализует логику разрешения, отклонения и отмены. Вот почему у самого промиса не нужен дополнительный метод .cancel(), и при этом вся логика, связанная с отменой, реализована внутри функции, которая создаёт этот промис.

import speculation from 'speculation';

function doWork(cancel = Promise.reject()) {
  speculation(
    (resolve, reject, onCancel) => {
      // делаем вычисления, которые завершаем вызовом resolve()

      onCancel(() => {
        // отменяем вычисления и освобождаем ресурсы
        reject(new Error('Cancelled'));
      });
    },
    cancel
  );
}

Отменённый промис получает статус rejected.

AbortController и AbortSignal

В браузерах начал появляться немного другой механизм для отмены асинхронных операций, возвращающих промисы. Например, fetch() может принимать объект типа AbortSignal и останавливать запрос.

const controller = new AbortController();
const signal = controller.signal;

const request = fetch(url, {signal});

// запрос можно отменить, если необходимо
controller.abort();

Значение signal.aborted меняется только из контроллера. Одновременно с этим испускается событие abort. В спецификации дан пример, как можно применять AbortController и AbortSignal для своих API.

Отменённый промис так же получает статус rejected, а значение устанавливается в new DOMException('Aborted', 'AbortError').

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

Автоматический вход на сайт с использованием 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);

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

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