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 +7 -0
- data/LICENSE +21 -0
- data/lib/tsurezure.rb +281 -0
- data/lib/utils/error_codes.rb +24 -0
- data/lib/utils/errors.rb +65 -0
- data/lib/utils/http_utils.rb +200 -0
- data/lib/utils/logbook.rb +56 -0
- data/lib/utils/object_utils.rb +63 -0
- data/lib/utils/response.rb +94 -0
- data/nodemon.json +5 -0
- data/readme.md +123 -0
- metadata +100 -0
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
|
data/lib/utils/errors.rb
ADDED
@@ -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
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: []
|