meshchat 0.8.0 → 0.10.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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +39 -11
  3. data/lib/meshchat.rb +48 -42
  4. data/lib/meshchat/configuration.rb +14 -0
  5. data/lib/meshchat/configuration/app_config.rb +63 -0
  6. data/lib/meshchat/configuration/database.rb +41 -0
  7. data/lib/meshchat/{config → configuration}/hash_file.rb +7 -6
  8. data/lib/meshchat/configuration/identity.rb +79 -0
  9. data/lib/meshchat/{config → configuration}/settings.rb +22 -26
  10. data/lib/meshchat/debug.rb +69 -0
  11. data/lib/meshchat/encryption.rb +7 -2
  12. data/lib/meshchat/encryption/aes_rsa.rb +2 -1
  13. data/lib/meshchat/encryption/passthrough.rb +5 -3
  14. data/lib/meshchat/locale/en.yml +14 -0
  15. data/lib/meshchat/models/node.rb +140 -0
  16. data/lib/meshchat/network.rb +19 -0
  17. data/lib/meshchat/network/dispatcher.rb +83 -0
  18. data/lib/meshchat/network/errors.rb +11 -0
  19. data/lib/meshchat/network/incoming.rb +13 -0
  20. data/lib/meshchat/network/incoming/message_decryptor.rb +51 -0
  21. data/lib/meshchat/network/incoming/message_processor.rb +75 -0
  22. data/lib/meshchat/network/incoming/request_processor.rb +30 -0
  23. data/lib/meshchat/network/local.rb +12 -0
  24. data/lib/meshchat/network/local/connection.rb +58 -0
  25. data/lib/meshchat/network/local/server.rb +69 -0
  26. data/lib/meshchat/network/message.rb +34 -0
  27. data/lib/meshchat/network/message/base.rb +139 -0
  28. data/lib/meshchat/network/message/chat.rb +9 -0
  29. data/lib/meshchat/network/message/disconnect.rb +21 -0
  30. data/lib/meshchat/network/message/emote.rb +9 -0
  31. data/lib/meshchat/network/message/factory.rb +80 -0
  32. data/lib/meshchat/network/message/node_list.rb +75 -0
  33. data/lib/meshchat/network/message/node_list_diff.rb +18 -0
  34. data/lib/meshchat/network/message/node_list_hash.rb +32 -0
  35. data/lib/meshchat/network/message/ping.rb +31 -0
  36. data/lib/meshchat/network/message/ping_reply.rb +12 -0
  37. data/lib/meshchat/network/message/whisper.rb +28 -0
  38. data/lib/meshchat/network/remote.rb +13 -0
  39. data/lib/meshchat/network/remote/connection.rb +28 -0
  40. data/lib/meshchat/network/remote/relay.rb +109 -0
  41. data/lib/meshchat/network/remote/relay_pool.rb +52 -0
  42. data/lib/meshchat/ui.rb +13 -0
  43. data/lib/meshchat/ui/cli.rb +48 -0
  44. data/lib/meshchat/ui/cli/base.rb +39 -0
  45. data/lib/meshchat/ui/cli/input_factory.rb +50 -0
  46. data/lib/meshchat/ui/cli/keyboard_line_input.rb +14 -0
  47. data/lib/meshchat/ui/command.rb +51 -0
  48. data/lib/meshchat/ui/command/base.rb +77 -0
  49. data/lib/meshchat/ui/command/bind.rb +47 -0
  50. data/lib/meshchat/ui/command/chat.rb +31 -0
  51. data/lib/meshchat/ui/command/config.rb +37 -0
  52. data/lib/meshchat/ui/command/emote.rb +23 -0
  53. data/lib/meshchat/ui/command/exit.rb +16 -0
  54. data/lib/meshchat/ui/command/help.rb +20 -0
  55. data/lib/meshchat/ui/command/identity.rb +16 -0
  56. data/lib/meshchat/ui/command/import.rb +42 -0
  57. data/lib/meshchat/ui/command/irb.rb +22 -0
  58. data/lib/meshchat/ui/command/offline.rb +23 -0
  59. data/lib/meshchat/ui/command/online.rb +18 -0
  60. data/lib/meshchat/ui/command/ping.rb +65 -0
  61. data/lib/meshchat/ui/command/ping_all.rb +19 -0
  62. data/lib/meshchat/ui/command/send_disconnect.rb +20 -0
  63. data/lib/meshchat/ui/command/server.rb +22 -0
  64. data/lib/meshchat/ui/command/share.rb +16 -0
  65. data/lib/meshchat/ui/command/whisper.rb +40 -0
  66. data/lib/meshchat/ui/display.rb +78 -0
  67. data/lib/meshchat/ui/display/base.rb +58 -0
  68. data/lib/meshchat/ui/display/manager.rb +59 -0
  69. data/lib/meshchat/ui/notifier.rb +9 -0
  70. data/lib/meshchat/ui/notifier/base.rb +33 -0
  71. data/lib/meshchat/version.rb +3 -2
  72. metadata +150 -80
  73. data/lib/meshchat/cli.rb +0 -188
  74. data/lib/meshchat/cli/base.rb +0 -13
  75. data/lib/meshchat/cli/input.rb +0 -37
  76. data/lib/meshchat/command/base.rb +0 -80
  77. data/lib/meshchat/command/bind.rb +0 -44
  78. data/lib/meshchat/command/chat.rb +0 -30
  79. data/lib/meshchat/command/config.rb +0 -34
  80. data/lib/meshchat/command/emote.rb +0 -20
  81. data/lib/meshchat/command/exit.rb +0 -13
  82. data/lib/meshchat/command/help.rb +0 -17
  83. data/lib/meshchat/command/identity.rb +0 -13
  84. data/lib/meshchat/command/import.rb +0 -41
  85. data/lib/meshchat/command/init.rb +0 -34
  86. data/lib/meshchat/command/irb.rb +0 -23
  87. data/lib/meshchat/command/listen.rb +0 -13
  88. data/lib/meshchat/command/offline.rb +0 -20
  89. data/lib/meshchat/command/online.rb +0 -15
  90. data/lib/meshchat/command/ping.rb +0 -65
  91. data/lib/meshchat/command/ping_all.rb +0 -15
  92. data/lib/meshchat/command/send_disconnect.rb +0 -15
  93. data/lib/meshchat/command/server.rb +0 -20
  94. data/lib/meshchat/command/share.rb +0 -13
  95. data/lib/meshchat/command/stop_listening.rb +0 -13
  96. data/lib/meshchat/command/whisper.rb +0 -38
  97. data/lib/meshchat/database.rb +0 -30
  98. data/lib/meshchat/display.rb +0 -33
  99. data/lib/meshchat/display/base.rb +0 -60
  100. data/lib/meshchat/display/manager.rb +0 -55
  101. data/lib/meshchat/instance.rb +0 -40
  102. data/lib/meshchat/message.rb +0 -41
  103. data/lib/meshchat/message/base.rb +0 -97
  104. data/lib/meshchat/message/chat.rb +0 -19
  105. data/lib/meshchat/message/disconnect.rb +0 -13
  106. data/lib/meshchat/message/emote.rb +0 -9
  107. data/lib/meshchat/message/node_list.rb +0 -63
  108. data/lib/meshchat/message/node_list_diff.rb +0 -15
  109. data/lib/meshchat/message/node_list_hash.rb +0 -33
  110. data/lib/meshchat/message/ping.rb +0 -32
  111. data/lib/meshchat/message/ping_reply.rb +0 -9
  112. data/lib/meshchat/message/relay.rb +0 -43
  113. data/lib/meshchat/message/whisper.rb +0 -36
  114. data/lib/meshchat/models/entry.rb +0 -104
  115. data/lib/meshchat/net/client.rb +0 -83
  116. data/lib/meshchat/net/listener/errors.rb +0 -11
  117. data/lib/meshchat/net/listener/request.rb +0 -48
  118. data/lib/meshchat/net/listener/request_processor.rb +0 -50
  119. data/lib/meshchat/net/listener/server.rb +0 -114
  120. data/lib/meshchat/net/request.rb +0 -29
  121. data/lib/meshchat/notifier/base.rb +0 -31
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ module Meshchat
3
+ # This file is stupid.
4
+ # But very helpful when debugging problems...
5
+ module Debug
6
+ module_function
7
+
8
+ # TODO: extract this idea to a gem
9
+ # - automatic logging of method calls
10
+ def log(method_list)
11
+ method_list = Array[method_list]
12
+ method_list.each do |method|
13
+ backup_name = "#{method}_bak".to_sym
14
+ alias_method :backup_name, :method
15
+ define_method(method) do |*args|
16
+ Display.debug("##{method}: ")
17
+ Display.debug(args.inspect)
18
+ end
19
+ end
20
+ end
21
+
22
+ def message_type_not_found(type)
23
+ Display.debug('Type not found: ' + type.to_s)
24
+ end
25
+
26
+ def not_on_local_network(node)
27
+ Display.debug('SENDING: ' + node.alias_name + ' is not on the local network')
28
+ end
29
+
30
+ def received_message_from_relay(message, relay_url)
31
+ Display.debug('RECEIVING on RELAY: ' + relay_url)
32
+ Display.debug('RECEIVING on RELAY: ')
33
+ Display.debug(message)
34
+ end
35
+
36
+ def sending_message_over_relay(node, message, relay_url)
37
+ Display.debug('SENDING on RELAY: ' + relay_url)
38
+ Display.debug('SENDING on RELAY: ' + node.as_json.to_json)
39
+ Display.debug('SENDING on RELAY: ' + message.class.name)
40
+ Display.debug('SENDING on RELAY: ' + message.inspect)
41
+ end
42
+
43
+ def receiving_message(message)
44
+ Display.debug('RECEIVING: ' + message.type)
45
+ Display.debug('RECEIVING: ' + message.sender.to_s)
46
+ Display.debug('RECEIVING: ' + message.message.to_s)
47
+ end
48
+
49
+ def sending_message(message)
50
+ Display.debug('SENDING: ' + message.type)
51
+ Display.debug('SENDING: ' + message.sender.to_s)
52
+ Display.debug('SENDING: ' + message.message.to_s)
53
+ end
54
+
55
+ def person_not_online(node, message, e)
56
+ Display.debug("#{message.class.name}: Issue connectiong to #{node.alias_name}@#{node.location}")
57
+ Display.debug(e.message)
58
+ end
59
+
60
+ def encryption_failed(node)
61
+ Display.info "Public key encryption for #{node.try(:alias_name) || 'unknown'} failed"
62
+ end
63
+
64
+ def creating_input_failed(e)
65
+ Display.error e.message
66
+ Display.error e.backtrace.join("\n").colorize(:red)
67
+ end
68
+ end
69
+ end
@@ -1,8 +1,14 @@
1
+ # frozen_string_literal: true
1
2
  require 'meshchat/encryption/aes_rsa'
2
3
  require 'meshchat/encryption/passthrough'
3
4
 
4
- module MeshChat
5
+ module Meshchat
5
6
  module Encryption
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :AES_RSA
10
+ autoload :Passthrough
11
+
6
12
  module_function
7
13
 
8
14
  DEFAULT_ENCRYPTOR = AES_RSA
@@ -22,6 +28,5 @@ module MeshChat
22
28
  def encrypt(*args)
23
29
  current_encryptor.encrypt(*args)
24
30
  end
25
-
26
31
  end
27
32
  end
@@ -1,4 +1,5 @@
1
- module MeshChat
1
+ # frozen_string_literal: true
2
+ module Meshchat
2
3
  module Encryption
3
4
  # Using AES266-CBC with RSA
4
5
  #
@@ -1,15 +1,17 @@
1
- module MeshChat
1
+ # frozen_string_literal: true
2
+ module Meshchat
2
3
  module Encryption
3
4
  # This should normally be for testing
4
5
  #
5
6
  # It just retuns the message that is asked to be encrypted
6
7
  module Passthrough
7
8
  module_function
8
- def encrypt(msg, *args)
9
+
10
+ def encrypt(msg, *_args)
9
11
  msg
10
12
  end
11
13
 
12
- def decrypt(msg, *args)
14
+ def decrypt(msg, *_args)
13
15
  msg
14
16
  end
15
17
  end
@@ -0,0 +1,14 @@
1
+ confirm_options: ' (Y/N)'
2
+ confirm_yes: yes
3
+ confirm_y: y
4
+ identity:
5
+ settings_not_detected: a settings file was not detected or incomplete, would you like to generate one?
6
+ unknown_error_try_again: something went wrong with identity creation. try again?
7
+ settings_are_invalid: settings are invalid
8
+ confirm_uid_replace: uid exists, are you sure you want a new identity?
9
+ confirm_alias_replace: alias exists, would you like to overwrite it?
10
+ confirm_key_replace: keys exist, overwrite?
11
+ ask_for_alias: 'Type the name/alias you would like to go by in meshchat:'
12
+ current_alias: 'Your current name/alias is: %{name}'
13
+ node:
14
+ not_found: "Node not found, or does not have a location. Have you imported %{name}?"
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ module Meshchat
3
+ class Node < 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_on_network, with: lambda { |e|
16
+ location_on_network = e.location_on_network || ''
17
+ is_domain = location_on_network.include?('//') ||
18
+ location_on_network.include?('localhost')
19
+
20
+ is_domain ? DOMAIN_WITH_PORT : IPV4_WITH_PORT
21
+ }
22
+
23
+ scope :on_local_network, -> { where(on_local_network: true) }
24
+ scope :on_relay, -> { where(on_relay: true) }
25
+ scope :online, -> { on_local_network.or(on_relay) }
26
+
27
+ class << self
28
+ def sha_preimage
29
+ all.map(&:public_key).sort.join(',')
30
+ end
31
+
32
+ def as_sha512
33
+ digest = Digest::SHA512.new
34
+ digest.hexdigest sha_preimage
35
+ end
36
+
37
+ def as_json
38
+ # must also include ourselves
39
+ # so that we can pass our own public key
40
+ # to those who don't have it
41
+ others = all.map(&:as_json)
42
+ me = APP_CONFIG.user.identity_as_json
43
+ others << me
44
+ end
45
+
46
+ def from_json(json)
47
+ new(
48
+ alias_name: json['alias'],
49
+ location_on_network: json['location'],
50
+ uid: json['uid'],
51
+ public_key: json['publickey']
52
+ )
53
+ end
54
+
55
+ def public_key_from_uid(uid)
56
+ find_by_uid(uid).try(:public_key)
57
+ end
58
+
59
+ # @param [Array] theirs array of hashes representing node entries
60
+ # @return [Array<-,+>] nodes only we have, and nodes only they have
61
+ def diff(theirs)
62
+ ours = as_json
63
+ we_only_have = ours - theirs
64
+ they_only_have = theirs - ours
65
+
66
+ [we_only_have, they_only_have]
67
+ end
68
+
69
+ def import_from_file(filename)
70
+ f = File.read(filename)
71
+ hash = JSON.parse(f)
72
+ n = from_json(hash)
73
+ n.save
74
+ n
75
+ rescue => e
76
+ Display.alert e.message
77
+ end
78
+
79
+ # Try to find the node, given a location, or uid
80
+ #
81
+ # TODO: do we want to also be able to find by relay address?
82
+ # - this would be non-unique
83
+ # - maybe finding should only happen via UID
84
+ # @param [String] location - the local network address
85
+ # @param [String] uid - the node's UID
86
+ # @param [Node] node - the node
87
+ # @return [Node]
88
+ def for(location: nil, uid: nil, node: nil)
89
+ unless node
90
+ node = Node.find_by_location_on_network(location) if location
91
+ node = Node.find_by_uid(uid) if uid && !node
92
+ end
93
+
94
+ unless node && node.valid?
95
+ msg = I18n.t('node.not_found', name: location || uid || '')
96
+ return Display.alert msg
97
+ end
98
+
99
+ node
100
+ end
101
+ end
102
+
103
+ def ==(other)
104
+ result = false
105
+
106
+ if other.is_a?(Hash)
107
+ result = as_json.values_at(*other.keys) == other.values
108
+ end
109
+
110
+ result || super
111
+ end
112
+
113
+ def online
114
+ on_relay? || on_local_network?
115
+ end
116
+ alias_method :online?, :online
117
+
118
+ def location
119
+ return location_of_relay if on_relay?
120
+ location_on_network
121
+ end
122
+
123
+ def location_is_web_socket?
124
+ location.match(/wss?/).present?
125
+ end
126
+
127
+ def as_json
128
+ {
129
+ 'alias' => alias_name,
130
+ 'location' => location_on_network,
131
+ 'uid' => uid,
132
+ 'publickey' => public_key
133
+ }
134
+ end
135
+
136
+ def as_info
137
+ "#{alias_name}@#{location}"
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ module Meshchat
3
+ module Network
4
+ extend ActiveSupport::Autoload
5
+ NETWORK_LOCAL = :local
6
+ NETWORK_RELAY = :relay
7
+
8
+ eager_autoload do
9
+ autoload :Errors
10
+ autoload :Message
11
+ autoload :Dispatcher
12
+ autoload :Incoming
13
+ autoload :Local
14
+ autoload :Remote
15
+ autoload :MessageProcessor
16
+ autoload :RequestProcessor
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+ module Meshchat
3
+ module Network
4
+ class Dispatcher
5
+ # creates messages
6
+ attr_reader :_message_factory
7
+
8
+ # standard peer-to-peer message sending
9
+ attr_reader :_local_client
10
+
11
+ # the action cable client ( web socket / connection beyond the firewall)
12
+ # - responsible for the relay server if the http client can't find the recipient
13
+ attr_reader :_relay_client
14
+
15
+ def initialize
16
+ @_message_factory = Message::Factory.new(self)
17
+ @_local_client = Local::Connection.new(self, @_message_factory)
18
+ @_relay_client = Remote::Connection.new(self, @_message_factory)
19
+ end
20
+
21
+ # @note Either the location, node, or uid should be present
22
+ #
23
+ # @param [String] location (Optional) location of target
24
+ # @param [String] uid (Optional) uid of target
25
+ # @param [Node] node (Optional) target
26
+ # @param [Message] message (Required) what to send to the target
27
+ def send_message(location: nil, uid: nil, node: nil, message: nil)
28
+ # verify node is valid
29
+ node = Node.for(location: location, uid: uid, node: node)
30
+ # don't proceed if we don't have a node
31
+ return unless node
32
+ # don't send to ourselves
33
+ return if APP_CONFIG.user['uid'] == node.uid
34
+
35
+ # everything is valid so far... DISPATCH!
36
+ dispatch!(node, message)
37
+ end
38
+
39
+ private
40
+
41
+ def dispatch!(node, message)
42
+ Debug.sending_message(message)
43
+
44
+ message = encrypted_message(node, message)
45
+
46
+ # determine last known sending method
47
+ if node.on_local_network?
48
+ try_dispatching_over_local_network_first(node, message)
49
+ else
50
+ try_dispatching_over_the_relay_first(node, message)
51
+ end
52
+ end
53
+
54
+ # this attempts to send over http to the local network,
55
+ # if that fails, the passed block will be invoked
56
+ def try_dispatching_over_local_network_first(node, message)
57
+ _local_client.send_message(node, message) do
58
+ Debug.not_on_local_network(node)
59
+ node.update(on_local_network: false)
60
+ _relay_client.send_message(node, message)
61
+ end
62
+ end
63
+
64
+ # this attempts to send over the relay first
65
+ # if that fails, the passed block will be invked
66
+ def try_dispatching_over_the_relay_first(node, message)
67
+ # Due to the constant-connection nature of web-sockets,
68
+ # The sending via http client will happen if the node's-
69
+ # on_local_network property is true.
70
+ # node.update(on_local_network: true)
71
+ _relay_client.send_message(node, message)
72
+ end
73
+
74
+ def encrypted_message(node, message)
75
+ message.encrypt_for(node)
76
+ rescue => e
77
+ Display.debug e.message
78
+ Display.debug e.backtrace
79
+ Debug.encryption_failed(node)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ module Meshchat
3
+ module Network
4
+ module Errors
5
+ class NotAuthorized < StandardError; end
6
+ class Forbidden < StandardError; end
7
+ class BadRequest < StandardError; end
8
+ class MessageTypeNotRecognized < StandardError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module Meshchat
3
+ module Network
4
+ module Incoming
5
+ extend ActiveSupport::Autoload
6
+ eager_autoload do
7
+ autoload :MessageProcessor
8
+ autoload :RequestProcessor
9
+ autoload :MessageDecryptor
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ module Meshchat
3
+ module Network
4
+ module Incoming
5
+ class MessageDecryptor
6
+ attr_reader :_json, :_message, :_input
7
+ attr_reader :_message_factory
8
+
9
+ def initialize(encoded_message, message_factory)
10
+ @_message_factory = message_factory
11
+ @_input = try_decrypt(encoded_message)
12
+ @_json = parse_json(@_input)
13
+ @_message = process_json
14
+ end
15
+
16
+ def message
17
+ _message
18
+ end
19
+
20
+ private
21
+
22
+ def parse_json(input)
23
+ return JSON.parse(input)
24
+ rescue => e
25
+ Display.debug e.message
26
+ Display.debug e.backtrace.join("\n")
27
+ raise Errors::BadRequest, 'could not parse json'
28
+ end
29
+
30
+ def try_decrypt(input)
31
+ begin
32
+ decoded = Base64.decode64(input)
33
+ input = Encryption.decrypt(decoded, APP_CONFIG.user[:privatekey])
34
+ rescue => e
35
+ Display.debug e.message
36
+ Display.debug e.backtrace.join("\n")
37
+ Display.debug input
38
+ raise Errors::NotAuthorized, e.message
39
+ end
40
+
41
+ input
42
+ end
43
+
44
+ def process_json
45
+ type = _json['type']
46
+ _message_factory.create(type, data: { payload: _json })
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end