right_sharding 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|