vines 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/LICENSE +1 -1
  2. data/Rakefile +33 -53
  3. data/conf/certs/README +10 -3
  4. data/conf/certs/ca-bundle.crt +55 -410
  5. data/lib/vines/cluster/connection.rb +1 -1
  6. data/lib/vines/command/ldap.rb +1 -1
  7. data/lib/vines/command/schema.rb +1 -1
  8. data/lib/vines/config.rb +11 -5
  9. data/lib/vines/jid.rb +3 -3
  10. data/lib/vines/stanza/iq/disco_items.rb +1 -1
  11. data/lib/vines/storage/local.rb +1 -0
  12. data/lib/vines/store.rb +50 -9
  13. data/lib/vines/stream.rb +5 -9
  14. data/lib/vines/stream/client/session.rb +1 -1
  15. data/lib/vines/stream/http.rb +3 -1
  16. data/lib/vines/stream/http/ready.rb +5 -1
  17. data/lib/vines/stream/http/request.rb +22 -0
  18. data/lib/vines/version.rb +1 -1
  19. data/test/config/host_test.rb +9 -9
  20. data/test/config/pubsub_test.rb +2 -2
  21. data/test/config_test.rb +5 -3
  22. data/test/jid_test.rb +9 -0
  23. data/test/rake_test_loader.rb +1 -1
  24. data/test/router_test.rb +7 -7
  25. data/test/stanza/iq/disco_info_test.rb +2 -2
  26. data/test/stanza/iq/private_storage_test.rb +1 -1
  27. data/test/stanza/iq_test.rb +1 -1
  28. data/test/stanza/presence/subscribe_test.rb +1 -1
  29. data/test/stanza/pubsub/subscribe_test.rb +3 -3
  30. data/test/stanza/pubsub/unsubscribe_test.rb +3 -3
  31. data/test/storage_test.rb +15 -8
  32. data/test/store_test.rb +131 -0
  33. data/test/stream/client/ready_test.rb +6 -6
  34. data/test/stream/component/ready_test.rb +1 -1
  35. data/test/stream/http/ready_test.rb +46 -11
  36. data/test/stream/http/request_test.rb +83 -11
  37. data/test/stream/http/sessions_test.rb +2 -2
  38. data/web/chat/coffeescripts/chat.coffee +1 -1
  39. data/web/chat/index.html +9 -10
  40. data/web/chat/javascripts/app.js +1 -1
  41. data/web/lib/coffeescripts/button.coffee +1 -1
  42. data/web/lib/coffeescripts/contact.coffee +2 -2
  43. data/web/lib/coffeescripts/filter.coffee +1 -1
  44. data/web/lib/coffeescripts/layout.coffee +2 -2
  45. data/web/lib/coffeescripts/login.coffee +1 -1
  46. data/web/lib/coffeescripts/logout.coffee +2 -2
  47. data/web/lib/coffeescripts/navbar.coffee +1 -1
  48. data/web/lib/coffeescripts/notification.coffee +1 -1
  49. data/web/lib/coffeescripts/router.coffee +1 -1
  50. data/web/lib/coffeescripts/session.coffee +1 -1
  51. data/web/lib/coffeescripts/transfer.coffee +1 -1
  52. data/web/lib/javascripts/base.js +10 -9
  53. metadata +65 -43
  54. data/web/chat/javascripts/chat.js +0 -390
  55. data/web/chat/javascripts/init.js +0 -21
  56. data/web/lib/javascripts/button.js +0 -39
  57. data/web/lib/javascripts/contact.js +0 -94
  58. data/web/lib/javascripts/filter.js +0 -88
  59. data/web/lib/javascripts/layout.js +0 -48
  60. data/web/lib/javascripts/login.js +0 -88
  61. data/web/lib/javascripts/logout.js +0 -11
  62. data/web/lib/javascripts/navbar.js +0 -69
  63. data/web/lib/javascripts/notification.js +0 -26
  64. data/web/lib/javascripts/router.js +0 -105
  65. data/web/lib/javascripts/session.js +0 -291
  66. data/web/lib/javascripts/transfer.js +0 -124
@@ -7,7 +7,7 @@ module Vines
7
7
  attr_accessor :host, :port, :database, :password
8
8
 
9
9
  def initialize
10
- @redis = nil
10
+ @redis, @host, @port, @database, @password = nil, nil, nil, nil, nil
11
11
  end
12
12
 
13
13
  # Return a shared redis connection.
@@ -7,7 +7,7 @@ module Vines
7
7
  raise 'vines ldap <domain>' unless opts[:args].size == 1
8
8
  require opts[:config]
9
9
  domain = opts[:args].first
10
- unless storage = Config.instance.vhosts[domain].storage rescue nil
10
+ unless storage = Config.instance.vhost(domain).storage rescue nil
11
11
  raise "#{domain} virtual host not found in conf/config.rb"
12
12
  end
13
13
  unless storage.ldap?
@@ -7,7 +7,7 @@ module Vines
7
7
  raise 'vines schema <domain>' unless opts[:args].size == 1
8
8
  require opts[:config]
9
9
  domain = opts[:args].first
10
- unless storage = Config.instance.vhosts[domain].storage rescue nil
10
+ unless storage = Config.instance.vhost(domain).storage rescue nil
11
11
  raise "#{domain} virtual host not found in conf/config.rb"
12
12
  end
13
13
  unless storage.respond_to?(:create_schema)
@@ -9,7 +9,7 @@ module Vines
9
9
  class Config
10
10
  LOG_LEVELS = %w[debug info warn error fatal].freeze
11
11
 
12
- attr_reader :router, :vhosts
12
+ attr_reader :router
13
13
 
14
14
  @@instance = nil
15
15
  def self.configure(&block)
@@ -67,13 +67,19 @@ module Vines
67
67
 
68
68
  # Return true if the domain is virtual hosted by this server.
69
69
  def vhost?(domain)
70
- @vhosts.key?(domain.to_s)
70
+ !!vhost(domain)
71
+ end
72
+
73
+ # Return the Host config object for this domain if it's hosted by this
74
+ # server.
75
+ def vhost(domain)
76
+ @vhosts[domain.to_s]
71
77
  end
72
78
 
73
79
  # Returns the storage system for the domain or a Storage::Null instance if
74
80
  # the domain is not hosted at this server.
75
81
  def storage(domain)
76
- host = @vhosts[domain.to_s]
82
+ host = vhost(domain)
77
83
  host ? host.storage : @null
78
84
  end
79
85
 
@@ -112,7 +118,7 @@ module Vines
112
118
 
113
119
  # Return true if private XML fragment storage is enabled for this domain.
114
120
  def private_storage?(domain)
115
- host = @vhosts[domain.to_s]
121
+ host = vhost(domain)
116
122
  host.private_storage? if host
117
123
  end
118
124
 
@@ -191,7 +197,7 @@ module Vines
191
197
  # Return true if all JIDs are allowed to exchange cross domain messages.
192
198
  def cross_domain?(*jids)
193
199
  !jids.flatten.index do |jid|
194
- !@vhosts[jid.domain].cross_domain_messages?
200
+ !vhost(jid.domain).cross_domain_messages?
195
201
  end
196
202
  end
197
203
  end
@@ -4,13 +4,13 @@ module Vines
4
4
  class JID
5
5
  include Comparable
6
6
 
7
- PATTERN = /^(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?$/.freeze
7
+ PATTERN = /\A(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?\Z/.freeze
8
8
 
9
9
  # http://tools.ietf.org/html/rfc6122#appendix-A
10
- NODE_PREP = /[[:space:][:cntrl:]"&'\/:<>@]/.freeze
10
+ NODE_PREP = /[[:cntrl:] "&'\/:<>@]/.freeze
11
11
 
12
12
  # http://tools.ietf.org/html/rfc3454#appendix-C
13
- NAME_PREP = /[[:space:][:cntrl:]]/.freeze
13
+ NAME_PREP = /[[:cntrl:] ]/.freeze
14
14
 
15
15
  attr_reader :node, :domain, :resource
16
16
  attr_writer :resource
@@ -15,7 +15,7 @@ module Vines
15
15
  query.default_namespace = NS
16
16
  unless to_pubsub_domain?
17
17
  to = (validate_to || stream.domain).to_s
18
- stream.config.vhosts[to].disco_items.each do |domain|
18
+ stream.config.vhost(to).disco_items.each do |domain|
19
19
  query << el.document.create_element('item', 'jid' => domain)
20
20
  end
21
21
  end
@@ -9,6 +9,7 @@ module Vines
9
9
  register :fs
10
10
 
11
11
  def initialize(&block)
12
+ @dir = nil
12
13
  instance_eval(&block)
13
14
  unless @dir && File.directory?(@dir) && File.writable?(@dir)
14
15
  raise 'Must provide a writable storage directory'
@@ -6,9 +6,12 @@ module Vines
6
6
  # This uses the conf/certs/*.crt files as the list of trusted root
7
7
  # CA certificates.
8
8
  class Store
9
- @@certs = nil
9
+ @@sources = nil
10
10
 
11
- def initialize
11
+ # Create a certificate store to read certificate files from the given
12
+ # directory.
13
+ def initialize(dir)
14
+ @dir = File.expand_path(dir)
12
15
  @store = OpenSSL::X509::Store.new
13
16
  certs.each {|c| @store.add_cert(c) }
14
17
  end
@@ -37,15 +40,53 @@ module Vines
37
40
  # certificates are used to start the trust chain needed to validate certs
38
41
  # we receive from clients and servers.
39
42
  def certs
40
- unless @@certs
43
+ unless @@sources
41
44
  pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
42
- dir = File.join(VINES_ROOT, 'conf', 'certs')
43
- certs = Dir[File.join(dir, '*.crt')].map {|f| File.read(f) }
44
- certs = certs.map {|c| c.scan(pattern) }.flatten
45
- certs.map! {|c| OpenSSL::X509::Certificate.new(c) }
46
- @@certs = certs.reject {|c| c.not_after < Time.now }
45
+ pairs = Dir[File.join(@dir, '*.crt')].map do |name|
46
+ pems = File.read(name).scan(pattern)
47
+ certs = pems.map {|pem| OpenSSL::X509::Certificate.new(pem) }
48
+ certs.reject! {|cert| cert.not_after < Time.now }
49
+ [name, certs]
50
+ end
51
+ @@sources = Hash[pairs]
52
+ end
53
+ @@sources.values.flatten
54
+ end
55
+
56
+ # Returns a pair of file names containing the public key certificate
57
+ # and matching private key for the given domain. This supports using
58
+ # wildcard certificate files to serve several subdomains.
59
+ #
60
+ # Finding the certificate and private key file for a domain follows these steps:
61
+ # - look for <domain>.crt and <domain>.key files in the conf/certs directory.
62
+ # if found, return those file names, else
63
+ # - inspect all conf/certs/*.crt files for certificates that contain the
64
+ # domain name either as the subject common name (CN) or as a DNS
65
+ # subjectAltName. The corresponding private key must be in a file of the
66
+ # same name as the certificate's, but with a .key extension.
67
+ #
68
+ # So in the simplest configuration, the tea.wonderland.lit encryption files would
69
+ # be named conf/certs/tea.wonderland.lit.crt and conf/certs/tea.wonderland.lit.key.
70
+ #
71
+ # However, in the case of a wildcard certificate for *.wonderland.lit, the
72
+ # files would be conf/certs/wonderland.lit.crt and conf/certs/wonderland.lit.key.
73
+ # These same two files would be returned for the subdomains of tea.wonderland.lit,
74
+ # crumpets.wonderland.lit, etc.
75
+ def files_for_domain(domain)
76
+ crt = File.expand_path("#{domain}.crt", @dir)
77
+ key = File.expand_path("#{domain}.key", @dir)
78
+ return [crt, key] if File.exists?(crt) && File.exists?(key)
79
+
80
+ # might be a wildcard cert file
81
+ @@sources.each do |file, certs|
82
+ certs.each do |cert|
83
+ if OpenSSL::SSL.verify_certificate_identity(cert, domain)
84
+ key = file.chomp(File.extname(file)) + '.key'
85
+ return [file, key] if File.exists?(file) && File.exists?(key)
86
+ end
87
+ end
47
88
  end
48
- @@certs
89
+ nil
49
90
  end
50
91
  end
51
92
  end
@@ -21,7 +21,7 @@ module Vines
21
21
  @remote_addr, @local_addr = addresses
22
22
  @user, @closed, @stanza_size = nil, false, 0
23
23
  @bucket = TokenBucket.new(100, 10)
24
- @store = Store.new
24
+ @store = Store.new(File.join(VINES_ROOT, 'conf', 'certs'))
25
25
  @nodes = EM::Queue.new
26
26
  process_node_queue
27
27
  create_parser
@@ -71,7 +71,7 @@ module Vines
71
71
 
72
72
  # Returns the Vines::Config::Host virtual host for the stream's domain.
73
73
  def vhost
74
- @config.vhosts[domain]
74
+ @config.vhost(domain)
75
75
  end
76
76
 
77
77
  # Reload the user's information into their active connections. Call this
@@ -118,14 +118,14 @@ module Vines
118
118
  end
119
119
 
120
120
  def encrypt
121
- cert, key = tls_files
122
- start_tls(:private_key_file => key, :cert_chain_file => cert, :verify_peer => true)
121
+ cert, key = @store.files_for_domain(domain)
122
+ start_tls(cert_chain_file: cert, private_key_file: key, verify_peer: true)
123
123
  end
124
124
 
125
125
  # Returns true if the TLS certificate and private key files for this domain
126
126
  # exist and can be used to encrypt this stream.
127
127
  def encrypt?
128
- tls_files.all? {|f| File.exists?(f) }
128
+ !@store.files_for_domain(domain).nil?
129
129
  end
130
130
 
131
131
  def unbind
@@ -235,10 +235,6 @@ module Vines
235
235
  @state
236
236
  end
237
237
 
238
- def tls_files
239
- %w[crt key].map {|ext| File.join(VINES_ROOT, 'conf', 'certs', "#{domain}.#{ext}") }
240
- end
241
-
242
238
  # Return true if this is a valid domain-only JID that can be used in
243
239
  # stream initiation stanza headers.
244
240
  def valid_address?(jid)
@@ -196,7 +196,7 @@ module Vines
196
196
 
197
197
  def unsubscribe_pubsub
198
198
  if connected?
199
- @config.vhosts[@user.jid.domain].unsubscribe_pubsub(@user.jid)
199
+ @config.vhost(@user.jid.domain).unsubscribe_pubsub(@user.jid)
200
200
  end
201
201
  end
202
202
 
@@ -40,7 +40,9 @@ module Vines
40
40
  end
41
41
 
42
42
  def process_request(request)
43
- if request.path == self.bind
43
+ if request.path == self.bind && request.options?
44
+ request.reply_to_options
45
+ elsif request.path == self.bind
44
46
  body = Nokogiri::XML(request.body).root
45
47
  if session = Sessions[body['sid']]
46
48
  @session = session
@@ -11,7 +11,11 @@ module Vines
11
11
  raise StreamErrors::NotAuthorized
12
12
  end
13
13
  stream.parse_body(node).each do |child|
14
- super(child)
14
+ begin
15
+ super(child)
16
+ rescue StanzaError => e
17
+ stream.error(e)
18
+ end
15
19
  end
16
20
  stream.terminate if terminate?(node)
17
21
  end
@@ -11,6 +11,7 @@ module Vines
11
11
  NOT_MODIFIED = 'Not Modified'.freeze
12
12
  IF_MODIFIED = 'If-Modified-Since'.freeze
13
13
  TEXT_PLAIN = 'text/plain'.freeze
14
+ OPTIONS = 'OPTIONS'.freeze
14
15
  CONTENT_TYPES = {
15
16
  'html' => 'text/html; charset="utf-8"',
16
17
  'js' => 'application/javascript; charset="utf-8"',
@@ -72,12 +73,33 @@ module Vines
72
73
  body = node.to_s
73
74
  header = [
74
75
  "HTTP/1.1 200 OK",
76
+ "Access-Control-Allow-Origin: *",
75
77
  "Content-Type: #{content_type}",
76
78
  "Content-Length: #{body.bytesize}"
77
79
  ].join("\r\n")
78
80
  @stream.stream_write([header, body].join("\r\n\r\n"))
79
81
  end
80
82
 
83
+ # Return true if the request method is OPTIONS, signaling a
84
+ # CORS preflight check.
85
+ def options?
86
+ @method == OPTIONS
87
+ end
88
+
89
+ # Send a 200 OK response, allowing any origin domain to connect to the
90
+ # server, in response to CORS preflight OPTIONS requests. This allows
91
+ # any web application using strophe.js to connect to our BOSH port.
92
+ def reply_to_options
93
+ allow = @headers['Access-Control-Request-Headers']
94
+ headers = [
95
+ "Access-Control-Allow-Origin: *",
96
+ "Access-Control-Allow-Methods: POST, GET, OPTIONS",
97
+ "Access-Control-Allow-Headers: #{allow}",
98
+ "Access-Control-Max-Age: #{60 * 60 * 24 * 30}"
99
+ ]
100
+ send_status(200, 'OK', headers)
101
+ end
102
+
81
103
  private
82
104
 
83
105
  # Attempt to rebuild the full request URI from the Host header. If it
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Vines
4
- VERSION = '0.4.0'
4
+ VERSION = '0.4.1'
5
5
  end
@@ -46,7 +46,7 @@ class HostTest < MiniTest::Unit::TestCase
46
46
  end
47
47
  end
48
48
  end
49
- refute_nil config.vhosts['wonderland.lit'].storage
49
+ refute_nil config.vhost('wonderland.lit').storage
50
50
  end
51
51
 
52
52
  def test_ldap_added_to_storage
@@ -81,8 +81,8 @@ class HostTest < MiniTest::Unit::TestCase
81
81
  end
82
82
  end
83
83
  %w[wonderland.lit verona.lit].each do |domain|
84
- refute_nil config.vhosts[domain].storage.ldap
85
- assert config.vhosts[domain].storage.ldap?
84
+ refute_nil config.vhost(domain).storage.ldap
85
+ assert config.vhost(domain).storage.ldap?
86
86
  end
87
87
  end
88
88
 
@@ -225,7 +225,7 @@ class HostTest < MiniTest::Unit::TestCase
225
225
  components 'TEA' => 'secr3t', CAKE: 'Passw0rd'
226
226
  end
227
227
  end
228
- host = config.vhosts['wonderland.lit']
228
+ host = config.vhost('wonderland.lit')
229
229
  refute_nil host
230
230
  assert_equal 2, host.components.size
231
231
  assert_equal host.components['tea.wonderland.lit'], 'secr3t'
@@ -239,7 +239,7 @@ class HostTest < MiniTest::Unit::TestCase
239
239
  components 'tea' => 'secr3t', cake: 'passw0rd'
240
240
  end
241
241
  end
242
- host = config.vhosts['wonderland.lit']
242
+ host = config.vhost('wonderland.lit')
243
243
  refute_nil host
244
244
  refute host.component?(nil)
245
245
  refute host.component?('tea')
@@ -309,7 +309,7 @@ class HostTest < MiniTest::Unit::TestCase
309
309
  pubsub 'TEA', :CAKE
310
310
  end
311
311
  end
312
- host = config.vhosts['wonderland.lit']
312
+ host = config.vhost('wonderland.lit')
313
313
  refute_nil host
314
314
  assert_equal 2, host.pubsubs.size
315
315
  refute_nil host.pubsubs['tea.wonderland.lit']
@@ -323,7 +323,7 @@ class HostTest < MiniTest::Unit::TestCase
323
323
  pubsub 'tea', :cake
324
324
  end
325
325
  end
326
- host = config.vhosts['wonderland.lit']
326
+ host = config.vhost('wonderland.lit')
327
327
  refute_nil host
328
328
  refute host.pubsub?(nil)
329
329
  refute host.pubsub?('tea')
@@ -348,7 +348,7 @@ class HostTest < MiniTest::Unit::TestCase
348
348
  storage(:fs) { dir Dir.tmpdir }
349
349
  end
350
350
  end
351
- host = config.vhosts['wonderland.lit']
351
+ host = config.vhost('wonderland.lit')
352
352
  refute_nil host
353
353
  refute host.private_storage?
354
354
  end
@@ -360,7 +360,7 @@ class HostTest < MiniTest::Unit::TestCase
360
360
  storage(:fs) { dir Dir.tmpdir }
361
361
  end
362
362
  end
363
- host = config.vhosts['wonderland.lit']
363
+ host = config.vhost('wonderland.lit')
364
364
  refute_nil host
365
365
  assert host.private_storage?
366
366
  assert config.private_storage?('wonderland.lit')
@@ -52,7 +52,7 @@ class ConfigPubSubTest < MiniTest::Unit::TestCase
52
52
  def test_subscribe_remote_jid_is_allowed
53
53
  topic = 'remote_jids_allowed'
54
54
  jid = 'romeo@verona.lit'
55
- @config.vhosts['wonderland.lit'].cross_domain_messages true
55
+ @config.vhost('wonderland.lit').cross_domain_messages true
56
56
  @pubsub.add_node(topic)
57
57
  @pubsub.subscribe(topic, jid)
58
58
  assert @pubsub.subscribed?(topic, jid)
@@ -105,7 +105,7 @@ class ConfigPubSubTest < MiniTest::Unit::TestCase
105
105
  alice = Vines::JID.new('alice@wonderland.lit')
106
106
  romeo = Vines::JID.new('romeo@verona.lit')
107
107
 
108
- @config.vhosts['wonderland.lit'].cross_domain_messages true
108
+ @config.vhost('wonderland.lit').cross_domain_messages true
109
109
  def @config.router
110
110
  unless @mock_router
111
111
  @mock_router = MiniTest::Mock.new
@@ -60,8 +60,10 @@ class ConfigTest < MiniTest::Unit::TestCase
60
60
  storage(:fs) { dir Dir.tmpdir }
61
61
  end
62
62
  end
63
- assert_equal ['wonderland.lit'], config.vhosts.keys
63
+ refute_nil config.vhost('wonderland.lit')
64
+ refute_nil config.vhost(Vines::JID.new('wonderland.lit'))
64
65
  assert config.vhost?('wonderland.lit')
66
+ assert config.vhost?(Vines::JID.new('wonderland.lit'))
65
67
  refute config.vhost?('alice@wonderland.lit')
66
68
  refute config.vhost?('tea.wonderland.lit')
67
69
  refute config.vhost?('bogus')
@@ -394,8 +396,8 @@ class ConfigTest < MiniTest::Unit::TestCase
394
396
  storage(:fs) { dir Dir.tmpdir }
395
397
  end
396
398
  end
397
- refute config.vhosts['wonderland.lit'].cross_domain_messages?
398
- assert config.vhosts['verona.lit'].cross_domain_messages?
399
+ refute config.vhost('wonderland.lit').cross_domain_messages?
400
+ assert config.vhost('verona.lit').cross_domain_messages?
399
401
  end
400
402
 
401
403
  def test_local_jid?
@@ -115,10 +115,19 @@ class JidTest < MiniTest::Unit::TestCase
115
115
  assert_raises(ArgumentError) { Vines::JID.new(%q{alice>s@wonderland.lit}) }
116
116
  assert_raises(ArgumentError) { Vines::JID.new("alice\u0000s@wonderland.lit") }
117
117
  assert_raises(ArgumentError) { Vines::JID.new("alice\ts@wonderland.lit") }
118
+ assert_raises(ArgumentError) { Vines::JID.new("alice\rs@wonderland.lit") }
119
+ assert_raises(ArgumentError) { Vines::JID.new("alice\ns@wonderland.lit") }
120
+ assert_raises(ArgumentError) { Vines::JID.new("alice\vs@wonderland.lit") }
121
+ assert_raises(ArgumentError) { Vines::JID.new("alice\fs@wonderland.lit") }
118
122
  assert_raises(ArgumentError) { Vines::JID.new(" alice@wonderland.lit") }
119
123
  assert_raises(ArgumentError) { Vines::JID.new("alice@wonderland.lit ") }
120
124
  assert_raises(ArgumentError) { Vines::JID.new("alice s@wonderland.lit") }
121
125
  assert_raises(ArgumentError) { Vines::JID.new("alice@w onderland.lit") }
126
+ assert_raises(ArgumentError) { Vines::JID.new("alice@w\tonderland.lit") }
127
+ assert_raises(ArgumentError) { Vines::JID.new("alice@w\ronderland.lit") }
128
+ assert_raises(ArgumentError) { Vines::JID.new("alice@w\nonderland.lit") }
129
+ assert_raises(ArgumentError) { Vines::JID.new("alice@w\vonderland.lit") }
130
+ assert_raises(ArgumentError) { Vines::JID.new("alice@w\fonderland.lit") }
122
131
  assert_raises(ArgumentError) { Vines::JID.new("alice@wonderland.lit/ res") }
123
132
  assert_raises(ArgumentError) { Vines::JID.new("alice@w\u0000onderland.lit") }
124
133
  assert_raises(ArgumentError) { Vines::JID.new("alice@wonderland.lit/\u0000res") }