network_clipboard 0.0.0
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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/bin/network_clipboard +5 -0
- data/lib/network_clipboard/client.rb +121 -0
- data/lib/network_clipboard/config.rb +41 -0
- data/lib/network_clipboard/connection.rb +80 -0
- data/lib/network_clipboard/discovery.rb +63 -0
- data/lib/network_clipboard.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +90 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8563a699ffa840eaa49ab7029b1c69dc5895b986
|
4
|
+
data.tar.gz: 37be775586fe6006e9d965e0b78860b69d34077a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ee2e998ccc19888dacf81e883cdb21f5057bfc61c90a0ad1c7e157dd94cd3695045fa323d4d07d2018f617ff681994b40d08b40ec4fcadf07fd5b010ab5a60ab
|
7
|
+
data.tar.gz: d15b51f1788896d289ef0bbd5dfa038936a7da4a23871bc517b4850486987df44af070da79e30a3b18a0926f63ba3acca8a17c653a386e98424bb8b828235400
|
checksums.yaml.gz.sig
ADDED
Binary file
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require_relative 'config'
|
2
|
+
require_relative 'discovery'
|
3
|
+
require_relative 'connection'
|
4
|
+
|
5
|
+
require 'clipboard'
|
6
|
+
require 'socket'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
module NetworkClipboard
|
10
|
+
class Client
|
11
|
+
LOGGER = Logger.new(STDOUT)
|
12
|
+
LOGGER.level = Logger::WARN
|
13
|
+
|
14
|
+
attr_writer :running
|
15
|
+
|
16
|
+
def self.run
|
17
|
+
c = Client.new
|
18
|
+
Signal.trap('INT'){c.running = false}
|
19
|
+
c.loop
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@config = Config.new
|
24
|
+
@discovery = Discovery.new(@config)
|
25
|
+
@tcp_server = TCPServer.new(@config.port)
|
26
|
+
@connections = {}
|
27
|
+
@running = true
|
28
|
+
end
|
29
|
+
|
30
|
+
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
|
43
|
+
|
44
|
+
def fetch_clipboard
|
45
|
+
update_clipboard(Clipboard.paste)
|
46
|
+
end
|
47
|
+
|
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)}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def send_new_clipboard(connection)
|
56
|
+
connection.send(@new_value)
|
57
|
+
end
|
58
|
+
|
59
|
+
def watch_incoming
|
60
|
+
@connections.values.each{|c| receive_clipboard(c)}
|
61
|
+
end
|
62
|
+
|
63
|
+
def receive_clipboard(connection)
|
64
|
+
inbound = connection.receive(false)
|
65
|
+
Clipboard.copy(inbound) if inbound
|
66
|
+
end
|
67
|
+
|
68
|
+
def announce
|
69
|
+
@discovery.announce
|
70
|
+
end
|
71
|
+
|
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}")
|
76
|
+
|
77
|
+
aes_connection = AESConnection.new(@config,TCPSocket.new(address,@config.port))
|
78
|
+
|
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
|
83
|
+
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
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_incoming
|
96
|
+
while true
|
97
|
+
begin
|
98
|
+
incoming = @tcp_server.accept_nonblock
|
99
|
+
rescue IO::WaitReadable
|
100
|
+
return
|
101
|
+
end
|
102
|
+
aes_connection = AESConnection.new(@config,incoming)
|
103
|
+
LOGGER.info("New Peer <- #{aes_connection.remote_client_id}")
|
104
|
+
|
105
|
+
if @connections[aes_connection.remote_client_id]
|
106
|
+
aes_connection.close
|
107
|
+
next
|
108
|
+
end
|
109
|
+
|
110
|
+
@connections[aes_connection.remote_client_id] = aes_connection
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def wait
|
115
|
+
return IO.select([
|
116
|
+
@discovery.receive_socket,
|
117
|
+
@tcp_server,
|
118
|
+
] + @connections.values.collect(&:socket), [], [], 5)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module NetworkClipboard
|
5
|
+
class Config
|
6
|
+
DEFAULTS = {
|
7
|
+
# http://tools.ietf.org/html/rfc2365
|
8
|
+
# Ultimately though, we'd need to reserve a specific address
|
9
|
+
# or implemend MADCAP or ZMAAP (Except nothing supports those).
|
10
|
+
multicast_ip: '239.255.193.172',
|
11
|
+
# Randomly picked by mashing the keyboard.
|
12
|
+
port: 53712,
|
13
|
+
# This file needs to be shared to enable clipboard transfer.
|
14
|
+
secret_file: '~/.networkclipboard.secret',
|
15
|
+
}
|
16
|
+
|
17
|
+
attr_reader :multicast_ip, :port, :secret, :client_id
|
18
|
+
|
19
|
+
def initialize(filename='~/.networkclipboard.conf')
|
20
|
+
filename = File.expand_path(filename)
|
21
|
+
begin
|
22
|
+
parsed = YAML.load(File.read(filename))
|
23
|
+
rescue Errno::ENOENT
|
24
|
+
parsed = {}
|
25
|
+
end
|
26
|
+
config = DEFAULTS.merge(parsed)
|
27
|
+
|
28
|
+
secret_filename = File.expand_path(config[:secret_file])
|
29
|
+
begin
|
30
|
+
@secret = [File.read(secret_filename)].pack('H*')
|
31
|
+
rescue Errno::ENOENT
|
32
|
+
@secret = SecureRandom.random_bytes(32)
|
33
|
+
File.open(secret_filename,'w',0400){|f|f.write(secret.unpack('H*')[0])}
|
34
|
+
end
|
35
|
+
|
36
|
+
@multicast_ip = config[:multicast_ip]
|
37
|
+
@port = config[:port]
|
38
|
+
@client_id = SecureRandom.uuid.gsub('-','')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
module NetworkClipboard
|
5
|
+
HANDSHAKE_STRING = "NetworkClipboard Handshake"
|
6
|
+
|
7
|
+
class HandshakeException < Exception
|
8
|
+
end
|
9
|
+
|
10
|
+
class AESConnection
|
11
|
+
|
12
|
+
attr_reader :remote_client_id, :socket
|
13
|
+
|
14
|
+
def initialize(config,socket)
|
15
|
+
@socket = socket
|
16
|
+
|
17
|
+
@encryptor = OpenSSL::Cipher::AES.new(128, :CBC)
|
18
|
+
@encryptor.encrypt
|
19
|
+
@encryptor.key = config.secret
|
20
|
+
|
21
|
+
@decryptor = OpenSSL::Cipher::AES.new(128, :CBC)
|
22
|
+
@decryptor.decrypt
|
23
|
+
@decryptor.key = config.secret
|
24
|
+
|
25
|
+
handshake(config.client_id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def handshake(client_id)
|
29
|
+
iv = @encryptor.random_iv
|
30
|
+
@socket.send([iv.size].pack('N'),0)
|
31
|
+
@socket.send(iv,0)
|
32
|
+
iv_size = @socket.recv(4).unpack('N')[0]
|
33
|
+
@decryptor.iv = @socket.recv(iv_size)
|
34
|
+
|
35
|
+
# Verify it all worked.
|
36
|
+
send(HANDSHAKE_STRING)
|
37
|
+
raise HandshakeException unless receive == HANDSHAKE_STRING
|
38
|
+
|
39
|
+
send(client_id)
|
40
|
+
@remote_client_id = receive
|
41
|
+
end
|
42
|
+
|
43
|
+
def send(new_content)
|
44
|
+
ciphertext = @encryptor.update(new_content) + @encryptor.final
|
45
|
+
@socket.send([ciphertext.size].pack('N'),0)
|
46
|
+
@socket.send(ciphertext,0)
|
47
|
+
@encryptor.reset
|
48
|
+
end
|
49
|
+
|
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
|
66
|
+
end
|
67
|
+
|
68
|
+
ciphertext,@partial_read = @partial_read.slice(4,@partial_read.size-4),nil
|
69
|
+
|
70
|
+
plaintext = @decryptor.update(ciphertext) + @decryptor.final
|
71
|
+
@decryptor.reset
|
72
|
+
|
73
|
+
return plaintext
|
74
|
+
end
|
75
|
+
|
76
|
+
def close
|
77
|
+
@socket.close
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'ipaddr'
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
module NetworkClipboard
|
6
|
+
class Discovery
|
7
|
+
attr_reader :receive_socket
|
8
|
+
|
9
|
+
def initialize(config)
|
10
|
+
@config = config
|
11
|
+
|
12
|
+
@receive_socket = UDPSocket.new()
|
13
|
+
@receive_socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, multicast_addr.hton + bind_addr.hton)
|
14
|
+
@receive_socket.bind(bind_addr.to_s,port)
|
15
|
+
|
16
|
+
@send_socket = UDPSocket.new()
|
17
|
+
@send_socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1)
|
18
|
+
@send_socket.connect(multicast_addr.to_s,port)
|
19
|
+
|
20
|
+
@authenticated_client_id = [
|
21
|
+
@config.client_id,
|
22
|
+
Digest::HMAC.digest(@config.secret,@config.client_id,Digest::SHA256),
|
23
|
+
].pack("H32A32")
|
24
|
+
end
|
25
|
+
|
26
|
+
def port
|
27
|
+
@port ||= @config.port
|
28
|
+
end
|
29
|
+
|
30
|
+
def bind_addr
|
31
|
+
@bind_addr ||= IPAddr.new('0.0.0.0')
|
32
|
+
end
|
33
|
+
|
34
|
+
def multicast_addr
|
35
|
+
@multicast_addr ||= IPAddr.new(@config.multicast_ip)
|
36
|
+
end
|
37
|
+
|
38
|
+
def announce
|
39
|
+
@send_socket.send(@authenticated_client_id,0)
|
40
|
+
end
|
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
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'network_clipboard/client'
|
data.tar.gz.sig
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: network_clipboard
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Christophe Biocca
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIDljCCAn6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBIMRowGAYDVQQDDBFjaHJp
|
14
|
+
c3RvcGhlLmJpb2NjYTEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
|
15
|
+
LGQBGRYDY29tMB4XDTE0MDkyMjA0MTAwMloXDTE1MDkyMjA0MTAwMlowSDEaMBgG
|
16
|
+
A1UEAwwRY2hyaXN0b3BoZS5iaW9jY2ExFTATBgoJkiaJk/IsZAEZFgVnbWFpbDET
|
17
|
+
MBEGCgmSJomT8ixkARkWA2NvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
18
|
+
ggEBAMO5U6bV+OsGcszf0Y5inNKNrJlnxaRJ+mOnk3OYlzqf0Ylpp9v65SNNxntA
|
19
|
+
NY75ZKTzBUkF5o8FkiXJ+eueKqyaDmqjuyFavfihQ/1e1MXnn76EI7JYZW3Mkjox
|
20
|
+
r6d9BIlY4rv55llkPBut11WZ0AoCSku3eTLB/Y4TgYpiQvsSjmHO28nTuiUmcbmm
|
21
|
+
PR7/EP000pbphXXo/GSLOZ+jy4wc09jsN/1AFB6TErvh3JFh1bW6NAy7vd/JyVbH
|
22
|
+
TLZ59kOD3ThhW1ZS+K/n/uYtFNzQRvRb9X/9yfifPAKzJpPiueJnofCBJ0VyPQ3j
|
23
|
+
dxoWi/m3N1Z9VS7/FSJ1g6gn+tECAwEAAaOBijCBhzAJBgNVHRMEAjAAMAsGA1Ud
|
24
|
+
DwQEAwIEsDAdBgNVHQ4EFgQUgihGjqqGQaGMoqaiqV2b0F0BBvEwJgYDVR0RBB8w
|
25
|
+
HYEbY2hyaXN0b3BoZS5iaW9jY2FAZ21haWwuY29tMCYGA1UdEgQfMB2BG2Nocmlz
|
26
|
+
dG9waGUuYmlvY2NhQGdtYWlsLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAgOeBPrTF
|
27
|
+
lvsQZVb+mqAI9WdF+nzRtjEUm7G1fGGWdOwT/DQO9g1jZ25R0/TNDax2tVupcRFE
|
28
|
+
OXvcOxJ1DkxJVe85HOybQsPucOCta4/hvdbA15uRKA5Jn3scM1RUfGxgwGHQLXjb
|
29
|
+
ntT9ySByz4fkLUp+NGYVL+VTob4XOlqCoJOrXkWlwkEKXtLXYvRL7/WpTb6qaIOc
|
30
|
+
L0Swr8sZae6mM5w1yyW/EpxG0JR90oqvw83ObXBmQVgffPLwJWHe4Ioysg7/UhY+
|
31
|
+
jeq3OvJDSi2MARCkZcHB7bVpVXSw+nwsP3J4RPs3JFn3m6utnIpc9lV+lt3XcR+9
|
32
|
+
o5+PMiZrAMX0bA==
|
33
|
+
-----END CERTIFICATE-----
|
34
|
+
date: 2014-05-21 00:00:00.000000000 Z
|
35
|
+
dependencies:
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: clipboard
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '1.0'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '1.0'
|
50
|
+
description: |
|
51
|
+
Allows sharing of clipboard between multiple machines on the same network.
|
52
|
+
Encrypted using AES-128-CBC, relies on pre-shared secret file.
|
53
|
+
Internal API not stable yet. Only use the executable.
|
54
|
+
email: christophe.biocca@gmail.com
|
55
|
+
executables:
|
56
|
+
- network_clipboard
|
57
|
+
extensions: []
|
58
|
+
extra_rdoc_files: []
|
59
|
+
files:
|
60
|
+
- bin/network_clipboard
|
61
|
+
- lib/network_clipboard.rb
|
62
|
+
- lib/network_clipboard/client.rb
|
63
|
+
- lib/network_clipboard/config.rb
|
64
|
+
- lib/network_clipboard/connection.rb
|
65
|
+
- lib/network_clipboard/discovery.rb
|
66
|
+
homepage: http://github.com/christophebiocca/network_clipboard
|
67
|
+
licenses:
|
68
|
+
- Apache-2.0
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 2.2.2
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Network Clipboard Sharing
|
90
|
+
test_files: []
|
metadata.gz.sig
ADDED
Binary file
|