socketry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []