api2cart-daemon 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 02311a7251c3822d41797800ea794496605e5d7f
4
+ data.tar.gz: ba81ae725d85c75a352a769b26106fd509618cb3
5
+ SHA512:
6
+ metadata.gz: 81427570dbeec402e9605beadf0f4cb7396d32388e819cb44a9ac8a6d9449b1eeb09ea32a8dc03466e313d8c039876a0d8366063793c934c744d0a935ddd2f45
7
+ data.tar.gz: 1667fcaf89f99096affec51f666b92ad3a3f47827ab9f8cf75e166e0bf589e7b60699d671dff0c72876b43840f4cb328e376c67bceb68d2156cf1a647e1dc853
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in api2cart-daemon.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Daniel Vartanov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,33 @@
1
+ # API2Cart Daemon
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'api2cart-daemon'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install api2cart-daemon
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ Api2cart::Daemon.run port
25
+ ```
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it ( https://github.com/[my-github-username]/api2cart-daemon/fork )
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'api2cart/daemon/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "api2cart-daemon"
8
+ spec.version = Api2cart::Daemon::VERSION
9
+ spec.authors = ["Daniel Vartanov"]
10
+ spec.email = ["dan@vartanov.net"]
11
+ spec.summary = %q{Anti throttling proxy server for API2Cart requests}
12
+ spec.description = %q{Anti throttling proxy server for API2Cart requests}
13
+ spec.homepage = "https://github.com/DanielVartanov/api2cart-daemon"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'celluloid-io'
22
+ spec.add_dependency 'http_parser.rb'
23
+ spec.add_dependency 'activesupport', '~> 4.1'
24
+ spec.add_dependency 'reel'
25
+ spec.add_development_dependency "bundler", "~> 1.7"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "http"
29
+ end
@@ -0,0 +1,17 @@
1
+ require 'api2cart/daemon/version'
2
+ require 'api2cart/daemon/http_message_reader'
3
+ require 'api2cart/daemon/proxy_server'
4
+ require 'api2cart/daemon/proxy_connection_handler'
5
+ require 'active_support/core_ext/module/delegation'
6
+
7
+ module Api2cart
8
+ module Daemon
9
+ def self.run(port)
10
+ ProxyServer.new.run(port)
11
+ end
12
+
13
+ def self.run_async(port)
14
+ ProxyServer.new.run_async(port)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ require 'ostruct'
2
+ require 'http_parser'
3
+
4
+ module Api2cart::Daemon
5
+ class HTTPMessageReader < Struct.new(:socket)
6
+ READ_BUFFER_SIZE = 16384
7
+
8
+ def initialize(*args)
9
+ super
10
+ initialize_parser!
11
+ end
12
+
13
+ def read_http_message
14
+ message = read_entire_message_from_socket!
15
+ host, port = parse_host_and_port(parser.headers['Host'])
16
+ OpenStruct.new message: message, request_host: host, request_port: port, request_url: parser.request_url
17
+ end
18
+
19
+ protected
20
+
21
+ attr_reader :parser
22
+
23
+ def initialize_parser!
24
+ @complete_http_message_received = false
25
+
26
+ @parser = HTTP::Parser.new
27
+
28
+ parser.on_message_complete = ->() do
29
+ @complete_http_message_received = true
30
+ end
31
+ end
32
+
33
+ def read_next_chunk
34
+ socket.readpartial(READ_BUFFER_SIZE).tap do |chunk|
35
+ parser << chunk
36
+ end
37
+ end
38
+
39
+ def read_entire_message_from_socket!
40
+ ''.tap do |raw_data|
41
+ raw_data << read_next_chunk until complete_http_message_received?
42
+ end
43
+ end
44
+
45
+ def complete_http_message_received?
46
+ !! @complete_http_message_received
47
+ end
48
+
49
+ def parse_host_and_port(host_and_port)
50
+ return nil if host_and_port.nil?
51
+
52
+ host, port = host_and_port.split(':')
53
+ port ||= 80
54
+ [host, port]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,74 @@
1
+ require 'reel/spy'
2
+
3
+ module Api2cart::Daemon
4
+ class ProxyConnectionHandler
5
+ def handle_proxy_connection(client_socket)
6
+ client_socket = Reel::Spy.new client_socket
7
+
8
+ http_message = read_http_message(client_socket)
9
+
10
+ response = if not_proxy_request?(http_message)
11
+ bad_request
12
+ else
13
+ begin
14
+ processed_message = process_client_request(http_message)
15
+ send_request_to_remote_server(http_message.request_host, http_message.request_port, processed_message)
16
+ rescue Exception => e
17
+ puts "! Problem connecting to server: #{e.inspect}"
18
+ internal_server_error(e)
19
+ end
20
+ end
21
+
22
+ send_response_to_client(client_socket, response)
23
+ end
24
+
25
+ protected
26
+
27
+ def process_client_request(client_request)
28
+ parsed_request_url = URI.parse client_request.request_url
29
+ client_request.message.gsub client_request.request_url, parsed_request_url.request_uri
30
+ end
31
+
32
+ def read_http_message(socket)
33
+ HTTPMessageReader.new(socket).read_http_message
34
+ end
35
+
36
+ def send_request_to_remote_server(host, port, request)
37
+ remote_server_socket = TCPSocket.new host, port
38
+ remote_server_socket = Celluloid::IO::TCPSocket.from_ruby_socket remote_server_socket
39
+ remote_server_socket = Reel::Spy.new remote_server_socket
40
+
41
+ remote_server_socket.write request
42
+ read_http_message(remote_server_socket).message
43
+ end
44
+
45
+ def send_response_to_client(client_socket, response)
46
+ client_socket.write response
47
+ client_socket.close
48
+ end
49
+
50
+ def not_proxy_request?(http_message)
51
+ URI.parse(http_message.request_url).host.nil?
52
+ end
53
+
54
+ def internal_server_error(exception)
55
+ <<MESSAGE + exception.inspect
56
+ HTTP/1.1 500 Internal Server Error
57
+ Content-Length: #{exception.inspect.bytesize}
58
+ Connection: close
59
+ Status: 500 Internal Server Error
60
+
61
+ MESSAGE
62
+ end
63
+
64
+ def bad_request
65
+ <<MESSAGE
66
+ HTTP/1.1 400 Bad Request
67
+ Content-Length: 0
68
+ Connection: close
69
+ Status: 400 Bad Request
70
+
71
+ MESSAGE
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,35 @@
1
+ require 'celluloid/io'
2
+
3
+ module Api2cart::Daemon
4
+ class ProxyServer
5
+ include Celluloid::IO
6
+
7
+ def run(port)
8
+ accept_connections bind_proxy_socket(port)
9
+ end
10
+
11
+ def run_async(port)
12
+ async.accept_connections bind_proxy_socket(port)
13
+ end
14
+
15
+ protected
16
+
17
+ def bind_proxy_socket(port)
18
+ TCPServer.new('', port).tap do
19
+ puts "API2Cart Daemon is running at 0.0.0.0:#{port}"
20
+ end
21
+ end
22
+
23
+ def accept_connections(proxy_socket)
24
+ loop { async.handle_connection proxy_socket.accept }
25
+ end
26
+
27
+ def handle_connection(client_socket)
28
+ begin
29
+ ProxyConnectionHandler.new.handle_proxy_connection(client_socket)
30
+ rescue Exception => e
31
+ puts "! Exception: #{e.inspect}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ module Api2cart
2
+ module Daemon
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,72 @@
1
+ describe Api2cart::Daemon, 'happy path' do
2
+ let(:daemon_proxy) { Api2cart::Daemon::ProxyServer.new }
3
+
4
+ before do
5
+ Celluloid.shutdown
6
+ Celluloid.boot
7
+ end
8
+
9
+ before { daemon_proxy.run_async 2048 }
10
+
11
+ after { Celluloid::Actor.kill(daemon_proxy) }
12
+
13
+ shared_examples 'it continues to serve' do
14
+ let(:mock_remote_server) { MockRemoteServer.new(4096, 'I am a mock server') }
15
+
16
+ before { mock_remote_server.run_async }
17
+
18
+ let(:proxy_response) { HTTP.via('localhost', 2048).get('http://localhost:4096').to_s }
19
+
20
+ specify do
21
+ expect(proxy_response).to eq("I am a mock server")
22
+ end
23
+ end
24
+
25
+ context 'when it is a direct request, not proxying one' do
26
+ let!(:response) { HTTP.get('http://localhost:2048') }
27
+
28
+ it 'responds with 400 [Bad Reqest]' do
29
+ expect(response.status).to eq(400)
30
+ end
31
+
32
+ include_examples 'it continues to serve'
33
+ end
34
+
35
+ context 'when request is recursive' do
36
+ let!(:response) { HTTP.via('localhost', 2048).get('http://localhost:2048').to_s }
37
+
38
+ it 'returns nothing' do
39
+ expect(response).to eq('')
40
+ end
41
+
42
+ include_examples 'it continues to serve'
43
+ end
44
+
45
+ context 'when server is unreachable' do
46
+ let!(:response) { HTTP.via('localhost', 2048).get('http://localhost:9999') }
47
+
48
+ it 'responds with 500 [Internal Server Error]' do
49
+ expect(response.status).to eq(500)
50
+ end
51
+
52
+ include_examples 'it continues to serve'
53
+ end
54
+
55
+ context 'when request is malformed' do
56
+ before do
57
+ client_socket = TCPSocket.new 'localhost', 2048
58
+ client_socket.write 'ok google, where is London Eye?'
59
+ end
60
+
61
+ include_examples 'it continues to serve'
62
+ end
63
+
64
+ context 'when client suddenly breaks connection' do
65
+ before do
66
+ client_socket = TCPSocket.new 'localhost', 2048
67
+ client_socket.close
68
+ end
69
+
70
+ include_examples 'it continues to serve'
71
+ end
72
+ end
@@ -0,0 +1,73 @@
1
+ describe Api2cart::Daemon, 'happy path' do
2
+ let(:mock_server) { MockRemoteServer.new(4096, 'I am a mock server') }
3
+ let(:daemon_proxy) { Api2cart::Daemon::ProxyServer.new }
4
+
5
+ before do
6
+ Celluloid.shutdown
7
+ Celluloid.boot
8
+ end
9
+
10
+ before do
11
+ mock_server.run_async
12
+ daemon_proxy.run_async 2048
13
+ end
14
+
15
+ after do
16
+ Celluloid::Actor.kill(daemon_proxy)
17
+ Celluloid::Actor.kill(mock_server)
18
+ end
19
+
20
+ shared_examples 'HTTP proxy server' do
21
+ let!(:proxy_response) { HTTP.via('localhost', 2048).get(request_url).to_s }
22
+
23
+ it 'proxies request to remote server' do
24
+ expect(proxy_response).to eq("I am a mock server")
25
+ expect(mock_server.request_log).to eq(expected_request_log)
26
+ end
27
+ end
28
+
29
+ context 'when URL is normalized' do
30
+ let(:request_url) { 'http://localhost:4096/' }
31
+
32
+ let(:expected_request_log) do
33
+ <<EXPECTED_SERVER_LOG
34
+ GET / HTTP/1.1\r
35
+ Host: localhost:4096\r
36
+ User-Agent: RubyHTTPGem/0.6.2\r
37
+ \r
38
+ EXPECTED_SERVER_LOG
39
+ end
40
+
41
+ include_examples 'HTTP proxy server'
42
+ end
43
+
44
+ context 'when URL is not normalized' do
45
+ let(:request_url) { 'http://localhost:4096' }
46
+
47
+ let(:expected_request_log) do
48
+ <<EXPECTED_SERVER_LOG
49
+ GET / HTTP/1.1\r
50
+ Host: localhost:4096\r
51
+ User-Agent: RubyHTTPGem/0.6.2\r
52
+ \r
53
+ EXPECTED_SERVER_LOG
54
+ end
55
+
56
+ include_examples 'HTTP proxy server'
57
+ end
58
+
59
+ context 'when URL contains path and query' do
60
+ let(:request_url) { 'http://localhost:4096/path?key=value&another_key=another_value' }
61
+
62
+ let(:expected_request_log) do
63
+ <<EXPECTED_SERVER_LOG
64
+ GET /path?key=value&another_key=another_value HTTP/1.1\r
65
+ Host: localhost:4096\r
66
+ User-Agent: RubyHTTPGem/0.6.2\r
67
+ \r
68
+ EXPECTED_SERVER_LOG
69
+ end
70
+
71
+ include_examples 'HTTP proxy server'
72
+ end
73
+ end
@@ -0,0 +1,139 @@
1
+ describe Api2cart::Daemon::HTTPMessageReader do
2
+ let(:socket) { StringIO.new(message) }
3
+ let(:http_message_reader) { Api2cart::Daemon::HTTPMessageReader.new(socket) }
4
+
5
+ let(:http_message) { http_message_reader.read_http_message }
6
+
7
+ context 'when message is a valid HTTP request' do
8
+ describe 'raw HTTP message' do
9
+ subject { http_message.message }
10
+
11
+ let(:message) do
12
+ <<MESSAGE
13
+ GET http://api.api2cart.com/v1.0/product.count.json HTTP/1.1
14
+ Host: api.api2cart.com
15
+ User-Agent: RubyHTTPGem/0.6.2
16
+
17
+ MESSAGE
18
+ end
19
+
20
+ it 'returns the full message' do
21
+ should == message
22
+ end
23
+
24
+ context 'when message is very long' do
25
+ let(:long_text) { '0' * Api2cart::Daemon::HTTPMessageReader::READ_BUFFER_SIZE * 3 }
26
+
27
+ let(:message) do
28
+ <<HEADERS + long_text
29
+ POST / HTTP/1.1
30
+ Host: localhost:4096
31
+ Content-Length: #{long_text.bytesize}
32
+
33
+ HEADERS
34
+ end
35
+
36
+ it 'parses it all not depending on buffer size' do
37
+ should == message
38
+ end
39
+ end
40
+ end
41
+
42
+ describe 'request host' do
43
+ subject { http_message.request_host }
44
+
45
+ let(:message) do
46
+ <<MESSAGE
47
+ GET http://api.api2cart.com/v1.0/product.count.json HTTP/1.1
48
+ Host: api.api2cart.com
49
+ User-Agent: RubyHTTPGem/0.6.2
50
+
51
+ MESSAGE
52
+ end
53
+
54
+ it 'parses request host from headers' do
55
+ should == 'api.api2cart.com'
56
+ end
57
+ end
58
+
59
+ describe 'request port' do
60
+ subject { http_message.request_port }
61
+
62
+ context 'when port is not specified' do
63
+ let(:message) do
64
+ <<MESSAGE
65
+ GET http://api.api2cart.com/v1.0/product.count.json HTTP/1.1
66
+ Host: api.api2cart.com
67
+ User-Agent: RubyHTTPGem/0.6.2
68
+
69
+ MESSAGE
70
+ end
71
+
72
+ it { should == 80 }
73
+ end
74
+
75
+ context 'when port is specified explicitly' do
76
+ let(:message) do
77
+ <<MESSAGE
78
+ GET http://localhost:4096 HTTP/1.1
79
+ Host: localhost:4096
80
+ User-Agent: RubyHTTPGem/0.6.2
81
+
82
+ MESSAGE
83
+ end
84
+
85
+ it 'parses port from headers' do
86
+ should == '4096'
87
+ end
88
+ end
89
+ end
90
+
91
+ describe 'request url' do
92
+ subject { http_message.request_url }
93
+
94
+ let(:message) do
95
+ <<MESSAGE
96
+ GET http://localhost:4096/path?key=value HTTP/1.1
97
+ Host: localhost:4096
98
+ User-Agent: RubyHTTPGem/0.6.2
99
+
100
+ MESSAGE
101
+ end
102
+
103
+ it 'parses request URL from request line' do
104
+ should == 'http://localhost:4096/path?key=value'
105
+ end
106
+ end
107
+ end
108
+
109
+ context 'when message is a valid HTTP response' do
110
+ subject { http_message.message }
111
+
112
+ let(:message) do
113
+ <<MESSAGE
114
+ HTTP/1.1 200 OK
115
+ Date: Tue, 18 Nov 2014 10:19:56 GMT
116
+ Server: Apache/2.2.27 (Unix) mod_ssl/2.2.27 OpenSSL/1.0.1e-fips mod_bwlimited/1.4 mod_fcgid/2.3.9
117
+ X-Powered-By: PHP/5.3.28
118
+ Expires: Thu, 19 Nov 1981 08:52:00 GMT
119
+ Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
120
+ Pragma: no-cache
121
+ Set-Cookie: PHPSESSID=61cc757b3cd2d0048f1796d8e27b0f34; expires=Tue, 18-Nov-2014 12:20:30 GMT; path=/
122
+ Access-Control-Allow-Origin: *
123
+ Access-Control-Allow-Headers: origin, x-requested-with, content-type
124
+ Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS
125
+ Transfer-Encoding: chunked
126
+ Content-Type: application/json; charset=utf-8
127
+
128
+ 4b\r
129
+ {"return_code" : 0, "return_message": "", "result" : {"products_count":76}}\r
130
+ 0\r
131
+ \r
132
+ MESSAGE
133
+ end
134
+
135
+ it 'returns the full message' do
136
+ should == message
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,14 @@
1
+ require_relative File.join(__dir__, '../lib/api2cart/daemon')
2
+
3
+ require 'rspec/core'
4
+ require 'http'
5
+
6
+ Dir[File.join(__dir__, "support/**/*.rb")].each { |f| require f }
7
+ Dir[File.join(__dir__, "shared_examples/**/*.rb")].sort.each { |f| require f }
8
+
9
+ RSpec.configure do |config|
10
+ config.before do
11
+ Celluloid.shutdown
12
+ Celluloid.boot
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ require 'stringio'
2
+
3
+ class MockRemoteServer < Struct.new(:port, :response)
4
+ include Celluloid::IO
5
+
6
+ def run_async
7
+ server_socket = TCPServer.new '', port
8
+ async.accept_connections server_socket
9
+ end
10
+
11
+ def request_log
12
+ request_logger.string
13
+ end
14
+
15
+ protected
16
+
17
+ def request_logger
18
+ @request_logger ||= StringIO.new
19
+ end
20
+
21
+ def accept_connections(server_socket)
22
+ loop do
23
+ client_socket = server_socket.accept
24
+ request_logger.write client_socket.readpartial(16384)
25
+ client_socket.write compose_http_response
26
+ client_socket.close
27
+ end
28
+ end
29
+
30
+ def compose_http_response
31
+ <<RESPONSE + response
32
+ HTTP/1.1 200 OK
33
+ Content-Type: text/plain
34
+ Content-Length: #{response.bytesize}
35
+ Connection: close
36
+
37
+ RESPONSE
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api2cart-daemon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Vartanov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: celluloid-io
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: http_parser.rb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '4.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '4.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: reel
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'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '1.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '1.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: http
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Anti throttling proxy server for API2Cart requests
126
+ email:
127
+ - dan@vartanov.net
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - .gitignore
133
+ - .rspec
134
+ - Gemfile
135
+ - LICENSE.txt
136
+ - README.md
137
+ - Rakefile
138
+ - api2cart-daemon.gemspec
139
+ - lib/api2cart/daemon.rb
140
+ - lib/api2cart/daemon/http_message_reader.rb
141
+ - lib/api2cart/daemon/proxy_connection_handler.rb
142
+ - lib/api2cart/daemon/proxy_server.rb
143
+ - lib/api2cart/daemon/version.rb
144
+ - spec/api2cart/daemon/errors_spec.rb
145
+ - spec/api2cart/daemon/happy_path_spec.rb
146
+ - spec/api2cart/daemon/http_message_reader_spec.rb
147
+ - spec/spec_helper.rb
148
+ - spec/support/mock_remote_server.rb
149
+ homepage: https://github.com/DanielVartanov/api2cart-daemon
150
+ licenses:
151
+ - MIT
152
+ metadata: {}
153
+ post_install_message:
154
+ rdoc_options: []
155
+ require_paths:
156
+ - lib
157
+ required_ruby_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - '>='
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ requirements: []
168
+ rubyforge_project:
169
+ rubygems_version: 2.4.2
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: Anti throttling proxy server for API2Cart requests
173
+ test_files:
174
+ - spec/api2cart/daemon/errors_spec.rb
175
+ - spec/api2cart/daemon/happy_path_spec.rb
176
+ - spec/api2cart/daemon/http_message_reader_spec.rb
177
+ - spec/spec_helper.rb
178
+ - spec/support/mock_remote_server.rb