cf-uaa-lib 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|