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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8563a699ffa840eaa49ab7029b1c69dc5895b986
4
- data.tar.gz: 37be775586fe6006e9d965e0b78860b69d34077a
3
+ metadata.gz: 6977532a681a67182fb89b165bae55ac45261000
4
+ data.tar.gz: 85be7d1ed65bc938f1356cdde5e317ae6f8b212c
5
5
  SHA512:
6
- metadata.gz: ee2e998ccc19888dacf81e883cdb21f5057bfc61c90a0ad1c7e157dd94cd3695045fa323d4d07d2018f617ff681994b40d08b40ec4fcadf07fd5b010ab5a60ab
7
- data.tar.gz: d15b51f1788896d289ef0bbd5dfa038936a7da4a23871bc517b4850486987df44af070da79e30a3b18a0926f63ba3acca8a17c653a386e98424bb8b828235400
6
+ metadata.gz: 1cb92234e9f79112666e167301c311b2e081ebeb965f32310e21f423e8be21622ce2f19f528c770f33b48eb481d696dd52055db1402c0f0c7d19a4cf25d9074f
7
+ data.tar.gz: cc47484d072601dab2720455b237310bf4d344e0d5b9990fc628d6c11a8626fb14d02178898f9dc454b39af352f72144eba595cd06553d2dad07024b83c545c5
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
- class Client
11
- LOGGER = Logger.new(STDOUT)
12
- LOGGER.level = Logger::WARN
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
- attr_writer :running
63
+ class Client
15
64
 
16
65
  def self.run
17
66
  c = Client.new
18
- Signal.trap('INT'){c.running = false}
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
- while @running
32
- [:announce,
33
- :discover,
34
- :watch_incoming,
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
- def fetch_clipboard
45
- update_clipboard(Clipboard.paste)
46
- end
89
+ @announce_thread.join
47
90
 
48
- def update_clipboard(new_value)
49
- @new_value,@last_value = new_value,@new_value
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 send_new_clipboard(connection)
56
- connection.send(@new_value)
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 watch_incoming
60
- @connections.values.each{|c| receive_clipboard(c)}
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 receive_clipboard(connection)
64
- inbound = connection.receive(false)
65
- Clipboard.copy(inbound) if inbound
66
- end
134
+ def incoming_loop
135
+ while @running
136
+ incoming = @tcp_server.accept
137
+ aes_connection = AESConnection.new(@config,incoming)
67
138
 
68
- def announce
69
- @discovery.announce
70
- end
139
+ LOGGER.debug("Incoming #{aes_connection.remote_client_id} from #{incoming.peeraddr(false)[-1]}")
71
140
 
72
- def discover
73
- @discovery.get_peer_announcements do |remote_client_id,address|
74
- next if @connections[remote_client_id] or remote_client_id < @config.client_id
75
- LOGGER.info("New Peer -> #{remote_client_id}")
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
- aes_connection = AESConnection.new(@config,TCPSocket.new(address,@config.port))
148
+ LOGGER.info("New Peer <- #{aes_connection.remote_client_id}")
78
149
 
79
- if aes_connection.remote_client_id != remote_client_id
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 find_incoming
96
- while true
97
- begin
98
- incoming = @tcp_server.accept_nonblock
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
- aes_connection = AESConnection.new(@config,incoming)
103
- LOGGER.info("New Peer <- #{aes_connection.remote_client_id}")
160
+ ensure
161
+ connection.close
104
162
 
105
- if @connections[aes_connection.remote_client_id]
106
- aes_connection.close
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 wait
115
- return IO.select([
116
- @discovery.receive_socket,
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 < Exception
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.recv(4).unpack('N')[0]
33
- @decryptor.iv = @socket.recv(iv_size)
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(blocking=true)
51
- begin
52
- @partial_read ||= ''
53
- while @partial_read.size < 4
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,@partial_read = @partial_read.slice(4,@partial_read.size-4),nil
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 close
77
- @socket.close
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 get_peer_announcements
43
- return enum_for(__method__) if !block_given?
44
- begin
45
- while true
46
- msg,ip = @receive_socket.recvfrom_nonblock(65536)
47
- other_client_id,other_digest = msg.unpack('H32A32')
48
-
49
- next if other_client_id == @config.client_id
50
-
51
- # We could do a constant time string compare, but an attacker can
52
- # just listen to announces and rebroadcast them as his own anyway.
53
- # This is just to skip other honest clients with different secrets
54
- # on the network, to avoid wasting time on a handshake.
55
- next unless other_digest == Digest::HMAC.digest(@config.secret,other_client_id,Digest::SHA256)
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
@@ -0,0 +1,4 @@
1
+ module NetworkClipboard
2
+ class NetworkClipboardError < StandardError
3
+ end
4
+ 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.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