Securely deliver digital products in PHP

0

Personally, I'm developing a system where the customer can make the purchase and receive a download link for the product. The system has a register area in which you can upload the ZIP file to a folder inside the server. However, I would like to find out how I can make the downloads safe, that is, other people can not download through the same file. I thought about creating a script that duplicates this original file to a public folder and renames this file to something random, of type j45287yudfre4587.zip whenever there is a purchase and releases a link so that the buyer can download, after some time it automatically delete this copy. I do not know if it's the right way or if there's a more professional way to do it.

    
asked by anonymous 25.12.2017 / 13:33

2 answers

0

You can do this:

<a href="download.php?id=id-do-arquivo-ou-alguma-coisa-que-o-identifique">Download</a>

Then in this file " download.php " you force the download of the file informed ..

Link href in php:

<a href='download.php?id={$row['file_name']}'>

Php download.php:

<?php
  // Verifique se um ID foi passado
    if(isset($_GET['ID'])) {
        // Pegue o ID$id
        $file_name= ($_GET['ID']);
        // Certifique-se de que o ID é de fato uma ID válida
    if($file_name == NULL) {
        die('O nome é inválido!');
    }
    else {
        // Conecte-se ao banco de dados
        $dbLink = new mysqli('localhost', 'root', "", 'db_name');
        if(mysqli_connect_errno()) {
            die("Falha na conexão do MySQL: ".mysqli_connect_error());
        }
         // Obtenha as informações do arquivo
        $query = "
            SELECT 'type', 'file_name', 'size', 'data'
            FROM 'arquivos'
            WHERE 'file_name' = {$file_name}";
        $result = $dbLink->query($query);

        if($result) {
            // Verifique se o resultado é válido
            if($result->num_rows == 1) {
            // Obter a linha
                $row = mysqli_fetch_assoc($result);

                header("Content-Type: ".$row['type']);
                header("Content-Length: ".$row['size']);
                header("Content-Disposition: attachment"); 
                // Imprimir dados
                echo $row['data'];
            }
            else {
                echo 'Erro! Não existe nenhum arquivo com essa ID.';
            }
            // Livre os recursos do mysqli
            @mysqli_free_result($result);
        }
        else {
            // Se houver um erro ao executar a consulta
            echo "Erro! Falha na consulta: <pre>{$dbLink->error}</pre>";
        }
        // Cechar a conexão do banco de dados
        @mysqli_close($dbLink);
    }
}
else {
    // Se nenhuma ID passou
    echo 'Erro! Nenhuma ID foi aprovada.';
}
?>

You can do this too:

<?php

$id = $_GET[id];

if ($id == "123456") {
 header('Location: http://www.site.com.br/Arquivo.rar');
  } else {
 echo "Arquivo não encontrado";
}

?>

It will identify the "? id = 123456 " in your Link and will download the File " link " Of course, you can put any id, or even more than 1 id, hence your code would have to stay that way ... link

<?php

$id = $_GET[id];

if ($id == "1") {
 header('Location: http://www.site.com.br/Arquivo1.rar');
  } elseif ($id == "2") {
 header('Location: http://www.site.com.br/Arquivo2.rar');
  } elseif ($id == "3") {
 header('Location: http://www.site.com.br/Arquivo3.rar');
}

?>
    
25.12.2017 / 14:42
1

Defining the database

Since you are using the database, you have to do something easier to maintain. Imagine a table called files:

+------------------------------------------+
|nome_arquivo varchar(100) not null unique||
|data_fim date                             |
|usuario_id  varchar (200)                 |
+------------------------------------------+ 

And a users table (you will not need a full authentication system, but it can be expanded):

+----------------------------------------------+
|id integer not null primary key auto_increment|
|nome varchar(100) not null unique             |
|token  varchar (200) not null unique          |
+----------------------------------------------+ 

Mini file access control system

So you do not need to create a login system (in the question it is not clear if you want this (but can be expanded to meet this need)) every time the user sends a new file, it will first generate a token. So we can imagine a simple formmule to do this, let's call user_token.php the file that contains this form:

<form action="gerar_token.php" method="post">
    <div>
        <label>Nome de usuário</label>
        <input type="text" name="nome">
    </div>
    <div>
        <input type="button" value="Obter token">
    </div>
</form>

In the file generate_token.php you would have to insert a new record in the users table, where the user name and a token for later access fields would be inserted. Staying like this:

<?php
$usuario_nome = null;
$usuario_token = null;

if(isset($_POST['nome'])){
    $nome = $_POST['nome'];

    //você pode ler mais sobre uniqid na referencia do php
    // em http://php.net/manual/pt_BR/function.uniqid.php
    //basicamente ela gerar um identificador unico
    //composto por caracteres alphanumericos
    $token = md5(uniqid(rand(), true));
    $id = criarUsuario($nome, $token);

    if($id !== 0){
        $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
        //como não tem dados vindo do formulario que precisam ser
        //colocados na consulta, não é necessario usuar prepare statement
        $resultado = $conexao->query('select nome, token from usuarios 
                        where id = ' . $id);
        $usuario = $resultado->fetch_assoc();
        $usuario_nome = $usuario['nome'];
        $usuario_token = $usuario['toekn'];
    }else{
        echo 'Aconteceu algum erro ao salvar!';
        echo ' Pode ser qualquer coisa, nome de usuario duplicado, etc.';
    }
}

/**
@param string $nome nome do usuario vindo do formulario
@param string $token campo aleatorio gerado com a função uniqid()
@return mixed 0 caso o usuario usuario não possa ser salvo
                (nome de usuario duplicado, por exemplo), e se o
                for salvo com sucesso retornara o id do novo registro
                criado (auto_increment), onde id > 0 (maior que zero)
*/
function criarUsuario($nome, $token){
      //aqui você deve substituir pelos dados de acesso do seu banco
      $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');

      //para evitar injeção de sql vamos usar prepare statement
      $statement= $conexao->prepare('insert into usuarios (nome, token) 
      values (?, ?)');

      //cada interrogação no instrução acima será representada pelo 
      //tipo (string, integer, etc) e qual valor irá receber, 
      //nesse exemplo $nome e $token
      $statement->bind_param('ss', $nome, $token);

      //e por fim executamos a instrução no banco
      $status = $statement->execute();

      //se a instrução tiver executado com sucesso, retornamos o id 
      //do novo registro criado, caso contrario retornamos 0 
      //(erro ao salvar)
      if($status === true){
          return $conexao->insert_id;
      }

      return 0;
}

/******************************************************************
Agora geramos o formulario para enviar arquivos. O formulario poderia
estar em outro arquivo, mas por simplificação vai ficar aqui mesmo.

Tambem mostramos para o usuario: seu nome de usuario e seu token
para que ele possa anota-los, para futuramente usar apenas o token para
enviar arquivos (já que é mais "dificil" de adivinhar)
******************************************************************/

//só mostra o formulario se as variaveis $usuario_nome e $usuario_token
forem diferentes de null, ou seja, foram cadastrados com sucesso
if($usuario_nome === null && $usuario_token === null){
    //encerra a execução do script (não mostrando o formulario abaixo)
    exit;
}

//caso sejam diferentes de null mostra o formulario
//veja que com o if assima foi possivel "omitir" o else
?>

<div>
    <label>
        Seu nome de usuario é:<?php echo $usuario_nome; ?>
    </label>
    <label>
        Seu token de acesso é:<?php echo $usuario_token; ?>
    </label>
</div>

<form action="salvar_arquivo.php" method="post">
    <div>
        <label>Insira seu token de acesso</label>
        <input type="text" name="token">
    </div>
    <div>
        <label>Escolha um arquivo</label>
        <input type="file" name="arquivo">
    </div>

    <div>
        <label>Salvar</label>
        <input type="submit" value="Salvar">
    </div>
</form>

Most of the functions used in the generate_token.php file are already commented out, but worth mentioning again uniqid , prepare statement , last insert id , fetch assoc (not necessarily in order).

But back to the structure of the above code ( generate_token.php ). In this file a if was created to verify that the registration form  user had been submitted. The function was then called cadastrarUsuario() responsible for inserting a new record in the table users and return the inserted registry id. The return of this function was used to make a select in the bank and return, among others, the token saved in the bank. And finally the form to send files was displayed. Just one more detail, this form (of sending files) can be placed in a separate file, when the user has already been "registered". Type this (file send_filename.php):

<form action="salvar_arquivo.php" method="post">
        <div>
            <label>Insira seu token de acesso</label>
            <input type="text" name="token">
        </div>
        <div>
            <label>Escolha um arquivo</label>
            <input type="file" name="arquivo">
        </div>

        <div>
            <label>Salvar</label>
            <input type="submit" value="Salvar">
        </div>
    </form>

And now it only lacks the code to save the file sent by the form and list the files of a specific user.

The file save_file.php looks like this:

<?php
if(isset($_POST['token']) && isset($_FILES['arquivo'])){
    //pode ser uma pasta fora do public do seu site
    //nesse caso esta no mesmo diretorio desse arquivo
    $diretorio_arquivos = __DIR__ . '/arquivos';
    $token = $_POST['token'];
    //cria nome de arquivo unico
    $nome_arquivo = $_FILES['name'] . uniqid(rand(), true);
    $arquivo_temporario = $_FILES['tmp_name'];

    //agora checamos se o tokem existe no banco
    if(tokenExiste($token) === true){
        $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
        $usuario_id = $conexao("select id from usuarios where token = '" .
                  $token . "'");
        $usuario_id = $usuario_id->fetch_assoc()['id'];
        $data_fim = '2018-01-20';

        $statement = $conexao->prepare('insert into arquivos 
        (nome_arquivo, data_fim, usuario_id) values (?, ?, ?)');
        $statement->bind_param('ssi', $nome_arquivo, $data_fim, 
        $usuario_id);
        $statement->execute();

        //move o arquivo para o diretorio definitivo
        move_uploaded_file ( $arquivo_temporario , 
        $diretorio_arquivos . '/' . $nome_arquivo );

        //da para fazer algumas validações, mas como seria
        //necessario fazer rollback no banco, deixo a seu criterio
        //implementar
        echo 'provavelmene arquivo!';
    }
    else{
       echo 'token invalido! Já fez o cadastro?';
    }
}

function tokenExiste($token){
    $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
    $statement = $conexao->prepare('select token from usuarios 
                 where token = ?');
    $statement->bind_param('s', $token);
    $statement->execute();

    $statement->store_result();

    //token existe
    if($statement->num_rows == 1){
        return true;
    }

    return false;
}

The code itself is very explanatory, but it is worth pointing out references to go deeper into move uploaded file , mysqli num rows .

Finally, to list the files of a user, simply join them between the users and files tables. Then in the file list_files.php :

<form method="post">
    <input type="text" name="token">
    <input type="submit">
</form>

<?php
    if(isset($_POST['token'])){
        $arquivos = listaArquivos($_POST['token']);

        //não existem arquivos ou token invalido, encerra o script
        if(count($arquivos) < 1){
             echo 'não existem arquivos ou token invalido';
             exit;
        }

        //exibir arquivos em ul
        echo '<ul>';
        foreach($arquivos as $arquivo){
            echo '<li><a href="ver.php?nome=' . $arquivo 
            . '&token=' . $_POST['token'] . '">' . $arquivo
            .'</a></li>';
        }
        echo '</ul>';
    }

    function listaArquivos($token){
        $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
        $statement = $conexao->prepare('select nome_arquivo from usuarios,
        arquivos where token = ? and usuario_id = id');

        $statement->bind_param('s', $token);
        $statement->execute();

        $nome_arquivo = null;
        //armazenar o nome de todos os arquivos
        $arquivos = [];

        $statement->bind_result($nome_arquivo);

        //percorre todo o resultset da consulta acima
        while ($statement->fetch()) {
            $arquivos[] = $nome_arquivo;
        }

        return $arquivos;
    }
?>

And as you always go deeper into bind result , fetch , statement fetch .

Finally, when you click on a link from the list above, you will be redirected to a file called ver.php which will receive as a parameter the name of the file to be displayed and the access token . The ver.php file looks like this:

<?php
$token = $_GET['token'];
$nome_arquivo = $_GET['nome'];

//se existir o token e o nome de arquivo associados a um mesmo usuario
//a função podeAcessar retorna true
if(podeAcessar($nome_arquivo, $token)){
    $diretorio_arquivos = __DIR__ . '/arquivos';

    //enviando o arquivo para o usuario
     $mime = mime_content_type($diretorio_arquivos . '/' . $nome_arquivo);
     $tamanho = filesize($diretorio_arquivos . '/' . $nome_arquivo)

     header("Content-Type: ". $mime);
     header("Content-Length: " . $tamanho);
     //retorna o conteudo do arquivo salvo no servidor
     //dependendo do tipo de mime ("extensão") o proprio navegador exibira.
     //caso não seja compativel com o render oferecido pelo navegador 
     //será baixado
     echo file_get_contents($diretorio_arquivos . '/' . $nome_arquivo);
}else{
 echo 'parece que você não pode acessar este arquivo!';
}

function podeAcessar($nome_arquivo, $token){
    $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
    $statement = $conexao->prepare('select nome_arquivo from usuarios, 
    arquivos where token = ? and usuario_id = id and nome_arquivo = ?');

    $statement->bind_param('ss', $token, $nome_arquivo);
    $statement->execute();

    $statement->store_result();

    //pode acessar
    if($statement->num_rows == 1){
        return true;
    }

    return false;
}

And to deepen the functions used consult mime content type , filesize , file_get_contents ( All in the php documentation.)

With this you should have a reasonably secure system, since the files will not be in the public directory, because you can create the directory folder a directory above the public folder (server root), for example, doing echo realpath($diretorio_arquivos . '/../pastadearquivos') . This allows you to access the lastfile folder that is outside the root of the server. In addition it still does to avoid injection attacks (both from sql, as trying to send an arbitrary path).

Note: there may be syntax errors (variables missing letters) since I did not use the interpreter to validate the above codes. But to get a general idea.

    
25.12.2017 / 17:34