2018-10-27

О desktop

Помните ярлыки в Windows. Файлики с расширением .lnk. Классическим фейлом было скопировать на дискетку ярлык вместо самого вордовского файла, а потом удивляться, почему это оно не открывается на другом компьютере.
В наших юниксах, оказывается, тоже есть ярлыки. Freedesktop.org определяет формат файла .desktop, который определяет то, что называется Desktop Entry. Это и ярлыки на рабочем столе, и пункты меню. И это работает и в GNOME, и в KDE, и во всех прочих юниксовых DE. Очень удобно.
Зачем мне ярлыки понадобились? Потому что, как оказалось, в KDE нельзя просто так запустить Java приложение, каким-нибудь java -jar app.jar, а потом закрепить (pin) его в каком-нибудь доке, чтобы потом быстро запускать. Иконка-то появляется, но запускает она просто /usr/bin/java, без параметров. И pin, в результате, не работает.
Чтобы pin заработал, нужно запускать правильный .desktop файл. Он может называться java-RedmineTimeTracker.RedmineTimeTracker.desktop и выглядеть как-то так:
[Desktop Entry]
Type=Application
Name=Redmine Time Tracker
Icon=java-RedmineTimeTracker.RedmineTimeTracker
Exec=java -XX:MaxRAM=256m -jar "/home/gelin/opt/RedmineTimeTracker/Redmine Time Tracker - 0.3.6.jar"
StartupWMClass=RedmineTimeTracker.RedmineTimeTracker
Terminal=false
Categories=Development
Обычный текстовый ini-подобный файл.
Type — это тип. "Application" — это значит, что это "ярлык" для приложения. Ещё есть "Link" и "Directory".
Name — это имя. Как оно будет называться в меню или искаться в dashboard.
Icon — это путь до файла с иконкой. В формате PNG или SVG. Или же имя зарегистрированной иконки. Об этом чуть позже.
Exec — это команда, которая запускает приложение. В данном случае — обычный java -jar, но с ограничением по памяти.
StartupWMClass — это то, ради чего мы вообще этот .desktop файл создавали. В X Window, также как и в Windows, у окна есть класс. Строка. И всякие доки определяют уникальную кнопочку для приложения как раз по этому классу. Из-за отсутствия класса и возникает проблема с Java приложениями. Поэтому мы здесь имя класса и указываем явно.
Есть небольшая проблемка, как определить класс окна для Java приложения. В этом нам поможет программка xprop. Запускаем нужное нам приложение. Запускаем xprop в консоли. Тыкаем по интересующему нас окну. Видим в консоли что-то вроде:
$ xprop | grep WM_CLASS
WM_CLASS(STRING) = "RedmineTimeTracker.RedmineTimeTracker", "RedmineTimeTracker.RedmineTimeTracker"
В данном случае класс совпадает с именем главного класса Java приложения. Я не нашёл, является ли это нормой для десктопных Java приложений. В любом случае, просто прописываем, что обнаружили, в StartupWMClass.
Terminal — флаг, нужно ли запускать приложение в терминале. В данном случае — нет.
Categories — список категорий, через точку с запятой. Это — стандартные категории, на которые делится главное меню. В данном случае это приложение связано с разработкой.
Иконка. Иконка — это ресурс, который можно зарегистрировать в системе, назначить ему имя, и использовать это имя в .desktop файле.
Регистрация производится с помощью программки xdg-icon-resource. XDG — это "X Desktop Group", так раньше назывался freedesktop.org. Сама эта программка — даже не программка, а здоровенный шелловый скрипт, внутри которого есть функции с интересными названиями вроде detectDE(), find_gtk_update_icon_cache() или need_kde_icon_path(). Этот скриптик очень много знает про всякие разные desktop environment.
$ xdg-icon-resource install \
--size 128 \
redmine_logo_green.png \
"java-RedmineTimeTracker.RedmineTimeTracker"
Обязательно нужно указать размер иконки. Больше — лучше. Но не все размеры ваша DE сможет съесть. Хорошо работает 128 и 256 пикселей (128x128 и 256x256).
Последний параметр — имя иконки. Оно обязательно должно состоять из двух слов. Первое, до минуса — это вендор. В данном случае это Java приложение, так что пусть будет "java". Второе, после минуса — собственно идентификатор иконки, для данного вендора. Так как нужен уникальный идентификатор, удобно использовать тут имя класса окна.
На самом деле xdg-icon-resource копирует иконку в каталог вроде ~/.local/share/hicolor/128x128/apps/, или другой, специфичный для вашей DE, и делает магию по обновлению кэша иконок DE.
Сам .desktop файл "устанавливается" тоже своей командой xdg-desktop-menu. Это тоже здоровенный шелловый скрипт.
$ xdg-desktop-menu install \
"java-RedmineTimeTracker.RedmineTimeTracker.desktop"
Имя файла тоже должно содержать префикс вендора, то есть "java-" в данном случае. Скрипт копирует ваш .desktop файл куда-нибудь в ~/.local/share/applications/ и магическим образом обновляет список приложений в DE.
Берём xprop, определяем класс окна. Берём иконку и засовываем её как системный ресурс с помощью xdg-icon-resource. Пишем .desktop файл и добавляем его в систему с помощью xdg-desktop-menu. Всё. Теперь мы умеем создавать "ярлыки" для любых приложений и засовать их в доки и меню.
freedesktop.org logo
И тут выходит Chrome 70. Где отломали одну очень полезную фичу, которой я интенсивно пользовался.
Вы знали, что в Хроме можно было сделать: "Open Main menu -> More Tools -> Add to Desktop"? В результате создавался тот самый .desktop файл, который можно закрепить в доке, и который запускал тот самый сайт, который вы таким образом добавили на рабочий стол, в отдельном окне, без табов и адресной строки.
То есть любой сайт можно "запускать" как обычное приложение. С отдельной кнопкой на панели задач. С нормальным переключением между окнами по Alt+Tab. И, так как технически это просто вкладка того же Хрома, только запущенная в отдельном окне, это жрёт заметно меньше памяти, чем Electron приложение того же Slack (потому что в каждом Электроне — своя копия движка браузера). У меня так запущены кучка Слаков, Gmail, DevDocs и прочие. Мне — очень удобно.
Так вот, в Chrome 70 этот пункт меню убрали. А у сохранившихся ярлыков какого-то чёрта поломалось оформление окон. Теперь они отображаются с хромовыми заголовками окон, что категорически не совпадает по стилю и расположению кнопок с окнами моего KDE.
Но мы же теперь умеем делать .desktop файлы. А у Chrome всё еще остался ключик командной строки --app, который делает то, что нам нужно. То есть запускает указанный url в отдельном окне без табов и адресной строки, и с правильным оформлением самого окна.
Так что берём своё счастье в свои руки. Качаем иконку сайта и засовываем её в систему с помощью xdg-icon-resource. Берите только иконку 256x256, а не 192x192, не все размеры DE съест.
Пишем .desktop файл. Например, chrome-devdocs.io.desktop:
[Desktop Entry]
Type=Application
Name=DevDocs API Documentation
Icon=chrome-devdocs.io
Exec=/opt/google/chrome/google-chrome --profile-directory=Default --app=https://devdocs.io
StartupWMClass=devdocs.io
Terminal=false
Categories=Network
В качестве вендора я тут взял "chrome-", по аналогии со старыми ярлыками. В качестве класса окна Хром выставляет доменное имя из указанного url. Остальное, думаю, понятно.
Устанавливаем файл с помощью xdg-desktop-menu. И наслаждаемся новым удобным приложением, которое на самом деле сайт.
P.S. Я это дело даже немного автоматизировал.

2018-10-14

Об ssh

Об эс-эс-эйч. Которая SSH. Которая Secure SHell. Не все, оказывается, толком представляют, что это такое. И уж тем более не подозревают обо всей мощи SSH.
Впервые я узнал об этой аббревиатуре, "SSH", из журнала "Byte" из 1990-х. В рамках курса английского в универе мы переводили эти исторические статьи. И вот в одной из них встретилась фраза вроде "he sshed to the remote server". Да, "to ssh" — это ещё и глагол.
SSH
SSH появился в 1995 усилиями ещё одного талантливого финна. Его звали Tatu Ylönen.
SSH — это сетевой протокол, работающий поверх TCP. Соответственно, у нас есть сервер, демон sshd, который слушает порт 22, и клиент ssh. У 22 порта есть своя история. Интернет тогда был маленький, и застолбить свой красивый привилегированный порт тогда было довольно просто.
Первая буква "S" в "SSH" действительно означает "Security". SSH возник именно как замена rlogin, rsh и telnet. Все они тоже позволяли подключиться к удалённому серверу и выполнять там команды, но весь трафик там передавался открытым текстом. А SSH — шифрует (и сжимает).
SSH возник примерно в одно время с первыми вариантами SSL (позднее известный как TLS). Поэтому они используют несколько разные подходы. Как минимум, SSH — это не SSL/TLS. А SSL/TLS — это универсальный способ добавить "S" в кучу других протоколов поверх TCP. Самый известный вариант: HTTPS.
В SSL/TLS публичный ключ сервера (и, опциально, клиента) представлен в виде сертификата, подписанного третьей доверенной стороной. И клиент доверяет серверу, если он доверяет этой третьей стороне. В SSH клиент доверяет каждому серверу индивидуально. При первом подключении пользователю явно задаётся вопрос: а доверяем ли мы этому хосту?
$ ssh [email protected]
The authenticity of host 'host.example.net (1.2.3.4)' can't be established.
ECDSA key fingerprint is SHA256:Xocqb7ZPtfsEmXnTCUOeRaCRMfzH1nbTxDVKp0YJBCA.
Are you sure you want to continue connecting (yes/no)?
Если ответить yes, то отпечаток ключа хоста сохранится в ~/.ssh/known_hosts. И при повторном подключении, если ключ сервера не изменился, вопросов уже не будет. А если изменился, то подключения не будет, потому что это уже другой сервер, если у него другой ключ. Простая и эффективная схема доверия.
Если по данному адресу действительно расположился новый сервер, с новым ключом, вам придётся забыть предыдущий ключ. Такой вот командой:
$ ssh-keygen -R "host.example.net"
# Host host.example.net found: line 497
/home/gelin/.ssh/known_hosts updated.
Original contents retained as /home/gelin/.ssh/known_hosts.old
В SSL/TLS клиент, чаще всего, анонимен. Хотя и может предъявить свой сертификат и ключ. В SSH — никакой анонимности. Клиент обязан сказать, от имени какого пользователя (локального пользователя данного сервера) он собирается действовать, предъявить ключ, а там, возможно, ещё и ввести пароль этого самого пользователя.
В простейшем случае мы подключаемся от root и вводим пароль root.
$ ssh [email protected]
[email protected]'s password:
Конечно, не надо так. Подключаться прямо к root, конечно, удобнее. Но подход Ubuntu, когда у root вообще нет пароля, и им вообще нельзя войти в систему, немного безопаснее. Сначала надо подключиться/войти под каким-нибудь "ubuntu". А потом всякие гадости можно делать через sudo. При этом понадобится ввести пароль этого самого "ubuntu".
Ну и, конечно же, не надо подключаться через SSH по паролю. Это ещё хуже, чем прямой root доступ. Потому что пароль можно подобрать. А 4096-битный RSA ключ, ну, почти нельзя. SSH предоставляет несколько методов аутентификации. Кроме "password" есть ещё весьма удобный и вполне безопасный "publickey".
У клиента всегда есть ключ. Без ключа нельзя подключиться. Публичный ключ клиента можно увидеть в файле ~/.ssh/id_rsa.pub.
$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcwgyvek9Oh5wUNbNUTiEtz1jeRn6WnF1mw3duhPC7B3N9HAvCVtYesu5xLy6wylcKYUdKPLo8Q6hEh8kTcHwL21xPQB4/Uq/PjLUvt+MS5mUfYKwWL/M9h37nztA/scK6ItYWMxP6hsX1/zhOkcw1VsLD0+tHRYVqmAm+qO2VQxJ4Gc0dJWeHIGPq1gLLLlJx1QJHUMbHFewtH8z18ood3w/Q07QIKy2kMxFTK/y6Kv1Ij0rxO1KnnzulJHOiNIffeec7nvjcwe0nLYW7y28sT9cxCBIxNu2Dzir8pqgM1TB+mAw/nsYft9CGYBWc+AaodERHXPIbm2wwUUFWnEAN [email protected]
Файл не обязательно будет называться id_rsa.pub. Например, сейчас модно переходить на криптографию на эллиптических кривых. Тогда файлик будет называться id_ed25519.pub. Главное, смотрите файлики, чьё имя заканчивается на .pub. Это — публичные ключи. А без .pub — это приватные ключи. Берегите приватные ключи как зеницу ока и никому их не давайте даже под страхом смерти. И задавайте хороший пароль при генерации ключей.
$ ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519 -C "[email protected]"
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/gelin/.ssh/id_ed25519.
Your public key has been saved in /home/gelin/.ssh/id_ed25519.pub.
The key fingerprint is:
SHA256:FOjHEnorwa9eAb4Jhnqeh4rZ5zzsG/S8LzkBBm+DVCc [email protected]
The key's randomart image is:
+--[ED25519 256]--+
|   .E ...        |
|  o  oo  .       |
| . =.o o.        |
| ...X.+.o        |
|. ooo*.+S        |
|.. o.=+.         |
|. .o+o+o         |
|.=.o*o+.         |
|+.+**o.+.        |
+----[SHA256]-----+
А публичные ключи можно раздавать налево и направо. Что я тут и делаю :) Всё равно, всё, что вы сможете с ними сделать, это дать мне доступ к вашим серверам :)
Чтобы аутентификация по публичному ключу заработала, ключ клиента (публичный!) нужно добавить в файл ~/.ssh/authorized_keys на сервере. В домашнем каталоге того пользователя, под кем мы подключаемся. Соответственно, домашний каталог у этого пользователя должен быть.
Добавить ключ в authorized_keys можно либо руками. Просто дописываете содержимое id_rsa.pub (одну строчку) в конец этого файла. Не забудьте пустую строку в конце. Либо вам может помочь команда ssh-copy-id.
$ ssh-copy-id -i ~/.ssh/id_ed25519 [email protected]
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/gelin/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh '[email protected]'"
and check to make sure that only the key(s) you wanted were added.
Но мы же хотели ещё запретить подключение с паролем и прямой root доступ. Надо настроить sshd демона. Обычно его конфигурация лежит в файлике /etc/ssh/sshd_config. Нас интересуют несколько параметров.
PermitRootLogin no
PubkeyAuthentication yes    # не путать с RSAAuthentication
PasswordAuthentication no
После изменений в sshd_config делайте service ssh reload, не restart. И тренируйтесь в переподключении через SSH в другой консоли. Запускайте ssh с ключиком -v, чтобы видеть, как клиент с сервером договариваются. А эту сессию, где вы конфиг правили, не закрывайте. Тогда ваше подключение останется живым, даже если вы отломали SSH доступ. Крайне неприятно терять связь с удалённым сервером, знаете ли.
У клиента тоже есть конфиг. В файле ~/.ssh/config. Туда очень удобно прописывать имена юзеров и нестандартные порты подключения к конкретным серверам. Или даже какие-то особые SSH ключи, раз уж их вам выдали.
Host host.example.net
User root
Port 2222
IdentityFile ~/.ssh/example_net_key
Как вы знаете, Git и Mercurial тоже работают через SSH. Вот такой хитрой конфигурацией можно ходить на какой-нибудь Bitbucket из-под разных аккаунтов (и с разными ключами):
Host bitbucket.org
# обычный bitbucket, который использует ключ id_rsa

Host work.bitbucket.org
# отдельный bitbucket с отдельным ключом
HostName bitbucket.org
# но на самом деле мы всё равно подключаемся к bitbucket.org
User git
# юзером git
IdentityFile ~/.ssh/[email protected]
# и используем этот специальный ключ
IdentitiesOnly yes
# и только его
~/.ssh/config разбит на секции по директивам Host. Host — это то, что вы пишите в командной строке.
$ git clone [email protected]:user/repo.git
Это не обязательно должен быть реально существующий хост, настоящий адрес указывается в HostName. Все последующие директивы относятся в предыдущему (последнему) Host.
По умолчанию SSH запускает shell пользователя на сервере. Но на самом деле SSH может выполнить совершенно любую команду на удалённом хосте. И stdin/stdout на клиенте станут stdin/stdout этой удалённой команды. В своё время мы так собирали статистику с роутеров, запуская удалённый скрипт на Perl, который плевался XML.
$ ssh [email protected] whoami
root
Через SSH можно передавать файлы. Это значительно безопаснее, чем какой-нибудь FTP, куда шифрование толком так и не прикрутили. И это даже может быть быстрее.
Простой способ: scp. Secure CoPy. Можно скопировать локальный файл на удалённый сервер, или наоборот.
$ scp ssh.md [email protected]:/tmp/
ssh.md                  100%   13KB  74.6KB/s   00:00
Параметр, указывающий файл или каталог на удалённом хосте, состоит из двух частей. До двоеточия: имя пользователя и хост, так же как в обычном SSH клиенте. После двоеточия: абсолютный или относительный (относительно домашнего каталога) путь на сервере.
Но scp — туп. Он всегда копирует файл целиком. А вот если файл большой, или файлов много, нужно использовать rsync.
rsync — умный. Он умеет делить файлы на куски и проверять каждый кусок на идентичность, и докачивать только недостающие или отличающиеся куски. Он может сжимать файлы перед отправкой. Он может, как оно следует из названия, синхронизировать целые каталоги. С его помощью легко можно почти полностью склонировать почти любую Unix систему. Нужно только SSH на удалённый сервер, и установленный там rsync.
$ rsync -avz --progress . [email protected]:/tmp/test
sending incremental file list
created directory /tmp/test
./
00 URIs.png
        100,554 100%    4.31MB/s    0:00:00 (xfr#1, to-chk=2/4)
01 URI & URL
         38,506 100%    1.84MB/s    0:00:00 (xfr#2, to-chk=1/4)
url.md
          8,761 100%  371.99kB/s    0:00:00 (xfr#3, to-chk=0/4)

sent 139,314 bytes  received 108 bytes  39,834.86 bytes/sec
total size is 147,821  speedup is 1.06
Будьте осторожнее с параметрами rsync. Он по-разному работает с файлами и каталогами. Если вам нужно синхронизировать содержимое двух уже существующих каталогов, и не нужно создавать новые подкаталоги там, куда вы синхронизируете, оба параметра-каталога должны заканчиваться на /.
Кстати, rsync прекрасно работает и без SSH, на localhost.
Усложняем. Допустим, у нас есть какой-то Continuous Integration, который собирает некий статический сайт. И мы хотим, чтобы по окончанию сборки обновлённые файлы сами заливались бы на сервер. rsync для этого идеально подходит. Но мы не настолько доверяем нашему CI, чтобы давать ему root доступ. Заводим отдельного пользователя для синхронизации. Но хочется этого пользователя ещё сильнее ограничить, чтобы он мог делать только rsync только конкретного каталога. (Ну а в моём случае просто не было возможности завести ещё одного пользователя, ибо это shared hosting.) И в SSH это можно.
В файле authorized_keys перед ключом можно указать ещё и команду, которая будет разрешена к выполнению клиенту, предъявившему этот ключ. И больше никакая другая. И ещё кучу опций. А ещё есть специальный скрипт на Perl, под названием rrsync, который ограничивает доступ rsync к одному каталогу.
command="$HOME/bin/rrsync ~/limited/path/",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa ...
Как видите, SSH много чего может. И это много чего тут явно запрещается.
Примерно так, через хитрые command для каждого ключа, и работают публичные Git репозитории вроде GitHub или GitLab. Хотите поднять что-то подобное (ограничиваясь только Git, безо всяких трекеров задач) у себя локально, смотрите в сторону gitolite.
У SSH есть агент. Он может хранить в памяти расшифрованные ключи, чтобы вам не приходилось вводить пароль для ключа при каждом подключении. А ещё он может пробрасывать загруженные на клиенте ключи так, чтобы они были доступны на сервере (это и называется agent forwarding). Это очень удобно, если вам нужно с сервера делать SSH на следующий сервер. Так бывает, да.
Добавить ключ в память агента можно так:
$ ssh-add ~/.ssh/special_key.pem
Identity added: /home/gelin/.ssh/special_key.pem (/home/gelin/.ssh/special_key.pem)
Через SSH можно пробросить порты (port forwarding). То есть сделать так, чтобы, например, подключения на локальный TCP порт 8080 перенаправлялись на порт 80 некоторого процесса на сервере. При этом прямого подключения к 80 порту на сервере у вас нет. Но у вас есть SSH доступ на сервер.
$ ssh -L 8080:localhost:80 -N -f [email protected]
Это называется local forwarding. В данном случае слушается локальный порт 8080 клиента. На сервере происходит подключение к localhost:80 (можно перенаправить подключение и к другому хосту, доступному с сервера). SSH будет только пробрасывать порт, но не будет запускать shell на сервере: -N. SSH клиент уйдёт работать в фон: -f.
Local Forwarding
Можно и наоборот. Открыть на сервере порт для подключения, и все подключения на этот порт будут проброшены на другой хост и порт, доступные на клиенте. Это называется remote forwarding. Синтаксис такой же, но нужен ключик -R.
Remote Forwarding
SSH может пробрасывать и сессии X11. Если помните, протокол X, который всё ещё доминирует на наших юниксах как средство отрисовки гуя, обладает сетевой прозрачностью. Собственно, в Linux когда-то TCP/IP запихнули в спешке, лишь бы графические окошки рисовались. А значит, имея запущенный X server на SSH клиенте, вполне можно запустить гуёвое приложение где-нибудь на мощном сервере так, чтобы рисовало окошки оно на клиенте. И всё, что вам для этого нужно, это xlib на сервере и SSH доступ туда. (Запутались, кто клиент, а кто сервер? Правильно, в иксах это нормально.)
SSH может не просто пробрасывать порты на конкретные хосты, но и выступать в роли SOCKS сервера.
$ ssh -D 1080 -f -C -q -N [email protected]
Здесь у вас на локальном порту 1080 клиента появится SOCKS сервер, который может подключаться к любому хосту, доступному с сервера. Все подключения будут проброшены через SSH. Данные будут сжиматься: -C.
Конечно, полноценные VPN туннели работают быстрее и лучше. Но и SSH, как видите, вполне достаточно.
SOCKS Proxy
Ну и, конечно же, Ansible работает через SSH. Он загружает и выполняет свои скрипты на Python через SSH.
Делайте "ssh". Это очень мощная и полезная штука.

2018-09-29

Об URL

Понадобилось как-то распарсить вот такую строчку. Это первая строка типичного HTTP запроса.
POST /service?user=123 HTTP/2.0
Такие строчки встречаются в логах Elastic Load Balancer (ELB). Я специально засунул туда айдишник пользователя, чтобы отделять запросы одних пользователей от запросов других. Да, Chrome, которым мы в основном пользуемся, ходит в Амазоновый ELB по протоколу HTTP/2.
Разбить строку по пробелам — легко. Но в серединке у нас URL. И мне нужно извлечь из этого URL path и один из параметров запроса. Регулярные выражения для URL я писать не хочу, неблагодарное это дело. Ведь должны же быть стандартные парсеры для URL или URI.
URIs
Раз уж у нас Java/Kotlin, давайте попробуем java.net.URL.
val url = java.net.URL("/service?user=123")
> java.net.MalformedURLException: no protocol: /service?user=123
Ну да. У нас же немножко неполный, прямо говоря, относительный, URL, каким он обычно и бывает в атрибуте href или заголовке HTTP запроса.
Ну давайте попробуем java.net.URI.
val url = java.net.URI("/service?user=123")
url.getPath()
> /service
url.getQuery()
> user=123
Победа? Ещё нет. У java.net.URL и у java.net.URI есть метод getQuery(). Он выделяет query часть URL, но не парсит её. Далее StackOverflow рекомендует снова воспользоваться регулярными выражениями или хотя бы разбить строку по символам "&" и "=".
Но я не хочу писать свой парсер. В любом парсере рано или поздно найдутся ошибки или уязвимости. И лучше, чтобы это был не ваш парсер. Тем более, что для такого популярного случая, как URL, уж точно должно существовать готовое решение. Почему это Java должна быть обделена?
Тем, кто под Android, повезло. android.net.Uri делает то, что нужно. Небольшая сложность возникнет при конструировании этого Uri. Фабричный метод Uri.fromParts(String scheme, String ssp, String fragment) требует явного указания схемы. А дальше у нас есть getQueryParameter(String key).
В мире Spring всё тоже неплохо. Там есть UriComponentsBuilder. С его фабричными методами тоже нужно разобраться, их много. И делает он UriComponents. А там уже есть MultiValueMap getQueryParams(). Даже круче, чем нужно.
Но у меня не Android. И я не хочу тащить Spring. Потому что это Lambda. Чем меньше классов и зависимостей, тем лучше.
Схема. Почему схема нужна явно? Если копнуть, окажется очень интересно.
URI — это не только URL. Есть ещё URN, где (например, "urn:isbn:5170224575") нет никакого пути или какой-либо иерархии. Есть просто имя в определённом пространстве имён. В "mailto:[email protected]" есть такие части URL, как имя пользователя и адрес сервера, но больше нет ничего. "tel:+1-816-555-1212" вообще ничего общего с HTTP URL не имеет.
Только URL имеет и имя хоста, и иерархический путь, и query, и fragment. И то лишь схемы "http", "https" и "ftp". Даже у наиболее близкого "file" уже нет имени хоста.
URI & URL
В мире URI — полный бардак. Но хорошая новость в том, что c любым URI (и, соответственно, URL) можно понять, как разобраться, выяснив схему. То есть, прочитав ASCII символы до первого двоеточия. Схема — важна.
В моём случае URL относительный. В нём пропущена схема и доменное имя. Схему нужно указать. И в данном случае всё просто. Это либо "http", либо "https", без разницы.
Проблема разбора URI/URL действительно является проблемой. Поэтому возникают библиотечки со странными именами вроде galimatias. Либо HTTP библиотеки обзаводятся своими реализациями методов работы с URL. Мало кого удовлетворяет стандартная библиотека Java.
Я остановился на прекрасной библиотеке OkHttp. Это — мощный (но лёгкий) HTTP клиент. Который, кстати, стал дефолтной подкапотной реализацией HTTP в последних версиях Android.
Там есть свой HttpUrl, который может почти всё, что нужно. Но он работает только с "http" и "https" схемами URL. Потому что он заточен на такие URL, и умеет справляться с различными кодировками не-ASCII символов в доменной части и в пути. Ну и, конечно же, он корректно парсит query. Почитайте JavaDoc, там подробно расписано, почему так, и чем ещё плох java.net.URL (спойлер: метод equals() там ходит в сеть).
Так что делать с относительным URL в моём случае? Считать его относительным. И делать resolve() от некоторого базового URL.
val baseUrl = HttpUrl.get("https://example.com")  // пофиг какой сервер
val url = baseUrl.resolve("/service?user=123")
val pathSegments = url?.pathSegments()
val user = url?.queryParameter("user")
Это работает точно так же, как разрешение href ссылки в HTML. Если будет относительный URL вроде "/service?user=123", то это отрезолвится в "https://example.com/service?user=123". Если будет абсолютный URL вроде "https://example.net/service?user=123", то это отрезолвится в новый абсолютный URL "https://example.net/service?user=123". То, что нужно.

2018-09-15

О почте

Бывает так.
Где-то на выделенном сервере работает тщательно настроенный Postfix. Через него ваше приложение, запущенное на этом же сервере, отправляет письма.
И SPF запись настроена. Она говорит, что с IP адреса нашего сервера действительно разрешается отправлять письма от нашего домена.
$ dig +noall +question +answer txt example.ru
;example.ru.              IN      TXT
example.ru.       28800   IN      TXT     "v=spf1 +a +mx include:_spf.yandex.net ~all"
И DKIM настроен. Письма подписываются, а ключ для проверки подписи тоже лежит в DNS.
$ dig +noall +question +answer txt mail._domainkey.example.ru
;mail._domainkey.example.ru. IN   TXT
mail._domainkey.example.ru. 28800 IN TXT  "v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDTFcJLY5SlBNwtTqCHx5VHZZBnap4Z75T9Jb6g/JpNcwu3sMdBPmt8zQsxNflBXmovlBzowa4rAwu0MCk1HgZHt+5Ohm+qRscXTrP19XAcV+DA6ZVXekRNwoY7K+jwn3VJaUpEn3BK171M3gUPQ1G4sRDwh0A+S5ZEetH7V3SjmwIDAQAB;"
И даже все предыдущие письма, которые мы сами себе отправляли в Gmail, содержат заголовки, подтверждающие, что все проверки проходят успешно.
Authentication-Results: mx.google.com;
 dkim=pass [email protected];
 spf=pass (google.com: domain of [email protected] designates 1.2.3.4 as permitted sender)
 [email protected]
Но вот в один прекрасный день мы видим в логах Postfix вот такое:
421-4.7.0 Our system has detected an unusual rate of unsolicited mail originating from your IP address. To protect our users from spam, mail sent from your IP address has been temporarily rate limited. Please visit https://support.google.com/mail/?p=UnsolicitedRateLimitError to review our Bulk Email Senders Guidelines. gsmtp (in reply to end of DATA command)
Google отказывается (временно) принимать письма, адресованные пользователям Gmail, когда они отправляются с нашего сервера.
Ну да, мы слали подтверждения регистрации. Да, это очень похожие письма. Да, это были десятки похожих писем. Но отправлялись они не все скопом, а по мере регистрации пользователей.
Что мы можем сделать ещё, чтобы удовлетворить Google? Давайте внимательно почитаем, что нам предлагают по указанной ссылке.
Gmail
Читаем внимательно, обращая внимание на каждый пункт.
Используйте Postmaster Tools.
Теоретически, Postmaster Tools — это такая веб консолька, где вы видите статистику получения почты Гуглом из вашего домена, и можете увидеть истинную причину ошибки. На практике я им не пользовался. Потому что там было пусто. Видимо, сильно мало писем мы рассылаем, чтобы там была хоть какая-то статистика.
Всегда отправляйте письма определенной категории с одного адреса.
Ну как бы у нас одна категория: письма подтверждения регистрации. И отправляются с одного адреса.
Не смешивайте разнотипный контент в одном сообщении.
Не такие уж и большие у нас письма, чтобы там появилось более одного типа контента.
Отправляйте сообщения с одного IP-адреса.
Сервер у нас один, так что IP адрес тоже один.
Убедитесь, что для ваших IP-адресов существуют действительные обратные записи DNS, указывающие на ваш домен.
Уже теплее.
DNS, как вы знаете, предназначен для преобразования символических (доменных) имён, типа "gmail.com", в IP адреса, типа "216.58.207.37" или "2a00:1450:4001:821::2005".
$ dig +noall +question +answer a gmail.com
;gmail.com.                     IN      A
gmail.com.              260     IN      A       216.58.207.37
$ dig +noall +question +answer aaaa gmail.com
;gmail.com.                     IN      AAAA
gmail.com.              55      IN      AAAA    2a00:1450:4001:821::2005
Но существует и обратное преобразование. IP адресу может быть сопоставлено некоторое каноничное доменное имя.
$ dig +noall +question +answer -x 216.58.207.37
;37.207.58.216.in-addr.arpa.    IN      PTR
37.207.58.216.in-addr.arpa. 86378 IN    PTR     fra16s24-in-f5.1e100.net.
$ dig +noall +question +answer -x 2a00:1450:4001:821::2005
;5.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.1.2.8.0.1.0.0.4.0.5.4.1.0.0.a.2.ip6.arpa. IN PTR
5.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.1.2.8.0.1.0.0.4.0.5.4.1.0.0.a.2.ip6.arpa. 86400 IN PTR fra16s20-in-x05.1e100.net.
Как видите, это DNS записи типа PTR в поддоменах in-addr.arpa. и ip6.arpa.. Из IP адреса там формируется доменное имя задом напёред.
Если что, домен 1e100.net, принадлежит Гуглу.
От нас требуется, чтобы обратная зона IP адреса нашего сервера, во-первых, существовала, и во-вторых, соответствовала нашему домену, с которого мы отправляем почту.
Обратная зона неразрывно связана с IP адресом. А значит, изменить её можно там, где вам выдали IP адрес. То есть, у хостера. Ищите где-нибудь в панельке управления сервером, где поменять обратную зону.
Reverse DNS settings
Раньше бывали почтовые сервера, которые не принимали письма с IP адресов, если обратная зона не соответствовала домену отправителя. Подобную проверку и сейчас можно включить в Postfix. Google, вроде, не настолько жесток. Но вполне очевидно, что правильная обратная зона улучшает репутацию сервера. Ведь наличие обратной зоны говорит о том, что это не какой-то домашний сервер с динамическим IP адресом, а честный оплаченный хост у приличного хостера.
Reverse DNS Check
Используйте во всех письмах массовой рассылки один и тот же адрес в поле "От:".
Ну мы так и делаем.
Подписывайте сообщения ключом DKIM. Gmail принимает ключи длиной не менее 1024 бит.
Это мы тщательно настроили в прошлый раз.
Опубликуйте запись SPF.
Есть такая запись.
Опубликуйте политику DMARC.
А вот это мы поленились сделать. Кажется, пришла пора каждому приличному почтовому домену, помимо SPF и DKIM, иметь ещё и DMARC.
В простейшем случае хватит и такого:
$ dig +noall +question +answer txt _dmarc.example.ru
;_dmarc.example.ru.       IN      TXT
_dmarc.example.ru. 28800  IN      TXT     "v=DMARC1;p=none"
Мол, про DMARC мы знаем, вот вам эта грешная политика, но ничего специального не хотим, отвалите. Главное, чтобы нужная TXT запись была.
Если вы уверены, что SPF и DKIM работают правильно, и хотите получать хоть какие-то сведения о том, кто шлёт почту от вашего домена, имеет смысл усложнить до "v=DMARC1;p=reject;pct=100;rua=mailto:[email protected]". Тут, во-первых, письма с неправильным SPF или DKIM будут отбрасываться, а во-вторых, сводная статистика по письмам с вашего домена будет присылаться на указанный емейл. Там приходят зазипованные XML, которые имеет смысл автоматически парсить и где-то диаграммки рисовать. В принципе, это всё уже автоматизировано.
DKIM, SPF, DMARC
У IP-адреса отправителя должна быть запись PTR для выполнения обратного запроса DNS. Необходимо, чтобы он совпадал с IP-адресом, который получен путем прямого преобразования доменного имени, указанного в записи PTR.
Ага. Значит, нужно пойти чуть глубже, чтобы доменное имя обратной зоны IP адреса нашего сервера резолвилось в тот же IP адрес.
$ dig +noall +question +answer a gmail.com
;gmail.com.                     IN      A
gmail.com.              147     IN      A       216.58.207.69
$ dig +noall +question +answer -x 216.58.207.69
;69.207.58.216.in-addr.arpa.    IN      PTR
69.207.58.216.in-addr.arpa. 82607 IN    PTR     fra16s25-in-f5.1e100.net.
$ dig +noall +question +answer fra16s25-in-f5.1e100.net.
;fra16s25-in-f5.1e100.net.      IN      A
fra16s25-in-f5.1e100.net. 86400 IN      A       216.58.207.69
Это требование слабее предыдущего упоминания обратной зоны. Приличные провайдеры заводят домен и обратную зону даже для динамических IP адресов.
$ dig +noall +question +answer -x 188.232.179.78
;78.179.232.188.in-addr.arpa.   IN      PTR
78.179.232.188.in-addr.arpa. 2572 IN    PTR     dynamicip-188-232-179-78.pppoe.omsk.ertelecom.ru.
$ dig +noall +question +answer dynamicip-188-232-179-78.pppoe.omsk.ertelecom.ru
;dynamicip-188-232-179-78.pppoe.omsk.ertelecom.ru. IN A
Упс. Или нет.
Reverse DNS
Домен отправителя должен пройти проверку SPF или DKIM.
Да. Уже да.
Чтобы получатель мог быстро отказаться от подписки, не покидая Gmail, мы настоятельно рекомендуем добавить заголовок List-Unsubscribe.
Упоминаются два RFC. RFC 2369 описывает List-* заголовки писем, в частности, заголовок List-Unsubscribe. RFC 8058 описывает заголовок List-Unsubscribe-Post.
Теперь понятно, как работают эти ссылки "отписаться" в Gmail. Если вы делаете рассылки, то включите эти заголовки в ваши письма, и Google будет лучше их воспринимать. А у пользователей будет возможность быстро отписаться от рассылки. Вам будет приходить HTTP POST запрос.
List-Unsubscribe: <https://example.com/unsubscribe.html/opaque123456789>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Но вообще-то у нас не рассылки, а уведомления.
List-Unsubsribe
С помощью поля заголовка "Precedence: bulk" обозначена массовая рассылка.
Precedence — это нестандартный заголовок. Он пару раз упоминался в довольно старых RFC как выходящий из употребления, и в основном использовался для предотвращения зацикливания автоматических ответов в списках рассылки. А вот эта вот рекомендация использовать Precedence: bulk — уникальная для современного Gmail.
Чёрт его знает, как Gmail обрабатывает этот заголовок. Но, вероятно, если вы рассылаете много похожих писем, стоит этот заголовок выставить. Не жалко.
No Spam
Итак. Чтобы уменьшить вероятность проблем с Gmail (и другими получателями), нужно:
  • Иметь валидную SPF запись в домене.
  • Настроить и использовать DKIM.
  • Иметь хоть какую-нибудь политику DMARC для домена.
  • Определить обратную зону для IP вашего сервера. Убедиться, что домен обратной зоны снова резолвится в IP сервера.
  • Если вы действительно делаете рассылку, добавить заголовки List-Unsubscribe и List-Unsubscribe-Post. Дайте пользователю возможность быстро отписаться.
  • Если вы рассылаете много похожих писем, добавить заголовок Precedence: bulk.
  • Следить за заголовками From, To, Return-Path, Reply-To. Они должны отражать действительность.
  • Следить за телом сообщений, не забывайте включать текстовую версию, помимо HTML.
  • Быть честными с пользователями, пишите в письмах только то, что нужно, не надо рекламы.
Успехов.

2018-09-02

О Сочи

Сочи — это не город. Это — сборище санаториев, профилакториев, лечебных учереждений, дач, бывших дач, экспроприированных и превращённых в парки, посёлков, отелей, посёлков из отелей, горных черкесских аулов, пасек и виноградников, а также немного заказников, зажатые между Северо-Западным Кавказом и Чёрным морем узкой полоской от границы с Абхазией и на 120 километров на северо-запад.
Самая юго-восточная часть всего этого называется Адлер. Именно тут находится аэропорт. Именно тут построены те самые стадионы, которые вы видели по телевизору на трансляции Зимней Олимпиады 2014. Олимпиада принесла сюда ровные дороги, многоуровневые развязки и два модерновых железнодорожных вокзала. Плюс навороченная горнолыжная инфраструктура где-то в горах, где я не был.
Говорят, после Олимпиады Сочи стал круглогодичным курортом. Летом сюда едут на море. Зимой сюда едут в горы.
Далее на северо-запад у нас находятся: Хоста (Хостинский район), собственно Сочи (Центральный район)... Ну и считается, что Большой Сочи простирается куда-то до посёлка Лазаревка (Лазаревский район) и немного дальше.
Карта Большого Сочи
Из Адлера в Сочи идёт прямая автомобильная дорога: улица Ленина, тихо и незаметно переходящая в Курортный проспект. Это — единственная прямая дорога в Сочи. Далее на север — только горные серпантины. Вправо-вверх, влево-вверх, вправо-вверх, влево-вверх,... перевал, вправо-вниз, влево-вниз...
Автомобильное движение тут адовое. Я очень обрадовался, что оказался тут без авто. В движении на горных дорогах есть куча неявных нюансов. Тормозить двигателем на спуске. Пропускать фуры и автобусы на поворотах. Они не влезают по ширине и в повороте им нужны обе полосы, включая встречную. И их пропускают, и встречные, и попутные. Никаких обгонов. Зато автобус периодически пропускает скопившиеся за ним легковушки там, где дорога слегка расширяется.
Вдоль всего побережья проходит ещё и железная дорога. В единственном ровном месте, где её можно проложить. То есть по самой границе пляжа. Поэтому шутка Михаила Задорнова про железную дорогу на пляже — вовсе не шутка. Отдыхающим в большинстве санаториев придётся перебираться через автомобильную и железную дороги по подземным переходам, чтобы попасть на пляж. Единственное место, где железная дорога отходит от моря вглубь — это Адлер, тот самый его район, где стоят олимпийские стадионы, и Имеретинский район чуть южнее. Поэтому именно туда мы рванули, чтобы к морю.
Улица Ленина на Google Street View
Море есть. Море тёплое. В конце августа вода была +28°C. А пляж — галечный. Песка нет. Есть камни. Круглые, овальные... На них можно лежать. Но ходить лучше только в тапочках.
Эти камни тут везде. Ими прижимают листочки с рекламами туристических туров в киосках на каждом углу. Ими придавливают основания зонтиков от солнца, чтобы устойчивей стояли. Тащить с черноморского побережья красивые круглые камушки — бессмысленно. Ибо их тут — килотонны.
Камни берутся из гор. Тут нет этой чёрной мясистой почвы. Тут нет грязи. Тут, если голая земля — то это камни. Понятно, как тут растут сосны. Не очень понятно, как тут растёт всё остальное, включая настоящие пальмы и настоящий бамбук. Зато совсем-совсем нет пыли, которая в Сибири через два дня появляется на подоконниках при открытом окне и скапливается на балконах.
Земля в Сочи
По деньгам слетать в Сочи выйдет столько же, что слетать куда-нибудь в Европу. С пересадкой в Москве, естественно.
С жильём вариантов чудовищно много. Но меньшинство из них представлено на booking.com. Только ленивый не сдаёт квартиры или комнаты для туристов. В Адлере целые кварталы застроены двухэтажными домиками для туристов. Или пятиэтажными небольшими отелями с рестораном на первом этаже. Цены на жильё могут различаться в десятки раз, ведь тут есть и несколько штук Radisson. Но, в общем-то, гостиница уровня европейского Ibis, и стоить будет соответствующе.
Нам с жильём не повезло лишь по одному пункту. На первом этаже отеля оказался ресторан. И не просто ресторан, а ресторан со столиками на улице, где вечерами громко поют. А в соседнем отеле — тоже ресторан, где вечерами тоже поют. И они друг с другом соревнуются, кто поёт громче. И окна номера как раз выходят на них обоих (и на море, эх). Заснуть можно только плотно закрыв все окна. Хорошо, что эти ребята стабильно затыкались где-то за полчаса до полуночи.
Еда — очень дешёвая. Я ожидал, что будет дороже, как в Москве. Но нет, оказалось так же дёшево, как в Омске. Это если не ходить по барам и ресторанам, а питаться в столовках, которых на побережье южнее Олимпийского парка едва ли не больше, чем баров. За 300-700 рублей вполне можно пообедать втроём (два взрослых + ребёнок). Ну и ясен пень, всюду просто на улице — шашлыки и люля-кебаб. Впрочем, индейки тут нет, не выращивают.
Развлечения стоят по-разному. Но обычно в два-три раза дешевле, чем что-нибудь аналогичное в Европе.
На пляж вход свободный и бесплатный. За лежак, или зонтик, или качели-лежанки — почасовая оплата. За разные водные покатушки — плата отдельно.
Солнце встаёт из-за гор в шесть утра. Солнце садится в море в семь вечера. Пешеходная набережная с велодорожкой. Продают всякую детскую пластиковую фигню. Катят на велосипедах, самокатах, электросамокатах, трехколёсных велосипедах с электроприводом, рикшах, чёрт знает на чём. Художники рисуют. Гитаристы поют. Какие-то ребята каждый вечер играли дуэтом на гитаре и скрипке. Молодцы!
Вид с балкона отеля
Сходили разок с ребёнком в Сочи Парк. Впервые попал в парк развлечений, где плата за вход. Нехилая плата: больше полутора тысяч. Но не пожалел. Потом можно развлекаться целый день. На всех аттракционах. Без дополнительной платы. Там шикарные американские горки. С не менее шикарной длиннющей очередью на вход, но организованной а-ля-аэропорт накопителями зигзагом. А на цепочную карусель меня не пустили, потому что там ограничение на массу в 75 кг на посетителя.
Но аттракционы там — не самое интересное. Есть шоу. Научные, развлекательные, цирковые. Надо только заранее узнать, когда и где они будут проходить. Есть площадки под открытым небом. Просто лазалки. Прыгалки с музыкальными инструментами. Мини-лабиринты с кривыми зеркалами. Большая площадка с водой, насосами, брызгалками, водяными колёсами и прочей прелестью. На этих площадках, даже безо всяких аттракционов, ребёнок может спокойно провести полдня.
Позднее выяснилось, что в Москве, в Парке Горького, тоже есть подобные площадки, даже круче. С совершенно свободным доступом, и, соответственно, толпами орущих ребятишек. Москвичи и гости столицы, или просто, у кого есть полдня до вечернего самолёта, имейте в виду.
Лазалки в Сочи Парке
Разок выбрались в центр Сочи. Познакомились с общественным транспортом. Всё плохо.
Есть электрички. Но станции идут километров через пять. Это вам не метро. До ближайшего вокзала может понадобиться пройти несколько километров. Далековато.
Есть автобусы. Цена — нормальная. По району — 22 рубля. Между районами (из Адлера в Сочи) — в четыре раза дороже. Платить водителю на входе через переднюю дверь. Кондиционеры — есть. Расписание — тоже есть. Но оно не работает. Двагис утверждает то же, что и таблички на остановке, что автобус тут ходит три-четыре раза в час. На самом деле: один автобус в час-полтора. Поэтому — набит битком, и надо стоять. На остановках в центре встречаются табло, показывающие время прибытия ближайших автобусов, но они работают нестабильно, автобусы умудряются оттуда исчезать или появляться мимо данных на табло.
Есть море. Но по нему ходят только редкие прогулочные яхты, да многочисленные катера, катающие отдыхающих на «бананах» и парашютах. Морского транспортного сообщения — нет. Единственный морской порт — в центре Сочи.
Самый-самый центр Сочи
Доехали на автобусе до парка «Ривьера». Мы хотели туда сходить в дельфинарий. Даже купили билеты онлайн. Но в тот день поехать не смогли. Спасибо sochi.kassir.ru. Хоть их заявление на возврат билета, в формате doc, — совершенно дурацкое, но моё письмо, присланное за три часа до представления, они рассмотрели, и деньги вернули.
Попали в «Ривьеру» на другой день. Оказалось, что «сад бабочек» — это лишь одна комната в дельфинарии. С кучей дохлых бабочек на булавках и парочкой полудохлых ещё летающих. И весь остальной «Ривьера» оставил какое-то впечатление старого советского никому не нужного парка развлечений.
Прогулялись по Курортному проспекту. Сочи — не город. Даже когда идёшь по центральной улице, нет ощущения, что ты в городе. Слева — горы. Справа — проглядывает море. Вокруг — сплошная зелень. И где-то в этой зелени вылезают здания этих самых санаториев или торчит шпиль морского вокзала. Какие-то отдельные строения в горном лесу. Где город? Где гущи домов? Где простор (помимо морского)? Или это я, прирождённый житель равнин, ничего не понимаю?
Посетили Сочинский дендрарий. Милое местечко. Крутая дача конца XIX века, сохранившаяся до наших дней. Забирайтесь наверх на канатной дороге. Спускайтесь зигзагами, разглядывая растения и животных. Там есть страусы и лебеди. А в нижней части парка живёт куча мелких петухов, которые кукарекают, выпрашивая вкусняшки.
Снова самый центр Сочи
По всему Сочи продают экскурсии. Сотни киосков. Десятки экскурсий. Напоминает Израиль. Экскурсионные автобусы собирают туристов пару часов, объезжая полсочи. А потом куда-то везут. К вечеру развозят обратно.
Мы поехали на 33 водопада. Это — ущелье, где действительно имеется куча водопадов. Небольших. Где-то 17 из них доступны туристам по проложенным дорожкам. Ещё свозили посмотреть на крупнейший на побережье дольмен. Это такие древнючие погребальные сооружения в виде громадного каменного домика с дыркой.
Дорогу к водопадам устроили с мокрым развлечением. Туристов погрузили в старенькие ГАЗ-66 и, кажется, в Урал-375. И повезли прямо по руслу горной реки. В конце лета горные реки пересыхают, и есть только отдельные неглубокие протоки. Вот эти протоки и форсировали вездеходы на скорости явно больше оптимальной. Туристы тряслись и визжали от радости, когда их окатывало брызгами от водной преграды. По мнению моей дочи, это было веселее американских горок.
Вторая наша экскурсия — на гору Ахун, где построена башня, с которой видно весь Большой Сочи. Да, видно. Да, красиво. Виды как в сторону моря, так и в сторону гор — замечательные. И в Агурское ущелье. Водопадов там не было, ибо для водопадов — не сезон, горные реки пересыхают.
На обеих экскурсиях нам устраивали дегустации. Мёда, сыра, вина и чачи. В двух разных местах. Как будто сыр и вино тут не делает только ленивый. Сыр — обычный сыр. Адыгейский, копчёный, как обычно. Мёд — разный и своеобразный. Вино — тоже разное. Чача — это 65-градусный самогон на отходах виноделия. Просто крепкий самогон.
Про вино понарассказывали много интересного. Тут, в Сочи, растёт только Изабелла, остальные сорта винограда привозят, и делают из них вино. На кавказском застолье предпочитают белые вина, потому что от них меньше пьянеешь. А на кавказком застолье нужно выпить много: по стакану на тост. Сухие вина — более натуральные. В сладких принудительно останавливают процесс брожжения, и уже типа не то. Вино вполне можно разбавлять водой (не из крана, но хотя бы кипячёной), это меняет (и даже улучшает) его вкус. Сталин любил Киндзмараули, или Хванчкару.
Сталина тут до странного часто поминают. «Тут была дача Сталина». «Этот проспект проложили по приказу Сталина»...
Всё, что дают продегустировать, можно купить. И даже увезти домой. И даже на самолёте, если у вас есть багаж. Вино наливают в пластиковые бутылки.
Тут я впервые попробовал полусухое вино, которое мне понравилось. А то белое полусладкое, полторашку которого мы таки купили, оказалось сногсшибательным. Оказалось, что достаточно двух стаканов, чтобы им напиться.
Шайтан-машины
Аэропорт Сочи, который в Адлере, теперь занимает первое почётное место в моём рейтинге аэропортов. Он — не похож на аэропорт. Нет очередей на входе. Нет многочисленных стоек регистрации, они как-то аккуратно прячутся за пальмами в холле. Ненавязчивый досмотр. А после досмотра на посадку ты попадаешь в магазин. Не в тесные коридоры терминала, заставленные сиденьями, а в нормальный duty free магазин. И должен зигзагами через этот магазин пройти. Гениальный маркетинг!
Сиденья и выходы будут потом. Но снова совершенно естественно перемешиваясь с магазинами и кафе. Там даже есть маленькая детская площадка, между двумя магазинами игрушек. Всё-таки ребята с Кавказа любят и умеют продавать. Даже экскурсовод Армен, человек-не-умолкающий-ни-на-минуту, не просто рассказывал о достопримечательностях, но и нещадно, но ненавязчиво, рекламировал, что там можно купить.
В аэропорту
Сочи — хорошее место для отдыха. Не бюджетное, в силу расстояний (бо́льшая часть потраченных денег — авиабилеты). Но хоть виза не нужна. А вот кроме отдыха там делать нечего. Убери туристов — и весь смысл существования этих санаториев полностью исчезнет.

2018-08-11

О ГИС

ГИС — это Географическая Информационная Система. Именно поэтому 2ГИС так называется. Это и геоинформационная система, то есть карта, и Городская Информационная Система, то есть справочник организаций.
Но 2ГИС — это, так сказать, read-only GIS. Вы можете посмотреть, поискать, построить маршрут. Но ничего не можете менять.
В серьёзном взрослом мире нужно таки менять. Рисовать карты. Рисовать что-нибудь на картах. Типичный пример: OpenStreetMap. Это общенародная карта, где всякий может подрисовать свой гараж или тропинку в саду.
Но OSM собирает только картографические данные. А иногда нужно просто нарисовать что-то в привязке к местности. Я не геодезист и не архитектор, но, подозреваю, этим людям постоянно нужно что-то проектировать именно на карте. И для них существуют специальные GIS программы. Так же как для инженеров/чертёжников существуют CAD.
Хотел сказать, что единственной вменяемой свободной GIS является QGIS. Именно ею я и пользовался. Но Википедия говорит, что десятки их. Десятки только свободных. Но я всё равно рекомендую QGIS.
QGIS splashscreen
(На самом деле уже релизнулась версия 3.2, но во второй ветке сплешскрины красивее.)
Для чего вам может понадобиться ГИС? Почти для всего, где вам нужна будет карта. И надо на этой карте что-то нарисовать. Наложить треки, расставить точки, подсчитать расстояния. Да хотя бы редактировать OSM. Простые гугло/яндекс карты далеко не всё это могут.
Например, такая задача. Есть местность, где производятся некие инженерно-копательные работы. Вам, как программисту, нужно отобразить на карте план этих работ, дополнить всякими метками и графиками. В веб-приложении. И чтобы было сэкси.
Как бы не проблема. Есть Mapbox или Leaflet. Они могут отображать на своих слоях любой GeoJSON. Понятно, что GeoJSON можно просто сохранить в БД и выдавать по запросу. Или можно генерировать его из каких-то пространственных данных в другом формате.
Но где взять эти пространственные данные? Где взять эти точки, линии и многоугольники, с широтой и долготой по WGS 84? Если всё, что у вас есть — некий план, нарисованный в Paint. Хорошо хоть, в масштабе.
QGIS screenshot
Открываем QGIS и создаём новый проект...
Технически QGIS проект — это просто каталог слоёв. В виде файла с расширением .qgs в формате XML. А сами слои — это уже отдельные файлы, и не только файлы. Слои можно упорядочивать, группировать, скрывать или отображать, добавлять новые, удалять из проекта. Можно редактировать, но не все.
Условно можно выделить три типа слоёв.
Векторные слои. Самые важные, ибо вся работа, как правило, делается в них. Эти самые точки, линии да многоугольники — есть векторные сущности.
Векторные данные можно загружать из БД. Из PostgreSQL, возможно с PostGIS, хотя он умеет пространственные данные и просто из коробки. Из SpatiaLite — геопространственного расширения SQLite.
Векторные данные можно брать из файлов. Тот же GeoJSON сгодится. Треки в виде GPX файлов. Можно даже импортировать настоящие автокадовые чертежи в DWG. Только сначала нужно их конвертировать в DXF.
Если не связываться с базами данных, то самый ходовой формат для векторного слоя — так называемые Shapefiles. В одном Shapefile можно хранить только один тип данных: точки, линии или полигоны. QGIS умеет редактировать shapefiles в полном объеме.
Технически это на самом деле несколько файлов. .shp — бинарный файл с географическими координатами. .dbf — табличка dBase со свойствами наших географических примитивов. И ещё несколько для всяких связей, индексов и описания используемой системы координат.
Да, добавляешь в проект десяток слоёв, и вот уже в папочке проекта завалялась сотня странных файлов. А учитывая, что один и тот же файл можно умудриться включить в проект в виде разных слоёв, например, с разным отображением, то вообще кошмар.
Shapefile vs
Второй тип слоёв: растровые. Обычные картинки из пикселей. Их тоже можно натянуть на карту. Обычно предпочитают TIFF, потому что в него можно включить метаинформацию о тех же координатах. А вот рядом с PNG могут снова образоваться вспомогательные файлы.
Третий тип слоёв: тайловые подложки. Если рисуем на местности, нужно эту местность для начала как-то представить. И, с помощью плагина OpenLayers мы можем добавить в наш проект слой с картинкой из OpenStreetMap, или карт Google, или даже спутниковых снимков от Bing или снова от Google. Это будет работать как обычные веб-карты (только медленнее), тайлы будут качаться из интернетов.
Шаг нулевой выполнен. Добавлен нижний слой с тайлами из OpenStreetMap.
Плагины. QGIS написан на PyQt. И плагины к нему пишутся на Python. Тысячи их. Собственно, весь QGIS — это сборище плагинов.
Шаг первый. Мы нашли местность. Теперь на эту местность нужно натянуть план из растровой картинки. Это называется Georeferencing.
Дело в том, что естественными координатами растрового изображения являются координаты его пикселей. Столько-то пикселей по горизонтали, столько-то по вертикали. Это ещё и целые числа. Это соотносится с нашими вещественными широтой и долготой, которые в градусах глобуса, почти никак. Вот координаты в пикселях и нужно преобразовать в координаты в градусах на глобусе. И хорошо ещё, если обойдётся линейным преобразованием.
Тут нужно ещё помнить, что глобусы тоже бывают разные. Но, слава богу, в этих наших веб-картах, с проекцией Меркатора, принята система координат под названием EPSG:3857 (она же "WGS 84 / Pseudo-Mercator"). Только, внимание, единица изменения тут — метры! А если нужны градусы (а они нужны), но нужны другие координаты: EPSG:4326 (они же просто "WGS 84"). Причём в GeoJSON принято сию систему координат (CRS — Coordinate Reference System) называть urn:ogc:def:crs:OGC:1.3:CRS84.
У каждого слоя может быть своя система координат. Слава богу, QGIS умеет самостоятельно всё пересчитывать на лету. Постарайтесь не запутаться. И помните, что первая координата: X — это долгота (longitude), потому что по горизонтали. А вторая координата: Y — это широта (latitude), потому что по вертикали.
Поехали дальше. Нужно сделать georeferencing растровой картинки. Смысл в том, чтобы сопоставить некоторые референсные точки на картинке (в пикселях) с точками на карте (в широте и долготе). Двух точек достаточно для линейного преобразования. Но лучше расставить больше точек, чтобы сгладить погрешности, да и попробовать что-нибудь нелинейное. Как правило стоит обозначить что-нибудь по углам картинки, и ещё что-нибудь в середине.
Как нам это сделать? Я нашёл аж три способа.
Номер раз. Стандартный плагин Georeferencer GDAL. Eго не нужно скачивать, достаточно просто включить. Даёт, пожалуй, наиболее точный результат. При точных входных данных, конечно. Умеет красивые нелинейные преобразования. Если у вас на картинке есть линии, которые должны быть соответствующими линиями на карте, у вас есть неплохой шанс точно натянуть картинку на эти линии.
Алгоритм работы такой. В отдельном окошке загружаем картинку. Затем тыкаем на точку картинки и задаём координаты этой точки на карте. Для поиска координат на карте пригодится другой стандартный плагин Coordinate Capture. Помните, X — это долгота, а Y — это широта.
Georeferencing points
Когда мы натыкали достаточно точек, и сохранили их в отдельном файлике для этой картинки, можно попробовать выгнуть картинку, сохранить выгнутую, и добавить её слоем в наш проект. Можно выбрать несколько видов трансформации. Мне больше всего понравился "Projective". Это линейная трансформация, то есть она не изогнёт картинку дугой. Это линейная трансформация в трёх изменениях, то есть двумерная картинка обрабатывается матрицей три на три, и получается какая угодно наклонная проекция на плоскость карты. Получается неплохо.
Georeferencing transformations
Если результат нас не устраивает, удаляем слой из проекта, и возвращаемся к проставлению дополнительных опорных точек, или даже удалению лишних. Не очень интерактивненько.
Способ номер два. Плагин Freehand raster georeferencer. Как понятно из названия, всё придётся делать руками. Сюда сместить, тут наклонить, тут поднянуть. Прямо таскаете картинку (её лучше сделать полупрозрачной) по карте, пока не ляжет нормально. Получается быстро. Кривые картинки мостить удобнее. Но, конечно, никакой суперточности. Для нашего плана из Paint — самое то, пожалуй.
Freehand georeferencer
Способ номер три. Плагин Raster Bender. Плагин экспериментальный, так что не забудьте разрешить установку экспериментальных плагинов. Им я так и не воспользовался, извините. Но иконка зачотная.
Bender
Зато я воспользовался его ближайшим родственником: плагином Vector Bender.
Дело в том, что ещё мне понадобилось натянуть на карту чертёж в том самом DWG. То есть сделать georeferencing для векторных данных. И если для растра есть несколько вполне приличных способов, то для вектора всё как-то не очень. Из более-менее стандартных средств есть разве что Affine Transformations. Но там нужно матрицу трансформации тупо задать руками. А откуда взять цифры? Облазать полкарты с линейкой? И всё равно промазать?
Вот тут Vector Bender и пригодился. Он тоже экспериментальный, так что будьте осторожны. И он не то, чтобы сильно удобный в использовании.
В процессе создаётся целый новый слой. С прямыми. Которые обозначают векторы трансформации. То есть нужно указать, какая точка куда перемещается. А потом переместить. А потом ещё указать, если промахнулись.
В отличие от штатного растрового Georeferencer, Vector Bender определяет тип трансформации по количеству векторов. Линейная будет, если будет задано ровно два вектора перемещения. Это не удобно. А если задать больше, он будет править геометрию. Очень локально, пытаясь прилепить вот эту ближайшую точку вот сюда.
Ладно. Картинку на карту натянули. Где нам взять GeoJSON? Да просто обвести картинку. Создать новый векторый слой (в Shapefile), и натыкать в нём точек, если нужно пометить точки. Или нарисовать линий, если нужно обвести линии. А потом этот слой сохранить в GeoJSON.
Не забудьте указать EPSG:4326 при сохранении. И выставьте точность в шесть знаков после запятой. Квадриллионные доли градуса, которые предлагаются по умолчанию, вам не нужны.
Ну и всё. Извините, что не прикладываю скриншоты со своего QGISа. Ибо NDA.