Transferência de ficheiros

THIS PAGE IS DEPRECATED: READ File transfer

Como é que o usamos?

Android

Quando está a falar com alguém no Android, tem a possibilidade de enviar uma fotografia no seu dispositivo ou de tirar uma fotografia com estes botões:

Botões_de_ficheiro_do_Android

Nota: quando envia um ficheiro, o outro tem de o aceitar. Nesse momento, aparece a mensagem “à espera do par - awaiting peer”:

Android_à_espera_do_par

Como é que funciona? (técnico)

Como funciona

Introdução

O Jami é uma aplicação distribuída e tem de funcionar sem qualquer conexão à Internet. Por isso, a transferência de ficheiros também! Basicamente, utilizamos o mesmo método para efetuar a transferência de ficheiros e chamadas, mas em TCP. Para resumir como funciona, podemos imaginar uma situação em que Alice (A) quer transferir um ficheiro para Bob (B).

Primeiro, Alice solicita uma conexão com Bob. Para isso, o Jami está a usar o ICE (RFC 6544), um protocolo usado para negociar ligações entre pares. Alice enviará, em um pacote encriptado via DHT, o IP do seu dispositivo. Assim, quando Bob receber os IPs de Alice, eles poderão negociar um transporte onde Bob poderá enviar pacotes para Alice. A negociação pode ser bem sucedida, mas se falhar, será utilizado um servidor TURN (o que estiver configurado nas definições) para efetuar a transferência. Se a negociação for bem sucedida, Bob enviará seus IPs para Alice para realizar a negociação na outra direção. Note-se que a ligação ainda não é segura, pelo que Bob enviará os IPs através da DHT numa mensagem encriptada. Se a segunda negociação falhar, o TURN será utilizado como recurso.

Agora que a ligação TCP bidirecional está aqui, o próximo passo será negociar um TLS 1.3 (geralmente um (TLS1.3)-(DHE-FFDHE8192)-(RSA-PSS-RSAE-SHA384)-(AES-256-GCM) quando escrevo estas linhas) entre a Alice e o Bob, e depois a Alice começará a transferir o ficheiro.

A primeira parte será um pequeno cabeçalho para descrever o conteúdo do ficheiro. Depois, quando o Bob aceitar a transferência, será transmitido o ficheiro completo.

Processo

Enviar um ficheiro

É utilizado o seguinte método:

1. Um cliente irá chamar DataTransferFacade::sendFile(). A DataTransferFacade é a classe correspondente à API exposta para os clientes. Ela é utilizada para gerir uma visão das transferências de ficheiros (as classes correspondentes são DataTransfer, IncomingFileTransfer, OutgoingFileTransfer e SubOutgoingFileTransfer). Este método irá pedir ao JamiAccount vinculado para solicitar uma conexão.

![Diagrama: diagrama de classe de transferência de dados]images/file-transfer-datatransfer-class-diagram.png)

2. O método DhtPeerConnector: requestConnection() é ativado e cria uma ligação entre todos os dispositivos ligados do par (encontrados no DHT). O DhtPeerConnector é usado para gerir o loop de eventos principal que gere as conexões. Quando um dispositivo é encontrado, o event loop criará um ClientConnector (que gere a conexão para um dispositivo) e lançará o método process().

3. Este método é utilizado para inicializar o transporte ICE e colocar uma PeerConnectionMsg (que contém a mensagem SDP, ver abaixo) no DHT e espera por uma resposta (DhtPeerConnector::Impl::onResponseMsg).

4. De seguida, é recebida uma resposta da DHT, que contém os endereços públicos do dispositivo par. Agora podemos negociar um link TLS (diretamente via ICE, ou via TURN como um fallback). Este TlsSocketEndpoint é dado ao objeto PeerConnection como uma saída e a transferência pode começar.

5.\ Quando o socket TLS está pronto, a chamada de retorno DataTransferFacade::Impl::onConnectionRequestReply é chamada, e um OutgoingFileTransfer é ligado à PeerConnection como uma entrada. Este OutgoingFileTransfer contém uma lista de SubOutgoingFileTransfer (um por dispositivo) onde cada subtransferência é uma transferência para um dispositivo. Fazemos isto para podermos fornecer a visão mais otimista da transferência (se um contacto tiver 3 dispositivos, e o contacto cancelar a transferência num dispositivo, mas aceitar a transferência nos outros dois, será mostrada a transferência mais avançada).

6. O SubOutgoingFileTransfer primeiro transfere o cabeçalho do ficheiro, aguarda a aceitação do par (uma mensagem «GO\n» no socket) e depois envia o ficheiro.

7. Se um cancelamento for recebido do par ou do cliente ou se a transferência de ficheiros terminar, a conexão será fechada através de uma mensagem CANCEL no DhtPeerConnector::eventLoop() e os recursos serão libertados.

TLSsocketEndpoint

Receber um ficheiro

A mesma estrutura é utilizada para receber ficheiros, mas o método muda um pouco:

  1. A classe JamiAccount é utilizada para receber mensagens do DHT, pois a primeira coisa recebida será a requisição do DHT.

  2. Em seguida, esta mensagem é enviada para DhtPeerConnector: onRequestMessage() através do eventLoop.

  3. O método DhtPeerConnector::Impl::answerToRequest tentará ligar-se ao servidor TURN (se não estiver ligado) e inicializar o transporte ICE. Este método abre 2 ligações de controlo a um servidor TURN (uma para autorizar pares IPv4, outra para pares IPv6, devido à RFC 6156) se ainda não estiver aberta e permite a ligação de endereços públicos pares. Depois, se o SDP recebido não contiver candidatos a ICE, usará o TURN e elaborará a resposta SDP para aguardar o par. Se o SDP contiver candidatos ICE, o método tentará negociar a ligação (ou recorrer ao TURN) e responderá ao SDP (com candidatos ICE ou não).

  4. Uma vez que os links estão prontos, como o remetente, um link TLS é negociado e dado ao PeerConnection dado ao IncomingFileTransfer como uma entrada. Os cabeçalhos do ficheiro chegam e o cliente pode agora aceitar ou cancelar a transferência.

Voltar a pedir uma transferência de ficheiros anterior

As specified in Other mime types, the data-transfer interactions are now synced and stored into conversations. So, a device can easily detects if a file was downloaded or not. If not, it can asks all members in the conversation to transmits the file again.

Para fazer isso, o dispositivo enviará um json com o mime-type: application/data-transfer-request+json contendo conversation (o id da conversa), interaction (a interação relacionada), deviceId o dispositivo que recebe o ficheiro.

O remetente verifica agora se o dispositivo é um dispositivo do par anunciado e se o dispositivo é um membro da conversa, e pode enviar o ficheiro através de uma transferência de ficheiros clássica.

O recetor pode agora aceitar a primeira transferência de entrada, descarregar o ficheiro e verificar se o sha3sum está correto.

Esquema

Diagrama: diagrama do esquema principal

SDP enviado através do DHT
0d04b932
7c33834e7cf944bf0e367b47
H6e6ca682 1 TCP 2130706431 2607:fad8:4:6:9eb6:d0ff:dead:c0de 50693 typ host tcptype passive
H6e6ca682 1 TCP 2130706431 2607:fad8:4:6:9eb6:d0ff:dead:c0de 9 typ host tcptype active
H42c1b577 1 TCP 2130706431 fe80::9eb6:d0ff:fee7:1412 50693 typ host tcptype passive
H42c1b577 1 TCP 2130706431 fe80::9eb6:d0ff:fee7:1412 9 typ host tcptype active
Hc0a8007e 1 TCP 2130706431 192.168.0.123 42751 typ host tcptype passive
Hc0a8007e 1 TCP 2130706431 192.168.0.123 9 typ host tcptype active
Sc0a8007e 1 TCP 1694498815 X.X.X.X 42751 typ srflx tcptype passive
Z.Z.Z.Z:YYYY
A.A.A.A:YYYY

Onde 0d04b932 é o ufrag e 7c33834e7cf944bf0e367b47 a palavra-passe da sessão ICE. 2130706431 e 1694498815 são as prioridades dos candidatos. 192.168.0.126 42751 typ host tcptype passive é um candidato a hospedeiro passivo e 1694498815 X.X.X.X 42751 typ srflx tcptype passive um hospedeiro passivo que reflete o IP público (mapeado via UPnP, por exemplo).

Vários dispositivos

Um utilizador pode vincular a sua conta a vários dispositivos. Assim, precisamos de implementar a transferência quando um utilizador envia um ficheiro para um contacto que tem vários dispositivos vinculados a esta conta.

Primeira abordagem

A primeira abordagem consistia em enviar um pedido através do DHT a todos os dispositivos e os primeiros dispositivos que respondessem receberiam o ficheiro a transferir. Isto é mau para o seu contacto porque ele não saberá qual o dispositivo que receberá a transferência.

Abordagem atual

Agora, continuamos a enviar um pedido a todos os dispositivos. A diferença é que todos os dispositivos terão a notificação de receção de um ficheiro e podem aceitar/recusar a transferência. A maior parte do código para isso está em data_transfer.cpp.

Agora (desde https://review.jami.net/c/jami-daemon/+/9327), quando um utilizador envia um ficheiro, irá pedir uma PeerConnection com todos os dispositivos pares. E para todas as ligações, anexamos um novo fluxo de entrada para podermos aceitar/recusar/cancelar cada transferência separadamente.

Em data_transfer.cpp definimos a classe OptimisticMetaOutgoingInfo que representa a visão otimista a mostrar ao cliente. É otimista porque se um contacto aceitar uma transferência num dispositivo e recusar noutros, esta classe mostrará a transferência de ficheiros em curso. E só apresentará um erro se todos os dispositivos recusarem a transferência.

Esta classe está ligada à SubOutgoingFileTransfer que representa o estado de uma transferência com um dispositivo. Mais tarde, os clientes terão a possibilidade de mostrar uma subtransferência em vez da otimista (ver lista de A FAZER).

Utilizar outro servidor TURN

Atualmente, o servidor TURN predefinido é turn.jami.net. Mas pode alojar o seu próprio servidor TURN. Por exemplo, executando um servidor coturn.

sudo turnserver -a -v -n -u user:password -r "realm"

Em seguida, pode configurar o servidor TURN nas configurações avançadas da aplicação.

Nota: isto requer alguns conhecimentos técnicos. Além disso, o servidor TURN deve ver o mesmo endereço IP do seu nó como o nó de destino ou a ligação entre pares falhará (porque a autorização será incorreta)

Lista de A FAZER

  1. Usar libtorrent?

  2. Mostrar o estado das subtransferências para ficheiros de saída