Свежие заметки

Автоматический вход на сайт с использованием 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 для домена, на который выполняется запрос, как я уже упомянул, пересылаются автоматически.

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

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

Автоматическое развёртывание проекта при обновлении кода в репозитории

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

Коммит в репозиторий, сборка, копирование артефакта

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

Для хостинга приватных Git-репозиториев я использую Bitbucket. Там есть настройка «Webhooks». Webhook — это URL, на который Bitbucket делает POST-запрос. Нам нужно написать скрипт, который бы вызывался бы на каждый запрос, загружал изменения из Git-запозитория и выполнял другие полезные действия.

Все команды и настройки были произведены на чистом сервере Ubuntu 17.04

Подготовка окружения

В файл /etc/apt/sources.list.d/nginx.list добавляем адрес репозитория с официальными пакетами nginx:

deb http://nginx.org/packages/ubuntu/ zesty nginx
deb-src http://nginx.org/packages/ubuntu/ zesty nginx

Теперь загрузим ключ, которым подписаны пакеты и установим nginx и fcgiwrap:

curl -L https://nginx.ru/keys/nginx_signing.key | apt-key add -
apt-get update
apt-get install nginx fcgiwrap

Добавим конфигурацию сервера, который будет принимать запросы от Git-репозитория. Создадим файл /etc/nginx/conf.d/deploy.conf со следующим содержимым:

server {
  listen 80;
  listen [::]:80;

  server_name deploy.my-cool-project.com;

  location /push-hook {
    gzip off;
    fastcgi_pass unix:/var/run/fcgiwrap.socket;
    include /etc/nginx/fastcgi_params;
    fastcgi_param DOCUMENT_ROOT /var/www/deploy;
    fastcgi_param SCRIPT_FILENAME /var/www/deploy/deploy.sh;
  }
}

Каждый раз, когда nginx получить запрос по адресу http://deploy.my-cool-project.com/push-hook, на сервере будет выполнен скрипт /var/www/deploy/deploy.sh. Давайте создадим его.

mkdir -p /var/www/deploy
nano deploy.sh

Содержимое файла deploy.sh:

#!/bin/sh

echo "Content-type:text/plain\n"
echo "OK!"

Обязательно установим флаг «eXecute» у этого скрипта:

chmod 755 /var/www/deploy/deploy.sh

Теперь можно проверить как он работает:

curl http://ci.my-cool-project.com/push-hook

Клонирование репозитория

Перейдём к настройке git и первоначальному клонированию проекта на сервере.

apt-get install git

Генерируем ключ для доступа:

ssh-keygen -t rsa -f /var/www/deploy/access-key -N ""

Добавляем этот ключ в панели управления репозиторием. Ключ для доступа имеет права «только чтение» и специально создаётся для того, чтобы система CI/CD могла получать изменения.

Сохраняем публичный ключ удаленного сервера:

ssh-keyscan -t rsa bitbucket.org 2>&1 >> /etc/ssh/ssh_known_hosts

Команда ssh делает это автоматически при первом подключении к неизвестному хосту во время процедуры обмена ключами. Но сохраняет она этот публичный ключ в файл для пользователя, выполнившего команду, и только после положительного ответа на запрос. Чтобы команда git pull нормально выполнялась в фоновом процессе под пользователем www-data, публичный ключ нужно добавить вручную в общий файл.

Установим владельца папки /var/www/deploy и её содержимого:

chown -R www-data:www-data /var/www/deploy

Теперь можно клонировать репозиторий:

cd /var/www/deploy
GIT_SSH_COMMAND='ssh -i /var/www/deploy/access-key -o UserKnownHostsFile=/dev/null'
git clone git@bitbucket.org:mistakster/test-project.git ./repo
chown -R www-data:www-data ./repo

Так как мы используем специальный ключ для доступа к репозитории, то мы явно должны сообщить команде git о нём. Однако она не предоставляет соответствующей опции. Данная проблема решается через переменную окружения GIT_SSH_COMMAND. В ней можно указать любые настройки, которые нам нужны.

Подтягивание изменений

Когда мы запушим в репозиторий какие-то изменения, Git-сервер вызовет webhook и передаст информацию о них в виде JSON-объекта. В самом простом варианте, деплой можно запускать при любых изменениях. Но корректнее было бы отслеживать только изменения в нужной ветке, например, master.

Для парсинга JSON в bash-скрипте я буду использовать команду jq.

apt-get install jq

Обновим файл deploy.sh:

#!/bin/sh

echo "Content-type:text/plain\n"

if
  [ -n "$HTTP_X_HOOK_UUID" ] &&
  [ -n "$HTTP_X_REQUEST_UUID" ] &&
  [ "$HTTP_X_EVENT_KEY" = "repo:push" ] &&
  [ "$HTTP_CONTENT_TYPE" = "application/json" ] &&
  [ "$REQUEST_METHOD" = "POST" ]
then
  MASTER_COMMITS=`jq '[.push.changes[].new | select(.name == "master" and .type == "branch")] | length'`

  if [ "$MASTER_COMMITS" -gt 0 ]
  then
    cd ./repo

    GIT_SSH_COMMAND='ssh -i /var/www/deploy/access-key -o UserKnownHostsFile=/dev/null'
    git fetch --all 2>&1
    git reset --hard origin/master
    ./build.sh

    echo "Build complete"
  else
    echo "Build skipped"
  fi
else
  echo "Wrong request"
fi

Теперь мы ожидаем только POST-запрос с соответствующим заголовками и данными. Если в среди всех коммитов встретились изменения в ветке master, то запускается загрузка этих изменений из репозитория и вызов скрпита build.sh в корне проекта.

Фактические действия, необходимые для сборки, например, загрузка пакетов из npm-репозитория, запуск webpack, копирование артефактов и т.п., удобнее вынести именно в скрипт build.sh. Это вполне логично, так как для разных проектов требуются разные действия. К тому же он будет обновляться вместе с проектом и загружаться автоматически.

Теперь можно настроить webhook в Bitbucket.

Настройка webhook в Bitbucket

Заключение

Данное решение покрывает лишь минимально необходимые требования. Оно не является заменой популярных систем CI/CD. Тем не менее, оно стабильно, прекрасно подходит для экспериментальных проектов и заметно сокращает количество рутинных операций для небольшой команды разработчиков.

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