março 2010
D S T Q Q S S
« fev    
 123456
78910111213
14151617181920
21222324252627
28293031  

Imagens NRG (Nero) no Linux

Foi uma surpresa pra mim quando estava pesquisando em como lidar com uma imagem do Nero (extensão NRG) no Linux, de que este tipo de arquivo é muito simples.

A imagem nada mais é do que um cabeçalho de 300KB proprietário do Nero seguido da imagem ISO do disco. Seguindo esta idéia, podemos extrair a imagem ISO do arquivo NRG facilmente utilizando o dd:

dd bs=1k skip=300 if=imagem.nrg of=imagem.iso

Isto irá copiar o arquivo imagem.nrg para imagem.iso, ignorando os 300 primeiros blocos de 1KB cada.

Podemos também montar a imagem no formato NRG diretamente utilizando o módulo de loop-back, pois o comando mount também permite que se ignore os 300KB iniciais (300 * 1024 = 307200 bytes), utilizando o parâmetro offset:

mount -o loop,ro,offset=307200 imagem.nrg /mnt

Provavelmente imagens com algum tipo de proteção contra cópia, ou outros formatos não padrões de ISO devem apresentar problemas, mas como estas imagens são raras, estas simples soluções resolvem o problema.

Reset de formulário turbinado

Quem nunca fez um formulário com um select, um radio ou um checkbox que mostra ou esconde outro trecho do formulário? Isto é uma prática bem comum, mas que o método reset () do formulário não tem como tratar.

Pesquisando um pouco, encontrei as propriedades defaultValue, defaultSelected e defaultChecked nos objetos de formulário do DOM. Com isso, podemos simular o método reset () do formulário. A propriedade defaultValue é válida para elementos do tipo text, password, textarea e file, a propriedade defaultSelected é válida para elementos option, e a propriedade defaultChecked é válida para elementos checkbox e radio.

Com isso, podemos criar facilmente uma função para resetar os valores de um formulário para o seu padrão:

function formReset ( formID)
{
  // Fazemos um laço por todos os elementos do formulário:
  for ( var x = 0; x < formID.elements.length; x++)
  {
    // Verificamos o tipo de elemento:
    switch ( formID.elements[x].type)
    {
      case 'text':
      case 'textarea':
      case 'password':
      case 'file':
        // Campos tipo text, textarea, password e file utilizam defaultValue:
        formID.elements[x].value = formID.elements[x].defaultValue;
        break;
      case 'select':
      case 'select-one':
        // Campos tipo select são tratados de forma especial. Temos que criar outro laço em seus elementos option para utilizar defaultSelected:
        for ( var y = 0; y < formID.elements[x].options.length; y++)
        {
          formID.elements[x].options[y].selected = formID.elements[x].options[y].defaultSelected;
        }
        break;
      case 'radio':
      case 'checkbox':
        // Campos tipo radio e checkbox utilizam defaultChecked:
        formID.elements[x].checked = formID.elements[x].defaultChecked;
        break;
    }
  }
}

Você deve estar pensando: Tá, e daí? Isso o método reset () faz! Bom, é aqui que podemos começar a brincar com o reset do formulário. Para começar, podemos dentro do laço fazer com que ele verifique se o elemento possui algum evento onchange, e caso exista, execute o mesmo após alterar o valor para o padrão. Ou seja, irá simular como se o usuário estivesse alterando o valor do formulário.

Podemos fazer isso assim:

if ( typeof ( formID.elements[x].onchange) == 'function')
{
  formID.elements[x].onchange ();
}

Com isso podemos incluir todo tratamento necessário do formulário no evento onchange do elemento, para que quando resetarmos o formulário através da função formReset () ele execute estas funções e retorne o formato original do formulário.

Mas não podemos executar o evento onchange de cada elemento, pois se o valor não foi alterado, não devemos executar. Isso serve principalmente para eventos checkbox. Por isso, temos que reescrever a função, ficando assim:

function formReset ( formID)
{
  // Fazemos um laço por todos os elementos do formulário:
  var changed = false;
  for ( var x = 0; x < formID.elements.length; x++)
  {
    // Verificamos o tipo de elemento:
    switch ( formID.elements[x].type)
    {
      case 'text':
      case 'textarea':
      case 'password':
      case 'file':
        // Campos tipo text, textarea, password e file utilizam defaultValue:
        if ( formID.elements[x].value != formID.elements[x].defaultValue)
        {
          formID.elements[x].value = formID.elements[x].defaultValue;
        }
        break;
      case 'select':
      case 'select-one':
        // Campos tipo select são tratados de forma especial. Temos que criar outro laço em seus elementos option para utilizar defaultSelected:
        for ( var y = 0; y < formID.elements[x].options.length; y++)
        {
          if ( formID.elements[x].options[y].selected != formID.elements[x].options[y].defaultSelected)
          {
            formID.elements[x].options[y].selected = formID.elements[x].options[y].defaultSelected;
          }
        }
        break;
      case 'radio':
      case 'checkbox':
        // Campos tipo radio e checkbox utilizam defaultChecked:
        if ( formID.elements[x].checked != formID.elements[x].defaultChecked)
        {
          formID.elements[x].checked = formID.elements[x].defaultChecked;
        }
        break;
    }
    // Se o elemento teve o valor alterado, executa o método onchange () (se definido):
    if ( changed == true && typeof ( formID.elements[x].onchange) == 'function')
    {
      formID.elements[x].onchange ();
      changed = false;
    }
  }
}

Podemos ainda incrementar um pouco mais, fazendo com que o foco seja apontado para o primeiro campo que seja do tipo text, select, password, radio ou checkbox:

function formReset ( formID)
{
  // Fazemos um laço por todos os elementos do formulário:
  var focusElement;
  var changed = false;
  for ( var x = 0; x < formID.elements.length; x++)
  {
    // Verificamos se ainda não existe elemento para foco setado, e o tipo de elemento. Se combinado, seta a variável:
    if ( ! focusElement && ( formID.elements[x].type == 'text' || formID.elements[x].type == 'select' || formID.elements[x].type == 'password' || formID.elements[x].type == 'checkbox' || formID.elements[x].type == 'radio'))
    {
      focusElement = formID.elements[x];
    }
    // Verificamos o tipo de elemento:
    switch ( formID.elements[x].type)
    {
      case 'text':
      case 'textarea':
      case 'password':
      case 'file':
        // Campos tipo text, textarea, password e file utilizam defaultValue:
        if ( formID.elements[x].value != formID.elements[x].defaultValue)
        {
          formID.elements[x].value = formID.elements[x].defaultValue;
        }
        break;
      case 'select':
      case 'select-one':
        // Campos tipo select são tratados de forma especial. Temos que criar outro laço em seus elementos option para utilizar defaultSelected:
        for ( var y = 0; y < formID.elements[x].options.length; y++)
        {
          if ( formID.elements[x].options[y].selected != formID.elements[x].options[y].defaultSelected)
          {
            formID.elements[x].options[y].selected = formID.elements[x].options[y].defaultSelected;
          }
        }
        break;
      case 'radio':
      case 'checkbox':
        // Campos tipo radio e checkbox utilizam defaultChecked:
        if ( formID.elements[x].checked != formID.elements[x].defaultChecked)
        {
          formID.elements[x].checked = formID.elements[x].defaultChecked;
        }
        break;
    }
    // Se o elemento teve o valor alterado, executa o método onchange () (se definido):
    if ( changed == true && typeof ( formID.elements[x].onchange) == 'function')
    {
      formID.elements[x].onchange ();
      changed = false;
    }
  }
  // Antes de encerrar, caso exista um campo escolhido para foco, executa o método focus () do mesmo:
  if ( focusElement)
  {
    focusElement.focus ();
  }
}

E para finalizar, vamos fazer uma última implementação. Que tal substituir o método reset () da classe HTMLFormElement do DOM para que todos os formulários do documento utilizem a função? Simples. Basta declarar a função assim (AVISO: apenas navegadores compatíveis como padrão W3C conseguem extender esta classe.):

// Altera o método reset () para a nossa própria classe:
HTMLFormElement.prototype.reset = function ()
{
  // Fazemos um laço por todos os elementos do formulário:
  var focusElement;
  var changed = false;
  for ( var x = 0; x < this.elements.length; x++)
  {
    // Verificamos se ainda não existe elemento para foco setado, e o tipo de elemento. Se combinado, seta a variável:
    if ( ! focusElement && ( this.elements[x].type == 'text' || this.elements[x].type == 'select' || this.elements[x].type == 'password' || this.elements[x].type == 'checkbox' || this.elements[x].type == 'radio'))
    {
      focusElement = this.elements[x];
    }
    // Verificamos o tipo de elemento:
    switch ( this.elements[x].type)
    {
      case 'text':
      case 'textarea':
      case 'password':
      case 'file':
        // Campos tipo text, textarea, password e file utilizam defaultValue:
        if ( this.elements[x].value != this.elements[x].defaultValue)
        {
          this.elements[x].value = this.elements[x].defaultValue;
          changed = true;
        }
        break;
      case 'select':
      case 'select-one':
        // Campos tipo select são tratados de forma especial. Temos que criar outro laço em seus elementos option para utilizar defaultSelected:
        for ( var y = 0; y < this.elements[x].options.length; y++)
        {
          if ( this.elements[x].options[y].selected != this.elements[x].options[y].defaultSelected)
          {
            this.elements[x].options[y].selected = this.elements[x].options[y].defaultSelected;
            changed = true;
          }
        }
        break;
      case 'radio':
      case 'checkbox':
        // Campos tipo radio e checkbox utilizam defaultChecked:
        if ( this.elements[x].checked != this.elements[x].defaultChecked)
        {
          this.elements[x].checked = this.elements[x].defaultChecked;
          changed = true;
        }
        break;
    }
    // Se o elemento teve o valor alterado, executa o método onchange () (se definido):
    if ( changed == true && typeof ( this.elements[x].onchange) == 'function')
    {
      this.elements[x].onchange ();
      changed = false;
    }
  }
  // Antes de encerrar, caso exista um campo escolhido para foco, executa o método focus () do mesmo:
  if ( focusElement)
  {
    focusElement.focus ();
  }
}

Isso resolve em partes, pois quando um reset é acionado como evento, a função não é executada. Para isso, devemos capturar o evento reset e alterar um pouco a função, para que ela referencie o formulário através da propriedade target do evento ou da palavra-chave this. O script final fica assim:

// Altera o método reset () para a nossa própria classe:
HTMLFormElement.prototype.reset = function ( event)
{
  // Verificamos como a função foi executada, setando a variável target:
  var target = event ? event.target : this;

  // Fazemos um laço por todos os elementos do formulário:
  var focusElement;
  var changed = false;
  for ( var x = 0; x < target.elements.length; x++)
  {
    // Verificamos se ainda não existe elemento para foco setado, e o tipo de elemento. Se combinado, seta a variável:
    if ( ! focusElement && ( target.elements[x].type == 'text' || target.elements[x].type == 'select' || target.elements[x].type == 'password' || target.elements[x].type == 'checkbox' || target.elements[x].type == 'radio'))
    {
      focusElement = target.elements[x];
    }
    // Verificamos o tipo de elemento:
    switch ( target.elements[x].type)
    {
      case 'text':
      case 'textarea':
      case 'password':
      case 'file':
        // Campos tipo text, textarea, password e file utilizam defaultValue:
        if ( target.elements[x].value != target.elements[x].defaultValue)
        {
          target.elements[x].value = target.elements[x].defaultValue;
          changed = true;
        }
        break;
      case 'select':
      case 'select-one':
        // Campos tipo select são tratados de forma especial. Temos que criar outro laço em seus elementos option para utilizar defaultSelected:
        for ( var y = 0; y < target.elements[x].options.length; y++)
        {
          if ( target.elements[x].options[y].selected != target.elements[x].options[y].defaultSelected)
          {
            target.elements[x].options[y].selected = target.elements[x].options[y].defaultSelected;
            changed = true;
          }
        }
        break;
      case 'radio':
      case 'checkbox':
        // Campos tipo radio e checkbox utilizam defaultChecked:
        if ( target.elements[x].checked != target.elements[x].defaultChecked)
        {
          target.elements[x].checked = target.elements[x].defaultChecked;
          changed = true;
        }
        break;
    }
    // Se o elemento teve o valor alterado, executa o método onchange () (se definido):
    if ( changed == true && typeof ( target.elements[x].onchange) == 'function')
    {
      target.elements[x].onchange ();
      changed = false;
    }
  }
  // Antes de encerrar, caso exista um campo escolhido para foco, executa o método focus () do mesmo:
  if ( focusElement)
  {
    focusElement.focus ();
  }
}

// Captura o evento onreset de todos os formulários do documento:
window.addEventListener ( 'reset', HTMLFormElement.prototype.reset, true);

Adicionei um formulário de testes aqui. Você também pode baixar o script aqui.

Javascript insertAfter()

O Javascript possui a função insertBefore(), mas não possui uma função similar para inserir após o objeto como é de se esperar que existisse uma chamada insertAfter(), mas podemos inserir um elemento após um determinado objeto utilizando a insertBefore():

function insertAfter ( newObj, target)
{
  target.parentNode.insertBefore ( newObj, target.nextSibling);
}

Para entender esta função, vamos primeiramente ver a sintaxe do comando insertBefore():

  • Função: insertBefore()
  • Descrição: Insere um elemento ao final do documento ou após um elemento especificado.
  • Sintaxe: elemento.insertBefore ( param1, param2)
  • Parâmetros:
    • param1: REQUERIDO – Novo elemento a ser adicionado.
    • param2: OPCIONAL – Elemento que se especificado o novo elemento (param1) será inserido após este. Se omitido, inserere no final.

Como a função insertAfter() que foi criada informa o “target”.nextSibling como segundo parâmetro, quando não ouver um nextSibling no elemento, será retornado nulo, e fará com que a função insertBefore() insira o elemento no final.

Podemos extender o DOM e adicionar esta função a todos os elementos, utilizando o seguinte código:

Element.prototype.insertAfter = function ( newObj)
{
  this.parentNode.insertBefore ( newObj, this.nextSibling);
}

Desta forma, podemos utilizar o comando insertAfter() como utilizamos o comando insertBefore(), tal como:

document.getElementById ( 'alvo').insertAfter ( objeto);

Para testar, .

O teste irá incluir após este elemento.

Simples e prático.

Recuperação de arquivos

Quem nunca apagou um arquivo por acidente, teve seu disco danificado por um vírus ou danificou sem querer a tabela de partição? Em sistema de arquivos FAT ou NTFS a recuperação é relativamente simples e fácil. Já no Linux com EXT2/EXT3, ReiserFS e outros, já não é tão simples assim.

Depois de ter removido um arquivo de uma pen drive por acidente, pesquisei rapidinho no Google uma solução em Linux e acabei descobrindo o TestDisk, uma ferramenta simples e muito poderosa que além de recuperar arquivos removidos de quaisquer dispositivo de armazenamento (disco, pen drive, SD Card, etc…), pode recuperar tabela de partições, localizar partições perdidas no disco e outras diversas ferramentas que ajudam e muito na hora do desespero.

O melhor de tudo é que está disponível para Linux, Windows (NT4, 2000, XP, 2003 e Vista), DOS (nativo ou em janela no Windows 9x), MaxOS X e OS2. É um software livre sob licença GPL, com fontes disponíveis, podendo ser executado também em sistemas BSD ou SunOS.

Um amigo testou ele no Windows Vista, e teve alguns problemas com a disposição do texto na tela, mas fora isso, funcionou normalmente.

Ele possui também um outro aplicativo voltado para a recuperação de imagens perdidas de cartões de memória de máquinas digitais, com suporte a EXIF, reconhecimento de arquivos JPEG, entre outras funções muito úteis, chamado PhotoRec.

É um ótimo aplicativo para termos sempre a mão naquela pen drive de utilitários que virou chaveiro, até porque ele não precisa ser instalado no Windows, basta executar diretamente do diretório.

Sony Ericsson MD300

Esses Modem 3G USB Sony Ericsson MD300 dias um cliente me deixou um modem USB Claro 3G modelo Sony Ericsson MD300 para utilizar em uma viagem. O modem é um pouco grande se comparado ao Huawei E220, além de ter o incômodo de ocupar a porta USB próxima a conectada, por ser muito largo. Ao contrário do Huawei, tive um pouco de dificuldade para configurar ele no meu Slackware, até descobrir que o modem tem o rádio desativado por padrão (o que não é nem um pouco lógico para um modem 3G, mas foi a implementação do fabricante). Para que o modem funcione no Linux, deve-se fazer com que o udev identifique corretamente o dispositivo, e na inicialização antes da discagem, ativar o rádio.

Para que o Linux reconheça o modem como dispositivo Modem USB, crie o arquivo /etc/udev/rules.d/50-md300modem.rules com o seguinte conteúdo:

ACTION!="add", GOTO="3G_End"
BUS=="usb", SYSFS{idProduct}=="d0cf", SYSFS{idVendor}=="0fce", NAME="%k",SYMLINK="modem3G-%n", PROGRAM="/bin/sh -c 'echo 3 &gt; /sys/%p/device/bConfigurationValue'"
LABEL="3G_End"

Não esqueça de reinicializar o serviço do udev. Em sistemas BSD Like, utilize o comando “/etc/rc.d/rc.udev reload”, e para RedHat, Ubuntu, e outros, utilize “service udev restart”. Depois disso, configure o modem para utilizar as seguintes strings de inicialização:

  1. ATZ (Reseta configuração do modem)
  2. AT+CFUN=1 (Habilita rádio do modem)
  3. ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0 (Outras configurações diversas)
  4. AT+CGDCONT=1,”IP”,”bandalarga.claro.com.br” (Seleciona operadora Claro 3G)

Agora é só configurar para discar o número *99***1#, autenticando com o usuário “claro” e a senha “claro”. Você pode utilizar o gnome-ppp, vmdial, entre outros utilizando as informações acima. Qualquer dúvida, poste o seu comentário.

Autenticação segura utilizando AJAX

Um grande problema que sempre existiu foi a autenticação via formulário, onde o usuário envia o seu nome de usuário e senha de forma “texto puro” na rede, possibilitando que quaisquer pessoa que tenha acesso ao tráfego web ou à aplicação no servidor tenha acesso a estas informações. O ideal é que um sistema não tenha acesso a senha do usuário, e normalmente armazena um hash criptográfico da mesma, mas mesmo assim, ele necessita receber a senha para validar a autenticação.

Para evitar esse problema, pode-se utilizar um serviço HTTP com criptografia (HTTPS), mas mesmo assim ainda não resolve o problema de a aplicação possuir a senha do usuário. Pensando nisso, desenvolvi uma solução utilizando AJAX, onde a autenticação é segura através de utilização de um “desafio”.

Para implementar este método de autenticação onde nenhuma informação sigilosa é trafegada em forma de texto puro na rede (nem mesmo hash da senha), devemos seguir os seguintes passos:

  1. O cliente requisita um desafio ao servidor utilizando AJAX, para manter o conteúdo atual do formulário e página;
  2. O servidor retorna um desafio, que é uma string randômica e única. Este desafio fica armazenado no servidor tendo uma validade de alguns segundos;
  3. O cliente calcula um hash com a concatenação do desafio com o hash da senha informada pelo usuário. O resultado então é enviado para o servidor juntamente com o nome do usuário;
  4. O servidor verifica na base qual é o último desafio enviado para o cliente (atendendo o tempo de expiração), concatena o mesmo com o hash da senha que possui no banco de dados, e compara o hash resultante desta concatenação com o valor enviado pelo cliente (que é o mesmo hash, do desafio concatenado com o hash da senha informada pelo usuário);
  5. Coincidindo os valores, a autenticação é bem sucedida.

Este processo faz com que a senha nunca seja exposta, nem mesmo o hash dela, pois é enviado apenas um hash do desafio (texto randômico e único gerado pelo servidor) com o hash real da senha. Como a cada autenticação o desafio criado pelo servidor é diferente, o resultado do hash será sempre diferente, fazendo com que seja impossível se obter alguma informação da senha.

O maior problema deste método de autenticação é que a senha na base de dados deve utilizar um único sistema de hash padronizado, ou serem mantidas em “texto puro”, o que é extremamente desaconselhado. Já vi algumas bases de autenticação que utilizavam SHA1 e MD5 misturados. Neste tipo de base este sistema de autenticação não funcionaria.

O processo é um pouco difícil de se entender, então vamos implementar passo a passo.

Primeiro precisamos escolher um método de hash a ser utilizado tanto no cliente quanto no servidor para criar a autenticação e o hash da senha em si. Existem diversas bibliotecas para JavaScript, sendo as que eu recomendo:

Utilizei por muito tempo o MD5, mas atualmente substitui pelo SHA2 de 256 bits por ser mais seguro e ser suportado por todas linguagens que utilizo. Por isto, o exemplo feito aqui utilizará ele como base. Você pode alterar para outros algorítimos facilmente se desejar.

O hash gerado pelo SHA2 de 256 bits possui 32 bytes em sua forma binária, mas utilizaremos ele em hexadecimal, que ocupa 64 bytes, para evitar problemas com caracteres especiais. Lembre-se que você deve armazenar o hash da senha no servidor utilizando o mesmo método utilizado no navegador do cliente.

Primeiramente, vamos criar uma função para retornar um objeto XMLHTTP:

function createXMLHTTP ()
{
  // Navegadores Mozilla/Safari:
  if ( window.XMLHttpRequest)
  {
    xmlHttpReq = new XMLHttpRequest ();
    xmlHttpReq.overrideMimeType ( 'text/xml');
  } else {
    // Navegadores IE:
    if ( window.ActiveXObject)
    {
      // O IE utiliza Msxml2.XMLHTTP:
      var ActiveXName = 'Msxml2.XMLHTTP';

      // O IE 5.5 ou superior utiliza Microsoft.XMLHTTP:
      if ( navigator.appVersion.indexOf ( 'MSIE 5.5') >= 0)
      {
        ActiveXName = 'Microsoft.XMLHTTP';
      }

      // Tenta instanciar:
      try
      {
        xmlHttpReq = new ActiveXObject ( ActiveXName);
      }

      // Avisa que o ActiveX está desabilitado:
      catch ( e)
      {
        alert ( 'Suporte a script no ActiveX deve ser habilitado.');
        return false;
      }
    } else {
      // Quaisquer outro navegador não é suportado:
      alert ( 'O seu navegador não é suportado por este aplicativo.');
      return false;
    }
  }

  return xmlHttpReq;
}

Então implementamos o código JavaScript que cria o objeto XMLHTTP (AJAX) e requisita o desafio ao servidor:

function getChallenge ( username)
{
  var xmlHttpReq = false;

  // Instancia um novo objeto XMLHTTP:
  xmlHttpReq = createXMLHTTP ();
  if ( ! xmlHttpReq)
  {
    // Retorna falso:
    return false;
  }

  // Insere os valores no objeto e envia:
  xmlHttpReq.open ( 'POST', 'auth.php', false);
  xmlHttpReq.setRequestHeader ( 'Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
  xmlHttpReq.send ( 'usuario=' + escape ( username));

  // Verifica se ocorreu erro (código HTTP diferente de 200):
  if ( xmlHttpReq.status != 200)
  {
    // Mostra uma mensagem de erro:
    alert ( "Ocorreu um erro ao autenticar no sistema.");

    // Retorna falso:
    return false;
  }

  // Verifica se o valor retornado é válido (possui 64 bytes):
  if ( xmlHttpReq.responseText.length != 64)
  {
    // Mostra uma mensagem de erro:
    alert ( "Ocorreu um erro ao autenticar no sistema.");

    // Retorna falso:
    return false;
  }

  // Retorna o valor randômico recebido do servidor:
  return xmlHttpReq.responseText;
}

Precisamos também de uma função para enviar o resultado da autenticação para o servidor:

function doLogin ( username, hash)
{
  var xmlHttpReq = false;

  // Instancia um novo objeto XMLHTTP:
  xmlHttpReq = createXMLHTTP ();

  // Insere os valores no objeto e envia:
  xmlHttpReq.open ( 'POST', 'auth.php', false);
  xmlHttpReq.setRequestHeader ( 'Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
  xmlHttpReq.send ( 'usuario=' + escape ( username) + '&hash=' + escape ( hash));

  // Verifica se ocorreu erro (código HTTP diferente de 200):
  if ( xmlHttpReq.status != 200)
  {
    // Mostra uma mensagem de erro:
    alert ( "Ocorreu um erro ao autenticar no sistema.");

    // Retorna falso:
    return false;
  }

  // Retorna o resultado:
  return xmlHttpReq.responseText;
}

Finalmente, precisamos de uma função para gerenciar a autenticação, ou seja, requisitar o desafio, criar o hash resultante do desafio e hash da senha e enviar ao servidor. Para isto, criamos a função de autenticação em JavaScript:

function handleLogin ( form)
{
  // Recupera informações da autenticação do formulário informado:
  username = document.getElementById('login_username').value;
  password = document.getElementById('login_password').value;

  // Retorna caso não seja informado o usuário:
  if ( ! username)
  {
    return false;
  }

  // Desabilita o botão de envio de formulário para evitar envio duplicado:
  document.getElementById('ok_button').disabled = true;
  document.getElementById('ok_button').value = 'Autenticando...';

  // Requisita o desafio:
  challenge = getChallenge ( username);

  // Envia requisição de autenticação:
  login = doLogin ( username, hex_sha256 ( hex_sha256 ( password) + challenge), challenge);

  // Verifica resultado:
  if ( login == '1')
  {
    // Autenticação bem sucedida:
    alert ( 'Autenticado como "' + username + '"!');
  } else {
    // Autenticação falhou. Exibe mensagem de erro:
    alert ( 'Falha na autenticação!');

    // Limpa o campo de senha:
    document.getElementById('login_password').value = "";

    // Seta o foco do navegador para o campo de senha:
    document.getElementById('login_password').focus ();
  }

  // Habilita o botão de autenticação do sistema:
  document.getElementById('ok_button').disabled = false;
  document.getElementById('ok_button').value = 'Autenticar';

  // Retorna falso:
  return false;
}

No lado do servidor criamos o código PHP para realizar a geração de desafios e autenticação no sistema. O desafio gerado será uma string de 64 bytes, um hash SHA256 de um número randômico. Este será o arquivo “auth.php”:

<?php
// Primeiro, precisamos conectar ao banco de dados:
if ( ! $_sql_id = @mysql_connect ( "localhost", "myuser", "mypass"))
{
  die ( "Erro: Não foi possível conectar ao banco de dados.");
}

// Selecionamos a base de dados padrão da conexão:
if ( ! @mysql_select_db ( "mydb", $_sql_id))
{
  die ( "Erro: Não foi possível selecionar a base de dados.");
}

// Verificamos se a requisição possui senha (se não possui, é uma requisição de desafio):
if ( empty ( $_POST["senha"]))
{
  // Criamos o hash randômico de 64 bytes:
  $desafio = bin2hex ( mhash ( MHASH_SHA256, uniqid ( rand ())));

  // Verificamos se o usuário informado existe, e recuperamos a informação do banco de dados:
  if ( ! $result = @mysql_query ( "SELECT * FROM `usuarios` WHERE `Usuario` = '" . mysql_real_escape_string ( $_POST["usuario"], $_sql_id) . "'", $_sql_id))
  {
    // Atenção: Não devemos nunca informar se um usuário existe ou não, por isto, retornaremos um hash como se a operação fosse bem sucedida:
    die ( $desafio);
  }

  // Verificamos se foi localizada a informação:
  if ( mysql_num_rows ( $result) != 1)
  {
    // Leia o comentário do "if" anterior.
    die ( $desafio);
  }

  // Recuperamos a informação para um array e liberamos a memória:
  $usuario = mysql_fetch_array ( $result);
  mysql_free_result ( $result);

  // Guardamos o desafio gerado na base de dados:
  @mysql_query ( "INSERT INTO `sessoes` (`Usuario`, `Desafio`, `Validade`, `IP`) VALUES (" . (int) $usuario["ID"] . ", '" . mysql_real_escape_string ( $desafio, $_sql_id) . "', " . ( time () + 30) . ", INET_ATON('" . mysql_real_escape_string ( $_SERVER["REMOTE_ADDR"], $_sql_id) . "'))", $_sql_id);

  // Retorna-se o desafio (mesmo que a gravação na base de dados tenha ocorrido erro):
  die ( $desafio);
} else {
  // É uma autenticação completa, ou seja, enviado usuário e hash da senha concatenada com o desafio.
  // Primeiramente, verificamos o último desafio enviado para o IP do cliente que ainda esteja válido:
  if ( ! $result = @mysql_query ( "SELECT * FROM `sessoes` WHERE `IP` = INET_ATON('" . mysql_real_escape_string ( $_SERVER["REMOTE_ADDR"], $_sql_id) . "') AND `Validade` >= " . time () . " ORDER BY ID DESC LIMIT 0, 1", $_sql_id))
  {
    // Retornamos sempre autenticação inválida (nenhuma mensagem de erro):
    die ( "0");
  }
  // Verifica se foi retornado algum valor:
  if ( mysql_num_rows ( $result) != 1)
  {
    // Retornamos sempre autenticação inválida (nenhuma mensagem de erro):
    die ( "0");
  }

  // Armazenamos a informação do desafio válido para concatenarmos com o hash da senha:
  $desafio = mysql_fetch_array ( $result);
  mysql_free_result ( $result);

  // Requisitamos a informação do usuário informado:
  if ( ! $result = @mysql_query ( "SELECT * FROM `usuarios` WHERE `Usuario` = '" . mysql_real_escape_string ( $_POST["usuario"], $_sql_id) . "'", $_sql_id))
  {
    die ( "0");
  }
  // Verificamos se foi retornado um valor válido:
  if ( mysql_num_rows ( $result) != 1)
  {
    // Retorna autenticação inválida (não informa que o usuário não existe):
    die ( "0");
  }
  $usuario = mysql_fetch_array ( $result);
  mysql_free_result ( $result);

  // Agora que temos a informação do último desafio válido enviado para este cliente, e a informação do usuário, fazemos a verificação do hash enviado:
  if ( bin2hex ( mhash ( MHASH_SHA256, $usuario["Senha"] . $desafio["Desafio"])) != $_POST["hash"])
  {
    // Se a comparação falhou, a senha informada é inválida:
    die ( "0");
  }

  // Temos a confirmação da senha enviada através do hash concatenado com o desafio. Então eliminamos o desafio da base de dados e criamos a sessão do usuário.

  // Criamos o hash randômico de 64 bytes para a cookie:
  $cookie = bin2hex ( mhash ( MHASH_SHA256, uniqid ( rand ())));

  // Alteramos o registro contendo o desafio para cookie:
  if ( ! @mysql_query ( "UPDATE `sessoes` SET `Desafio` = '', `Cookie` = '" . mysql_real_escape_string ( $cookie, $_sql_id) . "', `Validade` = " . ( time () + 3600) . " WHERE `IP` = INET_ATON('" . mysql_real_escape_string ( $_SERVER["REMOTE_ADDR"], $_sql_id) . "') AND `Desafio` = '" . mysql_real_escape_string ( $desafio["Desafio"], $_sql_id) . "' AND `Usuario` = " . (int) $usuario["ID"], $_sql_id))
  {
    die ( "0");
  }

  // Retornamos a cookie para o cliente juntamente com a confirmação de autenticação:
  setcookie ( "myapp", $cookie, 0, dirname ( $_SERVER["PHP_SELF"]), $_SERVER["HTTP_HOST"]);
  die ( "1");
}
?>

Para a base de dados, utilizaremos a seguinte estrutura de tabelas:

CREATE TABLE `sessoes` (
  `ID`        bigint unsigned NOT NULL auto_increment,
  `Usuario`   bigint unsigned NOT NULL,
  `Desafio`   char(64) NOT NULL,
  `Cookie`    char(64) NOT NULL,
  `Validade`  int unsigned NOT NULL,
  `IP`        int unsigned NOT NULL,
  PRIMARY KEY (`ID`),
  KEY         `Cookie` (`Cookie`),
  KEY         `ID` (`ID`),
  KEY         `Usuario` (`Usuario`),
  KEY         `Validade` (`Validade`),
  KEY         `IP` (`IP`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `usuarios` (
  `ID`        bigint unsigned NOT NULL auto_increment,
  `Usuario`   varchar(255) NOT NULL,
  `Senha`     char(64) NOT NULL,
  PRIMARY KEY (`ID`),
  KEY         `ID` (`ID`),
  KEY         `Usuario` (`Usuario`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Esta tabela será utilizada tanto para arquivar o hash randômico, quanto para arquivar o hash de sessão, que é uma string randômica única gerada para o cliente como chave da sessão, e enviada através de cookie. Sempre que uma cookie do sistema é recebida, verificamos nesta tabela o IP do cliente, a cookie e se a mesma ainda está dentro do tempo de expiração da sessão do sistema, sendo que desta informação temos o ID do usuário, que é armazenado na tabela de usuários. Este procedimento é muito similar ao utilizado internamente pelo PHP para manipulação de variáveis de sessão, mas implementado manualmente. As variáveis de sessão não podem ser consideradas seguras, visto que são armazenadas em “texto puro” dentro de arquivos temporários que são compartilhados no servidor com outras aplicações, ou seja, se você gravar dados sensíveis nas variáveis de sessão, outros aplicativos podem vir a ter acesso a esta informação. Veja o artigo Segurança de Sessões em PHP para maiores detalhes. A entrada nunca poderá ter os valores de Desafio e Cookie simultaneamente, assim que autenticado, o registro perde o valor de ‘Desafio’ e cria-se um valor para Cookie da sessão.

Para o teste, adicionei dois usuários. O usuário “usuario” com a senha “teste” e o usuário “admin” com a senha “admin”:

 INSERT INTO `usuarios` (`Usuario`, `Senha`) VALUES ('usuario', '46070d4bf934fb0d4b06d9e2c46e346944e322444900a435d7d9a95e6d7435f5');
 INSERT INTO `usuarios` (`Usuario`, `Senha`) VALUES ('admin', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918');

Para testar o sistema de autenticação, criei este exemplo.

Caso você deseje, pode fazer o download dos códigos deste artigo aqui.

Este código está sendo disponibilizado sob licença GPL versão 3. Caso você utilize este processo de autenticação em algum software, por favor me informe qual o sistema implementado, linguagem utilizada e se possuo permissão para postar uma referencia ao seu software neste artigo.

Fonetização

A função de fonetização para nomes já é utilizada a muito tempo em sistemas de grande porte para facilitar a localização de cadastros pelo nome. É extremamente útil quando um atendente está precisando localizar algum cadastro, para que não necessite ficar perguntando “Christine com H?”, “Daniela ou Daniele?”, etc… Ele é de simples utilização, bastando-se criar um campo adicional na base de dados contendo a forma fonética do nome, passando pela função de fonetização, assim como o termo pesquisado.

Fiz uma pesquisa bem extensa na internet e não consegui localizar nenhum código disponível em PHP que fosse realmente útil. Pesquisando um pouco mais a fundo, consegui localizar um código em Java, disponibilizado pelo Serviço de Informática do Instituto Do Coração de Porto Alegre. Este código foi gentilmente fornecido pela PROCEMPA e distribuído sob licença GPL versão 2. Ele pode ser encontrado aqui. Fiz uma versão simplificada em Java, que pode ser encontrada aqui.

Baseado neste código, reescrevi o mesmo em PHP. Ficou um pouco extenso para se publicar aqui, por isso, baixe o mesmo. A versão PHP foi disponibilizada sob a licença GPL versão 3.

WP-Print em português (Brasil)

Para aqueles que utilizam o WordPress e o plugin WP-Print, disponibilizo o arquivo traduzido para o Português Brasil da versão 2.30 que fiz. Caso tenham alguma sugestão, podem enviar por email ou colocar um comentário neste artigo.

O arquivo wp-print-pt_BR.pot contém o texto puro, necessitando ser processado pelo aplicativo “msgfmt” do GNU GetText para gerar o arquivo binário com extensão .mo, que é o arquivo utilizado pelo sistema. Caso você não se interesse pelo “fonte” da tradução, basta utilizar apenas o arquivo wp-print-pt_BR.mo. É utilizado a página de caracteres UTF-8.

Para instalar, basta colocá-lo no mesmo diretório onde está instalado o plugin WP-Print.

WP-EMail em português (Brasil)

Para aqueles que utilizam o WordPress e o plugin WP-EMail, disponibilizo o arquivo traduzido para o Português Brasil da versão 2.30 que fiz. Caso tenham alguma sugestão, podem enviar por email ou colocar um comentário neste artigo.

O arquivo wp-email-pt_BR.pot contém o texto puro, necessitando ser processado pelo aplicativo “msgfmt” do GNU GetText para gerar o arquivo binário com extensão .mo, que é o arquivo utilizado pelo sistema. Caso você não se interesse pelo “fonte” da tradução, basta utilizar apenas o arquivo wp-email-pt_BR.mo. É utilizado a página de caracteres UTF-8.

Para instalar, basta colocá-lo no mesmo diretório onde está instalado o plugin WP-EMail.

Interface de sessões do PHP em MySQL

Conforme visto no artigo Funcionamento de sessões no PHP, o sistema de arquivamento em disco é inseguro. Este artigo implementa uma classe que armazena os dados em banco de dados MySQL. Isto além de possibilitar uma maior segurança dos dados de sessão, permite que uma aplicação seja portada para um sistema de balanceamento de cargas muito mais facilmente, pois independentemente de qual servidor a requisição for processada, terá acesso as variáveis de sessão de forma transparente.

Esta classe nasceu da idéia postada em um comentário nas páginas do manual do PHP, mais especificamente na página do comando session_set_save_handler(). O exemplo original foi criado por maria (arroba) junkies (ponto) jp, melhorado por klose (arroba) openriverbed (ponto) de e algumas mudanças feitas por mim.

Este comando permite que sejam especificadas funções para o processamento de sessões do PHP, informadas como parâmetros, sendo elas:

  1. open: Executada quando é iniciada a sessão através da chamada da função session_start() ou automaticamente;
  2. close: Executada ao término da execução do script, normalmente após a gravação das informações da sessão através da função write() (veja abaixo), ou da destruição da sessão através da função destroy() (veja abaixo);
  3. read: Executada sempre que a sessão for inicializada para recuperar uma string com as informações da sessão;
  4. write: Executada sempre ao término da execução do script, antes da chamada da função close() (veja acima), para arquivar as mudanças efetuadas nas variáveis de sessão. Ocorre sempre que a execução do script termina ou através da chamada da função session_write_close();
  5. destroy: Executada quando uma sessão é destruída através do comando session_destroy(), eliminando os dados da sessão e encerrando o sistema de sessões através da chamada da função close() (veja acima);
  6. gc: É executada sempre que o PHP deseja limpar as variáveis com tempo de vida expirado. É chamado de “garbage cleanup”, e é executada automaticamente pelo servidor.

Seguindo as especificações desta função, podemos implementar a classe conforme segue:

<?php
/**
 * PHP session handling with MySQL-DB
 *
 * Created on 12.03.2008
 * @license    http://www.opensource.org/licenses/cpl.php Common Public License 1.0
 */

class MySQL_Session
{
  /**
   * A database connection resource and configuration.
   * @var resource
   */
  private static $_sess_id;
  private static $_sess_host;
  private static $_sess_user;
  private static $_sess_pass;
  private static $_sess_db;
  private static $_sess_table;

  /**
   * Open the session
   * @return bool
   */
  public static function open ()
  {
    if ( self::$_sess_id = @mysql_connect ( self::$_sess_host, self::$_sess_user, self::$_sess_pass))
    {
      return @mysql_select_db ( self::$_sess_db, self::$_sess_id);
    }

    return false;
  }

  /**
   * Close the session
   * @return bool
   */
  public static function close ()
  {
    return @mysql_close ( self::$_sess_id);
  }

  /**
   * Read the session
   * @param string session id
   * @return string string of the session
   */
  public static function read ( $id)
  {
    if ($result = @mysql_query ( "SELECT `data` FROM `" . mysql_real_escape_string ( self::$_sess_table, self::$_sess_id) . "` WHERE `handler` = '" . mysql_real_escape_string ( $id, self::$_sess_id) . "'", self::$_sess_id))
    {
      if ( mysql_num_rows ( $result))
      {
        $record = mysql_fetch_assoc ( $result);
        return $record["data"];
      }
    }

    return "";
  }

  /**
   * Write the session
   * @param string session id
   * @param string data of the session
   */
  public static function write ( $id, $data)
  {
    return @mysql_query ( "REPLACE INTO `" . mysql_real_escape_string ( self::$_sess_table, self::$_sess_id) . "` VALUES ('" . mysql_real_escape_string ( $id, self::$_sess_id) . "', " . time () . ", '" . mysql_real_escape_string ( $data, self::$_sess_id) . "')", self::$_sess_id);
  }

  /**
   * Destroy the session
   * @param string session id
   * @return bool
   */
  public static function destroy ( $id)
  {
    return @mysql_query ( "DELETE FROM `" . mysql_real_escape_string ( self::$_sess_table, self::$_sess_id) . "` WHERE `handler` = '" . mysql_real_escape_string ( $id, self::$_sess_id) . "'", self::$_sess_id);
  }

  /**
   * Garbage Collector
   * @param int life time (sec.)
   * @return bool
   * @see session.gc_divisor      100
   * @see session.gc_maxlifetime 1440
   * @see session.gc_probability    1
   * @usage execution rate 1/100
   *        (session.gc_probability/session.gc_divisor)
   */
  public static function gc ( $max)
  {
    return @mysql_query ( "DELETE FROM `" . mysql_real_escape_string ( self::$_sess_table, self::$_sess_id) . "` WHERE `expires` < " . (int) ( time () - $max), self::$_sess_id);
  }

  /**
   * Class constructor function
   * @param string MySQL server hostname
   * @param string username
   * @param string password
   * @param string database name
   * @param string table name
   */
  function __construct ( $hostname = "localhost", $username = "root", $password = "", $database = "", $table = "sessions")
  {
    // Change the database configurations:
    self::$_sess_host = $hostname;
    self::$_sess_user = $username;
    self::$_sess_pass = $password;
    self::$_sess_db = $database;
    self::$_sess_table = $table;

    // Change the session handler type:
    ini_set ( "session.save_handler", "user");

    // Change the session handler functions:
    session_set_save_handler ( array ( &$this, "open"), array ( &$this, "close"), array ( &$this, "read"), array ( &$this, "write"), array ( &$this, "destroy"), array ( &$this, "gc"));

    // Be nice, return true.
    return true;
  }
}
?>

Não devemos esquecer também a estrutura da tabela MySQL necessária para o funcionamento:

CREATE TABLE IF NOT EXISTS `sessions` (
  `handler`   char(26) NOT NULL,
  `expires`   int unsigned NOT NULL default '0',
  `data`      blob,
  PRIMARY KEY (`handler`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

A ativação da classe é extremamente simples, basta se incluir o arquivo contendo a classe e instanciar um novo objeto MySQL_Session, passando como parâmetros:

  1. hostname: Endereço do servidor MySQL;
  2. username: Usuário para autenticação no banco de dados;
  3. password: Senha para autenticação;
  4. database: Nome da base de dados a ser selecionada;
  5. table: Nome da tabela a ser utilizada. Normalmente (se utilizado a estrutura SQL acima descrita sem alterações) é a tabela “sessions”.

Podemos testar a classe com o seguinte exemplo:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en_US" lang="en_US">
<head>
  <title>MySQL Session Handling Class - Example</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>

<?php
// Include class file:
require_once ( "mysql-session.class.php");

// Instance new class:
$session = new MySQL_Session ( "hostname", "username", "password", "database", "table");

if ( session_id () == "")
{
  session_start ();
}

if ( isset ( $_SESSION["counter"]))
{
  $_SESSION["counter"]++;
} else {
  $_SESSION["counter"] = 1;
}
echo "Session ID: " . session_id () . "<br />\n";
echo "Counter: " . $_SESSION["counter"] . "<br />\n";
?>

</body>
</html>

Este exemplo pode ser facilmente portado para outros bancos de dados, assim como implementado funções de criptografia, etc.

Caso você deseje, pode fazer o download do conteúdo deste artigo aqui.