meshchat 0.5.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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1 -0
  3. data/README.md +7 -0
  4. data/lib/meshchat.rb +73 -0
  5. data/lib/meshchat/cli.rb +164 -0
  6. data/lib/meshchat/cli/command.rb +63 -0
  7. data/lib/meshchat/cli/config.rb +30 -0
  8. data/lib/meshchat/cli/exit.rb +9 -0
  9. data/lib/meshchat/cli/identity.rb +9 -0
  10. data/lib/meshchat/cli/import.rb +37 -0
  11. data/lib/meshchat/cli/init.rb +34 -0
  12. data/lib/meshchat/cli/input.rb +60 -0
  13. data/lib/meshchat/cli/irb.rb +18 -0
  14. data/lib/meshchat/cli/listen.rb +9 -0
  15. data/lib/meshchat/cli/ping.rb +61 -0
  16. data/lib/meshchat/cli/ping_all.rb +11 -0
  17. data/lib/meshchat/cli/server.rb +16 -0
  18. data/lib/meshchat/cli/share.rb +9 -0
  19. data/lib/meshchat/cli/stop_listening.rb +9 -0
  20. data/lib/meshchat/cli/whisper.rb +34 -0
  21. data/lib/meshchat/cli/who.rb +9 -0
  22. data/lib/meshchat/config/hash_file.rb +75 -0
  23. data/lib/meshchat/config/settings.rb +112 -0
  24. data/lib/meshchat/database.rb +30 -0
  25. data/lib/meshchat/display.rb +32 -0
  26. data/lib/meshchat/display/base.rb +53 -0
  27. data/lib/meshchat/display/manager.rb +51 -0
  28. data/lib/meshchat/encryption.rb +27 -0
  29. data/lib/meshchat/encryption/aes_rsa.rb +65 -0
  30. data/lib/meshchat/encryption/passthrough.rb +17 -0
  31. data/lib/meshchat/instance.rb +34 -0
  32. data/lib/meshchat/message.rb +38 -0
  33. data/lib/meshchat/message/base.rb +93 -0
  34. data/lib/meshchat/message/chat.rb +14 -0
  35. data/lib/meshchat/message/disconnection.rb +13 -0
  36. data/lib/meshchat/message/node_list.rb +41 -0
  37. data/lib/meshchat/message/node_list_diff.rb +15 -0
  38. data/lib/meshchat/message/node_list_hash.rb +29 -0
  39. data/lib/meshchat/message/ping.rb +32 -0
  40. data/lib/meshchat/message/ping_reply.rb +9 -0
  41. data/lib/meshchat/message/relay.rb +43 -0
  42. data/lib/meshchat/message/whisper.rb +36 -0
  43. data/lib/meshchat/models/entry.rb +104 -0
  44. data/lib/meshchat/net/client.rb +60 -0
  45. data/lib/meshchat/net/listener/request.rb +48 -0
  46. data/lib/meshchat/net/listener/request_processor.rb +45 -0
  47. data/lib/meshchat/net/listener/server.rb +61 -0
  48. data/lib/meshchat/net/request.rb +29 -0
  49. data/lib/meshchat/notifier/base.rb +50 -0
  50. data/lib/meshchat/version.rb +3 -0
  51. metadata +288 -0
@@ -0,0 +1,41 @@
1
+ module MeshChat
2
+ module Message
3
+ class NodeList < Base
4
+ def message
5
+ @message ||= Node.as_json
6
+ end
7
+
8
+ def handle
9
+ respond
10
+ return
11
+ end
12
+
13
+ # only need to respond if this server has node entries that the
14
+ # sender of this message doesn't have
15
+ def respond
16
+ received_list = message
17
+ we_only_have, they_only_have = Node.diff(received_list)
18
+
19
+ if we_only_have.present?
20
+ location = payload['sender']['location']
21
+
22
+ node = Node.find_by_location(location)
23
+
24
+ # give the sender our list
25
+ MeshChat::Net::Client.send(
26
+ node: node,
27
+ message: NodeListDiff.new(message: we_only_have)
28
+ )
29
+
30
+ # give the network their list
31
+ Node.online.each do |entry|
32
+ MeshChat::Net::Client.send(
33
+ node: entry,
34
+ message: NodeListDiff.new(message: they_only_have)
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module MeshChat
2
+ module Message
3
+ class NodeListDiff < Base
4
+ def handle
5
+ entries_we_do_not_have = message
6
+
7
+ entries_we_do_not_have.each do |entry_as_json|
8
+ # this will silently fail if there is a duplicate
9
+ # or if this is an invalid entry
10
+ Node.from_json(entry_as_json).save
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ module MeshChat
2
+ module Message
3
+ class NodeListHash < Base
4
+ def message
5
+ @message ||= Node.as_sha512
6
+ end
7
+
8
+ # node list hash is received
9
+ # @return [NilClass] no output for this message type
10
+ def handle
11
+ respond
12
+ return
13
+ end
14
+
15
+ def respond
16
+ if message != Node.as_sha512
17
+ location = payload['sender']['location']
18
+
19
+ node = Node.find_by_location(location)
20
+
21
+ MeshChat::Net::Client.send(
22
+ node: node,
23
+ message: NodeList.new(message: Node.as_json)
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module MeshChat
2
+ module Message
3
+ class Ping < Base
4
+
5
+ def display
6
+ # we'll never display our own ping to someone else...
7
+ # or shouldn't.... or there should be different output
8
+ # TODO: display is a bad method name
9
+ name = payload['sender']['alias']
10
+ location = payload['sender']['location']
11
+
12
+ "#{name}@#{location} pinged you."
13
+ end
14
+
15
+ def handle
16
+ respond
17
+ display
18
+ end
19
+
20
+ def respond
21
+ location = payload['sender']['location']
22
+
23
+ node = Node.find_by_location(location)
24
+
25
+ MeshChat::Net::Client.send(
26
+ node: node,
27
+ message: PingReply.new
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,9 @@
1
+ module MeshChat
2
+ module Message
3
+ class PingReply < Base
4
+ def display
5
+ 'ping successful'.freeze
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ module MeshChat
2
+ module Message
3
+ class Relay < Base
4
+ def initialize(
5
+ message: nil,
6
+ sender_name: nil,
7
+ sender_location: nil,
8
+ sender_uid: nil,
9
+ time_recieved: nil,
10
+ payload: nil,
11
+ destination: '',
12
+ hops: [])
13
+
14
+ # package the original message
15
+ message = {
16
+ message: message,
17
+ destination: destination,
18
+ hops: hops
19
+ }
20
+
21
+ super(
22
+ message: message,
23
+ sender_name: sender_name,
24
+ sender_location: sender_location,
25
+ sender_uid: sender_uid,
26
+ time_recieved: time_recieved,
27
+ payload: payload)
28
+ end
29
+
30
+ def display; end
31
+
32
+ def handle
33
+ respond
34
+ return
35
+ end
36
+
37
+ def respond
38
+
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ module MeshChat
2
+ module Message
3
+ class Whisper < Base
4
+ attr_accessor :_to
5
+
6
+ def initialize(
7
+ message: nil,
8
+ sender_name: nil,
9
+ sender_location: nil,
10
+ sender_uid: nil,
11
+ time_recieved: nil,
12
+ payload: nil,
13
+ to: '')
14
+
15
+ super(
16
+ message: message,
17
+ sender_name: sender_name,
18
+ sender_location: sender_location,
19
+ sender_uid: sender_uid,
20
+ time_recieved: time_recieved,
21
+ payload: payload)
22
+
23
+ self._to = to
24
+ end
25
+
26
+ def display
27
+ time_sent = payload['time_sent'].to_s
28
+ time = Date.parse(time_sent)
29
+ time_recieved = time.strftime('%e/%m/%y %H:%I:%M')
30
+
31
+ to = _to.present? ? "->#{_to}" : ''
32
+ "#{time_recieved} #{sender_name}#{to} > #{message}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,104 @@
1
+ module MeshChat
2
+ module Models
3
+ class Entry < ActiveRecord::Base
4
+ IPV4_WITH_PORT = /((?:(?:^|\.)(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])){4})(:\d*)?/
5
+ # http://rubular.com/r/WYT09ptct3
6
+ DOMAIN_WITH_PORT = /(https?:\/\/)?([\da-z\.-]+)\.?([a-z\.]{2,6})([\/\w \.-]*)*[^\/](:\d*)?/
7
+
8
+ validates :alias_name,
9
+ :location,
10
+ :public_key, presence: true
11
+
12
+ validates :uid, presence: true, uniqueness: true
13
+
14
+ # ipv4 with port
15
+ validates_format_of :location, with: ->(e){
16
+ location = e.location || ''
17
+ location.include?('//') || location.include?('localhost') ?
18
+ DOMAIN_WITH_PORT :
19
+ IPV4_WITH_PORT
20
+ }
21
+
22
+ scope :online, -> { where(online: true) }
23
+ scope :offline, -> { where(online: false) }
24
+
25
+ class << self
26
+ def sha_preimage
27
+ all.map(&:public_key).sort.join(',')
28
+ end
29
+
30
+ def as_sha512
31
+ digest = Digest::SHA512.new
32
+ digest.hexdigest sha_preimage
33
+ end
34
+
35
+ def as_json
36
+ # must also include ourselves
37
+ # so that we can pass our own public key
38
+ # to those who don't have it
39
+ others = all.map(&:as_json)
40
+ me = Settings.identity_as_json
41
+ others << me
42
+ end
43
+
44
+ def from_json(json)
45
+ new(
46
+ alias_name: json['alias'],
47
+ location: json['location'],
48
+ uid: json['uid'],
49
+ public_key: json['publicKey']
50
+ )
51
+ end
52
+
53
+ def public_key_from_uid(uid)
54
+ find_by_uid(uid).try(:public_key)
55
+ end
56
+
57
+ # @param [Array] theirs array of hashes representing node entries
58
+ # @return [Array<-,+>] nodes only we have, and nodes only they have
59
+ def diff(theirs)
60
+ ours = as_json
61
+ we_only_have = ours - theirs
62
+ they_only_have = theirs - ours
63
+
64
+ [we_only_have, they_only_have]
65
+ end
66
+
67
+ def import_from_file(filename)
68
+ begin
69
+ f = File.read(filename)
70
+ hash = JSON.parse(f)
71
+ n = from_json(hash)
72
+ n.save
73
+ n
74
+ rescue => e
75
+ Display.alert e.message
76
+ end
77
+ end
78
+ end
79
+
80
+ def ==(other)
81
+ result = false
82
+
83
+ if other.is_a?(Hash)
84
+ result = as_json.values_at(*other.keys) == other.values
85
+ end
86
+
87
+ result || super
88
+ end
89
+
90
+ def as_json
91
+ {
92
+ 'alias' => alias_name,
93
+ 'location' => location,
94
+ 'uid' => uid,
95
+ 'publicKey' => public_key
96
+ }
97
+ end
98
+
99
+ def as_info
100
+ "#{alias_name}@#{location}"
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,60 @@
1
+ module MeshChat
2
+ module Net
3
+ module Client
4
+ module_function
5
+
6
+ # @note Either the location, node, or uid should be present
7
+ #
8
+ # @param [String] location (Optional) location of target
9
+ # @param [String] uid (Optional) uid of target
10
+ # @param [Node] node (Optional) target
11
+ # @param [Message] message (Required) what to send to the target
12
+ def send(location: nil, uid: nil, node: nil, message: nil)
13
+ # verify node is valid
14
+ node = self.node_for(location: location, uid: uid, node: node)
15
+
16
+ Thread.new(node, message) do |node, message|
17
+ request = MeshChat::Net::Request.new(node, message)
18
+ payload = { message: request.payload }
19
+ begin
20
+ Curl::Easy.http_post(node.location, payload.to_json) do |c|
21
+ c.headers['Accept'] = 'application/json'
22
+ c.headers['Content-Type'] = 'application/json'
23
+ if MeshChat::Settings.debug?
24
+ puts message.render
25
+ c.verbose = true
26
+ c.on_debug do |type, data|
27
+ puts data
28
+ end
29
+ end
30
+ end
31
+ rescue => e
32
+ node.update(online: false)
33
+ Display.info "#{node.alias_name} has ventured offline"
34
+ Display.debug("#{message.class.name}: Issue connectiong to #{node.alias_name}@#{node.location}")
35
+ Display.debug(e.message)
36
+ end
37
+ end
38
+ end
39
+
40
+ # private
41
+
42
+ # @return [Node]
43
+ def node_for(location: nil, uid: nil, node: nil)
44
+ unless node
45
+ node = Models::Entry.find_by_location(location) if location
46
+ node = Models::Entry.find_by_uid(uid) if uid && !node
47
+ end
48
+
49
+ # TODO: also check for public key?
50
+ # without the public key, the message is sent in cleartext. :-\
51
+ if !(node && node.location)
52
+ Display.alert "Node not found, or does not have a location"
53
+ return
54
+ end
55
+
56
+ node
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,48 @@
1
+ module MeshChat
2
+ module Net
3
+ module Listener
4
+ class Request
5
+ attr_accessor :json, :message
6
+ attr_accessor :_input
7
+
8
+ def initialize(input)
9
+ self._input = try_decrypt(input)
10
+ self.json = JSON.parse(_input)
11
+ self.message = process_json
12
+ end
13
+
14
+ private
15
+
16
+ def try_decrypt(input)
17
+ begin
18
+ # TODO: do we want to try to decrypting anyway if decoding fails?
19
+ decoded = Base64.decode64(input)
20
+ input = Cipher.decrypt(decoded, Settings[:privateKey])
21
+ rescue => e
22
+ Display.debug e.message
23
+ Display.debug e.backtrace.join("\n")
24
+ Display.warning e.message
25
+ Display.info 'It\'s possible that this message was sent in cleartext, or was encrypted with the wrong public key'
26
+ end
27
+
28
+ Display.debug 'server received message:'
29
+ Display.debug input
30
+
31
+ input
32
+ end
33
+
34
+ def process_json
35
+ type = json['type']
36
+ klass = Message::TYPES[type]
37
+
38
+ unless klass
39
+ Display.alert 'message recieved and not recognized...'
40
+ return
41
+ end
42
+
43
+ klass.new(payload: json)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ module MeshChat
2
+ module Net
3
+ module Listener
4
+ module RequestProcessor
5
+
6
+ module_function
7
+
8
+ def process(raw)
9
+ request = Request.new(raw)
10
+
11
+ message = request.message
12
+ update_sender_info(request.json)
13
+
14
+ Display.present_message message
15
+ end
16
+
17
+ def update_sender_info(json)
18
+ sender = json['sender']
19
+
20
+ # if the sender isn't currently marked as active,
21
+ # perform the server list exchange
22
+ node = Node.find_by_uid(sender['uid'])
23
+ if node.nil?
24
+ return Display.alert "#{sender['alias']} is not authorized!"
25
+ end
26
+
27
+ unless node.online?
28
+ node.update(online: true)
29
+ payload = Message::NodeListHash.new
30
+ Client.send(
31
+ location: sender['location'],
32
+ message: payload)
33
+ end
34
+
35
+ # update the node's location/alias
36
+ # as they can change this info willy nilly
37
+ node.update(
38
+ location: sender['location'],
39
+ alias_name: sender['alias']
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end