minhttp 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.
- 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
|