carnivore-http 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 254e80996f94e8fb440fd9e685205764dc4c1d6d
4
- data.tar.gz: 56d1e0aab6e50decb27265752c90cb7490daf033
3
+ metadata.gz: 7f9f4a7936dcc06d41a919c221612c146426790a
4
+ data.tar.gz: 6153beb3e86f2dd7a3bf7eadad967a3fde8826b5
5
5
  SHA512:
6
- metadata.gz: 92ef01935a6852996c57cb73b290d67d9ecae93f0b4cff4b96cbc98e6fc25ef2fc31e57ba4c168eb89889055a2615784ef7a4bbfa909f226d7b24e1364593cce
7
- data.tar.gz: 0e1e6bea0f6e36e5d305c720aaf83add3c420c61fd6bd6b3b460835096f7b4bf211160ff6a380e76f54f3b8bdea00a120f5b4f8a2696b76293dd1030981f456b
6
+ metadata.gz: 459ee7c23877b7638eadf34b397dcfacec31237b7b6543ed0aaf7567ac26d155a87a422a1ea309bc7f09aaff0f82702321d51ab25a08e1e8c129f9042172697a
7
+ data.tar.gz: 029e4ebfc459957bb46db5fc1f3590efa1126d3dedb0e4f2831b70cb480860d4a33d4675d5a87acdd798f00451d3276a0a953c8e181f7ffdbc03072fa3480f7a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # V0.2.0
2
+ * Add support for message re-delivery and local persistence
3
+ * Add support for authorization
4
+ * Add HTTPS support
5
+ * Add `:http_path` source
6
+
1
7
  # v0.1.8
2
8
  * Add better message body handling
3
9
  * Update DSL inclusion on subclasses
data/Gemfile.lock CHANGED
@@ -1,40 +1,60 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- carnivore-http (0.1.5)
4
+ carnivore-http (0.1.9)
5
5
  blockenspiel
6
6
  carnivore (>= 0.1.8)
7
+ htauth
7
8
  reel (~> 0.5.0)
8
9
 
9
10
  GEM
10
11
  remote: https://rubygems.org/
11
12
  specs:
13
+ attribute_struct (0.2.8)
12
14
  blockenspiel (0.4.5)
13
- carnivore (0.2.2)
15
+ bogo (0.1.6)
16
+ hashie
17
+ multi_json
18
+ bogo-config (0.1.6)
19
+ attribute_struct
20
+ bogo (>= 0.1.4, < 1.0)
21
+ multi_json
22
+ multi_xml
23
+ carnivore (0.3.4)
24
+ bogo-config
14
25
  celluloid
15
26
  hashie
16
- mixlib-config
17
27
  multi_json
18
- celluloid (0.15.2)
19
- timers (~> 1.1.0)
20
- celluloid-io (0.15.0)
21
- celluloid (>= 0.15.0)
22
- nio4r (>= 0.5.0)
23
- hashie (2.1.1)
24
- http (0.6.0)
28
+ celluloid (0.16.0)
29
+ timers (~> 4.0.0)
30
+ celluloid-io (0.16.2)
31
+ celluloid (>= 0.16.0)
32
+ nio4r (>= 1.1.0)
33
+ form_data (0.1.0)
34
+ hashie (3.3.2)
35
+ highline (1.6.21)
36
+ hitimes (1.2.2)
37
+ hitimes (1.2.2-java)
38
+ htauth (1.1.0)
39
+ highline (~> 1.6)
40
+ http (0.7.1)
41
+ form_data (~> 0.1.0)
25
42
  http_parser.rb (~> 0.6.0)
26
43
  http_parser.rb (0.6.0)
27
- mixlib-config (2.1.0)
28
- multi_json (1.10.0)
29
- nio4r (1.0.0)
44
+ http_parser.rb (0.6.0-java)
45
+ multi_json (1.10.1)
46
+ multi_xml (0.5.5)
47
+ nio4r (1.1.0)
48
+ nio4r (1.1.0-java)
30
49
  reel (0.5.0)
31
50
  celluloid (>= 0.15.1)
32
51
  celluloid-io (>= 0.15.0)
33
52
  http (>= 0.6.0.pre)
34
53
  http_parser.rb (>= 0.6.0)
35
54
  websocket_parser (>= 0.1.6)
36
- timers (1.1.0)
37
- websocket_parser (0.1.6)
55
+ timers (4.0.1)
56
+ hitimes
57
+ websocket_parser (1.0.0)
38
58
 
39
59
  PLATFORMS
40
60
  java
data/README.md CHANGED
@@ -6,30 +6,77 @@ Provides HTTP `Carnivore::Source`
6
6
 
7
7
  ## HTTP
8
8
 
9
+ All requests are processed via single source
10
+
9
11
  ```ruby
10
12
  require 'carnivore'
11
13
  require 'carnivore-http'
12
14
 
13
15
  Carnivore.configure do
14
16
  source = Carnivore::Source.build(
15
- :type => :http, :args => {:port => 8080}
17
+ :type => :http,
18
+ :args => {
19
+ :port => 8080
20
+ }
16
21
  )
17
22
  end
18
23
  ```
19
24
 
20
25
  ## HTTP with configured end points
21
26
 
27
+ All point builder definitions are hooked into source:
28
+
22
29
  ```ruby
23
30
  require 'carnivore'
24
31
  require 'carnivore-http'
25
32
 
26
33
  Carnivore.configure do
27
34
  source = Carnivore::Source.build(
28
- :type => :http_endpoints, :args => {:auto_respond => false}
35
+ :type => :http_endpoints,
36
+ :args => {
37
+ :auto_respond => false
38
+ }
29
39
  )
30
40
  end.start!
31
41
  ```
32
42
 
43
+ ## HTTP paths
44
+
45
+ Multiple sources share same listener, and incoming messages
46
+ are matched based on HTTP method + path
47
+
48
+ ```ruby
49
+ require 'carnivore'
50
+ require 'carnivore-http'
51
+
52
+ Carnivore.configure do
53
+ source = Carnivore::Source.build(
54
+ :type => :http_paths,
55
+ :args => {
56
+ :port => 8080,
57
+ :path => '/test',
58
+ :method => 'get'
59
+ }
60
+ )
61
+ end
62
+ ```
63
+
64
+ ## Available options for `:args`
65
+
66
+ * `:bind` address to bind
67
+ * `:port` port to listen
68
+ * `:auto_respond` confirm request immediately
69
+ * `:ssl` ssl configuration
70
+ * `:cert` path to cert file
71
+ * `:key` path to key file
72
+ * `:authorization` access restrictors
73
+ * `:allowed_origins` list of IP or IP ranges
74
+ * `:htpasswd` htpasswd for authentication
75
+ * `:credentials` username/password key pair for authentication
76
+ * `:valid_on` 'any' match any restrictor, 'all' match all restrictors
77
+ * `:endpoint` specific uri to transmit (can include auth + path)
78
+ * `:method` HTTP method for transmission
79
+
33
80
  # Info
34
81
  * Carnivore: https://github.com/carnivore-rb/carnivore
35
82
  * Repository: https://github.com/carnivore-rb/carnivore-http
@@ -13,5 +13,6 @@ Gem::Specification.new do |s|
13
13
  s.add_dependency 'carnivore', '>= 0.1.8'
14
14
  s.add_dependency 'reel', '~> 0.5.0'
15
15
  s.add_dependency 'blockenspiel'
16
+ s.add_dependency 'htauth'
16
17
  s.files = Dir['**/*']
17
18
  end
@@ -1,143 +1,37 @@
1
- require 'reel'
2
- require 'tempfile'
3
- require 'carnivore/source'
4
- require 'carnivore-http/utils'
1
+ require 'carnivore-http'
5
2
 
6
3
  module Carnivore
7
4
  class Source
8
-
9
- # Carnivore HTTP source
10
- class Http < Source
11
-
12
- include Carnivore::Http::Utils::Params
13
-
14
- # @return [Hash] source arguments
15
- attr_reader :args
16
-
17
- # Setup the source
18
- #
19
- # @params args [Hash] setup arguments
20
- def setup(args={})
21
- @args = default_args(args)
22
- end
23
-
24
- # Default configuration arguments. If hash is provided, it
25
- # will be merged into the default arguments.
26
- #
27
- # @param args [Hash]
28
- # @return [Hash]
29
- def default_args(args={})
30
- Smash.new(
31
- :bind => '0.0.0.0',
32
- :port => '3000',
33
- :auto_respond => true
34
- ).merge(args)
35
- end
36
-
37
- # Tranmit message. The transmission can be a response
38
- # back to an open connection, or a request to a remote
39
- # source (remote carnivore-http source generally)
40
- #
41
- # @param message [Object] message to transmit
42
- # @param extras [Object] argument list
43
- def transmit(message, *extra)
44
- options = extra.detect{|x| x.is_a?(Hash)} || {}
45
- orig = extra.detect{|x| x.is_a?(Carnivore::Message)}
46
- con = options[:connection]
47
- if(orig && con.nil?)
48
- con = orig[:message][:connection]
49
- end
50
- if(con) # response
51
- payload = message.is_a?(String) ? message : MultiJson.dump(message)
52
- # TODO: add `options` options for marshaling: json/xml/etc
53
- debug "Transmit response type with payload: #{payload}"
54
- con.respond(options[:code] || :ok, payload)
55
- else # request
56
- url = File.join("http://#{args[:bind]}:#{args[:port]}", options[:path].to_s)
57
- method = (options[:method] || :post).to_sym
58
- if(options[:headers])
59
- base = HTTP.with_headers(options[:headers])
60
- else
61
- base = HTTP
62
- end
63
- payload = message.is_a?(String) ? message : MultiJson.dump(message)
64
- debug "Transmit request type with payload: #{payload}"
65
- base.send(method, url, :body => payload)
66
- end
67
- end
68
-
69
- # Confirm processing of message
70
- #
71
- # @param message [Carnivore::Message]
72
- # @param args [Hash]
73
- # @option args [Symbol] :code return code
74
- def confirm(message, args={})
75
- code = args.delete(:code) || :ok
76
- args[:response_body] = 'Thanks' if code == :ok && args.empty?
77
- debug "Confirming #{message} with: Code: #{code.inspect} Args: #{args.inspect}"
78
- message[:message][:request].respond(code, args[:response_body] || args)
79
- end
5
+ class Http < HttpSource
80
6
 
81
7
  # Process requests
82
8
  def process(*process_args)
83
- srv = Reel::Server::HTTP.supervise(args[:bind], args[:port]) do |con|
84
- con.each_request do |req|
85
- begin
86
- msg = build_message(con, req)
87
- callbacks.each do |name|
88
- c_name = callback_name(name)
89
- debug "Dispatching #{msg} to callback<#{name} (#{c_name})>"
90
- callback_supervisor[c_name].call(msg)
9
+ unless(@processing)
10
+ @processing = true
11
+ srv = build_listener do |con|
12
+ con.each_request do |req|
13
+ begin
14
+ msg = build_message(con, req)
15
+ msg = format(msg)
16
+ if(authorized?(msg))
17
+ callbacks.each do |name|
18
+ c_name = callback_name(name)
19
+ debug "Dispatching #{msg} to callback<#{name} (#{c_name})>"
20
+ callback_supervisor[c_name].call(msg)
21
+ end
22
+ req.respond(:ok, 'So long, and thanks for all the fish!') if args[:auto_respond]
23
+ else
24
+ req.respond(:unauthorized, 'You are not authorized to perform requested action!')
25
+ end
26
+ rescue => e
27
+ req.respond(:bad_request, "Failed to process request -> #{e}")
91
28
  end
92
- req.respond(:ok, 'So long, and thanks for all the fish!') if args[:auto_respond]
93
- rescue => e
94
- req.respond(:bad_request, "Failed to process request -> #{e}")
95
29
  end
96
30
  end
97
- end
98
- end
99
-
100
- # Size limit for inline body
101
- BODY_TO_FILE_SIZE = 1024 * 10 # 10K
102
-
103
- # Build message hash from request
104
- #
105
- # @param con [Reel::Connection]
106
- # @param req [Reel::Request]
107
- # @return [Hash]
108
- # @note
109
- # if body size is greater than BODY_TO_FILE_SIZE
110
- # the body will be a temp file instead of a string
111
- def build_message(con, req)
112
- msg = Smash.new(
113
- :request => req,
114
- :headers => Smash[
115
- req.headers.map{ |k,v| [k.downcase.tr('-', '_'), v]}
116
- ],
117
- :connection => con,
118
- :query => parse_query_string(req.query_string)
119
- )
120
- if(msg[:headers][:content_type] == 'application/json')
121
- msg[:body] = MultiJson.load(
122
- req.body.to_s
123
- )
124
- elsif(msg[:headers][:content_type] == 'application/x-www-form-urlencoded')
125
- msg[:body] = parse_query_string(
126
- req.body.to_s
127
- )
128
- if(msg[:body].size == 1 && msg[:body].values.first.is_a?(Array) && msg[:body].values.first.empty?)
129
- msg[:body] = msg[:body].keys.first
130
- end
131
- elsif(msg[:headers][:content_length].to_i > BODY_TO_FILE_SIZE)
132
- msg[:body] = Tempfile.new('carnivore-http')
133
- while((chunk = req.body.readpartial(2048)))
134
- msg[:body] << chunk
135
- end
136
- msg[:body].rewind
31
+ true
137
32
  else
138
- msg[:body] = req.body.to_s
33
+ false
139
34
  end
140
- format(msg)
141
35
  end
142
36
 
143
37
  end
@@ -76,28 +76,39 @@ module Carnivore
76
76
  set_points
77
77
  end
78
78
 
79
- # Start processing
80
- def connect
81
- info 'Override initialization process startup. Force enabling processing.'
82
- async.process
79
+ # Always auto start
80
+ def auto_process?
81
+ true
83
82
  end
84
83
 
85
84
  # Process requests
86
85
  def process(*process_args)
87
- srv = Reel::Server::HTTP.supervise(args[:bind], args[:port]) do |con|
88
- con.each_request do |req|
89
- begin
90
- msg = build_message(con, req)
91
- unless(@points.deliver(msg))
92
- warn "No match found for request: #{msg}"
93
- req.respond(:ok, 'So long, and thanks for all the fish!')
86
+ unless(processing)
87
+ @processing = true
88
+ srv = build_listener do |con|
89
+ con.each_request do |req|
90
+ begin
91
+ msg = build_message(con, req)
92
+ msg = format(msg)
93
+ if(authorized?(msg))
94
+ unless(@points.deliver(msg))
95
+ warn "No match found for request: #{msg} (path: #{msg[:message][:request].url})"
96
+ debug "Unmatched message (#{msg}): #{msg.inspect}"
97
+ req.respond(:not_found, 'So long, and thanks for all the fish!')
98
+ end
99
+ else
100
+ req.respond(:unauthorized, 'You are not authorized to perform requested action!')
101
+ end
102
+ rescue => e
103
+ error "Failed to process message: #{e.class} - #{e}"
104
+ debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
105
+ req.respond(:bad_request, 'Failed to process request')
94
106
  end
95
- rescue => e
96
- error "Failed to process message: #{e.class} - #{e}"
97
- debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
98
- req.respond(:bad_request, 'Failed to process request')
99
107
  end
100
108
  end
109
+ true
110
+ else
111
+ false
101
112
  end
102
113
  end
103
114
 
@@ -0,0 +1,99 @@
1
+ require 'bogo'
2
+ require 'carnivore-http/http'
3
+
4
+ module Carnivore
5
+ class Source
6
+
7
+ # Carnivore HTTP paths
8
+ class HttpPaths < HttpSource
9
+
10
+ finalizer :halt_listener
11
+ include Bogo::Memoization
12
+
13
+ # @return [String] end point path
14
+ attr_reader :http_path
15
+ # @return [Symbol] http method
16
+ attr_reader :http_method
17
+
18
+ # Kill listener on shutdown
19
+ def halt_listener
20
+ listener = memoize("#{args[:bind]}-#{args[:port]}", :global){ nil }
21
+ if(listener && listener.alive?)
22
+ listener.terminate
23
+ end
24
+ unmemoize("#{args[:bind]}-#{args[:port]}", :global)
25
+ unmemoize("#{args[:bind]}-#{args[:port]}-queues", :global)
26
+ end
27
+
28
+ # Setup message queue for source
29
+ def setup(*_)
30
+ @http_path = args.fetch(:path, '/')
31
+ @http_method = args.fetch(:method, 'get').to_s.downcase.to_sym
32
+ if(message_queues[queue_key])
33
+ raise ArgumentError.new "Conflicting HTTP path source provided! path: #{http_path} method: #{http_method}"
34
+ else
35
+ message_queues[queue_key] = Queue.new
36
+ end
37
+ super
38
+ end
39
+
40
+ # Setup the HTTP listener source
41
+ def connect
42
+ start_listener!
43
+ end
44
+
45
+ # @return [Queue] Message queue
46
+ def message_queues
47
+ memoize("#{args[:bind]}-#{args[:port]}-queues", :global) do
48
+ Smash.new
49
+ end
50
+ end
51
+
52
+ # @return [String]
53
+ def queue_key
54
+ "#{http_path}-#{http_method}"
55
+ end
56
+
57
+ # @return [Queue]
58
+ def message_queue
59
+ message_queues[queue_key]
60
+ end
61
+
62
+ # Start the HTTP(S) listener
63
+ def start_listener!
64
+ memoize("#{args[:bind]}-#{args[:port]}", :global) do
65
+ build_listener do |con|
66
+ con.each_request do |req|
67
+ begin
68
+ msg = build_message(con, req)
69
+ msg_queue = message_queues["#{req.path}-#{req.method.to_s.downcase}"]
70
+ if(msg_queue)
71
+ if(authorized?(msg))
72
+ msg_queue << msg
73
+ req.respond(:ok, 'So long and thanks for all the fish!')
74
+ else
75
+ req.respond(:unauthorized, 'You are not authorized to perform requested action!')
76
+ end
77
+ else
78
+ req.respond(:not_found, 'Requested path not found!')
79
+ end
80
+ rescue => e
81
+ req.respond(:bad_request, "Failed to process request -> #{e}")
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # @return [Object]
89
+ def receive(*_)
90
+ val = nil
91
+ until(val)
92
+ val = Celluloid::Future.new{ message_queue.pop }.value
93
+ end
94
+ val
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,357 @@
1
+ require 'reel'
2
+ require 'tempfile'
3
+ require 'carnivore/source'
4
+ require 'carnivore-http/utils'
5
+
6
+ module Carnivore
7
+ class Source
8
+
9
+ # Carnivore HTTP source
10
+ class HttpSource < Source
11
+
12
+ trap_exit :retry_delivery_failure
13
+
14
+ include Carnivore::Http::Utils::Params
15
+
16
+ # @return [Hash] source arguments
17
+ attr_reader :args
18
+ # @return [Carnivore::Http::RetryDelivery]
19
+ attr_reader :retry_delivery
20
+ # @return [Array<IPAddr>] allowed request origin addresses
21
+ attr_reader :auth_allowed_origins
22
+ # @return [HTAuth::PasswdFile]
23
+ attr_reader :auth_htpasswd
24
+
25
+ # Setup the source
26
+ #
27
+ # @params args [Hash] setup arguments
28
+ def setup(args={})
29
+ require 'fileutils'
30
+ @args = default_args(args)
31
+ @retry_delivery = Carnivore::Http::RetryDelivery.new(retry_directory)
32
+ self.link retry_delivery
33
+ if(args.get(:authorization, :allowed_origins))
34
+ require 'ipaddr'
35
+ @allowed_origins = [args.get(:authorization, :allowed_origins)].flatten.compact.map do |origin_check|
36
+ IPAddr.new(origin_check)
37
+ end
38
+ end
39
+ if(args.get(:authorization, :htpasswd))
40
+ require 'htauth'
41
+ @auth_htpasswd = HTAuth::PasswdFile.open(
42
+ args.get(:authorization, :htpasswd)
43
+ )
44
+ end
45
+ end
46
+
47
+ # Handle failed retry deliveries
48
+ #
49
+ # @param actor [Object] terminated actor
50
+ # @param reason [Exception] reason for termination
51
+ # @return [NilClass]
52
+ def retry_delivery_failure(actor, reason)
53
+ if(actor == retry_delivery)
54
+ if(reason)
55
+ error "Failed RetryDelivery encountered: #{reason}. Rebuilding."
56
+ @retry_delivery = Carnivore::Http::RetryDelivery.new(retry_directory)
57
+ else
58
+ info 'Encountered RetryDelivery failure. No reason so assuming teardown.'
59
+ end
60
+ else
61
+ error "Unknown actor failure encountered: #{reason}"
62
+ end
63
+ nil
64
+ end
65
+
66
+ # @return [String, NilClass] directory storing failed messages
67
+ def retry_directory
68
+ if(args[:retry_directory])
69
+ FileUtils.mkdir_p(File.join(args[:retry_directory], name.to_s)).first
70
+ end
71
+ end
72
+
73
+ # @return [String, NilClass] cache directory for initial writes
74
+ def retry_write_directory
75
+ base = retry_directory
76
+ if(base)
77
+ FileUtils.mkdir_p(File.join(base, '.write')).first
78
+ end
79
+ end
80
+
81
+ # Default configuration arguments. If hash is provided, it
82
+ # will be merged into the default arguments.
83
+ #
84
+ # @param args [Hash]
85
+ # @return [Hash]
86
+ def default_args(args={})
87
+ Smash.new(
88
+ :bind => '0.0.0.0',
89
+ :port => '3000',
90
+ :auto_respond => true,
91
+ :retry_directory => '/tmp/.carnivore-resend'
92
+ ).merge(args)
93
+ end
94
+
95
+ # Always auto start
96
+ def auto_process?
97
+ true
98
+ end
99
+
100
+ # Message is authorized for processing
101
+ #
102
+ # @param message [Carnivore::Message]
103
+ # @return [TrueClass, FalseClass]
104
+ # @note Authorization is driven via the source configuration.
105
+ # Valid structure looks like:
106
+ # {
107
+ # :type => 'http',
108
+ # :args => {
109
+ # :authorization => {
110
+ # :allowed_origins => ['127.0.0.1', '192.168.0.2', '192.168.6.0/24'],
111
+ # :htpasswd => '/path/to/htpasswd.file',
112
+ # :credentials => {
113
+ # :username1 => 'password1'
114
+ # },
115
+ # :valid_on => :all # or :any
116
+ # }
117
+ # }
118
+ # }
119
+ # When multiple authorization items are provided, the
120
+ # `:valid_on` will define behavior. It will default to `:all`.
121
+ def authorized?(message)
122
+ if(args.fetch(:authorization))
123
+ valid_on = args.fetch(:authorization, :valid_on, :all).to_sym
124
+ case valid_on
125
+ when :all
126
+ allowed_origin?(message) &&
127
+ allowed_htpasswd?(message) &&
128
+ allowed_credentials?(message)
129
+ when :any
130
+ allowed_origin?(message) ||
131
+ allowed_htpasswd?(message) ||
132
+ allowed_credentials?(message)
133
+ when :none
134
+ true
135
+ else
136
+ raise ArgumentError.new "Unknown authorization `:valid_on` provided! Given: #{valid_on}. Allowed: `any` or `all`"
137
+ end
138
+ else
139
+ true
140
+ end
141
+ end
142
+
143
+ # Check if message is allowed based on htpasswd file
144
+ #
145
+ # @param message [Carnivore::Message]
146
+ # @return [TrueClass, FalseClass]
147
+ def allowed_htpasswd?(message)
148
+ if(auth_htpasswd)
149
+ entry = auth_htpasswd.fetch(message[:message][:authentication][:username])
150
+ if(entry)
151
+ entry.authenticated?(message[:message][:authentication][:password])
152
+ else
153
+ false
154
+ end
155
+ else
156
+ true
157
+ end
158
+ end
159
+
160
+ # Check if message is allowed based on config credentials
161
+ #
162
+ # @param message [Carnivore::Message]
163
+ # @return [TrueClass, FalseClass]
164
+ def allowed_credentials?(message)
165
+ if(creds = args.get(:authorization, :credentials))
166
+ creds[message[:message][:authentication][:username]] == message[:message][:authentication][:password]
167
+ else
168
+ true
169
+ end
170
+ end
171
+
172
+ # Check if message is allowed based on origin
173
+ #
174
+ # @param message [Carnivore::Message]
175
+ # @return [TrueClass, FalseClass]
176
+ def allowed_origin?(message)
177
+ if(auth_allowed_origins)
178
+ !!auth_allowed_origins.detect do |allowed_check|
179
+ allowed_check.include?(message[:message][:origin])
180
+ end
181
+ else
182
+ true
183
+ end
184
+ end
185
+
186
+ # Tranmit message. The transmission can be a response
187
+ # back to an open connection, or a request to a remote
188
+ # source (remote carnivore-http source generally)
189
+ #
190
+ # @param message [Object] message to transmit
191
+ # @param extras [Object] argument list
192
+ def transmit(message, *extra)
193
+ options = extra.detect{|x| x.is_a?(Hash)} || {}
194
+ orig = extra.detect{|x| x.is_a?(Carnivore::Message)}
195
+ con = options[:connection]
196
+ if(orig && con.nil?)
197
+ con = orig[:message][:connection]
198
+ end
199
+ if(con) # response
200
+ payload = message.is_a?(String) ? message : MultiJson.dump(message)
201
+ # TODO: add `options` options for marshaling: json/xml/etc
202
+ code = options.fetch(:code, :ok)
203
+ info "Transmit response type with code: #{code}"
204
+ con.respond(code, payload)
205
+ else # request
206
+ if(args[:endpoint])
207
+ url = args[:endpoint]
208
+ else
209
+ url = "http#{'s' if args[:ssl]}://#{args[:bind]}"
210
+ if(args[:port])
211
+ url << ":#{args[:port]}"
212
+ end
213
+ url = URI.join(url, args.fetch(:path, '/')).to_s
214
+ end
215
+ if(options[:path])
216
+ url = URI.join(url, options[:path].to_s)
217
+ end
218
+ method = options.fetch(:method,
219
+ args.fetch(:method, :post)
220
+ ).to_s.downcase.to_sym
221
+ message_id = message.is_a?(Hash) ? message.fetch(:id, Celluloid.uuid) : Celluloid.uuid
222
+ payload = message.is_a?(String) ? message : MultiJson.dump(message)
223
+ info "Transmit request type for Message ID: #{message_id}"
224
+ async.perform_transmission(message_id.to_s, payload, method, url, options.fetch(:headers, {}))
225
+ end
226
+ end
227
+
228
+ # Transmit message to HTTP endpoint
229
+ #
230
+ # @param message_id [String]
231
+ # @param payload [String] serialized payload
232
+ # @param method [Symbol] HTTP method (:get, :post, etc)
233
+ # @param url [String] endpoint URL
234
+ # @param headers [Hash] request headers
235
+ # @return [NilClass]
236
+ def perform_transmission(message_id, payload, method, url, headers={})
237
+ write_for_retry(message_id, payload, method, url, headers)
238
+ retry_delivery.async.attempt_redelivery(message_id)
239
+ nil
240
+ end
241
+
242
+ # Persist message if enabled for send retry
243
+ #
244
+ # @param message_id [String] ID of originating message
245
+ # @param payload [String] serialized payload
246
+ # @param method [Symbol] HTTP method (:get, :post, etc)
247
+ # @param url [String] endpoint URL
248
+ # @param headers [Hash] request headers
249
+ # @return [TrueClass, FalseClass] message persisted
250
+ def write_for_retry(message_id, payload, method, url, headers)
251
+ data = {
252
+ :message_id => message_id,
253
+ :payload => payload,
254
+ :method => method,
255
+ :url => url,
256
+ :headers => headers
257
+ }
258
+ if(retry_directory)
259
+ stage_path = File.join(retry_write_directory, "#{message_id}.json")
260
+ final_path = File.join(retry_directory, File.basename(stage_path))
261
+ File.open(stage_path, 'w+') do |file|
262
+ file.write MultiJson.dump(data)
263
+ end
264
+ FileUtils.move(stage_path, final_path)
265
+ info "Failed message (ID: #{message_id}) persisted for resend"
266
+ true
267
+ end
268
+ end
269
+
270
+ # Confirm processing of message
271
+ #
272
+ # @param message [Carnivore::Message]
273
+ # @param args [Hash]
274
+ # @option args [Symbol] :code return code
275
+ def confirm(message, args={})
276
+ code = args.delete(:code) || :ok
277
+ args[:response_body] = 'Thanks' if code == :ok && args.empty?
278
+ debug "Confirming #{message} with: Code: #{code.inspect} Args: #{args.inspect}"
279
+ message[:message][:request].respond(code, args[:response_body] || args)
280
+ end
281
+
282
+ # Initialize http listener correctly based on configuration
283
+ #
284
+ # @param block [Proc] processing block
285
+ # @return [Reel::Server::HTTP, Reel::Server::HTTPS]
286
+ def build_listener(&block)
287
+ if(args[:ssl])
288
+ ssl_config = Smash.new(args[:ssl][key].dup)
289
+ [:key, :cert].each do |key|
290
+ if(ssl_config[key])
291
+ ssl_config[key] = File.open(ssl_config.delete(key))
292
+ end
293
+ end
294
+ Reel::Server::HTTPS.supervise(args[:bind], args[:port], ssl_config, &block)
295
+ else
296
+ Reel::Server::HTTP.supervise(args[:bind], args[:port], &block)
297
+ end
298
+ end
299
+
300
+ # Size limit for inline body
301
+ BODY_TO_FILE_SIZE = 1024 * 10 # 10K
302
+
303
+ # Build message hash from request
304
+ #
305
+ # @param con [Reel::Connection]
306
+ # @param req [Reel::Request]
307
+ # @return [Hash]
308
+ # @note
309
+ # if body size is greater than BODY_TO_FILE_SIZE
310
+ # the body will be a temp file instead of a string
311
+ def build_message(con, req)
312
+ msg = Smash.new(
313
+ :request => req,
314
+ :headers => Smash[
315
+ req.headers.map{ |k,v| [k.downcase.tr('-', '_'), v]}
316
+ ],
317
+ :connection => con,
318
+ :query => parse_query_string(req.query_string),
319
+ :origin => req.remote_addr,
320
+ :authentication => {}
321
+ )
322
+ if(msg[:headers][:content_type] == 'application/json')
323
+ msg[:body] = MultiJson.load(
324
+ req.body.to_s
325
+ )
326
+ elsif(msg[:headers][:content_type] == 'application/x-www-form-urlencoded')
327
+ msg[:body] = parse_query_string(
328
+ req.body.to_s
329
+ )
330
+ if(msg[:body].size == 1 && msg[:body].values.first.is_a?(Array) && msg[:body].values.first.empty?)
331
+ msg[:body] = msg[:body].keys.first
332
+ end
333
+ elsif(msg[:headers][:content_length].to_i > BODY_TO_FILE_SIZE)
334
+ msg[:body] = Tempfile.new('carnivore-http')
335
+ while((chunk = req.body.readpartial(2048)))
336
+ msg[:body] << chunk
337
+ end
338
+ msg[:body].rewind
339
+ else
340
+ msg[:body] = req.body.to_s
341
+ end
342
+ if(msg[:headers][:authorization])
343
+ user, pass = Base64.urlsafe_decode64(
344
+ msg[:headers][:authorization].split(' ').last
345
+ ).split(':', 2)
346
+ msg[:authentication] = {
347
+ :username => user,
348
+ :password => pass
349
+ }
350
+ end
351
+ msg
352
+ end
353
+
354
+ end
355
+
356
+ end
357
+ end
@@ -0,0 +1,88 @@
1
+ require 'carnivore-http'
2
+
3
+ module Carnivore
4
+ module Http
5
+
6
+ class RetryDelivery
7
+
8
+ include Celluloid
9
+ include Carnivore::Utils::Logging
10
+
11
+ # @return [String] message directory
12
+ attr_reader :message_directory
13
+
14
+ # Create new instance
15
+ #
16
+ # @param directory [String] path to messages
17
+ # @return [self]
18
+ def initialize(directory)
19
+ @message_directory = directory
20
+ every(60){ attempt_redelivery }
21
+ end
22
+
23
+ # Attempt to deliver messages found in message directory
24
+ #
25
+ # @return [TrueClass, FalseClass] attempt was made
26
+ # @note will not attempt if attempt is currently in progress
27
+ def attempt_redelivery(message_id = '*')
28
+ attempt = false
29
+ begin
30
+ unless(@delivering)
31
+ @delivering = true
32
+ attempt = true
33
+ Dir.glob(File.join(message_directory, "#{message_id}.json")).each do |file|
34
+ debug "Redelivery processing: #{file}"
35
+ begin
36
+ args = MultiJson.load(File.read(file)).to_smash
37
+ debug "Restored from file #{file}: #{args.inspect}"
38
+ if(redeliver(args[:message_id], args[:payload], args[:method], args[:url], args[:headers]))
39
+ FileUtils.rm(file)
40
+ end
41
+ rescue => e
42
+ error "Failed to process file (#{file}): #{e.class}: #{e}"
43
+ debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
44
+ end
45
+ end
46
+ end
47
+ rescue => e
48
+ error "Unexpected error encountered during message redelivery! #{e.class}: #{e}"
49
+ debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
50
+ ensure
51
+ @delivering = false
52
+ end
53
+ attempt
54
+ end
55
+
56
+ # Attempt redelivery of message
57
+ #
58
+ # @param message [Carnivore::Message]
59
+ # @param payload [String] serialized payload
60
+ # @param method [Symbol] HTTP method (:get, :post, etc)
61
+ # @param url [String] endpoint URL
62
+ # @param headers [Hash] request headers
63
+ # @return [TrueClass, FalseClass] redelivery was successful
64
+ def redeliver(message_id, payload, method, url, headers)
65
+ begin
66
+ base = headers.empty? ? HTTP : HTTP.with_headers(headers)
67
+ uri = URI.parse(url)
68
+ if(uri.userinfo)
69
+ base = base.basic_auth(:user => uri.user, :pass => uri.password)
70
+ end
71
+ result = base.send(method, url, :body => payload)
72
+ if(result.code < 200 || result.code > 299)
73
+ error "Invalid response code received for #{message_id}: #{result.code} - #{result.reason}"
74
+ false
75
+ else
76
+ info "Successful delivery of message on retry! Message ID: #{message_id}"
77
+ true
78
+ end
79
+ rescue => e
80
+ error "Transmission redelivery failure (Message ID: #{message_id}) - #{e.class}: #{e}"
81
+ debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
82
+ false
83
+ end
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -1,6 +1,6 @@
1
1
  module Carnivore
2
2
  module Http
3
3
  # current library version
4
- VERSION = Gem::Version.new('0.1.8')
4
+ VERSION = Gem::Version.new('0.2.0')
5
5
  end
6
6
  end
@@ -6,10 +6,13 @@ module Carnivore
6
6
  # HTTP namespace
7
7
  module Http
8
8
  autoload :PointBuilder, 'carnivore-http/point_builder'
9
+ autoload :RetryDelivery, 'carnivore-http/retry_delivery'
9
10
  end
10
11
 
11
12
  class Source
12
13
  autoload :Http, 'carnivore-http/http'
14
+ autoload :HttpSource, 'carnivore-http/http_source'
15
+ autoload :HttpPaths, 'carnivore-http/http_paths'
13
16
  autoload :HttpEndpoints, 'carnivore-http/http_endpoints'
14
17
  end
15
18
 
@@ -20,4 +23,5 @@ module Carnivore
20
23
  end
21
24
 
22
25
  Carnivore::Source.provide(:http, 'carnivore-http/http')
26
+ Carnivore::Source.provide(:http_paths, 'carnivore-http/http_paths')
23
27
  Carnivore::Source.provide(:http_endpoints, 'carnivore-http/http_endpoints')
data/test/specs/http.rb CHANGED
@@ -4,62 +4,65 @@ require 'carnivore-http'
4
4
 
5
5
  describe 'Carnivore::Source::Http' do
6
6
 
7
- describe 'Building an HTTP based source' do
8
-
9
- it 'returns the source' do
10
- Carnivore::Source.build(
11
- :type => :http,
12
- :args => {
13
- :name => :http_source,
14
- :bind => '127.0.0.1',
15
- :port => '8705'
16
- }
17
- )
18
- t = Thread.new{ Carnivore.start! }
19
- source_wait
20
- Carnivore::Supervisor.supervisor[:http_source].wont_be_nil
21
- t.terminate
7
+ before do
8
+ MessageStore.init
9
+ Carnivore::Source.build(
10
+ :type => :http,
11
+ :args => {
12
+ :name => :http_source,
13
+ :bind => '127.0.0.1',
14
+ :port => '8705'
15
+ }
16
+ ).add_callback(:store) do |message|
17
+ MessageStore.messages.push(message[:message][:body])
18
+ message.confirm!
22
19
  end
20
+ @runner = Thread.new{ Carnivore.start! }
21
+ source_wait
22
+ end
23
23
 
24
+ after do
25
+ @runner.terminate
24
26
  end
25
27
 
26
28
  describe 'HTTP source based communication' do
29
+
27
30
  before do
28
- MessageStore.init
29
- Carnivore::Source.build(
30
- :type => :http,
31
- :args => {
32
- :name => :http_source,
33
- :bind => '127.0.0.1',
34
- :port => '8705'
35
- }
36
- ).add_callback(:store) do |message|
37
- MessageStore.messages.push(message[:message][:body])
38
- end
39
- @runner = Thread.new{ Carnivore.start! }
40
- source_wait
31
+ MessageStore.messages.clear
41
32
  end
42
33
 
43
- after do
44
- @runner.terminate
34
+ describe 'Building an HTTP based source' do
35
+
36
+ it 'returns the source' do
37
+ Carnivore::Supervisor.supervisor[:http_source].wont_be_nil
38
+ end
39
+
45
40
  end
46
41
 
47
42
  describe 'message transmissions' do
43
+
48
44
  it 'should accept message transmits' do
49
45
  Carnivore::Supervisor.supervisor[:http_source].transmit('test message')
50
46
  end
51
47
 
52
48
  it 'should receive messages' do
53
49
  Carnivore::Supervisor.supervisor[:http_source].transmit('test message 2')
54
- source_wait
50
+ source_wait(2) do
51
+ !MessageStore.messages.empty?
52
+ end
53
+ MessageStore.messages.wont_be_empty
55
54
  MessageStore.messages.pop.must_equal 'test message 2'
56
55
  end
57
56
 
58
57
  it 'should accept http requests' do
59
58
  HTTP.get('http://127.0.0.1:8705/')
60
- source_wait
59
+ source_wait(2) do
60
+ !MessageStore.messages.empty?
61
+ end
62
+ MessageStore.messages.wont_be_empty
61
63
  MessageStore.messages.pop.wont_be_nil
62
64
  end
65
+
63
66
  end
64
67
  end
65
68
 
@@ -0,0 +1,87 @@
1
+ require 'http'
2
+ require 'minitest/autorun'
3
+ require 'carnivore-http'
4
+
5
+
6
+ describe 'Carnivore::Source::Http' do
7
+
8
+ before do
9
+ MessageStore.init
10
+
11
+ unless(@runner)
12
+ Carnivore::Source.build(
13
+ :type => :http_paths,
14
+ :args => {
15
+ :name => :fubar_source,
16
+ :path => '/fubar',
17
+ :method => :post,
18
+ :bind => '127.0.0.1',
19
+ :port => '8706'
20
+ }
21
+ ).add_callback(:store) do |message|
22
+ MessageStore.messages.push(message[:message][:body])
23
+ message.confirm!
24
+ end
25
+ Carnivore::Source.build(
26
+ :type => :http_paths,
27
+ :args => {
28
+ :name => :ohai_source,
29
+ :path => '/ohai',
30
+ :method => :get,
31
+ :bind => '127.0.0.1',
32
+ :port => '8706'
33
+ }
34
+ ).add_callback(:store) do |message|
35
+ MessageStore.messages.push(message[:message][:body])
36
+ message.confirm!
37
+ end
38
+ @runner = Thread.new{ Carnivore.start! }
39
+ source_wait
40
+ end
41
+ end
42
+
43
+ after do
44
+ @runner.terminate
45
+ end
46
+
47
+ describe 'HTTP source based communication' do
48
+
49
+ before do
50
+ MessageStore.messages.clear
51
+ end
52
+
53
+ describe 'Building an HTTP based source' do
54
+
55
+ it 'returns the sources' do
56
+ Carnivore::Supervisor.supervisor[:fubar_source].wont_be_nil
57
+ Carnivore::Supervisor.supervisor[:ohai_source].wont_be_nil
58
+ end
59
+
60
+ end
61
+
62
+ describe 'message transmissions' do
63
+
64
+ it 'should accept message transmits' do
65
+ Carnivore::Supervisor.supervisor[:fubar_source].transmit('test message')
66
+ Carnivore::Supervisor.supervisor[:ohai_source].transmit('test message')
67
+ end
68
+
69
+ it 'should receive messages' do
70
+ Carnivore::Supervisor.supervisor[:fubar_source].transmit('test message to fubar')
71
+ source_wait(4) do
72
+ !MessageStore.messages.empty?
73
+ end
74
+ MessageStore.messages.wont_be_empty
75
+ MessageStore.messages.pop.must_equal 'test message to fubar'
76
+ Carnivore::Supervisor.supervisor[:ohai_source].transmit('test message to ohai')
77
+ source_wait(4) do
78
+ !MessageStore.messages.empty?
79
+ end
80
+ MessageStore.messages.wont_be_empty
81
+ MessageStore.messages.pop.must_equal 'test message to ohai'
82
+ end
83
+
84
+ end
85
+ end
86
+
87
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carnivore-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Roberts
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-25 00:00:00.000000000 Z
11
+ date: 2015-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: carnivore
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: htauth
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  description: Carnivore HTTP source
56
70
  email: chrisroberts.code@gmail.com
57
71
  executables: []
@@ -68,11 +82,15 @@ files:
68
82
  - lib/carnivore-http.rb
69
83
  - lib/carnivore-http/http.rb
70
84
  - lib/carnivore-http/http_endpoints.rb
85
+ - lib/carnivore-http/http_paths.rb
86
+ - lib/carnivore-http/http_source.rb
71
87
  - lib/carnivore-http/point_builder.rb
88
+ - lib/carnivore-http/retry_delivery.rb
72
89
  - lib/carnivore-http/utils.rb
73
90
  - lib/carnivore-http/version.rb
74
91
  - test/spec.rb
75
92
  - test/specs/http.rb
93
+ - test/specs/paths.rb
76
94
  homepage: https://github.com/carnivore-rb/carnivore-http
77
95
  licenses:
78
96
  - Apache 2.0