udp_rest 0.9.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: 5c547a1a4aeafe5240118f42f1d060b861bba377
4
+ data.tar.gz: 1f67cc942b89d5d8da7b12066d019b96eaa54cf1
5
+ SHA512:
6
+ metadata.gz: 637dbc37f4e52a049b500f09aa60dcf167f27434fcfc9c2ea5c5fe8b442fc128e1d8cccf6b35c14ea42f3596c135cd1df3b03a1a5dea8f34f663dc8d56ebac33
7
+ data.tar.gz: e592fdfff3610df362d4a558d15bda20dde2b61302f4bafdb1ddbaebca62053291e71f101e349308ea8eb849725ae21e3cb6050ba5bcaaaa44fdb0b5d780ac0e
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in udp_rest.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,24 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ udp_rest (0.9.0)
5
+ colorize
6
+ trollop
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ colorize (0.8.1)
12
+ rake (10.5.0)
13
+ trollop (2.1.2)
14
+
15
+ PLATFORMS
16
+ ruby
17
+
18
+ DEPENDENCIES
19
+ bundler (~> 1.12)
20
+ rake (~> 10.0)
21
+ udp_rest!
22
+
23
+ BUNDLED WITH
24
+ 1.12.5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Nathan Reed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "udp_rest"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'time'
5
+ require 'udp_rest'
6
+
7
+ req_count = 0
8
+ port = (ARGV.last || 7890).to_i
9
+ puts "listening on 0.0.0.0:#{port}..."
10
+
11
+ UDPRest::Server.new(:port => port) do |s|
12
+ s.get '/' do
13
+ "Hello, World!\nVisit http://github.com/reednj/udp_rest for more info"
14
+ end
15
+
16
+ s.get '/hello' do
17
+ 'hello'
18
+ end
19
+
20
+ s.post '/time' do
21
+ Time.now.to_s
22
+ end
23
+
24
+ s.get '/time/unix' do
25
+ Time.now.to_i
26
+ end
27
+
28
+ s.get '/time/iso' do
29
+ Time.now.iso8601
30
+ end
31
+
32
+ s.get '/count' do
33
+ req_count += 1
34
+ req_count.to_s
35
+ end
36
+
37
+ s.get '/too_long' do
38
+ 'a' * 600
39
+ end
40
+
41
+ end
data/exe/udp-rest ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rest_client'
3
+ App.new.main
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'socket'
4
+ require 'colorize'
5
+ require 'trollop'
6
+ require 'udp_rest'
7
+
8
+ class App
9
+ def main
10
+ valid_methods = ['GET', 'PUT', 'POST', 'DELETE']
11
+ @opts = Trollop::options do
12
+ version "UDP RestClient (c) 2016 @reednj"
13
+ banner "Usage: udp-rest [options] <url>"
14
+ opt :method, "HTTP Method (GET, POST etc)", :type => :string, :default => 'GET'
15
+ opt :headers, "Show the response headers", :default => false
16
+ end
17
+
18
+ Trollop::educate if ARGV.empty?
19
+ url = ARGV.last
20
+ url = "uhttp://" + url unless url.start_with? 'uhttp://'
21
+
22
+ begin
23
+ if !valid_methods.include? @opts[:method].upcase
24
+ raise "Invalid REST method '#{@opts[:method]}'"
25
+ end
26
+
27
+ r = UDPRest::Client.uhttp(@opts[:method], url)
28
+ print_response(r)
29
+ rescue => e
30
+ puts e
31
+ end
32
+ end
33
+
34
+ def print_response(r)
35
+ if @opts[:headers]
36
+ puts r.ok? ? r.status_line.green : r.status_line.red
37
+ puts ''
38
+ end
39
+
40
+ puts r.text
41
+ end
42
+ end
data/lib/udp_rest.rb ADDED
@@ -0,0 +1,139 @@
1
+ require 'uri'
2
+ require 'socket'
3
+
4
+ require 'worker_thread'
5
+ require "udp_rest/version"
6
+ require "udp_rest/udp"
7
+ require "udp_rest/uhttp"
8
+
9
+ module UDPRest
10
+ class Server
11
+ def initialize(options = {})
12
+ @udp = UDPServer.new
13
+ @routes = {}
14
+
15
+ if block_given?
16
+ yield(self)
17
+ port = options[:port] || 80
18
+ options[:host] = options[:host] || '0.0.0.0'
19
+ self.listen(port, options)
20
+ end
21
+ end
22
+
23
+ def udp_server
24
+ @udp
25
+ end
26
+
27
+ def post(path, &block)
28
+ add_route('POST', path, &block)
29
+ end
30
+
31
+ def get(path, &block)
32
+ add_route('GET', path, &block)
33
+ end
34
+
35
+ def add_route(req_method, path, &block)
36
+ key = "#{req_method.upcase} #{path}"
37
+ @routes[key] = Proc.new &block
38
+ return key
39
+ end
40
+
41
+ def route_request(request)
42
+ key = "#{request.req_method.upcase} #{request.path}"
43
+ puts key
44
+
45
+ block = @routes[key]
46
+ return respond(404, 'Not Found') if block.nil?
47
+
48
+ # handle the response. No matter what gets returned
49
+ # we want to try and make it into something useful
50
+ result = block.call(request, self)
51
+ return result if result.is_a? UHTTPResponse
52
+ return respond(200, result.to_s) if result.respond_to? :to_s
53
+ return respond(200, 'ok')
54
+ end
55
+
56
+ def listen(port, options = {})
57
+ port = port || 80
58
+
59
+ @udp.listen(port, options) do |packet|
60
+ response = nil
61
+
62
+ if response.nil?
63
+ begin
64
+ request = UHTTPRequest.from_packet packet
65
+ rescue => e
66
+ puts "400 BAD REQUEST: #{e}"
67
+ response = respond(400, 'Bad Request')
68
+ end
69
+ end
70
+
71
+ if response.nil?
72
+ begin
73
+ response = route_request(request)
74
+
75
+ if response.to_s.bytesize > udp_server.max_packet_size
76
+ raise "response too long (#{response.to_s.bytesize} bytes)"
77
+ end
78
+ rescue => e
79
+ puts "500 APPLICATION ERROR: #{e}"
80
+ response = respond(500, 'Application Error')
81
+ end
82
+ end
83
+
84
+ @udp.send(response.to_s, packet.src_addr, packet.src_port)
85
+ end
86
+ end
87
+
88
+ def respond(code, text)
89
+ UHTTPResponse.new(code, :text => text)
90
+ end
91
+ end
92
+
93
+ class Client
94
+ attr_accessor :host
95
+ attr_accessor :port
96
+ attr_accessor :socket
97
+ attr_accessor :timeout
98
+
99
+ def initialize(host, port)
100
+ @max_packet_size = 512
101
+
102
+ self.host = host
103
+ self.port = port
104
+ self.socket = UDPSocket.new
105
+ self.timeout = 5.0
106
+ end
107
+
108
+ def send_text(text)
109
+ thread = WorkerThread.new.start :timeout => self.timeout do
110
+ self.socket.send(text, 0, self.host, self.port)
111
+ response_data = self.socket.recvfrom(@max_packet_size)
112
+ UDPPacket.new(response_data)
113
+ end
114
+
115
+ thread.join
116
+ packet = thread.value
117
+ raise "Request Timeout (#{host}:#{port})" if packet.nil?
118
+ return packet
119
+ end
120
+
121
+ def self.uhttp(req_method, url)
122
+ uri = URI(url)
123
+ client = self.new(uri.host, uri.port || 80)
124
+
125
+ req = UHTTPRequest.new
126
+ req.req_method = req_method
127
+ req.path = uri.path
128
+
129
+ packet = client.send_text(req.to_s)
130
+ UHTTPResponse.parse(packet.text)
131
+ end
132
+
133
+ def self.get(url)
134
+ self.uhttp('GET', url)
135
+ end
136
+
137
+ end
138
+
139
+ end
@@ -0,0 +1,71 @@
1
+ require 'uri'
2
+ require 'socket'
3
+
4
+ module UDPRest
5
+
6
+ class UDPRest::UDPServer
7
+ attr_accessor :socket
8
+
9
+ def initialize
10
+ @max_packet_size = 512
11
+ self.socket = UDPSocket.new
12
+ end
13
+
14
+ def max_packet_size
15
+ @max_packet_size
16
+ end
17
+
18
+ def listen(port, options = {})
19
+ @port = port.to_i
20
+ @host = options[:host] || '0.0.0.0'
21
+ self.socket.bind(@host, @port)
22
+
23
+ loop do
24
+ response = self.receive()
25
+ yield(response)
26
+ end
27
+ end
28
+
29
+ def receive
30
+ data = self.socket.recvfrom(@max_packet_size)
31
+ UDPPacket.new(data)
32
+ end
33
+
34
+ def send(text, host, port)
35
+ raise "message too long (max is #{@max_packet_size}b, was #{text.bytesize})" if text.bytesize > @max_packet_size
36
+ self.socket.send(text, 0, host, port)
37
+ end
38
+
39
+ def host
40
+ @host
41
+ end
42
+
43
+ def port
44
+ @port
45
+ end
46
+
47
+ end
48
+
49
+ class UDPRest::UDPPacket
50
+ attr_accessor :text
51
+ attr_accessor :addr_family
52
+ attr_accessor :src_port
53
+ attr_accessor :src_addr
54
+
55
+ def initialize(data = nil)
56
+ # assume this was initialized with the standard data structure
57
+ # that is returned by UDPSocket
58
+ if !data.nil?
59
+ self.text = data[0]
60
+ self.addr_family = data[1][0]
61
+ self.src_port = data[1][1]
62
+ self.src_addr = data[1][2]
63
+ end
64
+ end
65
+
66
+ def to_s
67
+ self.text
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,72 @@
1
+ require 'uri'
2
+ require 'socket'
3
+
4
+ module UDPRest
5
+
6
+ class UDPRest::UHTTPRequest
7
+ attr_accessor :req_method
8
+ attr_accessor :path
9
+ attr_accessor :protocol
10
+
11
+ def initialize
12
+ self.req_method = 'GET'
13
+ self.protocol = 'UHTTP/1.0'
14
+ self.path ='/'
15
+ end
16
+
17
+ def self.from_packet(p)
18
+ text = p
19
+ text = p.text if text.is_a? UDPPacket
20
+ data = text.split(' ')
21
+
22
+ raise 'invalid request' if data.length != 3
23
+ req = self.new
24
+ req.req_method = data[0]
25
+ req.path = data[1]
26
+ req.protocol = data[2]
27
+ return req
28
+ end
29
+
30
+ def to_s
31
+ self.path = '/' if path.nil? || path.empty?
32
+ "#{req_method} #{path} #{protocol}\n"
33
+ end
34
+ end
35
+
36
+ class UDPRest::UHTTPResponse
37
+ attr_accessor :code
38
+ attr_accessor :protocol
39
+ attr_accessor :text
40
+
41
+ def initialize(code, options = {})
42
+ self.code = code.to_i
43
+ self.protocol = options[:protocol] || 'UHTTP/1.0'
44
+ self.text = options[:text] || ''
45
+ end
46
+
47
+ def self.parse(s)
48
+ data = s.split("\n\n")
49
+ status = data[0].split(' ')
50
+ text = data[1] if data.length > 1
51
+ self.new(status[1], :text => text || '')
52
+ end
53
+
54
+ def ok?
55
+ code == 200
56
+ end
57
+
58
+ def status_text
59
+ return 'OK' if ok?
60
+ return 'FAILED'
61
+ end
62
+
63
+ def status_line
64
+ "#{protocol} #{code} #{status_text}"
65
+ end
66
+
67
+ def to_s
68
+ "#{status_line}\n\n#{text}"
69
+ end
70
+ end
71
+
72
+ end
@@ -0,0 +1,3 @@
1
+ module UDPRest
2
+ VERSION = "0.9.0"
3
+ end
@@ -0,0 +1,42 @@
1
+ # Helper for running threads in the background, with a timeout
2
+ # and error logging.
3
+ #
4
+ # Example:
5
+ #
6
+ # WorkerThread.new.start :timeout => 5.minutes do
7
+ # # long running task here...
8
+ # sleep 10.0
9
+ # end
10
+ #
11
+ class WorkerThread
12
+
13
+ def start(options = nil)
14
+ raise 'background_task needs a block' unless block_given?
15
+
16
+ options ||= {}
17
+
18
+ worker = Thread.new do
19
+ begin
20
+ yield
21
+ rescue => e
22
+ $stderr.puts "#{Time.now}\t#{e.class.to_s}\t#{e.message}\n"
23
+ raise e
24
+ end
25
+ end
26
+
27
+ # if the user set a timeout then we need a thread to monitor
28
+ # the worker to make sure it doesn't run too long
29
+ if !options[:timeout].nil?
30
+ Thread.new do
31
+ sleep options[:timeout].to_f
32
+
33
+ if worker.status != false
34
+ #$stderr.puts "#{Time.now}\tbackground_task thread timeout\n"
35
+ worker.kill
36
+ end
37
+ end
38
+ end
39
+
40
+ worker
41
+ end
42
+ end
data/readme.md ADDED
@@ -0,0 +1,35 @@
1
+ # REST over UDP
2
+
3
+ - Most REST requets are very small
4
+ - it should be possible to implement a simple version of HTTP that can run over UDP in order to make REST requets
5
+ - we have implemented that
6
+ - the max packet size is 512 bytes
7
+ - This gem makes it easy to create servers and clients for udp-rest
8
+ - it also contains a curl-like command line app
9
+ - obviously this is not going to work from any browser, but it can be useful for simple command line apps.
10
+
11
+ ## Try it out
12
+
13
+ There is a udp rest server running on uhttp.reednj.com. You can make requests to it by installing the gem.
14
+
15
+ gem install udp_rest
16
+ udp-rest uhttp.reednj.com
17
+
18
+ <SCREEN SHOT OF CONSOLE>
19
+
20
+ ## Server
21
+
22
+ Use this gem to create sinatra style servers to respond to requests.
23
+
24
+ <CODE>
25
+
26
+ ## Client
27
+
28
+ ## Benchmarks
29
+
30
+ - do some testing of the latency here, see if it really is faster. Pick a few different servers
31
+
32
+ ## Other Points
33
+
34
+ - encoding is always UTF-8
35
+ - the max request and response size is 512 bytes
data/udp_rest.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'udp_rest/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "udp_rest"
8
+ spec.version = UDPRest::VERSION
9
+ spec.authors = ["Nathan Reed"]
10
+ spec.email = ["reednj@gmail.com"]
11
+
12
+ spec.summary = %q{Client and server modules to allow making REST HTTP requests over UDP}
13
+ spec.homepage = "https://github.com/reednj/udp_rest"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = '>=1.9.3'
22
+ spec.add_development_dependency "bundler", "~> 1.12"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+
25
+ spec.add_dependency 'colorize'
26
+ spec.add_dependency 'trollop'
27
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: udp_rest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Reed
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-12-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: colorize
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: trollop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ - reednj@gmail.com
72
+ executables:
73
+ - udp-rest
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - Gemfile
79
+ - Gemfile.lock
80
+ - LICENSE.txt
81
+ - Rakefile
82
+ - bin/console
83
+ - bin/setup
84
+ - demo/simple_server.rb
85
+ - exe/udp-rest
86
+ - lib/rest_client.rb
87
+ - lib/udp_rest.rb
88
+ - lib/udp_rest/udp.rb
89
+ - lib/udp_rest/uhttp.rb
90
+ - lib/udp_rest/version.rb
91
+ - lib/worker_thread.rb
92
+ - readme.md
93
+ - udp_rest.gemspec
94
+ homepage: https://github.com/reednj/udp_rest
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 1.9.3
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.6.6
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Client and server modules to allow making REST HTTP requests over UDP
118
+ test_files: []