Как грамотно обработать iOS CNA при подключении к WiFI с Captive Portal?

Доброго времени суток!

Задача: нужна авторизация пользователей WiFi. На iOS требуется открыть портал авторизации не в CNA, а в Safari. Собственно, это у меня получилось...почти.

iOS при подключении к WiFi делает определенный запрос, ожидая получить определенный ответ. Если этот ответ подделать, то iOS решит, что интернет открыт. Но тогда не покажется CNA, а это плохо.

Идея такая:

1. Первый запрос не подделываем. Возвращаем HTML, в котором есть ссылка, например, click me. Откроется CNA (мааааленький ограниченный браузер);

2. Остальные запросы подделываем, как будто отвечает сам apple.

Частично это сработало. Если нажать на ссылку из п.1, CNA закроется, а ссылка начнет открываться в Safari. Вроде время радоваться, но шиш! Запрос уходит через мобильную сеть, а от WiFi-сети iOS молча отключается.

Я понимаю сейчас задачу так: когда CNA уже на экране, заставить iOS думать, что доступ в Интернет через этот WiFi уже открыт. Тогда iOS не отключится от WiFi и запрос в браузере уже пойдет правильно и будет счастье! Как это сделать, пока не догнал(

Решение явно существует. В московском метро это сделали.

Люди, помогите! Нет возможности кататься по метро и снифить трафик((

P.S. Стандартый CNA не подходит, т.к. он закрывается и отключается от WiFi на каждый чих. В моем случае, пользвателю иногда требуется зайти в приложение SMS и затем вернуться на портал.

UPD. : в CNA есть кнопка "Готово". Если ее нажать, вместо ссылки, то iOS решает, что доступ в Интернет есть. Вероятно, нужно как-то сэмулировать нажатие этой кнопки при переходе по ссылке
  • Вопрос задан
  • 7917 просмотров
Решения вопроса 1
OCTAGRAM
@OCTAGRAM
Следите за руками:

21:23:25 GET "captive.apple.com" /hotspot-detect.html HTTP/1.0 302 0 - "CaptiveNetworkSupport-346 wispr" - - - "http://captive.apple.com/hotspot-1.html"
21:23:25 GET "captive.apple.com" /hotspot-1.html HTTP/1.0 403 86 - "CaptiveNetworkSupport-346 wispr" - - - -
21:23:26 GET "captive.apple.com" /hotspot-detect.html HTTP/1.1 200 467 - "Mozilla/5.0 (iPhone; CPU iPhone OS 10_2 like Mac OS X) AppleWebKit/602.3.12 (KHTML, like Gecko) Mobile/14C92" - - - -
21:23:26 GET "captive.apple.com" /hotspot-detect.html HTTP/1.0 200 68 - "CaptiveNetworkSupport-346 wispr" - - - -
21:23:28 GET "captive.apple.com" /hotspot-2.html HTTP/1.1 302 0 - "Mozilla/5.0 (iPhone; CPU iPhone OS 10_2 like Mac OS X) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0 Mobile/14C92 Safari/602.1" - - - "http://мой-URL"


Первые два ответа — это чтобы показать wispr такую бяку, чтоб он захотел вмешаться. wispr и на Mac OS X, и на iOS заканчивается на " wispr". Сразу 403 или 200 без Success как-то не очень работают, а 302 — вполне, но раз 302, то и по другому адресу надо что-то ответить. Потом запускается CNS (нет Safari), и ему отдаётся страничка, на которой есть Success и скрипт, который через полсекунды создаст обычную (без _blank) ссылку с абсолютным URL (который с двоечкой) и нажмёт по ней click()'ом. При отдаче страницы на stateful сервере помечается этот комп. Либо переход по ссылке, либо Success и код 200 на странице, либо просто это делается каждый раз после первого открытия, пока не разобрался, что-то из этого заставляет сделать ещё один запрос wispr, и на этот раз помеченному компу отдаётся самый обычный Success, а когда через полсекунды пойдёт переход по ссылке, CNS уже будет в состоянии «Готово» и направит переход в настоящий Safari, а сам закроется. При обнаружении открытия ссылки настоящим Safari пометка с компа сбрасывается.

Бывает, зацикливается в CNA, но после увеличения задержки на секунду вроде нормализовалось.

with AWS.Status;
with AWS.Response;

package Worker_Echoes.Apple.Captive is

   function Service (Request : AWS.Status.Data)
     return AWS.Response.Data;

end Worker_Echoes.Apple.Captive;


with Ada.Strings.Fixed;

with AWS.URL;
with AWS.Messages;

with Worker_Echoes.Protected_Strings;
with Worker_Echoes.Config;

package body Worker_Echoes.Apple.Captive is

   use AWS;
   use all type AWS.Messages.Status_Code;

   Last_CNA_CNS_Peername : Protected_Strings.Protected_String;

   -------------
   -- Service --
   -------------

   function Service (Request : Status.Data) return Response.Data is
      URL_Object : constant URL.Object := Status.URI (Request);
      URL_String : constant String := URL.URL (URL_Object);
      User_Agent : constant String := Status.User_Agent (Request);
   begin
      if User_Agent'Length >= 6 and then User_Agent (User_Agent'Last - 5 .. User_Agent'Last) = " wispr" then
         if Last_CNA_CNS_Peername.Get = Status.Peername (Request) then
            return Response.Build
              (Status_Code => S200,
               Content_Type => "text/html; charset=utf-8",
               Message_Body => "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>");
         elsif URL_String = "http://captive.apple.com/hotspot-1.html" then
            return Response.Build
              (Status_Code => S200,
               Content_Type => "text/html; charset=utf-8",
               Message_Body => "<HTML><HEAD></HEAD><BODY></BODY></HTML>");
         else
            return Response.URL ("http://captive.apple.com/hotspot-1.html");
         end if;
      elsif Ada.Strings.Fixed.Index (User_Agent, "Safari") = 0 then
         Last_CNA_CNS_Peername.Set (Status.Peername (Request));

         -- 1. Not sure if intact "<BODY>Success</BODY>" matters, but probably yes.
         -- 2. There was no delay previously, thus A had to be created before BODY is processed.
         --    Now it's possible to click A with id, but touching working code was avoided.
         return Response.Build
           (Status_Code => S200,
            Content_Type => "text/html; charset=utf-8",
            Message_Body => "<HTML><HEAD><SCRIPT>" &
                               "window.setTimeout (function () {" &
                                  "var A = document.createElement (""a"");" &
                                  "A.setAttribute (""href"", ""http://captive.apple.com/hotspot-2.html"");" &
                                  "var Body = document.getElementsByTagName (""body"");" &
                                  "if (Body.length > 0) {" &
                                     "Body = Body [0];" &
                                  "} else {" &
                                     "Body = document.createElement (""body"");" &
                                     "document.getElementsByTagName (""html"") [0].appendChild (Body);" &
                                  "}" &
                                  "Body.appendChild (A);" &
                                  "A.click ();" &
                               "}, 1000);" &
                            "</SCRIPT>" &
                            "<STYLE>body { display: none; }</STYLE>" &
                            "<TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>");
      else
         Protected_Strings.Reset (Last_CNA_CNS_Peername, Status.Peername (Request)); -- reset if matches
         return Response.URL (Config.Get_Target_URL);
      end if;
   end Service;

end Worker_Echoes.Apple.Captive;
Ответ написан
Пригласить эксперта
Ответы на вопрос 2
@zzzevaka Автор вопроса
1. Спасибо большое OCTAGRAM за развернутый ответ. Комментарии не поддерживают форматирование, поэтому отвечу отдельным постом.

2. У меня получилось как-то проще. Может я что-то упускаю, но работает стабильно на нескольких айфонах с разными версиями IOS (8-10).

Код на python:

# функция обрабатывает URL /hotspot-.*.html
# для запоминания пользователей я использую redis
def get(self):
  # если мы помним, что этот пользователь уже обращался на hotspot-.*.html,
  # подделываем ответ captive.apple.com
  if self.redis_conn.get('apple_wispr:some_user_marker'):            
    self.finish(
      '''
        <HTML>
          <HEAD><TITLE>Success</TITLE></HEAD>
          <BODY>
            <a href="http://ya.ru">This link will open in Safari</a>
          </BODY>
        </HTML>    
      '''
  )
  # иначе, запоминаем этого пользователя на 10 секунд
  # и отдаем заглушку. всплывет CNA
  else:
    self.redis_conn.set('apple_wispr:some_user_marker', 1, 10)
    self.finish('<html></html>')


Как видно, даже без "success" в body прокатывает.

В nginx это выглядит так:

"GET /hotspot-detect.html HTTP/1.0" 200 13 "-" "CaptiveNetworkSupport-346 wispr"
"GET /hotspot-detect.html HTTP/1.1" 200 273 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 10_2 like Mac OS X) AppleWebKit/602.3.12 (KHTML, like Gecko) Mobile/14C92"
"GET /hotspot-detect.html HTTP/1.0" 200 273 "-" "CaptiveNetworkSupport-346 wispr"


3. Собственно в чем была моя проблема:

До того, что надо на первый wispr запрос отвечать коряво, а на все остальные подделывать оригинальный ответ captive.apple.com я допер еще до того, как задал этот вопрос.

Проблема была такая: при нажатии на ссылку, от WiFi-сети устройство отключалось и шло в safari через мобильный интернет. Это при том, что в CNA уже была кнопка "Done" и вроде как CNA уже обманут.

Когда до меня дошло, я долго себя материл за тупость. Причина:

Для перенаправления пользователя на сервер авторизации я использую функционал Mikrotik ip hotpsot.
При попытке ios достучаться до captive.apple.com, Mikrotik его редиректил (с помощью HTTP 302) на сервер авторизации. И так при КАЖДОМ ЗАПРОСЕ.

IOS на оригинальные запросы ВСЕГДА получал 302. Интересно, что частично он принимал ответы после редиректа за нормальные (кнопка done же появлялась).

Решение: перенаправить запросы на сервер авторизации без использования HTTP.
Я прописал в DNS Mikrotik'a A-запись captive.apple.com на свой сервер и добавил на сервере обработчик на /hotspot-.*.html

И похоже, что ура.
Ответ написан
@master_fox
Ну вот: всё верно, по A-записи ВСЕГДА при запросе домена captive.apple.com отправляете гаджет на свой сервер (предварительно прописанный в walled-garden, разумеется). А сервер нужно научить при подобных запросах глядеть в записи об открытых radius-сессиях на предмет наличия там мака девайса (да и логина юзера тоже), чтобы в случае наличии активной сессии (например, если последняя началась меньше чем N часов до установленного вами максимума) - отвечать гаджету всегда "Success" и отправлять с миром. По идее тогда айфоны будут меньше "нервничать" в руках пользователей, будучи УЖЕ авторизованными по механизмам MAC-cookie или HTTP-cookie.
М? :)
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы