minhttp 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/README.md +38 -0
- data/Rakefile +11 -0
- data/VERSION +1 -0
- data/cacert.pem +3987 -0
- data/examples/readme_example.rb +16 -0
- data/lib/minhttp.rb +112 -0
- data/lib/ssl_validator.rb +80 -0
- data/test/simple_test.rb +22 -0
- data/test/simple_test.rb~ +10 -0
- metadata +82 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'lib/min_http'
|
2
|
+
|
3
|
+
data = <<-HTTP
|
4
|
+
GET / HTTP/1.0\r
|
5
|
+
Host: www.google.com\r
|
6
|
+
|
7
|
+
HTTP
|
8
|
+
|
9
|
+
EventMachine::run do
|
10
|
+
Http::Min.connect("www.google.com", data) do |raw_response, parsed_response|
|
11
|
+
puts "Received #{parsed_response.status_code} status from Google"
|
12
|
+
puts "First 100 characters of raw HTTP response:"
|
13
|
+
puts raw_response[0..100]
|
14
|
+
EM::stop
|
15
|
+
end
|
16
|
+
end
|
data/lib/minhttp.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'http/parser'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'logger'
|
4
|
+
require_relative "ssl_validator"
|
5
|
+
|
6
|
+
#
|
7
|
+
# The minimal HTTP client
|
8
|
+
# Sends a raw http request (bytes)
|
9
|
+
# Parses the response and provides both the parsed and the raw response
|
10
|
+
# Supports ssl
|
11
|
+
#
|
12
|
+
module Http
|
13
|
+
class Min < EventMachine::Connection
|
14
|
+
|
15
|
+
attr_accessor :host, :ssl, :callback, :request_data
|
16
|
+
|
17
|
+
def self.connections
|
18
|
+
@@connections
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.configure(options={})
|
22
|
+
@@options = options
|
23
|
+
@@logger = options[:logger] || Logger.new(STDOUT)
|
24
|
+
@@connections = 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.configured?
|
28
|
+
class_variable_defined?("@@logger")
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.connect(host, data, port=80, ssl=false, &callback)
|
32
|
+
configure unless configured?
|
33
|
+
|
34
|
+
EventMachine.connect(host, port, self) do |c|
|
35
|
+
# this code runs after 'post_init', before 'connection_completed'
|
36
|
+
c.host = host
|
37
|
+
c.ssl = ssl
|
38
|
+
c.callback = callback
|
39
|
+
c.request_data = data
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def post_init
|
44
|
+
begin
|
45
|
+
@@connections += 1
|
46
|
+
@parser = Http::Parser.new
|
47
|
+
@response_data = ""
|
48
|
+
rescue Exception => e
|
49
|
+
@@logger.error("Error in post_init: #{e}")
|
50
|
+
raise e
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def connection_completed
|
55
|
+
begin
|
56
|
+
start_tls(:verify_peer => true) if @ssl
|
57
|
+
send_data @request_data
|
58
|
+
rescue Exception => e
|
59
|
+
puts "Error in connection_completed: #{e}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def receive_data(data)
|
64
|
+
@response_data << data
|
65
|
+
begin
|
66
|
+
@parser << data
|
67
|
+
rescue HTTP::Parser::Error => e
|
68
|
+
@@logger.warn "Failed to parse: #{data}"
|
69
|
+
raise e
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def unbind
|
74
|
+
begin
|
75
|
+
@@connections -= 1
|
76
|
+
@callback.call(@response_data, @parser)
|
77
|
+
rescue Exception => e
|
78
|
+
@@logger.error("Error in unbind: #{e}")
|
79
|
+
raise e
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Called once per cert received
|
85
|
+
# The certs aren't verified until the handshake is completed
|
86
|
+
#
|
87
|
+
def ssl_verify_peer(cert)
|
88
|
+
begin
|
89
|
+
@certs ||= []
|
90
|
+
@certs << cert unless @certs.include?(cert)
|
91
|
+
true
|
92
|
+
rescue Exception => e
|
93
|
+
@@logger.error("Error in ssl_verify_peer: #{e}")
|
94
|
+
raise e
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# Verify the certs and throw an exception if they are not valid
|
100
|
+
#
|
101
|
+
def ssl_handshake_completed
|
102
|
+
begin
|
103
|
+
return unless @@options[:verify_ssl]
|
104
|
+
close_connection unless Http::SSLValidator.validate(@certs, @host)
|
105
|
+
rescue Exception => e
|
106
|
+
@@logger.error("Error in ssl_handshake_completed: #{e}")
|
107
|
+
raise e
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Http
|
5
|
+
class SSLValidator
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def configure(logger)
|
9
|
+
@@logger = logger || Logger.new(STDOUT)
|
10
|
+
create_store
|
11
|
+
end
|
12
|
+
|
13
|
+
def configured?
|
14
|
+
class_variable_defined?("@@logger")
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Completes the 3 steps to certificate chain verification
|
19
|
+
# Also applies if there is just one cert in the chain, but the last
|
20
|
+
# step won't run
|
21
|
+
#
|
22
|
+
def validate(certs, host)
|
23
|
+
certs = certs.collect { |c| OpenSSL::X509::Certificate.new(c) }
|
24
|
+
@@logger.debug("Verifying certs for #{host}")
|
25
|
+
|
26
|
+
# 1. Verify that the last cert has a valid hostname
|
27
|
+
unless OpenSSL::SSL.verify_certificate_identity(certs.last, host)
|
28
|
+
@@logger.error("Hostname #{host} does not match cert: #{certs.last}")
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
|
32
|
+
# 2. Verify that the first cert can be validated by a root certificate
|
33
|
+
unless @@store.verify(certs.first)
|
34
|
+
@@logger.error("Cert not validated by any of the root certificates in my store: #{certs.first}")
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
|
38
|
+
# 3. Verify that every cert in the chain is validated by the cert after it
|
39
|
+
(certs.length - 1).times do |i|
|
40
|
+
cert_a = certs[i+1]
|
41
|
+
cert_b = certs[i]
|
42
|
+
unless cert_a.verify(cert_b.public_key)
|
43
|
+
@@logger.error("Broken link in certificate chain for #{host} between #{cert_a} and #{cert_b}")
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
#
|
54
|
+
# Create a Ruby OpenSSL certificate store with all the certs
|
55
|
+
# in the given root certificate file. Ruby doesn't supply a clean
|
56
|
+
# way to get the root certs out of a root cert file so I have to split
|
57
|
+
# it manually.
|
58
|
+
#
|
59
|
+
def create_store
|
60
|
+
configure unless configured?
|
61
|
+
|
62
|
+
# Root certs downloaded from: http://curl.haxx.se/ca/cacert.pem
|
63
|
+
ca_file_path = File.join(File.dirname(__FILE__), "../cacert.pem")
|
64
|
+
|
65
|
+
@@store = OpenSSL::X509::Store.new
|
66
|
+
splitter = "END CERTIFICATE-----"
|
67
|
+
File.read(ca_file_path).strip.split(splitter).each do |c|
|
68
|
+
begin
|
69
|
+
c << splitter
|
70
|
+
@@store.add_cert OpenSSL::X509::Certificate.new(c)
|
71
|
+
rescue OpenSSL::X509::CertificateError
|
72
|
+
@@logger.warn "Error loading cert from #{c} from #{ca_file_path}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
@@store
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/test/simple_test.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require_relative "../lib/minhttp"
|
3
|
+
|
4
|
+
class SimpleTest < MiniTest::Unit::TestCase
|
5
|
+
def test_simple_google
|
6
|
+
data = <<-HTTP
|
7
|
+
GET / HTTP/1.0\r
|
8
|
+
Host: www.google.com\r
|
9
|
+
|
10
|
+
HTTP
|
11
|
+
|
12
|
+
EventMachine::run do
|
13
|
+
Http::Min.connect("www.google.com", data) do |raw_response, parsed_response|
|
14
|
+
assert(parsed_response.status_code == 200, "Response from google should be 200 but is #{parsed_response.status_code}")
|
15
|
+
assert(raw_response.length > 0, "Raw response from google should be have size larger than 0")
|
16
|
+
EM::stop
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: minhttp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease: !!null
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andrew Farmer
|
9
|
+
autorequire: !!null
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-05-10 00:00:00.000000000 -07:00
|
13
|
+
default_executable: !!null
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: http_parser.rb
|
17
|
+
requirement: &9355460 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *9355460
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: eventmachine
|
28
|
+
requirement: &9338000 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *9338000
|
37
|
+
description: MinHTTP allows one to send and receive raw HTTP requests. It's a very
|
38
|
+
thin wrapper around EventMachine's connect method with some SSL validation added.
|
39
|
+
email:
|
40
|
+
- ahfarmer@gmail.com
|
41
|
+
executables: []
|
42
|
+
extensions: []
|
43
|
+
extra_rdoc_files: []
|
44
|
+
files:
|
45
|
+
- README.md
|
46
|
+
- VERSION
|
47
|
+
- Gemfile
|
48
|
+
- Rakefile
|
49
|
+
- cacert.pem
|
50
|
+
- examples/readme_example.rb
|
51
|
+
- lib/ssl_validator.rb
|
52
|
+
- lib/minhttp.rb
|
53
|
+
- test/simple_test.rb~
|
54
|
+
- test/simple_test.rb
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: http://github.com/ahfarmer/minhttp
|
57
|
+
licenses: []
|
58
|
+
post_install_message: !!null
|
59
|
+
rdoc_options: []
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ! '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubyforge_project: minhttp
|
76
|
+
rubygems_version: 1.5.0
|
77
|
+
signing_key: !!null
|
78
|
+
specification_version: 3
|
79
|
+
summary: An HTTP library for the minimalist.
|
80
|
+
test_files:
|
81
|
+
- test/simple_test.rb~
|
82
|
+
- test/simple_test.rb
|