network_clipboard 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/lib/network_clipboard/client.rb +123 -70
- data/lib/network_clipboard/connection.rb +20 -24
- data/lib/network_clipboard/discovery.rb +15 -18
- data/lib/network_clipboard/error.rb +4 -0
- metadata +2 -1
- metadata.gz.sig +0 -0
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 6977532a681a67182fb89b165bae55ac45261000
         | 
| 4 | 
            +
              data.tar.gz: 85be7d1ed65bc938f1356cdde5e317ae6f8b212c
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 1cb92234e9f79112666e167301c311b2e081ebeb965f32310e21f423e8be21622ce2f19f528c770f33b48eb481d696dd52055db1402c0f0c7d19a4cf25d9074f
         | 
| 7 | 
            +
              data.tar.gz: cc47484d072601dab2720455b237310bf4d344e0d5b9990fc628d6c11a8626fb14d02178898f9dc454b39af352f72144eba595cd06553d2dad07024b83c545c5
         | 
    
        checksums.yaml.gz.sig
    CHANGED
    
    | Binary file | 
    
        data.tar.gz.sig
    CHANGED
    
    | Binary file | 
| @@ -7,115 +7,168 @@ require 'socket' | |
| 7 7 | 
             
            require 'logger'
         | 
| 8 8 |  | 
| 9 9 | 
             
            module NetworkClipboard
         | 
| 10 | 
            -
               | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 10 | 
            +
              LOGGER = Logger.new(STDOUT)
         | 
| 11 | 
            +
              LOGGER.level = Logger::WARN
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              class ConnectionWrapper
         | 
| 14 | 
            +
                def initialize(client,connection)
         | 
| 15 | 
            +
                  @client = client
         | 
| 16 | 
            +
                  @connection = connection
         | 
| 17 | 
            +
                  @read_thread = Thread.new{read_loop}
         | 
| 18 | 
            +
                  @write_thread = Thread.new{write_loop}
         | 
| 19 | 
            +
                  @running = true
         | 
| 20 | 
            +
                  @value = nil
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def read_loop
         | 
| 24 | 
            +
                  while @running
         | 
| 25 | 
            +
                    begin
         | 
| 26 | 
            +
                      new_value = @connection.receive()
         | 
| 27 | 
            +
                    rescue DisconnectedError
         | 
| 28 | 
            +
                      if @running
         | 
| 29 | 
            +
                        LOGGER.error("Client #{@connection.remote_client_id} went away")
         | 
| 30 | 
            +
                        @running = false
         | 
| 31 | 
            +
                      end
         | 
| 32 | 
            +
                      break
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
                    next if @value == new_value
         | 
| 35 | 
            +
                    LOGGER.info("Received new clipboard value from #{@connection.remote_client_id}")
         | 
| 36 | 
            +
                    Clipboard.copy(@value = new_value)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                  @connection.close_read
         | 
| 39 | 
            +
                  LOGGER.debug("Read loop completed")
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def write_loop
         | 
| 43 | 
            +
                  while @running
         | 
| 44 | 
            +
                    new_value = Clipboard.paste
         | 
| 45 | 
            +
                    (sleep(2); next) if new_value.nil? or new_value.empty? or @value == new_value
         | 
| 46 | 
            +
                    LOGGER.info("Sending clipboard value to #{@connection.remote_client_id}")
         | 
| 47 | 
            +
                    @connection.send(@value = new_value)
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                  @connection.close_write
         | 
| 50 | 
            +
                  LOGGER.debug("Write loop completed")
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
                
         | 
| 53 | 
            +
                def join
         | 
| 54 | 
            +
                  @read_thread.join
         | 
| 55 | 
            +
                  @write_thread.join
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def stop
         | 
| 59 | 
            +
                  @running = false
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
              end
         | 
| 13 62 |  | 
| 14 | 
            -
             | 
| 63 | 
            +
              class Client
         | 
| 15 64 |  | 
| 16 65 | 
             
                def self.run
         | 
| 17 66 | 
             
                  c = Client.new
         | 
| 18 | 
            -
                  Signal.trap('INT'){c. | 
| 67 | 
            +
                  Signal.trap('INT'){c.stop}
         | 
| 19 68 | 
             
                  c.loop
         | 
| 20 69 | 
             
                end
         | 
| 21 70 |  | 
| 22 71 | 
             
                def initialize
         | 
| 23 72 | 
             
                  @config = Config.new
         | 
| 24 73 | 
             
                  @discovery = Discovery.new(@config)
         | 
| 74 | 
            +
             | 
| 25 75 | 
             
                  @tcp_server = TCPServer.new(@config.port)
         | 
| 76 | 
            +
             | 
| 26 77 | 
             
                  @connections = {}
         | 
| 78 | 
            +
                  @connections_mutex = Mutex.new
         | 
| 79 | 
            +
             | 
| 27 80 | 
             
                  @running = true
         | 
| 28 81 | 
             
                end
         | 
| 29 82 |  | 
| 30 83 | 
             
                def loop
         | 
| 31 | 
            -
                   | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
                     :fetch_clipboard,
         | 
| 36 | 
            -
                     :find_incoming,
         | 
| 37 | 
            -
                     :wait,
         | 
| 38 | 
            -
                    ].each do |action|
         | 
| 39 | 
            -
                      send(action) if @running
         | 
| 40 | 
            -
                    end
         | 
| 41 | 
            -
                  end
         | 
| 42 | 
            -
                end
         | 
| 84 | 
            +
                  Thread.abort_on_exception = true
         | 
| 85 | 
            +
                  @announce_thread = Thread.new{announce_loop}
         | 
| 86 | 
            +
                  @discover_thread = Thread.new{discover_loop}
         | 
| 87 | 
            +
                  @incoming_loop = Thread.new{incoming_loop}
         | 
| 43 88 |  | 
| 44 | 
            -
             | 
| 45 | 
            -
                  update_clipboard(Clipboard.paste)
         | 
| 46 | 
            -
                end
         | 
| 89 | 
            +
                  @announce_thread.join
         | 
| 47 90 |  | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
                  if @new_value != @last_value
         | 
| 51 | 
            -
                    @connections.values.each{|c| send_new_clipboard(c)}
         | 
| 91 | 
            +
                  @connections.values.each do |connection|
         | 
| 92 | 
            +
                    connection.join
         | 
| 52 93 | 
             
                  end
         | 
| 53 94 | 
             
                end
         | 
| 54 95 |  | 
| 55 | 
            -
                def  | 
| 56 | 
            -
                   | 
| 96 | 
            +
                def announce_loop
         | 
| 97 | 
            +
                  while @running
         | 
| 98 | 
            +
                    LOGGER.debug("Announcing")
         | 
| 99 | 
            +
                    @discovery.announce
         | 
| 100 | 
            +
                    LOGGER.debug("Announced")
         | 
| 101 | 
            +
                    sleep(15)
         | 
| 102 | 
            +
                  end
         | 
| 57 103 | 
             
                end
         | 
| 58 104 |  | 
| 59 | 
            -
                def  | 
| 60 | 
            -
                  @ | 
| 105 | 
            +
                def discover_loop
         | 
| 106 | 
            +
                  while @running
         | 
| 107 | 
            +
                    LOGGER.debug("Discovering")
         | 
| 108 | 
            +
                    remote_client_id,address = @discovery.get_peer_announcement
         | 
| 109 | 
            +
                    LOGGER.debug("Found #{remote_client_id} on #{address}")
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                    @connections_mutex.synchronize do
         | 
| 112 | 
            +
                      next if @connections[remote_client_id]
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                      aes_connection = AESConnection.new(@config,TCPSocket.new(address,@config.port))
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                      if aes_connection.remote_client_id != remote_client_id
         | 
| 117 | 
            +
                        LOGGER.error("Client Id #{aes_connection.remote_client_id} doesn't match original value #{remote_client_id}")
         | 
| 118 | 
            +
                        aes_connection.close
         | 
| 119 | 
            +
                        next
         | 
| 120 | 
            +
                      end
         | 
| 121 | 
            +
                        
         | 
| 122 | 
            +
                      if @connections[aes_connection.remote_client_id]
         | 
| 123 | 
            +
                        LOGGER.error("Duplicate connections #{aes_connection} and #{@connections[aes_connection.remote_client_id]}")
         | 
| 124 | 
            +
                        aes_connection.close
         | 
| 125 | 
            +
                        next
         | 
| 126 | 
            +
                      end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                      LOGGER.info("New Peer -> #{remote_client_id}")
         | 
| 129 | 
            +
                      @connections[aes_connection.remote_client_id] = ConnectionWrapper.new(self,aes_connection)
         | 
| 130 | 
            +
                    end
         | 
| 131 | 
            +
                  end
         | 
| 61 132 | 
             
                end
         | 
| 62 133 |  | 
| 63 | 
            -
                def  | 
| 64 | 
            -
                   | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 134 | 
            +
                def incoming_loop
         | 
| 135 | 
            +
                  while @running
         | 
| 136 | 
            +
                    incoming = @tcp_server.accept
         | 
| 137 | 
            +
                    aes_connection = AESConnection.new(@config,incoming)
         | 
| 67 138 |  | 
| 68 | 
            -
             | 
| 69 | 
            -
                  @discovery.announce
         | 
| 70 | 
            -
                end
         | 
| 139 | 
            +
                    LOGGER.debug("Incoming #{aes_connection.remote_client_id} from #{incoming.peeraddr(false)[-1]}")
         | 
| 71 140 |  | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 141 | 
            +
                    @connections_mutex.synchronize do
         | 
| 142 | 
            +
                      if @connections[aes_connection.remote_client_id]
         | 
| 143 | 
            +
                        LOGGER.info("Connection already established to #{aes_connection.remote_client_id}, dropping.")
         | 
| 144 | 
            +
                        aes_connection.close
         | 
| 145 | 
            +
                        next
         | 
| 146 | 
            +
                      end
         | 
| 76 147 |  | 
| 77 | 
            -
             | 
| 148 | 
            +
                      LOGGER.info("New Peer <- #{aes_connection.remote_client_id}")
         | 
| 78 149 |  | 
| 79 | 
            -
             | 
| 80 | 
            -
                      LOGGER.error("Client Id #{aes_connection.remote_client_id} doesn't match original value #{remote_client_id}")
         | 
| 81 | 
            -
                      aes_connection.close
         | 
| 82 | 
            -
                      next
         | 
| 150 | 
            +
                      @connections[aes_connection.remote_client_id] = ConnectionWrapper.new(self,aes_connection)
         | 
| 83 151 | 
             
                    end
         | 
| 84 | 
            -
                      
         | 
| 85 | 
            -
                    if @connections[aes_connection.remote_client_id]
         | 
| 86 | 
            -
                      LOGGER.error("Duplicate connections #{aes_connection} and #{@connections[aes_connection.remote_client_id]}")
         | 
| 87 | 
            -
                      aes_connection.close
         | 
| 88 | 
            -
                      next
         | 
| 89 | 
            -
                    end
         | 
| 90 | 
            -
             | 
| 91 | 
            -
                    @connections[aes_connection.remote_client_id] = aes_connection
         | 
| 92 152 | 
             
                  end
         | 
| 93 153 | 
             
                end
         | 
| 94 154 |  | 
| 95 | 
            -
                def  | 
| 96 | 
            -
                   | 
| 97 | 
            -
                     | 
| 98 | 
            -
                       | 
| 99 | 
            -
                    rescue IO::WaitReadable
         | 
| 100 | 
            -
                      return
         | 
| 155 | 
            +
                def run_connection(connection)
         | 
| 156 | 
            +
                  begin
         | 
| 157 | 
            +
                    while @running
         | 
| 158 | 
            +
                      Clipboard.copy(connection.receive())
         | 
| 101 159 | 
             
                    end
         | 
| 102 | 
            -
             | 
| 103 | 
            -
                     | 
| 160 | 
            +
                  ensure
         | 
| 161 | 
            +
                    connection.close
         | 
| 104 162 |  | 
| 105 | 
            -
                     | 
| 106 | 
            -
                       | 
| 107 | 
            -
                      next
         | 
| 163 | 
            +
                    @connections_mutex.synchronize do
         | 
| 164 | 
            +
                      @connections.delete(connection.remote_client_id)
         | 
| 108 165 | 
             
                    end
         | 
| 109 | 
            -
             | 
| 110 | 
            -
                    @connections[aes_connection.remote_client_id] = aes_connection
         | 
| 111 166 | 
             
                  end
         | 
| 112 167 | 
             
                end
         | 
| 113 168 |  | 
| 114 | 
            -
                def  | 
| 115 | 
            -
                   | 
| 116 | 
            -
             | 
| 117 | 
            -
                    @tcp_server,
         | 
| 118 | 
            -
                  ] + @connections.values.collect(&:socket), [], [], 5)
         | 
| 169 | 
            +
                def stop
         | 
| 170 | 
            +
                  @running = false
         | 
| 171 | 
            +
                  @connections.values.each(&:stop)
         | 
| 119 172 | 
             
                end
         | 
| 120 173 | 
             
              end
         | 
| 121 174 | 
             
            end
         | 
| @@ -1,10 +1,15 @@ | |
| 1 | 
            +
            require_relative 'error'
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'socket'
         | 
| 2 4 | 
             
            require 'openssl'
         | 
| 3 5 |  | 
| 4 6 | 
             
            module NetworkClipboard
         | 
| 5 7 | 
             
              HANDSHAKE_STRING = "NetworkClipboard Handshake"
         | 
| 6 8 |  | 
| 7 | 
            -
              class HandshakeException <  | 
| 9 | 
            +
              class HandshakeException < NetworkClipboardError
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              class DisconnectedError < NetworkClipboardError
         | 
| 8 13 | 
             
              end
         | 
| 9 14 |  | 
| 10 15 | 
             
              class AESConnection
         | 
| @@ -29,8 +34,8 @@ module NetworkClipboard | |
| 29 34 | 
             
                  iv = @encryptor.random_iv
         | 
| 30 35 | 
             
                  @socket.send([iv.size].pack('N'),0)
         | 
| 31 36 | 
             
                  @socket.send(iv,0)
         | 
| 32 | 
            -
                  iv_size = @socket. | 
| 33 | 
            -
                  @decryptor.iv = @socket. | 
| 37 | 
            +
                  iv_size = @socket.read(4).unpack('N')[0]
         | 
| 38 | 
            +
                  @decryptor.iv = @socket.read(iv_size)
         | 
| 34 39 |  | 
| 35 40 | 
             
                  # Verify it all worked.
         | 
| 36 41 | 
             
                  send(HANDSHAKE_STRING)
         | 
| @@ -47,34 +52,25 @@ module NetworkClipboard | |
| 47 52 | 
             
                  @encryptor.reset
         | 
| 48 53 | 
             
                end
         | 
| 49 54 |  | 
| 50 | 
            -
                def receive( | 
| 51 | 
            -
                   | 
| 52 | 
            -
             | 
| 53 | 
            -
                     | 
| 54 | 
            -
                      @partial_read += @socket.recv_nonblock(4 - @partial_read.size)
         | 
| 55 | 
            -
                    end
         | 
| 56 | 
            -
                    while @partial_read.size < (total_size = 4 + @partial_read.unpack('N')[0])
         | 
| 57 | 
            -
                      @partial_read += @socket.recv_nonblock(total_size - @partial_read.size)
         | 
| 58 | 
            -
                    end
         | 
| 59 | 
            -
                  rescue IO::WaitReadable
         | 
| 60 | 
            -
                    if blocking
         | 
| 61 | 
            -
                      IO.select([@socket])
         | 
| 62 | 
            -
                      retry
         | 
| 63 | 
            -
                    else
         | 
| 64 | 
            -
                      return nil
         | 
| 65 | 
            -
                    end
         | 
| 55 | 
            +
                def receive()
         | 
| 56 | 
            +
                  size_bits = @socket.read(4)
         | 
| 57 | 
            +
                  if size_bits.nil? and @socket.eof?
         | 
| 58 | 
            +
                    raise DisconnectedError
         | 
| 66 59 | 
             
                  end
         | 
| 67 | 
            -
             | 
| 68 | 
            -
                  ciphertext | 
| 69 | 
            -
             | 
| 60 | 
            +
                  ciphertext_size = size_bits.unpack('N')[0]
         | 
| 61 | 
            +
                  ciphertext = @socket.read(ciphertext_size)
         | 
| 70 62 | 
             
                  plaintext = @decryptor.update(ciphertext) + @decryptor.final
         | 
| 71 63 | 
             
                  @decryptor.reset
         | 
| 72 64 |  | 
| 73 65 | 
             
                  return plaintext
         | 
| 74 66 | 
             
                end
         | 
| 75 67 |  | 
| 76 | 
            -
                def  | 
| 77 | 
            -
                  @socket. | 
| 68 | 
            +
                def close_read
         | 
| 69 | 
            +
                  @socket.close_read
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def close_write
         | 
| 73 | 
            +
                  @socket.close_write
         | 
| 78 74 | 
             
                end
         | 
| 79 75 | 
             
              end
         | 
| 80 76 | 
             
            end
         | 
| @@ -39,24 +39,21 @@ module NetworkClipboard | |
| 39 39 | 
             
                  @send_socket.send(@authenticated_client_id,0)
         | 
| 40 40 | 
             
                end
         | 
| 41 41 |  | 
| 42 | 
            -
                def  | 
| 43 | 
            -
                   | 
| 44 | 
            -
             | 
| 45 | 
            -
                     | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
                      yield [other_client_id,ip[2]]
         | 
| 58 | 
            -
                    end
         | 
| 59 | 
            -
                  rescue IO::WaitReadable
         | 
| 42 | 
            +
                def get_peer_announcement
         | 
| 43 | 
            +
                  while true
         | 
| 44 | 
            +
                    msg,ip = @receive_socket.recvfrom(65536)
         | 
| 45 | 
            +
                    other_client_id,other_digest = msg.unpack('H32A32')
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    # Retry if we got our own announcement.
         | 
| 48 | 
            +
                    next if other_client_id == @config.client_id
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    # We could do a constant time string compare, but an attacker can
         | 
| 51 | 
            +
                    # just listen to announces and rebroadcast them as his own anyway.
         | 
| 52 | 
            +
                    # This is just to skip other honest clients with different secrets
         | 
| 53 | 
            +
                    # on the network, to avoid wasting time on a failed handshake.
         | 
| 54 | 
            +
                    next unless other_digest == Digest::HMAC.digest(@config.secret,other_client_id,Digest::SHA256)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    return [other_client_id,ip[2]]
         | 
| 60 57 | 
             
                  end
         | 
| 61 58 | 
             
                end
         | 
| 62 59 | 
             
              end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: network_clipboard
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 0.0.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Christophe Biocca
         | 
| @@ -63,6 +63,7 @@ files: | |
| 63 63 | 
             
            - lib/network_clipboard/config.rb
         | 
| 64 64 | 
             
            - lib/network_clipboard/connection.rb
         | 
| 65 65 | 
             
            - lib/network_clipboard/discovery.rb
         | 
| 66 | 
            +
            - lib/network_clipboard/error.rb
         | 
| 66 67 | 
             
            homepage: http://github.com/christophebiocca/network_clipboard
         | 
| 67 68 | 
             
            licenses:
         | 
| 68 69 | 
             
            - Apache-2.0
         | 
    
        metadata.gz.sig
    CHANGED
    
    | Binary file |