octokey 0.1.pre.2 → 0.1.pre.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,210 @@
1
+ class Octokey
2
+ # An AuthRequest is sent by the client when it wants to log in or sign up.
3
+ #
4
+ # It includes an {Octokey::Challenge} so that we can verify its recency, and
5
+ # also the username the user wishes to log in as, the url that they wish to
6
+ # log in to, and the public key corresponding to their private key.
7
+ #
8
+ # You can create an Octokey::AuthRequest from any string, and later determine
9
+ # whether or not it was valid by calling {#valid?}
10
+ class AuthRequest
11
+ # The service name is used to check that the client knows which protocol it is speaking.
12
+ SERVICE_NAME = "octokey-auth"
13
+ # The auth method indicates that the client wants to use publickey authentication.
14
+ AUTH_METHOD = "publickey"
15
+ # The signing algorithm is copied straight from SSH.
16
+ SIGNING_ALGORITHM = "ssh-rsa"
17
+
18
+ attr_accessor :challenge_buffer, :request_url, :username, :service_name,
19
+ :auth_method, :signing_algorithm, :public_key, :signature_buffer,
20
+ :invalid_buffer
21
+
22
+ # Given a challenge and a private key, generate an auth request.
23
+ #
24
+ # @param [Hash] opts
25
+ # @option opts [String] :request_url
26
+ # @option opts [String] :username
27
+ # @option opts [String] :challenge The base64-encoded challenge
28
+ # @option opts [OpenSSL::PKey::RSA] :private_key
29
+ # @return [Octokey::AuthRequest]
30
+ def self.generate(opts)
31
+ private_key = opts[:private_key] or raise ArgumentError, "No private_key given"
32
+ challenge = opts[:challenge] or raise ArgumentError, "No challenge given"
33
+
34
+ new.instance_eval do
35
+ self.challenge_buffer = Octokey::Buffer.new(challenge)
36
+ self.request_url = opts[:request_url] or raise ArgumentError, "No request_url given"
37
+ self.username = opts[:username] or raise ArgumentError, "No username given"
38
+ self.service_name = SERVICE_NAME
39
+ self.auth_method = AUTH_METHOD
40
+ self.signing_algorithm = SIGNING_ALGORITHM
41
+ self.public_key = Octokey::PublicKey.from_key(private_key.public_key)
42
+ self.signature_buffer = signature_buffer_with(private_key)
43
+
44
+ self
45
+ end
46
+ end
47
+
48
+ # Parse an auth request sent from the client.
49
+ #
50
+ # @param[String] The base64-encoded auth request from the client.
51
+ # @return [Octokey::AuthRequest]
52
+ def self.from_string(string)
53
+ buffer = Octokey::Buffer.new(string)
54
+ new.instance_eval do
55
+ begin
56
+ self.challenge_buffer, self.request_url, self.username,
57
+ self.service_name, self.auth_method, self.signing_algorithm,
58
+ self.public_key, self.signature_buffer =
59
+ buffer.scan_all(
60
+ :buffer, :string, :string,
61
+ :string, :string, :string,
62
+ :public_key, :buffer)
63
+ rescue Octokey::InvalidBuffer => e
64
+ self.invalid_buffer = e.message
65
+ end
66
+
67
+ self
68
+ end
69
+ end
70
+
71
+ # Get any errors ignoring those caused by the challenge.
72
+ #
73
+ # @param [Hash] opts
74
+ # @return [Array<String>]
75
+ def errors_ignoring_challenge(opts)
76
+ return [invalid_buffer] if invalid_buffer
77
+ errors = []
78
+
79
+ errors += request_url_errors(opts)
80
+ errors << "Auth request username mismatch" unless username == opts[:username]
81
+ errors << "Auth request service name mismatch" unless service_name == SERVICE_NAME
82
+ errors << "Auth request auth method unsupported" unless auth_method == AUTH_METHOD
83
+ errors << "Auth request signing algorithm unsupported" unless signing_algorithm == SIGNING_ALGORITHM
84
+
85
+ if public_key.valid?
86
+ errors += signature_errors(public_key.public_key, signature_buffer.dup)
87
+ else
88
+ errors += public_key.errors
89
+ end
90
+
91
+ errors
92
+ end
93
+
94
+ # Get any errors caused by the challenge.
95
+ #
96
+ # @param [Hash] opts
97
+ # @return [Array<String>]
98
+ def challenge_errors(opts)
99
+ return [] if invalid_buffer
100
+ Octokey::Config.get_challenge(challenge_buffer.to_s, opts).errors(opts)
101
+ end
102
+
103
+ # Get all the error for this auth request.
104
+ #
105
+ # @param [Hash] opts
106
+ # @return [Array<String>]
107
+ def errors(opts)
108
+ errors_ignoring_challenge(opts) + challenge_errors(opts)
109
+ end
110
+
111
+ # If the challenge was valid, would this auth request be valid?
112
+ #
113
+ # This can be used to check whether the auth request should be retried.
114
+ #
115
+ # @param [Hash] opts
116
+ # @return [Boolean]
117
+ def valid_ignoring_challenge?(opts)
118
+ errors_ignoring_challenge(opts) == []
119
+ end
120
+
121
+ # Is this auth request valid?
122
+ #
123
+ # @param [Hash] opts
124
+ # @return [Boolean]
125
+ def valid?(opts)
126
+ errors(opts) == []
127
+ end
128
+
129
+ # Get the Base64-encoded version of this auth request.
130
+ #
131
+ # @return [String]
132
+ def to_s
133
+ unsigned_buffer.add_buffer(signature_buffer).to_s
134
+ end
135
+
136
+ # Get a string that identifies this auth request while debugging
137
+ #
138
+ # @return [String]
139
+ def inspect
140
+ "#<Octokey::AuthRequest #{to_s.inspect}>"
141
+ end
142
+
143
+ private
144
+
145
+ # What are the problems with the signature?
146
+ #
147
+ # @param [OpenSSL::PKey::RSA] key the public key
148
+ # @param [Octokey::Buffer] signature_buffer the signature buffer
149
+ # @return [Array<String>]
150
+ def signature_errors(key, signature_buffer)
151
+ algorithm_used, signature = signature_buffer.scan_all(:string, :varbytes)
152
+
153
+ errors = []
154
+ errors << "Signature type mismatch" unless algorithm_used == signing_algorithm
155
+ errors << "Signature mismatch" unless key.verify(OpenSSL::Digest::SHA1.new, signature, unsigned_buffer.raw)
156
+ errors
157
+
158
+ rescue Octokey::InvalidBuffer => e
159
+ ["Signature #{e.message}"]
160
+ end
161
+
162
+ # What are the problems with the request url?
163
+ #
164
+ # @param [Hash] opts
165
+ # @return [Array<String>]
166
+ def request_url_errors(opts)
167
+ url = URI.parse(request_url)
168
+
169
+ valid_hostname = Octokey::Config.valid_hostnames.any? do |hostname|
170
+ if hostname[/\A\*\.(.*)\z/]
171
+ url.host.end_with?($1)
172
+ else
173
+ url.host == hostname
174
+ end
175
+ end
176
+
177
+ errors = []
178
+ errors << "Request url insecure" unless url.scheme == "https"
179
+ errors << "Request url mismatch" unless valid_hostname
180
+ errors
181
+
182
+ rescue URI::InvalidURIError
183
+ ["Request url invalid"]
184
+ end
185
+
186
+ # Get the buffer containing everything other than the signature.
187
+ #
188
+ # @return [Octokey::Buffer]
189
+ def unsigned_buffer
190
+ Octokey::Buffer.new.
191
+ add_buffer(challenge_buffer).
192
+ add_string(request_url).
193
+ add_string(username).
194
+ add_string(service_name).
195
+ add_string(auth_method).
196
+ add_string(signing_algorithm).
197
+ add_public_key(public_key)
198
+ end
199
+
200
+ # Get the signature buffer using the given key.
201
+ #
202
+ # @param [OpenSSL::PKey::RSA] private_key
203
+ # @return [Octokey::Buffer]
204
+ def signature_buffer_with(private_key)
205
+ Octokey::Buffer.new.
206
+ add_string(SIGNING_ALGORITHM).
207
+ add_varbytes(private_key.sign(OpenSSL::Digest::SHA1.new, unsigned_buffer.raw))
208
+ end
209
+ end
210
+ end
@@ -1,12 +1,24 @@
1
1
  require 'base64'
2
2
  class Octokey
3
+ # Buffers are used throughout Octokey to provide a bijective serialization format.
4
+ # For any valid buffer, there's exactly one valid object, and vice-versa.
5
+ #
6
+ # Mostly we used Base64-encoded buffers to avoid problems with potentially 8-bit
7
+ # unsafe channels. You should take care not to perform any operations on the Base64
8
+ # encoded form as there are many accepted formats for Base64-encoding a given string.
9
+ #
10
+ # In the current implementation, reading out of a buffer is a destructive operation,
11
+ # you should first .dup any buffer that you want to read more than once.
3
12
  class Buffer
4
- attr_accessor :buffer, :pos
13
+ attr_accessor :buffer, :invalid_buffer
5
14
 
6
15
  # to avoid DOS caused by duplicating enourmous buffers,
7
16
  # we limit the maximum size of any string stored to 100k
8
17
  MAX_STRING_SIZE = 100 * 1024
9
18
 
19
+ # Create a new buffer from raw bits.
20
+ #
21
+ # @param [String] raw
10
22
  def self.from_raw(raw = "")
11
23
  ret = new
12
24
  ret.buffer = raw.dup
@@ -14,95 +26,107 @@ class Octokey
14
26
  ret
15
27
  end
16
28
 
29
+ # Create a new buffer from a Base64-encoded string.
30
+ # @param [String] string
17
31
  def initialize(string = "")
18
32
  self.buffer = Base64.decode64(string || "")
19
- self.pos = 0
20
- buffer.force_encoding('BINARY') if @buffer.respond_to?(:force_encoding)
33
+ buffer.force_encoding('BINARY') if buffer.respond_to?(:force_encoding)
34
+ self.invalid_buffer = "Badly formatted Base64" unless to_s == string
21
35
  end
22
36
 
37
+ # Get the underlying bits contained in this buffer.
38
+ # @return [String]
23
39
  def raw
24
40
  buffer
25
41
  end
26
-
27
- def empty?
28
- buffer.empty?
29
- end
30
-
42
+
43
+ # Get the canonical Base64 representation of this buffer.
44
+ # @return [String]
31
45
  def to_s
32
46
  Base64.encode64(buffer).gsub("\n", "")
33
47
  end
34
48
 
35
- def <<(bytes)
36
- buffer << bytes
49
+ # Get a string that describes this buffer suitably for debugging.
50
+ # @return [String]
51
+ def inspect
52
+ "#<Octokey::Buffer @buffer=#{to_s.inspect}>"
37
53
  end
38
54
 
39
- def scan(n)
40
- ret, buf = [buffer[0...n], buffer[n..-1]]
41
- if ret.size < n || !buf
42
- raise InvalidBuffer, "Tried to read beyond end of buffer"
43
- end
44
- self.buffer = buf
45
- ret
55
+ # Is this buffer empty?
56
+ # @return [Boolean]
57
+ def empty?
58
+ buffer.empty?
46
59
  end
47
60
 
61
+ # Add an unsigned 8-bit number to this buffer
62
+ # @param [Fixnum] x
63
+ # @return [Octokey::Buffer] self
64
+ # @raise [Octokey::InvalidBuffer] if x is not a uint8
48
65
  def add_uint8(x)
49
66
  raise InvalidBuffer, "Invalid uint8: #{x}" if x < 0 || x >= 2 ** 8
50
67
  buffer << [x].pack("C")
68
+ self
51
69
  end
52
70
 
71
+ # Destructively read an unsigned 8-bit number from this buffer
72
+ # @return [Fixnum]
73
+ # @raise [Octokey::InvalidBuffer]
53
74
  def scan_uint8
54
75
  scan(1).unpack("C").first
55
76
  end
56
77
 
57
- def add_uint32(x)
58
- raise InvalidBuffer, "Invalid uint32: #{x}" if x < 0 || x >= 2 ** 32
59
- buffer << [x].pack("N")
60
- end
61
-
62
- def scan_uint32
63
- scan(4).unpack("N").first
64
- end
65
-
66
- def add_uint64(x)
67
- raise InvalidBuffer, "Invalid uint64: #{x}" if x < 0 || x >= 2 ** 64
68
- add_uint32(x >> 32 & 0xffff_ffff)
69
- add_uint32(x & 0xffff_ffff)
70
- end
71
-
72
- def scan_uint64
73
- (scan_uint32 << 32) + scan_uint32
74
- end
75
-
76
- def add_uint128(x)
77
- raise InvalidBuffer, "Invalid uint128: #{x}" if x < 0 || x >= 2 ** 128
78
- add_uint64(x >> 64 & 0xffff_ffff_ffff_ffff)
79
- add_uint64(x & 0xffff_ffff_ffff_ffff)
80
- end
81
-
82
- def scan_uint128
83
- (scan_uint64 << 64) + scan_uint64
84
- end
85
-
78
+ # Add a timestamp to this buffer
79
+ #
80
+ # Times are stored to millisecond precision, and are limited to
81
+ # 2 **48 to give plenty of margin for implementations using doubles
82
+ # as the backing for their date time, which nicely gives us a range
83
+ # ending just after the year 10000.
84
+ #
85
+ # @param [Time] time
86
+ # @return [Octokey::Buffer] self
87
+ # @raise [Octokey::InvalidBuffer] if the time is too far into the future
86
88
  def add_time(time)
87
- add_uint64((time.to_f * 1000).to_i)
89
+ seconds, millis = [time.to_i, (time.usec / 1000.0).round]
90
+ raw = seconds * 1000 + millis
91
+ raise Octokey::InvalidBuffer, "Invalid time" if raw >= 2 ** 48
92
+ add_uint64(raw)
93
+ self
88
94
  end
89
95
 
96
+ # Destructively read a timestamp from this buffer
97
+ #
98
+ # Times are stored to millisecond precision
99
+ #
100
+ # @return [Time]
101
+ # @raise [Octokey::InvalidBuffer]
90
102
  def scan_time
91
- Time.at(scan_uint64.to_f / 1000)
103
+ raw = scan_uint64
104
+ raise Octokey::InvalidBuffer, "Invalid time" if raw >= 2 ** 48
105
+ seconds, millis = [raw / 1000, raw % 1000]
106
+ Time.at(seconds).utc + (millis / 1000.0)
92
107
  end
93
108
 
109
+ # Add an IPv4 or IPv6 address to this buffer
110
+ #
111
+ # @param [IPAddr] ipaddr
112
+ # @return [Octokey::Buffer] self
113
+ # @raise [Octokey::InvalidBuffer] not a valid IP address
94
114
  def add_ip(ipaddr)
95
115
  if ipaddr.ipv4?
96
116
  add_uint8(4)
97
- add_uint32(ipaddr.to_i)
117
+ buffer << ipaddr.hton
98
118
  elsif ipaddr.ipv6?
99
119
  add_uint8(6)
100
- add_uint128(ipaddr.to_i)
120
+ buffer << ipaddr.hton
101
121
  else
102
122
  raise InvalidBuffer, "Unsupported IP address: #{ipaddr.to_s}"
103
123
  end
124
+ self
104
125
  end
105
126
 
127
+ # Destructively read an IPv4 or IPv6 address from this buffer.
128
+ # @return [IPAddr]
129
+ # @raise [Octokey::InvalidBuffer]
106
130
  def scan_ip
107
131
  type = scan_uint8
108
132
  case type
@@ -111,69 +135,210 @@ class Octokey
111
135
  when 6
112
136
  IPAddr.new_ntoh scan(16)
113
137
  else
114
- raise InvalidBuffer, "Unsupported IP address family: #{type}"
138
+ raise InvalidBuffer, "Unknown IP family: #{type.inspect}"
115
139
  end
116
140
  end
117
141
 
142
+ # Add a length-prefixed number of bytes to this buffer
143
+ # @param [String] bytes
144
+ # @return [Octokey::Buffer] self
145
+ # @raise [Octokey::InvalidBuffer] if there are too any bytes
118
146
  def add_varbytes(bytes)
147
+ bytes.force_encoding('BINARY') if bytes.respond_to?(:force_encoding)
119
148
  size = bytes.size
120
- raise InvalidBuffer, "String too long: #{size}" if size > MAX_STRING_SIZE
149
+ raise InvalidBuffer, "Too much length: #{size}" if size > MAX_STRING_SIZE
121
150
  add_uint32 size
122
- self << bytes
151
+ buffer << bytes
152
+ self
123
153
  end
124
154
 
155
+ # Destructively read a length-prefixed number of bytes from this buffer
156
+ # @return [String] bytes
157
+ # @raise [Octokey::InvalidBuffer]
125
158
  def scan_varbytes
126
159
  size = scan_uint32
127
- raise InvalidBuffer, "String too long: #{size}" if size > MAX_STRING_SIZE
160
+ raise InvalidBuffer, "Too much length: #{size}" if size > MAX_STRING_SIZE
128
161
  scan(size)
129
162
  end
130
163
 
164
+ # Add a length-prefixed number of bytes of UTF-8 string to this buffer
165
+ # @param [String] string
166
+ # @return [Octokey::Buffer] self
167
+ # @raise [Octokey::InvalidBuffer] if the string is not utf-8
131
168
  def add_string(string)
132
- if string.respond_to?(:encode)
133
- add_varbytes string.encode('BINARY')
134
- else
135
- add_varbytes string
136
- end
169
+ add_varbytes(validate_utf8(string))
137
170
  end
138
171
 
172
+ # Destructively read a length-prefixed number of bytes of UTF-8 string
173
+ # @return [String] with encoding == 'utf-8' on ruby-1.9
174
+ # @raise [Octokey::InvalidBuffer]
139
175
  def scan_string
140
- string = scan_varbytes
141
- if string.respond_to?(:encode)
142
- string.encode('UTF-8')
143
- else
144
- string
145
- end
146
- rescue EncodingError => e
147
- raise InvalidBuffer, e
176
+ validate_utf8(scan_varbytes)
148
177
  end
149
178
 
179
+ # Add the length-prefixed contents of another buffer to this one.
180
+ # @param [Octokey::Buffer] buffer
181
+ # @return [Octokey::Buffer] self
182
+ # @raise [Octokey::InvalidBuffer]
150
183
  def add_buffer(buffer)
151
184
  add_varbytes buffer.raw
185
+ self
152
186
  end
153
187
 
188
+ # Destrictively read a length-prefixed buffer out of this one.
189
+ # @return [Octokey::Buffer]
190
+ # @raise [Octokey::InvalidBuffer]
154
191
  def scan_buffer
155
192
  Octokey::Buffer.from_raw scan_varbytes
156
193
  end
157
194
 
195
+ # Add an unsigned multi-precision integer to this buffer
196
+ # @param [OpenSSL::BN,Fixnum] x
197
+ # @return [Octokey::Buffer] self
198
+ # @raise [Octokey::InvalidBuffer] if x is negative or enourmous
158
199
  def add_mpint(x)
159
- raise InvalidBuffer, "Got negative mpint" if x < 0
200
+ raise InvalidBuffer, "Invalid mpint: #{mpint.inspect}" if x < 0
160
201
  bytes = OpenSSL::BN.new(x.to_s, 10).to_s(2)
161
202
  bytes = "\x00" + bytes if bytes.bytes.first >= 0x80
162
203
  add_varbytes(bytes)
204
+ self
163
205
  end
164
206
 
207
+ # Destructively read an unsigned multi-precision integer from this buffer
208
+ # @return [OpenSSL::BN]
209
+ # @raise [Octokey::InvalidBuffer]
165
210
  def scan_mpint
166
- bytes = scan_varbytes
211
+ raw = scan_varbytes
167
212
 
168
- if bytes.bytes.first >= 0x80
169
- raise InvalidBuffer, "Got negative mpint"
213
+ first, second = raw.bytes.first(2)
214
+
215
+ # ensure only positive numbers with no superflous leading 0s
216
+ if first >= 0x80 || first == 0x00 && second < 0x80
217
+ raise InvalidBuffer, "Badly formatted mpint"
170
218
  end
171
219
 
172
- OpenSSL::BN.new(bytes, 2)
220
+ OpenSSL::BN.new(raw, 2)
173
221
  end
174
222
 
175
- def inspect
176
- "#<Octokey::Buffer @buffer=#{to_s.inspect}>"
223
+ # Destructively read a public key from this buffer
224
+ #
225
+ # NOTE: the returned public key may not be valid, you must call
226
+ # .valid? on it before trying to use it.
227
+ #
228
+ # @return [Octokey::PublicKey]
229
+ # @raise [Octokey::InvalidBuffer]
230
+ def scan_public_key
231
+ Octokey::PublicKey.from_buffer(scan_buffer)
232
+ end
233
+
234
+ # Add a public key to this buffer
235
+ # @param [Octokey::PublicKey] public_key
236
+ # @return [Octokey::Buffer] self
237
+ # @raise [Octokey::InvalidBuffer]
238
+ def add_public_key(public_key)
239
+ add_buffer public_key.to_buffer
240
+ end
241
+
242
+ # Destructively read the entire buffer.
243
+ #
244
+ # It's strongly recommended that you use this method to parse buffers, as it
245
+ # remembers to verify that the buffer doesn't contain any trailing bytes; and
246
+ # will return nothing if the buffer is invalid, so your code doesn't have to
247
+ # deal with half-parsed buffers.
248
+ #
249
+ # The tokens should correspond to the scan_X methods defined here. For example:
250
+ # type, e, n = buffer.scan_all(:string, :mpint, :mpint)
251
+ # is equivalent to:
252
+ # type, e, n, _ = [buffer.scan_string, buffer.scan_mpint, buffer.scan_mpint,
253
+ # buffer.scan_end]
254
+ #
255
+ # @param [Array<Symbol>] tokens
256
+ # @return [Array<Object>]
257
+ # @raise [Octokey::InvalidBuffer]
258
+ def scan_all(*tokens)
259
+ ret = tokens.map do |token|
260
+ raise "invalid token type: #{token.inspect}" unless respond_to?("scan_#{token}")
261
+ send("scan_#{token}")
262
+ end
263
+
264
+ scan_end
265
+ ret
266
+ end
267
+
268
+ # Verify that the buffer has been completely scanned.
269
+ # @raise [Octokey::InvalidBuffer] if there is still buffer to read.
270
+ def scan_end
271
+ raise InvalidBuffer, "Buffer too long" unless empty?
272
+ end
273
+
274
+ private
275
+
276
+ # Destructively read bytes from the front of this buffer.
277
+ # @param [Fixnum] n
278
+ # @return [String]
279
+ # @raise [Octokey::InvalidBuffer]
280
+ def scan(n)
281
+ raise InvalidBuffer, invalid_buffer if invalid_buffer
282
+ ret, buf = [buffer[0...n], buffer[n..-1]]
283
+ if ret.size < n || !buf
284
+ raise InvalidBuffer, "Buffer too short"
285
+ end
286
+ self.buffer = buf
287
+ ret
288
+ end
289
+
290
+ # Add an unsigned 32-bit number to this buffer
291
+ # @param [Fixnum] x
292
+ # @return [Octokey::Buffer] self
293
+ # @raise [Octokey::InvalidBuffer] if x is not a uint32
294
+ def add_uint32(x)
295
+ raise InvalidBuffer, "Invalid uint32: #{x}" if x < 0 || x >= 2 ** 32
296
+ buffer << [x].pack("N")
297
+ self
298
+ end
299
+
300
+ # Destructively read an unsigned 32-bit number from this buffer
301
+ # @return [Fixnum]
302
+ # @raise [Octokey::InvalidBuffer]
303
+ def scan_uint32
304
+ scan(4).unpack("N").first
305
+ end
306
+
307
+ # Add an unsigned 64-bit number to this buffer
308
+ # @param [Fixnum] x
309
+ # @return [Octokey::Buffer] self
310
+ # @raise [Octokey::InvalidBuffer] if x is not a uint64
311
+ def add_uint64(x)
312
+ raise InvalidBuffer, "Invalid uint64: #{x}" if x < 0 || x >= 2 ** 64
313
+ add_uint32(x >> 32 & 0xffff_ffff)
314
+ add_uint32(x & 0xffff_ffff)
315
+ self
316
+ end
317
+
318
+ # Destructively read an unsigned 64-bit number from this buffer
319
+ # @return [Fixnum]
320
+ # @raise [Octokey::InvalidBuffer]
321
+ def scan_uint64
322
+ (scan_uint32 << 32) + scan_uint32
323
+ end
324
+
325
+ # Check whether a string is valid utf-8
326
+ # @param [String] string
327
+ # @return [String] string
328
+ # @raise [Octokey::InvalidBuffer] invalid utf-8
329
+ def validate_utf8(string)
330
+ if string.respond_to?(:force_encoding)
331
+ string.force_encoding('UTF-8')
332
+ raise InvalidBuffer, "String not UTF-8" unless string.valid_encoding?
333
+ string
334
+ else
335
+ require 'iconv'
336
+ begin
337
+ Iconv.conv('utf-8', 'utf-8', string)
338
+ rescue Iconv::Failure
339
+ raise InvalidBuffer, "String not UTF-8"
340
+ end
341
+ end
177
342
  end
178
343
  end
179
344
  end