network_clipboard 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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