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

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.
@@ -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