right_sharding 1.0.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.
- data/README.md +25 -0
- data/lib/right_sharding.rb +53 -0
- data/lib/right_sharding/base_proxy.rb +204 -0
- data/lib/right_sharding/current_account.rb +68 -0
- data/lib/right_sharding/extensions/core.rb +10 -0
- data/lib/right_sharding/instance_api_proxy.rb +121 -0
- data/lib/right_sharding/routing.rb +298 -0
- data/lib/right_sharding/shard_handler.rb +137 -0
- data/lib/right_sharding/user_api_proxy.rb +147 -0
- metadata +239 -0
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Right Sharding
|
2
|
+
|
3
|
+
Right Sharding is a Rack Middleware that makes requests aware of RightScale shards.
|
4
|
+
See: https://wookiee.rightscale.com/display/rightscale/Sharding
|
5
|
+
|
6
|
+
It currently supports RightSite and Right API requests.
|
7
|
+
|
8
|
+
Maintained by the RightScale Cornsilk team.
|
9
|
+
|
10
|
+
## Setup
|
11
|
+
|
12
|
+
Right Sharding uses rconf to install bundler.
|
13
|
+
|
14
|
+
Install latest rconf:
|
15
|
+
$ gem install rconf
|
16
|
+
|
17
|
+
Run rconf:
|
18
|
+
$ rconf
|
19
|
+
|
20
|
+
## Testing
|
21
|
+
|
22
|
+
### Unit Testing
|
23
|
+
|
24
|
+
Run rspec:
|
25
|
+
$ bundle exec spec spec/
|
@@ -0,0 +1,53 @@
|
|
1
|
+
basedir = File.dirname(__FILE__)
|
2
|
+
|
3
|
+
#Require the core suite of RightSharding classes and modules
|
4
|
+
require File.join(basedir, "right_sharding", "routing")
|
5
|
+
require File.join(basedir, "right_sharding", "base_proxy")
|
6
|
+
require File.join(basedir, "right_sharding", "current_account")
|
7
|
+
require File.join(basedir, "right_sharding", "instance_api_proxy")
|
8
|
+
require File.join(basedir, "right_sharding", "shard_handler")
|
9
|
+
require File.join(basedir, "right_sharding", "user_api_proxy")
|
10
|
+
require File.join(basedir, "right_sharding", 'extensions', 'core')
|
11
|
+
|
12
|
+
|
13
|
+
module RightSharding
|
14
|
+
def self.activate(config, options = {})
|
15
|
+
# Ensure that this super-important module attribute is set before any init code attempts to use it!
|
16
|
+
RightSharding::Routing.skip_shard_routing_callback = options.delete(:skip_shard_routing_callback)
|
17
|
+
|
18
|
+
# The library uses only the CurrentAccount middleware.
|
19
|
+
current_account_only = options.delete(:current_account_only)
|
20
|
+
|
21
|
+
# Add middleware to update the global_session account based on received headers/params.
|
22
|
+
# Must come after GlobalSession.
|
23
|
+
config.middleware.insert_after ::Rack::MethodOverride, ::Rack::CurrentAccount, options
|
24
|
+
|
25
|
+
unless current_account_only
|
26
|
+
# Add the middleware to intercept the requests and reroute users to the appropriate shards
|
27
|
+
# It must come after GlobalSession and CurrentAccount; GlobalSession will be used for the
|
28
|
+
# legacy path.
|
29
|
+
config.middleware.insert_after ::Rack::CurrentAccount, ::Rack::ShardRouting, options
|
30
|
+
|
31
|
+
# Add the middleware to proxy the instance facing api to the appropriate shards
|
32
|
+
config.middleware.insert_before ::Rack::ShardRouting, ::Rack::ShardInstanceApiProxy, options
|
33
|
+
|
34
|
+
# This will proxy the specific accounts passed in the options variable.
|
35
|
+
config.middleware.insert_after(::Rack::ShardInstanceApiProxy, ::Rack::ShardUserApiProxy, options)
|
36
|
+
end
|
37
|
+
rescue NoMethodError => e
|
38
|
+
raise NoMethodError, "GlobalSession must be used first."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# HACK: monkey patch to ensure no extra headers get added
|
44
|
+
# do not allow Rest-Client default_headers to add 'application/xml'
|
45
|
+
# See: https://github.com/rest-client/rest-client/blob/v1.6.1/lib/restclient/request.rb#L279
|
46
|
+
require 'rest_client'
|
47
|
+
module RestClient
|
48
|
+
class Request
|
49
|
+
def default_headers
|
50
|
+
{}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
|
3
|
+
#
|
4
|
+
# THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
|
5
|
+
# AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
|
6
|
+
# reproduction, modification, or disclosure of this program is
|
7
|
+
# strictly prohibited. Any use of this program by an authorized
|
8
|
+
# licensee is strictly subject to the terms and conditions,
|
9
|
+
# including confidentiality obligations, set forth in the applicable
|
10
|
+
# License Agreement between RightScale, Inc. and
|
11
|
+
# the licensee.
|
12
|
+
#++
|
13
|
+
|
14
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
|
15
|
+
|
16
|
+
module RightSharding
|
17
|
+
module Rack
|
18
|
+
module Middleware
|
19
|
+
|
20
|
+
class BaseProxy
|
21
|
+
REST_CLIENT_DEFAULT_TIMEOUT = 1000 # max time to read a response in seconds
|
22
|
+
REST_CLIENT_DEFAULT_OPEN_TIMEOUT = 1000 # max time to open a connection in seconds
|
23
|
+
class BaseProxyYield < StandardError; end
|
24
|
+
|
25
|
+
class BaseProxyException < StandardError
|
26
|
+
attr_reader :code, :headers, :body
|
27
|
+
|
28
|
+
def initialize(code, headers, body)
|
29
|
+
@env = nil # subclass needs to set this
|
30
|
+
@code = code
|
31
|
+
@headers = headers
|
32
|
+
@body = body
|
33
|
+
|
34
|
+
super(body)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#-----------------------------------------------------------------------
|
39
|
+
# Rack Middleware
|
40
|
+
#-----------------------------------------------------------------------
|
41
|
+
|
42
|
+
def initialize(app, params = {})
|
43
|
+
@app = app
|
44
|
+
|
45
|
+
@config_object = Module.const_get(params[:config_object])
|
46
|
+
@shard_object = Module.const_get(params[:shard_object])
|
47
|
+
@sharding_object = Module.const_get(params[:sharding_object])
|
48
|
+
@logger = Module.const_get(params[:logger])
|
49
|
+
end
|
50
|
+
|
51
|
+
# Subclass needs to implement
|
52
|
+
# def call(env)
|
53
|
+
|
54
|
+
#-----------------------------------------------------------------------
|
55
|
+
# Getters
|
56
|
+
#-----------------------------------------------------------------------
|
57
|
+
|
58
|
+
def get_this_shard_id
|
59
|
+
RightSharding::ShardHandler.fetch_this_shard_id(@config_object)
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_shard_from_shard_id(shard_id)
|
63
|
+
RightSharding::ShardHandler.fetch_shard(@shard_object, shard_id)
|
64
|
+
end
|
65
|
+
|
66
|
+
#-----------------------------------------------------------------------
|
67
|
+
# General Helper
|
68
|
+
#-----------------------------------------------------------------------
|
69
|
+
|
70
|
+
def proxy_request_to_the_destinated_shard(hostname, options={})
|
71
|
+
request = get_request
|
72
|
+
|
73
|
+
rest_options = {}
|
74
|
+
rest_options[:url] = "#{request.protocol}#{hostname}:#{request.port}#{request.request_uri}"
|
75
|
+
rest_options[:method] = request.method
|
76
|
+
rest_options[:headers] = request.get_headers_for_proxy
|
77
|
+
rest_options[:payload] = request.get_payload_for_proxy if request.get_payload_for_proxy
|
78
|
+
rest_options[:cookies] = request.get_cookies_for_proxy if request.get_cookies_for_proxy
|
79
|
+
rest_options[:timeout] = REST_CLIENT_DEFAULT_TIMEOUT
|
80
|
+
rest_options[:open_timeout] = REST_CLIENT_DEFAULT_OPEN_TIMEOUT
|
81
|
+
|
82
|
+
shard_proxy_logger(:info, "#{self.class}: Proxying the request to '#{hostname}' with url: #{rest_options[:url]}")
|
83
|
+
|
84
|
+
response = RestClient::Request.execute(rest_options)
|
85
|
+
if options[:proxy_global_session]
|
86
|
+
set_global_session_from_response(response)
|
87
|
+
end
|
88
|
+
|
89
|
+
[response.code, get_rack_format_response_headers(response), response.to_s]
|
90
|
+
rescue URI::InvalidURIError, SocketError, Errno::ECONNREFUSED, GlobalSession::ClientError => e
|
91
|
+
shard_proxy_logger(:error, "#{self.class}: failed: #{e.class} proxy request to url: #{rest_options[:url]}\n" + e.message + "\n " + e.backtrace.join("\n "))
|
92
|
+
[503, {}, "Unable to proxy the request to the destination '#{hostname}'. #{e.message}"]
|
93
|
+
rescue RestClient::Exception => e
|
94
|
+
shard_proxy_logger(:error, "#{self.class}: failed: #{e.class} proxy request to url: #{rest_options[:url]}\n" + e.message + "\n " + e.backtrace.join("\n "))
|
95
|
+
[e.http_code, get_rack_format_response_headers(e.response), e.response.to_s]
|
96
|
+
end
|
97
|
+
|
98
|
+
#-----------------------------------------------------------------------
|
99
|
+
# Request Helper
|
100
|
+
#-----------------------------------------------------------------------
|
101
|
+
|
102
|
+
def get_request
|
103
|
+
request = ActionController::Request.new(@env)
|
104
|
+
|
105
|
+
override_request_ssl(request)
|
106
|
+
|
107
|
+
define_get_headers_for_proxy(request)
|
108
|
+
define_get_payload_for_proxy(request)
|
109
|
+
define_get_cookies_for_proxy(request)
|
110
|
+
|
111
|
+
request
|
112
|
+
end
|
113
|
+
|
114
|
+
def override_request_ssl(request)
|
115
|
+
if (@http_proto && @http_proto == "http") || (@env['HTTP_X_FORWARDED_PROTO'] && @env['HTTP_X_FORWARDED_PROTO'].split(',')[0] == "http")
|
116
|
+
def request.ssl?; false; end
|
117
|
+
else
|
118
|
+
def request.ssl?; true; end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def define_get_headers_for_proxy(request)
|
123
|
+
def request.get_headers_for_proxy
|
124
|
+
pheaders = {}
|
125
|
+
|
126
|
+
# preserve headers
|
127
|
+
headers.each {|key, value| pheaders[$1] = value if key =~ /^HTTP_(.*)/}
|
128
|
+
|
129
|
+
# Send the 'X_FORWARDED_FOR' that we received on this server so that IP Whitelisting can check against that
|
130
|
+
# Prevent spoofing by calcuating the HMAC using a shared secret (in this case, the library_auth_key because
|
131
|
+
# we didn't want to add a new shared secret for this temporary proxy stuff)
|
132
|
+
forwarded_for_hmac = OpenSSL::HMAC.hexdigest('sha1', ::LIBRARY_AUTH_KEY, pheaders["X_FORWARDED_FOR"] || "")
|
133
|
+
pheaders["X_RS_PROXY_FORWARDED_FOR"] = "#{pheaders["X_FORWARDED_FOR"]};#{forwarded_for_hmac}"
|
134
|
+
|
135
|
+
# chain x_forwarded_for
|
136
|
+
pheaders["X_FORWARDED_FOR"] = [pheaders["X_FORWARDED_FOR"], self.ip].join(",") unless self.ip.blank?
|
137
|
+
|
138
|
+
pheaders
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def define_get_payload_for_proxy(request)
|
143
|
+
def request.get_payload_for_proxy
|
144
|
+
raw_post.blank? ? nil : raw_post
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def define_get_cookies_for_proxy(request)
|
149
|
+
def request.get_cookies_for_proxy
|
150
|
+
if headers["rack.request.cookie_hash"].blank?
|
151
|
+
nil
|
152
|
+
else
|
153
|
+
headers["rack.request.cookie_hash"].reject {|k, v| k !~ /rs_gbl|session_id/}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
#-----------------------------------------------------------------------
|
159
|
+
# Response Helper
|
160
|
+
#-----------------------------------------------------------------------
|
161
|
+
|
162
|
+
def get_rack_format_response_headers(response)
|
163
|
+
response.raw_headers.inject({}) do |out, (key, value)|
|
164
|
+
out[key] = %w{ set-cookie }.include?(key.downcase) ? value : value.first
|
165
|
+
out
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def set_global_session_from_response(response)
|
170
|
+
# Get the response's global session cookie if it exists
|
171
|
+
cookie_name = @env['global_session'].directory.configuration['cookie']['name']
|
172
|
+
cookie = CGI.unescape(response.cookies[cookie_name]) if response.cookies[cookie_name] # unescape expects a string
|
173
|
+
@env['global_session'] = @env['global_session'].directory.create_session(cookie)
|
174
|
+
end
|
175
|
+
|
176
|
+
#-----------------------------------------------------------------------
|
177
|
+
# URI Parsing Helper
|
178
|
+
#-----------------------------------------------------------------------
|
179
|
+
|
180
|
+
def get_api_version
|
181
|
+
request = ActionController::Request.new(@env)
|
182
|
+
|
183
|
+
if !request.parameters.blank? && !request.parameters[:api_version].blank?
|
184
|
+
api_version = request.parameters[:api_version]
|
185
|
+
elsif !@env["HTTP_X_API_VERSION"].blank?
|
186
|
+
api_version = @env["HTTP_X_API_VERSION"]
|
187
|
+
end
|
188
|
+
|
189
|
+
return (!api_version.blank? ? api_version.to_f : nil)
|
190
|
+
end
|
191
|
+
|
192
|
+
#-----------------------------------------------------------------------
|
193
|
+
# Logging Helper
|
194
|
+
#-----------------------------------------------------------------------
|
195
|
+
|
196
|
+
def shard_proxy_logger(level, message)
|
197
|
+
@logger.send(level, "#{message}")
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
|
2
|
+
|
3
|
+
module RightSharding
|
4
|
+
module Rack
|
5
|
+
module Middleware
|
6
|
+
|
7
|
+
class CurrentAccount
|
8
|
+
attr_reader :app
|
9
|
+
attr_reader :config_object, :logger
|
10
|
+
#-----------------------------------------------------------------------
|
11
|
+
# Rack Middleware
|
12
|
+
#-----------------------------------------------------------------------
|
13
|
+
|
14
|
+
def initialize(app, params = {})
|
15
|
+
@app = app
|
16
|
+
|
17
|
+
@config_object = Module.const_get(params[:config_object])
|
18
|
+
@logger = Module.const_get(params[:logger])
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO Current this replaces the account id set in env["global_session"]["account"]
|
22
|
+
# ultimately, we want this middleware to simply determine the correct account and set it
|
23
|
+
# in an env variable such as request.env['rightscale.current_account']. Once we refactor all
|
24
|
+
# the apps to use that instead, we can remove the use of global_session['account']
|
25
|
+
def call(env)
|
26
|
+
reset_global_session_account(env)
|
27
|
+
return @app.call(env)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Passing a X_ACCOUNT header or account query parameter can override
|
31
|
+
# the account provided in the global_session cookie. The X_ACCOUNT header
|
32
|
+
# or account query parameter must be the id of the account, and not the href.
|
33
|
+
#
|
34
|
+
# Order of precedence:
|
35
|
+
# "X_ACCOUNT" header > "account" query parameter > global_session
|
36
|
+
#
|
37
|
+
def reset_global_session_account(env)
|
38
|
+
if env['HTTP_X_ACCOUNT']
|
39
|
+
account_id = env['HTTP_X_ACCOUNT']
|
40
|
+
from = 'header'
|
41
|
+
elsif env['QUERY_STRING']
|
42
|
+
query_params = ::Rack::Utils.parse_query(env['QUERY_STRING'])
|
43
|
+
if query_params['account']
|
44
|
+
account_id = query_params['account']
|
45
|
+
from = 'query string'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Only reset the account_id if it exists, is numeric, and is different from what's
|
50
|
+
# currently in the global session
|
51
|
+
if account_id && account_id =~ /\A\d+\Z/ && account_id.to_i != env["global_session"]["account"]
|
52
|
+
logger.info("RightSharding::CurrentAccount: Resetting global_session account: " +
|
53
|
+
"#{env["global_session"]["account"]} -> #{account_id} (#{from})")
|
54
|
+
env["global_session"]["account"] = account_id.to_i
|
55
|
+
end
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Rack
|
65
|
+
unless defined?(::Rack::CurrentAccount)
|
66
|
+
CurrentAccount = ::RightSharding::Rack::Middleware::CurrentAccount
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
|
3
|
+
#
|
4
|
+
# THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
|
5
|
+
# AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
|
6
|
+
# reproduction, modification, or disclosure of this program is
|
7
|
+
# strictly prohibited. Any use of this program by an authorized
|
8
|
+
# licensee is strictly subject to the terms and conditions,
|
9
|
+
# including confidentiality obligations, set forth in the applicable
|
10
|
+
# License Agreement between RightScale, Inc. and
|
11
|
+
# the licensee.
|
12
|
+
#++
|
13
|
+
|
14
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
|
15
|
+
|
16
|
+
module RightSharding
|
17
|
+
module Rack
|
18
|
+
module Middleware
|
19
|
+
|
20
|
+
class InstanceApiProxy < BaseProxy
|
21
|
+
|
22
|
+
class InstanceApiProxyYield < BaseProxyYield; end
|
23
|
+
|
24
|
+
class InstanceApiProxyException < BaseProxyException; end
|
25
|
+
|
26
|
+
#-----------------------------------------------------------------------
|
27
|
+
# Rack Middleware
|
28
|
+
#-----------------------------------------------------------------------
|
29
|
+
|
30
|
+
def initialize(app, params = {})
|
31
|
+
super
|
32
|
+
@http_proto = params[:http_proto]
|
33
|
+
end
|
34
|
+
|
35
|
+
def call(env)
|
36
|
+
@env = env
|
37
|
+
|
38
|
+
if is_instance_api?(1.0)
|
39
|
+
unless token = get_instance_api_token_from_uri
|
40
|
+
raise InstanceApiProxyYield, "Unable to extract the token out: '#{@env["PATH_INFO"]}'"
|
41
|
+
end
|
42
|
+
|
43
|
+
unless shard_info = get_shard_info_from_database(token)
|
44
|
+
shard_info = get_shard_info_from_right_net(token)
|
45
|
+
end
|
46
|
+
|
47
|
+
unless shard_info.blank?
|
48
|
+
if shard_info[:shard_id] != get_this_shard_id
|
49
|
+
# the account is migrated
|
50
|
+
return proxy_request_to_the_destinated_shard(shard_info[:shard_hostname])
|
51
|
+
end
|
52
|
+
else
|
53
|
+
# we are here when the account/shard cannot be found or the account is disabled in every shard, we'll pass on and let the app to deal with it
|
54
|
+
raise InstanceApiProxyYield, "Unable to find the account/shard or the account is disabled: '#{@env["PATH_INFO"]}'"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# everything is good
|
59
|
+
return @app.call(env)
|
60
|
+
rescue InstanceApiProxyYield => e
|
61
|
+
shard_proxy_logger(:error, e.message)
|
62
|
+
return @app.call(env)
|
63
|
+
rescue InstanceApiProxyException => e
|
64
|
+
shard_proxy_logger(:error, "failed: #{e.class} " + e.message + "\n " + e.backtrace.join("\n "))
|
65
|
+
return [e.code, e.headers, e.body]
|
66
|
+
end
|
67
|
+
|
68
|
+
#-----------------------------------------------------------------------
|
69
|
+
# Getters
|
70
|
+
#-----------------------------------------------------------------------
|
71
|
+
|
72
|
+
def get_shard_info_from_database(token)
|
73
|
+
RightSharding::ShardHandler.fetch_instance_api_token_shard_id_and_hostname(token)
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_shard_id_from_right_net(token)
|
77
|
+
RightNetData.get_instance(:auth_token => token)
|
78
|
+
rescue RightSupport::Net::NoResult => e
|
79
|
+
raise InstanceApiProxyException.new(503, {}, "Unable to retrieve the instance from the given token")
|
80
|
+
rescue RestClient::Exception => e
|
81
|
+
raise InstanceApiProxyException.new(e.response.code, get_rack_format_response_headers(e.response), e.response.to_s)
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_shard_info_from_right_net(token)
|
85
|
+
if instance_attributes = get_shard_id_from_right_net(token)
|
86
|
+
if !instance_attributes[:shard_id].blank? && instance_attributes[:shard_id].to_i != 0
|
87
|
+
if shard = get_shard_from_shard_id(instance_attributes[:shard_id])
|
88
|
+
{:shard_id => shard.id, :shard_hostname => shard.hostname}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
#-----------------------------------------------------------------------
|
95
|
+
# URI Parsing Helper
|
96
|
+
#-----------------------------------------------------------------------
|
97
|
+
|
98
|
+
def is_instance_api?(version)
|
99
|
+
@env["PATH_INFO"] =~ /(?:^|\/)api\/inst\/ec2_instances\// && get_api_version == version
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_instance_api_token_from_uri
|
103
|
+
@env["PATH_INFO"][/(?:^|\/)api\/inst\/ec2_instances\/([^\/]+)(?:\/|$)/, 1]
|
104
|
+
end
|
105
|
+
|
106
|
+
# Overrides BaseProxy, default is to return 1.0 as the version if an API
|
107
|
+
# version is not detected. (required for v3 instance api requests)
|
108
|
+
def get_api_version
|
109
|
+
super || 1.0
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module Rack
|
119
|
+
ShardInstanceApiProxy = ::RightSharding::Rack::Middleware::InstanceApiProxy unless defined?(::Rack::ShardInstanceApiProxy)
|
120
|
+
end
|
121
|
+
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
|
2
|
+
|
3
|
+
module RightSharding
|
4
|
+
# Rails routing functionality embedded into RightSharding. The module contains public utility
|
5
|
+
# methods; most of the actual routing and redirection logic is tucked away inside the routing
|
6
|
+
# middleware that lives under this namespace.
|
7
|
+
#
|
8
|
+
# @see RightSharding::Rack::Middleware::Routing
|
9
|
+
module Routing
|
10
|
+
|
11
|
+
# mattr_accessor is Rails feature so it is not gonna work when we run "rake spec".
|
12
|
+
# # mattr_accessor :skip_shard_routing_callback
|
13
|
+
#
|
14
|
+
def self.skip_shard_routing_callback
|
15
|
+
@@skip_shard_routing_callback ||= nil
|
16
|
+
end
|
17
|
+
def self.skip_shard_routing_callback=(skip_shard_routing_callback)
|
18
|
+
@@skip_shard_routing_callback = skip_shard_routing_callback
|
19
|
+
end
|
20
|
+
|
21
|
+
module_function
|
22
|
+
|
23
|
+
# Determine whether shard routing and account-prefixing should be skipped for a given path and request method.
|
24
|
+
# This delegates to a callback that should be provided by whoever initializes the Routing middleware.
|
25
|
+
#
|
26
|
+
# @see RightSharding::Rack::Middleware::Routing#initialize
|
27
|
+
#
|
28
|
+
# @return [Boolean] true if shard routing and account-prefixing should be skipped; false otherwise
|
29
|
+
def skip_shard_routing?(path, request_method)
|
30
|
+
skip_shard_routing_callback.present? && skip_shard_routing_callback.call(path, request_method)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Extract the current-account ID from a dashboard (right_site or embedded library) path.
|
34
|
+
#
|
35
|
+
# @return [String] a numeric string, or nil if the path does not include account info
|
36
|
+
def fetch_current_account_id_from_path(path)
|
37
|
+
path[/(?:^|\/)acct\/(\d+)(?:\/|$)/, 1]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Prepend "/acct/X" to a URL path. Does not check if the path already contains this prefix;
|
41
|
+
# be careful!
|
42
|
+
#
|
43
|
+
# @param [String] original_path the path-info of the original URI, e.g. "/dashboard;index"
|
44
|
+
# @param [Integer] account_id the account ID to prepend to the path
|
45
|
+
# @return [String] the original_path with an account-id prefix
|
46
|
+
def add_account_to_path(original_path, account_id)
|
47
|
+
case original_path
|
48
|
+
when /(?:^|\/)api(?:\/|$)/
|
49
|
+
original_path.sub(/(^|\/)api(\/|$)/, '\1' + "api/acct/#{account_id}" + '\2')
|
50
|
+
else
|
51
|
+
"/acct/#{account_id}" + (original_path =~ /^\// ? "" : "/") + original_path
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module Rack
|
57
|
+
module Middleware
|
58
|
+
|
59
|
+
class Routing
|
60
|
+
|
61
|
+
attr_reader :app
|
62
|
+
attr_reader :config_object, :sharding_object, :logger
|
63
|
+
|
64
|
+
#-----------------------------------------------------------------------
|
65
|
+
# Rack Middleware
|
66
|
+
#-----------------------------------------------------------------------
|
67
|
+
|
68
|
+
def initialize(app, params = {})
|
69
|
+
@app = app
|
70
|
+
|
71
|
+
@config_object = Module.const_get(params[:config_object])
|
72
|
+
@sharding_object = Module.const_get(params[:sharding_object])
|
73
|
+
@logger = Module.const_get(params[:logger])
|
74
|
+
|
75
|
+
@http_proto = params[:http_proto]
|
76
|
+
@skip_shard_routing_callback = params[:skip_shard_routing_callback]
|
77
|
+
end
|
78
|
+
|
79
|
+
def call(env)
|
80
|
+
# On every request, we want to initialize/clear the local cache to keep track the Account and Shard map,
|
81
|
+
# so it can reduce the hit to any cache system. The cache exists throughout the request and only that request.
|
82
|
+
ShardHandler::CurrentRequestCache.init
|
83
|
+
|
84
|
+
return app.call(env) if skip_shard_routing?(env["PATH_INFO"], env["REQUEST_METHOD"])
|
85
|
+
|
86
|
+
shard_id = RightSharding::ShardHandler.fetch_this_shard_id(config_object)
|
87
|
+
|
88
|
+
# HTTP_X_FORWARDED_HOST header may contain multiple values, pick the first one
|
89
|
+
forward_host = (env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"]).split(',')[0]
|
90
|
+
|
91
|
+
# NOTE: we use HTTP_HOST instead of SERVER_NAME because:
|
92
|
+
# * for the configuration like WEBrick server, it returns both hostname and port. (development env)
|
93
|
+
# * for others, it only returns the hostname
|
94
|
+
uri_handler = new_uri_handler(get_http_proto(env),
|
95
|
+
forward_host,
|
96
|
+
env['PATH_INFO'],
|
97
|
+
env['QUERY_STRING'])
|
98
|
+
|
99
|
+
if (account_id = fetch_current_account_id_from_path(env["PATH_INFO"]))
|
100
|
+
requested_shard = ShardHandler.fetch_shard_id_and_hostname(sharding_object, account_id.to_i)
|
101
|
+
|
102
|
+
if requested_shard.blank? || requested_shard[:shard_id].blank?
|
103
|
+
# no shard info, pass the request through
|
104
|
+
shard_routing_logger(:error, "Could not locate shard info", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
|
105
|
+
return app.call(env)
|
106
|
+
elsif requested_shard[:shard_id] != shard_id
|
107
|
+
# the requested account is in a different shard => redirect
|
108
|
+
|
109
|
+
# reset the hostname to a new hostname and everything else remains the same
|
110
|
+
reset_request_uri(uri_handler, requested_shard[:shard_hostname], env["REQUEST_METHOD"], env['PATH_INFO'], env['QUERY_STRING'])
|
111
|
+
|
112
|
+
return reroute_to_shard(uri_handler)
|
113
|
+
else
|
114
|
+
# everything good => continue to the next in the chain
|
115
|
+
return app.call(env)
|
116
|
+
end
|
117
|
+
else
|
118
|
+
## legacy or api route
|
119
|
+
# always default to the account_id from global_session if none given in the url
|
120
|
+
if (account_id = env["global_session"]["account"])
|
121
|
+
requested_shard = ShardHandler.fetch_shard_id_and_hostname(sharding_object, account_id.to_i)
|
122
|
+
|
123
|
+
path_info = env['PATH_INFO']
|
124
|
+
# add account to the legacy route
|
125
|
+
if path_info && !path_info.empty?
|
126
|
+
# we don't rewrite the route if it is api-1.5 request
|
127
|
+
if path_info !~ /(?:^|\/)api\//
|
128
|
+
shard_routing_logger(:info, "Missing account_id in URL", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
|
129
|
+
path_info = add_account_to_path(path_info, account_id)
|
130
|
+
else
|
131
|
+
api_version = get_api_version(env, uri_handler)
|
132
|
+
|
133
|
+
if api_version
|
134
|
+
# we don't rewrite the route if it is api-1.0 instance facing request
|
135
|
+
if api_version < 1.5 && path_info !~ /(?:^|\/)api\/inst\//
|
136
|
+
shard_routing_logger(:error, "Invalid API (<1.5) Request", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
|
137
|
+
path_info = add_account_to_path(path_info, account_id)
|
138
|
+
end
|
139
|
+
else
|
140
|
+
shard_routing_logger(:error, "Missing API version", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# requested the account is in a different shard => redirect
|
146
|
+
if requested_shard.blank? || requested_shard[:shard_id].blank?
|
147
|
+
# no shard info, pass the request through
|
148
|
+
shard_routing_logger(:error, "Could not locate shard info", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
|
149
|
+
return app.call(env)
|
150
|
+
elsif requested_shard[:shard_id] != shard_id
|
151
|
+
# reset the hostname and the path (added /acct/:id) and everything else remains the same
|
152
|
+
reset_request_uri(uri_handler, requested_shard[:shard_hostname], env["REQUEST_METHOD"], path_info, env['QUERY_STRING'])
|
153
|
+
|
154
|
+
return reroute_to_shard(uri_handler)
|
155
|
+
else
|
156
|
+
if uri_handler.path != path_info
|
157
|
+
# rewrite request uri by adding account into the route
|
158
|
+
reset_request_uri(uri_handler,
|
159
|
+
env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"],
|
160
|
+
env["REQUEST_METHOD"], path_info,
|
161
|
+
env['QUERY_STRING'])
|
162
|
+
|
163
|
+
return reroute_to_shard(uri_handler)
|
164
|
+
else
|
165
|
+
return app.call(env)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
else
|
169
|
+
# Invalid
|
170
|
+
shard_routing_logger(:error, "Invalid Request", uri_handler, env["QUERY_STRING"], env["REQUEST_METHOD"])
|
171
|
+
return app.call(env)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
ensure
|
175
|
+
ShardHandler::CurrentRequestCache.destroy
|
176
|
+
end
|
177
|
+
|
178
|
+
private ######################################################################
|
179
|
+
def get_http_proto(env)
|
180
|
+
if @http_proto
|
181
|
+
@http_proto
|
182
|
+
elsif env['HTTPS'] == 'on'
|
183
|
+
'https'
|
184
|
+
elsif env['HTTP_X_FORWARDED_SSL'] == 'on'
|
185
|
+
'https'
|
186
|
+
elsif env['HTTP_X_FORWARDED_PROTO']
|
187
|
+
env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
|
188
|
+
else
|
189
|
+
'https'
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
#-----------------------------------------------------------------------
|
195
|
+
# URL Parser
|
196
|
+
#-----------------------------------------------------------------------
|
197
|
+
|
198
|
+
# Call through to the equivalent module method in RightSharding::Routing.
|
199
|
+
def fetch_current_account_id_from_path(path)
|
200
|
+
RightSharding::Routing.fetch_current_account_id_from_path(path)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Call through to the equivalent module method in RightSharding::Routing.
|
204
|
+
def skip_shard_routing?(path, request_method)
|
205
|
+
RightSharding::Routing.skip_shard_routing?(path, request_method)
|
206
|
+
end
|
207
|
+
|
208
|
+
def get_api_version(env, uri_handler)
|
209
|
+
api_version = nil
|
210
|
+
|
211
|
+
if env["HTTP_X_API_VERSION"]
|
212
|
+
api_version = env["HTTP_X_API_VERSION"]
|
213
|
+
elsif uri_handler.query
|
214
|
+
query_hash = CGI.parse(uri_handler.query)
|
215
|
+
|
216
|
+
api_version_key = query_hash.keys.find {|query_key| query_key =~ /api_version/i}
|
217
|
+
api_version = api_version_key ? query_hash[api_version_key][0] : nil
|
218
|
+
end
|
219
|
+
|
220
|
+
return (!api_version.blank? ? api_version.to_f : nil)
|
221
|
+
end
|
222
|
+
|
223
|
+
#-----------------------------------------------------------------------
|
224
|
+
# URI Constructor
|
225
|
+
#-----------------------------------------------------------------------
|
226
|
+
|
227
|
+
def new_uri_handler(schema, hostname_with_port, path_info, query_string)
|
228
|
+
hostname, port = hostname_with_port.split(":")
|
229
|
+
|
230
|
+
# Ensure port is a float for URI::HTTP.build to not fail
|
231
|
+
port = port.to_i unless port.nil?
|
232
|
+
|
233
|
+
uri_handler = if schema == "http"
|
234
|
+
URI::HTTP.build(:host => hostname, :port => port)
|
235
|
+
else
|
236
|
+
URI::HTTPS.build(:host => hostname, :port => port)
|
237
|
+
end
|
238
|
+
|
239
|
+
uri_handler.path = path_info || ""
|
240
|
+
# set the query string to nil if it is blank (otherwise it will leave a '?' in the url)
|
241
|
+
uri_handler.query = (query_string && !query_string.empty?) ? query_string : nil
|
242
|
+
uri_handler
|
243
|
+
end
|
244
|
+
|
245
|
+
def reset_request_uri(uri_handler, hostname_with_optional_port, request_method, path_info, query_string)
|
246
|
+
hostname, port = hostname_with_optional_port.split(":")
|
247
|
+
|
248
|
+
uri_handler.host = hostname
|
249
|
+
uri_handler.port = port unless port.nil?
|
250
|
+
|
251
|
+
if request_method.downcase == "get"
|
252
|
+
uri_handler.path = path_info || ""
|
253
|
+
# set the query string to nil if it is blank (otherwise it will leave a '?' in the url)
|
254
|
+
uri_handler.query = (query_string && !query_string.empty?) ? query_string : nil
|
255
|
+
else
|
256
|
+
uri_handler.path = ""
|
257
|
+
uri_handler.query = nil
|
258
|
+
end
|
259
|
+
|
260
|
+
uri_handler
|
261
|
+
end
|
262
|
+
|
263
|
+
# Call through to the equivalent module method in RightSharding::Routing.
|
264
|
+
def add_account_to_path(original_path, account_id)
|
265
|
+
RightSharding::Routing.add_account_to_path(original_path, account_id)
|
266
|
+
end
|
267
|
+
|
268
|
+
#-----------------------------------------------------------------------
|
269
|
+
# Redirect Helpers
|
270
|
+
#-----------------------------------------------------------------------
|
271
|
+
|
272
|
+
def reroute_to_shard(uri_handler)
|
273
|
+
location = uri_handler.to_s
|
274
|
+
|
275
|
+
[ 301, { "Location" => location }, [ redirect_message(location) ] ]
|
276
|
+
end
|
277
|
+
|
278
|
+
def redirect_message(location)
|
279
|
+
%Q(Redirecting to <a href="#{location}">#{location}</a>)
|
280
|
+
end
|
281
|
+
|
282
|
+
#-----------------------------------------------------------------------
|
283
|
+
# Logging Helpers
|
284
|
+
#-----------------------------------------------------------------------
|
285
|
+
|
286
|
+
def shard_routing_logger(type, header, uri, query_string, request_method)
|
287
|
+
logger.send(type, "RightSharding::Routing: #{header} - #{uri} - '#{query_string}' - '#{request_method}' request")
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
module Rack
|
297
|
+
ShardRouting = ::RightSharding::Rack::Middleware::Routing unless defined?(::Rack::ShardRouting)
|
298
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
|
3
|
+
#
|
4
|
+
# THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
|
5
|
+
# AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
|
6
|
+
# reproduction, modification, or disclosure of this program is
|
7
|
+
# strictly prohibited. Any use of this program by an authorized
|
8
|
+
# licensee is strictly subject to the terms and conditions,
|
9
|
+
# including confidentiality obligations, set forth in the applicable
|
10
|
+
# License Agreement between RightScale, Inc. and
|
11
|
+
# the licensee.
|
12
|
+
#++
|
13
|
+
|
14
|
+
module RightSharding
|
15
|
+
module ShardHandler
|
16
|
+
|
17
|
+
SHARDING_CACHE_EXPIRATION_RANGE = (30..60).to_a
|
18
|
+
|
19
|
+
#---------------------------------------------------------------------------
|
20
|
+
# THIS_SHARD_ID
|
21
|
+
#---------------------------------------------------------------------------
|
22
|
+
|
23
|
+
def self.fetch_this_shard_id(config_object)
|
24
|
+
# defer the call to the object after we are sure that it is not in cache
|
25
|
+
this_shard_id_proc = Proc.new { config_object.find_this_shard_id }
|
26
|
+
|
27
|
+
if CurrentRequestCache.is_activated?
|
28
|
+
CurrentRequestCache.find_or_save_this_shard_id(this_shard_id_proc)
|
29
|
+
else
|
30
|
+
# In this case, we are in script/console, migration, or daemon etc, we don't want to cache it in Thread.current
|
31
|
+
this_shard_id_proc.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
#---------------------------------------------------------------------------
|
36
|
+
# THIS_SHARD
|
37
|
+
#---------------------------------------------------------------------------
|
38
|
+
|
39
|
+
def self.shard_object_cache_key(shard_id)
|
40
|
+
"shard/#{shard_id}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.fetch_shard(shard_object, shard_id)
|
44
|
+
# defer the call to the object after we are sure that it is not in cache
|
45
|
+
this_shard_proc = Proc.new {
|
46
|
+
shard_object.get_shard_object(shard_id, shard_object_cache_key(shard_id), SHARDING_CACHE_EXPIRATION_RANGE.rand.minutes)
|
47
|
+
}
|
48
|
+
|
49
|
+
if CurrentRequestCache.is_activated?
|
50
|
+
CurrentRequestCache.find_or_save_this_shard(this_shard_proc)
|
51
|
+
else
|
52
|
+
# In this case, we are in script/console, migration, or daemon etc, we don't want to cache it in Thread.current
|
53
|
+
this_shard_proc.call
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.fetch_this_shard(config_object, shard_object)
|
58
|
+
fetch_shard(shard_object, fetch_this_shard_id(config_object))
|
59
|
+
end
|
60
|
+
|
61
|
+
#---------------------------------------------------------------------------
|
62
|
+
# SHARD information for each Account
|
63
|
+
#---------------------------------------------------------------------------
|
64
|
+
|
65
|
+
def self.shard_cache_key(account_id)
|
66
|
+
"account/#{account_id}/shard_id_and_hostname"
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.fetch_shard_id_and_hostname(sharding_object, account_id)
|
70
|
+
# defer the call to the object after we are sure that it is not in cache
|
71
|
+
get_shard_id_and_hostname_proc = Proc.new {
|
72
|
+
sharding_object.get_shard_id_and_hostname(account_id, shard_cache_key(account_id), SHARDING_CACHE_EXPIRATION_RANGE.rand.minutes)
|
73
|
+
}
|
74
|
+
|
75
|
+
if CurrentRequestCache.is_activated?
|
76
|
+
CurrentRequestCache.find_or_save_shard_map(account_id, get_shard_id_and_hostname_proc)
|
77
|
+
else
|
78
|
+
# In this case, we are in script/console, migration, or daemon etc, we don't want to cache it in Thread.current
|
79
|
+
get_shard_id_and_hostname_proc.call
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.fetch_shard_hostname(sharding_object, account_id)
|
84
|
+
requested_shard = fetch_shard_id_and_hostname(sharding_object, account_id)
|
85
|
+
requested_shard[:shard_hostname]
|
86
|
+
end
|
87
|
+
|
88
|
+
#---------------------------------------------------------------------------
|
89
|
+
# SHARD information for each InstanceApiToken
|
90
|
+
#---------------------------------------------------------------------------
|
91
|
+
|
92
|
+
def self.instance_api_token_shard_cache_key(token)
|
93
|
+
"instance_api_token/#{token}/shard_id_and_hostname"
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.fetch_instance_api_token_shard_id_and_hostname(token)
|
97
|
+
# FIXME: cyclical dependency - InstanceApiToken is defined in right_site
|
98
|
+
InstanceApiToken.get_shard_id_and_hostname(token, instance_api_token_shard_cache_key(token), SHARDING_CACHE_EXPIRATION_RANGE.rand.minutes)
|
99
|
+
end
|
100
|
+
|
101
|
+
#---------------------------------------------------------------------------
|
102
|
+
# CurrentRequestCache - caching data only for this request
|
103
|
+
#---------------------------------------------------------------------------
|
104
|
+
|
105
|
+
class CurrentRequestCache
|
106
|
+
CACHE_ID = :right_sharding_current_request_cache
|
107
|
+
|
108
|
+
def self.init
|
109
|
+
Thread.current[CACHE_ID] = {}
|
110
|
+
|
111
|
+
Thread.current[CACHE_ID]["shard_map"] = {}
|
112
|
+
Thread.current[CACHE_ID]["this_shard_id"] = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.destroy
|
116
|
+
Thread.current[CACHE_ID] = nil
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.is_activated?
|
120
|
+
!Thread.current[CACHE_ID].nil?
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.find_or_save_shard_map(account_id, value_proc)
|
124
|
+
Thread.current[CACHE_ID]["shard_map"][account_id] ||= value_proc.call
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.find_or_save_this_shard_id(value_proc)
|
128
|
+
Thread.current[CACHE_ID]["this_shard_id"] ||= value_proc.call
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.find_or_save_this_shard(value_proc)
|
132
|
+
Thread.current[CACHE_ID]["this_shard"] ||= value_proc.call
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2012 RightScale, Inc, All Rights Reserved Worldwide.
|
3
|
+
#
|
4
|
+
# THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE
|
5
|
+
# AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use,
|
6
|
+
# reproduction, modification, or disclosure of this program is
|
7
|
+
# strictly prohibited. Any use of this program by an authorized
|
8
|
+
# licensee is strictly subject to the terms and conditions,
|
9
|
+
# including confidentiality obligations, set forth in the applicable
|
10
|
+
# License Agreement between RightScale, Inc. and
|
11
|
+
# the licensee.
|
12
|
+
#++
|
13
|
+
|
14
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "right_sharding"))
|
15
|
+
|
16
|
+
module RightSharding
|
17
|
+
module Rack
|
18
|
+
module Middleware
|
19
|
+
|
20
|
+
class UserApiProxy < BaseProxy
|
21
|
+
|
22
|
+
ACCOUNTS_TO_PROXY = []
|
23
|
+
|
24
|
+
class UserApiProxyYield < BaseProxyYield; end
|
25
|
+
class UserApiProxyException < BaseProxyException; end
|
26
|
+
|
27
|
+
#-----------------------------------------------------------------------
|
28
|
+
# Rack Middleware
|
29
|
+
#-----------------------------------------------------------------------
|
30
|
+
|
31
|
+
def initialize(app, params = {})
|
32
|
+
super
|
33
|
+
@http_proto = params[:http_proto]
|
34
|
+
@accounts_to_proxy = params[:accounts_to_proxy] || []
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(env)
|
38
|
+
@env = env
|
39
|
+
|
40
|
+
if is_an_api_call?
|
41
|
+
account_id = get_account_id()
|
42
|
+
if is_proxiable?(account_id)
|
43
|
+
this_shard_id = RightSharding::ShardHandler.fetch_this_shard_id(@config_object)
|
44
|
+
requested_shard = RightSharding::ShardHandler.fetch_shard_id_and_hostname(@sharding_object, account_id.to_i)
|
45
|
+
requested_shard_id = requested_shard.blank? ? nil : requested_shard[:shard_id]
|
46
|
+
|
47
|
+
if !requested_shard_id.blank? && (this_shard_id != requested_shard_id)
|
48
|
+
@env['global_session.req.update'] = false
|
49
|
+
hostname = get_shard_hostname(account_id)
|
50
|
+
return proxy_request_to_the_destinated_shard(hostname, :proxy_global_session => true)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# everything is good
|
56
|
+
return @app.call(env)
|
57
|
+
rescue UserApiProxyYield => e
|
58
|
+
shard_proxy_logger(:error, e.message)
|
59
|
+
return @app.call(env)
|
60
|
+
rescue UserApiProxyException => e
|
61
|
+
shard_proxy_logger(:error, "failed: #{e.class} " + e.message + "\n " + e.backtrace.join("\n "))
|
62
|
+
return [e.code, e.headers, e.body]
|
63
|
+
end
|
64
|
+
|
65
|
+
# -----------------------------------------------------------------------
|
66
|
+
# General Helpers
|
67
|
+
# -----------------------------------------------------------------------
|
68
|
+
|
69
|
+
def is_proxiable?(account_id)
|
70
|
+
return true if @accounts_to_proxy.any? {|p| p == account_id}
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_shard_hostname(account_id)
|
75
|
+
RightSharding::ShardHandler.fetch_shard_hostname(@sharding_object, account_id)
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_account_id()
|
79
|
+
account_id = nil
|
80
|
+
api_version = get_api_version()
|
81
|
+
if api_version == 1.0 # It can be in url or global session, prefer url
|
82
|
+
if @env["PATH_INFO"] =~ /(?:^|\/)api\/acct\/([^\/]+)(?:\/|$)/
|
83
|
+
account_id = $1
|
84
|
+
else
|
85
|
+
if @env['global_session'] && @env['global_session']['account']
|
86
|
+
account_id = @env['global_session']['account']
|
87
|
+
end
|
88
|
+
end
|
89
|
+
elsif api_version == 1.5 # version 1.5 assumes a global account session
|
90
|
+
if @env['global_session'] && @env['global_session']['account']
|
91
|
+
account_id = @env['global_session']['account']
|
92
|
+
elsif @env['PATH_INFO'] =~ /(?:^|\/)api\/session.*$/
|
93
|
+
# We get here if it's a session request
|
94
|
+
req = ::Rack::Request.new(@env)
|
95
|
+
params = req.params
|
96
|
+
if params['account_href'] =~ /\/api\/accounts\/(\d+)/
|
97
|
+
account_id = $1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
account_id.nil? ? nil : account_id.to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
def is_an_api_call?
|
105
|
+
return true if @env['PATH_INFO'] =~ /(?:^|\/)api\/.*$/
|
106
|
+
false
|
107
|
+
end
|
108
|
+
|
109
|
+
def new_uri_handler(schema, hostname_with_port, path_info, query_string)
|
110
|
+
hostname, port = hostname_with_port.split(":")
|
111
|
+
|
112
|
+
uri_handler = if schema == "http"
|
113
|
+
URI::HTTP.build(:host => hostname, :port => port)
|
114
|
+
else
|
115
|
+
URI::HTTPS.build(:host => hostname, :port => port)
|
116
|
+
end
|
117
|
+
|
118
|
+
uri_handler.path = path_info || ""
|
119
|
+
# set the query string to nil if it is blank (otherwise it will leave a '?' in the url)
|
120
|
+
uri_handler.query = (query_string && !query_string.empty?) ? query_string : nil
|
121
|
+
uri_handler
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_http_proto(env)
|
125
|
+
if @http_proto
|
126
|
+
@http_proto
|
127
|
+
elsif env['HTTPS'] == 'on'
|
128
|
+
'https'
|
129
|
+
elsif env['HTTP_X_FORWARDED_SSL'] == 'on'
|
130
|
+
'https'
|
131
|
+
elsif env['HTTP_X_FORWARDED_PROTO']
|
132
|
+
env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
|
133
|
+
else
|
134
|
+
'https'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
module Rack
|
145
|
+
ShardUserApiProxy = ::RightSharding::Rack::Middleware::UserApiProxy unless defined?(::Rack::ShardUserApiProxy)
|
146
|
+
end
|
147
|
+
|
metadata
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: right_sharding
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 17
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 3
|
10
|
+
version: 1.0.3
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- RightScale, Inc.
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2014-07-25 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
type: :runtime
|
22
|
+
name: global_session
|
23
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - <
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 27
|
29
|
+
segments:
|
30
|
+
- 4
|
31
|
+
- 0
|
32
|
+
version: "4.0"
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
hash: 9
|
36
|
+
segments:
|
37
|
+
- 1
|
38
|
+
- 0
|
39
|
+
- 15
|
40
|
+
version: 1.0.15
|
41
|
+
prerelease: false
|
42
|
+
requirement: *id001
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
type: :runtime
|
45
|
+
name: mime-types
|
46
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ~>
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 47
|
52
|
+
segments:
|
53
|
+
- 1
|
54
|
+
- 16
|
55
|
+
version: "1.16"
|
56
|
+
prerelease: false
|
57
|
+
requirement: *id002
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
type: :runtime
|
60
|
+
name: rest-client
|
61
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
hash: 97
|
67
|
+
segments:
|
68
|
+
- 1
|
69
|
+
- 7
|
70
|
+
- 0
|
71
|
+
- 3
|
72
|
+
version: 1.7.0.3
|
73
|
+
prerelease: false
|
74
|
+
requirement: *id003
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
type: :runtime
|
77
|
+
name: actionpack
|
78
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - <
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
hash: 27
|
84
|
+
segments:
|
85
|
+
- 4
|
86
|
+
- 0
|
87
|
+
version: "4.0"
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
hash: 9
|
91
|
+
segments:
|
92
|
+
- 2
|
93
|
+
- 3
|
94
|
+
- 5
|
95
|
+
version: 2.3.5
|
96
|
+
prerelease: false
|
97
|
+
requirement: *id004
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
type: :runtime
|
100
|
+
name: right_support
|
101
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ~>
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
hash: 9
|
107
|
+
segments:
|
108
|
+
- 2
|
109
|
+
- 5
|
110
|
+
version: "2.5"
|
111
|
+
prerelease: false
|
112
|
+
requirement: *id005
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
type: :runtime
|
115
|
+
name: simple_uuid
|
116
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ~>
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
hash: 15
|
122
|
+
segments:
|
123
|
+
- 0
|
124
|
+
- 2
|
125
|
+
version: "0.2"
|
126
|
+
prerelease: false
|
127
|
+
requirement: *id006
|
128
|
+
- !ruby/object:Gem::Dependency
|
129
|
+
type: :development
|
130
|
+
name: nokogiri
|
131
|
+
version_requirements: &id007 !ruby/object:Gem::Requirement
|
132
|
+
none: false
|
133
|
+
requirements:
|
134
|
+
- - ~>
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
hash: 3
|
137
|
+
segments:
|
138
|
+
- 1
|
139
|
+
- 5
|
140
|
+
- 0
|
141
|
+
version: 1.5.0
|
142
|
+
prerelease: false
|
143
|
+
requirement: *id007
|
144
|
+
- !ruby/object:Gem::Dependency
|
145
|
+
type: :development
|
146
|
+
name: jeweler
|
147
|
+
version_requirements: &id008 !ruby/object:Gem::Requirement
|
148
|
+
none: false
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
hash: 3
|
153
|
+
segments:
|
154
|
+
- 0
|
155
|
+
version: "0"
|
156
|
+
prerelease: false
|
157
|
+
requirement: *id008
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
type: :development
|
160
|
+
name: yard
|
161
|
+
version_requirements: &id009 !ruby/object:Gem::Requirement
|
162
|
+
none: false
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
hash: 3
|
167
|
+
segments:
|
168
|
+
- 0
|
169
|
+
version: "0"
|
170
|
+
prerelease: false
|
171
|
+
requirement: *id009
|
172
|
+
- !ruby/object:Gem::Dependency
|
173
|
+
type: :development
|
174
|
+
name: redcarpet
|
175
|
+
version_requirements: &id010 !ruby/object:Gem::Requirement
|
176
|
+
none: false
|
177
|
+
requirements:
|
178
|
+
- - <
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
hash: 7
|
181
|
+
segments:
|
182
|
+
- 3
|
183
|
+
- 0
|
184
|
+
version: "3.0"
|
185
|
+
prerelease: false
|
186
|
+
requirement: *id010
|
187
|
+
description:
|
188
|
+
email:
|
189
|
+
executables: []
|
190
|
+
|
191
|
+
extensions: []
|
192
|
+
|
193
|
+
extra_rdoc_files:
|
194
|
+
- README.md
|
195
|
+
files:
|
196
|
+
- lib/right_sharding.rb
|
197
|
+
- lib/right_sharding/base_proxy.rb
|
198
|
+
- lib/right_sharding/current_account.rb
|
199
|
+
- lib/right_sharding/extensions/core.rb
|
200
|
+
- lib/right_sharding/instance_api_proxy.rb
|
201
|
+
- lib/right_sharding/routing.rb
|
202
|
+
- lib/right_sharding/shard_handler.rb
|
203
|
+
- lib/right_sharding/user_api_proxy.rb
|
204
|
+
- README.md
|
205
|
+
homepage:
|
206
|
+
licenses: []
|
207
|
+
|
208
|
+
post_install_message:
|
209
|
+
rdoc_options: []
|
210
|
+
|
211
|
+
require_paths:
|
212
|
+
- lib
|
213
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
214
|
+
none: false
|
215
|
+
requirements:
|
216
|
+
- - ">="
|
217
|
+
- !ruby/object:Gem::Version
|
218
|
+
hash: 3
|
219
|
+
segments:
|
220
|
+
- 0
|
221
|
+
version: "0"
|
222
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
223
|
+
none: false
|
224
|
+
requirements:
|
225
|
+
- - ">="
|
226
|
+
- !ruby/object:Gem::Version
|
227
|
+
hash: 3
|
228
|
+
segments:
|
229
|
+
- 0
|
230
|
+
version: "0"
|
231
|
+
requirements: []
|
232
|
+
|
233
|
+
rubyforge_project:
|
234
|
+
rubygems_version: 1.8.15
|
235
|
+
signing_key:
|
236
|
+
specification_version: 3
|
237
|
+
summary: ""
|
238
|
+
test_files: []
|
239
|
+
|