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
|