Android клиент для rutracker - обходим блокировку при помощи Google Compression proxy

Оригинал поста на хабре

Полагаю, что все пользователя хабра так или иначе нашли способ попадать на рутрекер, но порой бывает лень включать свой тор, прокси, впн или что либо ещё. Мне вот стало лень, и поэтому я решил написать свой маленький клиент. Для обхода блокировок я решил использовать google compression proxy. Интересная, хорошая и полезная штука - странно, что по её поводу на хабре не было статей. Забегая вперёд, сразу скажу, что всё получилось, и работающую версию можно попробовать на своём девайсе. Однако в процессе возникло много всяких интересных нюансов, которые любопытны несколько больше, чем само приложение. Итак, начнём!

Google Compression proxy

Чтобы не повторять гугловые мануалы (все ссылки вы можете найти в конце статьи), просто скажу, что этот прокси сервер позволяет вашему Google Chrome значительно уменьшать количество воспринимаемого трафика за счёт его сжатия серверами Google. Работать прокси умеет по HTTP и по HTTPS. В первом случае используется адрес compress.googlezip.net, во втором - proxy.googlezip.net. Интересно, что для прокси требуется свой заголовок. В официальной документации его нет, однако можно найти исходники от гугла и немного в них покопаться. Выглядят они вот так (ссылка на самый интересный файл, размещено уже у меня на гихабе, поскольку в официальном репозитории на google code уже ничего посмотреть нельзя). Оттуда получаем такое добро:
1
2
3
4
5
6
7
8
9
var authHeader = function() {
var authValue = 'ac4500dd3b7579186c1b0620614fdb1f7d61f944';
var timestamp = Date.now().toString().substring(0, 10);
var chromeVersion = navigator.appVersion.match(/Chrome\/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
return {
name: 'Chrome-Proxy',
value: 'ps=' + timestamp + '-' + Math.floor(Math.random() * 1000000000) + '-' + Math.floor(Math.random() * 1000000000) + '-' + Math.floor(Math.random() * 1000000000) + ', sid=' + MD5(timestamp + authValue + timestamp) + ', b=' + chromeVersion[3] + ', p=' + chromeVersion[4] + ', c=win'
};
};

Всё довольно очевидно, но можно остановиться подробнее.

Для каждого запроса должен присутствовать заголовок Chrome-Proxy. В нём должна быть следующая строка:

1
ps=<timestamp>-<num1>-<num2>-<num3>, sid=<md5 string>, b=<build>, p=<patch>, c=<platform>

где:

timestamp: время в linux timestamp num1, num2, num3: некие случайные числа, которые можно поставить в 0 md5 string: md5 хэш строки авторизации auth string: "<timestamp>" + "<auth key>" + "<timestamp>" auth key: ac4500dd3b7579186c1b0620614fdb1f7d61f944 - просто некий ключ… Один на всех, и все на одного. build: номер билда хрома - например, 2214 patch: номер патча хрома - например, 115 platform: платформа - например, “win” В качестве полного примера можно привести такой заголовок: Chrome-Proxy: ps=1439961190-0-0-0, sid=9fb96126616582c4be88ab7fe26ef593, b=2214, p=115, c=win

Как ни странно и не смешно, можно использовать эту самую строку при любом количестве запросов без всяких изменений… Например, на этом основано расширение для Firefox, которое занимается пересжатием трафика. Видимо, просто делалась защита от ленивого дурака. Однако, честным вариантом будет переписать это на Java так:

1
2
3
4
5
6
7
8
9
10
11
public static String[] authHeader() {
String[] result = new String[2];
result[0] = "Chrome-Proxy";
String authValue = "ac4500dd3b7579186c1b0620614fdb1f7d61f944";
String timestamp = Long.toString(System.currentTimeMillis()).substring(0, 10);
String[] chromeVersion = {"49", "0", "2623", "87"};
String sid = (timestamp + authValue + timestamp);
sid = Utils.md5(sid);
result[1] = "ps=" + timestamp + "-" + Integer.toString((int) (Math.random() * 1000000000)) + "-" + Integer.toString((int) (Math.random() * 1000000000)) + "-" + Integer.toString((int) (Math.random() * 1000000000)) + ", sid=" + sid + ", b=" + chromeVersion[2] + ", p=" + chromeVersion[3] + ", c=win";
return result;
}

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

WebView через SSL

Чтобы не идти долгим и печальным путём написания клиента с нуля, я решил "просто" показывать всё как есть через стандартный WebView, благо ранее уже писал простое-клиент, которое делает примерно то же самое, и шустро работает даже на тяжёлом веб сайте. Кажется - работы на полчаса. Как же я ошибался... Если посмотреть решения по проксированию WebView в интернете, то становится очень грустно - все делают примерно так:
1
2
3
4
5
6
7
8
9
10
public static void setKitKatWebViewProxy(Context appContext, String host, int port, String exclusionList) {
Properties properties = System.getProperties();
properties.setProperty("http.proxyHost", host);
properties.setProperty("http.proxyPort", port + "");
properties.setProperty("https.proxyHost", host);
properties.setProperty("https.proxyPort", port + "");
properties.setProperty("http.nonProxyHosts", exclusionList);
properties.setProperty("https.nonProxyHosts", exclusionList);
/// ... such much shit
}

Оставшуюся часть намеренно опустил - там идёт ещё около сотни строк с условиями по версии андроида и жутким шаманством. При этом у многих это всё равно не работает, плюс есть проблемы с переключением режима проксирования - и её “решают” путём установки Thread.Sleep(1000) между операциями. Хотя я не являюсь Java разработчиком, а просто иногда балуюсь, мне поплохело. Здравый смысл подсказал мне, что нужно перехватывать запросы из Webview (для этого у WebViewClient есть чудесная функция shouldInterceptRequest), и далее самому заниматься проксированием. Это у меня даже вполне получилось:

1
2
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("compress.googlezip.net", 80));
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(proxy);

Всё отлично, всё работает! Только одна проблема. Как заметили внимательные читатели, в параметрах функции указан 80ый порт. По довольно смешной причине. А именно - потому что HttpURLConnection не умеет работать с HTTPS проксями. Совсем. Никак. У меня ушло куча времени, чтобы понять, что всё настолько плохо, и что нельзя сделать HTTPS прокси ни через HttpURLConnection, ни через популярный okHttp . Я немного призадумался, затем решительным жестом отмёл все доводы Google о том, что библиотеки Apache не подходят для Android, стряхнул пыль с проверенных jar’ов и решительно подключил их к проекту. И всё получилось! Пятый и шестой андроид на ура восприняли такой ужасный проступок. Если кто-то знает, как можно было решить проблему без использования библиотек Apache - расскажите. Конечно, можно было бы сделать всё на сокетах, но это довольно печально.

Итак, мы наконец смогли отобразить главную страницу рутрекера. Казалось бы, победа близко. Как же я ошибался.

Реклама

Практически с первой попытки отладки я столкнулся с тем, что всё чудовищно тормозит. Причина довольно очевидна - безумное количество рекламы всевозможных видов. Мне очень не хотелось с ней что-то делать - все мы знаем положение рутрекера, и чувакам явно нужно много золота для защиты от DDOS'a и родного государства - но с рекламой приложением было пользоваться вообще нереально. Правильным решением было бы находить её и вырезать из тела страницы, но более быстрым для реализации подходом было просто порезать её по хостам:

довольно очевидный код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static boolean is_adv(Uri url) {
String[] adv_hosts = {"marketgid.com", "adriver.ru", "thisclick.network", "hghit.com",
"onedmp.com", "acint.net", "yadro.ru", "tovarro.com", "marketgid.com", "rtb.com", "adx1.com",
"directadvert.ru", "rambler.ru"};
String[] adv_paths = {"brand", "iframe"};
String host = url.getHost();
for (String item : adv_hosts) {
if (StringUtils.containsIgnoreCase(host, item)) {
return true;
}
}
if (StringUtils.containsIgnoreCase(url.getHost(), "rutracker.org")) {
String path = url.getPath();
for (String item : adv_paths) {
{
if (StringUtils.containsIgnoreCase(path, item)) {
return true;
}
}
}
}
return false;
}

И после этого мы просто обрубаем её получение:

1
2
3
4
if (Utils.is_adv(url)) {
Log.d("WebView", "Not fetching advertisment");
return new WebResourceResponse("text/javascript", "UTF-8", null);
}

Задним умом я только что подумал, что возможно было бы проще сделать список разрешённых хостов… Но этим самым умом все сильны. Теперь приложение стало работать не то что с приемлимой, а с очень бодрой и приятной скоростью. Если владельцев рутрекера это огорчит, то блокировку уберу - но скорее всего вместе с приложением. С чудовищными тормозами в нём просто не будет смысла.

Отправка форм

Довольно откинувшись на стул, я обнаружил... Что не работает авторизация. Что было на самом деле весьма очевидно - поскольку в перехватываемом мной и передаваемом далее запросе я не отправлял данные, которые должны уйти в POST. Казалось бы, пара минут - и всё будет хорошо. Как же я ошибался... Выяснилось, что способов перехватить POST из WebView нету. Совсем. Никак. Лучшие рекомендованные практики - внедрять в страницу свой javascript и вызывать из него специальные Java методы. Или переводить сервер на GET запросы. От первого варианта мне несколько поплохело, а второй недоступен по понятным причинам. Да и был бы некорректен. Почесав голову и попробовав отловить POST ещё в нескольких местах, я пришёл к выводу, что нормального решения всё же нет. В результате сделал решение смешное. А именно - при получении страницы менять метод всех форм с POST на GET. А после этого при следующем обращении конвертировать переданные в адресной строке параметры в тело POST запроса. Звучит ужасно, но всё не так плохо, если у вас нет адресной строки, в которой можно опозориться, больших переменных или файлов для передачи. Хотя нет, вру конечно. Всё очень плохо, но другого адекватного пути я не нашёл.

довольно очевидный код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static UrlEncodedFormEntity get2post(Uri url) {
Set<String> params = url.getQueryParameterNames();
if (params.isEmpty())
return null;

List<NameValuePair> paramsArray = new ArrayList<>();

for (String name : params) {
Log.d("Utils", "converting parameter " + name + " to post");
paramsArray.add(new BasicNameValuePair(name, url.getQueryParameter(name)));
}
try {
return new UrlEncodedFormEntity(paramsArray, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}

Где мои куки, чувак?

Когда я в очередной раз откинулся на спинку кресла после успешно пройденной авторизации и кликнул следующую ссылку, я обнаружил, что авторизация умирает на следующей странице. Что в общем тоже довольно логично, поскольку никто волшебно куками не управляет, а посылаем мы все заголовки вручную. Но здесь я в кои-то веки не ошибся в том, что дописать будет просто.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (Utils.is_login_form(url)) {

Header[] all = response.getAllHeaders();
for (Header header1 : all) {
Log.d("WebView", "LOGIN HEADER: " + header1.getName() + " : " + header1.getValue());

}
Header[] cookies = response.getHeaders("set-cookie");
if (cookies.length > 0) {
String val = cookies[0].getValue();
val = val.substring(0, val.indexOf(";"));
authCookie = val.trim();
CookieManager.put(MainContext, authCookie);
Log.d("WebView", "=== Auth cookie: ==='" + val + "'");
//redirect does not work o_O
String msgText = "<script>window.location = \"http://rutracker.org/forum/index.php\"</script>";
ByteArrayInputStream msgStream = new ByteArrayInputStream(msgText.getBytes("UTF-8"));
return new WebResourceResponse("text/html", "UTF-8", msgStream);

}
}

Правда, внимательный читатель обнаружит, что там идёт какая-то странная переадресация яваскриптом. Да, всё так. В ответе должен был быть 302 заголовок авторизации, но почему-то его никуда не приходило. В результате я оказывался на странице форума по некорректному адресу с доменом login.rutracker.org - всё работало, но, поскольку ссылки везде относительные, при следующем же клике наступал облом. Кстати, здесь же можно заметить, что пользовательскую куку мы бережно сохраняем, чтобы не пришлось потом заново авторизоваться.

Теперь-то всё?!

Именно с этим вопросом я в который раз открывал страничку, уже уверенный, что ничего не упустил. Как же я... В общем, можно было свободно искать через нативный поиск или через гугловый без авторизации, можно было смотреть любую тему, нельзя было только одно... Скачать торрент. Поскольку по нажатию ссылок ничего хорошего не происходит. Но в этот раз всё тоже было несложно - в интерфейс добавилась программная менюшка с кнопочкой Share, которая позволяет отправить куда угодно Magnet ссылку. Было бы удобнее кидать ссылку на торрент файл, но он у вас явно будет блокирован. Конечно, можно было бы скачивать торрент файл и передавать его через шаринг - но это как-нибудь в другой раз.

Результат

Можно посмотреть по скринам - главная страница и расшаривание magnet ссылки из топика.

Осталось только опубликовать

На основании тех странных android приложений, которые я выкладывал раньше, у меня сложилось ощущение, что они пропускают вообще всё. Так что я с лёгким сердцем отправил приложение на публикацию и сел писать статью. Обычно приложение появлялось на google play в течении пары часов, так что времени как раз хватало. К сожалению, в этот раз прошло 8 часов. И в ответ пришло
1
2
3
4
5
6
7
8
After review, Rutracker free - unofficial, ru.jehy.proxy_rutracker, has been suspended and removed from Google Play as a policy strike because it violates the <strong>impersonation</strong> policy.
<strong>Next Steps</strong>
<ol> <li> Read through the <a href="https://play.google.com/about/spam.html#impersonation-intellectual-property:impersonation">Impersonation</a> article for more details and examples of policy violations.</li>
<li> Make sure your app is compliant with the <a href="https://play.google.com/about/spam.html#impersonation-intellectual-property">Impersonation and Intellectual Property</a> policy and all other policies listed in the <a href="https://play.google.com/about/developer-content-policy.html">Developer Program Policies</a>. Remember additional enforcement could occur if there are further policy issues with your apps.</li>
<li> <a href="https://play.google.com/apps/publish">Sign in to your Developer Console</a> and submit the policy compliant app using a new package name and a new app name.</li></ol>
<strong>What if I have permission to use the content?</strong>

<a href="https://support.google.com/googleplay/android-developer/troubleshooter/2993242">Contact our support team</a> to provide a justification for its use. Justification may include providing proof that you are authorized to use the content in your app or some other legal justification.Additional suspensions of any nature may result in the termination of your developer account, and investigation and possible termination of related Google accounts. If your account is terminated, payments will cease and Google may recover the proceeds of any past sales and/or the cost of any associated fees (such as chargebacks and transaction fees) from you.If you&amp;rsquo;ve reviewed the policy and feel this suspension may have been in error,please reach out to our <a href="https://support.google.com/googleplay/android-developer/troubleshooter/2993242">policy support team</a>. One of my colleagues will get back to you within 2 business days.

Не совсем понятно, кто кого имперсонифицировал, но у меня было три варианта, что же не понравилось Google:

  1. Что я взял откуда-то иконки;
  2. Что я упомянул в комментариях, что приложение работает через google compression proxy;
  3. Что в качестве банеров я использовал варианты смешных логотипов рутрекера с нового конкурса;
Конечно,в любом случае довольно неприятно, что нужно зачем-то переименовывать pakage и закачивать его заново. Да тебя ещё и предупреждают, что "я тебя запомнил, и ещё раз так сделаешь - забаню". Ну как-то по гопнически. Не ожидал, прям обидно. Из упомянутых выше вариантов я решил, что скорее всего виноват второй - упоминание Google всуе. Ну ладно, как скажете - не стал писать, как работает приложение. Просто "приложение с проксированием, которое позволяет обходить блокировку rutracker.org". Ну и заодно поставил кривые нарисованные за пять минут иконки и аналогичные банеры. И что вы думаете - мои усилия были вознаграждены! Дальше мне пришёл следующий ответ.
After review, Rutracker free, ru.jehy.rutracker_free, has been suspended and removed from Google Play as a policy strike because it violates the webviews and affiliate spam policy.

Next Steps

Read through the Webviews and Affiliate Spam article for more details and examples of policy violations.
Make sure your app is compliant with the Spam policy and all other policies listed in the Developer Program Policies. Remember that additional enforcement could occur if there are further policy issues with your apps.
If it’s possible to bring your app into compliance, you can sign in to your Developer Console and submit the policy compliant app using a new package name and a new app name.

В общем, меня обвинили в том, что не то приложение только реферральные ссылки передаёт, то ли ничего кроме вебвьюва не делает. Ничего "левого" в приложении нет, а называть это "обычным отображением" тоже неверно - идёт довольно много работы по проксированию. Ну и особенно это меня удивило в свете того, что я уже успешно закачивал на Google Play приложения, которые фактически только состоят из вебвью на сайт. Нюансов было два - в приложении была авторизация, и я был хозяином сайта, на который шёл вебвью. Но я это никак не указывал, и Google узнать этого не мог.
В общем, на оба этих обвинения я ответил просьбой разобраться - сутки ответа нет, может быть ответ придёт ещё через сутки. Хотя надежды как-то мало. Так что ставим пока что приложение из APK. Если оно таки появится на Google Play, то можно будет обновиться оттуда.

<h1>ToDo</h1>
Если делать полноценное приятное приложение, то можно было бы добавить много всего хорошего. В том числе
<ol>
<li>Стили с адаптацией к просмотру с телефона и планшета; <b>UPD: сделано в какой-то степени;</b></li>
<li>Автоматическое обновление; <b>UPD: сделано;</b></li>
<li>Корректное вырезание рекламы;</li>
<li>Передачу торрентов торрент файлами, а не magnet ссылками;</li>
<li>Выход из авторизованного состояния на рутрекере (да, сейчас ты там авторизуешься навсегда);</li>
<li>Какие-то осмысленные сообщения о вероятных ошибках;</li>
<li>Совместимость с большим количеством устройств - сейчас можно попробовать запустить на Android от 4.0 до 6 - но результат непредсказуем - надо много тестировать. У меня работает на Nexus 5 с Android 6 и на Sony Xperia Z3 с Android 5;</li>
<li>Удобный ввод авторизации и поиска без того, чтобы тыкать на ужасные маленькие элементы веб формы;</li>
<li>Убрать из кода некоторое количество копи-пейста;</li>
<li>Добавить шифрование хранимой пользовательской куки уникальным для устройства ключом на случай, если данные с телефона украдут;</li>
<li>Реализовать монетизацию приложения, свои всплывающие банеры и ссылки, которые принесли бы мне тонны золота.</li>
</ol>
Но, к сожалению, у меня нет времени этим серьёзно заниматься - хотелось просто сделать некий работающий прототип - чтобы показать, что веб приложения в приложениях Android работают, работают хорошо и быстро. Попутно, правда, подтвердилось мнение о том, что среднее по больнице качество разработки на Android довольно сильно страдает, стандартные библиотеки не покрывают всех кейсов, и есть большое количество странных задач, которые никем толково не реализованы.
Буду рад, если кто-то возьмёт этот код для разработки более серьёзного приложения или же зашлёт пулл реквестов - обещаю их внимательно отсматривать и применять.

<h1>Q&amp;A</h1>

 - Но ведь гораздо проще зайти на рутрекер через ХХХ (например, просто включив экономию трафика на телефоне или в браузере).
 - Да.

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

 - А если рутрекер заблокирует гугл за большое количество заходов с их прокси?
 - К ним и так сейчас через неё ходят, просто используя хром. Так что вряд ли. И вообще, банить всех подряд - плохая идея, так можно и чиновником стать.

 - Ваш код ужасен!
 - Да, я уже упоминал, что я не Java разработчик.

 - У меня не заработало.
 - Да, это общая проблема Android - полноценное приложение надо тестировать в 10 раз дольше, чем писать. Увы, у меня такой возможности не было. Присылайте ошибки, пулл реквесты - поправим.

 - А можно вообще так использовать Google Compression Proxy?
 - Пока непонятно - приложение забанили ещё до этого потенциального вопроса.

 - Я не доверяю вам, наверняка вы крадёте все мои пароли и скачиваете себе мои торренты.
 - Пожалуйста - соберите приложение себе из исходников.

 - А почему иконки такие страшные?
 - Посмотрите под заголовком "осталось только опубликовать" - там всё объясняется.

 - Это же раздолье для злых роботов и пауков!
 - Да нет, не обольщайтесь. С очевидностью, там тоже есть свои лимиты и проверки на роботов. Есть гораздо более простые пути для ботов.

 - Сайт криво отображается.
 - Да, местами есть проблемы. Но связанные скорее с некачественной вёрсткой.

<h1>Привет рутрекеру</h1>
Отдельно - несколько пожеланий для администрации рутрекера на случай, если вдруг они сюда заглянут

<ol>
<li>Пожалуйста, подумайте о альтернативных вариантах монетизации. Всё равно большинство пользователей обходят эти жуткие гирлянды банеров адблоком.</li>
<li>Было бы хорошо внедрить различных провайдеров авторизации - гуглового или фейсбучного, например.</li>
<li>Обновите вёрстку и внешний вид до чего-то более красивого, удобного и желательно с мобильной версией.</li>
</ol>

<h1>Ссылки</h1>
<b>UPD</b>: <a href="https://habrahabr.ru/post/279553/">пост </a>про то, как реализовано обновление, и как вообще жить без Google Play;

<ol>
<li>Для тех, кто не осилил целиком текст - ещё раз скрины: <a href="https://habrastorage.org/files/1d7/650/2ac/1d76502ac6e547c3bdd9ea22a15e593d.png">главная страница</a> и <a href="https://habrastorage.org/files/a09/ca2/fc2/a09ca2fc22a143429ded78b4c669a2f1.png">расшаривание magnet ссылки из топика</a>.</li>
<li>Актуальные <a href="https://github.com/jehy/rutracker-free">исходники</a> и <a href="https://github.com/jehy/rutracker-free/releases">релизы </a>на гитхабе;</li>
<li><a href="http://whois.jehy.ru/">Тут </a>можно взять меня на работу - да, я её ищу;</li>
<li>Забавная <a href="http://wirama.web.id/using-google-chromes-compression-proxy-for-every-browser/ ">статья </a>о том, как использовать Google Compression Proxy+Squid. С неё я и начинал;</li>
<li><a href="https://code.google.com/archive/p/datacompressionproxy/">Тут </a>лежат исходники от compression proxy от гугла. Поскольку google code не даёт их посмотреть, можно посмотреть на <a href="https://github.com/jehy/datacompressionproxy/blob/master/background.js">моём гитхабе</a>. Его я и разбирал;</li>
<li>Что такое google compression proxy <a href="https://developer.chrome.com/multidevice/data-compression">для пользователя</a> и <a href="https://developer.chrome.com/multidevice/data-compression-for-isps">для администратора</a>;</li>
<li>Хороший <a href="http://superuser.com/questions/945924/how-do-i-use-google-data-compression-proxy-on-firefox">ответ и разбор стандарта google compression proxy</a>, правда только для HTTP траффика, не HTTPS. Есть пример реализации для firefox. Его я и переводил для статьи;</li>
<li><a href="https://github.com/miku-nyan/DCP-bridge">Реализация прокси на Java</a>, которая проксирует через google data compression server. Сделано для какого-то image board. Хороший код, есть что посмотреть;</li>
<li>Единственный найденный мной вариант по <a href="http://stackoverflow.com/questions/15048102/httprouteplanner-how-does-it-work-with-an-https-proxy">проксированию HTTP трафика через HTTPS прокси</a> при помощи библиотек apache. Его я и дорабатывал.</li>
</ol>

Чем плохо быть full stack разработчиком

Оригинал поста на хабре

Введение

Прежде всего определимся с терминами. Есть много разных представлений о том, кто же такой full stack разработчик, кто-то даже вполне обоснованно считает, что такие разработчики - это миф, но в этой статье будет иметься в виду разработчик, который обладает знаниями и умениями, позволяющими с нуля написать некий софт и вывести его в продакшн. При этом софт может быть рассчитан на web платформу, мобильные приложения или десктопные. Идеальный full stack разработчик - это тот, кто владеет в какой-то мере всеми платформами и может разработать и установить на них свой софт. Но это действительно скорее миф.

Неплохое определение с quora.com: “Когда люди ищут full stack разработчика, они ожидают увидеть поющего и танцующего техномага. Ну или хотя бы кого-то, кто не будет слишком сильно жаловаться, когда его попросят поработать вне его зоны комфорта”.

Возможно, по заголовку кому-то покажется, что это жалобный пост, который говорит о том, как плохо живётся неквалифицированному школьнику, который похватал всего из разных статей в интернете. Нет, пост не жалобный, мы говорим про full stack, а не full slack, и в конце будет так же рассмотрен список плюсов. И мы будем рассматривать не школьника, а разработчика с опытом работы в пять и более лет. Просто посмотрим, какие минусы есть в таком развитии.

Версионирование базы данных на лету

Оригинал поста на хабре

Здравствуйте, меня зовут Евгений, и я веб разработчик. Несколько лет назад мне перепала функция DBA (Database Administrator), я получил по этому поводу несколько сертификатов и решал соответствующие задачи. Я давно хотел описать задачу версионирования базы данных, но мне казалось, что для этого должны быть какие-то беспроигрышные варианты, которые хорошо знают умелые дяди, а я просто чего-то недопонимаю. Вчерашнее собеседование и последующий поиск по тематическим ресурсам показал, что это не так, и задача действительно сложна, актуальна и не решается однозначно. Разберём её по пунктам.

Что мы версионируем

Мы используем контроль версий только для DDL (Data Definition Language) запросов. Сами данные нас не интересуют. Почему? Рассмотрим два крайних случая.
  1. Данных мало (скажем, менее 50 мегабайт). В этом случае, мы можем просто периодически делать полный дамп базы и смело складывать его в репозиторий.
  2. Данных много (больше гигабайта). В этом случае версионирование нам мало поможет, всё равно разобраться в этом будет довольно проблематично. Целесообразно в данном случае использовать стандартную схему с бекапами и архив логом, которая позволяет нам получить целостную версию базы на любой момент во времени.

Ещё один способ определения качества воздуха на Arduino — с передачей данных в сеть

Оригинал поста на хабре

Меня зовут Евгений, и я - веб разработчик. Уже есть десятки постов о различных метеостанциях на ардуино, но мне хотелось написать о том, что в 2016 году можно быстро, легко и без знания электротехники собрать полезный датчик, претендующий на IoT, который легко может работать с вашей инфраструктурой, написанной на чём угодно. Если вам интересно, зачем вообще измерять СО2, то полезные ссылки вы можете найти в конце поста.

Итак, наша цель - сделать датчик температуры, влажности и СО2 с отображением данных на дисплей и отсылкой на веб сервер. Что нам для этого понадобится:

Компоненты

1\. Ардуино. Я взял Wemos D1, Arduino совместимую плату, основанную на микроконтроллере ESP-8266EX. Wemos совместим с ардуино IDE, у него есть свой Wi-Fi, и стоит он 6.3$.

2. Датчик СО2. Ранее я пробовал обычный MQ-135, но даже после прожига, калибровки и учёта поправок на температуру и влажность погрешности был довольно заметные - около 300 ppm. Так что для гарантии точных измерений я взял MH-Z19 - самый дорогой компонент схемы, 27$.

3. Датчик температуры и влажности. Использовал стандартный и любимый всеми DHT11 за 1.44$. В комментариях подсказывают, что лучше брать DHT22, но для схемы и стоимости это не принципиально:

4. I2C совместимый дисплей. Я взял самый дешёвый hd44780 за 3.06$.

5. Чтобы это выглядело не очень ужасно, и было перемещаемо, так же неплохо иметь корпус. Чтобы не заморачиваться с выпиливанием, я взял корпус за 8$ с дырками и креплениям под usb и дисплей. Внимание - в корпусе должны быть дырки для проветривания, иначе он будет измерять только свою особую атмосферу.

6. Отладочный USB кабель и около 10 проводков. Цену учитывать не буду.

Итоговая стоимость - 44 доллара, если брать на алиэкспресс. Аналогичные устройства без возможности что-то передавать в сеть сейчас стоят у китайцев в районе 100$. Аналог от Tion, которые в количестве делают свои посты на гиктаймс с рекламой бризеров, пока в разработке (не имею к ним отношения, а жаль).

Заказал, обождал месяц - приступаем к сборке! Нет ничего проще.

Сборка

1. Подключаем датчик температуры и влажности. Земля к земле, прах к праху, плюс к пяти вольтам на ардуино, цифровой выход к цифровому выходу (я использовал D5).

2. Подключаем датчик СО2. У него есть богатый выбор интерфейсов - PWM, аналоговый и цифровой вывод. Единственный элемент пайки - надо приделать ножки на нужные выходы. Проверял лично - работают все. Остановился на получении цифровых данных - точно, красиво, лаконично, и так же есть возможность отсылать команды на калибровку, что мне впрочем не потребовалось. Опять же - земля к земле, плюс к другим пяти вольтам, TX и RX выводы на цифровые пины - в моём случае, на D6 и D7.

3. Подключаем дисплей. И снова - земля к земле, плюс на последний оставшийся выход на пять вольт, SDA на дисплее к SDA на ардуино, SCL так же к SCL.

И… Всё! Можно закручивать получившееся добро в корпус, если вы конечно не умудрились сделать какую-нибудь ошибку при подключении. Обратите внимание, чтобы у датчика СО2 не оказались прижаты воздухозаборники (или как это корректнее назвать, белые такие штуки). Как бонус, при сборке в корпусе значительно уменьшаются отклонения в измерении.

Прошивка

Отлично, нам осталось “только” написать софт. В ссылках ниже есть репозиторий на гитхабе, который можно просто залить и использовать. Единственный тонкий момент - у вашего дисплея может оказаться другой адрес. Воспользуйтесь мини программой из ссылок для сканирования адресов i2c и поменяйте на нужный, если сразу не заработает. Перед сканированием отключите остальные устройства, иначе можно получить много мусора.

Да, Wemos D1 является совместимым с Arduino, и вам нужно просто добавить в Arduino IDE соответствующую борду. Подключаем стандартным коротким кабелем по micro USB и заливаем прошивку. Если вы всё сделали верно, то устройство сначала постарается подключиться к Wi-Fi (сеть и пароль берётся из файла настроек), затем ждёт некоторое время для “разогрева” датчиков, и наконец покажет данные на дисплее. Если по сети данные отправить не получится, то будет об этом сообщать. Если уровень CO2 допустимый, то после загрузки подсветка выключится, и включится только если вам пора открыть форточку.

Результат

У меня это выглядит так (осторожно, трафик):

В разобранном виде

В собранном виде, с выключенной подсветкой

Пора проветрить!

А как же сеть?

Теперь насчёт работы с сетью. Не мудрствуя лукаво, я собираю данные в JSON и отсылаю его на сервер обычным POST запросом, где простой скрипт на PHP кладёт его в MySQL базу. Далее можно посмотреть, как изменялись ваши условия жизни в течении дня при помощи PHP и Google Charts - вся серверная часть тоже есть в ссылках.

В итоге по смешной цене, с нулевыми знаниями в электротехнике и с минимальными затратами времени мы может получить примерно такие симпатичные графики: Осторожно, трафик

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

Да, показания я сравнивал с вот таким китайским датчиком Green Life - показания практически идентичные, только мой немного быстрее реагирует на изменение обстановки:

На картинке поднята контрастность дисплея - на нём всё видно отлично, но телефон перекорёжило от его света:

Выводы

Конечно, многое можно было бы сделать лучше. Навскидку я вижу следующие минусы:

  1. Дисплей с выключенной обладает очень низкой контрастностью. Лучше брать другой, разорившись ещё центов на 10.
  2. При включенной подсветке экран немного мерцает. Не критично, но видимо не нравится ему, что от ардуины запитывается сразу три устройства на пять вольт. Вероятно, правильно было бы сделать раздельное питание.
  3. Конечно, всё это можно красиво спаять, а не просто покидать в коробку, кое как соединив.
  4. Датчик температуры и влажности иногда возвращает левые огромные значения. Их я просто отсекаю, но вообще неприятно. Возможно, сказывается общая запитка или некачественное соединение.
  5. Конечно, можно было бы накапливать статистику и периодически её отсылать (и накапливать в случае ошибок) - никому не нужно знать своё качество воздуха с точностью до 5 секунд.
  6. Чтобы не вбивать данные вайфайной точки при компиляции, можно было бы сделать, чтобы в случае, если коннект не удался, устройство переходило в режим вайфай точки (да, Wemos это умеет), подцепившись к которой, можно его перенастроить - в том числе ввести новое имя сети и пароль.
  7. Wemos D1 всем хорош, но у меня иногда вылетает (раз в 2-3 дня) по неизвестным причинам, и сам перегружается только через несколько часов. Вроде как не мой софт кривой, просто борда не очень стабильная. Можно с этим бороться подключением устройства к какой-нибудь ардуино нано, которая его перезапускает при необходимости, или просто поставить железку с таймером, которая будет раз в полчаса перезапускать устройство (благо у него есть вход RESET).
  8. При выдёргивании и возврате обратно питания, датчик СО2 сходит с ума, что лечится перезагрузкой. Я это решил простым способом - при накоплении некоторого количества непрерывных ошибок, устройство само себя перезапускает, после чего всё приходит в норму. Вероятно, можно было бы решить это более правильным способом на уровне железа.
  9. Температура измеряется с точностью до двух градусов, влажность - с точностью до десяти процентов. В бытовых целях достаточно, а для какого-то другого использования понадобится датчик поточнее.
  10. Если вам требуется отправить один небольшой пакет данных, то библиотека для работы с JSON это убийство воробьёв из пушки. Гораздо проще собрать JSON ручками, или вообще передавать параметры при помощи GET/POST, что я вначале и делал. Но при наличии огромного количества оперативки на Wemos - почему нет, так красивее. Но при портировании кода на маломощную ардуину скорее всего придётся переписать это.
  11. В текущей реализации нет управления яркостью подсветки - она может быть только включена и выключена. Можно убрать перемычку и поставить вместо неё сопротивление или ввести на ардуино - тогда вы сможете управлять яркостью подсветки.
  12. Так же можно сделать управление или получение информации при помощи приложения. Самый простой способ, как ни смешно - добавить телеграмм бота - библиотека для связи ардуино через телеграмм уже есть.

Что дальше

А дальше я хочу отдохнуть от рабочих будней и поиграть с друзьями в лазертаг. Одна проблема - стоит игра от 500 рублей в час на человека. Это при полном отсутствии расходников. А купить комплект для игры будет стоит каких-то совсем заоблачных денег - примерно от 8000. Так что для того, чтобы поиграть в лазертаг, надо сначала его собрать. Планирую сделать что-то вроде Skirmos - где каких-то денег будет стоит только сам ствол, который должен быть довольно прочным. Интересно? Следите за публикациями! А что вы делаете на Arduino?

Ссылки

Основное

  1. Код для ардуино
  2. Серверная часть
  3. Посмотреть на воздух в моём офисе можно тут
  4. Взять меня на работу (да, я её ищу) можно тут

Полезное

  1. Если вы ничего не знаете про СО2 - https://geektimes.ru/company/tion/blog/269134/
  2. О wemos d1
  3. Спецификации датчика MH-Z19
  4. Хорошая статья, примерно про то же самое, но без сети, с дисплеем от телефона, с работой по PWM и на Arduino nano
  5. Сканирование устройств на i2c
  6. Будущий аналог датчика от Tion (не имею к нему отношения)
  7. Решение для перезапуска ESP8266 на уровне железа
  8. Ещё одна хорошая статья про ESP, Lua и CO2
  9. А тут студенты МАМИ занимаются всякой хорошей элетроникой. Кстати, им требуются преподаватели. Скоро буду там вести лекции.

Использованные библиотеки

  1. Библиотека для работы с дисплеем
  2. Библиотека для создания JSON
  3. Библиотека для работы с семейством сенсоров DHT

Один день Зимы

Огромные кристаллы снежинок, порхая в воздухе, неумолимо стремились к грязному асфальту, превращаясь на нём в тёмную слякоть. Они могли танцевать, застывать на предметах, резкие порывы ветра иногда вскидывали их обратно вверх - но судьба их была предрешена. Я подставил им лицо, и они таяли, оставляя на нём длинные влажные следы. Неплохая судьба, если сравнить их с теми, что были безжалостно размазаны и стекали в канализацию тёмными лужами. А ведь когда-то я шёл сквозь снег и ни одна снежинка не посмела ко мне приблизиться, словно облетая невидимую преграду.

Бег с Microsoft Band

Не так давно стал счастливым обладателем Microsoft Band. До него бегал сначала с Endomondo на телефоне, потом с Strava, потом получил Suunto Quest с HR датчиком, и радостно использовал его. Поскольку у нас пока в продаже MS Band нету, то наверняка обзор будет интересен. Если что, купить это чудо можно примерно за 10.000 рублей на ebay. Не за 17, как у нас продают :)

Итак, что же мы хотим от устройства?

  1. Пульс. Думаю, комментировать очевидное не стоит.
  2. GPS. Важно знать пройденное расстояние и интересно смотреть на маршруты, которые пробежал. На той же Strava можно ещё сравнивать своё время на известном маршруте с другими бегунами. Бывает интересно - побегал в произвольной местности, а потом оказалось, что там тоже кто-то бегал. Сравниваешь, радуешься… Или огорчаешься и мотивируешься :)
  3. Количество шагов. Не так важно, но интересно.

Маскировка ссылок в произвольных полях с wp-noexternallinks

Внимание! Я более не занимаюсь поддержкой этой плагина, после версии 3.5.9.9! Пожалуйста, задавайте вопросы на странице поддержки wordpress.

Маскировать ссылки в произвольных полях при помощи wp-noexternallinks очень просто. Сначала, нужно найти место, где идёт вывод вашего произвольного поля. Скорее всего, он идёт в файле вашей темы, single.php. Выглядеть он должен как-то так:

echo get_post_meta(get_the_ID(), 'имя_вашего_поля', 1);

Затем редактируем его, чтобы он выглядел так:

//получить содержимое поля
$text=get_post_meta(get_the_ID(), 'имя_вашего_поля', 1);
//Использовать на содержимом поля все те же фильтры, что на стандартном выводе контента страницы
//В эти фильтры так же входит фильтр wp-noexternallinks:
$text_filtered=apply_filters('the_content',$text);
//Вывести отфильтрованный текст:
echo $text_filtered;

Заметьте, что если вы не маскируете ссылки в тексте поста, а маскируете их в комментариях, то пятая строчка должна выглядеть так:

//Использовать на содержимом поля все те же фильтры, что на стандартном выводе комментария пользователя
//В эти фильтры так же входит фильтр wp-noexternallinks:
$text_filtered=apply_filters('comment_text',$text);

Заметьте, что если произвольное поле выводится при помощи плагина, то вам придётся править его код или как-то ещё обрабатывать его вывод.

Обновление Я так же добавил фильтр, который можно вызывать в коде без остальных, вот так:

$text_filtered=apply_filters('wp_noexternallinks',$text);

Masking links in custom fields with wp-noexternallinks

Warning! I no longer support this plugin after version 3.5.9.9! Please ask questions on wordpress support page.

Masking links in custom fields with wp-noexternallinks is really simple. At first, you need to find the place where your custom field is being output. It should be in your theme file, single.php.

Then edit output so it looks like this (you should change custom_field_name to your field name):

//get custom field text with links:
$text=get_post_meta(get_the_ID(), 'custom_field_name', 1);
//Apply all the filters that are used for post content on custom field text.
//It will also apply wp-noexternallinks filter:
$text_filtered=apply_filters('the_content',$text);
//output filtered text:
echo $text_filtered;

Notice that if you don’t mask links in your posts’s content but mask them in comments, your fifth line should look like this:

//Apply all the filters that are used for user comments on custom field text.
//It will also apply wp-noexternallinks filter:
$text_filtered=apply_filters('comment_text',$text);

If custom field output is generated by plugin then unfortunately you will have to edit plugin code or try to preprocess it’s output.

UPDATE I also provided custom filter to process links. You can call it like this:

$text_filtered=apply_filters('wp_noexternallinks',$text);

Encrypting links for WP-NoExternalLinks

Warning! I no longer support this plugin after version 3.5.9.9! Please ask questions on wordpress support page.

This article is based on this one and the latest version of plugin. Now you can easily encrypt your links without changing plugin code.

  1. Ensure that you have php-mcrypt installed:

For CentOS, type

sudo yum install php-mcrypt

For ubuntu, type

sudo apt-get install php5-mcrypt

For windows, enable this extension in php.ini:

extension=php_mcrypt.dll
  1. Create file wp-content/uploads/custom-parser.php and add here the following:
salt ='ms8-sRt-kBs5-s1wQ';
    $this->iv=mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND);
  }
  function encode_link($url)
  {
    if(!$this->salt)
      $this->init_crypt();
    return trim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $this->salt, $url, MCRYPT_MODE_ECB, $this->iv)));
  }
  function decode_link($url)
  { 
    if(!$this->salt)
      $this->init_crypt();
    return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $this->salt, base64_decode($url), MCRYPT_MODE_ECB, $this->iv));
  }
}
?>

Do not forget to change $salt to any custom value! Also, add statistic handling from the original function if you use statistics.

Custom parser for WP-NoExternalLinks

Warning! I no longer support this plugin after version 3.5.9.9! Please ask questions on wordpress support page.

Recently I added a new feature to this plugin. Now you can extend it yourself without danger of plugin update which could remove all your changes in plugin code.

In this sample, we will overwrite function check_follow, which checks if link is posted by admin and has rel=”follow” attribute. Let’s imagine we want all our authors to have the same options. How can we accomplish it? Easily! Just create file custom-parser.php in directory wp-content/uploads and replace

user_can($author,'manage_options' );
from original function with
user_can($author,'publish_posts' );
- voila!

Here is the full code of our new modified class which will be loaded instead of basic one:

wp-content/uploads/custom-parser.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
if(!defined('DB_NAME'))
die('Error: Plugin "wp-noexternallinks" does not support standalone calls, damned hacker.');

#include base parser
include_once(ABSPATH . 'wp-content/plugins/wp-noexternallinks/wp-noexternallinks-parser.php');

class custom_parser extends wp_noexternallinks_parser
{
function check_follow($matches)
{
#support of "meta=follow" option for admins. disabled by default to minify processing.
if(!$this->options['dont_mask_admin_follow'])
return false;
$id=array(get_comment_ID(),get_the_ID());//it is either page or post
if($id[0])
$this->debug_info('It is a comment. id '.$id[0]);
elseif($id[1])
$this->debug_info('It is a page. id '.$id[1]);
$author=false;
if($id[0])
$author=get_comment_author($id[0]);
else if($id[1])
$author=get_the_author_meta('ID');
if(!$author)
$this->debug_info('it is neither post or page, applying usual rules');
elseif(user_can($author,'publish_posts' )&&(stripos($matches[0],'rel="follow"')!==FALSE || stripos($matches[0],"rel='follow'")!==FALSE))
{
$this->debug_info('This link has a follow atribute and is posted by author, not masking it.');
#wordpress adds rel="nofollow" by itself when posting new link in comments. get rid of it! Also, remove our follow attibute - it is unneccesary.
return str_ireplace(array('rel="follow"',"rel='follow'",'rel="nofollow"'),'',$matches[0]);
}
else
$this->debug_info('it does not have rel follow or is not posted by author, masking it');
return false;
}
}
?>

One more sample is encrypting links.