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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +2 -0
- data/api2cart-daemon.gemspec +29 -0
- data/lib/api2cart/daemon.rb +17 -0
- data/lib/api2cart/daemon/http_message_reader.rb +57 -0
- data/lib/api2cart/daemon/proxy_connection_handler.rb +74 -0
- data/lib/api2cart/daemon/proxy_server.rb +35 -0
- data/lib/api2cart/daemon/version.rb +5 -0
- data/spec/api2cart/daemon/errors_spec.rb +72 -0
- data/spec/api2cart/daemon/happy_path_spec.rb +73 -0
- data/spec/api2cart/daemon/http_message_reader_spec.rb +139 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/mock_remote_server.rb +39 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|