gloo-web 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2024 Eric Crane. All rights reserved.
3
+ #
4
+ # The Response for a web Request.
5
+ #
6
+
7
+ module WebSvr
8
+ class Response
9
+
10
+ #
11
+ # SEE: https://stackoverflow.com/questions/23714383/what-are-all-the-possible-values-for-http-content-type-header#48704300
12
+ # for a list of content types.
13
+ #
14
+ CONTENT_TYPE = 'Content-Type'.freeze
15
+ TEXT_TYPE = 'text/plain'.freeze
16
+ JSON_TYPE = 'application/json'.freeze
17
+ HTML_TYPE = 'text/html'.freeze
18
+
19
+ attr_reader :code, :type, :data
20
+ attr_accessor :location
21
+ attr_accessor :file_name
22
+
23
+
24
+ # ---------------------------------------------------------------------
25
+ # Initialization
26
+ # ---------------------------------------------------------------------
27
+
28
+ #
29
+ # Set up the web server.
30
+ #
31
+ def initialize( engine = nil,
32
+ code = WebSvr::ResponseCode::SUCCESS,
33
+ type = HTML_TYPE,
34
+ data = nil,
35
+ assetCache = false,
36
+ file_name = nil,
37
+ download = false )
38
+
39
+ @engine = engine
40
+ @log = @engine.log if @engine
41
+
42
+ @code = code
43
+ @type = type
44
+ @data = data
45
+ @assetCache = assetCache
46
+ @location = nil
47
+ @file_name = file_name
48
+ @download = download
49
+ end
50
+
51
+
52
+ # ---------------------------------------------------------------------
53
+ # Static Helper Functions
54
+ # ---------------------------------------------------------------------
55
+
56
+ #
57
+ # Helper to create a successful JSON response with the given data.
58
+ #
59
+ def self.json_response( engine, data,
60
+ code = WebSvr::ResponseCode::SUCCESS )
61
+
62
+ return WebSvr::Response.new( engine, code, JSON_TYPE, data )
63
+ end
64
+
65
+ #
66
+ # Helper to create a successful text response with the given data.
67
+ #
68
+ def self.text_response( engine, data,
69
+ code = WebSvr::ResponseCode::SUCCESS )
70
+
71
+ return WebSvr::Response.new( engine, code, TEXT_TYPE, data )
72
+ end
73
+
74
+ #
75
+ # Helper to create a successful web response with the given data.
76
+ #
77
+ def self.html_response( engine, data,
78
+ code = WebSvr::ResponseCode::SUCCESS )
79
+
80
+ return WebSvr::Response.new( engine, code, HTML_TYPE, data )
81
+ end
82
+
83
+ #
84
+ # Helper to create a redirect response.
85
+ #
86
+ def self.redirect_response( engine, target )
87
+ code = WebSvr::ResponseCode::FOUND
88
+ data = <<~TEXT
89
+ <head>
90
+ <html>
91
+ <body><a href="#{target}">target is here</a></body>
92
+ </html>
93
+ </head>
94
+ TEXT
95
+
96
+ response = WebSvr::Response.new( engine, code, HTML_TYPE, data )
97
+ response.location = target
98
+
99
+ return response
100
+ end
101
+
102
+
103
+ # ---------------------------------------------------------------------
104
+ # Data Functions
105
+ # ---------------------------------------------------------------------
106
+
107
+ #
108
+ # Add content to the payload.
109
+ #
110
+ def add content
111
+ @data = '' if @data.nil?
112
+ @data << content
113
+ end
114
+
115
+ #
116
+ # Get the headers for the response.
117
+ #
118
+ def headers
119
+ #
120
+ # TO DO: Add more cookie headers here.
121
+ #
122
+ # https://stackoverflow.com/questions/3295083/how-do-i-set-a-cookie-with-a-ruby-rack-middleware-component
123
+ # https://www.rubydoc.info/gems/rack/1.4.7/Rack/Session/Cookie
124
+ #
125
+
126
+ headers = { CONTENT_TYPE => @type }
127
+
128
+ if @location
129
+ headers[ 'Location' ] = @location
130
+ end
131
+
132
+ if @assetCache || @file_name
133
+ headers[ 'Cache-Control' ] = 'public, max-age=604800'
134
+ headers[ 'Expires' ] = (Time.now.utc + 604800).to_s
135
+ end
136
+
137
+ if @file_name
138
+ disp = @download ? 'attachment' : 'inline'
139
+ headers[ 'Content-Disposition' ] = "#{disp}; filename=#{@file_name}"
140
+ headers[ 'Content-Length' ] = @data.length.to_s
141
+ end
142
+
143
+ session = @engine&.running_app&.obj&.session
144
+ headers = session.add_session_for_response( headers ) if session
145
+
146
+ # Clear out session data after the response is prepared.
147
+ @engine&.running_app&.obj&.reset_session_data
148
+
149
+ return headers
150
+ end
151
+
152
+ #
153
+ # Get the final result that will be returned as the
154
+ # response to the web request.
155
+ #
156
+ def result
157
+ return [ @code, headers, @data ]
158
+ end
159
+
160
+
161
+ # ---------------------------------------------------------------------
162
+ # Helper functions
163
+ # ---------------------------------------------------------------------
164
+
165
+ #
166
+ # Write the result information to the log.
167
+ #
168
+ def log
169
+ return unless @log
170
+
171
+ @log.info "Response #{@code} #{@type}"
172
+ end
173
+
174
+ end
175
+ end
@@ -0,0 +1,67 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2024 Eric Crane. All rights reserved.
3
+ #
4
+ # Standard Response Codes.
5
+ #
6
+ # See:
7
+ # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
8
+ # https://www.geeksforgeeks.org/10-most-common-http-status-codes/
9
+ #
10
+
11
+ module WebSvr
12
+ class ResponseCode
13
+
14
+ # WebSvr::ResponseCode::SUCCESS
15
+ SUCCESS = 200.freeze
16
+ CODE_200 = 'Success/OK'.freeze
17
+
18
+ CREATED = 201.freeze
19
+ CODE_201 = 'Created'.freeze
20
+
21
+ ACCEPTED = 202.freeze
22
+ CODE_202 = 'Accepted'.freeze
23
+
24
+ NO_CONTENT = 204.freeze
25
+ CODE_204 = 'No Content'.freeze
26
+
27
+ PARTIAL_CONTENT = 206.freeze
28
+ CODE_206 = 'Partial Content'.freeze
29
+
30
+ MOVED_PERM = 301.freeze
31
+ CODE_301 = 'Moved Permanently'.freeze
32
+
33
+ FOUND = 302.freeze
34
+ CODE_302 = 'Found'.freeze
35
+
36
+ SEE_OTHER = 303.freeze
37
+ CODE_303 = 'See Other'.freeze
38
+
39
+ NOT_MODIFIED = 304.freeze
40
+ CODE_304 = 'Not Modified'.freeze
41
+
42
+ TEMP_REDIRECT = 307.freeze
43
+ CODE_307 = 'Temporary Redirect'.freeze
44
+
45
+ PERM_REDIRECT = 308.freeze
46
+ CODE_308 = 'Permanent Redirect'.freeze
47
+
48
+ BAD_REQUEST = 400.freeze
49
+ CODE_400 = 'Bad Request'.freeze
50
+
51
+ UNAUTHORIZED = 401.freeze
52
+ CODE_401 = 'Unauthorized'.freeze
53
+
54
+ FORBIDDEN = 403.freeze
55
+ CODE_403 = 'Forbidden'.freeze
56
+
57
+ NOT_FOUND = 404.freeze
58
+ CODE_404 = 'Not Found'.freeze
59
+
60
+ SERVER_ERR = 500.freeze
61
+ CODE_500 = 'Internal Server Error'.freeze
62
+
63
+ NOT_IMPLEMENTED = 501.freeze
64
+ CODE_501 = 'Not Implemented'.freeze
65
+
66
+ end
67
+ end
@@ -0,0 +1,102 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2024 Eric Crane. All rights reserved.
3
+ #
4
+ # Starting work on web server inside gloo.
5
+ #
6
+ # UNDER CONSTRUCTION!
7
+ #
8
+ # Simple tests:
9
+ # > curl http://localhost:8087/test/
10
+ # > curl http://localhost:8087/web/
11
+ # > curl http://localhost:8087/test/1
12
+ # > curl http://localhost:8087/test?param=123
13
+ #
14
+ # Run in loop:
15
+ # for i in {1..99}; do curl http://localhost:8087/; done
16
+ #
17
+ # Links:
18
+ # https://github.com/rack/rack
19
+ # https://github.com/rack/rack/blob/main/lib/rack/builder.rb
20
+ # https://thoughtbot.com/blog/ruby-rack-tutorial
21
+ # https://www.rubydoc.info/gems/rack/1.5.5/Rack/Runtime
22
+ #
23
+
24
+ require 'rack'
25
+
26
+ module WebSvr
27
+ class Server
28
+
29
+ # ---------------------------------------------------------------------
30
+ # Initialization
31
+ # ---------------------------------------------------------------------
32
+
33
+ #
34
+ # Set up the web server.
35
+ #
36
+ def initialize( engine, handler, config = nil, ssl_config = nil )
37
+ @config = config ? config : WebSvr::Config.new
38
+ @ssl_config = ssl_config
39
+ @engine = engine
40
+ @log = @engine.log
41
+ @handler = handler
42
+
43
+ @log.debug 'Gloo web server intialized…'
44
+ end
45
+
46
+
47
+ # ---------------------------------------------------------------------
48
+ # Start and Stop the server.
49
+ # ---------------------------------------------------------------------
50
+
51
+ #
52
+ # Start the web server.
53
+ #
54
+ def start
55
+ opts = {
56
+ :Port => @config.port,
57
+ :Host => @config.host
58
+ }
59
+ Thread.abort_on_exception = true
60
+ @server_thread = Thread.new {
61
+ Rack::Handler::Thin.run( self, **options=opts ) do |server|
62
+ if @ssl_config
63
+ server.ssl = true
64
+ server.ssl_options = @ssl_config
65
+ end
66
+ end
67
+ }
68
+ @log.debug 'Web server has started.'
69
+ end
70
+
71
+ #
72
+ # Stop the web server
73
+ #
74
+ def stop
75
+ @log.debug 'Stopping the web server…'
76
+
77
+ @server_thread.kill
78
+
79
+ @log.debug 'The web server has been stopped.'
80
+ end
81
+
82
+
83
+ # ---------------------------------------------------------------------
84
+ # Handle events
85
+ # ---------------------------------------------------------------------
86
+
87
+ #
88
+ # Handle a request for a resource.
89
+ #
90
+ def call( env )
91
+ request = WebSvr::Request.new( @engine, @handler, env )
92
+ request.log
93
+
94
+ response = request.process
95
+ response.log if response
96
+
97
+ return response ? response.result : nil
98
+ end
99
+
100
+
101
+ end
102
+ end
@@ -0,0 +1,213 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2024 Eric Crane. All rights reserved.
3
+ #
4
+ # Helpers for getting and setting session data.
5
+ #
6
+ # Resources:
7
+ # https://www.rubydoc.info/gems/rack/1.5.5/Rack/Request#cookies-instance_method
8
+ # https://rubydoc.info/github/rack/rack/Rack/Utils#set_cookie_header-class_method
9
+ # https://en.wikipedia.org/wiki/HTTP_cookie
10
+ #
11
+ require 'base64'
12
+
13
+ module WebSvr
14
+ class Session
15
+
16
+ SESSION_CONTAINER = 'session'.freeze
17
+ SESSION_ID_NAME = 'session_id'.freeze
18
+
19
+
20
+ # ---------------------------------------------------------------------
21
+ # Initialization
22
+ # ---------------------------------------------------------------------
23
+
24
+ #
25
+ # Set up the web server.
26
+ #
27
+ def initialize( engine, server_obj )
28
+ @engine = engine
29
+ @log = @engine.log
30
+
31
+ @server_obj = server_obj
32
+ @include_in_response = false
33
+ @clearing_session = false
34
+ end
35
+
36
+
37
+ # ---------------------------------------------------------------------
38
+ # Set Session Data for Request
39
+ # ---------------------------------------------------------------------
40
+
41
+ #
42
+ # Get the session data from the encrypted cookie.
43
+ # Add it to the session container.
44
+ #
45
+ def set_session_data_for_request( env )
46
+ begin
47
+ cookie_hash = Rack::Utils.parse_cookies( env )
48
+
49
+ # Are we using sessions?
50
+ if @server_obj.use_session?
51
+ data = cookie_hash[ session_name ]
52
+
53
+ if data
54
+ data = decode_decrypt( data )
55
+ return unless data
56
+
57
+ @session_id = data[ SESSION_ID_NAME ]
58
+
59
+ data.each do |key, value|
60
+ unless key == SESSION_ID_NAME
61
+ @server_obj.set_session_var( key, value )
62
+ end
63
+ end
64
+ end
65
+ end
66
+ rescue => e
67
+ @engine.log_exception e
68
+ end
69
+ end
70
+
71
+
72
+ # ---------------------------------------------------------------------
73
+ # Set Session Data for Response
74
+ # ---------------------------------------------------------------------
75
+
76
+ #
77
+ # Temporarily set the flag to add the session data to the response.
78
+ # Once this is done, the flag will be cleared and it will not
79
+ # be added to the next request unless specifically set.
80
+ #
81
+ def add_session_to_response
82
+ @include_in_response = true
83
+ end
84
+
85
+ def init_session_id
86
+ @session_id = Gloo::Objs::CsrfToken.generate_csrf_token
87
+ return @session_id
88
+ end
89
+
90
+ #
91
+ # Initialize the session id and add it to the data.
92
+ # Use the current session ID if it is there.
93
+ #
94
+ def get_session_id
95
+ if @clearing_session
96
+ @clearing_session = false
97
+ return nil
98
+ end
99
+
100
+ init_session_id if @session_id.blank?
101
+
102
+ return @session_id
103
+ end
104
+
105
+ #
106
+ # Clear out the session Id.
107
+ # Set the flag to add the session data to the response.
108
+ #
109
+ def clear_session_data
110
+ @session_id = nil
111
+ @clearing_session = true
112
+ add_session_to_response
113
+ end
114
+
115
+ #
116
+ # If there is session data, encrypt and add it to the response.
117
+ # Once done, clear out the session data.
118
+ #
119
+ def add_session_for_response( headers )
120
+ # Are we using sessions?
121
+ if @server_obj.use_session? && @include_in_response
122
+ # Reset the flag because we are adding to the session data now
123
+ @include_in_response = false
124
+
125
+ # Build and add encrypted session data
126
+ data = @server_obj.get_session_data
127
+ data[ SESSION_ID_NAME ] = get_session_id
128
+
129
+ unless data.empty?
130
+ data = encrypt_encode( data )
131
+ session_hash = {
132
+ value: data,
133
+ path: cookie_path,
134
+ expires: cookie_expires,
135
+ http_only: true }
136
+
137
+ if secure_cookie?
138
+ session_hash[ :secure ] = true
139
+ end
140
+
141
+ Rack::Utils.set_cookie_header!( headers, session_name, session_hash )
142
+ end
143
+ end
144
+
145
+ return headers
146
+ end
147
+
148
+
149
+ # ---------------------------------------------------------------------
150
+ # Helper functions
151
+ # ---------------------------------------------------------------------
152
+
153
+ #
154
+ # Encrypt and encode the session data.
155
+ #
156
+ def encrypt_encode( data )
157
+ return Gloo::Objs::Cipher.encrypt( data.to_json, key, iv )
158
+ end
159
+
160
+ #
161
+ # Decode and decrypt the session data.
162
+ #
163
+ def decode_decrypt( data )
164
+ return nil unless data && key && iv
165
+
166
+ data = Gloo::Objs::Cipher.decrypt( data, key, iv )
167
+ return JSON.parse( data )
168
+ end
169
+
170
+ #
171
+ # Get the session cookie name.
172
+ #
173
+ def session_name
174
+ return @server_obj.session_name
175
+ end
176
+
177
+ #
178
+ # Get the key for the encryption cipher.
179
+ #
180
+ def key
181
+ return @server_obj.encryption_key
182
+ end
183
+
184
+ #
185
+ # Get the initialization vector for the cipher.
186
+ #
187
+ def iv
188
+ return @server_obj.encryption_iv
189
+ end
190
+
191
+ #
192
+ # Get the path for the session cookie.
193
+ #
194
+ def cookie_path
195
+ return @server_obj.session_cookie_path
196
+ end
197
+
198
+ #
199
+ # Get the expiration time for the session cookie.
200
+ #
201
+ def cookie_expires
202
+ return @server_obj.session_cookie_expires
203
+ end
204
+
205
+ #
206
+ # Should the session cookie be secure?
207
+ #
208
+ def secure_cookie?
209
+ return @server_obj.session_cookie_secure
210
+ end
211
+
212
+ end
213
+ end
@@ -0,0 +1,149 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2024 Eric Crane. All rights reserved.
3
+ #
4
+ # A helper class used to render HTML tables.
5
+ #
6
+
7
+ module WebSvr
8
+ class TableRenderer
9
+
10
+ TABLE = 'table'.freeze
11
+ THEAD = 'thead'.freeze
12
+ HEAD_CELL = 'head_cell'.freeze
13
+ ROW = 'row'.freeze
14
+ CELL = 'cell'.freeze
15
+
16
+ NO_DATA_FOUND = "<p>No data found.</p>".freeze
17
+
18
+ # ---------------------------------------------------------------------
19
+ # Initialization
20
+ # ---------------------------------------------------------------------
21
+
22
+ #
23
+ # Set up the web server.
24
+ #
25
+ def initialize( engine )
26
+ @engine = engine
27
+ @log = @engine.log
28
+ end
29
+
30
+
31
+ # ---------------------------------------------------------------------
32
+ # Container Renderer
33
+ # ---------------------------------------------------------------------
34
+
35
+ #
36
+ # Render the query result set to an HTML table.
37
+ #
38
+ # params = {
39
+ # head: head,
40
+ # cols: result[0],
41
+ # rows: rows,
42
+ # styles: self.styles,
43
+ # cell_renderers: self.cell_renderers
44
+ # }
45
+ #
46
+ def data_to_table params
47
+ data = params[ :rows ]
48
+ return NO_DATA_FOUND if data.nil? || ( data.length == 0 )
49
+
50
+ single_row = true if ( data.length == 1 )
51
+ single_row = false if params[ :always_rows ]
52
+
53
+ if single_row
54
+ return data_to_single_row_table( params )
55
+ else
56
+ return data_to_table_rows( params )
57
+ end
58
+ end
59
+
60
+ #
61
+ # Show in single-row (form) format.
62
+ #
63
+ def data_to_single_row_table( params )
64
+ styles = params[ :styles ]
65
+ str = "<table class='#{styles[ TABLE ]}'> <tbody>"
66
+ row = params[ :rows ].first
67
+
68
+ params[ :columns ].each do |head|
69
+ next unless head[ :visible ]
70
+ cell = row[ head[ :data_index ] ]
71
+
72
+ if head[ :cell_renderer ]
73
+ cell_value = render_cell( row, head, params[ :columns ] )
74
+ else
75
+ cell_value = cell
76
+ end
77
+
78
+ str += "<tr class='#{styles[ ROW ]}'>"
79
+ str += "<th class='#{styles[ HEAD_CELL ]}'>#{head[ :title ]}</th>"
80
+ str += "<td class='#{styles[ CELL ]}'>#{cell_value}</td>"
81
+ str += "</tr>"
82
+ end
83
+
84
+ str += "</tbody></table>"
85
+ return str
86
+ end
87
+
88
+ #
89
+ # Show in normal, multi-row format.
90
+ #
91
+ def data_to_table_rows( params )
92
+ styles = params[ :styles ]
93
+ # headers = params[ :head ]
94
+
95
+ str = "<table class='#{styles[ TABLE ]}'>"
96
+ str << "<thead class='#{styles[ THEAD ]}'><tr>"
97
+
98
+ params[ :columns ].each do |head|
99
+ next unless head[ :visible ]
100
+ str += "<th class='#{styles[ HEAD_CELL ]}'>#{head[ :title ]}</th>"
101
+ end
102
+ str << "</tr></thead><tbody>"
103
+
104
+ params[ :rows ].each do |row|
105
+ str += "<tr class='#{styles[ ROW ]}'>"
106
+
107
+ # row.each_with_index do |cell, i|
108
+ params[ :columns ].each do |head|
109
+ next unless head[ :visible ]
110
+
111
+ cell = row[ head[ :data_index ] ]
112
+ this_col_name = head[ :name ]
113
+
114
+ if head[ :cell_renderer ]
115
+ cell_value = render_cell( row, head, params[ :columns ] )
116
+ else
117
+ cell_value = cell
118
+ end
119
+ str += "<td class='#{styles[ CELL ]}'>#{cell_value}</td>"
120
+ end
121
+ str += "</tr>"
122
+ end
123
+ str += "</tbody></table>"
124
+
125
+ return str
126
+ end
127
+
128
+ #
129
+ # Render a cell using the cell renderer and the given
130
+ # context data (the row's values).
131
+ #
132
+ def render_cell row, col, cols
133
+ params = {}
134
+
135
+ cols.each_with_index do |c, i|
136
+ params[ c[ :name ] ] = row[ c[ :data_index ] ]
137
+ end
138
+
139
+ content = col[ :cell_renderer ]
140
+ content = @engine.running_app.obj.embedded_renderer.render content, params
141
+
142
+ # renderer = ERB.new( col[ :cell_renderer ] )
143
+ # content = renderer.result_with_hash( params )
144
+
145
+ return content
146
+ end
147
+ end
148
+
149
+ end