tsurezure 0.0.1

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
+ SHA256:
3
+ metadata.gz: a04f09bb09e26d531a84b5738dd7719b2dee55ef2c73845bca3700c1a5f20c25
4
+ data.tar.gz: 6b3c83431259f111dd024f00860dfa9e10a8caad5e123f51b7085ca6dbced9db
5
+ SHA512:
6
+ metadata.gz: '038ed41337233b9e7debfaed3da1d2984f8d2e11b46e02ed175da71e12eab878433a4151cefa06fbf0b247dd19d4eff5acb2fb252b6951f0df0084397eae4c14'
7
+ data.tar.gz: b9a87b64323fb67155ff68f24f3c390807663de010b39c57d2a73a5aa4559181994603bc41b11ba44af74bc3824b78e317368166a63a37e4c2091b2159b342fa
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 e. brown
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/lib/tsurezure.rb ADDED
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'socket'
5
+ require 'json'
6
+ require 'pry'
7
+
8
+ require_relative 'utils/http_utils' # mainly used to create http responses.
9
+ require_relative 'utils/object_utils' # various object validation utilities
10
+ require_relative 'utils/error_codes' # for generating errors.
11
+ require_relative 'utils/response' # handles request and generates responses.
12
+
13
+ $TRZR_PROCESS_MODE = nil
14
+ $TRZR_LOG = true
15
+
16
+ ARGV.each do |arg|
17
+ $TRZR_PROCESS_MODE = 'development' if arg == '--development'
18
+ $TRZR_PROCESS_MODE = 'production' if arg == '--production'
19
+ $TRZR_LOG = false if arg == '--silent'
20
+ end
21
+
22
+ $TRZR_PROCESS_MODE = 'production' if $TRZR_PROCESS_MODE.nil?
23
+
24
+ # main class for tsurezure.
25
+ class Tsurezure
26
+ include OUtil
27
+
28
+ ##
29
+ # this class is made to handle requests coming from
30
+ # a single client on a tcp server.
31
+ class RequestHandler
32
+ include HTTPUtils
33
+ include ErrorCodes
34
+ include TResponse
35
+
36
+ ##
37
+ # initializes with a client socket returned from a +TCPSocket+ object.
38
+
39
+ def initialize(session)
40
+ @session = session
41
+ # endpoints are organized into arrays, sorted by method.
42
+ # ex: { get: [ ...endpoint objects ], post: [ ... ] }
43
+ # etc.
44
+ end
45
+
46
+ ##
47
+ # handles an incoming request from the open socket in +@session+.
48
+ # constructs a formatted request object from the original request object,
49
+ # and calls +send_final_response+ in order to send a response.
50
+ def handle_request(request)
51
+ url_main = request[:url].split('?')[0]
52
+
53
+ request_object = {
54
+ method: request[:method],
55
+ url: url_main,
56
+ params: HTTPUtils::URLUtils.extract_url_params(request[:url]),
57
+ protocol: request[:protocol],
58
+ headers: request[:headers],
59
+ data: request[:data]
60
+ }
61
+
62
+ generate_response request_object
63
+ end
64
+
65
+ ##
66
+ # generate a response from a supplied request object
67
+ # from +handle_request+.
68
+ def generate_response(request_object)
69
+ type = 'text/plain'
70
+
71
+ unless request_object[:options].nil? || request_object[:options].empty?
72
+ type = request_object[:options][:content_type]
73
+ end
74
+
75
+ res = TResponse.get_response request_object, @endpoints
76
+
77
+ # to initialize: session and length of response
78
+ responder = HTTPUtils::ServerResponse.new(
79
+ @session,
80
+ res[:message].bytesize
81
+ )
82
+
83
+ go_through_middleware request_object, responder, res, type
84
+ end
85
+
86
+ def get_correct_middleware(request_object)
87
+ @middleware.keys.select do |pat|
88
+ HTTPUtils::URLUtils.matches_url_regex(pat, request_object[:url]) ||
89
+ pat == '*'
90
+ end
91
+ end
92
+
93
+ def fix_req(request, mid)
94
+ request[:vars] =
95
+ HTTPUtils::URLUtils.get_match_indices(
96
+ mid[:path_regex],
97
+ request[:url]
98
+ ) || {}
99
+
100
+ request[:options] = mid[:options]
101
+
102
+ request
103
+ end
104
+
105
+ def send_middleware_response(req, resp, type)
106
+ res = resp.merge req
107
+
108
+ responder = HTTPUtils::ServerResponse.new(
109
+ @session,
110
+ res[:message].bytesize
111
+ )
112
+
113
+ # pp res
114
+
115
+ responder.respond res[:message], res[:options] || {}, res[:status], type
116
+ end
117
+
118
+ def call_each_middleware(request, middleware, type)
119
+ alt = nil
120
+
121
+ middleware.each do |path|
122
+ break if alt
123
+
124
+ @middleware[path]&.each do |mid|
125
+ alt = mid[:callback].call fix_req(request, mid)
126
+ end
127
+ end
128
+
129
+ return true unless alt
130
+
131
+ send_middleware_response(request, alt, type)
132
+ end
133
+
134
+ def go_through_middleware(request_object, responder, res, type)
135
+ exp = get_correct_middleware request_object
136
+
137
+ done = call_each_middleware request_object, exp, type
138
+
139
+ return unless done
140
+
141
+ # to send: response, options, status, content_type
142
+ responder.respond res[:message], res[:options] || {}, res[:status], type
143
+ end
144
+
145
+ ##
146
+ # main process, allows server to handle requests
147
+ def process(client, endpoints, middleware)
148
+ @endpoints = endpoints
149
+ @middleware = middleware
150
+ @request = client.gets
151
+ # wait until server isn't recieving anything
152
+ return if @session.gets.nil?
153
+ return if @session.gets.chop.length.zero?
154
+
155
+ request_made = HTTPUtils.make_proper_request client, @request
156
+
157
+ request_to_handle = HTTPUtils.make_request_object request_made
158
+
159
+ handle_request request_to_handle
160
+ end
161
+ end
162
+
163
+ ##
164
+ # prepares the server to run on a specified port.
165
+ def initialize(port)
166
+ raise ErrorCodes.nan_error 'port' unless port.is_a? Numeric
167
+ raise ErrorCodes.range_error 0, 65_535 unless (0..65_535).include? port
168
+
169
+ @server = TCPServer.new port
170
+ @port = port
171
+ @endpoints = {}
172
+ @middleware = {}
173
+ end
174
+
175
+ attr_reader :endpoints # access endpoints object from outside scope
176
+
177
+ def add_middleware(path, callback, options)
178
+ unless path.is_a? String
179
+ raise ArgumentError, 'first argument to middleware\
180
+ must be string or function.'
181
+ end
182
+
183
+ middleware_object = {
184
+ options: options, callback: callback, path_regex: path
185
+ }
186
+
187
+ @middleware[path] << middleware_object if @middleware[path]
188
+
189
+ @middleware[path] = [middleware_object] unless @middleware[path]
190
+ end
191
+
192
+ def register(http_method, path, callback, options = nil)
193
+ http_method = http_method.upcase
194
+ insurance = ensure_registration http_method, path, callback, options
195
+
196
+ raise ArgumentError, insurance if insurance.class == String
197
+
198
+ # register a new endpoint but do not register dupes
199
+ @endpoints[http_method] = {} unless @endpoints.key? http_method
200
+
201
+ new_endpoint = { path: path, responder: callback, options: options }
202
+
203
+ add_new_endpoint new_endpoint, http_method
204
+ end
205
+
206
+ ##
207
+ # run when the server is prepared to accept requests.
208
+ def listen
209
+ puts "running on port #{@port}!"
210
+ # create a new thread for handle each incoming request
211
+ loop do
212
+ Thread.start(@server.accept) do |client|
213
+ RequestHandler.new(client).process client, @endpoints, @middleware
214
+ end
215
+ end
216
+ end
217
+
218
+ private
219
+
220
+ # ----------------------------------------
221
+ # :section: registration of endpoints and
222
+ # all endpoint management methods follow.
223
+ # ----------------------------------------
224
+
225
+ def ensure_registration(*args)
226
+ verification = verify_registration(*args)
227
+
228
+ return verification unless verification
229
+
230
+ verification # to register
231
+ end
232
+
233
+ def validate_registration_params(method, path, responder)
234
+ unless TResponse::Utils.new.valid_methods.include? method
235
+ return "#{method} is not a valid http method."
236
+ end
237
+
238
+ return 'invalid path type. must be a string.' unless path.class == String
239
+
240
+ if path.empty? || path.chr != '/'
241
+ return 'invalid path. must begin with "/".'
242
+ end
243
+
244
+ return 'invalid responder type. must a proc.' unless responder.class == Proc
245
+
246
+ true
247
+ end
248
+
249
+ def verify_registration(http_method, path, responder, options)
250
+ valid = validate_registration_params http_method, path, responder
251
+
252
+ return valid unless valid == true
253
+ return true if options.nil? || options.empty?
254
+ return 'invalid options type.' unless options.class == Hash
255
+
256
+ valid_opts = %w[content_type method location]
257
+
258
+ opts_valid = OUtil.check_against_array(options.keys, valid_opts, 'register')
259
+
260
+ return 'invalid options provided to register.' unless opts_valid
261
+
262
+ true # to ensure_registration
263
+ end
264
+
265
+ def add_new_endpoint(endpoint, method)
266
+ @endpoints[method].each do |_, value|
267
+ if value[:path] == endpoint[:path]
268
+ raise ArgumentError, 'cannot register duplicate path.'
269
+ end
270
+ end
271
+
272
+ # add endpoint to list of registered endpoints
273
+ @endpoints[method][endpoint[:path]] = endpoint
274
+ end
275
+
276
+ def kill
277
+ abort
278
+ end
279
+ end
280
+
281
+ at_exit { puts 'shutting down. goodbye...' }
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # for storing generic, reusable error codes.
4
+ module ErrorCodes
5
+ def self.nan_error(item)
6
+ "invalid parameter: #{item} must be a number."
7
+ end
8
+
9
+ def self.no_method_error(item)
10
+ "invalid http method: #{item} is not a valid http method."
11
+ end
12
+
13
+ def self.invalid_type_error(item)
14
+ "invalid type: #{item} is not a valid type."
15
+ end
16
+
17
+ def self.range_error(min, max)
18
+ "invalid port number: port must be in range [#{min}, #{max}]"
19
+ end
20
+
21
+ def self.invalid_structure_error(keys, method)
22
+ "invalid object with keys #{keys} supplied to #{method}"
23
+ end
24
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # simple methods for showing error messages
4
+ module ErrorMessage
5
+ def self.invalid_key_error(method, key)
6
+ raise ArgumentError, {
7
+ message: "invalid key '#{key}' used as parameter to #{method}."
8
+ }.to_json
9
+ end
10
+
11
+ def self.invalid_http_method_error(key, val, method)
12
+ raise ArgumentError, {
13
+ message: "#{key} used to access #{method}. use #{val}.",
14
+ status: 405,
15
+ options: {
16
+ allowed: val
17
+ }
18
+ }.to_json
19
+ end
20
+
21
+ def self.missing_arguments_error(method)
22
+ raise ArgumentError, {
23
+ status: 500,
24
+ message: "missing arguments to #{method}"
25
+ }.to_json
26
+ end
27
+
28
+ def self.missing_parameter_error(method)
29
+ raise ArgumentError, {
30
+ status: 400,
31
+ message: "missing url parameter id to #{method}."
32
+ }.to_json
33
+ end
34
+
35
+ def self.invalid_structure_error(method, keys)
36
+ raise ArgumentError, {
37
+ status: 400,
38
+ message: "invalid object with keys #{keys} passed to #{method}."
39
+ }.to_json
40
+ end
41
+
42
+ def self.make_http_error(status, message, options)
43
+ error = { status: status, options: options }
44
+ error.delete :options if options.nil?
45
+
46
+ case status.to_s.chr.to_i
47
+ when 4
48
+ error[:message] = "bad request: #{message}"
49
+ when 5
50
+ error[:message] = "server error: #{message}"
51
+ end
52
+
53
+ error.to_json
54
+ end
55
+ end
56
+
57
+ # module for multiple custom error classes
58
+ module CustomError
59
+ # for throwing a server error (like 500)
60
+ class ServerError < StandardError
61
+ def initialize(message = 'an internal server error occurred.')
62
+ super
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logbook'
4
+
5
+ # all utilities for dealing with http-related things
6
+ module HTTPUtils
7
+ include Logbook
8
+ # class URLUtils - for dealing with urls
9
+ class URLUtils
10
+ def self.extract_url_params(url)
11
+ url_params = {}
12
+
13
+ if url.split('?')
14
+ .length > 1
15
+ url.split('?')[1].split('&').map do |e|
16
+ key, value = e.split('=')
17
+
18
+ break if value.nil?
19
+
20
+ url_params[key] = value
21
+ end
22
+ end
23
+
24
+ url_params
25
+ end
26
+
27
+ def self.url_path_matches?(url, path)
28
+ split_url = url.split '/'
29
+ split_path = path.split '/'
30
+
31
+ return false if split_url.empty? || split_url.length != split_path.length
32
+
33
+ true
34
+ end
35
+
36
+ def self.get_match_indices(url, path)
37
+ split_url = url.split '/'
38
+ split_path = path.split '/'
39
+
40
+ hash_with_variables = {}
41
+
42
+ return unless url_path_matches? url, path
43
+
44
+ split_url.each_with_index do |pname, idx|
45
+ part = pname.sub(':', '')
46
+ hash_with_variables[part] = split_path[idx] if pname[0] == ':'
47
+ end
48
+
49
+ hash_with_variables
50
+ end
51
+
52
+ def self.matches_url_regex(url, regex)
53
+ return unless url_path_matches? url, regex
54
+
55
+ matches = url.scan %r{((?<=\/):[^\/]+)}
56
+ newregexp = url.dup
57
+
58
+ return url.match? Regexp.new("^#{regex}$") if matches.empty?
59
+
60
+ matches.each do |mat|
61
+ newregexp.gsub!(Regexp.new(mat[0].to_s), '.+')
62
+ end
63
+
64
+ url.match? Regexp.new(newregexp)
65
+ end
66
+ end
67
+
68
+ # for dealing with header data.
69
+ class HeaderUtils
70
+ def self.get_headers(client)
71
+ headers = {}
72
+
73
+ while (line = client.gets.split(' ', 2))
74
+ break if line[0] == ''
75
+
76
+ headers[line[0].chop] = line[1].strip
77
+ end
78
+
79
+ headers
80
+ end
81
+
82
+ def self.get_req_data(client, headers)
83
+ data = client.read headers['Content-Length'].to_i
84
+
85
+ return if data.empty?
86
+
87
+ data
88
+ end
89
+ end
90
+
91
+ def self.make_proper_request(client, request)
92
+ headers = HeaderUtils.get_headers(client)
93
+ data = HeaderUtils.get_req_data(client, headers)
94
+ method = request.split(' ')[0]
95
+ url = request.split(' ')[1]
96
+ proto = request.split(' ')[2]
97
+
98
+ { headers: headers, data: data, method: method, url: url, protocol: proto }
99
+ end
100
+
101
+ def self.make_request_object(req)
102
+ req[:data] = '{}' if req[:data].nil?
103
+
104
+ {
105
+ headers: req[:headers],
106
+ data: JSON.parse(req[:data]),
107
+ method: req[:method],
108
+ url: req[:url],
109
+ protocol: req[:protocol]
110
+ }
111
+ end
112
+
113
+ # class ServerResponses - for sending HTTP responses
114
+ class ServerResponse
115
+ def initialize(session, length)
116
+ @session = session
117
+ @length = length
118
+ end
119
+
120
+ def valid_options?(options)
121
+ acceptable_keys = %i[location method]
122
+
123
+ return true if acceptable_keys.any? { |key| options.key? key }
124
+
125
+ false
126
+ end
127
+
128
+ def respond(response,
129
+ options = {},
130
+ status = 200,
131
+ content_type = 'application/json')
132
+ @content_type = options[:content_type] || content_type
133
+
134
+ if respond_to? "r_#{status}"
135
+ method("r_#{status}").call unless valid_options? options
136
+ method("r_#{status}").call options if valid_options? options
137
+ else
138
+ r_400
139
+ end
140
+
141
+ @session.puts
142
+ @session.puts response
143
+ @session.close
144
+ end
145
+
146
+ def r_200
147
+ @session.puts 'HTTP/1.1 200 OK'
148
+ @session.puts "Content-Type: #{@content_type}"
149
+ @session.puts "Content-Length: #{@length}"
150
+ end
151
+
152
+ def r_201
153
+ @session.puts 'HTTP/1.1 201 Created'
154
+ @session.puts "Content-Type: #{@content_type}"
155
+ @session.puts "Content-Length: #{@length}"
156
+ end
157
+
158
+ def r_301(options)
159
+ @session.puts 'HTTP/1.1 301 Moved Permanently'
160
+ @session.puts "Content-Type: #{@content_type}"
161
+ @session.puts "Content-Length: #{@length}"
162
+ @session.puts "Location: #{options[:location]}"
163
+ end
164
+
165
+ def r_304
166
+ @session.puts 'HTTP/1.1 304 Not Modified'
167
+ @session.puts "Content-Type: #{@content_type}"
168
+ @session.puts "Content-Length: #{@length}"
169
+ end
170
+
171
+ def r_400
172
+ @session.puts 'HTTP/1.1 400 Bad Request'
173
+ @session.puts "Content-Type: #{@content_type}"
174
+ @session.puts "Content-Length: #{@length}"
175
+ end
176
+
177
+ def r_404
178
+ @session.puts 'HTTP/1.1 404 Not Found'
179
+ @session.puts "Content-Type: #{@content_type}"
180
+ @session.puts "Content-Length: #{@length}"
181
+ end
182
+
183
+ def r_405(options)
184
+ @session.puts 'HTTP/1.1 405 Method Not Allowed'
185
+ @session.puts "Content-Type: #{@content_type}"
186
+ @session.puts "Content-Length: #{@length}"
187
+ @session.puts "Allow: #{options[:method]}"
188
+ end
189
+
190
+ def r_500
191
+ @session.puts 'HTTP/1.1 500 Internal Server Error'
192
+ @session.puts "Content-Type: #{@content_type}"
193
+ @session.puts "Content-Length: #{@length}"
194
+ end
195
+ end
196
+ end
197
+
198
+ __END__
199
+
200
+ this is the file that made me realize that the content_length header actually directly controls the length of the content in the response. I incorrectly set the content_length once as a mistake, and my responses were coming out short. interesting!
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # module for logging things
4
+ module Logbook
5
+ # dev mode logging
6
+ class Dev
7
+ def self.log_json(data, break_around = true, tag = 'info')
8
+ return unless $TRZR_PROCESS_MODE == 'development' && $TRZR_LOG == true
9
+
10
+ if break_around
11
+ puts "\r\n#{tag} - #{caller.first}".black.bg_green
12
+ puts JSON.pretty_generate data
13
+ puts
14
+ else
15
+ puts "#{tag}\r\n#{caller.first}"
16
+ pp data
17
+ end
18
+ end
19
+
20
+ def self.log(data, break_around = true, tag = 'info')
21
+ return unless $TRZR_PROCESS_MODE == 'development' && $TRZR_LOG == true
22
+
23
+ if break_around
24
+ puts "\r\n#{tag} - #{caller.first}".black.bg_green
25
+ pp data
26
+ puts
27
+ else
28
+ puts "#{tag}\r\n#{caller.first}"
29
+ pp data
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # some slight mods to the string class to allow colorization
36
+ class String
37
+ def green
38
+ "\e[32m#{self}\e[0m"
39
+ end
40
+
41
+ def blue
42
+ "\e[34m#{self}\e[0m"
43
+ end
44
+
45
+ def black
46
+ "\e[30m#{self}\e[0m"
47
+ end
48
+
49
+ def bg_red
50
+ "\e[41m#{self}\e[0m"
51
+ end
52
+
53
+ def bg_green
54
+ "\e[42m#{self}\e[0m"
55
+ end
56
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+
5
+ # module for validating object structure, keys etc.
6
+ module OUtil
7
+ include ErrorMessage
8
+ # check url params to make sure they're not empty
9
+ # on methods that require them
10
+ def self.check_params(params, method)
11
+ return unless params.nil? || params.empty?
12
+
13
+ raise ErrorMessage.missing_parameter_error(method)
14
+ end
15
+
16
+ # make sure a key is the correct value
17
+ def self.check_key(key, value, method)
18
+ return if key == value
19
+
20
+ raise ErrorMessage.invalid_key_error(method, key)
21
+ end
22
+
23
+ def self.check_http_method(key, value, method)
24
+ return if key == value
25
+
26
+ raise ErrorMessage.invalid_http_method_error(key, value, method)
27
+ end
28
+
29
+ def check_key_type(object, key, type, method)
30
+ return if object[key].is_a? type
31
+
32
+ raise ErrorMessage.invalid_key_error(method, key)
33
+ end
34
+
35
+ def self.check_object_keys(keys, valid_keys, method)
36
+ key_bank = []
37
+
38
+ keys.each do |key|
39
+ break unless valid_keys.keys.include? key.to_sym
40
+ break unless key.is_a? valid_keys[key.to_sym]
41
+
42
+ key_bank.push key
43
+ end
44
+
45
+ return if key_bank.length == valid_keys.length
46
+
47
+ raise ErrorMessage.invalid_structure_error(method, "(#{keys.join(', ')})")
48
+ end
49
+
50
+ def self.check_against_array(keys, valid_keys, method)
51
+ inv_keys = []
52
+
53
+ keys.each do |key|
54
+ key = key.to_s
55
+
56
+ inv_keys << key unless valid_keys.include? key
57
+ end
58
+
59
+ return true if inv_keys.length.zero?
60
+
61
+ raise ErrorMessage.invalid_structure_error(method, "(#{keys.join(', ')})")
62
+ end
63
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './http_utils' # mainly used to create http responses.
4
+
5
+ ##
6
+ # module for handling all incoming requests to the server
7
+ # stands for TsurezureResponse
8
+ module TResponse
9
+ include HTTPUtils
10
+ # anything that will be needed to create responses
11
+ class Utils
12
+ def initialize
13
+ @valid_methods = %w[
14
+ CONNECT COPY DELETE GET HEAD
15
+ LINK LOCK MKCOL MOVE OPTIONS
16
+ OPTIONS PATCH POST PROPFIND
17
+ PROPPATCH PURGE PUT TRACE
18
+ UNLINK UNLOCK VIEW
19
+ ]
20
+ end
21
+
22
+ attr_reader :valid_methods
23
+
24
+ def self.validate_request(request_params)
25
+ # make sure the user has provided a valid http
26
+ # method, a valid uri, and a valid response /
27
+ # response type
28
+ valid_methods = %w[
29
+ CONNECT COPY DELETE GET HEAD
30
+ LINK LOCK MKCOL MOVE OPTIONS
31
+ OPTIONS PATCH POST PROPFIND
32
+ PROPPATCH PURGE PUT TRACE
33
+ UNLINK UNLOCK VIEW
34
+ ]
35
+
36
+ return false unless valid_methods.include? request_params[:method]
37
+ end
38
+
39
+ def self.get_correct_endpoint(request_object, endpoints)
40
+ endpoints.keys.select do |pat|
41
+ HTTPUtils::URLUtils.matches_url_regex(pat, request_object[:url])
42
+ end
43
+ end
44
+
45
+ def self.ensure_response(request, endpoints)
46
+ return false if request.nil? || request.empty?
47
+ return false if endpoints.nil? || endpoints.empty?
48
+
49
+ endpoint = endpoints[get_correct_endpoint(request, endpoints)[0]]
50
+
51
+ return false if endpoint.nil?
52
+
53
+ true
54
+ end
55
+ end
56
+
57
+ # creates the final response from the server
58
+ def self.get_response(request, endpoints)
59
+ Utils.validate_request request
60
+
61
+ @endpoints = endpoints[request[:method]]
62
+
63
+ # if no endpoint, respond with root endpoint or 404 middleware
64
+
65
+ unless Utils.ensure_response(request, @endpoints) == true
66
+ return { options: { content_type: 'application/json' },
67
+ code: 22, status: 404,
68
+ message: { status: 404, message: 'undefined endpoint' }.to_json }
69
+ end
70
+
71
+ endpoint = @endpoints[Utils.get_correct_endpoint(request, @endpoints)[0]]
72
+
73
+ # find the correct endpoint to respond with
74
+ activate_endpoint endpoint, request
75
+ end
76
+
77
+ def self.activate_endpoint(endpoint, request)
78
+ final = endpoint.merge request
79
+ final.delete :responder
80
+
81
+ final[:vars] =
82
+ HTTPUtils::URLUtils.get_match_indices(final[:path], final[:url])
83
+
84
+ response_from_endpoint = endpoint[:responder].call final
85
+
86
+ unless response_from_endpoint.is_a? Hash
87
+ return { status: 200, message: response_from_endpoint }
88
+ end
89
+
90
+ response_from_endpoint[:options] = final[:options]
91
+
92
+ response_from_endpoint
93
+ end
94
+ end
data/nodemon.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "ignore": [".git"],
3
+ "watch": ["./"],
4
+ "ext": "rb, Gemfile"
5
+ }
data/readme.md ADDED
@@ -0,0 +1,123 @@
1
+ # tsurezure
2
+
3
+ this is a simple web server framework written in ruby. mainly made as a way for me to quickly put together rest apis in my favorite language.
4
+
5
+ it can be used in a very similar manner to the javascript framework express.
6
+
7
+ * * *
8
+
9
+ ## usage
10
+
11
+ ### installing (from rubygems)
12
+
13
+ just run `gem install tsurezure` and you'll have whatever the latest version is that I've put up.
14
+
15
+ ### installing (from source):
16
+
17
+ requires:
18
+
19
+ - ruby
20
+ - nodejs + nodemon (**only** for hot reloading server in development mode, not necessarily required)
21
+
22
+ after cloning this repo, from the root project directory, just run `rake start` to start in production mode, or `rake dev` to run in development mode, which adds hot reloading with nodemon. gem dependencies will install automatically.
23
+
24
+ to build the gem: run `gem build tsurezure.gemspec`. then, install using `gem install tsurezure-version-number`. `version-number` is whatever version is installed based on the `.gemspec` file.
25
+
26
+ ### actually using tsurezure:
27
+
28
+ as for how to use tsurezure, here's a simple hello world to get started:
29
+
30
+ ```ruby
31
+ require 'tsurezure'
32
+
33
+ # create an instance of tsurezure
34
+ server = Tsurezure.new(8888)
35
+
36
+ # url: http://localhost:8888/user/1
37
+
38
+ # create an endpoint
39
+ server.register 'get', '/user/:id', lambda { |req|
40
+ url_vars = req[:vars] # { "id" => "1" }
41
+ params = req[:params] # {}
42
+
43
+ # create a respsonse for the endpoint
44
+ {
45
+ status: 200,
46
+ message: {
47
+ message: "hello user ##{url_vars['id']}!"
48
+ }.to_json
49
+ }
50
+ }, content_type: 'application/json' # options hash
51
+
52
+ # throw in some middleware
53
+ server.add_middleware '/user/:id', lambda { |req|
54
+ url_vars = req[:vars]
55
+
56
+ # show a different response based on the request itself.
57
+ # if you return from middleware, the return value will
58
+ # be sent as the final response.
59
+ if req[:vars]['id'] == '1'
60
+ return {
61
+ status: 200, message: {
62
+ message: "hey user #1! you're the first one here!"
63
+ }.to_json
64
+ }
65
+
66
+ end
67
+ }, content_type: 'application/json'
68
+
69
+ #listen for connections
70
+ server.listen
71
+ ```
72
+
73
+ after you run this file, open up your browser or whatever and go to `http://localhost:8888/user/1`. you should see a json response that looks like this:
74
+
75
+ ```json
76
+ {
77
+ "message": "hey user #1! you're the first one here!"
78
+ }
79
+ ```
80
+
81
+ the registration function for creating endpoints is very simple:
82
+
83
+ ```ruby
84
+ register http_method, path, callback, options
85
+ ```
86
+
87
+ `http_method` is the method to access the endpoint with. `path` is just the url.
88
+
89
+ `path` can be a path that contains variables (such as `/user/:id`). see the example above to see how it works.
90
+
91
+ `callback` is a lambda that contains the logic used to send a response. it will recieve one argument: the request that was sent to that endpoint. whatever is returned from the proc will be sent as the response from that endpoint.
92
+
93
+ `options` is a hash containing various options to somehow modify the response. valid options:
94
+
95
+ - `content_type` - determines the mime type of the response
96
+ - `location` - if a location header is required (301, etc), this is used to provide it.
97
+ - `method` - if an allow header is required (405), this is used to provide it.
98
+
99
+ for middleware, it's much the same:
100
+
101
+ ```ruby
102
+ add_middleware path, callback, options
103
+ ```
104
+
105
+ `path` can be a path that contains variables. used in the same way as the `path` for endpoints.
106
+
107
+ `callback` is a lambda that you can use to intercept and pre-process responses. if you return from a callback in middleware, then that return value will be sent as the final response.
108
+
109
+ `options` for middleware are the same as the `options` for endpoints.
110
+
111
+ * * *
112
+
113
+ ## todo
114
+
115
+ - [ ] make it so registered uris can only be accessed with the specified method, and everything else returns a 405 (maybe make this an option??)
116
+
117
+ - [ ] give the user an option to add middleware specifically for catching errors
118
+
119
+ ## misc
120
+
121
+ disclaimer: I don't know ruby, and this is my first time using it to make something.
122
+
123
+ the name comes from yukueshirezutsurezure, one of my favorite bands. it's pronounced 'tsɯ-ɾe-dzɯ-ɾe.'
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tsurezure
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - jpegzilla
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.8.3
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.8'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.8.3
33
+ - !ruby/object:Gem::Dependency
34
+ name: pry
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.13.1
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.13.1
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.9'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.9'
61
+ description: a simple ruby web server framework. like a ball of loose yarn...
62
+ email: eris@jpegzilla.com
63
+ executables: []
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - LICENSE
68
+ - lib/tsurezure.rb
69
+ - lib/utils/error_codes.rb
70
+ - lib/utils/errors.rb
71
+ - lib/utils/http_utils.rb
72
+ - lib/utils/logbook.rb
73
+ - lib/utils/object_utils.rb
74
+ - lib/utils/response.rb
75
+ - nodemon.json
76
+ - readme.md
77
+ homepage: https://github.com/jpegzilla/tsurezure
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.0.3
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: tsurezure is a simple web server framework.
100
+ test_files: []