суббота, 5 ноября 2011 г.

Indy 9, SSL и SOCKS Proxy

В предыдущем посте я обещал рассказать о том, как удалось прикрутить SOCKS proxy для отправки писем через защищенный SSL канал. Для тех, кто не в курсе, или подзабыл, напомню, что я, по требованию пользователей, добавлял в старую, добрую прогу возможность отправки писем через SMTP сервер, требующий использования защищенного канала. Для тестов использовался публичный SMTP сервер smtp.gmail.com. Как я решал эту задачку с использованием Delphi 6 и Indy 9, было описано ранее. Но в процессе решения возникла неожиданно еще одна проблема, не имеющая отношения к поставленной задаче.

Корни проблемы - в правилах безопасности, запрещающих использование публичных почтовых служб на рабочем месте. Если опустить ненужные подробности и сразу обратиться к сути проблемы, то имеем следующий расклад: достучаться из рабочей сетки через общий прокси до SMTP сервера Gmail просто невозможно. А очень хотелось.

Естественно, сразу появилась мысль использовать какой-нибудь хитрый прокси. Чтение документации Indy 9, в принципе, подсказало решение. Indy 9 использовать тунельные HTTP прокси не умеет, остается попробовать SOCKS прокси. Сказано - сделано. На формочку тестовой программулины ложится groupbox с элементами для ввода адреса и порта прокси (я знал, какой прокси буду использовать, и знал, что он не потребует авторизации, соответственно, не озадачивался вводом имени и пароля для прокси) и флажком (checkbox), определяющим необходимость использовать прокси (часть экспериментов с тестовой программкой производилась не на рабочем месте). Кроме того, на форму был также брошен компонент TIdSocksInfo. На обработке события OnClick кнопочки  "Послать" был добавлен код, который устанавливал значения нужных свойств компонента TIdSocksInfo из элементов ввода, а сам этот компонент прописывал в свойство SocksInfo компонента TIdSSLIOHandlerSocket. Все это, конечно, только в случае, если взведен флажок (checkbox) "Использовать SOCKS прокси". Ничего сложного. 

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

Далее началась всем известная бодяга под названием дебагирование. Дебагировал я, или, как многие говорят, трассировал, исходники ставшего уже родным Indy. Собственно, источник ошибки был найден довольно быстро. Оказалось, что при обращении к прокси производится попытка использовать SSL сокет, а сам сокет создавался уже после улаживания всех формальностей с прокси. Единственное, что меня удивило, так это то, что при  трассировке я оказывался в нужном месте (процедуре TIdSSLSocket.Send) объекта ( TIdSSLSocket ), которого не существовало. И только при попытке обращения к свойству не созданного объекта (объект имел адрес nil), возникало исключение. Получается, что вызвать метод несуществующего объекта я могу, а вот использовать его свойство - извините... Странно, конечно, но пока это меня занимало меньше всего. Хотелось все-таки пробиться к smtp.gmail.com и с рабочего места: не тестировать же софт только по ночам из дома.

В принципе, найти быстрый обходной путь, мне помогла другая... ээээ... особенность, назовем это так, Indy версии 9. А именно то, что он не умеет работать в режиме explicit TLS. Об этой особенности я уже писал раньше. Тогда, для того, чтобы обойти такое поведение Indy при работе с TLS, я использовал свойство PassThrough компонента TIdSSLIOHandlerSocket.
Ошибка про доступ не по тому адресу возникала при попытке послать почту по каналу, защищенному SSL версии 3. При отправке почты по каналу, защищенному протоколом  TLS версии 1 ошибки не было. Казалось бы, в чем разница? А разница была в том, что из-за особенностей работы Indy с протоколом TLS, на момент попытки соединения с SMTP сервером, свойство PassThrough компонента TIdSSLIOHandlerSocket имело разные значения в зависимости от  используемого протокола: для протокола SSL значение этого свойства было равно False, а для протокола TLS - True, так как надо было попробовать обратиться к серверу по незащищенному каналу, чтобы получить приглашение использовать команду STARTTLS.

Итак, я понял, что для того, чтобы использовать SOCKS прокси при посылке письма по защищенному SSL каналу надо, чтобы на момент попытки установить соединение свойство PassThrough имело значение True. Выставить это значение можно тогда, когда становится понятно, что надо использовать прокси. Оставался вопрос, когда надо это свойство вернуть в значение False?

Я стал подробнее исследовать, что делает каждый компонент Indy, задействованный в этом триллере. Оказалось, что нужно дождаться момента, когда произойдет соединение с сервером, но реального обмена информацией с ним еще не производилось. Это меня обрадовало. Еще больше меня обрадовало то, что найти нужное место мне удалось довольно быстро. Им оказался обработчик события OnConnected компонента TidSMTP.

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

type
   TIdSMTPAccess = class(TIdSMTP)
   end;

procedure TMainTestForm.btnSendClick(Sender: TObject);
var NameToSend: String;
begin
  // Подготовим сообщение
  msgContent.From.Address := edtFrom.Text;
  msgContent.Recipients.EMailAddresses := edtTo.Text;
  msgContent.Subject := edtSubject.Text;
  msgContent.Body.Text := mMessage.Lines.Text;

  // Подготовим соединение с SMTP сервером
  smtpClient.Host := edtHost.Text;
  smtpClient.Port := StrToInt(edtPort.Text);
  if chckAuthentication.Checked then
  begin
    smtpClient.AuthenticationType := atLogin;
    smtpClient.Username := edtUserName.Text;
    smtpClient.Password := edtPassword.Text;
  end
  else
    smtpClient.AuthenticationType := atNone;

  try
    // Проверим работоспособность OpenSSL
    if cbxSecurity.ItemIndex > 0 then
      TIdSSLContext.Create.Free;

    // Определим протокол и версию для защищенного соединения
    case cbxSecurity.ItemIndex of
      1: sslIOHandler.SSLOptions.Method := sslvSSLv2;
      2: sslIOHandler.SSLOptions.Method := sslvSSLv23;
      3: sslIOHandler.SSLOptions.Method := sslvSSLv3;
      4: begin
         sslIOHandler.SSLOptions.Method := sslvTLSv1;
         // Из-за невозможности немедленного использования TLS запретим
         // шифрование
         sslIOHandler.PassThrough := True;
         end;
    end;

    // Обработчик канала
    if cbxSecurity.ItemIndex <= 0 then
      smtpClient.IOHandler := nil
    else
      smtpClient.IOHandler := sslIOHandler;

    if chckProxy.Checked then
    begin
      socksProxy.Host := edtProxyHost.Text;
      socksProxy.Port := StrToInt(edtProxyPort.Text);
      socksProxy.Version := svSocks5;
      sslIOHandler.SocksInfo := socksProxy;
      sslIOHandler.PassThrough := True;
    end
    else
      sslIOHandler.SocksInfo := nil;

    // Попытка соединения
    smtpClient.Connect();
    try
      // Если сервер требует STARTTLS
      if smtpClient.LastCmdResult.Text.IndexOf('STARTTLS') <> -1 then
      begin
        smtpClient.SendCmd('STARTTLS', 220);
        // Разрешим использование шифрования
        TIdSSLIOHandlerSocket(smtpClient.IOHandler).PassThrough := False;
        // Перечитаем возможности сервера в плане AUTH
        smtpClient.AuthSchemesSupported.Clear;
        if Length(smtpClient.HeloName) > 0 then
          NameToSend := smtpClient.HeloName
        else
          NameToSend := smtpClient.LocalName;
        if smtpClient.SendCmd('EHLO ' + NameToSend) = 250 then
          TIdSMTPAccess(smtpClient).GetAuthTypes;
      end;
      // Отправим письмо
      smtpClient.Send(msgContent);
    finally
      smtpClient.Disconnect;
    end;

  except   // Что-то пошло не так
    on e: Exception do
      begin
        ShowMessage(e.Message);
        cbxSecurity.ItemIndex := 0;
      end;
  end;

end;

procedure TMainTestForm.smtpClientConnected(Sender: TObject);
begin
  if (smtpClient.IOHandler = sslIOHandler) and
     (sslIOHandler.SSLOptions.Method in [sslvSSLv2, sslvSSLv23, sslvSSLv3]) and
     (sslIOHandler.PassThrough) then
    sslIOHandler.PassThrough := False;
end;

Надо отметить, что описанное решение является обходом проблемы, тем, что в английском языке называется workaround, в контексте работы с найденными ошибками. В том, что это ошибка Indy 9 лично у меня никаких вопросов не вызывает.

Возникает, наверное, вопрос, а зачем весь этот сыр-бор? Есть же Indy 10, а там, возможно, таких проблем бы и не было. Что ж, может и не было бы. Но замена Indy 9 на Indy 10 наверняка потребовала бы больших трудозатрат, чем мое небольшое исследование. Не факт, что все прошло бы гладко. Не факт, что у Indy 10 нет своих каких-нибудь тараканов. Ну и от добра добра не ищут - софт то рабочий, проверенный годами эксплуатации. Так что, на мой взгляд, игра стоила свеч.

Может, кто-то найдет этот материал полезным. Ведь наверняка есть определенное количество программ, использующих Indy 9. Вот, в общем-то, и все.

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

2 комментария:

  1. if chckAuthentication.Checked свойство какого объекта в Indy 9

    ОтветитьУдалить
    Ответы
    1. Здравствуйте. Для проверки работоспособности кода я создал небольшую формочку, бросив на нее несколько компонентов, в том числе, TCheckBox, назвав его chckAuthentication - чтобы можно попробовать режимы с аутентификацией и без нее. Так что, chckAuthentication.Checked не имеет отношения к Indy 9.

      Удалить