carnivore-http 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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