Как известно, практически во всех вагонах Московского Метро действуют Wi-Fi точки доступа, с помощью которых пользователи могут получить доступ в интернет и приятно скоротать время поездки в метро с работы домой: почитать новости, проверить почту, посмотреть котиков на YouTube и т.д.

Каждому устройству, прежде чем ему будет предоставлен доступ к сети интернет, необходимо аутентифицироваться. В первый раз пользователю на указанный номер телефона посылается СМС с кодом, после чего система запоминает MAC-адрес устройства и в дальнейшем пользователю для аутентификации требуется только нажать на ссылку «Войти в интернет» и немного подождать.

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

Если в метро вы не частый гость, то подобная схема может и не вызвать у вас раздражения, однако при ежедневном использовании она все таки надоедает, поэтому, как говорил один известный и харизматичный политик: «Хватит это терпеть!», сегодня мы будем автоматизировать аутентификацию в Московском метро.

В первую очередь нам понадобится приложение Tasker. Раздобыть его можно тут (за небольшую денежку), ну или где-нибудь тут (на свой страх и риск). Лично я предпочел первый вариант и не пожалел.

Tasker — приложение, которое позволяет в зависимости от определенных условий (дата/время/местоположение/состояние устройств/показания датчиков и т.д.) производить определенные действия (отправку сообщений/вывод уведомлений/включение-отключение устройств/отрисовка простеньких интерфейсов и т.д.). Списки и условий, и действий просто огромны и зависят от версии Android и аппаратного оснащения устройства, так что приводить их полностью смысла нет.

Итак, после запуска Tasker, в первую очередь нужно перевести интерфейс на английский язык, ибо перевод тут хромает на обе ноги: Настройки->Интерфейс->Язык->English и перезапустить приложение. Теперь перед нами есть четыре вкладки:
  • Profiles — профили управляют связью между состоянием устройства/различными событиями и задачами;
  • Tasks — задачи описывают последовательность действий, которые необходимо выполнить;
  • Scenes — сцены это как бы самодельные формы, которые задачи могут создавать и настраивать, и контролы на которых могут запускать задачи;
  • Vars — список глобальных переменных, которые могут быть использованы для хранения данных между запусками задач.


Перейдем на закладку Tasks и создадим новую задачу, назвав ее Metro Auth:



В открывшемся окне нам необходимо для начала определить несколько переменных. Переменные определяются следующим образом:


  • Variable name — имя переменной, должно начинаться с символа % и состоять из строчных букв. Если в имени переменной будет хоть одна заглавная буква, то переменная станет глобальной, а нам это ни к чему;
  • To — значение переменной.

Так вот, нам необходимо создать следующие переменные:
  • %url — содержит ссылку на страницу, по которой мы будем тестировать нужна ли аутентификация или нет. Чем меньше объем передаваемых данных при этом — тем лучше (тело страницы все равно не нужно). С протоколом HTTPS возможны проблемы, так что лучше использовать HTTP;
  • %forms — содержит список идентификаторов HTML-форм, которые мы будем использовать для аутентификации. На момент написания статьи корректное значение — 'auth-form,hidden_form', однако если ребята из московского метро вдруг что-то изменят, я хотел бы показать как составить такой список самостоятельно, так что пока в качестве значения поставим один пробел (пустые переменные недопустимы);
  • %debug — эта переменная, при задании значения, отличного от нуля, будет вызывать показ дополнительной отладочной информации, которая поможет нам составить вышеупомянутый список форм.

Помимо простых действий, Tasker пред��ставляет нам возможность писать скрипты произвольной сложности с помощью нескольких инструментов. Мы будем использовать простой JavaScript:



Здесь необходимо выставить максимальный Timeout выполнения скрипта — 50 секунд, просто на всякий случай. Галочка Auto Exit отвечает за автоматическое завершения действия после завершения основного потока скрипта. В случае если используются асинхронные запросы (наш случай) или функция setTimeout, эту галочку необходимо снимать, а завершение действия определять самостоятельно с помощью функции exit();.

Сам скрипт я представлю в двух вариантах форматирования: приличное форматирование нужно если хочется рассмотреть скрипт, не сломав глаза, и форматирование под узкий экран позволяет скрипту выглядеть более-менее прилично на узком экране телефона. Изначально скрипт набирался на телефоне в «узком» варианте, и уже потом я переформатировал его для статьи:

Скрипт в приличном форматировании
function getUrl(url1,url2){
    url1=url1.split('?')[0];
    return url2.length?
        (/^http(s?):\/\//i.test(url2)?url2:
            (url2[0]=='/'?url1.split('/').slice(0,3).join('/')+url2:url1.split('/').slice(0,-1).join('/')+'/'+url2)
        ):url1;
}

function getVars(form,tag){
    vars='';
    fields=form.getElementsByTagName(tag);
    for(i=0;i<fields.length;i++)
        vars=vars+(i?'&':'')+fields[i].name+'='+fields[i].value;
    return vars;
}

function submit(xhr,request,form){
    request.url=getUrl(request.url,form.action);
    request.method=form.method;
    vars1=getVars(form,'input');
    vars2=getVars(form,'textarea');
    request.vars=vars1||vars2?(vars1?vars1:'')+(vars1&&vars2?'&':'')+(vars2?vars2:''):null;
    getPage(request,processPage,xhr);
}

function processPage(xhr,request){
    redir=xhr.getResponseHeader('Location');
    if(redir){
        if(redir==request.url) finalize('Ошибка: циклическое перенаправление');
        else{
            log('Перенаправление\n\n');
            getPage({'url':redir},processPage,xhr);
        }
    } else {
        forms=local('forms').split(',');
        id=null;
        for(i=0;i<forms.length;i++)
            if(xhr.response.getElementById(forms[i])) id=forms[i];
        if(id)submit(xhr,request,xhr.response.getElementById(id));
        else if(Number(local('debug'))){
            log('Формы на странице:\n');
            forms=xhr.response.getElementsByTagName('form');
            if(forms.length)
                for(i=0;i<forms.length;i++)
                    log((i?', "':'"')+forms[i].id+'"');
            else log('отсутствуют');
            finalize();
        } else finalize('Аутентификация успешна');
    }
}

function checkConn(xhr,request){
    redir=xhr.getResponseHeader('Location');
    if(redir){
        log('Перенаправление\n\n');
        getPage({'url':redir},processPage,xhr);
    } else {
        log('Аутентификация не требуется');
        finalize();
    }
}

function log(txt){
    logs=logs+(txt?txt:'');
}

function requestToText(request){
    return 'URL: '+request.url+'\nMethod: '+request.method+', Vars: '+request.vars+'\n\n';
}

function finalize(txt){
    log(txt);
    if(Number(local('debug'))) alert(logs);
    else if(txt) flashLong(txt);
    exit();
}

function getPage(request,func,xhr){
    if(!request.method) request.method='GET';
    if(!request.vars) request.vars=null;
    if(!xhr){
        xhr=new XMLHttpRequest();
        xhr.responseType="document";
        xhr.timeout=20*1000;
    }
    xhr.open(request.method,request.url,true);
    xhr.onload=function(){
        if(xhr.status==200 || xhr.status==401){
            log (requestToText(request)+'HTTP status: '+xhr.status+' '+xhr.statusText+'\n');
            func(xhr,request);
        } else {
            log(requestToText(request));
            finalize('Ошибка HTTP: '+xhr.status+' '+xhr.statusText);
        }
    }
    xhr.onerror=function(){
        log(requestToText(request));
        finalize('Ошибка: отсутствует соединение');
    }
    xhr.ontimeout=function(){
        log(requestToText(request));
        finalize('Ошибка: таймаут соединения');
    }
    xhr.send(request.vars);
}

logs='';
getPage({'url':local('url')},checkConn);

Скрипт в форматировании под узкий экран
function getUrl(url1,url2){
  url1=url1.split('?')[0];
  return url2.length?
    (/^http(s?):\/\//i.test(url2)?
      url2:
        (url2[0]=='/'?
        url1.split('/').slice(0,3).join('/')+url2:
        url1.split('/').slice(0,-1).join('/')+
      '/'+url2)
    ):url1;
}

function getVars(form,tag){
  vars='';
  fields=form.getElementsByTagName(
    tag);
  for(i=0;i<fields.length;i++)
    vars=vars+(i?'&':'')+fields[i].name+
      '='+fields[i].value;
  return vars;
}

function submit(xhr,request,form){
  request.url=getUrl(request.url,
    form.action);
  request.method=form.method;
  vars1=getVars(form,'input');
  vars2=getVars(form,'textarea');
  request.vars=vars1||vars2?
    (vars1?vars1:'')+
    (vars1&&vars2?'&':'')+
    (vars2?vars2:'')
    :null;
  getPage(request,processPage,xhr);
}

function processPage(xhr,request){
  redir=xhr.getResponseHeader(
    'Location');
  if(redir){
    if(redir==request.url)
      finalize('Ошибка: циклическое '+
        'перенаправление');
    else{
      log('Перенаправление\n\n');
      getPage({'url':redir},processPage,
        xhr);
    }
  } else {
    forms=local('forms').split(',');
    id=null;
    for(i=0;i<forms.length;i++)
      if(xhr.response.getElementById(
          forms[i]))
        id=forms[i];
    if(id)submit(xhr,request,
      xhr.response.getElementById(id));
    else if(Number(local('debug'))){
      log('Формы на странице:\n');
      forms=xhr.response.
        getElementsByTagName('form');
      if(forms.length)
        for(i=0;i<forms.length;i++)
          log((i?', "':'"')+forms[i].id+'"');
      else log('отсутствуют');
      finalize();
    } else finalize(
      'Аутентификация успешна');
  }
}

function checkConn(xhr,request){
  redir=xhr.getResponseHeader(
    'Location');
  if(redir){
    log('Перенаправление\n\n');
    getPage({'url':redir},processPage,
      xhr);
  } else {
    log('Аутентификация не '+
      'требуется');
    finalize();
  }
}

function log(txt){logs=logs+(txt?txt:'');}

function requestToText(request){
  return 'URL: '+request.url+
    '\nMethod: '+request.method+
    ', Vars: '+request.vars+'\n\n';
}

function finalize(txt){
  log(txt);
  if(Number(local('debug'))) alert(logs);
  else if(txt) flashLong(txt);
  exit();
}

function getPage(request,func,xhr){
  if(!request.method)
    request.method='GET';
  if(!request.vars)request.vars=null;
  if(!xhr){
    xhr=new XMLHttpRequest();
    xhr.responseType="document";
    xhr.timeout=20*1000;
  }
  xhr.open(request.method,
    request.url,true);
  xhr.onload=function(){
    if(xhr.status==200 ||
        xhr.status==401){
      log (requestToText(request)+
        'HTTP status: '+xhr.status+' '+
        xhr.statusText+'\n');
        func(xhr,request);
    } else {
      log(requestToText(request));
      finalize('Ошибка HTTP: '+
        xhr.status+' '+xhr.statusText);
    }
  }
  xhr.onerror=function(){
    log(requestToText(request));
    finalize('Ошибка: отсутствует '+
      'соединение');
  }
  xhr.ontimeout=function(){
    log(requestToText(request));
    finalize('Ошибка: таймаут '+
      'соединения');
  }
  xhr.send(request.vars);
}

logs='';
getPage({'url':local('url')},checkConn);

Набор скрипта с телефонной клавиатуры, к сожалению, не располагает к комментариям, но я вкратце опишу алгоритм:
  1. Пытаемся загрузить страницу, указанную в переменной %url
  2. Если в ответе нет HTTP-заголовка Location, значит нас не перенаправляют, а значит в данный момент аутентификация не нужна, выход
  3. Загружаем страницу, на которую нас направили.
  4. Если есть заголовок Location, возвращаемся к п.3
  5. Если на странице есть форма из списка в переменной %forms, изображаем ее submit и возвращаемся к п.3
  6. В остальных случаях — мы успешно прошли аутентификацию


После завершения ввода скрипта, у нас получилась вот такая задача:



С помощью первой иконки в нижнем ряду можно попробовать ее запустить. Теперь самое время спускаться в метро, чтобы настроить ее!

В метро, подключившись к точке доступа, пробуем запустить задачу. Если нет явных проблем с доступностью серверов, то мы увидим сообщение, похожее на то, что изображено на следующем рисунке слева. Внизу мы видим идентификатор формы, которая есть на последней загруженной странице — auth-form. Эта форма — явно наш клиент, вносим ее название в переменную %forms и запускаем задачу еще раз, получаем примерно то, что изображено на следующем рисунке в центре. Новый идентификатор формы — hidden_form. Добавляем ее в переменную %forms, теперь ее значение будет 'auth-form,hidden_form'. Запускаем задачу еще раз и видим примерно то, что изображено на следующей рисунке справа — либо будет форма без идентификатора, либо пометка «отсутствуют» (зависит от ветки метро). Если теперь запустить браузер, будет понятно, что аутентификацию мы прошли. Присваиваем переменной %debug значение «0», и закрываем задачу — тут мы закончили.



Теперь дело за малым — настроить автоматический запуск задачи при подключении к нужной точке доступа. Переходим на закладку Profiles и создаем новый профиль, который будет активироваться после подключения к точке доступа московского метро. После того, как закончим формировать описание точки доступа, Tasker спросит нас с какой задачей связывать это профиль, выбираем, естественно, Metro Auth.



Еще одни нюанс: хотя и редко, но аутентификация все таки слетает, хотя отключения от точки и не происходило. Если не было отключения, не было и переподключения, а значит Tasker не запустит задачу повторно, поэтому мы настроим Tasker так, чтобы аутентификация автоматически проверялась каждые 2 минуты (минимальный возможный интервал), для этого нам надо долгим нажатием на уже ск��нфигурированное условие вызвать меню, в котором добавить временное условие, в котором установить интервал.



Ну вот и все. Отныне и до тех пор, пока не придется менять идентификаторы в переменной %forms, алгоритм ваших действий при заходе в вагон следующий:
  1. Включить Wi-Fi;
  2. Дождаться сообщения «Аутентификация завершена» на экране;
  3. Загадочно улыбнуться и заняться своими делами.


UPD: По совету Self_Perfection я экспортировал проект и выложил одним файлом. Этот файл нужно скачать и положить в папку /sdcard/Tasker/projects, потом запустить Tasker, долгим нажатием на иконку домика в нижнем левом углу вызвать меню и выбрать Import. В этой версии я вынес проверку раз в две минуты в отдельный профиль — так должно расторопнее работать.