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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1 -0
- data/README.md +7 -0
- data/lib/meshchat.rb +73 -0
- data/lib/meshchat/cli.rb +164 -0
- data/lib/meshchat/cli/command.rb +63 -0
- data/lib/meshchat/cli/config.rb +30 -0
- data/lib/meshchat/cli/exit.rb +9 -0
- data/lib/meshchat/cli/identity.rb +9 -0
- data/lib/meshchat/cli/import.rb +37 -0
- data/lib/meshchat/cli/init.rb +34 -0
- data/lib/meshchat/cli/input.rb +60 -0
- data/lib/meshchat/cli/irb.rb +18 -0
- data/lib/meshchat/cli/listen.rb +9 -0
- data/lib/meshchat/cli/ping.rb +61 -0
- data/lib/meshchat/cli/ping_all.rb +11 -0
- data/lib/meshchat/cli/server.rb +16 -0
- data/lib/meshchat/cli/share.rb +9 -0
- data/lib/meshchat/cli/stop_listening.rb +9 -0
- data/lib/meshchat/cli/whisper.rb +34 -0
- data/lib/meshchat/cli/who.rb +9 -0
- data/lib/meshchat/config/hash_file.rb +75 -0
- data/lib/meshchat/config/settings.rb +112 -0
- data/lib/meshchat/database.rb +30 -0
- data/lib/meshchat/display.rb +32 -0
- data/lib/meshchat/display/base.rb +53 -0
- data/lib/meshchat/display/manager.rb +51 -0
- data/lib/meshchat/encryption.rb +27 -0
- data/lib/meshchat/encryption/aes_rsa.rb +65 -0
- data/lib/meshchat/encryption/passthrough.rb +17 -0
- data/lib/meshchat/instance.rb +34 -0
- data/lib/meshchat/message.rb +38 -0
- data/lib/meshchat/message/base.rb +93 -0
- data/lib/meshchat/message/chat.rb +14 -0
- data/lib/meshchat/message/disconnection.rb +13 -0
- data/lib/meshchat/message/node_list.rb +41 -0
- data/lib/meshchat/message/node_list_diff.rb +15 -0
- data/lib/meshchat/message/node_list_hash.rb +29 -0
- data/lib/meshchat/message/ping.rb +32 -0
- data/lib/meshchat/message/ping_reply.rb +9 -0
- data/lib/meshchat/message/relay.rb +43 -0
- data/lib/meshchat/message/whisper.rb +36 -0
- data/lib/meshchat/models/entry.rb +104 -0
- data/lib/meshchat/net/client.rb +60 -0
- data/lib/meshchat/net/listener/request.rb +48 -0
- data/lib/meshchat/net/listener/request_processor.rb +45 -0
- data/lib/meshchat/net/listener/server.rb +61 -0
- data/lib/meshchat/net/request.rb +29 -0
- data/lib/meshchat/notifier/base.rb +50 -0
- data/lib/meshchat/version.rb +3 -0
- 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,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
|