meshchat 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|