cf-uaa-lib 1.3.0
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/.gitignore +8 -0
- data/Gemfile +16 -0
- data/README.md +43 -0
- data/Rakefile +50 -0
- data/cf-uaa-lib.gemspec +45 -0
- data/lib/uaa.rb +18 -0
- data/lib/uaa/http.rb +147 -0
- data/lib/uaa/misc.rb +67 -0
- data/lib/uaa/scim.rb +206 -0
- data/lib/uaa/token_coder.rb +124 -0
- data/lib/uaa/token_issuer.rb +173 -0
- data/lib/uaa/util.rb +159 -0
- data/lib/uaa/version.rb +18 -0
- data/spec/http_spec.rb +37 -0
- data/spec/misc_spec.rb +42 -0
- data/spec/scim_spec.rb +117 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/token_coder_spec.rb +128 -0
- data/spec/token_issuer_spec.rb +190 -0
- metadata +210 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
#--
|
2
|
+
# Cloud Foundry 2012.02.03 Beta
|
3
|
+
# Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
|
4
|
+
#
|
5
|
+
# This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
6
|
+
# You may not use this product except in compliance with the License.
|
7
|
+
#
|
8
|
+
# This product includes a number of subcomponents with
|
9
|
+
# separate copyright notices and license terms. Your use of these
|
10
|
+
# subcomponents is subject to the terms and conditions of the
|
11
|
+
# subcomponent's license, as noted in the LICENSE file.
|
12
|
+
#++
|
13
|
+
|
14
|
+
require "openssl"
|
15
|
+
require "uaa/util"
|
16
|
+
|
17
|
+
module CF::UAA
|
18
|
+
|
19
|
+
# This class is for OAuth Resource Servers.
|
20
|
+
# Resource Servers get tokens and need to validate and decode them,
|
21
|
+
# but they do not obtain them from the Authorization Server. This
|
22
|
+
# class it for Resource Servers which accept Bearer JWT tokens. An
|
23
|
+
# instance of this class can be used to decode and verify the contents
|
24
|
+
# of a bearer token. The Authorization Server will have signed the
|
25
|
+
# token and shared a secret key so that the Resource Server can verify
|
26
|
+
# the signature. The Authorization Server may also have given the
|
27
|
+
# Resource Server an id, in which case it must verify a matching value
|
28
|
+
# is in the access token.
|
29
|
+
class TokenCoder
|
30
|
+
|
31
|
+
def self.init_digest(algo) # :nodoc:
|
32
|
+
OpenSSL::Digest::Digest.new(algo.sub('HS', 'sha').sub('RS', 'sha'))
|
33
|
+
end
|
34
|
+
|
35
|
+
# takes a token_body (the middle section of the jwt) and returns a signed token
|
36
|
+
def self.encode(token_body, skey, pkey = nil, algo = 'HS256')
|
37
|
+
segments = [Util.json_encode64("typ" => "JWT", "alg" => algo)]
|
38
|
+
segments << Util.json_encode64(token_body)
|
39
|
+
if ["HS256", "HS384", "HS512"].include?(algo)
|
40
|
+
sig = OpenSSL::HMAC.digest(init_digest(algo), skey, segments.join('.'))
|
41
|
+
elsif ["RS256", "RS384", "RS512"].include?(algo)
|
42
|
+
sig = pkey.sign(init_digest(algo), segments.join('.'))
|
43
|
+
elsif algo == "none"
|
44
|
+
sig = ""
|
45
|
+
else
|
46
|
+
raise ArgumentError, "unsupported signing method"
|
47
|
+
end
|
48
|
+
segments << Util.encode64(sig)
|
49
|
+
segments.join('.')
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.decode(token, skey = nil, pkey = nil, verify = true)
|
53
|
+
pkey = OpenSSL::PKey::RSA.new(pkey) unless pkey.nil? || pkey.is_a?(OpenSSL::PKey::PKey)
|
54
|
+
segments = token.split('.')
|
55
|
+
raise DecodeError, "Not enough or too many segments" unless [2,3].include? segments.length
|
56
|
+
header_segment, payload_segment, crypto_segment = segments
|
57
|
+
signing_input = [header_segment, payload_segment].join('.')
|
58
|
+
header = Util.json_decode64(header_segment)
|
59
|
+
payload = Util.json_decode64(payload_segment)
|
60
|
+
return payload if !verify || (algo = header["alg"]) == "none"
|
61
|
+
signature = Util.decode64(crypto_segment)
|
62
|
+
if ["HS256", "HS384", "HS512"].include?(algo)
|
63
|
+
raise DecodeError, "Signature verification failed" unless
|
64
|
+
signature == OpenSSL::HMAC.digest(init_digest(algo), skey, signing_input)
|
65
|
+
elsif ["RS256", "RS384", "RS512"].include?(algo)
|
66
|
+
raise DecodeError, "Signature verification failed" unless
|
67
|
+
pkey.verify(init_digest(algo), signature, signing_input)
|
68
|
+
else
|
69
|
+
raise DecodeError, "Algorithm not supported"
|
70
|
+
end
|
71
|
+
payload
|
72
|
+
end
|
73
|
+
|
74
|
+
# Create a new token en/decoder for a service that is associated with
|
75
|
+
# the the audience_ids, the symmetrical token validation key, and the
|
76
|
+
# public and/or private keys. Parameters:
|
77
|
+
# * audience_ids - an array or space separated strings and should
|
78
|
+
# indicate values which indicate the token is intended for this service
|
79
|
+
# instance. It will be compared with tokens as they are decoded to
|
80
|
+
# ensure that the token was intended for this audience.
|
81
|
+
# * skey - is used to sign and validate tokens using symetrical key
|
82
|
+
# algoruthms
|
83
|
+
# * pkey - pkey may be a string or File which includes public and
|
84
|
+
# optionally private key data in PEM or DER formats. The private key
|
85
|
+
# is used to sign tokens and the public key is used to validate tokens.
|
86
|
+
def initialize(audience_ids, skey, pkey = nil)
|
87
|
+
@audience_ids, @skey, @pkey = Util.arglist(audience_ids), skey, pkey
|
88
|
+
@pkey = OpenSSL::PKey::RSA.new(pkey) unless pkey.nil? || pkey.is_a?(OpenSSL::PKey::PKey)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Encode a JWT token. Takes a hash of values to use as the token body.
|
92
|
+
# Returns a signed token in JWT format (header, body, signature).
|
93
|
+
# Algorithm may be HS256, HS384, HS512, RS256, RS384, RS512, or none --
|
94
|
+
# assuming the TokenCoder instance is configured with the appropriate
|
95
|
+
# key -- i.e. pkey must include a private key for the RS algorithms.
|
96
|
+
def encode(token_body = {}, algorithm = 'HS256')
|
97
|
+
token_body['aud'] = @audience_ids unless token_body['aud']
|
98
|
+
token_body['exp'] = Time.now.to_i + 7 * 24 * 60 * 60 unless token_body['exp']
|
99
|
+
self.class.encode(token_body, @skey, @pkey, algorithm)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns hash of values decoded from the token contents. If the
|
103
|
+
# token contains resource ids and they do not contain the id of the
|
104
|
+
# caller there will be an AuthError. If the token has expired there
|
105
|
+
# will also be an AuthError.
|
106
|
+
def decode(auth_header)
|
107
|
+
unless auth_header && (tkn = auth_header.split).length == 2 && tkn[0] =~ /^bearer$/i
|
108
|
+
raise DecodeError, "invalid authentication header: #{auth_header}"
|
109
|
+
end
|
110
|
+
reply = self.class.decode(tkn[1], @skey, @pkey)
|
111
|
+
auds = Util.arglist(reply["aud"])
|
112
|
+
if @audience_ids && (!auds || (auds & @audience_ids).empty?)
|
113
|
+
raise AuthError, "invalid audience: #{auds.join(' ')}"
|
114
|
+
end
|
115
|
+
exp = reply["exp"]
|
116
|
+
unless exp.is_a?(Integer) && exp > Time.now.to_i
|
117
|
+
raise AuthError, "token expired"
|
118
|
+
end
|
119
|
+
reply
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
#--
|
2
|
+
# Cloud Foundry 2012.02.03 Beta
|
3
|
+
# Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
|
4
|
+
#
|
5
|
+
# This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
6
|
+
# You may not use this product except in compliance with the License.
|
7
|
+
#
|
8
|
+
# This product includes a number of subcomponents with
|
9
|
+
# separate copyright notices and license terms. Your use of these
|
10
|
+
# subcomponents is subject to the terms and conditions of the
|
11
|
+
# subcomponent's license, as noted in the LICENSE file.
|
12
|
+
#++
|
13
|
+
|
14
|
+
# Web or Native Clients (in the OAuth2 sense) would use this class to get tokens
|
15
|
+
# that they can use to get access to resources
|
16
|
+
|
17
|
+
# Client Apps that want to get access on behalf of their users to
|
18
|
+
# resource servers need to get tokens via authcode and implicit flows,
|
19
|
+
# request scopes, etc., but they don't need to process tokens. This
|
20
|
+
# class is for these use cases.
|
21
|
+
|
22
|
+
require 'securerandom'
|
23
|
+
require 'uaa/http'
|
24
|
+
|
25
|
+
module CF::UAA
|
26
|
+
|
27
|
+
# The Token class holds access and refresh tokens as well as token meta-data
|
28
|
+
# such as token type and expiration time. The info hash MUST include
|
29
|
+
# access_token, token_type and scope (if granted scope differs from requested
|
30
|
+
# scope). It should include expires_in. It may include refresh_token, scope,
|
31
|
+
# and other values from the auth server.
|
32
|
+
class Token
|
33
|
+
attr_reader :info
|
34
|
+
def initialize(info); @info = info end
|
35
|
+
def auth_header; "#{info['token_type']} #{info['access_token']}" end
|
36
|
+
end
|
37
|
+
|
38
|
+
class TokenIssuer
|
39
|
+
|
40
|
+
include Http
|
41
|
+
|
42
|
+
def initialize(target, client_id, client_secret = nil, token_target = nil)
|
43
|
+
@target, @client_id, @client_secret = target, client_id, client_secret
|
44
|
+
@token_target = token_target || target
|
45
|
+
end
|
46
|
+
|
47
|
+
# login prompts for use by app to collect credentials for implicit grant
|
48
|
+
def prompts
|
49
|
+
reply = json_get @target, '/login'
|
50
|
+
return reply['prompts'] if reply && reply['prompts']
|
51
|
+
raise BadResponse, "No prompts in response from target #{@target}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# gets an access token in a single call to the UAA with the client
|
55
|
+
# credentials used for authentication. The credentials arg should
|
56
|
+
# be an object such as a hash that can be converted to a json
|
57
|
+
# representation of the credential name/value pairs
|
58
|
+
# as specified by the information retrieved by #prompts
|
59
|
+
def implicit_grant_with_creds(credentials, scope = nil)
|
60
|
+
# this manufactured redirect_uri is a convention here, not part of OAuth2
|
61
|
+
redir_uri = "https://uaa.cloudfoundry.com/redirect/#{@client_id}"
|
62
|
+
uri = authorize_path_args("token", redir_uri, scope, state = SecureRandom.uuid)
|
63
|
+
|
64
|
+
# the accept header is only here so the uaa will issue error replies in json to aid debugging
|
65
|
+
headers = {'content-type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json' }
|
66
|
+
body = URI.encode_www_form(credentials.merge('source' => 'credentials'))
|
67
|
+
status, body, headers = request(@target, :post, uri, body, headers)
|
68
|
+
raise BadResponse, "status #{status}" unless status == 302
|
69
|
+
req_uri, reply_uri = URI.parse(redir_uri), URI.parse(headers['location'])
|
70
|
+
fragment, reply_uri.fragment = reply_uri.fragment, nil
|
71
|
+
raise BadResponse, "bad location header" unless req_uri == reply_uri
|
72
|
+
parse_implicit_params(fragment, state)
|
73
|
+
rescue URI::Error => e
|
74
|
+
raise BadResponse, "bad location header in reply: #{e.message}"
|
75
|
+
end
|
76
|
+
|
77
|
+
# constructs a uri that the client is to return to the browser to direct
|
78
|
+
# the user to the authorization server to get an authcode. The redirect_uri
|
79
|
+
# is embedded in the returned uri so the authorization server can redirect
|
80
|
+
# the user back to the client app.
|
81
|
+
def implicit_uri(redirect_uri, scope = nil)
|
82
|
+
@target + authorize_path_args("token", redirect_uri, scope)
|
83
|
+
end
|
84
|
+
|
85
|
+
def implicit_grant(implicit_uri, callback_query)
|
86
|
+
in_params = Util.decode_form_to_hash(URI.parse(implicit_uri).query)
|
87
|
+
unless in_params['state'] && in_params['redirect_uri']
|
88
|
+
raise ArgumentError, "redirect must happen before implicit grant"
|
89
|
+
end
|
90
|
+
parse_implicit_params callback_query, in_params['state']
|
91
|
+
end
|
92
|
+
|
93
|
+
def autologin_uri(redirect_uri, credentials, scope = nil)
|
94
|
+
headers = {'content_type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json',
|
95
|
+
'authorization' => Http.basic_auth(@client_id, @client_secret) }
|
96
|
+
body = URI.encode_www_form(credentials)
|
97
|
+
reply = json_parse_reply(*request(@target, :post, "/autologin", body, headers))
|
98
|
+
raise BadResponse, "no autologin code in reply" unless reply['code']
|
99
|
+
@target + authorize_path_args('code', redirect_uri, scope, SecureRandom.uuid, code: reply[:code])
|
100
|
+
end
|
101
|
+
|
102
|
+
# constructs a uri that the client is to return to the browser to direct
|
103
|
+
# the user to the authorization server to get an authcode. The redirect_uri
|
104
|
+
# is embedded in the returned uri so the authorization server can redirect
|
105
|
+
# the user back to the client app.
|
106
|
+
def authcode_uri(redirect_uri, scope = nil)
|
107
|
+
@target + authorize_path_args('code', redirect_uri, scope)
|
108
|
+
end
|
109
|
+
|
110
|
+
def authcode_grant(authcode_uri, callback_query)
|
111
|
+
ac_params = Util.decode_form_to_hash(URI.parse(authcode_uri).query)
|
112
|
+
unless ac_params['state'] && ac_params['redirect_uri']
|
113
|
+
raise ArgumentError, "authcode redirect must happen before authcode grant"
|
114
|
+
end
|
115
|
+
begin
|
116
|
+
params = Util.decode_form_to_hash(callback_query)
|
117
|
+
authcode = params['code']
|
118
|
+
raise BadResponse unless params['state'] == ac_params['state'] && authcode
|
119
|
+
rescue URI::InvalidURIError, ArgumentError, BadResponse
|
120
|
+
raise BadResponse, "received invalid response from target #{@target}"
|
121
|
+
end
|
122
|
+
request_token('grant_type' => 'authorization_code', 'code' => authcode, 'redirect_uri' => ac_params['redirect_uri'])
|
123
|
+
end
|
124
|
+
|
125
|
+
def owner_password_grant(username, password, scope = nil)
|
126
|
+
request_token('grant_type' => 'password', 'username' => username, 'password' => password, 'scope' => scope)
|
127
|
+
end
|
128
|
+
|
129
|
+
def client_credentials_grant(scope = nil)
|
130
|
+
request_token('grant_type' => 'client_credentials', 'scope' => scope)
|
131
|
+
end
|
132
|
+
|
133
|
+
def refresh_token_grant(refresh_token, scope = nil)
|
134
|
+
request_token('grant_type' => 'refresh_token', 'refresh_token' => refresh_token, 'scope' => scope)
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def parse_implicit_params(encoded_params, state)
|
140
|
+
params = Util.decode_form_to_hash(encoded_params)
|
141
|
+
raise BadResponse, "mismatched state" unless state && params.delete('state') == state
|
142
|
+
raise TargetError.new(params), "error response from #{@target}" if params['error']
|
143
|
+
raise BadResponse, "no type and token" unless params['token_type'] && params['access_token']
|
144
|
+
exp = params['expires_in'].to_i
|
145
|
+
params['expires_in'] = exp if exp.to_s == params['expires_in']
|
146
|
+
Token.new params
|
147
|
+
rescue URI::InvalidURIError, ArgumentError
|
148
|
+
raise BadResponse, "received invalid response from target #{@target}"
|
149
|
+
end
|
150
|
+
|
151
|
+
# returns a CF::UAA::Token object which includes the access token and metadata.
|
152
|
+
def request_token(params)
|
153
|
+
if scope = Util.arglist(params.delete('scope'))
|
154
|
+
params['scope'] = Util.strlist(scope)
|
155
|
+
end
|
156
|
+
headers = {'content-type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json',
|
157
|
+
'authorization' => Http.basic_auth(@client_id, @client_secret) }
|
158
|
+
body = URI.encode_www_form(params)
|
159
|
+
reply = json_parse_reply(*request(@token_target, :post, '/oauth/token', body, headers))
|
160
|
+
raise BadResponse unless reply['token_type'] && reply['access_token']
|
161
|
+
Token.new reply
|
162
|
+
end
|
163
|
+
|
164
|
+
def authorize_path_args(response_type, redirect_uri, scope, state = SecureRandom.uuid, args = {})
|
165
|
+
params = args.merge('client_id' => @client_id, 'response_type' => response_type, 'redirect_uri' => redirect_uri, 'state' => state)
|
166
|
+
params['scope'] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
|
167
|
+
params['nonce'], params['response_type'] = state, "#{response_type} id_token" if scope && scope.include?('openid')
|
168
|
+
"/oauth/authorize?#{URI.encode_www_form(params)}"
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
data/lib/uaa/util.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
#--
|
2
|
+
# Cloud Foundry 2012.02.03 Beta
|
3
|
+
# Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
|
4
|
+
#
|
5
|
+
# This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
6
|
+
# You may not use this product except in compliance with the License.
|
7
|
+
#
|
8
|
+
# This product includes a number of subcomponents with
|
9
|
+
# separate copyright notices and license terms. Your use of these
|
10
|
+
# subcomponents is subject to the terms and conditions of the
|
11
|
+
# subcomponent's license, as noted in the LICENSE file.
|
12
|
+
#++
|
13
|
+
|
14
|
+
require 'multi_json'
|
15
|
+
require "base64"
|
16
|
+
require 'logger'
|
17
|
+
require 'uri'
|
18
|
+
|
19
|
+
# :nodoc:
|
20
|
+
module CF
|
21
|
+
# Namespace for Cloudfoundry UAA
|
22
|
+
module UAA end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Logger # :nodoc:
|
26
|
+
Severity::TRACE = Severity::DEBUG - 1
|
27
|
+
def trace(progname, &blk); add(Logger::Severity::TRACE, nil, progname, &blk) end
|
28
|
+
def trace? ; @level <= Logger::Severity::TRACE end
|
29
|
+
end
|
30
|
+
|
31
|
+
module CF::UAA
|
32
|
+
|
33
|
+
# all CF::UAA exceptions are derived from UAAError
|
34
|
+
class UAAError < RuntimeError; end
|
35
|
+
|
36
|
+
# Authentication error
|
37
|
+
class AuthError < UAAError; end
|
38
|
+
|
39
|
+
# error for decoding tokens, base64 encoding, or json
|
40
|
+
class DecodeError < UAAError; end
|
41
|
+
|
42
|
+
# low level helper functions useful to the UAA client APIs
|
43
|
+
class Util
|
44
|
+
|
45
|
+
# http headers and various protocol tags tend to contain '-' characters,
|
46
|
+
# are intended to be case-insensitive, and often end up as keys in ruby
|
47
|
+
# hashes. The :undash style converts to lowercase for at least
|
48
|
+
# consistent case if not exactly case insensitive, and with '_' instead
|
49
|
+
# of '-' for ruby convention. :todash reverses :undash (except for case).
|
50
|
+
# :uncamel and :tocamel provide similar translations for camel-case keys.
|
51
|
+
# returns new key
|
52
|
+
def self.hash_key(k, style)
|
53
|
+
case style
|
54
|
+
when :undash then k.to_s.downcase.tr('-', '_').to_sym
|
55
|
+
when :todash then k.to_s.downcase.tr('_', '-')
|
56
|
+
when :uncamel then k.to_s.downcase.gsub(/([A-Z])([^A-Z]*)/,'_\1\2').to_sym
|
57
|
+
when :tocamel then k.to_s.gsub(/(_[a-z])([^_]*)/) { $1[1].upcase + $2 }
|
58
|
+
when :tosym then k.to_sym
|
59
|
+
when :tostr then k.to_s
|
60
|
+
when :down then k.to_s.downcase
|
61
|
+
when :none then k
|
62
|
+
else raise ArgumentError, "unknown hash key style: #{style}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# modifies obj in place changing any hash keys to style (see hash_key).
|
67
|
+
# Recursively modifies subordinate hashes. Returns modified obj
|
68
|
+
def self.hash_keys!(obj, style = :none)
|
69
|
+
return obj if style == :none
|
70
|
+
return obj.each {|o| hash_keys!(o, style)} if obj.is_a? Array
|
71
|
+
return obj unless obj.is_a? Hash
|
72
|
+
newkeys, nk = {}, nil
|
73
|
+
obj.delete_if { |k, v|
|
74
|
+
hash_keys!(v, style)
|
75
|
+
newkeys[nk] = v unless (nk = hash_key(k, style)) == k
|
76
|
+
nk != k
|
77
|
+
}
|
78
|
+
obj.merge!(newkeys)
|
79
|
+
end
|
80
|
+
|
81
|
+
# makes a new copy of obj with hash keys to style (see hash_key).
|
82
|
+
# Recursively modifies subordinate hashes. Returns modified obj
|
83
|
+
def self.hash_keys(obj, style = :none)
|
84
|
+
return obj.collect {|o| hash_keys(o, style)} if obj.is_a? Array
|
85
|
+
return obj unless obj.is_a? Hash
|
86
|
+
obj.each_with_object({}) {|(k, v), h|
|
87
|
+
h[hash_key(k, style)] = hash_keys(v, style)
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
# Takes an x-www-form-urlencoded string and returns a hash of key value pairs.
|
92
|
+
# Useful for OAuth parameters. It raises an ArgumentError if a key occurs
|
93
|
+
# more than once, which is a restriction of OAuth query strings.
|
94
|
+
# OAuth parameters are case sensitive, scim parameters are case-insensitive
|
95
|
+
# See ietf rfc 6749 section 3.1.
|
96
|
+
def self.decode_form_to_hash(url_encoded_pairs, style = :none)
|
97
|
+
URI.decode_www_form(url_encoded_pairs).each_with_object({}) do |p, o|
|
98
|
+
k = hash_key(p[0], style)
|
99
|
+
raise ArgumentError, "duplicate keys in form parameters" if o[k]
|
100
|
+
o[k] = p[1]
|
101
|
+
end
|
102
|
+
rescue Exception => e
|
103
|
+
raise ArgumentError, e.message
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.json(obj) MultiJson.dump(obj) end
|
107
|
+
def self.json_encode64(obj = {}) encode64(json(obj)) end
|
108
|
+
def self.json_decode64(str) json_parse(decode64(str)) end
|
109
|
+
def self.encode64(obj) Base64::urlsafe_encode64(obj).gsub(/=*$/, '') end
|
110
|
+
def self.decode64(str)
|
111
|
+
return unless str
|
112
|
+
pad = str.length % 4
|
113
|
+
str << '=' * (4 - pad) if pad > 0
|
114
|
+
Base64::urlsafe_decode64(str)
|
115
|
+
rescue ArgumentError
|
116
|
+
raise DecodeError, "invalid base64 encoding"
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.json_parse(str, style = :none)
|
120
|
+
hash_keys!(MultiJson.load(str), style) if str && !str.empty?
|
121
|
+
rescue MultiJson::DecodeError
|
122
|
+
raise DecodeError, "json decoding error"
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.truncate(obj, limit = 50)
|
126
|
+
return obj.to_s if limit == 0
|
127
|
+
limit = limit < 5 ? 1 : limit - 4
|
128
|
+
str = obj.to_s[0..limit]
|
129
|
+
str.length > limit ? str + '...': str
|
130
|
+
end
|
131
|
+
|
132
|
+
# many parameters in these classes can be given as arrays, or as a list of
|
133
|
+
# arguments separated by spaces or commas. This method handles the possible
|
134
|
+
# inputs and returns an array of arguments.
|
135
|
+
def self.arglist(arg, default_arg = nil)
|
136
|
+
arg = default_arg unless arg
|
137
|
+
return arg if arg.nil? || arg.respond_to?(:join)
|
138
|
+
raise ArgumentError, "arg must be Array or space|comma delimited strings" unless arg.respond_to?(:split)
|
139
|
+
arg.split(/[\s\,]+/).reject { |e| e.empty? }
|
140
|
+
end
|
141
|
+
|
142
|
+
# reverse of arglist, puts arrays of strings into a single, space-delimited string
|
143
|
+
def self.strlist(arg, delim = ' ')
|
144
|
+
arg.respond_to?(:join) ? arg.join(delim) : arg.to_s if arg
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.default_logger(level = nil, sink = nil)
|
148
|
+
if sink || !@default_logger
|
149
|
+
@default_logger = Logger.new(sink || $stdout)
|
150
|
+
level = :info unless level
|
151
|
+
@default_logger.formatter = Proc.new { |severity, time, pname, msg| puts msg }
|
152
|
+
end
|
153
|
+
@default_logger.level = Logger::Severity.const_get(level.upcase) if level
|
154
|
+
@default_logger
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|