torchat 0.0.1.rc.1

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 (43) hide show
  1. data/README.md +43 -0
  2. data/bin/torchatd +537 -0
  3. data/doc/protocol/broadcast.md +26 -0
  4. data/doc/protocol/groupchat.md +140 -0
  5. data/doc/protocol/latency.md +30 -0
  6. data/doc/protocol/standard.md +279 -0
  7. data/doc/protocol/typing.md +41 -0
  8. data/etc/torchat.yml +12 -0
  9. data/lib/torchat.rb +239 -0
  10. data/lib/torchat/protocol.rb +256 -0
  11. data/lib/torchat/protocol/broadcast.rb +40 -0
  12. data/lib/torchat/protocol/groupchat.rb +197 -0
  13. data/lib/torchat/protocol/latency.rb +44 -0
  14. data/lib/torchat/protocol/standard.rb +367 -0
  15. data/lib/torchat/protocol/typing.rb +36 -0
  16. data/lib/torchat/session.rb +603 -0
  17. data/lib/torchat/session/broadcast/message.rb +50 -0
  18. data/lib/torchat/session/broadcasts.rb +72 -0
  19. data/lib/torchat/session/buddies.rb +152 -0
  20. data/lib/torchat/session/buddy.rb +343 -0
  21. data/lib/torchat/session/buddy/joined_group_chat.rb +42 -0
  22. data/lib/torchat/session/buddy/joined_group_chats.rb +46 -0
  23. data/lib/torchat/session/buddy/latency.rb +54 -0
  24. data/lib/torchat/session/event.rb +79 -0
  25. data/lib/torchat/session/file_transfer.rb +173 -0
  26. data/lib/torchat/session/file_transfer/block.rb +51 -0
  27. data/lib/torchat/session/file_transfers.rb +89 -0
  28. data/lib/torchat/session/group_chat.rb +123 -0
  29. data/lib/torchat/session/group_chat/participant.rb +38 -0
  30. data/lib/torchat/session/group_chat/participants.rb +52 -0
  31. data/lib/torchat/session/group_chats.rb +74 -0
  32. data/lib/torchat/session/incoming.rb +187 -0
  33. data/lib/torchat/session/outgoing.rb +102 -0
  34. data/lib/torchat/tor.rb +107 -0
  35. data/lib/torchat/utils.rb +87 -0
  36. data/lib/torchat/version.rb +24 -0
  37. data/test/file_transfer/receiver.rb +41 -0
  38. data/test/file_transfer/sender.rb +45 -0
  39. data/test/group_chat/a.rb +37 -0
  40. data/test/group_chat/b.rb +37 -0
  41. data/test/group_chat/c.rb +57 -0
  42. data/torchat.gemspec +21 -0
  43. metadata +140 -0
@@ -0,0 +1,43 @@
1
+ torchat - the Ruby implementation
2
+ =================================
3
+ You can find the official implementation [here](https://github.com/prof7bit/TorChat).
4
+
5
+ This aims to be a Ruby implementation and daemon to deal with a TorChat session even from
6
+ outside or with other languages without having to implement the whole thing.
7
+
8
+ The main target is supporting TorChat in bitlbee.
9
+
10
+ Setup the daemon
11
+ ----------------
12
+ The daemon to work needs a tor configured with a hidden service, you can leave it automatic by
13
+ passing `-t` as an option or you can configure it yourself.
14
+
15
+ If you have an already created default TorChat session you just have to run `torchatd -t`, it
16
+ will do all is needed, and you'll be able to connect to it on port `11110`. If you use bitlbee
17
+ you just have to add the account, you can also pass a password with `-P password` which will have
18
+ to be the password you added the account with.
19
+
20
+ The daemon has a helper function to generate the needed tor configuration file based either
21
+ on a YAML file or a TorChat configuration file.
22
+
23
+ Example of tor configuration generation:
24
+
25
+ ```
26
+ $ torchatd -g -c etc/torchat.yml
27
+ SocksPort 11108
28
+
29
+ HiddenServiceDir hidden_service
30
+ HiddenServicePort 11009 127.0.0.1:11008
31
+
32
+ DataDirectory tor_data
33
+
34
+ AvoidDiskWrites 1
35
+ LongLivedPorts 11009
36
+ FetchDirInfoEarly 1
37
+ CircuitBuildTimeout 30
38
+ NumEntryGuards 6
39
+ ```
40
+
41
+ Contacting the author
42
+ ---------------------
43
+ If you want to contact me on torchat my id is `7acbk6jpofsanyfp`.
@@ -0,0 +1,537 @@
1
+ #! /usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'torchat'
4
+
5
+ options = {}
6
+
7
+ OptionParser.new do |o|
8
+ options[:host] = '127.0.0.1'
9
+ options[:port] = 11110
10
+ options[:file_transfer_ports] = []
11
+
12
+ o.on '-p', '--profile NAME', 'the profile name' do |name|
13
+ options[:profile] = name
14
+ end
15
+
16
+ o.on '-c', '--config PATH', 'the path to the config file' do |path|
17
+ options[:config] = path
18
+ end
19
+
20
+ o.on '-g', '--generate [PATH]', 'the path to save the generated torrc' do |path|
21
+ options[:generate] = path || '-'
22
+ end
23
+
24
+ o.on '-t', '--tor', 'enable automatic generation and run of Tor' do
25
+ options[:tor] = true
26
+ end
27
+
28
+ o.on '-l', '--listen HOST:PORT', 'the host and port to listen on' do |value|
29
+ host, port = value.split(':')
30
+
31
+ options[:host] = host unless host.empty?
32
+ options[:port] = port.to_i unless port.empty?
33
+ end
34
+
35
+ o.on '-s', '--ssl KEY:CERT', 'the private key and cert files' do |path|
36
+ options[:ssl] = { key: path.split(':').first, cert: path.split(':').last }
37
+ end
38
+
39
+ o.on '-P', '--password PASSWORD' do |password|
40
+ options[:password] = password
41
+ end
42
+
43
+ o.on '-f', '--file-transfer-ports PORT...', Array, 'the list of ports to listen on for file transfers' do |ports|
44
+ options[:file_transfer_ports] = ports.map {|value|
45
+ value.include?('-') ? Range.new(*value.split('-').map(&:to_i)) : value.to_i
46
+ }.flatten.compact.uniq
47
+ end
48
+
49
+ o.on '-o', '--online' do
50
+ options[:online] = true
51
+ end
52
+
53
+ o.on '-d', '--debug [LEVEL=1]', 'enable debug mode' do |value|
54
+ ENV['DEBUG'] = value || ?1
55
+ end
56
+ end.parse!
57
+
58
+ class Torchatd
59
+ class Connection < EventMachine::Protocols::LineAndTextProtocol
60
+ attr_accessor :daemon, :host, :port, :ssl
61
+
62
+ def authorized?; @authorized; end
63
+ def authorize!; @authorized = true; end
64
+
65
+ def receive_line (line)
66
+ return if line.lstrip.empty?
67
+
68
+ @daemon.process self, line.lstrip
69
+ end
70
+
71
+ def send_response (text)
72
+ send_data "#{text.to_s.lstrip}\n"
73
+ end
74
+
75
+ def unbind
76
+ @daemon.connections.delete self
77
+ end
78
+ end
79
+
80
+ attr_reader :password, :file_transfer_ports, :connections
81
+ attr_accessor :profile, :tor
82
+
83
+ def initialize (password = nil, file_transfer_ports = [])
84
+ @password = password
85
+ @file_transfer_ports = file_transfer_ports.flatten.compact.uniq
86
+
87
+ @buddies = []
88
+ @connections = []
89
+ @pings = Hash.new { |h, k| h[k] = {} }
90
+
91
+ yield self if block_given?
92
+ end
93
+
94
+ def start (host, port, ssl = nil)
95
+ return if @started
96
+
97
+ @started = true
98
+
99
+ @signature = EM.start_server host, port, Connection do |conn|
100
+ @connections << conn
101
+
102
+ conn.daemon = self
103
+ conn.host = host
104
+ conn.port = port
105
+ conn.ssl = ssl
106
+
107
+ unless @password
108
+ conn.authorize!
109
+ end
110
+ end
111
+ end
112
+
113
+ def stop
114
+ EM.stop_server @signature
115
+
116
+ profile.stop
117
+ tor.stop if tor
118
+ end
119
+
120
+ def process (connection, line)
121
+ command, rest = line.force_encoding('UTF-8').split(' ', 2)
122
+
123
+ case command.downcase.to_sym
124
+ when :starttls
125
+ if connection.ssl
126
+ connection.start_tls(private_key_file: connection.ssl[:key], cert_chain_file: connection.ssl[:cert])
127
+ else
128
+ connection.start_tls
129
+ end
130
+
131
+ return
132
+
133
+ when :pass
134
+ if !@password || @password == rest
135
+ connection.authorize!
136
+ connection.send_response "AUTHORIZED #{profile.session.id}"
137
+ end
138
+
139
+ return
140
+ end
141
+
142
+ unless connection.authorized?
143
+ connection.send_response "UNAUTHORIZED #{command}"
144
+ return
145
+ end
146
+
147
+ case command.downcase.to_sym
148
+ when :whoami
149
+ connection.send_response "WHOAMI #{profile.id}"
150
+
151
+ when :list
152
+ connection.send_response "LIST #{profile.buddies.keys.join(' ')}"
153
+
154
+ when :remove
155
+ profile.buddies.remove rest
156
+
157
+ when :add
158
+ attribute, rest = rest.split(' ')
159
+
160
+ if rest && attribute == 'tmp'
161
+ profile.buddies.add_temporary rest
162
+ else
163
+ profile.buddies.add attribute
164
+ end
165
+
166
+ when :typing
167
+ id, mode = rest.split(' ')
168
+
169
+ if buddy = profile.buddies[id]
170
+ buddy.send_typing(mode)
171
+ end
172
+
173
+ when :status
174
+ if rest && Torchat.normalize_id(rest, true)
175
+ if buddy = profile.buddies[rest]
176
+ connection.send_response "#{rest} STATUS #{buddy.status}"
177
+ end
178
+ else
179
+ profile.status = rest
180
+ end
181
+
182
+ when :client
183
+ if buddy = profile.buddies[rest]
184
+ if buddy.client.name
185
+ connection.send_response "#{rest} CLIENT_NAME #{buddy.client.name}"
186
+ end
187
+
188
+ if buddy.client.version
189
+ connection.send_response "#{rest} CLIENT_VERSION #{buddy.client.version}"
190
+ end
191
+ end
192
+
193
+ when :name
194
+ if rest && Torchat.normalize_id(rest, true)
195
+ if buddy = profile.buddies[rest]
196
+ connection.send_response "#{rest} NAME #{buddy.name}"
197
+ end
198
+ else
199
+ profile.name = rest
200
+ end
201
+
202
+ when :description
203
+ if rest && Torchat.normalize_id(rest, true)
204
+ if buddy = profile.buddies[rest]
205
+ connection.send_response "#{rest} DESCRIPTION #{buddy.description}"
206
+ end
207
+ else
208
+ profile.description = rest
209
+ end
210
+
211
+ when :message
212
+ profile.send_message_to *rest.split(' ', 2)
213
+
214
+ when :block
215
+ profile.buddies[rest].block!
216
+
217
+ when :allow
218
+ profile.buddies[rest].allow!
219
+
220
+ when :broadcast
221
+ profile.send_broadcast rest
222
+
223
+ when :groupchats
224
+ connection.send_response "GROUPCHATS #{profile.group_chats.keys.join(' ')}"
225
+
226
+ when :groupchat_participants
227
+ if group_chat = profile.group_chats[rest]
228
+ connection.send_response "GROUPCHAT_PARTICIPANTS #{group_chat.id} #{group_chat.participants.keys.join ' '}"
229
+ end
230
+
231
+ when :groupchat_invite
232
+ group_chat, buddy = rest.split ' '
233
+
234
+ if buddy
235
+ if (buddy = profile.buddies[buddy]) && (group_chat = profile.group_chats[group_chat])
236
+ group_chat.invite(buddy)
237
+ end
238
+ else
239
+ if buddy = profile.buddies[group_chat]
240
+ profile.group_chats.create.invite(buddy)
241
+ end
242
+ end
243
+
244
+ when :groupchat_leave
245
+ group_chat, reason = rest.split ' ', 2
246
+
247
+ if group_chat = profile.group_chats[group_chat]
248
+ group_chat.leave reason
249
+ end
250
+
251
+ when :groupchat_message
252
+ group_chat, message = rest.split ' ', 2
253
+
254
+ if group_chat = profile.group_chats[group_chat]
255
+ group_chat.send_message message
256
+ end
257
+
258
+ when :latency
259
+ id, rest = rest.split ' ', 2
260
+
261
+ if (buddy = profile.buddies[id]) && buddy.supports?(:latency)
262
+ @pings[id][buddy.latency.ping!.id] = [Time.now, rest]
263
+ end
264
+
265
+ else
266
+ connection.send_response "UNIMPLEMENTED #{command}"
267
+ end
268
+ rescue => e
269
+ Torchat.debug e
270
+ end
271
+
272
+ def received_packet (packet)
273
+ return unless @buddies.include? packet.from
274
+
275
+ if packet.type == :message
276
+ packet.to_s.lines.each {|line|
277
+ send_everyone "#{packet.from.id} MESSAGE #{line}"
278
+ }
279
+ elsif packet.type == :status
280
+ send_everyone "#{packet.from.id} STATUS #{packet}"
281
+ elsif packet.type == :client
282
+ send_everyone "#{packet.from.id} CLIENT_NAME #{packet}"
283
+ elsif packet.type == :version
284
+ send_everyone "#{packet.from.id} CLIENT_VERSION #{packet}"
285
+ elsif packet.type == :profile_name && !packet.nil?
286
+ send_everyone "#{packet.from.id} NAME #{packet}"
287
+ elsif packet.type == :profile_text && !packet.nil?
288
+ send_everyone "#{packet.from.id} DESCRIPTION #{packet}"
289
+ elsif packet.type == :remove_me
290
+ send_everyone "#{packet.from.id} REMOVE"
291
+ end
292
+ end
293
+
294
+ def file_transfer (what, file_transfer, *args)
295
+ if what == :start
296
+
297
+ elsif what == :stop
298
+
299
+ elsif what == :complete
300
+
301
+ end
302
+ end
303
+
304
+ def typing (buddy, mode)
305
+ send_everyone "#{buddy.id} TYPING #{mode}"
306
+ end
307
+
308
+ def broadcast (message)
309
+ send_everyone "BROADCAST #{message}"
310
+ end
311
+
312
+ def group_chat (what, group_chat, buddy = nil, *args)
313
+ if what == :create
314
+ send_everyone "GROUPCHAT_CREATE #{group_chat.id}"
315
+ elsif what == :invite
316
+ send_everyone "#{buddy.id} GROUPCHAT_INVITE #{group_chat.id}"
317
+ elsif what == :join
318
+ send_everyone "#{buddy.id} GROUPCHAT_JOIN #{group_chat.id}#{" #{args.first.id}" if args.first}"
319
+ elsif what == :joined
320
+ send_everyone "GROUPCHAT_JOINED #{group_chat.id}"
321
+ send_everyone "GROUPCHAT_PARTICIPANTS #{group_chat.id} #{group_chat.participants.keys.join ' '}"
322
+ elsif what == :leave
323
+ send_everyone "#{buddy.id} GROUPCHAT_LEAVE #{group_chat.id}#{" #{args.first}" if args.first}"
324
+ elsif what == :left
325
+ send_everyone "GROUPCHAT_LEFT #{group_chat.id} #{args.first}"
326
+ elsif what == :message
327
+ send_everyone "#{buddy.id} GROUPCHAT_MESSAGE #{group_chat.id} #{args.first}"
328
+ elsif what == :destroy
329
+ send_everyone "GROUPCHAT_DESTROY #{group_chat.id}"
330
+ end
331
+ end
332
+
333
+ def latency (buddy, amount, id)
334
+ send_everyone "#{buddy.id} LATENCY #{@pings[buddy.id].delete(id).last}"
335
+ end
336
+
337
+ def cleanup!
338
+ @pings.each {|id, pings|
339
+ pings.reject! {|time, payload|
340
+ (Time.now - time).to_i >= 80
341
+ }
342
+ }
343
+
344
+ @pings.reject!(&:empty?)
345
+ end
346
+
347
+ def connected?; @connected; end
348
+
349
+ def connected (buddy)
350
+ @buddies << buddy
351
+
352
+ send_everyone "#{buddy.id} CONNECTED"
353
+
354
+ send_everyone "#{buddy.id} NAME #{buddy.name}" if buddy.name
355
+ send_everyone "#{buddy.id} DESCRIPTION #{buddy.description}" if buddy.description
356
+
357
+ if buddy.client.name
358
+ send_everyone "#{buddy.id} CLIENT_NAME #{buddy.client.name}"
359
+ end
360
+
361
+ if buddy.client.version
362
+ send_everyone "#{buddy.id} CLIENT_VERSION #{buddy.client.version}"
363
+ end
364
+ end
365
+
366
+ def disconnected (buddy)
367
+ return unless @buddies.include? buddy
368
+
369
+ send_everyone "#{buddy.id} DISCONNECTED"
370
+
371
+ @buddies.delete buddy
372
+ end
373
+
374
+ def removed (buddy)
375
+ return unless @buddies.include? buddy
376
+
377
+ send_everyone "#{buddy.id} REMOVE"
378
+ end
379
+
380
+ def send_everyone (text, even_unauthorized = false)
381
+ @connections.each {|connection|
382
+ next unless connection.authorized? || even_unauthorized
383
+
384
+ connection.send_response text
385
+ }
386
+ end
387
+ end
388
+
389
+ EM.run {
390
+ Torchatd.new(options[:password], options[:file_transfer_ports]) {|d|
391
+ d.profile = options[:config] ? Torchat.new(options[:config]) : Torchat.profile(options[:profile])
392
+
393
+ if options[:generate]
394
+ if options[:generate] == '-'
395
+ print d.profile.tor.rc
396
+ else
397
+ File.open(options[:generate], 'w') { |f| f.print d.profile.tor.rc }
398
+ end
399
+
400
+ exit
401
+ end
402
+
403
+ puts 'torchatd starting...'
404
+
405
+ if options[:tor]
406
+ d.profile.tor.file = 'torrc.txt'
407
+
408
+ d.profile.tor.start "#{d.profile.path || '~/.torchat'}/Tor", -> {
409
+ abort 'could not load the onion id' if 20.times {
410
+ break if File.exists? 'hidden_service/hostname'
411
+
412
+ sleep 1
413
+ }
414
+ }, -> {
415
+ abort 'tor exited with errors'
416
+ }
417
+ end
418
+
419
+ unless d.profile.config['id']
420
+ if d.profile.path
421
+ if File.readable?("#{d.profile.path}/Tor/hidden_service/hostname")
422
+ d.profile.config['id'] = File.read("#{d.profile.path}/Tor/hidden_service/hostname")[/^(.*?)\.onion/, 1]
423
+ end
424
+ end or abort 'could not deduce the onion id'
425
+ end
426
+
427
+ puts "torchatd started for #{d.profile.config['id']} on #{options[:host]}:#{options[:port]}"
428
+
429
+ %w[INT KILL].each {|sig|
430
+ trap sig do
431
+ puts 'torchatd stopping...'
432
+
433
+ d.stop
434
+
435
+ EM.stop_event_loop
436
+ end
437
+ }
438
+
439
+ d.profile.start {|s|
440
+ s.on :connect_to do |e|
441
+ Torchat.debug "connecting to #{e.address}:#{e.port}"
442
+ end
443
+
444
+ s.on :connect_failure do |e|
445
+ Torchat.debug "#{e.buddy.id} failed to connect"
446
+ end
447
+
448
+ s.on :connect do |e|
449
+ Torchat.debug "#{e.buddy.id} connected"
450
+ end
451
+
452
+ s.on :verify do |e|
453
+ Torchat.debug "#{e.buddy.id} has been verified"
454
+ end
455
+
456
+ s.on :ready do |e|
457
+ d.connected e.buddy
458
+ end
459
+
460
+ s.on :remove_buddy do |e|
461
+ d.removed e.buddy
462
+ end
463
+
464
+ s.on :disconnect do |e|
465
+ Torchat.debug "#{e.buddy.id} disconnected"
466
+
467
+ d.disconnected e.buddy
468
+ end
469
+
470
+ s.on_packet do |e|
471
+ d.received_packet e.packet unless e.packet.extension
472
+ end
473
+
474
+ s.on :file_transfer_start do |e|
475
+ d.file_transfer :start, e.file_transfer
476
+ end
477
+
478
+ s.on :file_transfer_stop do |e|
479
+ d.file_transfer :stop, e.file_transfer
480
+ end
481
+
482
+ s.on :file_transfer_complete do |e|
483
+ d.file_transfer :complete, e.file_transfer
484
+ end
485
+
486
+ s.on :typing do |e|
487
+ d.typing e.buddy, e.mode
488
+ end
489
+
490
+ s.on :broadcast do |e|
491
+ d.broadcast e.message
492
+ end
493
+
494
+ s.on :group_chat_create do |e|
495
+ d.group_chat :create, e.group_chat
496
+ end
497
+
498
+ s.on :group_chat_invite do |e|
499
+ d.group_chat :invite, e.group_chat, e.buddy
500
+ end
501
+
502
+ s.on :group_chat_join do |e|
503
+ if e.buddy
504
+ d.group_chat :join, e.group_chat, e.buddy, e.invited_by
505
+ else
506
+ d.group_chat :joined, e.group_chat, nil, e.invited_by
507
+ end
508
+ end
509
+
510
+ s.on :group_chat_message do |e|
511
+ d.group_chat :message, e.group_chat, e.buddy, e.message
512
+ end
513
+
514
+ s.on :group_chat_leave do |e|
515
+ if e.buddy
516
+ d.group_chat :leave, e.group_chat, e.buddy, e.reason
517
+ else
518
+ d.group_chat :left, e.group_chat, nil, e.reason
519
+ end
520
+ end
521
+
522
+ s.on :group_chat_destroy do |e|
523
+ d.group_chat :destroy, e.group_chat
524
+ end
525
+
526
+ s.on :latency do |e|
527
+ d.latency e.buddy, e.amount, e.id
528
+ end
529
+
530
+ s.online! if options[:online]
531
+ }
532
+
533
+ EM.add_periodic_timer 60 do
534
+ d.cleanup!
535
+ end
536
+ }.start(options[:host], options[:port], options[:ssl])
537
+ }