torchat 0.0.1.rc.1

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