O limite da persistência ao buscar uma solução

Categoria: Artigos
Categoria Pai: ZØST
Acessos: 6945
Imagem meramente ilustrativa

Em busca da verdade (O relato)

Recentemente eu estabeleci uma meta de aprender a usar a API WinInet para realizar conexões simples a recursos na web ou em redes locais. Como parte dessa meta, eu estou escrevendo uma função para realização de conexões POST e GET usando WinInet + Crypto API (para utilização de certificados-cliente). Para entender a saga que vou relatar aqui é necessário entender qual é a minha intenção com esta função. A ideia é bem simples: eu quero acessar um recurso online que necessita de um certificado-cliente e quando há a tentativa de conexão inicial sem o certificado, haverá uma falha interna (um erro esperado) e a própria função se encarregará de solicitar o certificado e tentar conectar-se novamente. O resultado dessa operação deve ser um retorno válido em caso de sucesso ou exceções em caso de insucesso.

Após criar a função e todas as subfunções, eventos, e estruturas de dados necessárias para seu funcionamento eu resolvi testar a função com meu próprio site. O resultado foi bem sucedido e o retorno foi, claro, o código-fonte do mesmo. Em seguida resolvi testar a aplicação automática de um certificado-cliente, para isso eu realizei uma conexão a um webservice que precisa de autenticação do lado do cliente.

Ao realizar a conexão, quando a requisição é enviada (HttpSendRequest), é feita uma tentativa de handshake com o servidor, que naturalmente falha, porque eu estou propositalmente deixando que a função lide com certos problemas. Neste caso, ao não conseguir concluir o handshake, HttpSendRequest retornará o erro ERROR_INTERNET_CLIENT_AUTH_CERT_NEEDED (12044).

Ao detectar este erro, a minha função vai tentar resolver o problema aplicando um certificado-cliente à requisição, repetindo-a em seguida. O certificado (arquivo .pfx) juntamente com sua senha foram previamente configurados em dois parâmetros da minha função, a qual os usará apenas caso o erro ERROR_INTERNET_CLIENT_AUTH_CERT_NEEDED aconteça. Após aplicar o certificado-cliente a requisição, a nova tentativa foi bem sucedida (handshake concluído) e, finalmente, o conteúdo esperado é retornado.

A fim de poder testar outros comportamentos eu resolvi colocar no Form de teste um TEdit para que eu pudesse informar em tempo de execução a senha do arquivo .pfx. O primeiro teste foi feito com a senha correta e funcionou como esperado. E foi aí que a coisa começou a ficar estranha. Sem fechar a aplicação eu realizei uma nova tentativa usando uma senha errada e o resultado foi bem sucedido também, ou seja, mesmo realizando uma conexão totalmente nova ela parece ter utilizado as informações da conexão anterior, a qual já possuía um certificado aplicado.

Apenas para deixar bem claro, a função que estou desenvolvendo está protegida com vários blocos try...finally que sempre fecham todos os handles, logo, no meu entendimento, toda vez que eu clicasse no botão "Enviar Requisição", uma requisição totalmente nova deveria ser feita.

Resolvi fazer mais um teste, dessa vez eu fiz a primeira conexão com a senha errada para ver o comportamento. A primeira tentativa retornava o erro ERROR_INVALID_PASSWORD (86), que faz todo sentido, mas aí, novamente sem fechar a aplicação, eu alterei a senha, informando a senha correta e, para minha surpresa o erro ERROR_INTERNET_SECURITY_CHANNEL_ERROR (12157) foi mostrado. Depois disso, qualquer tentativa de conexão invariavelmente resultava em ERROR_INTERNET_SECURITY_CHANNEL_ERROR, me obrigando a fechar a aplicação para realizar novamente uma requisição "do zero".


Foi aí que começou a minha saga em busca de uma solução para isso, pois na minha cabeça era inconcebível que se eu fechasse todos os handles de conexão, algo da conexão anterior ainda pudesse existir. Comecei fazendo pesquisas simples no Google e não achei nada que pudesse me ajudar. Escrevi uma pergunta no forum do MSDN, para qual eu obtive apenas uma resposta que não ajudou muito. Apelei, claro, para o Stack Overflow e fiz lá uma pergunta que explicava meu problema e até o presente momento nenhuma resposta[1].

A apelação maior começou quando eu, sem achar nenhuma resposta, resolvi ver quais as funções que a WinInet.dll exportava. Para isso eu usei o excelente programa DLL Export Viewer e procurei funções que tivessem palavras como Clear, Delete, Reset, Cache etc. Para minha tristeza, a maioria das funções "suspeitas" não tinha qualquer referência no MSDN, me levando a problemas mais profundos (achar a ajuda para uma função que eu nem mesmo sei se serve para resolver meu problema).

Sem encontrar nada útil na WinInet, resolvi pensar um pouco mais e cheguei a uma conclusão óbvia:

Se o problema diz respeito a aplicação de um certificado a uma requisição, então deve haver algo de útil na biblioteca schannel.dll, que é a biblioteca responsável por implementar a camada SSL nas conexões com o WinInet

Usei o DLL Export Viewer na biblioteca e lá achei a função SslEmptyCache, algo realmente promissor que tinha até mesmo documentação no MSDN, a qual li e concluí prematuramente que este seria o fim de minha jornada. Corri para implementar e, de novo, para minha tristeza, não surtiu qualquer efeito.

Já quase sem esperanças de resolver esse problema eu olhei bem para o nome da função SslEmptyCache e lembrei que na caixa de diálogo Propriedades da Internet existia um botão que lembrava muito o nome dela:

Ao clicar no botão Limpar estado SSL, a mensagem "O cache SSL foi esvaziado com êxito" é exibida. Uma ponta de esperança surgiu, pois tanto o texto do botão como a mensagem de sucesso contém palavras que eu estava esperando encontrar (Limpar, SSL, Cache, Esvaziado). Corri para fazer um teste, que consistiu em clicar no botão Limpar estado SSL antes de realizar o clique no botão da minha aplicação que faz a requisição e o resultado: SUCESSO ABSOLUTO! Aquilo que eu realmente estava procurando era como limpar o estado SSL.


Minha busca chegou a um novo patamar: descobrir o que o botão Limpar estado SSL faz, assim, voltei ao Google e perguntei exatamente isso. Não foi fácil, mas eu achei em meia dúzia de locais, pessoas que afirmaram que o comando abaixo era executado ao se pressionar o botão Limpar estado SSL:

"C:\Windows\system32\rundll32.exe" "C:\Windows\system32\WININET.dll",DispatchAPICall 3

Eu achei muito estranho que o pressionamento de um botão em uma caixa de diálogo execute o rundll32, já que, sendo um programa compilado, é muito melhor chamar a função DispatchAPICall importando-a diretamente a partir de WinInet.dll. Para o programa executar esta linha de comando ele precisa criar um processo, algo que seria totalmente desnecessário nesse caso, até mesmo insano!

Resolvi então testar de qualquer forma esta linha de comando, primeiro, diretamente na caixa de diálogo Executar do Windows, depois, diretamente no programa, importando a função DispatchAPICall, tanto estática, quanto dinamicamente e o resultado foi o mesmo: a segunda requisição pareceu ter "usado" o estado SSL anterior, isto é, o estado  SSL não foi limpo. Acreditei demais nas respostas encontradas no Google e acabei levando um tiro no pé. Em vários locais eu encontrei a linha de comando que usa o rundll32 e por conta disso acreditei piamente que esta era a solução, mas ela se mostrou ineficaz. Eu imagino que esta solução deva funcionar em Windows antigos, mas eu fiz os testes no Windows 7 e Windows 10, logo, qualquer versão entre estes dois invariavelmente iria falhar de qualquer modo e por conta disso, ela não serviu pra mim.

Nem tudo estava perdido. Eu já sabia onde estava o comando que funcionava e tudo que eu precisava saber é o que o botão Limpar estado SSL fazia e foi aí que eu resolvi pôr a mão na massa eu mesmo para descobrir. Existe um programa muito bom chamado API Monitor, que é capaz de monitorar (duuuh!) chamadas de API específicas e exibir informações valiosas a respeito do que está acontecendo.

Abri a tela do API monitor, selecionei os grupos Internet, Security and Identity e Web Development. Em seguida, abri a caixa de diálogo Opções da Internet, fui até a aba Conteúdo. Inicializei o monitoramento do API Monitor e cliquei no botão Limpar estado SSL. Como num passe de mágica, surgiram os vencedores dessa corrida:

Pronto! As funções de API executadas ao se pressionar o botão Limpar estado SSL são SslEmptyCache e IncrementUrlCacheHeaderData e tudo que eu precisava fazer era executar estas duas funções dentro da minha aplicação antes de realizar a requisição e para fazer isso eu precisava de mais informações a respeito delas. A função SslEmptyCache possui documentação no MSDN e lá eu descobri que ela deveria ser executada exatamente como o API Monitor mostrou, ou seja, SslEmptyCache(nil,0). Restava apenas saber como a função IncrementUrlCacheHeaderData funcionava mas não há qualquer documentação oficial a respeito dessa função e a única coisa que pude concluir a respeito dela é que seu primeiro parâmetro tem recebe o valor constante 14, mas sem saber como passar o segundo parâmetro eu não poderia usá-la. Voltei ao Google e digitei apenas o nome da função e, sem achar nada útil na primeira página, passei para a página seguinte:


Admita, você automaticamente pularia os dois primeiros resultados não é? Felizmente eu não fiquei intimidado com o texto em chinês e resolvi abrir o segundo link (pulei o primeiro por intuição mesmo) e lá estava a resposta que eu procurava, uma explicação sobre as duas funções que eu pretendia utilizar:

清除IE浏览器SSL缓存

如题,建立SSL连接后,IE浏览器默认会缓存SSL会话状态信息。为了保证下次登录或连接时不再使用缓存中的旧有信息,就必须清理SSL缓存。

一般,我们可以找到“工具”-“Internet 选项”-“内容”,点击“清除SSL状态”即可:

但是,如果通过程序自动删除呢?这就需要调用微软提供的API接口来解决了。

经过Google,网上信息实在少的可怜,不过终于还是找到了解决办法,那就是:

(1) 调用Schannel.dll库中的SslEmptyCache方法,第一个参数传“null”,第二个参数传“0”

(2)调用Wininet.dll库中的IncrementUrlCacheHeaderData方法,第一个参数传“14”,第二个参数传Int指针地址(值为0)

Obviamente eu usei o Google Translator, o qual retornou um português bastante claro, levando em conta que é uma tradução automática:

Limpe o cache SSL do navegador IE

Como exemplo, depois de estabelecer uma conexão SSL, o navegador IE armazenará as informações de estado da sessão SSL por padrão. Para garantir que a informação antiga no cache não seja mais utilizada na próxima vez que você efetuar o login ou se conectar, você deve limpar o cache SSL.

Em geral, podemos encontrar "Ferramentas" - "Opções da Internet" - "Conteúdo", clique em "Limpar Status SSL" para:

No entanto, se é excluído automaticamente pelo programa? Isso precisa ser resolvido chamando a API fornecida pela Microsoft.

Após o Google, a informação online é realmente pitiful, mas finalmente encontrou uma solução, isto é:

(1) chamar o método SslEmptyCache da Schannel.dll, o primeiro parâmetro passou "nulo", o segundo parâmetro passou "0"

(2) chamar o método WinInet.dll IncrementUrlCacheHeaderData, o primeiro parâmetro passou "14", o segundo parâmetro passou no endereço do ponteiro Int (o valor é 0)

Acima, uma palavra não foi traduzida (pitiful) que quer dizer "lamentável" e pode-se então perceber que até mesmo o chinês teve problemas para achar essa solução. Juntamente com essa explicação havia um código em C++ o qual, finalmente acabou com todas as minhas dúvidas:

typedef BOOL (CALLBACK *pDelSSL)(LPSTR pszTargetName,DWORD dwFlags);
typedef BOOL (CALLBACK *pDelSSL2)(DWORD inp,DWORD dwFlags);

void CTestDlg::OnOK() {

  pDelSSL DelSSL;
  HINSTANCE hdll = NULL;
  hdll = LoadLibrary("Schannel.dll");
  
  if (hdll != NULL) {
    if((DelSSL = (pDelSSL)GetProcAddress(hdll, _T("SslEmptyCacheA"))) == NULL) {
      //MessageBox("加载函数失败");
    }
  }

  BOOL A = DelSSL(NULL,0);

  FreeLibrary(hdll);

  HINSTANCE h_wininetDLL = LoadLibrary("wininet.dll");
  pDelSSL2 DelSSL2 = (pDelSSL2) GetProcAddress(h_wininetDLL,"IncrementUrlCacheHeaderData");
  DWORD buf = 0;
  A = DelSSL2(14,&buf);
  
  FreeLibrary(h_wininetDLL);
}

Com o código acima eu consegui entender que o segundo parâmetro de IncrementUrlCacheHeaderData é um ponteiro para um DWORD que recebe um valor incremental que representa quantas vezes a função foi executada (provavelmente com o parâmetro 14). Diante disso consegui finalmente criar a chamada para a função da seguinte forma: IncrementUrlCacheHeaderData(14,@buffer), onde buffer é a variável do tipo DWORD que será incrementada, descartando o seu valor inicial. O valor retornado em buffer é global no Windows, o que significa que, ao executar a função IncrementUrlCacheHeaderData em duas aplicações distintas este valor será compartilhado entre elas, por exemplo, ao executar esta função na aplicação A, buffer = 1. Ao executar esta função na aplicação B, buffer = 2, Ao clicar o botão Limpar estado SSL na caixa de diálogo Opções da Internet e executar esta função na aplicação A, buffer = 4. Por fim, resolvi meu problema com o trecho de (pseudo) código a seguir:

type
  TSslEmptyCache = function (pszTargetName: LPSTR; dwFlags: DWORD): BOOL; WINAPI;
  TIncrementUrlCacheHeaderData = function (nIdx: DWORD; lpdwData: LPDWORD): BOOL; WINAPI;

var
  SchannelDLLHandle, WinInetHandle: HMODULE;
  SslEmptyCache: TSslEmptyCache;
  IncrementUrlCacheHeaderData: TIncrementUrlCacheHeaderData;

SchannelDLLHandle := LoadLibrary('schannel.dll');
WinInetHandle := LoadLibrary('wininet.dll');

if (SchannelDLLHandle > 0) and (WinInetHandle > 0) then
  try
    SslEmptyCache := GetProcAddress(SchannelDLLHandle,'SslEmptyCacheW');
    IncrementUrlCacheHeaderData := GetProcAddress(WinInetHandle,'IncrementUrlCacheHeaderData');
    if Assigned(SslEmptyCache) and Assigned(IncrementUrlCacheHeaderData) then
    begin
      SslEmptyCache(nil,0);
      IncrementUrlCacheHeaderData(14,@buffer);
    end;
  finally
    FreeLibrary(SchannelDLLHandle);
    FreeLibrary(WinInetHandle);
  end;

Tudo que eu tive de fazer foi colocar este código ANTES de executar a requisição e agora cada requisição que usa certificados não reutiliza os dados de certificado de uma conexão anterior.


Algumas dicas globais

Vou deixar aqui algumas dicas as quais sempre uso para tentar achar a solução de um problema. Para começar a desenvolver a função de requisição usando WinInet, eu precisei entender como realizar uma conexão usando esta API e, como qualquer API do Windows, toda documentação existente é voltada para o C++ ou C#. De cara já podemos extrair uma lição muito importante:

Dica #1: Conheça mais de uma linguagem

Nem sempre aquilo que você procura está disponível na linguagem que você domina, principalmente quando você estiver buscando informações sobre alguma função da API do Windows, portanto é de suma importância que você entenda o mínimo da linguagem básica do sistema operacional para o qual você está desenvolvendo. No caso do Windows a linguagem é o C++. Você não precisa ser especialista em C++ para entendê-la. Qualquer tutorial de C++ básico dará boas dicas da sintaxe e a maior dificuldade será se habituar com os ponteiros e outras notações diferentes, mas nada que uma pesquisa no Google não resolva

Após achar a função inicial que eu precisaria utilizar para desenvolver a função, eu descobri que todo o procedimento de requisição/resposta segue uma lógica baseada em cerca de 5 funções que realizam a tarefa. A primeira função retorna um handle, que é usada numa segunda função que retorna outro handle, que é usado numa terceira função que retorna mais um handle, etc. Essa sequencia de operações nem sempre é explicada em um único local. Normalmente o MSDN (local que possui a documentação para quase todas as funções de API da Microsoft) possui a documentação das funções isoladamente e às vezes um exemplo mínimo de uso dessas funções (em C++, claro). Muito mais raro é encontrar no MSDN um exemplo completo que faça tudo o que você quer, portanto é necessário expandir os horizontes:

Dica #2: Pesquise exemplos em fontes diversas

O MSDN deve ser a fonte principal de recursos para as funções de API do Windows, no entanto ele não é focado em exemplos. Mesmo que você não esteja pesquisando a respeito da API do Windows, sempre é uma boa prática buscar exemplos em mais de uma fonte. O Google é uma ferramenta muito importante para buscar exemplos, por exemplo, se você quer um exemplo a respeito da função HttpSendRequest, no Google digite: HttpSendRequest example Delphi ou simplesmente HttpSendRequest example, para buscar exemplos em qualquer linguagem. Se você tiver sorte, pode ser que alguém já tenha criado um exemplo em Delphi e a primeira sentença de busca já vai retornar o que você precisa, do contrário, ao usar a segunda sentença podem ser retornados mais resultados em outras linguagens, mas, considerando que você entendeu a Lição #1, isso não será problema

Se você é um leitor atento, deve ter percebido que eu usei a palavra example (em inglês) ao invés de exemplo e o motivo disso é simples, nos levando a mais uma lição:

Dica #3: Entenda inglês

Não tem jeito, infelizmente (ou felizmente), todas as linguagens de programação de alto padrão são baseadas em inglês. Não faz nenhum sentido existir um programador que não saiba um mínimo do idioma do Tio Sam ou que seja avesso a realizar buscas usando este idioma. A não ser que você esteja buscando por algo muito específico que você tenha certeza que foi feito por brasileiros, sempre a melhor opção é buscar resultados em inglês. A grande maioria dos resultados, e normalmente os resultados mais corretos são feitos por programadores que falam inglês, não pelo fato de eles serem melhores programadores, mas sim pelo fato de que eles tem acesso a recursos no idioma que eles já dominam desde criança. Você não precisa falar e nem escrever bem o inglês, mas se você entender o que está escrito, certamente vai chegar no resultado mais rapidamente

Mesmo que você não saiba inglês, hoje em dia existe o Google Translator, o qual, a cada dia que passa, se torna mais e mais preciso. Não existe mais desculpa pra ficar procurando soluções apenas em português, até porque, ao achar uma solução em outro idioma, certamente você deve entender a parte da codificação, a qual, normalmente é a maior parte.

Na década de noventa, quando a Internet era só um bebê (engatinhando, cagando, chorando e comendo sem parar), a única forma para achar as respostas para nossas perguntas era utilizar a ajuda da ferramenta:

Dica #4: Utilize a ajuda da ferramenta

Muita gente desconhece o poder da tecla F1. Eu não sei se isso é decorrente da evolução do Windows ou se é simples desconhecimento, mas desde o Windows '95, talvez bem antes disso, ficou padronizada a tecla F1 como tecla de ajuda. Quando você tiver um código aberto no Delphi e não entender algum método, coloque o cursor em cima desse método e pressione F1. Se você tiver um pouco de sorte, magicamente a ajuda vai aparecer explicando do que aquele método se trata. A partir da ajuda do Delphi você pode tomar conhecimento de outras funções que fazem coisas semelhantes ou complementares, expandindo assim ainda mais seu conhecimento acerca da linguagem. Eu considero a ajuda do Delphi a fonte inicial de informações para desenvolvedores iniciantes, porque nem tudo existe lá, mas certamente existem tópicos básicos, intermediários e alguns avançados.

Como se pode ver, o importante é usar todos os recursos disponíveis da melhor forma possível e com "melhor forma possível", entenda, dentre outras coisas, saber quando parar de buscar a informação e, principalmente quando não parar de buscar:

Dica #5: Ao usar o Google, não pare na primeira página. Persista um pouco mais...

Existe uma anedota que fala que se você não achar o que procura no Google na primeira página, pode desistir porque o que você procura não existe. Como eu falei, isso é apenas uma anedota, mas as pessoas insistem em assumir como verdade. O Google é uma ferramenta muito poderosa, mas o que ele lista, muitas vezes não depende somente dele. Sites mal indexados podem não aparecer na primeira página de respostas e você pode estar perdendo de achar o que procura, simplesmente por preguiça. Insista! Não negligencie os resultados das páginas com número maior que 1

Depois de falar estas cinco dicas eu quase ia esquecendo de mais uma que é a motivadora de todas as outras:

Dica #6: Seja curioso e questionador!

Todos os meios utilizados para encontrar a solução de um problema dependem da curiosidade do programador. Se você é um programador e nunca se perguntou a respeito de coisas que um programa qualquer faz então você tem sérios problemas. Se você nunca questionou algo relacionado a forma como algum programa foi feito, da mesma forma, você estará fadado ao insucesso. Não ser curioso e nem questionador só vai mesmo tornar você um programador preguiçoso, que só espera pelos outros para resolver seus problemas e, pode acreditar, nem sempre aquele seu colega esperto vai estar disponível. Aprenda a andar com suas próprias pernas e corra atrás do que precisa!

 



1 Se houver alguma resposta lá, provavelmente fui eu mesmo quem respondeu, porque eu consgui resolver esse problema ;) Além da pergunta para o problema inicial, existe outra que eu fiz após ter tido mais pistas e, mesmo nela, as respostas eram padronizadas (usavam uma suposta solução que não funcionava)