socketry 0.5.1 → 0.6.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 +5 -5
- data/context/configuration.md +154 -0
- data/lib/socketry/version.rb +5 -1
- data/lib/socketry.rb +3 -22
- data/license.md +21 -0
- data/readme.md +47 -0
- data/releases.md +6 -0
- data/skills/socketry-github-repository/SKILL.md +95 -0
- data/skills/socketry-pull-request/SKILL.md +54 -0
- metadata +20 -69
- data/.gitignore +0 -9
- data/.rspec +0 -4
- data/.rubocop.yml +0 -59
- data/.ruby-version +0 -1
- data/.travis.yml +0 -16
- data/CHANGES.md +0 -35
- data/Gemfile +0 -20
- data/Guardfile +0 -10
- data/LICENSE.txt +0 -22
- data/README.md +0 -110
- data/Rakefile +0 -10
- data/lib/socketry/exceptions.rb +0 -40
- data/lib/socketry/resolver/resolv.rb +0 -67
- data/lib/socketry/resolver/system.rb +0 -48
- data/lib/socketry/ssl/server.rb +0 -53
- data/lib/socketry/ssl/socket.rb +0 -190
- data/lib/socketry/tcp/server.rb +0 -85
- data/lib/socketry/tcp/socket.rb +0 -345
- data/lib/socketry/timeout.rb +0 -76
- data/lib/socketry/udp/datagram.rb +0 -29
- data/lib/socketry/udp/socket.rb +0 -185
- data/logo.png +0 -0
- data/socketry.gemspec +0 -29
data/CHANGES.md
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
## 0.5.1 (2016-11-26)
|
|
2
|
-
|
|
3
|
-
* Fix regression in Socketry::SSL::Socket#close
|
|
4
|
-
|
|
5
|
-
## 0.5.0 (2016-11-26)
|
|
6
|
-
|
|
7
|
-
* Require Ruby 2.2.6+ (earlier 2.2 versions had async I/O bugs)
|
|
8
|
-
* Extract Socketry::SSL::Socket#accept into its own method
|
|
9
|
-
* Simplify Socketry::SSL::Socket#from_socket API
|
|
10
|
-
* Raise Socketry::SSL::CertificateVerifyError for certificate verification errors
|
|
11
|
-
* Specs and bugfixes for Socketry::SSL::Server
|
|
12
|
-
* Rename Socketry::UDP::Datagram accessors to `remote_host`, `remote_addr`, and `remote_port`
|
|
13
|
-
* Update to RuboCop 0.45.0
|
|
14
|
-
|
|
15
|
-
## 0.4.0 (2016-11-25)
|
|
16
|
-
|
|
17
|
-
* Specs and bugfixes for SSL sockets
|
|
18
|
-
* Specs and bugfixes for UDP sockets
|
|
19
|
-
* Add Socketry::UDP::Datagram class
|
|
20
|
-
* Add Socketry::AddressInUseError exception
|
|
21
|
-
|
|
22
|
-
## 0.3.0 (2016-09-24)
|
|
23
|
-
|
|
24
|
-
* Implement Socketry::TCP::Socket#read and #write
|
|
25
|
-
* Use StandardError as the base class for Socketry::Error
|
|
26
|
-
* Add Socketry::ConnectionRefusedError
|
|
27
|
-
* Parameterize SSL contexts
|
|
28
|
-
|
|
29
|
-
## 0.2.0 (2016-09-12)
|
|
30
|
-
|
|
31
|
-
* Rename Socketry::TCP::Socket#connected? -> #closed?
|
|
32
|
-
|
|
33
|
-
## 0.1.0 (2016-09-11)
|
|
34
|
-
|
|
35
|
-
* Initial release
|
data/Gemfile
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
source "https://rubygems.org"
|
|
4
|
-
|
|
5
|
-
gemspec
|
|
6
|
-
|
|
7
|
-
group :development do
|
|
8
|
-
gem "guard-rspec", require: false
|
|
9
|
-
gem "pry", require: false
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
group :test do
|
|
13
|
-
gem "rspec", "~> 3", require: false
|
|
14
|
-
gem "rubocop", "0.45.0", require: false
|
|
15
|
-
gem "coveralls", require: false
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
group :development, :test do
|
|
19
|
-
gem "rake"
|
|
20
|
-
end
|
data/Guardfile
DELETED
data/LICENSE.txt
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2016 Tony Arcieri
|
|
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
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# 
|
|
2
|
-
|
|
3
|
-
[![Gem Version][gem-image]][gem-link] [![Build Status][build-image]][build-link] [![Code Climate][codeclimate-image]][codeclimate-link] [![Coverage Status][coverage-image]][coverage-link] [![MIT licensed][license-image]][license-link]
|
|
4
|
-
|
|
5
|
-
[gem-image]: https://badge.fury.io/rb/socketry.svg
|
|
6
|
-
[gem-link]: https://rubygems.org/gems/socketry
|
|
7
|
-
[build-image]: https://secure.travis-ci.org/socketry/socketry.svg?branch=master
|
|
8
|
-
[build-link]: https://travis-ci.org/socketry/socketry
|
|
9
|
-
[codeclimate-image]: https://codeclimate.com/github/socketry/socketry.svg?branch=master
|
|
10
|
-
[codeclimate-link]: https://codeclimate.com/github/socketry/socketry
|
|
11
|
-
[coverage-image]: https://coveralls.io/repos/github/socketry/socketry/badge.svg?branch=master
|
|
12
|
-
[coverage-link]: https://coveralls.io/github/socketry/socketry?branch=master
|
|
13
|
-
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg
|
|
14
|
-
[license-link]: https://github.com/socketry/socketry/blob/master/LICENSE.txt
|
|
15
|
-
|
|
16
|
-
High-level Ruby socket library with support for TCP, UDP, and SSL sockets.
|
|
17
|
-
|
|
18
|
-
Implements thread-safe timeouts using asynchronous I/O and high-precision monotonic timers.
|
|
19
|
-
|
|
20
|
-
## Motivation
|
|
21
|
-
|
|
22
|
-
By default, Ruby sockets do not provide a built-in timeout mechanism. The only
|
|
23
|
-
timeout mechanism provided by the language leverages [timeout.rb], which uses
|
|
24
|
-
[unsafe multithreaded behaviors] to implement timeouts.
|
|
25
|
-
|
|
26
|
-
While Socketry provides a synchronous, blocking API similar to Ruby's own
|
|
27
|
-
`TCPSocket` and `UDPSocket` classes, behind the scenes it uses non-blocking I/O
|
|
28
|
-
to implement thread-safe timeouts.
|
|
29
|
-
|
|
30
|
-
[timeout.rb]: http://ruby-doc.org/stdlib-2.3.1/libdoc/timeout/rdoc/Timeout.html
|
|
31
|
-
[unsafe multithreaded behaviors]: http://blog.headius.com/2008/02/ruby-threadraise-threadkill-timeoutrb.html
|
|
32
|
-
|
|
33
|
-
## Installation
|
|
34
|
-
|
|
35
|
-
Add this line to your application's Gemfile:
|
|
36
|
-
|
|
37
|
-
```ruby
|
|
38
|
-
gem "socketry"
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
And then execute:
|
|
42
|
-
|
|
43
|
-
$ bundle
|
|
44
|
-
|
|
45
|
-
Or install it yourself as:
|
|
46
|
-
|
|
47
|
-
$ gem install socketry
|
|
48
|
-
|
|
49
|
-
## Basic Usage
|
|
50
|
-
|
|
51
|
-
Below is a basic example of how to use Socketry to make an HTTPS request:
|
|
52
|
-
|
|
53
|
-
```ruby
|
|
54
|
-
require "socketry"
|
|
55
|
-
|
|
56
|
-
socket = Socketry::SSL::Socket.connect("github.com", 443)
|
|
57
|
-
socket.writepartial("GET / HTTP/1.0\r\nHost: github.com\r\n\r\n")
|
|
58
|
-
p socket.readpartial(1024)
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
[TCP], [SSL], and [UDP] servers and sockets also available.
|
|
62
|
-
|
|
63
|
-
[TCP]: https://github.com/socketry/socketry/wiki/TCP
|
|
64
|
-
[SSL]: https://github.com/socketry/socketry/wiki/SSL
|
|
65
|
-
[UDP]: https://github.com/socketry/socketry/wiki/UDP
|
|
66
|
-
|
|
67
|
-
## Documentation
|
|
68
|
-
|
|
69
|
-
[Please see the Socketry wiki](https://github.com/socketry/socketry/wiki)
|
|
70
|
-
for more detailed documentation and usage notes.
|
|
71
|
-
|
|
72
|
-
[YARD API documentation](http://www.rubydoc.info/gems/socketry/)
|
|
73
|
-
is also available.
|
|
74
|
-
|
|
75
|
-
## Supported Ruby Versions
|
|
76
|
-
|
|
77
|
-
This library aims to support and is [tested against][travis] the following Ruby
|
|
78
|
-
versions:
|
|
79
|
-
|
|
80
|
-
* Ruby 2.2.6+
|
|
81
|
-
* Ruby 2.3.0+
|
|
82
|
-
* JRuby 9.1.6.0+
|
|
83
|
-
|
|
84
|
-
If something doesn't work on one of these versions, it's a bug.
|
|
85
|
-
|
|
86
|
-
This library may inadvertently work (or seem to work) on other Ruby versions,
|
|
87
|
-
however support will only be provided for the versions listed above.
|
|
88
|
-
|
|
89
|
-
If you would like this library to support another Ruby version or
|
|
90
|
-
implementation, you may volunteer to be a maintainer. Being a maintainer
|
|
91
|
-
entails making sure all tests run and pass on that implementation. When
|
|
92
|
-
something breaks on your implementation, you will be responsible for providing
|
|
93
|
-
patches in a timely fashion. If critical issues for a particular implementation
|
|
94
|
-
exist at the time of a major release, support for that Ruby version may be
|
|
95
|
-
dropped.
|
|
96
|
-
|
|
97
|
-
[travis]: http://travis-ci.org/socketry/socketry
|
|
98
|
-
|
|
99
|
-
## Contributing
|
|
100
|
-
|
|
101
|
-
* Fork this repository on github
|
|
102
|
-
* Make your changes and send us a pull request
|
|
103
|
-
* If we like them we'll merge them
|
|
104
|
-
* If we've accepted a patch, feel free to ask for commit access
|
|
105
|
-
|
|
106
|
-
## License
|
|
107
|
-
|
|
108
|
-
Copyright (c) 2016 Tony Arcieri. Distributed under the MIT License. See
|
|
109
|
-
[LICENSE.txt](https://github.com/socketry/socketry/blob/master/LICENSE.txt)
|
|
110
|
-
for further details.
|
data/Rakefile
DELETED
data/lib/socketry/exceptions.rb
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Socketry
|
|
4
|
-
# Generic catch all for all Socketry errors
|
|
5
|
-
Error = Class.new(StandardError)
|
|
6
|
-
|
|
7
|
-
# Failed to connect to a remote host
|
|
8
|
-
ConnectionRefusedError = Class.new(Socketry::Error)
|
|
9
|
-
|
|
10
|
-
# Invalid address
|
|
11
|
-
AddressError = Class.new(Socketry::Error)
|
|
12
|
-
|
|
13
|
-
# Address is already in use
|
|
14
|
-
AddressInUseError = Class.new(Socketry::Error)
|
|
15
|
-
|
|
16
|
-
# Timeouts performing an I/O operation
|
|
17
|
-
TimeoutError = Class.new(Socketry::Error)
|
|
18
|
-
|
|
19
|
-
# Cannot perform operation in current state
|
|
20
|
-
StateError = Class.new(Socketry::Error)
|
|
21
|
-
|
|
22
|
-
# Internal consistency error within the library
|
|
23
|
-
InternalError = Class.new(Socketry::Error)
|
|
24
|
-
|
|
25
|
-
module Resolver
|
|
26
|
-
# DNS resolution errors
|
|
27
|
-
Error = Class.new(Socketry::AddressError)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
module SSL
|
|
31
|
-
# Errors related to SSL
|
|
32
|
-
Error = Class.new(Socketry::Error)
|
|
33
|
-
|
|
34
|
-
# Certificate could not be verified
|
|
35
|
-
CertificateVerifyError = Class.new(Socketry::SSL::Error)
|
|
36
|
-
|
|
37
|
-
# Hostname verification error
|
|
38
|
-
HostnameError = Class.new(CertificateVerifyError)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "resolv"
|
|
4
|
-
|
|
5
|
-
module Socketry
|
|
6
|
-
module Resolver
|
|
7
|
-
# Pure Ruby DNS resolver provided by the standard library
|
|
8
|
-
class Resolv
|
|
9
|
-
# Resolve a hostname by creating and discaring a Socketry::Resolver::Resolv
|
|
10
|
-
# instance. For better performance, create and reuse an instance.
|
|
11
|
-
def self.resolve(hostname, **options)
|
|
12
|
-
resolver = new
|
|
13
|
-
begin
|
|
14
|
-
resolver.resolve(hostname, **options)
|
|
15
|
-
ensure
|
|
16
|
-
resolver.close
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Create a new instance of Socketry::Resolver::Resolv.
|
|
21
|
-
#
|
|
22
|
-
# Arguments are passed directly to Resolv::DNS. See the Ruby documentation
|
|
23
|
-
# for more information:
|
|
24
|
-
#
|
|
25
|
-
# https://ruby-doc.org/stdlib-2.3.1/libdoc/resolv/rdoc/Resolv/DNS.html
|
|
26
|
-
#
|
|
27
|
-
def initialize(*args)
|
|
28
|
-
@hosts = ::Resolv::Hosts.new
|
|
29
|
-
@resolver = ::Resolv::DNS.new(*args)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Resolve a domain name using IPSocket.getaddress. This uses getaddrinfo(3)
|
|
33
|
-
# on POSIX operating systems.
|
|
34
|
-
#
|
|
35
|
-
# @param hostname [String] name of the host whose IP address we'd like to obtain
|
|
36
|
-
# @return [IPAddr] resolved IP address
|
|
37
|
-
# @raise [Socketry::Resolver::Error] an error occurred resolving the domain name
|
|
38
|
-
# @raise [Socketry::TimeoutError] a timeout occured before the name could be resolved
|
|
39
|
-
# @raise [Socketry::AddressError] the name was resolved to an unsupported address
|
|
40
|
-
def resolve(hostname, timeout: nil)
|
|
41
|
-
raise TypeError, "expected String, got #{hostname.class}" unless hostname.is_a?(String)
|
|
42
|
-
return IPAddr.new(@hosts.getaddress(hostname).sub(/%.*$/, ""))
|
|
43
|
-
rescue ::Resolv::ResolvError
|
|
44
|
-
case timeout
|
|
45
|
-
when Integer, Float
|
|
46
|
-
@resolver.timeouts = timeout
|
|
47
|
-
when NilClass
|
|
48
|
-
nil # no timeout
|
|
49
|
-
else raise TypeError, "expected Numeric, got #{timeout.class}"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
begin
|
|
53
|
-
IPAddr.new(@resolver.getaddress(hostname).to_s)
|
|
54
|
-
rescue ::Resolv::ResolvError => ex
|
|
55
|
-
raise Socketry::Resolver::Error, ex.message, ex.backtrace
|
|
56
|
-
rescue ::Resolv::ResolvTimeout => ex
|
|
57
|
-
raise Socketry::TimeoutError, ex.message, ex.backtrace
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Close the resolver
|
|
62
|
-
def close
|
|
63
|
-
@resolver.close
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "timeout"
|
|
4
|
-
|
|
5
|
-
module Socketry
|
|
6
|
-
module Resolver
|
|
7
|
-
# System DNS resolver backed by the POSIX getaddrinfo(3) function
|
|
8
|
-
module System
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
# Resolve a domain name using IPSocket.getaddress. This uses getaddrinfo(3)
|
|
12
|
-
# on POSIX operating systems.
|
|
13
|
-
#
|
|
14
|
-
# @param hostname [String] name of the host whose IP address we'd like to obtain
|
|
15
|
-
# @return [IPAddr] resolved IP address
|
|
16
|
-
# @raise [Socketry::Resolver::Error] an error occurred resolving the domain name
|
|
17
|
-
# @raise [Socketry::TimeoutError] a timeout occured before the name could be resolved
|
|
18
|
-
# @raise [Socketry::AddressError] the name was resolved to an unsupported address
|
|
19
|
-
def resolve(hostname, timeout: nil)
|
|
20
|
-
raise TypeError, "expected String, got #{hostname.class}" unless hostname.is_a?(String)
|
|
21
|
-
|
|
22
|
-
begin
|
|
23
|
-
case timeout
|
|
24
|
-
when Integer, Float
|
|
25
|
-
# NOTE: ::Timeout is not thread safe. For thread safety, use Socketry::Resolver::Resolv
|
|
26
|
-
result = ::Timeout.timeout(timeout) { IPSocket.getaddress(hostname) }
|
|
27
|
-
when NilClass
|
|
28
|
-
result = IPSocket.getaddress(hostname)
|
|
29
|
-
else raise TypeError, "expected Numeric, got #{timeout.class}"
|
|
30
|
-
end
|
|
31
|
-
rescue ::SocketError => ex
|
|
32
|
-
raise Socketry::Resolver::Error, ex.message, ex.backtrace
|
|
33
|
-
rescue ::Timeout::Error => ex
|
|
34
|
-
raise Socketry::TimeoutError, ex.message, ex.backtrace
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
begin
|
|
38
|
-
IPAddr.new(result)
|
|
39
|
-
rescue IPAddr::InvalidAddressError => ex
|
|
40
|
-
raise Socketry::AddressError, ex.message, ex.backtrace
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Use Socketry::Resolver::System as the default resolver
|
|
46
|
-
DEFAULT_RESOLVER = System
|
|
47
|
-
end
|
|
48
|
-
end
|
data/lib/socketry/ssl/server.rb
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Socketry
|
|
4
|
-
# Secure Sockets Layer (a.k.a. Transport Layer Security, or TLS)
|
|
5
|
-
module SSL
|
|
6
|
-
# SSL Server
|
|
7
|
-
class Server < Socketry::TCP::Server
|
|
8
|
-
# Create a new SSL server
|
|
9
|
-
#
|
|
10
|
-
# @return [Socketry::SSL::Server]
|
|
11
|
-
def initialize(
|
|
12
|
-
hostname_or_port,
|
|
13
|
-
port = nil,
|
|
14
|
-
ssl_socket_class: OpenSSL::SSL::SSLSocket,
|
|
15
|
-
ssl_params: nil,
|
|
16
|
-
**args
|
|
17
|
-
)
|
|
18
|
-
raise TypeError, "expected Hash, got #{ssl_params.class}" if ssl_params && !ssl_params.is_a?(Hash)
|
|
19
|
-
|
|
20
|
-
@ssl_socket_class = ssl_socket_class
|
|
21
|
-
@ssl_params = ssl_params
|
|
22
|
-
|
|
23
|
-
super(hostname_or_port, port, **args)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Accept a connection to the server
|
|
27
|
-
#
|
|
28
|
-
# Note that this method also performs an SSL handshake and will therefore
|
|
29
|
-
# block other sockets which are ready to be accepted.
|
|
30
|
-
#
|
|
31
|
-
# Multithreaded servers should invoke this method after spawning a thread
|
|
32
|
-
# to ensure a slow/malicious connection can't cause a denial-of-service
|
|
33
|
-
# attack against the server.
|
|
34
|
-
#
|
|
35
|
-
# @param timeout [Numeric, NilClass] (default nil, unlimited) seconds to wait before aborting the accept
|
|
36
|
-
#
|
|
37
|
-
# @return [Socketry::SSL::Socket]
|
|
38
|
-
def accept(timeout: nil, **args)
|
|
39
|
-
tcp_socket = super(timeout: timeout, **args)
|
|
40
|
-
|
|
41
|
-
ssl_socket = Socketry::SSL::Socket.new(
|
|
42
|
-
read_timeout: @read_timeout,
|
|
43
|
-
write_timeout: @write_timeout,
|
|
44
|
-
resolver: @resolver,
|
|
45
|
-
ssl_socket_class: @ssl_socket_class,
|
|
46
|
-
ssl_params: @ssl_params
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
ssl_socket.accept(tcp_socket, timeout: timeout)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
data/lib/socketry/ssl/socket.rb
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Socketry
|
|
4
|
-
# Secure Sockets Layer (a.k.a. Transport Layer Security, or TLS)
|
|
5
|
-
module SSL
|
|
6
|
-
# SSL Sockets
|
|
7
|
-
class Socket < Socketry::TCP::Socket
|
|
8
|
-
# Create an unconnected Socketry::SSL::Socket
|
|
9
|
-
#
|
|
10
|
-
# @param read_timeout [Numeric] Seconds to wait before an uncompleted read errors
|
|
11
|
-
# @param write_timeout [Numeric] Seconds to wait before an uncompleted write errors
|
|
12
|
-
# @param timer [Object] A timekeeping object to use for measuring timeouts
|
|
13
|
-
# @param resolver [Object] A resolver object to use for resolving DNS names
|
|
14
|
-
# @param socket_class [Object] Underlying socket class which implements I/O ops
|
|
15
|
-
# @param ssl_socket_class [Object] Class which provides the underlying SSL implementation
|
|
16
|
-
# @param ssl_context [OpenSSL::SSL::SSLContext] SSL configuration object
|
|
17
|
-
# @param ssL_params [Hash] Parameter hash to set on the given SSL context
|
|
18
|
-
#
|
|
19
|
-
# @return [Socketry::SSL::Socket]
|
|
20
|
-
def initialize(
|
|
21
|
-
ssl_socket_class: OpenSSL::SSL::SSLSocket,
|
|
22
|
-
ssl_context: OpenSSL::SSL::SSLContext.new,
|
|
23
|
-
ssl_params: nil,
|
|
24
|
-
**args
|
|
25
|
-
)
|
|
26
|
-
raise TypeError, "invalid SSL context (#{ssl_context.class})" unless ssl_context.is_a?(OpenSSL::SSL::SSLContext)
|
|
27
|
-
raise TypeError, "expected Hash, got #{ssl_params.class}" if ssl_params && !ssl_params.is_a?(Hash)
|
|
28
|
-
|
|
29
|
-
@ssl_socket_class = ssl_socket_class
|
|
30
|
-
|
|
31
|
-
@ssl_context = ssl_context
|
|
32
|
-
@ssl_context.set_params(ssl_params) if ssl_params && !ssl_params.empty?
|
|
33
|
-
|
|
34
|
-
@ssl_socket = nil
|
|
35
|
-
|
|
36
|
-
super(**args)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Make an SSL connection to a remote host
|
|
40
|
-
#
|
|
41
|
-
# @param remote_addr [String] DNS name or IP address of the host to connect to
|
|
42
|
-
# @param remote_port [Fixnum] TCP port to connect to
|
|
43
|
-
# @param local_addr [String] DNS name or IP address to bind to locally
|
|
44
|
-
# @param local_port [Fixnum] Local TCP port to bind to
|
|
45
|
-
# @param timeout [Numeric] Number of seconds to wait before aborting connect
|
|
46
|
-
# @param enable_sni [true, false] (default: true) Enables Server Name Indication (SNI)
|
|
47
|
-
# @param verify_hostname [true, false] (default: true) Ensure server's hostname matches cert
|
|
48
|
-
#
|
|
49
|
-
# @raise [Socketry::AddressError] an invalid address was given
|
|
50
|
-
# @raise [Socketry::TimeoutError] connect operation timed out
|
|
51
|
-
# @raise [Socketry::SSL::Error] an error occurred negotiating an SSL connection
|
|
52
|
-
# @return [self]
|
|
53
|
-
def connect(
|
|
54
|
-
remote_addr,
|
|
55
|
-
remote_port,
|
|
56
|
-
local_addr: nil,
|
|
57
|
-
local_port: nil,
|
|
58
|
-
timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:connect],
|
|
59
|
-
enable_sni: true,
|
|
60
|
-
verify_hostname: true
|
|
61
|
-
)
|
|
62
|
-
super(remote_addr, remote_port, local_addr: local_addr, local_port: local_port, timeout: timeout)
|
|
63
|
-
|
|
64
|
-
@ssl_socket = @ssl_socket_class.new(@socket, @ssl_context)
|
|
65
|
-
@ssl_socket.hostname = remote_addr if enable_sni
|
|
66
|
-
@ssl_socket.sync_close = true
|
|
67
|
-
|
|
68
|
-
begin
|
|
69
|
-
@ssl_socket.connect_nonblock
|
|
70
|
-
rescue IO::WaitReadable
|
|
71
|
-
retry if @socket.wait_readable(timeout)
|
|
72
|
-
raise Socketry::TimeoutError, "connection to #{remote_addr}:#{remote_port} timed out"
|
|
73
|
-
rescue IO::WaitWritable
|
|
74
|
-
retry if @socket.wait_writable(timeout)
|
|
75
|
-
raise Socketry::TimeoutError, "connection to #{remote_addr}:#{remote_port} timed out"
|
|
76
|
-
rescue OpenSSL::SSL::SSLError => ex
|
|
77
|
-
raise Socketry::SSL::CertificateVerifyError, ex.message if ex.message.include?("certificate verify failed")
|
|
78
|
-
raise Socketry::SSL::Error, ex.message, ex.backtrace
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
begin
|
|
82
|
-
@ssl_socket.post_connection_check(remote_addr) if verify_hostname
|
|
83
|
-
rescue OpenSSL::SSL::SSLError => ex
|
|
84
|
-
raise Socketry::SSL::HostnameError, ex.message, ex.backtrace
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
self
|
|
88
|
-
rescue => ex
|
|
89
|
-
@socket.close rescue nil
|
|
90
|
-
@socket = nil
|
|
91
|
-
@ssl_socket.close rescue nil
|
|
92
|
-
@ssl_socket = nil
|
|
93
|
-
raise ex
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Accept an SSL connection from a Socketry or Ruby socket
|
|
97
|
-
#
|
|
98
|
-
# @param tcp_socket [TCPSocket, Socketry::TCP::Socket] raw TCP socket to begin SSL handshake with
|
|
99
|
-
# @param timeout [Numeric, NilClass] (default nil, unlimited) seconds to wait before aborting the accept
|
|
100
|
-
#
|
|
101
|
-
# @return [self]
|
|
102
|
-
def accept(tcp_socket, timeout: nil)
|
|
103
|
-
tcp_socket = IO.try_convert(tcp_socket) || raise(TypeError, "couldn't convert #{tcp_socket.class} to IO")
|
|
104
|
-
ssl_socket = @ssl_socket_class.new(tcp_socket, @ssl_context)
|
|
105
|
-
|
|
106
|
-
begin
|
|
107
|
-
ssl_socket.accept_nonblock
|
|
108
|
-
rescue IO::WaitReadable
|
|
109
|
-
retry if IO.select([tcp_socket], nil, nil, timeout)
|
|
110
|
-
raise Socketry::TimeoutError, "failed to complete handshake after #{timeout} seconds"
|
|
111
|
-
rescue IO::WaitWritable
|
|
112
|
-
retry if IO.select(nil, [tcp_socket], nil, timeout)
|
|
113
|
-
raise Socketry::TimeoutError, "failed to complete handshake after #{timeout} seconds"
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
from_socket(ssl_socket)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Wrap a Ruby OpenSSL::SSL::SSLSocket (or other low-level SSL socket)
|
|
120
|
-
#
|
|
121
|
-
# @param ssl_socket [OpenSSL::SSL::SSLSocket] SSL socket class associated with this socket
|
|
122
|
-
#
|
|
123
|
-
# @return [self]
|
|
124
|
-
def from_socket(ssl_socket)
|
|
125
|
-
raise TypeError, "expected #{@ssl_socket_class}, got #{ssl_socket.class}" unless ssl_socket.is_a?(@ssl_socket_class)
|
|
126
|
-
raise StateError, "already connected" if @socket
|
|
127
|
-
|
|
128
|
-
@socket = ssl_socket.to_io
|
|
129
|
-
@ssl_socket = ssl_socket
|
|
130
|
-
@ssl_socket.sync_close = true
|
|
131
|
-
|
|
132
|
-
self
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Perform a non-blocking read operation
|
|
136
|
-
#
|
|
137
|
-
# @param size [Fixnum] number of bytes to attempt to read
|
|
138
|
-
# @param outbuf [String, NilClass] an optional buffer into which data should be read
|
|
139
|
-
#
|
|
140
|
-
# @raise [Socketry::Error] an I/O operation failed
|
|
141
|
-
# @return [String, :wait_readable] data read, or :wait_readable if operation would block
|
|
142
|
-
def read_nonblock(size, outbuf: nil)
|
|
143
|
-
case outbuf
|
|
144
|
-
when String
|
|
145
|
-
perform { @ssl_socket.read_nonblock(size, outbuf, exception: false) }
|
|
146
|
-
when NilClass
|
|
147
|
-
perform { @ssl_socket.read_nonblock(size, exception: false) }
|
|
148
|
-
else raise TypeError, "unexpected outbuf class: #{outbuf.class}"
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Perform a non-blocking write operation
|
|
153
|
-
#
|
|
154
|
-
# @param data [String] number of bytes to attempt to read
|
|
155
|
-
#
|
|
156
|
-
# @raise [Socketry::Error] an I/O operation failed
|
|
157
|
-
# @return [Fixnum, :wait_writable] number of bytes written, or :wait_writable if op would block
|
|
158
|
-
def write_nonblock(data)
|
|
159
|
-
perform { @ssl_socket.write_nonblock(data, exception: false) }
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Close the socket
|
|
163
|
-
#
|
|
164
|
-
# @return [true, false] true if the socket was open, false if closed
|
|
165
|
-
def close
|
|
166
|
-
return false unless super
|
|
167
|
-
@ssl_socket.close
|
|
168
|
-
@ssl_socket = nil
|
|
169
|
-
true
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
private
|
|
173
|
-
|
|
174
|
-
# Perform a non-blocking I/O operation
|
|
175
|
-
def perform
|
|
176
|
-
ensure_connected
|
|
177
|
-
yield
|
|
178
|
-
# Some buggy Rubies continue to raise this exception
|
|
179
|
-
rescue IO::WaitWritable
|
|
180
|
-
:wait_writable
|
|
181
|
-
# Due to SSL, we may need to write to complete a read (e.g. handshaking, renegotiation)
|
|
182
|
-
rescue IO::WaitReadable
|
|
183
|
-
:wait_readable
|
|
184
|
-
rescue => ex
|
|
185
|
-
# TODO: more specific exceptions
|
|
186
|
-
raise Socketry::Error, ex.message, ex.backtrace
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|