2016-04-22 48 views
6

Sto costruendo una libreria Ruby client che si connette a un server e attende i dati, ma consente anche agli utenti di inviare dati chiamando un metodo.Come creare un socket SSL bidirezionale in Ruby

Il meccanismo che uso è quello di avere una classe che inizializza una coppia di socket, in questo modo:

def initialize 
    @pipe_r, @pipe_w = Socket.pair(:UNIX, :STREAM, 0) 
end 

Il metodo che ho consentono agli sviluppatori di chiamare per inviare i dati al server è simile al seguente:

def send(data) 
    @pipe_w.write(data) 
    @pipe_w.flush 
end 

Poi ho un ciclo in un thread separato, dove seleziono da un socket collegata al server e dal @pipe_r:

def socket_loop 
    Thread.new do 
    socket = TCPSocket.new(host, port) 

    loop do 
     ready = IO.select([socket, @pipe_r]) 

     if ready[0].include?(@pipe_r) 
     data_to_send = @pipe_r.read_nonblock(1024) 
     socket.write(data_to_send) 
     end 

     if ready[0].include?(socket) 
     data_received = socket.read_nonblock(1024) 
     h2 << data_received 
     break if socket.nil? || socket.closed? || socket.eof? 
     end 
    end 
    end 
end 

Questo funziona magnificamente, ma solo con un normale TCPSocket come nell'esempio. Ho bisogno di utilizzare un OpenSSL::SSL::SSLSocket invece, tuttavia secondo the IO.select docs:

Il modo migliore per utilizzare IO.select invoca dopo metodi non bloccante, come read_nonblock, write_nonblock, ecc

[...]

In particolare, la combinazione di metodi non bloccanti e IO.select è preferibile per oggetti di tipo IO come OpenSSL :: SSL :: SSLSocket.

In base a questo, ho bisogno di chiamare IO.selectdopo metodi non bloccante, mentre nel mio ciclo lo uso prima in modo da poter scegliere tra 2 diversi oggetti IO.

La proposta esempio su come utilizzare IO.select con una presa SSL è:

begin 
    result = socket.read_nonblock(1024) 
rescue IO::WaitReadable 
    IO.select([socket]) 
    retry 
rescue IO::WaitWritable 
    IO.select(nil, [socket]) 
    retry 
end 

Tuttavia questo funziona solo se IO.select viene utilizzato con un solo IO oggetto .

La mia domanda è: come posso far funzionare il mio esempio precedente con un socket SSL, dato che ho bisogno di scegliere tra gli oggetti @pipe_r e socket?

EDIT: Ho provato ciò che suggeriva @ steffen-ullrich, ma senza successo. Sono stato in grado di fare i miei test passano utilizzando la seguente:

loop do 

    begin 
    data_to_send = @pipe_r.read_nonblock(1024) 
    socket.write(data_to_send) 
    rescue IO::WaitReadable, IO::WaitWritable 
    end 

    begin 
    data_received = socket.read_nonblock(1024) 
    h2 << data_received 
    break if socket.nil? || socket.closed? || socket.eof? 

    rescue IO::WaitReadable 
    IO.select([socket, @pipe_r]) 

    rescue IO::WaitWritable 
    IO.select([@pipe_r], [socket]) 

    end 
end 

questo non sembra così male, ma qualsiasi input è il benvenuto.

risposta

2

Non ho familiarità con lo stesso ruby, ma con i problemi di utilizzo di select con socket basati su SSL. I socket SSL si comportano diversamente con i socket TCP poiché i dati SSL vengono trasferiti nei frame e non come un flusso di dati, ma tuttavia la semantica del flusso viene applicata all'interfaccia socket.

Spieghiamo con un esempio, prima utilizzando una semplice connessione TCP:

  • il server invia 1000 byte in una singola scrittura.
  • I dati verranno consegnati al client e inseriti nel buffer del socket nel kernel. Quindi selezionare restituirà che i dati sono disponibili.
  • L'applicazione client legge prima solo 100 byte.
  • Gli altri 900 byte verranno quindi conservati nel buffer del socket nel kernel. La prossima chiamata di select restituirà di nuovo i dati disponibili, dato che select guarda il buffer del socket nel kernel.

Con SSL Questo è diverso:

  • Il server invia 1000 byte in una singola scrittura. Lo stack SSL crittograferà quindi questi 1000 byte in un singolo frame SSL e invierà questo frame al client.
  • Questo frame SSL verrà consegnato al client e inserito nel buffer del socket nel kernel. Quindi selezionare restituirà che i dati sono disponibili.
  • Ora l'applicazione client legge solo 100 byte. Poiché il frame SSL contiene 1000 byte e dal momento che ha bisogno del frame completo per decrittografare i dati, lo stack SSL leggerà l'intero frame dal socket, lasciando nulla nel buffer del socket nel kernel. Quindi decrittografa il frame e restituisce i 100 byte richiesti all'applicazione. Il resto dei 900 byte decrittografati verrà mantenuto nello stack SSL nello spazio utente.
  • Poiché il buffer del socket nel kernel è vuoto, la chiamata successiva di select non restituirà che i dati siano disponibili. Pertanto, se l'applicazione riguarda solo la selezione, ora non ci saranno ancora dati non letti, cioè i 900 byte conservati nello stack SSL.

Come trattare con questa differenza:

  • Un modo è quello di cercare sempre di leggere i dati di almeno 32768, perché questa è la dimensione massima di un frame SSL. In questo modo si può essere certi che nessun dato sia ancora conservato nello stack SSL (la lettura SSL non leggerà oltre i limiti del frame SSL).
  • Un altro modo è controllare lo stack SSL per i dati già decrittografati prima di chiamare select. Solo se nessun dato è conservato nello stack SSL selezionare deve essere chiamato. Per verificare la presenza di tali "dati in sospeso", utilizzare the pending method.
  • Provare a leggere più dati dal socket non bloccante finché non sono disponibili altri dati. In questo modo puoi essere sicuro che nessun dato è ancora in sospeso nello stack SSL. Notare che questo farà anche delle letture sul socket TCP sottostante e quindi potrebbe preferire i dati sul socket SSL rispetto ai dati su altri socket (che vengono letti solo dopo aver selezionato con successo). Questa è la raccomandazione che hai citato, ma preferirei gli altri.
+0

Grazie @ steffen-ullrich, questa è una risposta molto chiara. Purtroppo ho provato a utilizzare il metodo in sospeso prima di usare select e ottengo sempre che ci siano 0 byte disponibili. Ho anche provato nonblock_read (32768) e anche questo non è il trucco. Proverò ancora un po '. – ostinelli

+0

Accetterà questa risposta perché chiarisce il modo corretto per risolvere questi problemi. Grazie ancora! – ostinelli