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 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,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'network_clipboard'
4
+
5
+ NetworkClipboard::Client.run
@@ -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