nanoserve 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: '019575a37a4493c4816afbe726f0c2a0495049ea'
4
+ data.tar.gz: bf54d9e145418838ce211764c5555ffaa68ae97d
5
+ SHA512:
6
+ metadata.gz: 5576fec9ea2afa3d053996248045c8df2d418a8eab235354727b983b7457f2aaebe0f88a847f745b5534c499bc58c081d3c1689283df38fb4118c7cf66a0b06f
7
+ data.tar.gz: 18c903426feae6ec6eb8c1d27e693f8cda25553635cff9087da1d71908245116e3d9573b21bba04d277264729accf8bddb4d4f3e660f2d410eba28b71f791741
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,29 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ nanoserve (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.1)
10
+ method_source (0.8.2)
11
+ minitest (5.10.3)
12
+ pry (0.10.4)
13
+ coderay (~> 1.1.0)
14
+ method_source (~> 0.8.1)
15
+ slop (~> 3.4)
16
+ rake (12.0.0)
17
+ slop (3.6.0)
18
+
19
+ PLATFORMS
20
+ ruby
21
+
22
+ DEPENDENCIES
23
+ minitest (~> 5.10)
24
+ nanoserve!
25
+ pry
26
+ rake (~> 12.0)
27
+
28
+ BUNDLED WITH
29
+ 1.15.4
data/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2017, Loic Nageleisen
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+ require 'bundler'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'test'
8
+ t.test_files = FileList['test/test_*.rb']
9
+ t.verbose = true
10
+ end
data/lib/nanoserve.rb ADDED
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'logger'
5
+ require 'time'
6
+
7
+ module NanoServe
8
+ class TCPResponder
9
+ def initialize(host, port, &block)
10
+ @host = host
11
+ @port = port
12
+ @block = block
13
+ @thr = nil
14
+ end
15
+
16
+ def start(y)
17
+ server = TCPServer.new(@port)
18
+
19
+ @thr = Thread.new do
20
+ Thread.abort_on_exception = true
21
+ conn = server.accept
22
+ port, host = conn.peeraddr[1, 2]
23
+ client = "#{host}:#{port}"
24
+ logger.debug "#{client}: connected"
25
+
26
+ begin
27
+ @block.call(conn, y)
28
+ rescue EOFError
29
+ logger.debug "#{client}: disconnected"
30
+ ensure
31
+ logger.debug "#{client}: closed"
32
+ conn.close
33
+ end
34
+ end
35
+
36
+ return unless block_given?
37
+
38
+ yield
39
+
40
+ @thr.join
41
+ server.close
42
+
43
+ y
44
+ end
45
+
46
+ def stop
47
+ @thr.kill
48
+ end
49
+
50
+ def logger
51
+ @logger ||= Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
52
+ end
53
+ end
54
+
55
+ class HTTPResponder < TCPResponder
56
+ def initialize(host, port)
57
+ super(host, port) do |conn, y|
58
+ req = Request.new
59
+ buf = +''
60
+ loop do
61
+ line = conn.readline
62
+ break if line.chomp == ''
63
+ req << line
64
+ buf << line
65
+ end
66
+ logger.debug "request:\n" + buf.gsub(/^/, ' ')
67
+
68
+ res = Response.new
69
+ logger.debug 'calling'
70
+ yield(res, req, y)
71
+ logger.debug "response:\n" + res.to_s.gsub(/^/, ' ')
72
+ conn.write(res.to_s)
73
+ end
74
+ end
75
+
76
+ class RequestError < StandardError; end
77
+
78
+ class Request
79
+ def initialize
80
+ @method = nil
81
+ @uri = nil
82
+ @http_version = nil
83
+ @sep = nil
84
+ @headers = {}
85
+ end
86
+
87
+ def params
88
+ Hash[*@uri.query.split('&').map { |kv| kv.split('=') }.flatten]
89
+ end
90
+
91
+ def <<(line)
92
+ if @method.nil?
93
+ parse_request(line.chomp)
94
+ elsif @sep.nil?
95
+ parse_header(line.chomp)
96
+ else
97
+ @body << line
98
+ end
99
+ end
100
+
101
+ REQ_RE = %r{(?<method>[A-Z]+)\s+(?<path>\S+)\s+(?<version>HTTP/\d+.\d+)$}
102
+
103
+ def parse_request(str)
104
+ unless (m = str.match(REQ_RE))
105
+ raise RequestError, "cannot parse request: '#{str}'"
106
+ end
107
+
108
+ @method = parse_method(m[:method])
109
+ @uri = parse_path(m[:path])
110
+ @http_version = parse_version(m[:version])
111
+ end
112
+
113
+ def parse_method(str)
114
+ str
115
+ end
116
+
117
+ def parse_path(str)
118
+ URI(str)
119
+ end
120
+
121
+ def parse_version(str)
122
+ str
123
+ end
124
+
125
+ def parse_header(str)
126
+ (@sep = '' && return) if str == ''
127
+
128
+ unless (m = str.match(/(?<header>[A-Z][-A-Za-z]*):\s+(?<value>.+)$/))
129
+ raise RequestError, "cannot parse header: '#{str}'"
130
+ end
131
+
132
+ @headers[m[:header]] = m[:value]
133
+ end
134
+ end
135
+
136
+ class Response
137
+ def headers
138
+ {
139
+ 'Date' => date,
140
+ 'Content-Type' => content_type,
141
+ 'Content-Length' => content_length,
142
+ 'Last-Modified' => last_modified,
143
+ 'Server' => server,
144
+ 'ETag' => etag,
145
+ 'Connection' => connection,
146
+ }
147
+ end
148
+
149
+ def body
150
+ @body ||= ''
151
+ end
152
+
153
+ def body=(value)
154
+ @body = value.tap { @content_length = body.bytes.count.to_s }
155
+ end
156
+
157
+ def to_s
158
+ (status_line + header_block + body_block).encode('ASCII-8BIT')
159
+ end
160
+
161
+ private
162
+
163
+ def status_code
164
+ 200
165
+ end
166
+
167
+ def status_string
168
+ 'OK'
169
+ end
170
+
171
+ def status_line
172
+ "HTTP/1.1 #{status_code} #{status_string}\r\n"
173
+ end
174
+
175
+ def header_block
176
+ headers.map { |k, v| [k, v].join(': ') }.join("\r\n") + "\r\n\r\n"
177
+ end
178
+
179
+ def body_block
180
+ body
181
+ end
182
+
183
+ def content_length
184
+ @content_length ||= body.bytes.count.to_s
185
+ end
186
+
187
+ def content_type
188
+ @content_type ||= 'text/html; charset=UTF-8'
189
+ end
190
+
191
+ def date
192
+ @date ||= Time.now.httpdate
193
+ end
194
+
195
+ def last_modified
196
+ @last_modified ||= date
197
+ end
198
+
199
+ def etag
200
+ @etag ||= SecureRandom.uuid
201
+ end
202
+
203
+ def server
204
+ 'NanoServe'
205
+ end
206
+
207
+ def connection
208
+ 'close'
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'nanoserve'
5
+
6
+ require 'securerandom'
7
+ require 'uri'
8
+ require 'net/http'
9
+ require 'pry'
10
+ require 'pathname'
11
+
12
+ class TestNanoServe < MiniTest::Test
13
+ def test_tcp_responder
14
+ uuid = SecureRandom.uuid.encode('UTF-8')
15
+ uri = URI('tcp://localhost:2000')
16
+
17
+ r = NanoServe::TCPResponder.new(uri.host, uri.port) do |conn, buf|
18
+ buf << conn.readpartial(1024)
19
+ end
20
+
21
+ buf = r.start(+'') do
22
+ s = TCPSocket.new(uri.host, uri.port)
23
+ s.write(uuid)
24
+ s.close
25
+ end
26
+
27
+ assert_equal(uuid, buf)
28
+ end
29
+
30
+ def test_http_responder
31
+ uuid = SecureRandom.uuid.encode('UTF-8')
32
+ uri = URI('http://localhost:2000')
33
+
34
+ r = NanoServe::HTTPResponder.new(uri.host, uri.port) do |res, req, y|
35
+ y << req
36
+
37
+ res.body = <<-EOS.gsub(/^ {8}/, '')
38
+ <html>
39
+ <head>
40
+ <title>An Example Page</title>
41
+ </head>
42
+ <body>
43
+ Hello World, this is a very simple HTML document.
44
+ </body>
45
+ </html>
46
+ EOS
47
+ end
48
+
49
+ req = r.start([]) do
50
+ Net::HTTP.get(uri + "test?uuid=#{uuid}")
51
+ end
52
+
53
+ assert_equal(uuid, req.first.params['uuid'])
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nanoserve
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Loic Nageleisen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-09-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12.0'
55
+ description: " NanoServe allows you to wait for an external call and act on it.\n"
56
+ email: loic.nageleisen@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - Gemfile
62
+ - Gemfile.lock
63
+ - LICENSE
64
+ - Rakefile
65
+ - lib/nanoserve.rb
66
+ - test/test_nanoserve.rb
67
+ homepage:
68
+ licenses:
69
+ - 3BSD
70
+ metadata: {}
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '2.3'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 2.6.13
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: Listen to one-shot connections
91
+ test_files: []