desviar 0.0.9

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.
data/lib/auth.rb ADDED
@@ -0,0 +1,100 @@
1
+ require 'webrick/httpauth/htpasswd'
2
+
3
+ module Desviar::Auth
4
+
5
+ def self.htpasswd
6
+ @htpasswd ||= Htpasswd.new(git.path_to("htpasswd"))
7
+ end
8
+
9
+ def self.authentication
10
+ @authentication ||= Rack::Auth::Basic::Request.new request.env
11
+ end
12
+
13
+ def self.authenticated?
14
+ request.env["REMOTE_USER"] && request.env["desviar.authenticated"]
15
+ end
16
+
17
+ def self.authenticate(username, password)
18
+ checked = [ username, password ] == authentication.credentials
19
+ validated = authentication.provided? && authentication.basic?
20
+ granted = htpasswd.authenticated? username, password
21
+ if checked and validated and granted
22
+ request.env["desviar.authenticated"] = true
23
+ request.env["REMOTE_USER"] = authentication.username
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+ def self.unauthorized!(realm = Desviar::info)
30
+ headers "WWW-Authenticate" => %(Basic realm="#{realm}")
31
+ throw :halt, [ 401, "Authorization Required" ]
32
+ end
33
+
34
+ def self.bad_request!
35
+ throw :halt, [ 400, "Bad Request" ]
36
+ end
37
+
38
+ def self.authenticate!
39
+ return if authenticated?
40
+ unauthorized! unless authentication.provided?
41
+ bad_request! unless authentication.basic?
42
+ unauthorized! unless authenticate(*authentication.credentials)
43
+ request.env["REMOTE_USER"] = authentication.username
44
+ end
45
+
46
+ def self.access_granted?(username, password)
47
+ authenticated? || authenticate(username, password)
48
+ end
49
+
50
+ end
51
+
52
+ class Desviar::Auth2
53
+
54
+ def initialize(file)
55
+ @handler = WEBrick::HTTPAuth::Htpasswd.new(file)
56
+ yield self if block_given?
57
+ end
58
+
59
+ def find(username)
60
+ password = @handler.get_passwd(nil, username, false)
61
+ if block_given?
62
+ yield password ? [password, password[0,2]] : [nil, nil]
63
+ else
64
+ password
65
+ end
66
+ end
67
+
68
+ def authenticated?(username, password)
69
+ self.find username do |crypted, salt|
70
+ crypted && salt && crypted == password.crypt(salt)
71
+ end
72
+ end
73
+
74
+ def create(username, password)
75
+ @handler.set_passwd(nil, username, password)
76
+ end
77
+ alias update create
78
+
79
+ def destroy(username)
80
+ @handler.delete_passwd(nil, username)
81
+ end
82
+
83
+ def include?(username)
84
+ users.include? username
85
+ end
86
+
87
+ def size
88
+ users.size
89
+ end
90
+
91
+ def write!
92
+ @handler.flush
93
+ end
94
+
95
+ private
96
+
97
+ def users
98
+ @handler.each{|username, password| username }
99
+ end
100
+ end
@@ -0,0 +1,55 @@
1
+ require 'htauth'
2
+
3
+ module Sinatra
4
+ module Authorization
5
+ module HelperMethods
6
+
7
+ def passwd_file
8
+ File.expand_path '../config/.htpasswd', __FILE__
9
+ end
10
+
11
+ def auth
12
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
13
+ # @auth ||= Rack::Auth::Digest::MD5.new(request.env)
14
+ end
15
+
16
+ def unauthorized!(realm = "Please Authenticate")
17
+ header 'WWW-Authenticate' => %(Basic realm="#{realm}")
18
+ throw :halt, [ 401, 'Authorization Required' ]
19
+ end
20
+
21
+ def bad_request!
22
+ throw :halt, [ 400, 'Bad Request' ]
23
+ end
24
+
25
+ def authorized?
26
+ request.env['REMOTE_USER']
27
+ end
28
+
29
+ def authorize(username, password)
30
+ return false if !File.exists?(passwd_file)
31
+ pf = HTAuth::PasswdFile.new(passwd_file)
32
+ user = pf.fetch(username)
33
+ !user.nil? && user.authenticated?(password)
34
+ end
35
+
36
+ def require_administrative_privileges
37
+ return if authorized?
38
+ unauthorized! unless auth.provided?
39
+ bad_request! unless auth.basic?
40
+ unauthorized! unless authorize(*auth.credentials)
41
+ request.env['REMOTE_USER'] = auth.username
42
+ end
43
+
44
+ def admin?
45
+ authorized?
46
+ end
47
+
48
+
49
+ end
50
+ def self.registered(app)
51
+ app.helpers HelperMethods
52
+ end
53
+ end
54
+ register Authorization
55
+ end
data/lib/encrypt.rb ADDED
@@ -0,0 +1,417 @@
1
+ # Very slightly modified for Desviar by Rich Braun, Jul 2013
2
+ #
3
+ # Author:: Seth Falcon (<seth@opscode.com>)
4
+ # Copyright:: Copyright 2010-2011 Opscode, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'base64'
21
+ require 'openssl'
22
+ require 'yaml'
23
+ require 'yajl'
24
+ require 'open-uri'
25
+
26
+ # An EncryptedDataBagItem represents a read-only data bag item where
27
+ # all values, except for the value associated with the id key, have
28
+ # been encrypted.
29
+ #
30
+ # EncrypedDataBagItem can be used in recipes to decrypt data bag item
31
+ # members.
32
+ #
33
+ # Data bag item values are assumed to have been encrypted using the
34
+ # default symmetric encryption provided by Encryptor.encrypt where
35
+ # values are converted to YAML prior to encryption.
36
+ #
37
+ # If the shared secret is not specified at initialization or load,
38
+ # then the contents of the file referred to in
39
+ # Chef::Config[:encrypted_data_bag_secret] will be used as the
40
+ # secret. The default path is /etc/chef/encrypted_data_bag_secret
41
+ #
42
+ # EncryptedDataBagItem is intended to provide a means to avoid storing
43
+ # data bag items in the clear on the Chef server. This provides some
44
+ # protection against a breach of the Chef server or of Chef server
45
+ # backup data. Because the secret must be stored in the clear on any
46
+ # node needing access to an EncryptedDataBagItem, this approach
47
+ # provides no protection of data bag items from actors with access to
48
+ # such nodes in the infrastructure.
49
+ #
50
+ module Desviar
51
+ class EncryptedItem
52
+ ALGORITHM = 'aes-256-cbc'
53
+
54
+ class UnacceptableEncryptedDataBagItemFormat < StandardError
55
+ end
56
+
57
+ class UnsupportedEncryptedDataBagItemFormat < StandardError
58
+ end
59
+
60
+ class DecryptionFailure < StandardError
61
+ end
62
+
63
+ class UnsupportedCipher < StandardError
64
+ end
65
+
66
+ # Implementation class for converting plaintext data bag item values to an
67
+ # encrypted value, including any necessary wrappers and metadata.
68
+ module Encryptor
69
+
70
+ # "factory" method that creates an encryptor object with the proper class
71
+ # for the desired encrypted data bag format version.
72
+ #
73
+ # +Chef::Config[:data_bag_encrypt_version]+ determines which version is used.
74
+ def self.new(value, secret, iv=nil, version=2)
75
+ format_version = version
76
+ case format_version
77
+ when 1
78
+ Version1Encryptor.new(value, secret, iv)
79
+ when 2
80
+ Version2Encryptor.new(value, secret, iv)
81
+ else
82
+ raise UnsupportedEncryptedDataBagItemFormat,
83
+ "Invalid encrypted data bag format version `#{format_version}'. Supported versions are '1', '2'"
84
+ end
85
+ end
86
+
87
+ class Version1Encryptor
88
+ attr_reader :key
89
+ attr_reader :plaintext_data
90
+
91
+ # Create a new Encryptor for +data+, which will be encrypted with the given
92
+ # +key+.
93
+ #
94
+ # === Arguments:
95
+ # * data: An object of any type that can be serialized to json
96
+ # * key: A String representing the desired passphrase
97
+ # * iv: The optional +iv+ parameter is intended for testing use only. When
98
+ # *not* supplied, Encryptor will use OpenSSL to generate a secure random
99
+ # IV, which is what you want.
100
+ def initialize(plaintext_data, key, iv=nil)
101
+ @plaintext_data = plaintext_data
102
+ @key = key
103
+ @iv = iv && Base64.decode64(iv)
104
+ end
105
+
106
+ # Returns a wrapped and encrypted version of +plaintext_data+ suitable for
107
+ # using as the value in an encrypted data bag item.
108
+ def for_encrypted_item
109
+ {
110
+ "encrypted_data" => encrypted_data,
111
+ "iv" => Base64.encode64(iv),
112
+ "version" => 1,
113
+ "cipher" => ALGORITHM
114
+ }
115
+ end
116
+
117
+ # Generates or returns the IV.
118
+ def iv
119
+ # Generated IV comes from OpenSSL::Cipher::Cipher#random_iv
120
+ # This gets generated when +openssl_encryptor+ gets created.
121
+ openssl_encryptor if @iv.nil?
122
+ @iv
123
+ end
124
+
125
+ # Generates (and memoizes) an OpenSSL::Cipher::Cipher object and configures
126
+ # it for the specified iv and encryption key.
127
+ def openssl_encryptor
128
+ @openssl_encryptor ||= begin
129
+ encryptor = OpenSSL::Cipher::Cipher.new(ALGORITHM)
130
+ encryptor.encrypt
131
+ @iv ||= encryptor.random_iv
132
+ encryptor.iv = @iv
133
+ encryptor.key = Digest::SHA256.digest(key)
134
+ encryptor
135
+ end
136
+ end
137
+
138
+ # Encrypts and Base64 encodes +serialized_data+
139
+ def encrypted_data
140
+ @encrypted_data ||= begin
141
+ enc_data = openssl_encryptor.update(serialized_data)
142
+ enc_data << openssl_encryptor.final
143
+ Base64.encode64(enc_data)
144
+ end
145
+ end
146
+
147
+ # Wraps the data in a single key Hash (JSON Object) and converts to JSON.
148
+ # The wrapper is required because we accept values (such as Integers or
149
+ # Strings) that do not produce valid JSON when serialized without the
150
+ # wrapper.
151
+ def serialized_data
152
+ Yajl::Encoder.encode(:json_wrapper => plaintext_data)
153
+ end
154
+ end
155
+
156
+ class Version2Encryptor < Version1Encryptor
157
+
158
+ # Returns a wrapped and encrypted version of +plaintext_data+ suitable for
159
+ # using as the value in an encrypted data bag item.
160
+ def for_encrypted_item
161
+ {
162
+ "encrypted_data" => encrypted_data,
163
+ "hmac" => hmac,
164
+ "iv" => Base64.encode64(iv),
165
+ "version" => 2,
166
+ "cipher" => ALGORITHM
167
+ }
168
+ end
169
+
170
+ # Generates an HMAC-SHA2-256 of the encrypted data (encrypt-then-mac)
171
+ def hmac
172
+ @hmac ||= begin
173
+ digest = OpenSSL::Digest::Digest.new("sha256")
174
+ raw_hmac = OpenSSL::HMAC.digest(digest, key, encrypted_data)
175
+ Base64.encode64(raw_hmac)
176
+ end
177
+ end
178
+
179
+ end
180
+ end
181
+
182
+ #=== Decryptor
183
+ # For backwards compatibility, Chef implements decryption/deserialization for
184
+ # older encrypted data bag item formats in addition to the current version.
185
+ # Each decryption/deserialization strategy is implemented as a class in this
186
+ # namespace. For convenience the factory method +Decryptor.for()+ can be used
187
+ # to create an instance of the appropriate strategy for the given encrypted
188
+ # data bag value.
189
+ module Decryptor
190
+
191
+ # Detects the encrypted data bag item format version and instantiates a
192
+ # decryptor object for that version. Call #for_decrypted_item on the
193
+ # resulting object to decrypt and deserialize it.
194
+ def self.for(encrypted_value, key)
195
+ format_version = format_version_of(encrypted_value)
196
+ assert_format_version_acceptable!(format_version)
197
+ case format_version
198
+ when 2
199
+ Version2Decryptor.new(encrypted_value, key)
200
+ when 1
201
+ Version1Decryptor.new(encrypted_value, key)
202
+ when 0
203
+ Version0Decryptor.new(encrypted_value, key)
204
+ else
205
+ raise UnsupportedEncryptedDataBagItemFormat,
206
+ "This version of chef does not support encrypted data bag item format version '#{format_version}'"
207
+ end
208
+ end
209
+
210
+ def self.format_version_of(encrypted_value)
211
+ if encrypted_value.respond_to?(:key?)
212
+ encrypted_value["version"]
213
+ else
214
+ 0
215
+ end
216
+ end
217
+
218
+ def self.assert_format_version_acceptable!(format_version)
219
+ unless format_version.kind_of?(Integer) and format_version >= 1
220
+ raise UnacceptableEncryptedDataBagItemFormat,
221
+ "The encrypted data bag item has format version `#{format_version}', " +
222
+ "but the config setting 'data_bag_decrypt_minimum_version' requires version 1"
223
+ end
224
+ end
225
+
226
+ class Version1Decryptor
227
+
228
+ attr_reader :encrypted_data
229
+ attr_reader :key
230
+
231
+ def initialize(encrypted_data, key)
232
+ @encrypted_data = encrypted_data
233
+ @key = key
234
+ end
235
+
236
+ def for_decrypted_item
237
+ Yajl::Parser.parse(decrypted_data)["json_wrapper"]
238
+ rescue Yajl::ParseError
239
+ # convert to a DecryptionFailure error because the most likely scenario
240
+ # here is that the decryption step was unsuccessful but returned bad
241
+ # data rather than raising an error.
242
+ raise DecryptionFailure, "Error decrypting data bag value. Most likely the provided key is incorrect"
243
+ end
244
+
245
+ def encrypted_bytes
246
+ Base64.decode64(@encrypted_data["encrypted_data"])
247
+ end
248
+
249
+ def iv
250
+ Base64.decode64(@encrypted_data["iv"])
251
+ end
252
+
253
+ def decrypted_data
254
+ @decrypted_data ||= begin
255
+ plaintext = openssl_decryptor.update(encrypted_bytes)
256
+ plaintext << openssl_decryptor.final
257
+ rescue OpenSSL::Cipher::CipherError => e
258
+ raise DecryptionFailure, "Error decrypting data bag value: '#{e.message}'. Most likely the provided key is incorrect"
259
+ end
260
+ end
261
+
262
+ def openssl_decryptor
263
+ @openssl_decryptor ||= begin
264
+ assert_valid_cipher!
265
+ d = OpenSSL::Cipher::Cipher.new(ALGORITHM)
266
+ d.decrypt
267
+ d.key = Digest::SHA256.digest(key)
268
+ d.iv = iv
269
+ d
270
+ end
271
+ end
272
+
273
+ def assert_valid_cipher!
274
+ # In the future, chef may support configurable ciphers. For now, only
275
+ # aes-256-cbc is supported.
276
+ requested_cipher = @encrypted_data["cipher"]
277
+ unless requested_cipher == ALGORITHM
278
+ raise UnsupportedCipher,
279
+ "Cipher '#{requested_cipher}' is not supported by this version of Chef. Available ciphers: ['#{ALGORITHM}']"
280
+ end
281
+ end
282
+
283
+ end
284
+
285
+ class Version2Decryptor < Version1Decryptor
286
+
287
+ def decrypted_data
288
+ validate_hmac! unless @decrypted_data
289
+ super
290
+ end
291
+
292
+ def validate_hmac!
293
+ digest = OpenSSL::Digest::Digest.new("sha256")
294
+ raw_hmac = OpenSSL::HMAC.digest(digest, key, @encrypted_data["encrypted_data"])
295
+
296
+ if candidate_hmac_matches?(raw_hmac)
297
+ true
298
+ else
299
+ raise DecryptionFailure, "Error decrypting data bag value: invalid hmac. Most likely the provided key is incorrect"
300
+ end
301
+ end
302
+
303
+ private
304
+
305
+ def candidate_hmac_matches?(expected_hmac)
306
+ return false unless @encrypted_data["hmac"]
307
+ expected_bytes = expected_hmac.bytes.to_a
308
+ candidate_hmac_bytes = Base64.decode64(@encrypted_data["hmac"]).bytes.to_a
309
+ valid = expected_bytes.size ^ candidate_hmac_bytes.size
310
+ expected_bytes.zip(candidate_hmac_bytes) { |x, y| valid |= x ^ y.to_i }
311
+ valid == 0
312
+ end
313
+ end
314
+
315
+ class Version0Decryptor
316
+
317
+ attr_reader :encrypted_data
318
+ attr_reader :key
319
+
320
+ def initialize(encrypted_data, key)
321
+ @encrypted_data = encrypted_data
322
+ @key = key
323
+ end
324
+
325
+ def for_decrypted_item
326
+ YAML.load(decrypted_data)
327
+ end
328
+
329
+ def decrypted_data
330
+ @decrypted_data ||= begin
331
+ plaintext = openssl_decryptor.update(encrypted_bytes)
332
+ plaintext << openssl_decryptor.final
333
+ rescue OpenSSL::Cipher::CipherError => e
334
+ raise DecryptionFailure, "Error decrypting data bag value: '#{e.message}'. Most likely the provided key is incorrect"
335
+ end
336
+ end
337
+
338
+ def encrypted_bytes
339
+ Base64.decode64(@encrypted_data)
340
+ end
341
+
342
+ def openssl_decryptor
343
+ @openssl_decryptor ||= begin
344
+ d = OpenSSL::Cipher::Cipher.new(ALGORITHM)
345
+ d.decrypt
346
+ d.pkcs5_keyivgen(key)
347
+ d
348
+ end
349
+ end
350
+ end
351
+ end
352
+
353
+ def initialize(enc_hash, secret)
354
+ @enc_hash = enc_hash
355
+ @secret = secret
356
+ end
357
+
358
+ def [](key)
359
+ value = @enc_hash[key]
360
+ if key == "id" || value.nil?
361
+ value
362
+ else
363
+ Decryptor.for(value, @secret).for_decrypted_item
364
+ end
365
+ end
366
+
367
+ def []=(key, value)
368
+ raise ArgumentError, "assignment not supported for #{self.class}"
369
+ end
370
+
371
+ def to_hash
372
+ @enc_hash.keys.inject({}) { |hash, key| hash[key] = self[key]; hash }
373
+ end
374
+
375
+ def self.encrypt_data_bag_item(plain_hash, secret)
376
+ plain_hash.inject({}) do |h, (key, val)|
377
+ h[key] = if key != "id"
378
+ Encryptor.new(val, secret).for_encrypted_item
379
+ else
380
+ val
381
+ end
382
+ h
383
+ end
384
+ end
385
+
386
+ def self.load(data_bag, name, secret = nil)
387
+ raw_hash = Chef::DataBagItem.load(data_bag, name)
388
+ secret = secret || self.load_secret
389
+ self.new(raw_hash, secret)
390
+ end
391
+
392
+ def self.load_secret(path=nil)
393
+ path ||= Chef::Config[:encrypted_data_bag_secret]
394
+ secret = case path
395
+ when /^\w+:\/\//
396
+ # We have a remote key
397
+ begin
398
+ Kernel.open(path).read.strip
399
+ rescue Errno::ECONNREFUSED
400
+ raise ArgumentError, "Remote key not available from '#{path}'"
401
+ rescue OpenURI::HTTPError
402
+ raise ArgumentError, "Remote key not found at '#{path}'"
403
+ end
404
+ else
405
+ if !File.exist?(path)
406
+ raise Errno::ENOENT, "file not found '#{path}'"
407
+ end
408
+ IO.read(path).strip
409
+ end
410
+ if secret.size < 1
411
+ raise ArgumentError, "invalid zero length secret in '#{path}'"
412
+ end
413
+ secret
414
+ end
415
+
416
+ end
417
+ end
data/lib/model.rb ADDED
@@ -0,0 +1,44 @@
1
+ # Data model for Desviar main object
2
+ #
3
+ # Copyright 2013 Richard Braun
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ module Desviar
12
+ module Model
13
+ class Main
14
+ include DataMapper::Resource
15
+
16
+ property :id, Serial # primary serial key
17
+ property :redir_uri, String, :required => true, :length => 255
18
+ property :temp_uri, String, :length => 64
19
+ property :expiration, Integer, :required => true
20
+ property :captcha, Boolean
21
+ property :captcha_prompt, Text
22
+ property :captcha_button, String, :length => 20
23
+ property :captcha_validated, Boolean
24
+ property :content, Text, :length => $config[:contentmax]
25
+ property :notes, Text
26
+ property :cipher_iv, Binary, :length => 16
27
+ property :hmac, String, :length => 46
28
+ property :owner, String, :length => 16
29
+ property :created_at, DateTime
30
+ property :updated_at, DateTime
31
+ property :expires_at, DateTime
32
+
33
+ Syntaxi.line_number_method = 'floating'
34
+ Syntaxi.wrap_at_column = 80
35
+
36
+ def formatted_notes
37
+ sub = Time.now.strftime('[code-%d]')
38
+ html = Syntaxi.new("[code lang='ruby']#{self.notes.gsub('[/code]',sub)}
39
+ [/code]").process
40
+ "<div class=\"syntax syntax_ruby\">#{html.gsub(sub, '[/code]')}</div>"
41
+ end
42
+ end
43
+ end
44
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,17 @@
1
+ module Desviar
2
+ VERSION = "0.0.9"
3
+ RELEASE = "2013-07-28"
4
+ TIMESTAMP = "2013-07-28 22:49:26 -07:00"
5
+
6
+ def self.info
7
+ "#{name} v#{VERSION} (#{RELEASE})"
8
+ end
9
+
10
+ def self.to_h
11
+ { :name => name,
12
+ :version => VERSION,
13
+ :release => RELEASE,
14
+ :timestamp => TIMESTAMP,
15
+ :info => info }
16
+ end
17
+ end
Binary file
data/views/captcha.erb ADDED
@@ -0,0 +1,8 @@
1
+ <div class="desviar">
2
+ <%= @desviar.captcha_prompt %>
3
+ <hr>
4
+ <form action="/desviar/<%= @desviar.temp_uri %>" method="POST">
5
+ <%= recaptcha_tag(:challenge) %>
6
+ <input type="submit" value="<%= @button %>"/>
7
+ </form>
8
+ </div>
data/views/config.erb ADDED
@@ -0,0 +1,41 @@
1
+ <div class="desviar">
2
+ <h2>Run-time configuration</h2>
3
+ <hr>
4
+ <form action="/config" method="POST">
5
+ <p>
6
+ <%=
7
+ values = $config.select { |key, val| !$config[:hidden].include?(key.to_s) &&
8
+ key.to_s.index('msg_') != 0 }
9
+ html = ""
10
+ values.each do |key, val|
11
+ html << "<label for='#{key}'>#{key}: </label>"
12
+ if $config.has_key?("msg_#{key.to_s}".to_sym)
13
+ helptext = "title='#{$config["msg_#{key.to_s}".to_sym]}' "
14
+ else
15
+ helptext = ""
16
+ end
17
+ if $optvals.include?(key)
18
+ html << "<select name='config[#{key}]' #{helptext}>"
19
+ $optvals[key].each do |opt, text|
20
+ sel = " selected='selected'" if val.to_s == opt.to_s
21
+ html << "<option value='#{opt.to_s}'#{sel}>#{opt.to_s}</option>"
22
+ end
23
+ html << "</select>"
24
+ else
25
+ html << "<input name='config[#{key}]' #{helptext}"
26
+ if $config[:hashed].include?(key.to_s)
27
+ html << " type='password'"
28
+ else
29
+ html << " value='#{val}' type='text' "
30
+ end
31
+ html << "/>"
32
+ end
33
+ html << "<br>"
34
+ end
35
+ html
36
+ %>
37
+ <input type="submit" value="Save"/>
38
+ </form>
39
+ <hr>
40
+ <i><font size=-1>Note: changes will persist only until next restart</font><i>
41
+ </div>
data/views/content.erb ADDED
@@ -0,0 +1 @@
1
+ <%= @content %>