sitehub 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +114 -0
  6. data/LICENSE +28 -0
  7. data/README.md +198 -0
  8. data/Rakefile +11 -0
  9. data/lib/sitehub.rb +9 -0
  10. data/lib/sitehub/builder.rb +82 -0
  11. data/lib/sitehub/collection.rb +35 -0
  12. data/lib/sitehub/collection/route_collection.rb +28 -0
  13. data/lib/sitehub/collection/split_route_collection.rb +50 -0
  14. data/lib/sitehub/collection/split_route_collection/split.rb +18 -0
  15. data/lib/sitehub/constants.rb +23 -0
  16. data/lib/sitehub/constants/http_header_keys.rb +25 -0
  17. data/lib/sitehub/constants/rack_http_header_keys.rb +17 -0
  18. data/lib/sitehub/cookie.rb +54 -0
  19. data/lib/sitehub/cookie/attribute.rb +22 -0
  20. data/lib/sitehub/cookie/flag.rb +22 -0
  21. data/lib/sitehub/cookie_rewriting.rb +35 -0
  22. data/lib/sitehub/forward_proxies.rb +50 -0
  23. data/lib/sitehub/forward_proxy.rb +67 -0
  24. data/lib/sitehub/forward_proxy_builder.rb +99 -0
  25. data/lib/sitehub/http_headers.rb +60 -0
  26. data/lib/sitehub/logging.rb +5 -0
  27. data/lib/sitehub/logging/access_logger.rb +74 -0
  28. data/lib/sitehub/logging/error_logger.rb +36 -0
  29. data/lib/sitehub/logging/log_entry.rb +14 -0
  30. data/lib/sitehub/logging/log_stash.rb +10 -0
  31. data/lib/sitehub/logging/log_wrapper.rb +23 -0
  32. data/lib/sitehub/middleware.rb +21 -0
  33. data/lib/sitehub/path_directive.rb +32 -0
  34. data/lib/sitehub/path_directives.rb +21 -0
  35. data/lib/sitehub/request_mapping.rb +43 -0
  36. data/lib/sitehub/resolver.rb +11 -0
  37. data/lib/sitehub/reverse_proxy.rb +53 -0
  38. data/lib/sitehub/rules.rb +13 -0
  39. data/lib/sitehub/string_sanitiser.rb +7 -0
  40. data/lib/sitehub/transaction_id.rb +16 -0
  41. data/lib/sitehub/version.rb +3 -0
  42. data/mem_usage.txt +1584 -0
  43. data/sitehub.gemspec +43 -0
  44. data/spec/basket_spec.rb +30 -0
  45. data/spec/sitehub/builder_spec.rb +203 -0
  46. data/spec/sitehub/collection/route_collection_spec.rb +91 -0
  47. data/spec/sitehub/collection/split_route_collection_spec.rb +111 -0
  48. data/spec/sitehub/collection_spec.rb +40 -0
  49. data/spec/sitehub/cookie/attribute_spec.rb +37 -0
  50. data/spec/sitehub/cookie/flag_spec.rb +27 -0
  51. data/spec/sitehub/cookie_rewriting_spec.rb +67 -0
  52. data/spec/sitehub/cookie_spec.rb +61 -0
  53. data/spec/sitehub/error_handling_spec.rb +21 -0
  54. data/spec/sitehub/forward_proxies_spec.rb +99 -0
  55. data/spec/sitehub/forward_proxy_builder_spec.rb +295 -0
  56. data/spec/sitehub/forward_proxy_spec.rb +138 -0
  57. data/spec/sitehub/http_headers_spec.rb +71 -0
  58. data/spec/sitehub/integration_spec.rb +21 -0
  59. data/spec/sitehub/logging/access_logger_spec.rb +127 -0
  60. data/spec/sitehub/logging/error_logger_spec.rb +80 -0
  61. data/spec/sitehub/logging/log_entry_spec.rb +34 -0
  62. data/spec/sitehub/logging/log_stash_spec.rb +21 -0
  63. data/spec/sitehub/logging/log_wrapper_spec.rb +33 -0
  64. data/spec/sitehub/middleware_spec.rb +69 -0
  65. data/spec/sitehub/path_directive_spec.rb +50 -0
  66. data/spec/sitehub/path_directives_spec.rb +45 -0
  67. data/spec/sitehub/request_mapping_spec.rb +71 -0
  68. data/spec/sitehub/resolver_spec.rb +15 -0
  69. data/spec/sitehub/reverse_proxy_spec.rb +105 -0
  70. data/spec/sitehub/transaction_id_spec.rb +28 -0
  71. data/spec/sitehub_spec.rb +19 -0
  72. data/spec/spec_helper.rb +26 -0
  73. data/spec/support/patch/rack/response.rb +25 -0
  74. data/spec/support/shared_contexts/async_context.rb +69 -0
  75. data/spec/support/shared_contexts/middleware_context.rb +51 -0
  76. data/spec/support/shared_contexts/rack_test_context.rb +12 -0
  77. data/spec/support/shared_contexts/sitehub_context.rb +25 -0
  78. data/spec/support/silent_warnings.rb +5 -0
  79. metadata +359 -0
@@ -0,0 +1,99 @@
1
+ require 'uuid'
2
+ require_relative 'collection/split_route_collection'
3
+ require_relative 'rules'
4
+ require_relative 'resolver'
5
+ require_relative 'collection/route_collection'
6
+ require_relative 'middleware'
7
+
8
+ class SiteHub
9
+
10
+ class ForwardProxyBuilder
11
+ include Middleware
12
+ include Rules, Resolver
13
+
14
+ class InvalidDefinitionException < Exception;
15
+ end
16
+
17
+ attr_reader :mapped_path, :default_proxy, :routes, :middlewares, :splits, :sitehub_cookie_name
18
+
19
+ def initialize(url: nil, mapped_path:, rule:nil, sitehub_cookie_name: nil, &block)
20
+ @mapped_path = mapped_path
21
+ @middlewares = []
22
+ @splits = Collection::SplitRouteCollection.new
23
+ @routes = Collection::RouteCollection.new
24
+ @sitehub_cookie_name = sitehub_cookie_name
25
+ rule(rule) if rule
26
+ default(url: url) if url
27
+ if block
28
+ instance_eval(&block)
29
+ raise InvalidDefinitionException unless valid?
30
+ end
31
+ end
32
+
33
+ def valid?
34
+ return true if @default_proxy
35
+ endpoints.valid?
36
+ end
37
+
38
+ def endpoints collection=nil
39
+ return @endpoints || Collection::RouteCollection.new unless collection
40
+
41
+ raise InvalidDefinitionException, 'you cant register routes and splits at the same level' if @endpoints && @endpoints != collection
42
+ @endpoints = collection
43
+ end
44
+
45
+ def split(percentage:, url:, label:, &block)
46
+ endpoints(splits)
47
+ endpoints.add label.to_sym, forward_proxy(label: label, url: url), percentage
48
+ end
49
+
50
+ def route url:nil, label:nil, rule: nil, &block
51
+ endpoints(routes)
52
+
53
+ if block
54
+ raise InvalidDefinitionException, 'rule must be specified when supplying a block'unless rule
55
+ builder = self.class.new(mapped_path: mapped_path, rule: rule, &block).build
56
+ endpoints.add UUID.generate(:compact), builder
57
+ else
58
+ endpoints.add label.to_sym, forward_proxy(url: url, label: label, rule: rule)
59
+ end
60
+ end
61
+
62
+ def default(url:)
63
+ @default_proxy = forward_proxy(label: :default, url: url)
64
+ end
65
+
66
+ def sitehub_cookie_path path=nil
67
+ return @sitehub_cookie_path unless path
68
+ @sitehub_cookie_path = path
69
+ end
70
+
71
+
72
+ def build
73
+ endpoints.transform do |proxy|
74
+ apply_middleware(proxy).tap do |wrapped_proxy|
75
+ wrapped_proxy.extend(Rules)
76
+ wrapped_proxy.extend(Resolver) unless wrapped_proxy.is_a?(Resolver)
77
+ wrapped_proxy.rule(proxy.rule)
78
+ end
79
+ end
80
+ @default_proxy = apply_middleware(default_proxy) if default_proxy
81
+ self
82
+ end
83
+
84
+ def resolve(id: nil, env:)
85
+ id = id.to_s.to_sym
86
+ endpoints[id] || endpoints.resolve(env: env) || default_proxy
87
+ end
88
+
89
+ def == other
90
+ other.is_a?(ForwardProxyBuilder) && self.default_proxy == other.default_proxy && self.endpoints == other.endpoints
91
+ end
92
+
93
+
94
+ def forward_proxy(label:, url:, rule: nil)
95
+ ForwardProxy.new(url: url, id: label.to_sym, mapped_path: mapped_path, sitehub_cookie_path: sitehub_cookie_path, sitehub_cookie_name: sitehub_cookie_name, rule: rule)
96
+ end
97
+ end
98
+
99
+ end
@@ -0,0 +1,60 @@
1
+ require 'sitehub/constants'
2
+ class SiteHub
3
+ module HttpHeaders
4
+ include Constants::HttpHeaderKeys
5
+ include Constants
6
+
7
+ HTTP_OR_SSL_PORT = /:80(?!\d+)|:443/
8
+ HTTP_PREFIX = /^HTTP_/
9
+ RACK_HTTP_HEADER_ID = /#{HTTP_PREFIX.source}[A-Z_]+$/
10
+ COMMAND_FOLLOWED_BY_SPACES = /,\s+/
11
+
12
+ def split_field(f)
13
+ f ? f.split(COMMAND_FOLLOWED_BY_SPACES).collect { |i| i.downcase } : []
14
+ end
15
+
16
+ def sanitise_headers(src)
17
+
18
+ sanitised_headers = {}
19
+
20
+ connections = split_field(src[CONNECTION_HEADER])
21
+ src.each do |key, value|
22
+ key = key.downcase.gsub(UNDERSCORE, HYPHEN)
23
+ if HopByHop.member?(key) ||
24
+ connections.member?(key) ||
25
+ ShouldNotTransfer.member?(key)
26
+ next
27
+ end
28
+ sanitised_headers[key] = value
29
+ end
30
+
31
+ sanitised_headers[LOCATION_HEADER].gsub!(HTTP_OR_SSL_PORT, EMPTY_STRING) if sanitised_headers[LOCATION_HEADER]
32
+
33
+ sanitised_headers
34
+ end
35
+
36
+
37
+
38
+ def extract_http_headers(env)
39
+ headers = env.reject do |k, v|
40
+ !RackHttpHeaderKeys::HTTP_HEADER_FILTER_EXCEPTIONS.include?(k.to_s.upcase) && (!(RACK_HTTP_HEADER_ID === k) || v.nil?)
41
+ end.map do |k, v|
42
+ [reconstruct_header_name(k), v]
43
+ end.inject(Rack::Utils::HeaderHash.new) do |hash, k_v|
44
+ k, v = k_v
45
+ hash[k] = v
46
+ hash
47
+ end
48
+
49
+ x_forwarded_for = (headers[X_FORWARDED_FOR_HEADER].to_s.split(COMMAND_FOLLOWED_BY_SPACES) << env[RackHttpHeaderKeys::REMOTE_ADDRESS_ENV_KEY]).join(COMMA_WITH_SPACE)
50
+
51
+ headers.merge!(X_FORWARDED_FOR_HEADER => x_forwarded_for)
52
+ end
53
+
54
+
55
+
56
+ def reconstruct_header_name(name)
57
+ name.sub(HTTP_PREFIX, EMPTY_STRING).gsub(UNDERSCORE, HYPHEN)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'logging/access_logger'
2
+ require_relative 'logging/error_logger'
3
+ require_relative 'logging/log_entry'
4
+ require_relative 'logging/log_stash'
5
+ require_relative 'logging/log_wrapper'
@@ -0,0 +1,74 @@
1
+ require 'logger'
2
+ require 'rack/commonlogger'
3
+ require_relative 'log_wrapper'
4
+ require 'sitehub/constants'
5
+
6
+ #Very heavily based on Rack::CommonLogger
7
+ class SiteHub
8
+ module Logging
9
+ class AccessLogger
10
+ attr_reader :logger, :start_time
11
+
12
+ include Constants
13
+
14
+ FORMAT = %{%s - %s [%s] transaction_id:%s: "%s %s%s => %s %s" %d %s %0.4f\n}.freeze
15
+ PATH_INFO = RackHttpHeaderKeys::PATH_INFO
16
+ REQUEST_METHOD = RackHttpHeaderKeys::REQUEST_METHOD
17
+ SCRIPT_NAME = RackHttpHeaderKeys::SCRIPT_NAME
18
+ QUERY_STRING = RackHttpHeaderKeys::QUERY_STRING
19
+ X_FORWARDED_FOR = RackHttpHeaderKeys::X_FORWARDED_FOR
20
+ REMOTE_ADDR = RackHttpHeaderKeys::REMOTE_ADDR
21
+ HTTP_VERSION = RackHttpHeaderKeys::HTTP_VERSION
22
+ REMOTE_USER = RackHttpHeaderKeys::REMOTE_USER
23
+ TRANSACTION_ID = RackHttpHeaderKeys::TRANSACTION_ID
24
+ CONTENT_LENGTH = HttpHeaderKeys::CONTENT_LENGTH
25
+ ZERO_STRING = '0'
26
+ STATUS_RANGE = 0..3
27
+
28
+ def initialize app, logger = ::Logger.new(STDOUT)
29
+ @app = app
30
+ @logger = LogWrapper.new(logger)
31
+ end
32
+
33
+ def call env
34
+ start_time = Time.now
35
+
36
+ @app.call(env).tap do |response|
37
+ status, headers, body = response.to_a
38
+ log env, status, headers, env[REQUEST_MAPPING], start_time
39
+ end
40
+ end
41
+
42
+ def log(env, status, header, mapped_request, began_at)
43
+ now = Time.now
44
+ length = extract_content_length(header)
45
+
46
+
47
+ msg = log_template % [
48
+ env[X_FORWARDED_FOR] || env[REMOTE_ADDR] || HYPHEN,
49
+ env[REMOTE_USER] || "-",
50
+ now.strftime(TIME_STAMP_FORMAT),
51
+ env[TRANSACTION_ID],
52
+ env[REQUEST_METHOD],
53
+ env[PATH_INFO],
54
+ env[QUERY_STRING].empty? ? EMPTY_STRING : QUESTION_MARK+env[QUERY_STRING],
55
+ mapped_request.mapped_url.to_s,
56
+ env[HTTP_VERSION],
57
+ status.to_s[STATUS_RANGE],
58
+ length,
59
+ now - began_at]
60
+
61
+ logger.write(msg)
62
+ end
63
+
64
+ def log_template
65
+ FORMAT
66
+ end
67
+
68
+ def extract_content_length(headers)
69
+ value = headers[CONTENT_LENGTH] or return HYPHEN
70
+ value.to_s == ZERO_STRING ? HYPHEN : value
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,36 @@
1
+ require 'logger'
2
+ require 'sitehub/constants'
3
+ require_relative 'log_wrapper'
4
+ require_relative 'log_stash'
5
+ class SiteHub
6
+ module Logging
7
+ class ErrorLogger
8
+ include Constants
9
+ LOG_TEMPLATE = '[%s] ERROR: %s - %s'
10
+
11
+
12
+ attr_reader :logger
13
+
14
+ def initialize(app, logger = Logger.new(STDERR))
15
+ @app = app
16
+ @logger = LogWrapper.new(logger)
17
+ end
18
+
19
+ def call env
20
+ env[ERRORS] ||= LogStash.new
21
+ @app.call(env).tap do
22
+ unless env[ERRORS].empty?
23
+ messages = env[ERRORS].collect { |log_entry| log_message(error: log_entry.message, transaction_id: env[RackHttpHeaderKeys::TRANSACTION_ID]) }
24
+
25
+ logger.write(messages.join(NEW_LINE))
26
+ end
27
+ end
28
+ end
29
+
30
+ def log_message(error:, transaction_id:)
31
+ LOG_TEMPLATE % [Time.now.strftime(TIME_STAMP_FORMAT), transaction_id, error]
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ class SiteHub
2
+ module Logging
3
+ class LogEntry
4
+ attr_reader :message, :time
5
+ def initialize message, time=Time.now
6
+ @message, @time = message, time
7
+ end
8
+
9
+ def == other
10
+ other.is_a?(LogEntry) && self.message == other.message && self.time == other.time
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'log_entry'
2
+ class SiteHub
3
+ module Logging
4
+ class LogStash < Array
5
+ def << message
6
+ super(LogEntry.new(message))
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ class SiteHub
2
+ module Logging
3
+ class LogWrapper
4
+ attr_reader :logger
5
+
6
+ def initialize logger
7
+ @logger = logger
8
+ end
9
+
10
+ def write msg
11
+ if logger.respond_to?(:<<)
12
+ logger << msg
13
+ elsif logger.respond_to?(:write)
14
+ logger.write(msg)
15
+ end
16
+ end
17
+
18
+ def == other
19
+ other.is_a?(LogWrapper) && self.logger == other.logger
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ class SiteHub
2
+ module Middleware
3
+ def middlewares
4
+ @middleware ||= []
5
+ end
6
+
7
+ def use(middleware_clazz, *args, &block)
8
+ middlewares << [middleware_clazz, args, block|| proc{}]
9
+ end
10
+
11
+ def apply_middleware(forward_proxy)
12
+ middlewares.reverse.inject(forward_proxy) do |app, middleware_def|
13
+ middleware = middleware_def[0]
14
+ args = middleware_def[1] || []
15
+ block = middleware_def[2] || proc {}
16
+
17
+ middleware.new(app, *args, &block)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ class SiteHub
2
+ class PathDirective
3
+ attr_reader :matcher, :path_template
4
+
5
+ def initialize matcher, path_template
6
+ @matcher, @path_template = matcher, path_template
7
+ end
8
+
9
+ def match? url
10
+ !!matcher.match(url)
11
+ end
12
+
13
+ def path_template
14
+ @path_template.dup
15
+ end
16
+
17
+ def apply url
18
+ url_components = matcher.match(url).captures
19
+
20
+ path_template.tap do |p|
21
+ url_components.each_with_index do |m, index|
22
+ p.gsub!(RequestMapping::CAPTURE_GROUP_REFERENCE % (index+1), m)
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ def == other
29
+ other.is_a?(PathDirective) && self.matcher == other.matcher && self.path_template == other.path_template
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ require 'sitehub/path_directive'
2
+ class SiteHub
3
+ class PathDirectives < Array
4
+ def initialize map={}
5
+ enriched = map.map do |array|
6
+ matcher,path_template = array.first, array.last
7
+
8
+ matcher = matcher.is_a?(Regexp) ? matcher : %r{#{matcher}}
9
+ PathDirective.new(matcher, path_template)
10
+ end
11
+
12
+ super enriched
13
+ end
14
+
15
+ def find(url)
16
+ super() do |directive|
17
+ directive.match?(url)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ require 'sitehub/constants'
2
+ class SiteHub
3
+ class RequestMapping
4
+ attr_reader :source_url, :mapped_url, :mapped_path
5
+
6
+ BASE_URL_MATCHER = %r{^\w+://[\w+\.-]+(:\d+)?}
7
+ CAPTURE_GROUP_REFERENCE='$%s'
8
+ USER_SUPPLIED_CAPTURE = 1..-1
9
+
10
+ def initialize(source_url:, downstream_url: EMPTY_STRING, mapped_url: EMPTY_STRING, mapped_path:)
11
+ @source_url, @mapped_url = source_url, mapped_url.dup
12
+ @mapped_path = mapped_path.is_a?(Regexp) ? mapped_path : Regexp.new(mapped_path)
13
+ @downstream_url = downstream_url
14
+ end
15
+
16
+ def cookie_path
17
+ if mapped_path.is_a?(Regexp)
18
+ mapped_path.source[/^(.*)?\(/,1].gsub(/\/$/, '')
19
+ end
20
+ end
21
+
22
+
23
+ def computed_uri
24
+ @computed_uri ||= begin
25
+ url_components = url_scanner_regex.match(source_url).captures[USER_SUPPLIED_CAPTURE]
26
+ mapped_url.tap do |url|
27
+ url_components.each_with_index do |match, index|
28
+ url.gsub!(CAPTURE_GROUP_REFERENCE % (index+1), match)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def == other
35
+ other.is_a?(RequestMapping) && source_url == other.source_url && mapped_url == other.mapped_url && mapped_path == other.mapped_path
36
+ end
37
+
38
+ private
39
+ def url_scanner_regex
40
+ %r{#{BASE_URL_MATCHER.source}#{mapped_path.source}}
41
+ end
42
+ end
43
+ end