tsurezure 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|