miketracy-wwmd 0.2.16 → 0.2.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/History.txt +21 -0
  2. data/{README → README.rdoc} +27 -2
  3. data/lib/wwmd.rb +4 -4
  4. data/lib/wwmd/class_extensions.rb +2 -0
  5. data/lib/wwmd/{mixins.rb → class_extensions/extensions_base.rb} +25 -121
  6. data/lib/wwmd/class_extensions/extensions_encoding.rb +79 -0
  7. data/lib/wwmd/{mixins_external.rb → class_extensions/extensions_external.rb} +0 -0
  8. data/lib/wwmd/class_extensions/extensions_nilclass.rb +11 -0
  9. data/lib/wwmd/{mixins_extends.rb → class_extensions/extensions_rbkb.rb} +0 -0
  10. data/lib/wwmd/{encoding.rb → class_extensions/mixins_string_encoding.rb} +6 -6
  11. data/lib/wwmd/page.rb +3 -245
  12. data/lib/wwmd/page/auth.rb +0 -166
  13. data/lib/wwmd/page/constants.rb +7 -4
  14. data/lib/wwmd/page/form.rb +0 -15
  15. data/lib/wwmd/page/form_array.rb +96 -74
  16. data/lib/wwmd/page/headers.rb +25 -21
  17. data/lib/wwmd/page/helpers.rb +30 -0
  18. data/lib/wwmd/{hpricot_html2text.rb → page/html2text_hpricot.rb} +1 -1
  19. data/lib/wwmd/{nokogiri_html2text.rb → page/html2text_nokogiri.rb} +0 -0
  20. data/lib/wwmd/page/inputs.rb +1 -1
  21. data/lib/wwmd/page/irb_helpers.rb +37 -13
  22. data/lib/wwmd/page/page.rb +238 -0
  23. data/lib/wwmd/page/parsing_convenience.rb +8 -3
  24. data/lib/wwmd/page/scrape.rb +15 -19
  25. data/lib/wwmd/page/spider.rb +11 -11
  26. data/lib/wwmd/urlparse.rb +20 -5
  27. data/lib/wwmd/viewstate.rb +9 -112
  28. data/lib/wwmd/viewstate/viewstate.rb +101 -0
  29. data/lib/wwmd/viewstate/viewstate_deserializer_methods.rb +36 -36
  30. data/lib/wwmd/viewstate/viewstate_types.rb +0 -4
  31. data/lib/wwmd/viewstate/viewstate_utils.rb +6 -1
  32. data/lib/wwmd/viewstate/vs_stubs.rb +22 -0
  33. data/lib/wwmd/viewstate/{vs_array.rb → vs_stubs/vs_array.rb} +3 -1
  34. data/lib/wwmd/viewstate/{vs_binary_serialized.rb → vs_stubs/vs_binary_serialized.rb} +3 -1
  35. data/lib/wwmd/viewstate/{vs_hashtable.rb → vs_stubs/vs_hashtable.rb} +3 -1
  36. data/lib/wwmd/viewstate/{vs_hybrid_dict.rb → vs_stubs/vs_hybrid_dict.rb} +3 -1
  37. data/lib/wwmd/viewstate/{vs_indexed_string.rb → vs_stubs/vs_indexed_string.rb} +1 -1
  38. data/lib/wwmd/viewstate/{vs_indexed_string_ref.rb → vs_stubs/vs_indexed_string_ref.rb} +3 -1
  39. data/lib/wwmd/viewstate/{vs_int_enum.rb → vs_stubs/vs_int_enum.rb} +3 -1
  40. data/lib/wwmd/viewstate/{vs_list.rb → vs_stubs/vs_list.rb} +2 -1
  41. data/lib/wwmd/viewstate/{vs_pair.rb → vs_stubs/vs_pair.rb} +3 -1
  42. data/lib/wwmd/viewstate/vs_stubs/vs_read_types.rb +11 -0
  43. data/lib/wwmd/viewstate/{vs_read_value.rb → vs_stubs/vs_read_value.rb} +3 -1
  44. data/lib/wwmd/viewstate/{vs_sparse_array.rb → vs_stubs/vs_sparse_array.rb} +3 -1
  45. data/lib/wwmd/viewstate/{vs_string.rb → vs_stubs/vs_string.rb} +2 -1
  46. data/lib/wwmd/viewstate/{vs_string_array.rb → vs_stubs/vs_string_array.rb} +4 -2
  47. data/lib/wwmd/viewstate/{vs_string_formatted.rb → vs_stubs/vs_string_formatted.rb} +4 -2
  48. data/lib/wwmd/viewstate/{viewstate_class_helpers.rb → vs_stubs/vs_stub_helpers.rb} +2 -1
  49. data/lib/wwmd/viewstate/{vs_triplet.rb → vs_stubs/vs_triplet.rb} +3 -1
  50. data/lib/wwmd/viewstate/{vs_type.rb → vs_stubs/vs_type.rb} +3 -1
  51. data/lib/wwmd/viewstate/{vs_unit.rb → vs_stubs/vs_unit.rb} +3 -1
  52. data/lib/wwmd/viewstate/{vs_value.rb → vs_stubs/vs_value.rb} +4 -2
  53. data/lib/wwmd/wwmd_config.rb +44 -36
  54. data/lib/wwmd/wwmd_puts.rb +9 -0
  55. data/lib/wwmd/wwmd_utils.rb +22 -24
  56. data/tasks/setup.rb +1 -1
  57. metadata +41 -35
  58. data/README.txt +0 -62
  59. data/lib/wwmd/viewstate/vs_read_types.rb +0 -11
  60. data/wwmd.gemspec +0 -0
@@ -1,30 +1,6 @@
1
- =begin rdoc
2
- This is where we do all the undocumented auth stuff. NTLM is here and
3
- hooked in.
4
-
5
- WWMDNTLM is an incredibly naive NTLM implementation (used to get
6
- around NTLM for one project ahwile back
7
- =end
8
-
9
1
  module WWMD
10
2
  class Page
11
3
 
12
- #:stopdoc:
13
-
14
- #:section: Authentication helpers
15
-
16
- # check if this request requires NTLM
17
- def ntlm?
18
- return false if self.code != 401
19
- count = 0
20
- self.header_data.each do |i|
21
- if i[0] =~ /www-authenticate/i
22
- count += 1 if (i[1] == "Negotiate" || i[1] == "NTLM")
23
- end
24
- end
25
- return (count > 0)
26
- end
27
-
28
4
  # does this request have an authenticate header?
29
5
  def auth?
30
6
  return false if self.code != 401
@@ -37,147 +13,5 @@ module WWMD
37
13
  return (count > 0)
38
14
  end
39
15
 
40
- # not sure why this is here
41
- def ntlm_perform(exp=nil)#:nodoc:
42
- self.perform
43
- return (self.code == exp)
44
- end
45
-
46
- # perform a get usig NTLM
47
- def ntlm_get(url=nil?,debug=false)
48
- self.clear_header('Authorization')
49
- nobj = WWMDNTLM.new(self.opts)
50
- self.url = @urlparse.parse(self.opts[:base_url],url) if not url.nil?
51
- self.perform
52
- return "This request does not appear to require NTLM" if not self.ntlm?
53
- self.headers['Authorization'] = nobj.type_1_msg
54
- self.perform
55
- type2 = self.header_data.get_value('WWW-Authenticate')
56
- nonce = nobj.get_nonce(type2)
57
- type3 = nobj.type_3_msg(nonce)
58
- self.headers['Authorization'] = type3
59
- self.perform
60
- self.clear_header('Authorization')
61
- return self.code
62
- end
63
-
64
- #:startdoc:
65
-
66
- end
67
-
68
- class WWMDNTLM#:nodoc:
69
- attr_accessor :hostname
70
- attr_accessor :domain
71
- attr_accessor :username
72
- attr_accessor :password
73
- attr_accessor :opts
74
- attr_accessor :negotiate_flags
75
- attr_accessor :debug
76
-
77
- def initialize(opts,debug=false)
78
- @opts = opts
79
- @hostname = self.opts[:hostname]
80
- @domain = self.opts[:domain]
81
- @username = self.opts[:username]
82
- @password = self.opts[:password]
83
- @hostname = "LOCALHOST" if self.hostname.nil?
84
- @negotiate_flags = 0x00002201.to_l32
85
- @debug = debug
86
- end
87
-
88
- def type_1_msg
89
- # do not add domain for now here as it doesn't seem to be needed
90
- # it does need to be set for type3 messages
91
- host_len = self.hostname.size
92
- host_off = 0x20
93
- # if self.domain.nil?
94
- if true
95
- dom_off = 0
96
- dom_len = 0
97
- else
98
- dom_off = (host_off + hostname.size)
99
- dom_len = self.domain.size
100
- end
101
- msg = ""
102
- msg << "NTLMSSP\x00" # signature[8]
103
- msg << 0x01.to_l32 # type[4]
104
- msg << self.negotiate_flags # NegotiateFlags[4]
105
- msg << dom_len.to_l16 # domain string length[2]
106
- msg << dom_len.to_l16 # domain string length[2]
107
- msg << dom_off.to_l32 # domain string offset[4]
108
- msg << host_len.to_l16 # host string length[2]
109
- msg << host_len.to_l16 # host string length[2]
110
- msg << host_off.to_l32 # host string offset[4]
111
- # msg << self.domain if not self.domain.nil? # domain[var]
112
- msg << self.hostname # host name[var]
113
- return "NTLM " + msg.b64e
114
- end
115
-
116
- def get_nonce(t2msg)
117
- # Signature[8]
118
- # MessageType[4]
119
- # TargetNameFields[8]
120
- # NegotiateFlags[4]
121
- # ServerChallenge[8]
122
- # Reserved[8] ! 0x00
123
- # TargetInfoFields[8]
124
- # Version[8]
125
- # Payload[var]
126
- msg = t2msg.split[1].b64d
127
- return msg[24..31]
128
- end
129
-
130
- def type_3_msg(nonce)
131
- hlen = 0x40
132
- poff = hlen
133
- domain = self.domain.to_utf16
134
- username = self.username.to_utf16
135
- hostname = self.hostname.to_utf16
136
- lmresp = NTLM.lm_response(self.opts[:password],nonce,:lmhash)
137
- ntresp = NTLM.lm_response(self.opts[:password],nonce,:nthash)
138
- msg = ""
139
- msg << "NTLMSSP\x00" # Signature[8]
140
- msg << 0x03.to_l32 # MessageType[4]
141
- # LmChallengeResonseFields[8]
142
- msg << lmresp.size.to_l16 # LmChallengeResponseLen[2]
143
- msg << lmresp.size.to_l16 # LmChallengeResponseMaxLen[2]
144
- msg << poff.to_l32 # LmChallengeResponseBufferOffset[4]
145
- poff += lmresp.size
146
- # msg << 0x40.to_l32 # LmChallengeResponseBufferOffset[4]
147
- # NtChallengeResponseFields[8]
148
- msg << ntresp.size.to_l16 # NtChallengeResponseLen[2]
149
- msg << ntresp.size.to_l16 # NtChallengeResponseMaxLen[2]
150
- # msg << 0x58.to_l32 # NtChallengeResponseBufferOffset[4]
151
- msg << poff.to_l32 # NtChallengeResponseBufferOffset[4]
152
- poff += ntresp.size
153
- # DomainNameFields[8]
154
- msg << domain.size.to_l16 # DomainNameLen[2]
155
- msg << domain.size.to_l16 # DomainNameMaxLen[2]
156
- msg << poff.to_l32 # DomainNameBufferOffset[4]
157
- poff += domain.size
158
- # UserNameFields[8]
159
- msg << username.size.to_l16 # UserNameLen[2]
160
- msg << username.size.to_l16 # UserNameMaxLen[2]
161
- msg << poff.to_l32 # UserNameBufferOffset[4]
162
- poff += username.size
163
- # WorkstationFields[8]
164
- msg << hostname.size.to_l16 # WorkstationLen[2]
165
- msg << hostname.size.to_l16 # WorkstationMaxLen[2]
166
- msg << poff.to_l32 # WorkstationBufferOffset[4]
167
- poff += hostname.size
168
- # EncryptedRandomSessionKeyFields[8]
169
- msg << 0x00.to_l16 # EncryptedRandomSessionKeyLen[2]
170
- msg << 0x00.to_l16 # EncryptedRandomSessionKeyMaxLen[2]
171
- msg << 0x00.to_l32 # EncryptedRandomSessionKeyBufferOffset[4]
172
- msg << self.negotiate_flags # NegotiateFlags[4]
173
- # Version[8] (optional do not add)
174
- # Payload[var]
175
- msg << lmresp # LmChallenge[var]
176
- msg << ntresp # NtChallenge[var]
177
- msg << domain # DomainName[var]
178
- msg << username # UserName[var]
179
- msg << hostname # Workstation[var]
180
- return "NTLM " + msg.b64e
181
- end
182
16
  end
183
17
  end
@@ -18,10 +18,11 @@ module WWMD
18
18
  ESCAPE = {
19
19
  :url => /[^a-zA-Z0-9\-_%]/,
20
20
  :nalnum => /[^a-zA-Z0-9]/,
21
- :xss => /[^a-zA-Z0-9=?&()']/,
21
+ :xss => /[^a-zA-Z0-9=?()']/,
22
22
  :ltgt => /[<>]/,
23
23
  :all => /.*/,
24
- :b64 => /[=+\/]/,
24
+ # :b64 => /[=+\/]/,
25
+ :b64 => /[^a-zA-Z0-9]/,
25
26
  :none => :none,
26
27
  :default => :default,
27
28
  }
@@ -33,11 +34,13 @@ module WWMD
33
34
  :ie7 => "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
34
35
  :ie8 => "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)",
35
36
  :opera => "Opera/9.20 (Windows NT 6.0; U; en)",
36
- :safari => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_4_11; en) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.22"
37
+ :safari => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_4_11; en) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.22",
38
+ :safari4 => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17",
39
+ :wwmd => "Mozilla/5.0 (compatible; WWMD #{WWMD::VERSION}; o_hai)"
37
40
  }
38
41
 
39
42
  DEFAULT_HEADERS = {
40
- "User-Agent" => UA[:moz3],
43
+ "User-Agent" => UA[:wwmd],
41
44
  "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
42
45
  "Accept-Language" => "en-US,en;q=0.8,en-au;q=0.6,en-us;q=0.4,en;q=0.2",
43
46
  "Accept-Encoding" => "gzip,deflate",
@@ -40,21 +40,6 @@ module WWMD
40
40
  return self.get_attribute("action")
41
41
  end
42
42
 
43
- def report
44
- puts "action = #{self.action}"
45
- self.fields.each { |field| puts field.to_text }
46
- return nil
47
- end
48
-
49
- alias_method :show, :report
50
-
51
- def to_form_array
52
- FormArray.new(self.fields)
53
- end
54
-
55
- def to_array
56
- self.to_form_array
57
- end
58
43
  end
59
44
 
60
45
  class Field < Form
@@ -6,35 +6,48 @@ Accessing this either as a hash or an array (but => won't work)
6
6
 
7
7
  Some of the methods in here are kept for backward compat before the refactor
8
8
  and now everything in this array should be accessed with []= and []
9
+
10
+ Set :action and take a block. Page#submit_form should take this and do the
11
+ right thing.
9
12
  =end
10
13
 
11
14
  module WWMD
12
15
  class FormArray < Array
16
+ attr_accessor :action
17
+ attr_accessor :delimiter
18
+ attr_accessor :equals
19
+
20
+ def initialize(fields=nil,action=nil,&block)
21
+ set_fields(fields)
22
+ @delimiter = "&"
23
+ @equals = "="
24
+ @action = action
25
+ instance_eval(&block) if block_given?
26
+ end
13
27
 
14
- def initialize(fields=nil)
15
- if not fields.nil?
16
- # this first one is an array of field objects
17
- if fields.class == Array
18
- fields.each do |f|
19
- name = f['name']
20
- if self.name_exists(name)
21
- if f['type'] == "hidden"
22
- self.set name,f.get_value
23
- elsif f['type'] == "checkbox" and f.to_html.grep(/checked/) != ''
24
- self[name] = f.get_value
25
- end
26
- else
27
- self << [ f['name'],f.get_value ]
28
+ def set_fields(fields=nil)
29
+ return nil if fields.nil?
30
+ # this first one is an array of field objects
31
+ if fields.class == Array
32
+ fields.each do |f|
33
+ name = f['name']
34
+ if self.name_exists(name)
35
+ if f['type'] == "hidden"
36
+ self.set name,f.get_value
37
+ elsif f['type'] == "checkbox" and f.to_html.grep(/checked/) != ''
38
+ self[name] = f.get_value
28
39
  end
29
- end
30
- elsif fields.class == Hash
31
- fields.each_pair { |k,v| self[k] = v }
32
- elsif fields.class == String
33
- fields.split("&").each do |f|
34
- k,v = f.split("=",2)
35
- self[k] = v
40
+ else
41
+ self << [ f['name'],f.get_value ]
36
42
  end
37
43
  end
44
+ elsif fields.class == Hash
45
+ fields.each_pair { |k,v| self[k] = v }
46
+ elsif fields.class == String
47
+ fields.split(@delimiter).each do |f|
48
+ k,v = f.split(@equals,2)
49
+ self[k] = v
50
+ end
38
51
  end
39
52
  end
40
53
 
@@ -43,6 +56,7 @@ module WWMD
43
56
  def clone
44
57
  ret = self.class.new
45
58
  self.each { |r| ret << r.clone }
59
+ ret.action = self.action
46
60
  return ret
47
61
  end
48
62
 
@@ -64,14 +78,6 @@ module WWMD
64
78
  self << [key,value]
65
79
  end
66
80
 
67
- def clear_viewstate
68
- self.each { |k,v|
69
- self[k] = "" if k == "__VIEWSTATE"
70
- }
71
- end
72
-
73
- alias_method :extend!, :add #:nodoc (this is here for backward compat)
74
-
75
81
  # key = Fixnum set value at index key
76
82
  # key = String find key named string and set value
77
83
  def set_value!(key,value)
@@ -87,6 +93,8 @@ module WWMD
87
93
  return [key,value]
88
94
  end
89
95
 
96
+ # get a value using its index
97
+ # override Array#[]
90
98
  alias_method :old_get, :[]#:nodoc:
91
99
  def [](*args)
92
100
  if args.first.class == Fixnum
@@ -133,6 +141,10 @@ module WWMD
133
141
 
134
142
  alias_method :get, :get_value
135
143
 
144
+ def keys
145
+ self.map { |k,v| k }
146
+ end
147
+
136
148
  def setall!(value)
137
149
  self.each_index { |i| self.set_value!(i,value) }
138
150
  end
@@ -176,39 +188,38 @@ module WWMD
176
188
 
177
189
  alias_method :unescape_all, :unescape_all!#:nodoc:
178
190
 
179
- # convert form into a post parameters string
180
- def to_post
181
- ret = []
182
- self.each do |i|
183
- ret.push(i.join("="))
184
- end
185
- ret.join("&")
191
+ # remove form elements with null values
192
+ def remove_nulls!
193
+ self.delete_if { |x| x[1].to_s.empty? || x[1].nil? }
186
194
  end
187
195
 
188
- # convert form into a get parameters string
189
- #
190
- # pass me a base to get a full url to pass to Page.get
191
- def to_get(base="")
192
- ret = []
193
- self.each do |i|
194
- ret.push(i.join("="))
195
- end
196
- ret = ret.join("&")
197
- return base.clip + "?" + ret.to_s
196
+ alias_method :squeeze!, :remove_nulls!
197
+
198
+ # remove form elements with null keys (for housekeeping returns)
199
+ def remove_null_keys!
200
+ self.delete_if { |x,y| x.to_s.empty? || x.nil? }
198
201
  end
199
202
 
200
- # IRB: puts the form in human readable format
201
- # if you <tt>form.show(true)</tt> it will show unescaped values
202
- def show(unescape=false)
203
- if unescape
204
- self.each_index { |i| puts i.to_s + " :: " + self[i][0].to_s + " = " + self[i][1].to_s.unescape }
205
- else
206
- self.each_index { |i| puts i.to_s + " :: " + self[i][0].to_s + " = " + self[i][1].to_s }
207
- end
208
- return nil
203
+ alias_method :squeeze_keys!, :remove_null_keys!
204
+
205
+ ## viewstate
206
+
207
+ # clear viewstate variables
208
+ def clear_viewstate
209
+ self.each { |k,v|
210
+ self[k] = "" if k =~ /^__/
211
+ }
212
+ end
213
+
214
+ # remove viewstate variables
215
+ def rm_viewstate
216
+ # my least favorite ruby idiom
217
+ self.replace(self.map { |k,v| [k,v] if not k =~ /^__/ }.reject { |x| x.nil? })
209
218
  end
210
219
 
211
- # meh
220
+ alias_method :extend!, :add #:nodoc (this is here for backward compat)
221
+
222
+ # add viewstate stuff
212
223
  def add_viewstate#:nodoc:
213
224
  self.insert(0,[ "__VIEWSTATE","" ])
214
225
  self.insert(0,[ "__EVENTARGUMENT","" ])
@@ -217,31 +228,40 @@ module WWMD
217
228
  return nil
218
229
  end
219
230
 
220
- # alias_method, :add_state, :add_viewstate#:nodoc:
231
+ ## conversions
221
232
 
222
- # remove form elements with null values
223
- def remove_nulls!
224
- self.delete_if { |x| x[1].to_s.empty? || x[1].nil? }
233
+ # convert form into a post parameters string
234
+ def to_post
235
+ ret = []
236
+ self.each do |i|
237
+ ret << i.join(@equals)
238
+ end
239
+ ret.join(@delimiter)
225
240
  end
226
241
 
227
- alias_method :squeeze!, :remove_nulls!
228
-
229
- # remove form elements with null keys (for housekeeping returns)
230
- def remove_null_keys!
231
- self.delete_if { |x,y| x.to_s.empty? || x.nil? }
242
+ # convert form into a get parameters string
243
+ #
244
+ # pass me a base to get a full url to pass to Page.get
245
+ def to_get(base="")
246
+ ret = []
247
+ self.each do |i|
248
+ ret << i.join(@equals)
249
+ end
250
+ ret = ret.join(@delimiter)
251
+ return base.clip + "?" + ret.to_s
232
252
  end
233
253
 
234
- alias_method :squeeze_keys!, :remove_null_keys!
254
+ ## parsing convenience
235
255
 
236
256
  # dump a web page containing a csrf example of the current FormArray
237
- def to_csrf(action,unescval=false)
257
+ def to_csrf(action=nil,unescval=false)
258
+ action = self.action if not action
238
259
  ret = ""
239
260
  ret << "<html><body>\n"
240
261
  ret << "<form method='post' id='wwmdtest' name='wwmdtest' action='#{action}'>\n"
241
262
  self.each do |key,val|
242
263
  val = val.unescape.gsub(/'/) { %q[\'] } if unescval
243
- ret << "<input name='#{key.to_s.unescape}' type='hidden' value='#{val}' />\n"
244
- # ret << "<input name='#{key.to_s.unescape}' type='hidden' value='#{val.to_s.unescape.gsub(/'/,"\\'")}' />\n"
264
+ ret << "<input name='#{key.to_s.unescape}' type='hidden' value='#{val.to_s.unescape}' />\n"
245
265
  end
246
266
  ret << "</form>\n"
247
267
  ret << "<script>document.wwmdtest.submit()</script>\n"
@@ -249,17 +269,14 @@ module WWMD
249
269
  return ret
250
270
  end
251
271
 
252
- def keys
253
- self.map { |k,v| k }
254
- end
255
-
272
+ # add markers for burp intruder to form
256
273
  def burpify #:nodoc:
257
274
  ret = self.clone
258
275
  ret.each_index do |i|
259
276
  next if ret[i][0] =~ /^__/
260
277
  ret.set_value!(i,"#{ret.get_value(i)}" + "\302\247" + "\302\247")
261
278
  end
262
- system("echo '#{ret.to_post}' | pbcopy")
279
+ ret.to_post.pbcopy
263
280
  return ret
264
281
  end
265
282
 
@@ -269,5 +286,10 @@ module WWMD
269
286
  end
270
287
  alias_method :fp, :fingerprint #:nodoc:
271
288
 
289
+ def from_array(arr)
290
+ self.clear
291
+ arr.each { |k,v| self[k] = v }
292
+ end
293
+
272
294
  end
273
295
  end