tsurezure 0.0.1

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
+ 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: []