socketry 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dae535663b1bc19486c7766d86d1444cf097fb81
4
+ data.tar.gz: 31212e71094435898cd1f2937bece83f9f863708
5
+ SHA512:
6
+ metadata.gz: 5e4bedb944e9683e08e6027edc585c09383c9f962eec07375a70fff0aaac3fdeedf5020ec12beaa56213de0bb96d1b1a3d66fbd0cd5655544e22b96530f52836
7
+ data.tar.gz: b169b991a93b8667ad3f532889751e05aa5e25a98825452da04ddcbcfc5652a8fb01218f0a48212d111e02316f677156d72190ba64e6a74b0bfbe1325ed251b2
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ --backtrace
2
+ --color
3
+ --format=documentation
4
+ --order random
5
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,56 @@
1
+ AllCops:
2
+ DisplayCopNames: true
3
+
4
+ #
5
+ # Style
6
+ #
7
+
8
+ LineLength:
9
+ Max: 128
10
+
11
+ Style/AccessorMethodName:
12
+ Enabled: false
13
+
14
+ Style/ConditionalAssignment:
15
+ Enabled: false
16
+
17
+ Style/RescueModifier:
18
+ Enabled: false
19
+
20
+ Style/SpaceBeforeFirstArg:
21
+ Enabled: false
22
+
23
+ Style/StringLiterals:
24
+ EnforcedStyle: double_quotes
25
+
26
+ #
27
+ # Metrics
28
+ #
29
+
30
+ Metrics/AbcSize:
31
+ Max: 50
32
+
33
+ Metrics/ClassLength:
34
+ Max: 200
35
+
36
+ Metrics/CyclomaticComplexity:
37
+ Max: 10
38
+
39
+ Metrics/MethodLength:
40
+ Max: 50
41
+
42
+ Metrics/ParameterLists:
43
+ Enabled: false
44
+
45
+ Metrics/PerceivedComplexity:
46
+ Max: 10
47
+
48
+ #
49
+ # Lint
50
+ #
51
+
52
+ Lint/HandleExceptions:
53
+ Enabled: false
54
+
55
+ Lint/ShadowedException:
56
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.1
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+ sudo: false
3
+
4
+ bundler_args: --without development doc
5
+
6
+ rvm:
7
+ - 2.2
8
+ - 2.3.1
9
+ - jruby-9.0.5.0
10
+
11
+ matrix:
12
+ fast_finish: true
13
+
14
+ branches:
15
+ only:
16
+ - master
data/CHANGES.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 (2016-09-11)
2
+
3
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,20 @@
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.42.0", require: false
15
+ gem "coveralls", require: false
16
+ end
17
+
18
+ group :development, :test do
19
+ gem "rake"
20
+ end
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ directories %w(lib spec)
4
+ clearing :on
5
+
6
+ guard :rspec, cmd: "bundle exec rspec" do
7
+ watch(%r{^spec/.+_spec\.rb$})
8
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
9
+ watch("spec/spec_helper.rb") { "spec" }
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2015-2016 Tony Arcieri, Zachary Anker
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,55 @@
1
+ # Socketry [![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]
2
+
3
+ [gem-image]: https://badge.fury.io/rb/socketry.svg
4
+ [gem-link]: https://rubygems.org/gems/socketry
5
+ [build-image]: https://secure.travis-ci.org/socketry/socketry.svg?branch=master
6
+ [build-link]: https://travis-ci.org/socketry/socketry
7
+ [codeclimate-image]: https://codeclimate.com/github/socketry/socketry.svg?branch=master
8
+ [codeclimate-link]: https://codeclimate.com/github/socketry/socketry
9
+ [coverage-image]: https://coveralls.io/repos/github/socketry/socketry/badge.svg?branch=master
10
+ [coverage-link]: https://coveralls.io/github/socketry/socketry?branch=master
11
+ [license-image]: https://img.shields.io/badge/license-MIT-blue.svg
12
+ [license-link]: https://github.com/socketry/socketry/blob/master/LICENSE.txt
13
+
14
+ High-level wrappers for Ruby sockets with advanced thread-safe timeout support.
15
+
16
+ **Does not require Celluloid!** Socketry provides sockets with thread-safe
17
+ timeout support that can be used with any multithreaded Ruby app. That said,
18
+ Socketry can also be used to provide asynchronous I/O with [Celluloid::IO].
19
+
20
+ [Celluloid::IO]: https://github.com/celluloid/celluloid-io
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile:
25
+
26
+ ```ruby
27
+ gem 'socketry'
28
+ ```
29
+
30
+ And then execute:
31
+
32
+ $ bundle
33
+
34
+ Or install it yourself as:
35
+
36
+ $ gem install socketry
37
+
38
+ ## Usage
39
+
40
+ TODO: Coming soon!
41
+
42
+ ## Contributing
43
+
44
+ * Fork this repository on github
45
+ * Make your changes and send us a pull request
46
+ * If we like them we'll merge them
47
+ * If we've accepted a patch, feel free to ask for commit access
48
+
49
+ ## License
50
+
51
+ Copyright (c) 2016 Tony Arcieri
52
+
53
+ Distributed under the MIT License. See
54
+ [LICENSE.txt](https://github.com/socketry/socketry/blob/master/LICENSE.txt)
55
+ for further details.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ require "bundler/gem_tasks"
3
+
4
+ require "rspec/core/rake_task"
5
+ RSpec::Core::RakeTask.new
6
+
7
+ require "rubocop/rake_task"
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %w(spec rubocop)
data/lib/socketry.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby stdlib dependencies
4
+ require "io/wait"
5
+ require "ipaddr"
6
+ require "socket"
7
+ require "openssl"
8
+
9
+ # External gems
10
+ require "hitimes"
11
+
12
+ # Socketry codebase
13
+ require "socketry/version"
14
+
15
+ require "socketry/exceptions"
16
+ require "socketry/resolver/resolv"
17
+ require "socketry/resolver/system"
18
+ require "socketry/timeout"
19
+
20
+ require "socketry/tcp/server"
21
+ require "socketry/tcp/socket"
22
+ require "socketry/ssl/server"
23
+ require "socketry/ssl/socket"
24
+ require "socketry/udp/socket"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Socketry
4
+ # Generic catch all for all Socketry errors
5
+ Error = Class.new(::IOError)
6
+
7
+ # Invalid address
8
+ AddressError = Class.new(Socketry::Error)
9
+
10
+ # Timeouts performing an I/O operation
11
+ TimeoutError = Class.new(Socketry::Error)
12
+
13
+ # Cannot perform operation in current state
14
+ StateError = Class.new(Socketry::Error)
15
+
16
+ # Internal consistency error within the library
17
+ InternalError = Class.new(Socketry::Error)
18
+
19
+ module Resolver
20
+ # DNS resolution errors
21
+ Error = Class.new(Socketry::AddressError)
22
+ end
23
+
24
+ module SSL
25
+ # Errors related to SSL
26
+ Error = Class.new(Socketry::Error)
27
+
28
+ # Hostname verification error
29
+ HostnameError = Class.new(Socketry::SSL::Error)
30
+ end
31
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Socketry
4
+ module Resolver
5
+ # Pure Ruby DNS resolver provided by the standard library
6
+ class Resolv
7
+ # Resolve a hostname by creating and discaring a Socketry::Resolver::Resolv
8
+ # instance. For better performance, create and reuse an instance.
9
+ def self.resolve(hostname, **options)
10
+ resolver = new
11
+ begin
12
+ resolver.resolve(hostname, **options)
13
+ ensure
14
+ resolver.close
15
+ end
16
+ end
17
+
18
+ # Create a new instance of Socketry::Resolver::Resolv.
19
+ #
20
+ # Arguments are passed directly to Resolv::DNS. See the Ruby documentation
21
+ # for more information:
22
+ #
23
+ # https://ruby-doc.org/stdlib-2.3.1/libdoc/resolv/rdoc/Resolv/DNS.html
24
+ #
25
+ def initialize(*args)
26
+ @hosts = ::Resolv::Hosts.new
27
+ @resolver = ::Resolv::DNS.new(*args)
28
+ end
29
+
30
+ # Resolve a domain name using IPSocket.getaddress. This uses getaddrinfo(3)
31
+ # on POSIX operating systems.
32
+ #
33
+ # @param hostname [String] name of the host whose IP address we'd like to obtain
34
+ # @return [IPAddr] resolved IP address
35
+ # @raise [Socketry::Resolver::Error] an error occurred resolving the domain name
36
+ # @raise [Socketry::TimeoutError] a timeout occured before the name could be resolved
37
+ # @raise [Socketry::AddressError] the name was resolved to an unsupported address
38
+ def resolve(hostname, timeout: nil)
39
+ raise TypeError, "expected String, got #{hostname.class}" unless hostname.is_a?(String)
40
+ return IPAddr.new(@hosts.getaddress(hostname).sub(/%.*$/, ""))
41
+ rescue ::Resolv::ResolvError
42
+ case timeout
43
+ when Integer, Float
44
+ @resolver.timeouts = timeout
45
+ when NilClass
46
+ # no timeout
47
+ else raise TypeError, "expected Numeric, got #{timeout.class}"
48
+ end
49
+
50
+ begin
51
+ IPAddr.new(@resolver.getaddress(hostname).to_s)
52
+ rescue ::Resolv::ResolvError => ex
53
+ raise Socketry::Resolver::Error, ex.message, ex.backtrace
54
+ rescue ::Resolv::ResolvTimeout => ex
55
+ raise Socketry::TimeoutError, ex.message, ex.backtrace
56
+ end
57
+ end
58
+
59
+ # Close the resolver
60
+ def close
61
+ @resolver.close
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,62 @@
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_context = OpenSSL::SSL::SSLContext.new
22
+ @ssl_context.set_params(ssl_params) if ssl_params
23
+ @ssl_context.freeze
24
+
25
+ super(hostname_or_port, port, **args)
26
+ end
27
+
28
+ # Accept a connection to the server
29
+ #
30
+ # Note that this method also performs an SSL handshake and will therefore
31
+ # block other sockets which are ready to be accepted.
32
+ #
33
+ # Multithreaded servers should invoke this method after spawning a thread
34
+ # to ensure a slow/malicious connection can't cause a denial-of-service
35
+ # attack against the server.
36
+ #
37
+ # @param timeout [Numeric, NilClass] seconds to wait before aborting the accept
38
+ # @return [Socketry::SSL::Socket]
39
+ def accept(timeout: nil, **args)
40
+ ruby_socket = super(timeout: timeout, **args).to_io
41
+ ssl_socket = @ssl_socket_class.new(ruby_socket, @ssl_context)
42
+
43
+ begin
44
+ ssl_socket.accept_nonblock
45
+ rescue IO::WaitReadable
46
+ retry if IO.select([ruby_socket], nil, nil, timeout)
47
+ raise Socketry::TimeoutError, "failed to complete handshake after #{timeout} seconds"
48
+ rescue IO::WaitWritable
49
+ retry if IO.select(nil, [ruby_socket], nil, timeout)
50
+ raise Socketry::TimeoutError, "failed to complete handshake after #{timeout} seconds"
51
+ end
52
+
53
+ Socketry::SSL::Socket.new(
54
+ read_timeout: @read_timeout,
55
+ write_timeout: @write_timeout,
56
+ resolver: @resolver,
57
+ socket_class: @socket_class
58
+ ).from_socket(ruby_socket)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,145 @@
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
+ # @return [Socketry::SSL::Socket]
16
+ def initialize(ssl_socket_class: OpenSSL::SSL::SSLSocket, ssl_params: nil, **args)
17
+ raise TypeError, "expected Hash, got #{ssl_params.class}" if ssl_params && !ssl_params.is_a?(Hash)
18
+
19
+ @ssl_socket_class = ssl_socket_class
20
+ @ssl_context = OpenSSL::SSL::SSLContext.new
21
+ @ssl_context.set_params(ssl_params) if ssl_params
22
+ @ssl_context.freeze
23
+ @ssl_socket = nil
24
+
25
+ super(**args)
26
+ end
27
+
28
+ # Make an SSL connection to a remote host
29
+ #
30
+ # @param remote_addr [String] DNS name or IP address of the host to connect to
31
+ # @param remote_port [Fixnum] TCP port to connect to
32
+ # @param local_addr [String] DNS name or IP address to bind to locally
33
+ # @param local_port [Fixnum] Local TCP port to bind to
34
+ # @param timeout [Numeric] Number of seconds to wait before aborting connect
35
+ # @param socket_class [Class] Custom low-level socket class
36
+ # @raise [Socketry::AddressError] an invalid address was given
37
+ # @raise [Socketry::TimeoutError] connect operation timed out
38
+ # @raise [Socketry::SSL::Error] an error occurred negotiating an SSL connection
39
+ # @return [self]
40
+ def connect(
41
+ remote_addr,
42
+ remote_port,
43
+ local_addr: nil,
44
+ local_port: nil,
45
+ timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:connect],
46
+ verify_hostname: true
47
+ )
48
+ super(remote_addr, remote_port, local_addr: local_addr, local_port: local_port, timeout: timeout)
49
+
50
+ @ssl_socket = OpenSSL::SSL::SSLSocket.new(@socket, @ssl_context)
51
+ @ssl_socket.hostname = remote_addr
52
+
53
+ begin
54
+ @ssl_socket.connect_nonblock
55
+ rescue IO::WaitReadable
56
+ retry if @socket.wait_readable(timeout)
57
+ raise Socketry::TimeoutError, "connection to #{remote_addr}:#{remote_port} timed out"
58
+ rescue IO::WaitWritable
59
+ retry if @socket.wait_writable(timeout)
60
+ raise Socketry::TimeoutError, "connection to #{remote_addr}:#{remote_port} timed out"
61
+ rescue OpenSSL::SSL::SSLError => ex
62
+ raise Socketry::SSL::Error, ex.message, ex.backtrace
63
+ end
64
+
65
+ begin
66
+ @ssl_socket.post_connection_check(remote_addr) if verify_hostname
67
+ rescue OpenSSL::SSL::SSLError => ex
68
+ raise Socketry::SSL::HostnameError, ex.message, ex.backtrace
69
+ end
70
+
71
+ self
72
+ rescue => ex
73
+ @socket.close rescue nil
74
+ @socket = nil
75
+ @ssl_socket.close rescue nil
76
+ @ssl_socket = nil
77
+ raise ex
78
+ end
79
+
80
+ # Wrap a Ruby OpenSSL::SSL::SSLSocket (or other low-level SSL socket)
81
+ #
82
+ # @param socket [::Socket] (or specified socket_class) low-level socket to wrap
83
+ # @param ssl_socket [OpenSSL::SSL::SSLSocket] SSL socket class associated with this socket
84
+ # @return [self]
85
+ def from_socket(socket, ssl_socket)
86
+ raise TypeError, "expected #{@socket_class}, got #{socket.class}" unless socket.is_a?(@socket_class)
87
+ raise TypeError, "expected #{@ssl_socket_class}, got #{ssl_socket.class}" unless ssl_socket.is_a?(@ssl_socket_class)
88
+ raise StateError, "already connected" if @socket && @socket != socket
89
+
90
+ @socket = socket
91
+ @ssl_socket = ssl_socket
92
+ @ssl_socket.sync_close = true
93
+
94
+ self
95
+ end
96
+
97
+ # Perform a non-blocking read operation
98
+ #
99
+ # @param size [Fixnum] number of bytes to attempt to read
100
+ # @param outbuf [String, NilClass] an optional buffer into which data should be read
101
+ # @raise [Socketry::Error] an I/O operation failed
102
+ # @return [String, :wait_readable] data read, or :wait_readable if operation would block
103
+ def read_nonblock(size)
104
+ ensure_connected
105
+ @ssl_socket.read_nonblock(size, exception: false)
106
+ # Some buggy Rubies continue to raise exceptions in these cases
107
+ rescue IO::WaitReadable
108
+ :wait_readable
109
+ # Due to SSL, we may need to write to complete a read (e.g. renegotiation)
110
+ rescue IO::WaitWritable
111
+ :wait_writable
112
+ rescue => ex
113
+ # TODO: more specific exceptions
114
+ raise Socketry::Error, ex.message, ex.backtrace
115
+ end
116
+
117
+ # Perform a non-blocking write operation
118
+ #
119
+ # @param data [String] number of bytes to attempt to read
120
+ # @raise [Socketry::Error] an I/O operation failed
121
+ # @return [Fixnum, :wait_writable] number of bytes written, or :wait_writable if op would block
122
+ def write_nonblock(data)
123
+ ensure_connected
124
+ @ssl_socket.write_nonblock(data, exception: false)
125
+ # Some buggy Rubies continue to raise this exception
126
+ rescue IO::WaitWriteable
127
+ :wait_writable
128
+ # Due to SSL, we may need to write to complete a read (e.g. renegotiation)
129
+ rescue IO::WaitReadable
130
+ :wait_readable
131
+ rescue => ex
132
+ # TODO: more specific exceptions
133
+ raise Socketry::Error, ex.message, ex.backtrace
134
+ end
135
+
136
+ # Close the socket
137
+ #
138
+ # @return [true, false] true if the socket was open, false if closed
139
+ def close
140
+ @ssl_socket.close
141
+ super
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Socketry
4
+ # Transmission Control Protocol
5
+ module TCP
6
+ # Transmission Control Protocol servers: Accept connections from the network
7
+ class Server
8
+ include Socketry::Timeout
9
+ alias uptime lifetime
10
+
11
+ attr_reader :read_timeout, :write_timeout, :resolver, :socket_class
12
+
13
+ # Create a new TCP server, yielding the server socket and auto-closing it
14
+ def self.open(hostname_or_port, port = nil, **args)
15
+ server = new(hostname_or_port, port, **args)
16
+ result = yield server
17
+ server.close
18
+ result
19
+ end
20
+
21
+ # Create a new TCP server
22
+ #
23
+ # @return [Socketry::TCP::Server]
24
+ def initialize(
25
+ hostname_or_port,
26
+ port = nil,
27
+ read_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:read],
28
+ write_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:write],
29
+ timer: Socketry::Timeout::DEFAULT_TIMER.new,
30
+ resolver: Socketry::Resolver::DEFAULT_RESOLVER,
31
+ server_class: ::TCPServer,
32
+ socket_class: ::TCPSocket
33
+ )
34
+ @read_timeout = read_timeout
35
+ @write_timeout = write_timeout
36
+ @resolver = resolver
37
+ @socket_class = socket_class
38
+
39
+ if port
40
+ @server = server_class.new(@resolver.resolve(hostname_or_port).to_s, port)
41
+ else
42
+ @server = server_class.new(hostname_or_port)
43
+ end
44
+
45
+ start_timer(timer)
46
+ end
47
+
48
+ # Accept a connection to the server
49
+ #
50
+ # @param timeout [Numeric, NilClass] seconds to wait before aborting the accept
51
+ # @return [Socketry::TCP::Socket]
52
+ def accept(timeout: nil)
53
+ set_timeout(timeout)
54
+
55
+ begin
56
+ # Note: `exception: false` for TCPServer#accept_nonblock is only supported in Ruby 2.3+
57
+ ruby_socket = @server.accept_nonblock
58
+ rescue IO::WaitReadable, Errno::EAGAIN
59
+ # Ruby 2.2 has trouble using io/wait here
60
+ retry if IO.select([@server], nil, nil, time_remaining(timeout))
61
+ raise Socketry::TimeoutError, "no connection received after #{timeout} seconds"
62
+ end
63
+
64
+ Socketry::TCP::Socket.new(
65
+ read_timeout: @read_timeout,
66
+ write_timeout: @write_timeout,
67
+ resolver: @resolver,
68
+ socket_class: @socket_class
69
+ ).from_socket(ruby_socket)
70
+ ensure
71
+ clear_timeout(timeout)
72
+ end
73
+
74
+ # Close the server
75
+ def close
76
+ return false unless @server
77
+ @server.close rescue nil
78
+ @server = nil
79
+ true
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Socketry
4
+ # Transmission Control Protocol
5
+ module TCP
6
+ # Transmission Control Protocol sockets: Provide stream-like semantics
7
+ class Socket
8
+ include Socketry::Timeout
9
+
10
+ attr_reader :remote_addr, :remote_port, :local_addr, :local_port
11
+ attr_reader :read_timeout, :write_timeout, :resolver, :socket_class
12
+
13
+ # Create a Socketry::TCP::Socket with the default options, then connect
14
+ # to the given host.
15
+ #
16
+ # @param remote_addr [String] DNS name or IP address of the host to connect to
17
+ # @param remote_port [Fixnum] TCP port to connect to
18
+ # @return [Socketry::TCP::Socket]
19
+ def self.connect(remote_addr, remote_port, **args)
20
+ new.connect(remote_addr, remote_port, **args)
21
+ end
22
+
23
+ # Create an unconnected Socketry::TCP::Socket
24
+ #
25
+ # @param read_timeout [Numeric] Seconds to wait before an uncompleted read errors
26
+ # @param write_timeout [Numeric] Seconds to wait before an uncompleted write errors
27
+ # @param timer [Object] A timekeeping object to use for measuring timeouts
28
+ # @param resolver [Object] A resolver object to use for resolving DNS names
29
+ # @param socket_class [Object] Underlying socket class which implements I/O ops
30
+ # @return [Socketry::TCP::Socket]
31
+ def initialize(
32
+ read_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:read],
33
+ write_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:write],
34
+ timer: Socketry::Timeout::DEFAULT_TIMER.new,
35
+ resolver: Socketry::Resolver::DEFAULT_RESOLVER,
36
+ socket_class: ::Socket
37
+ )
38
+ @read_timeout = read_timeout
39
+ @write_timeout = write_timeout
40
+
41
+ @socket_class = socket_class
42
+ @resolver = resolver
43
+
44
+ @family = nil
45
+ @socket = nil
46
+
47
+ @remote_addr = nil
48
+ @remote_port = nil
49
+ @local_addr = nil
50
+ @local_port = nil
51
+
52
+ start_timer(timer)
53
+ end
54
+
55
+ # Connect to a remote host
56
+ #
57
+ # @param remote_addr [String] DNS name or IP address of the host to connect to
58
+ # @param remote_port [Fixnum] TCP port to connect to
59
+ # @param local_addr [String] DNS name or IP address to bind to locally
60
+ # @param local_port [Fixnum] Local TCP port to bind to
61
+ # @param timeout [Numeric] Number of seconds to wait before aborting connect
62
+ # @param socket_class [Class] Custom low-level socket class
63
+ # @raise [Socketry::AddressError] an invalid address was given
64
+ # @raise [Socketry::TimeoutError] connect operation timed out
65
+ # @return [self]
66
+ def connect(
67
+ remote_addr,
68
+ remote_port,
69
+ local_addr: nil,
70
+ local_port: nil,
71
+ timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:connect]
72
+ )
73
+ ensure_disconnected
74
+
75
+ @remote_addr = remote_addr
76
+ @remote_port = remote_port
77
+ @local_addr = local_addr
78
+ @local_port = local_port
79
+
80
+ begin
81
+ set_timeout(timeout)
82
+
83
+ remote_addr = @resolver.resolve(remote_addr, timeout: time_remaining(timeout))
84
+ local_addr = @resolver.resolve(local_addr, timeout: time_remaining(timeout)) if local_addr
85
+ raise ArgumentError, "expected IPAddr from resolver, got #{remote_addr.class}" unless remote_addr.is_a?(IPAddr)
86
+
87
+ if remote_addr.ipv4?
88
+ @family = ::Socket::AF_INET
89
+ elsif remote_addr.ipv6?
90
+ @family = ::Socket::AF_INET6
91
+ else raise Socketry::AddressError, "unsupported IP address family: #{remote_addr}"
92
+ end
93
+
94
+ socket = @socket_class.new(@family, ::Socket::SOCK_STREAM, 0)
95
+ socket.bind Addrinfo.tcp(local_addr.to_s, local_port) if local_addr
96
+ remote_sockaddr = ::Socket.sockaddr_in(remote_port, remote_addr.to_s)
97
+
98
+ # Note: `exception: false` for Socket#connect_nonblock is only supported in Ruby 2.3+
99
+ begin
100
+ socket.connect_nonblock(remote_sockaddr)
101
+ rescue Errno::EINPROGRESS, Errno::EALREADY
102
+ # Earlier JRuby 9.x versions do not seem to correctly support Socket#wait_writable in this case
103
+ # Newer versions seem to behave correctly
104
+ retry if IO.select(nil, [socket], nil, time_remaining(timeout))
105
+
106
+ socket.close
107
+ raise Socketry::TimeoutError, "connection to #{remote_addr}:#{remote_port} timed out"
108
+ rescue Errno::EISCONN
109
+ # Sometimes raised when we've connected successfully
110
+ end
111
+
112
+ @socket = socket
113
+ ensure
114
+ clear_timeout(timeout)
115
+ end
116
+
117
+ self
118
+ end
119
+
120
+ # Re-establish a lost TCP connection
121
+ #
122
+ # @param timeout [Numeric] Number of seconds to wait before aborting re-connect
123
+ # @raise [Socketry::StateError] not in a disconnected state
124
+ def reconnect(timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:connect])
125
+ ensure_disconnected
126
+ raise StateError, "can't reconnect: never completed initial connection" unless @remote_addr
127
+ connect(@remote_addr, @remote_port, local_addr: @local_addr, local_port: @local_port, timeout: timeout)
128
+ end
129
+
130
+ # Wrap a Ruby/low-level socket in an Socketry::TCP::Socket
131
+ #
132
+ # @param socket [::Socket] (or specified socket_class) low-level socket to wrap
133
+ def from_socket(socket)
134
+ ensure_disconnected
135
+ raise TypeError, "expected #{@socket_class}, got #{socket.class}" unless socket.is_a?(@socket_class)
136
+ @socket = socket
137
+ self
138
+ end
139
+
140
+ # Perform a non-blocking read operation
141
+ #
142
+ # @param size [Fixnum] number of bytes to attempt to read
143
+ # @param outbuf [String, NilClass] an optional buffer into which data should be read
144
+ # @raise [Socketry::Error] an I/O operation failed
145
+ # @return [String, :wait_readable] data read, or :wait_readable if operation would block
146
+ def read_nonblock(size, outbuf: nil)
147
+ ensure_connected
148
+ case outbuf
149
+ when String
150
+ @socket.read_nonblock(size, outbuf, exception: false)
151
+ when NilClass
152
+ @socket.read_nonblock(size, exception: false)
153
+ else raise TypeError, "unexpected outbuf class: #{outbuf.class}"
154
+ end
155
+ rescue IO::WaitReadable
156
+ # Some buggy Rubies continue to raise this exception
157
+ :wait_readable
158
+ rescue IOError => ex
159
+ raise Socketry::Error, ex.message, ex.backtrace
160
+ end
161
+
162
+ # Read a partial amounth of data, blocking until it becomes available
163
+ #
164
+ # @param size [Fixnum] number of bytes to attempt to read
165
+ # @raise [Socketry::Error] an I/O operation failed
166
+ # @return [String]
167
+ def readpartial(size, outbuf: nil, timeout: @read_timeout)
168
+ set_timeout(timeout)
169
+
170
+ begin
171
+ while (result = read_nonblock(size, outbuf: outbuf)) == :wait_readable
172
+ next if @socket.wait_readable(read_timeout)
173
+ raise TimeoutError, "read timed out after #{timeout} seconds"
174
+ end
175
+ ensure
176
+ clear_timeout(timeout)
177
+ end
178
+
179
+ result || :eof
180
+ end
181
+
182
+ # Perform a non-blocking write operation
183
+ #
184
+ # @param data [String] number of bytes to attempt to read
185
+ # @raise [Socketry::Error] an I/O operation failed
186
+ # @return [Fixnum, :wait_writable] number of bytes written, or :wait_writable if op would block
187
+ def write_nonblock(data)
188
+ ensure_connected
189
+ @socket.write_nonblock(data, exception: false)
190
+ rescue IO::WaitWriteable
191
+ # Some buggy Rubies continue to raise this exception
192
+ :wait_writable
193
+ rescue IOError => ex
194
+ raise Socketry::Error, ex.message, ex.backtrace
195
+ end
196
+
197
+ # Write a partial amounth of data, blocking until it's completed
198
+ #
199
+ # @param data [String] number of bytes to attempt to read
200
+ # @raise [Socketry::Error] an I/O operation failed
201
+ # @return [Fixnum, :wait_writable] number of bytes written, or :wait_writable if op would block
202
+ def writepartial(data, timeout: @write_timeout)
203
+ set_timeout(timeout)
204
+
205
+ begin
206
+ while (result = write_nonblock(data)) == :wait_writable
207
+ next if @socket.wait_writable(read_timeout)
208
+ raise TimeoutError, "write timed out after #{timeout} seconds"
209
+ end
210
+ ensure
211
+ clear_timeout(timeout)
212
+ end
213
+
214
+ result || :eof
215
+ end
216
+
217
+ # Check whether Nagle's algorithm has been disabled
218
+ #
219
+ # @return [true] Nagle's algorithm has been explicitly disabled
220
+ # @return [false] Nagle's algorithm is enabled (default)
221
+ def nodelay
222
+ ensure_connected
223
+ @socket.getsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY).int.nonzero?
224
+ end
225
+
226
+ # Disable or enable Nagle's algorithm
227
+ #
228
+ # @param flag [true, false] disable or enable coalescing multiple writesusing Nagle's algorithm
229
+ def nodelay=(flag)
230
+ ensure_connected
231
+ @socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, flag ? 1 : 0)
232
+ end
233
+
234
+ # Return a raw Ruby I/O object
235
+ #
236
+ # @return [IO] Ruby I/O object
237
+ def to_io
238
+ ensure_connected
239
+ ::IO.try_convert(@socket)
240
+ end
241
+
242
+ # Close the socket
243
+ #
244
+ # @return [true, false] true if the socket was open, false if closed
245
+ def close
246
+ return false unless connected?
247
+ @socket.close
248
+ true
249
+ ensure
250
+ @socket = nil
251
+ end
252
+
253
+ # Is the socket currently connected?
254
+ #
255
+ # This method returns the local connection state. However, it's possible
256
+ # the remote side has closed the connection, so it's not actually
257
+ # possible to actually know if the socket is actually still open without
258
+ # reading from or writing to it. It's sort of like the Heisenberg
259
+ # uncertainty principle of sockets.
260
+ #
261
+ # @return [true, false] do we locally think the socket is open?
262
+ def connected?
263
+ @socket != nil
264
+ end
265
+
266
+ private
267
+
268
+ def ensure_connected
269
+ return true if connected?
270
+ raise StateError, "not connected"
271
+ end
272
+
273
+ def ensure_disconnected
274
+ return true unless connected?
275
+ raise StateError, "already connected"
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Socketry
4
+ # Timeout subsystem
5
+ module Timeout
6
+ DEFAULT_TIMER = Hitimes::Interval
7
+
8
+ # Default timeouts (in seconds)
9
+ DEFAULT_TIMEOUTS = {
10
+ read: 5,
11
+ write: 5,
12
+ connect: 5
13
+ }.freeze
14
+
15
+ # Start a timer in the included object
16
+ #
17
+ # @param timer [#start, #to_f] a timer object (ideally monotonic)
18
+ # @return [true] timer started successfully
19
+ # @raise [Socketry::InternalError] if timer is already started
20
+ def start_timer(timer = DEFAULT_TIMER_CLASS.new)
21
+ raise Socketry::InternalError, "timer already started" if defined?(@timer)
22
+ raise Socketry::InternalError, "deadline already set" if defined?(@deadline)
23
+
24
+ @deadline = nil
25
+ @timer = timer
26
+ @timer.start
27
+ true
28
+ end
29
+
30
+ # Return how long since the timer has been started
31
+ #
32
+ # @return [Float] number of seconds since the timer has been started
33
+ # @raise [Socketry::InternalError] if timer has not been started
34
+ def lifetime
35
+ raise Socketry::InternalError, "timer not started" unless @timer
36
+ @timer.to_f
37
+ end
38
+
39
+ # Set a timeout. Only one timeout may be active at a given time for a given object.
40
+ #
41
+ # @param timeout [Numeric] number of seconds until the timeout is reached
42
+ # @return [Float] deadline (relative to #lifetime) at which the timeout is reached
43
+ # @raise [Socketry::InternalError] if timeout is already set
44
+ def set_timeout(timeout)
45
+ raise Socketry::InternalError, "deadline already set" if @deadline
46
+ return unless timeout
47
+ @deadline = lifetime + timeout
48
+ end
49
+
50
+ # Clear an already-set timeout
51
+ #
52
+ # @param timeout [Numeric] to gauge whether the timeout actually needs to be cleared
53
+ # @raise [Socketry::InternalError] if timeout has not been set
54
+ def clear_timeout(timeout)
55
+ return unless timeout
56
+ raise Socketry::InternalError, "no deadline set" unless @deadline
57
+ @deadline = nil
58
+ end
59
+
60
+ # Calculate number of seconds remaining until we hit the timeout
61
+ #
62
+ # @param timeout [Numeric] to gauge whether a timeout needs to be calculated
63
+ # @return [Float] number of seconds remaining until we hit the timeout
64
+ # @raise [Socketry::TimeoutError] if we've already hit the timeout
65
+ # @raise [Socketry::InternalError] if timeout has not been set
66
+ def time_remaining(timeout)
67
+ return unless timeout
68
+ raise Socketry::InternalError, "no deadline set" unless @deadline
69
+ remaining = @deadline - lifetime
70
+ raise Socketry::TimeoutError, "time expired" if remaining <= 0
71
+ remaining
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Socketry
4
+ # User Datagram Protocol: "fire-and-forget" packet protocol
5
+ module UDP
6
+ # User Datagram Protocol sockets
7
+ class Socket
8
+ include Socketry::Timeout
9
+
10
+ attr_reader :read_timeout, :write_timeout, :resolver, :socket_class
11
+
12
+ # Create a UDP socket matching the given socket's address family
13
+ #
14
+ # @param remote_addr [String] address to connect/bind to
15
+ # @return [Socketry::UDP::Socket]
16
+ def self.from_addr(remote_addr, resolver: Socketry::Resolver::DEFAULT_RESOLVER)
17
+ addr = resolver.resolve(remote_addr)
18
+ if addr.ipv4?
19
+ new(family: :ipv4)
20
+ elsif addr.ipv6?
21
+ new(family: :ipv6)
22
+ else raise Socketry::AddressError, "unsupported IP address family: #{addr}"
23
+ end
24
+ end
25
+
26
+ # Bind to the given address and port
27
+ #
28
+ # @return [Socketry::UDP::Socket]
29
+ def self.bind(remote_addr, remote_port, resolver: Socketry::Resolver::DEFAULT_RESOLVER)
30
+ from_addr(remote_addr, resolver: resolver).bind(remote_addr, remote_port)
31
+ end
32
+
33
+ # Connect to the given address and port
34
+ #
35
+ # @return [Socketry::UDP::Socket]
36
+ def self.connect(remote_addr, remote_port, resolver: Socketry::Resolver::DEFAULT_RESOLVER)
37
+ from_addr(remote_addr, resolver: resolver).connect(remote_addr, remote_port)
38
+ end
39
+
40
+ # Create a new UDP socket
41
+ #
42
+ # @return [Socketry::UDP::Socket]
43
+ def initialize(
44
+ family: :ipv4,
45
+ read_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:read],
46
+ write_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:write],
47
+ timer: Socketry::Timeout::DEFAULT_TIMER.new,
48
+ resolver: Socketry::Resolver::DEFAULT_RESOLVER,
49
+ socket_class: ::UDPSocket
50
+ )
51
+ case family
52
+ when :ipv4
53
+ @address_family = ::Socket::AF_INET
54
+ when :ipv6
55
+ @address_family = ::Socket::AF_INET6
56
+ when ::Socket::AF_INET, ::Socket::AF_INET6
57
+ @address_family = address_family
58
+ else raise ArgumentError, "invalid address family: #{address_family.inspect}"
59
+ end
60
+
61
+ @socket = socket_class.new(@address_family)
62
+ @read_timeout = read_timeout
63
+ @write_timeout = write_timeout
64
+ @resolver = resolver
65
+
66
+ start_timer(timer)
67
+ end
68
+
69
+ # Bind to the given address and port
70
+ #
71
+ # @return [self]
72
+ def bind(remote_addr, remote_port)
73
+ @socket.bind(@resolver.resolve(remote_addr), remote_port)
74
+ self
75
+ rescue => ex
76
+ # TODO: more specific exceptions
77
+ raise Socketry::Error, ex.message, ex.backtrace
78
+ end
79
+
80
+ # Create a new UDP socket
81
+ #
82
+ # @return [self]
83
+ def connect(remote_addr, remote_port)
84
+ @socket.connect(@resolver.resolve(remote_addr), remote_port)
85
+ self
86
+ rescue => ex
87
+ # TODO: more specific exceptions
88
+ raise Socketry::Error, ex.message, ex.backtrace
89
+ end
90
+
91
+ # Perform a non-blocking receive
92
+ #
93
+ # @return [String, :wait_readable] received packet or indication to wait
94
+ def recvfrom_nonblock(maxlen)
95
+ @socket.recvfrom_nonblock(maxlen)
96
+ rescue ::IO::WaitReadable
97
+ :wait_readable
98
+ rescue => ex
99
+ # TODO: more specific exceptions
100
+ raise Socketry::Error, ex.message, ex.backtrace
101
+ end
102
+
103
+ # Perform a blocking receive
104
+ #
105
+ # @return [String] received data
106
+ def recvfrom(maxlen, timeout: @read_timeout)
107
+ set_timeout(timeout)
108
+
109
+ begin
110
+ while (result = recvfrom_nonblock(maxlen)) == :wait_readable
111
+ next if @socket.wait_readable(time_remaining(timeout))
112
+ raise Socketry::TimeoutError, "recvfrom timed out after #{timeout} seconds"
113
+ end
114
+ ensure
115
+ clear_timeout(imeout)
116
+ end
117
+
118
+ result
119
+ end
120
+
121
+ # Send data to the given host and port
122
+ def send(msg, host:, port:)
123
+ @socket.send(msg, 0, @resolver.resolve(host), port)
124
+ rescue => ex
125
+ # TODO: more specific exceptions
126
+ raise Socketry::Error, ex.message, ex.backtrace
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Socketry
4
+ VERSION = "0.1.0"
5
+ end
data/socketry.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "socketry/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "socketry"
8
+ spec.version = Socketry::VERSION
9
+ spec.authors = ["Tony Arcieri", "Zachary Anker"]
10
+ spec.email = ["bascule@gmail.com"]
11
+ spec.licenses = ["MIT"]
12
+ spec.homepage = "https://github.com/celluloid/socketry/"
13
+ spec.summary = "High-level wrappers for Ruby sockets with advanced thread-safe timeout support"
14
+ spec.description = <<-DESCRIPTION.strip.gsub(/\s+/, " ")
15
+ Socketry wraps Ruby's sockets with an advanced timeout engine which is able to provide multiple
16
+ simultaneous timeout behaviors in a thread-safe way.
17
+ DESCRIPTION
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.required_ruby_version = ">= 2.2.2"
25
+
26
+ spec.add_runtime_dependency "hitimes", ">= 1.2"
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.0"
29
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: socketry
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tony Arcieri
8
+ - Zachary Anker
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2016-09-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: hitimes
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '1.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '1.2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.0'
42
+ description: Socketry wraps Ruby's sockets with an advanced timeout engine which is
43
+ able to provide multiple simultaneous timeout behaviors in a thread-safe way.
44
+ email:
45
+ - bascule@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".gitignore"
51
+ - ".rspec"
52
+ - ".rubocop.yml"
53
+ - ".ruby-version"
54
+ - ".travis.yml"
55
+ - CHANGES.md
56
+ - Gemfile
57
+ - Guardfile
58
+ - LICENSE.txt
59
+ - README.md
60
+ - Rakefile
61
+ - lib/socketry.rb
62
+ - lib/socketry/exceptions.rb
63
+ - lib/socketry/resolver/resolv.rb
64
+ - lib/socketry/resolver/system.rb
65
+ - lib/socketry/ssl/server.rb
66
+ - lib/socketry/ssl/socket.rb
67
+ - lib/socketry/tcp/server.rb
68
+ - lib/socketry/tcp/socket.rb
69
+ - lib/socketry/timeout.rb
70
+ - lib/socketry/udp/socket.rb
71
+ - lib/socketry/version.rb
72
+ - socketry.gemspec
73
+ homepage: https://github.com/celluloid/socketry/
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.2.2
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 2.5.1
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: High-level wrappers for Ruby sockets with advanced thread-safe timeout support
97
+ test_files: []