itsi-server 0.2.2 → 0.2.4
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 +4 -4
- data/Cargo.lock +28 -29
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_server/Cargo.toml +1 -1
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +26 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +28 -11
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +1 -1
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +1 -2
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +14 -2
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +86 -41
- data/ext/itsi_server/src/services/itsi_http_service.rs +46 -35
- data/ext/itsi_server/src/services/static_file_server.rs +31 -3
- data/lib/itsi/http_request.rb +31 -34
- data/lib/itsi/http_response.rb +10 -8
- data/lib/itsi/passfile.rb +6 -6
- data/lib/itsi/server/config/config_helpers.rb +33 -33
- data/lib/itsi/server/config/dsl.rb +16 -21
- data/lib/itsi/server/config/known_paths.rb +11 -7
- data/lib/itsi/server/config/middleware/endpoint/endpoint.rb +0 -4
- data/lib/itsi/server/config/middleware/error_response.md +13 -0
- data/lib/itsi/server/config/middleware/location.rb +25 -21
- data/lib/itsi/server/config/middleware/proxy.rb +15 -14
- data/lib/itsi/server/config/middleware/rackup_file.rb +7 -10
- data/lib/itsi/server/config/middleware/static_assets.md +40 -0
- data/lib/itsi/server/config/middleware/static_assets.rb +8 -4
- data/lib/itsi/server/config/middleware/string_rewrite.md +14 -0
- data/lib/itsi/server/config/option.rb +0 -1
- data/lib/itsi/server/config/options/include.rb +1 -1
- data/lib/itsi/server/config/options/nodelay.md +2 -2
- data/lib/itsi/server/config/options/reuse_address.md +1 -1
- data/lib/itsi/server/config/typed_struct.rb +32 -35
- data/lib/itsi/server/config.rb +107 -92
- data/lib/itsi/server/default_app/default_app.rb +1 -1
- data/lib/itsi/server/grpc/grpc_call.rb +4 -5
- data/lib/itsi/server/grpc/grpc_interface.rb +6 -7
- data/lib/itsi/server/rack/handler/itsi.rb +0 -1
- data/lib/itsi/server/rack_interface.rb +1 -2
- data/lib/itsi/server/route_tester.rb +26 -24
- data/lib/itsi/server/typed_handlers/param_parser.rb +25 -0
- data/lib/itsi/server/typed_handlers/source_parser.rb +9 -7
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +22 -22
- data/lib/itsi/standard_headers.rb +80 -80
- metadata +3 -3
@@ -1,14 +1,15 @@
|
|
1
1
|
use crate::default_responses::{NOT_FOUND_RESPONSE, TIMEOUT_RESPONSE};
|
2
2
|
use crate::ruby_types::itsi_server::itsi_server_config::{ItsiServerTokenPreference, ServerParams};
|
3
3
|
use crate::server::binds::listener::ListenerInfo;
|
4
|
-
use crate::server::http_message_types::{
|
4
|
+
use crate::server::http_message_types::{
|
5
|
+
ConversionExt, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
|
6
|
+
};
|
5
7
|
use crate::server::lifecycle_event::LifecycleEvent;
|
6
8
|
use crate::server::middleware_stack::MiddlewareLayer;
|
7
9
|
use crate::server::request_job::RequestJob;
|
8
10
|
use crate::server::serve_strategy::single_mode::RunningPhase;
|
9
11
|
use crate::server::signal::send_lifecycle_event;
|
10
|
-
use chrono;
|
11
|
-
use chrono::Local;
|
12
|
+
use chrono::{self, DateTime, Local};
|
12
13
|
use either::Either;
|
13
14
|
use http::header::ACCEPT_ENCODING;
|
14
15
|
use http::{HeaderValue, Request};
|
@@ -18,6 +19,7 @@ use itsi_error::ItsiError;
|
|
18
19
|
use regex::Regex;
|
19
20
|
use std::sync::atomic::{AtomicBool, Ordering};
|
20
21
|
use std::sync::OnceLock;
|
22
|
+
use std::time::{Duration, Instant};
|
21
23
|
use tracing::error;
|
22
24
|
|
23
25
|
use std::{future::Future, ops::Deref, pin::Pin, sync::Arc};
|
@@ -68,17 +70,16 @@ impl Deref for RequestContextInner {
|
|
68
70
|
}
|
69
71
|
|
70
72
|
pub struct RequestContextInner {
|
71
|
-
pub request_id:
|
73
|
+
pub request_id: u64,
|
72
74
|
pub service: ItsiHttpService,
|
73
75
|
pub accept: ResponseFormat,
|
74
76
|
pub matching_pattern: Option<Arc<Regex>>,
|
75
77
|
pub origin: OnceLock<Option<String>>,
|
76
78
|
pub response_format: OnceLock<ResponseFormat>,
|
77
|
-
pub
|
78
|
-
pub
|
79
|
-
pub request_start_time: OnceLock<chrono::DateTime<Local>>,
|
79
|
+
pub request_start_time: OnceLock<DateTime<Local>>,
|
80
|
+
pub start_instant: Instant,
|
80
81
|
pub if_none_match: OnceLock<Option<String>>,
|
81
|
-
pub supported_encoding_set: Vec<HeaderValue
|
82
|
+
pub supported_encoding_set: OnceLock<Vec<HeaderValue>>,
|
82
83
|
pub is_ruby_request: Arc<AtomicBool>,
|
83
84
|
}
|
84
85
|
|
@@ -87,27 +88,38 @@ impl HttpRequestContext {
|
|
87
88
|
service: ItsiHttpService,
|
88
89
|
matching_pattern: Option<Arc<Regex>>,
|
89
90
|
accept: ResponseFormat,
|
90
|
-
supported_encoding_set: Vec<HeaderValue>,
|
91
91
|
is_ruby_request: Arc<AtomicBool>,
|
92
92
|
) -> Self {
|
93
93
|
HttpRequestContext {
|
94
94
|
inner: Arc::new(RequestContextInner {
|
95
|
-
request_id: rand::random::<
|
95
|
+
request_id: rand::random::<u64>(),
|
96
96
|
service,
|
97
97
|
matching_pattern,
|
98
98
|
accept,
|
99
99
|
origin: OnceLock::new(),
|
100
100
|
response_format: OnceLock::new(),
|
101
|
-
start_time: chrono::Utc::now(),
|
102
|
-
request: None,
|
103
101
|
request_start_time: OnceLock::new(),
|
102
|
+
start_instant: Instant::now(),
|
104
103
|
if_none_match: OnceLock::new(),
|
105
|
-
supported_encoding_set,
|
104
|
+
supported_encoding_set: OnceLock::new(),
|
106
105
|
is_ruby_request,
|
107
106
|
}),
|
108
107
|
}
|
109
108
|
}
|
110
109
|
|
110
|
+
pub fn set_supported_encoding_set(&self, req: &HttpRequest) {
|
111
|
+
let supported_encoding_set = req
|
112
|
+
.headers()
|
113
|
+
.get_all(ACCEPT_ENCODING)
|
114
|
+
.into_iter()
|
115
|
+
.cloned()
|
116
|
+
.collect::<Vec<_>>();
|
117
|
+
self.inner
|
118
|
+
.supported_encoding_set
|
119
|
+
.set(supported_encoding_set)
|
120
|
+
.unwrap();
|
121
|
+
}
|
122
|
+
|
111
123
|
pub fn set_origin(&self, origin: Option<String>) {
|
112
124
|
self.inner.origin.set(origin).unwrap();
|
113
125
|
}
|
@@ -121,28 +133,29 @@ impl HttpRequestContext {
|
|
121
133
|
}
|
122
134
|
|
123
135
|
pub fn short_request_id(&self) -> String {
|
124
|
-
format!("{:
|
136
|
+
format!("{:08x}", self.inner.request_id & 0xffff_ffff)
|
125
137
|
}
|
126
138
|
|
127
139
|
pub fn request_id(&self) -> String {
|
128
|
-
format!("{:
|
140
|
+
format!("{:08x}", self.inner.request_id)
|
129
141
|
}
|
130
142
|
|
131
|
-
pub fn
|
143
|
+
pub fn init_logging_params(&self) {
|
132
144
|
self.inner
|
133
145
|
.request_start_time
|
134
146
|
.get_or_init(chrono::Local::now);
|
135
147
|
}
|
136
148
|
|
137
|
-
pub fn
|
149
|
+
pub fn start_instant(&self) -> Instant {
|
150
|
+
self.inner.start_instant
|
151
|
+
}
|
152
|
+
|
153
|
+
pub fn start_time(&self) -> Option<DateTime<Local>> {
|
138
154
|
self.inner.request_start_time.get().cloned()
|
139
155
|
}
|
140
156
|
|
141
|
-
pub fn get_response_time(&self) ->
|
142
|
-
self.inner
|
143
|
-
.request_start_time
|
144
|
-
.get()
|
145
|
-
.map(|instant| Local::now() - instant)
|
157
|
+
pub fn get_response_time(&self) -> Duration {
|
158
|
+
self.inner.start_instant.elapsed()
|
146
159
|
}
|
147
160
|
|
148
161
|
pub fn set_response_format(&self, format: ResponseFormat) {
|
@@ -152,6 +165,10 @@ impl HttpRequestContext {
|
|
152
165
|
pub fn response_format(&self) -> &ResponseFormat {
|
153
166
|
self.inner.response_format.get().unwrap()
|
154
167
|
}
|
168
|
+
|
169
|
+
pub fn supported_encoding_set(&self) -> Option<&Vec<HeaderValue>> {
|
170
|
+
self.inner.supported_encoding_set.get()
|
171
|
+
}
|
155
172
|
}
|
156
173
|
|
157
174
|
const SERVER_TOKEN_VERSION: HeaderValue =
|
@@ -170,12 +187,7 @@ impl Service<Request<Incoming>> for ItsiHttpService {
|
|
170
187
|
let accept: ResponseFormat = req.accept().into();
|
171
188
|
let accept_clone = accept.clone();
|
172
189
|
let is_single_mode = self.server_params.workers == 1;
|
173
|
-
|
174
|
-
.headers()
|
175
|
-
.get_all(ACCEPT_ENCODING)
|
176
|
-
.into_iter()
|
177
|
-
.cloned()
|
178
|
-
.collect::<Vec<_>>();
|
190
|
+
|
179
191
|
let request_timeout = self.server_params.request_timeout;
|
180
192
|
let is_ruby_request = Arc::new(AtomicBool::new(false));
|
181
193
|
let irr_clone = is_ruby_request.clone();
|
@@ -187,7 +199,6 @@ impl Service<Request<Incoming>> for ItsiHttpService {
|
|
187
199
|
self_clone,
|
188
200
|
matching_pattern,
|
189
201
|
accept_clone.clone(),
|
190
|
-
supported_encoding_set,
|
191
202
|
irr_clone,
|
192
203
|
);
|
193
204
|
let mut depth = 0;
|
@@ -229,8 +240,8 @@ impl Service<Request<Incoming>> for ItsiHttpService {
|
|
229
240
|
Ok(resp)
|
230
241
|
};
|
231
242
|
|
232
|
-
|
233
|
-
|
243
|
+
if let Some(timeout_duration) = request_timeout {
|
244
|
+
Box::pin(async move {
|
234
245
|
match timeout(timeout_duration, service_future).await {
|
235
246
|
Ok(result) => result,
|
236
247
|
Err(_) => {
|
@@ -249,9 +260,9 @@ impl Service<Request<Incoming>> for ItsiHttpService {
|
|
249
260
|
Ok(TIMEOUT_RESPONSE.to_http_response(accept).await)
|
250
261
|
}
|
251
262
|
}
|
252
|
-
}
|
253
|
-
|
254
|
-
|
255
|
-
}
|
263
|
+
})
|
264
|
+
} else {
|
265
|
+
Box::pin(service_future)
|
266
|
+
}
|
256
267
|
}
|
257
268
|
}
|
@@ -51,6 +51,10 @@ pub static ROOT_STATIC_FILE_SERVER: LazyLock<StaticFileServer> = LazyLock::new(|
|
|
51
51
|
not_found_behavior: NotFoundBehavior::Error(ErrorResponse::not_found()),
|
52
52
|
serve_hidden_files: false,
|
53
53
|
allowed_extensions: vec!["html".to_string(), "css".to_string(), "js".to_string()],
|
54
|
+
miss_cache: Cache::builder()
|
55
|
+
.max_capacity(1000)
|
56
|
+
.time_to_live(Duration::from_secs(1))
|
57
|
+
.build(),
|
54
58
|
})
|
55
59
|
.unwrap()
|
56
60
|
});
|
@@ -85,6 +89,7 @@ pub struct StaticFileServerConfig {
|
|
85
89
|
pub headers: Option<HashMap<String, String>>,
|
86
90
|
pub serve_hidden_files: bool,
|
87
91
|
pub allowed_extensions: Vec<String>,
|
92
|
+
pub miss_cache: Cache<String, NotFoundBehavior>,
|
88
93
|
}
|
89
94
|
|
90
95
|
#[derive(Debug, Clone)]
|
@@ -389,6 +394,29 @@ impl StaticFileServer {
|
|
389
394
|
abs_path: &str,
|
390
395
|
accept: ResponseFormat,
|
391
396
|
) -> std::result::Result<ResolvedAsset, NotFoundBehavior> {
|
397
|
+
let ext_opt = Path::new(key)
|
398
|
+
.extension()
|
399
|
+
.and_then(|e| e.to_str())
|
400
|
+
.map(|s| s.to_lowercase());
|
401
|
+
|
402
|
+
// If the allowed list is non-empty, enforce membership
|
403
|
+
if !self.allowed_extensions.is_empty() {
|
404
|
+
match ext_opt {
|
405
|
+
Some(ref ext)
|
406
|
+
if self
|
407
|
+
.allowed_extensions
|
408
|
+
.iter()
|
409
|
+
.any(|ae| ae.eq_ignore_ascii_case(ext)) => {}
|
410
|
+
None if self.config.try_html_extension => {}
|
411
|
+
_ => {
|
412
|
+
return Err(self.config.not_found_behavior.clone());
|
413
|
+
}
|
414
|
+
}
|
415
|
+
}
|
416
|
+
|
417
|
+
if let Some(cached_nf) = self.miss_cache.get(key) {
|
418
|
+
return Err(cached_nf.clone());
|
419
|
+
}
|
392
420
|
// First check if we have a cached mapping for this key
|
393
421
|
if let Some(path) = self.key_to_path.lock().await.get(key) {
|
394
422
|
// Check if the cached entry is still valid
|
@@ -449,7 +477,6 @@ impl StaticFileServer {
|
|
449
477
|
|
450
478
|
let mut full_path = self.config.root_dir.clone();
|
451
479
|
full_path.push(normalized_path);
|
452
|
-
debug!("Resolving path {:?}", full_path);
|
453
480
|
// Check if path exists and is a file
|
454
481
|
match tokio::fs::metadata(&full_path).await {
|
455
482
|
Ok(metadata) => {
|
@@ -561,7 +588,6 @@ impl StaticFileServer {
|
|
561
588
|
}
|
562
589
|
Err(_) => {
|
563
590
|
// Path doesn't exist, try with .html extension if configured
|
564
|
-
debug!("Path doesn't exist");
|
565
591
|
if self.config.try_html_extension {
|
566
592
|
let mut html_path = full_path.clone();
|
567
593
|
html_path.set_extension("html");
|
@@ -592,7 +618,9 @@ impl StaticFileServer {
|
|
592
618
|
}
|
593
619
|
|
594
620
|
// If we get here, we couldn't resolve the key to a file
|
595
|
-
|
621
|
+
let nf = self.config.not_found_behavior.clone();
|
622
|
+
self.miss_cache.insert(key.to_string(), nf.clone());
|
623
|
+
Err(nf)
|
596
624
|
}
|
597
625
|
|
598
626
|
async fn stream_file_range(
|
data/lib/itsi/http_request.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require "stringio"
|
4
4
|
require "socket"
|
5
5
|
require "uri"
|
6
|
-
require_relative
|
6
|
+
require_relative "http_request/response_status_shortcodes"
|
7
7
|
|
8
8
|
module Itsi
|
9
9
|
class HttpRequest
|
@@ -22,7 +22,7 @@ module Itsi
|
|
22
22
|
end
|
23
23
|
[header, rack_form]
|
24
24
|
end.to_h.tap do |hm|
|
25
|
-
hm.default_proc = proc { |
|
25
|
+
hm.default_proc = proc { |_, key| "HTTP_#{key.upcase.gsub(/-/, "_")}" }
|
26
26
|
end
|
27
27
|
|
28
28
|
def to_rack_env
|
@@ -78,7 +78,7 @@ module Itsi
|
|
78
78
|
end
|
79
79
|
|
80
80
|
def respond(
|
81
|
-
_body = nil, _status = 200, _headers = nil,
|
81
|
+
_body = nil, _status = 200, _headers = nil, # rubocop:disable Lint/UnderscorePrefixedVariableName
|
82
82
|
json: nil,
|
83
83
|
html: nil,
|
84
84
|
text: nil,
|
@@ -90,13 +90,12 @@ module Itsi
|
|
90
90
|
body: _body,
|
91
91
|
&blk
|
92
92
|
)
|
93
|
-
|
94
93
|
if json
|
95
94
|
if as
|
96
95
|
begin
|
97
96
|
validate!(json, as: as)
|
98
97
|
rescue ValidationError => e
|
99
|
-
json = {type:
|
98
|
+
json = { type: "error", message: "Validation Error: #{e.message}" }
|
100
99
|
status = 400
|
101
100
|
end
|
102
101
|
end
|
@@ -118,15 +117,13 @@ module Itsi
|
|
118
117
|
end
|
119
118
|
|
120
119
|
response.respond(status: status, headers: headers, body: body, hijack: hijack, &blk)
|
121
|
-
|
122
|
-
|
123
120
|
end
|
124
121
|
|
125
122
|
def hijack
|
126
123
|
self.hijacked = true
|
127
124
|
UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
|
128
125
|
server_sock.autoclose = false
|
129
|
-
|
126
|
+
response.hijack(server_sock.fileno)
|
130
127
|
server_sock.sync = true
|
131
128
|
app_sock.sync = true
|
132
129
|
app_sock
|
@@ -150,37 +147,37 @@ module Itsi
|
|
150
147
|
as ? apply_schema!(params, as) : params
|
151
148
|
end
|
152
149
|
|
153
|
-
def params(schema=nil)
|
154
|
-
params =
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
150
|
+
def params(schema = nil)
|
151
|
+
params = if url_encoded?
|
152
|
+
URI.decode_www_form(build_input_io.read).to_h
|
153
|
+
elsif json?
|
154
|
+
JSON.parse(build_input_io.read)
|
155
|
+
elsif multipart?
|
156
|
+
Rack::Multipart::Parser.parse(
|
157
|
+
build_input_io,
|
158
|
+
content_length,
|
159
|
+
content_type,
|
160
|
+
Rack::Multipart::Parser::TEMPFILE_FACTORY,
|
161
|
+
Rack::Multipart::Parser::BUFSIZE,
|
162
|
+
Rack::Utils.default_query_parser
|
163
|
+
).params
|
164
|
+
else
|
165
|
+
{}
|
166
|
+
end
|
169
167
|
|
170
168
|
params.merge!(query_params).merge!(url_params)
|
171
169
|
validated = schema ? apply_schema!(params, schema) : params
|
172
|
-
|
173
|
-
if multipart?
|
174
|
-
raise "#params must take a block for multipart requests"
|
175
|
-
else
|
176
|
-
return validated
|
177
|
-
end
|
178
|
-
else
|
170
|
+
if block_given?
|
179
171
|
yield validated
|
172
|
+
else
|
173
|
+
raise "#params must take a block for multipart requests" if multipart?
|
174
|
+
|
175
|
+
validated
|
176
|
+
|
180
177
|
end
|
181
178
|
rescue ValidationError => e
|
182
179
|
if response.json?
|
183
|
-
respond(json: {error: e.message}, status: 400)
|
180
|
+
respond(json: { error: e.message }, status: 400)
|
184
181
|
else
|
185
182
|
respond(e.message, 400)
|
186
183
|
end
|
@@ -191,7 +188,7 @@ module Itsi
|
|
191
188
|
# Unexpected error.
|
192
189
|
# Don't reveal potential sensitive information to client.
|
193
190
|
if response.json?
|
194
|
-
respond(json: {error: "Internal Server Error"}, status: 500)
|
191
|
+
respond(json: { error: "Internal Server Error" }, status: 500)
|
195
192
|
else
|
196
193
|
respond("Internal Server Error", 500)
|
197
194
|
end
|
@@ -205,7 +202,7 @@ module Itsi
|
|
205
202
|
if params.key?(:tempfile)
|
206
203
|
params[:tempfile].unlink
|
207
204
|
else
|
208
|
-
|
205
|
+
params.each_value { |v| clean_temp_files(v) }
|
209
206
|
end
|
210
207
|
when Array then params.each { |v| clean_temp_files(v) }
|
211
208
|
end
|
data/lib/itsi/http_response.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
|
+
require "forwardable"
|
3
4
|
require "stringio"
|
4
5
|
require "socket"
|
5
6
|
|
6
7
|
module Itsi
|
7
|
-
|
8
8
|
class HttpResponse
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
def respond(
|
10
|
+
_body = nil, _status = 200, _header = nil, # rubocop:disable Lint/UnderscorePrefixedVariableName
|
11
|
+
status: _status, headers: _header, body: _body,
|
12
|
+
hijack: false
|
13
|
+
)
|
14
|
+
self.status = status.is_a?(Symbol) ? HTTP_STATUS_NAME_TO_CODE_MAP.fetch(status) : status.to_i
|
13
15
|
|
14
16
|
body = body.to_s unless body.is_a?(String)
|
15
17
|
|
@@ -33,9 +35,9 @@ module Itsi
|
|
33
35
|
|
34
36
|
# If you hijack the connection, you are responsible for closing it.
|
35
37
|
# Otherwise, the response will be closed automatically.
|
36
|
-
|
38
|
+
close unless hijack
|
37
39
|
else
|
38
|
-
|
40
|
+
close
|
39
41
|
end
|
40
42
|
end
|
41
43
|
end
|
data/lib/itsi/passfile.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
module Itsi
|
2
2
|
class Server
|
3
|
-
|
4
3
|
module Passfile
|
5
|
-
require
|
4
|
+
require "io/console"
|
6
5
|
|
7
6
|
module_function
|
8
7
|
|
@@ -18,7 +17,7 @@ module Itsi
|
|
18
17
|
line.chomp!
|
19
18
|
next if line.empty?
|
20
19
|
|
21
|
-
user, pass = line.split(
|
20
|
+
user, pass = line.split(":", 2)
|
22
21
|
creds[user] = pass
|
23
22
|
end
|
24
23
|
end
|
@@ -26,14 +25,14 @@ module Itsi
|
|
26
25
|
end
|
27
26
|
|
28
27
|
def save(creds, filename)
|
29
|
-
File.open(filename,
|
28
|
+
File.open(filename, "w", 0o600) do |f|
|
30
29
|
creds.each do |u, p|
|
31
30
|
f.puts "#{u}:#{p}"
|
32
31
|
end
|
33
32
|
end
|
34
33
|
end
|
35
34
|
|
36
|
-
def echo(
|
35
|
+
def echo(_, algorithm)
|
37
36
|
print "Enter username: "
|
38
37
|
username = $stdin.gets.chomp
|
39
38
|
|
@@ -56,6 +55,7 @@ module Itsi
|
|
56
55
|
|
57
56
|
def add(filename, algorithm)
|
58
57
|
return unless (creds = load(filename))
|
58
|
+
|
59
59
|
print "Enter username: "
|
60
60
|
username = $stdin.gets.chomp
|
61
61
|
|
@@ -98,11 +98,11 @@ module Itsi
|
|
98
98
|
def list(filename)
|
99
99
|
puts "Current credentials in '#{filename}':"
|
100
100
|
return unless (creds = load(filename))
|
101
|
+
|
101
102
|
creds.each do |u, p|
|
102
103
|
puts "#{u}:#{p}"
|
103
104
|
end
|
104
105
|
end
|
105
|
-
|
106
106
|
end
|
107
107
|
end
|
108
108
|
end
|
@@ -2,7 +2,6 @@ module Itsi
|
|
2
2
|
class Server
|
3
3
|
module Config
|
4
4
|
module ConfigHelpers
|
5
|
-
|
6
5
|
def self.load_and_register(klass)
|
7
6
|
config_type = klass.name.split("::").last.downcase.gsub(/([a-z]()[A-Z])/, '\1_\2')
|
8
7
|
|
@@ -17,35 +16,35 @@ module Itsi
|
|
17
16
|
following = klass.subclasses
|
18
17
|
new_class = (following - current).first
|
19
18
|
|
20
|
-
documentation_file = "#{file[/(.*)\.rb/,1]}.md"
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
.gsub(/^\{\{[^\}]+\}\}/, "") # Strip Hugo blocks
|
29
|
-
end
|
19
|
+
documentation_file = "#{file[/(.*)\.rb/, 1]}.md"
|
20
|
+
documentation_file = "#{file[%r{(.*)/[^/]+\.rb}, 1]}/_index.md" unless File.exist?(documentation_file)
|
21
|
+
next unless File.exist?(documentation_file) && new_class
|
22
|
+
|
23
|
+
new_class.documentation IO.read(documentation_file)
|
24
|
+
.gsub(/^---.*?\n.*?-+/m, "") # Strip frontmatter
|
25
|
+
.gsub(/^(```.*?)\{.*?\}.*$/, "\\1") # Strip filename from code blocks
|
26
|
+
.gsub(/^\{\{[^}]+\}\}/, "") # Strip Hugo blocks
|
30
27
|
end
|
31
28
|
end
|
32
29
|
|
33
|
-
def normalize_keys!(hash, expected=[])
|
30
|
+
def normalize_keys!(hash, expected = [])
|
34
31
|
hash.keys.each do |key|
|
35
32
|
value = hash.delete(key)
|
36
33
|
key = key.to_s.downcase.to_sym
|
37
34
|
hash[key] = value
|
38
35
|
raise "Unexpected key: #{key}" unless expected.include?(key)
|
36
|
+
|
39
37
|
expected -= [key]
|
40
38
|
end
|
41
|
-
raise "Missing required keys: #{expected.join(
|
39
|
+
raise "Missing required keys: #{expected.join(", ")}" unless expected.empty?
|
40
|
+
|
42
41
|
hash
|
43
42
|
end
|
44
43
|
|
45
|
-
def self.included(cls)
|
46
|
-
def cls.inherited(base)
|
44
|
+
def self.included(cls) # rubocop:disable Metrics/PerceivedComplexity,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
|
45
|
+
def cls.inherited(base) # rubocop:disable Metrics/MethodLength,Lint/MissingSuper,Metrics/PerceivedComplexity
|
47
46
|
%i[detail documentation insert_text schema].each do |attr|
|
48
|
-
base.define_singleton_method(attr) do |value=nil|
|
47
|
+
base.define_singleton_method(attr) do |value = nil|
|
49
48
|
@middleware_class_attrs ||= {}
|
50
49
|
if value
|
51
50
|
@middleware_class_attrs[attr] = value
|
@@ -54,12 +53,12 @@ module Itsi
|
|
54
53
|
end
|
55
54
|
end
|
56
55
|
|
57
|
-
base.define_method(attr) do |
|
56
|
+
base.define_method(attr) do |_value = nil|
|
58
57
|
self.class.send(attr)
|
59
58
|
end
|
60
59
|
end
|
61
60
|
|
62
|
-
def base.schema(value=nil, &blk)
|
61
|
+
def base.schema(value = nil, &blk)
|
63
62
|
@middleware_class_attrs ||= {}
|
64
63
|
if blk
|
65
64
|
@middleware_class_attrs[:schema] = TypedStruct.new(&blk)
|
@@ -75,29 +74,30 @@ module Itsi
|
|
75
74
|
|
76
75
|
config_type = cls.name.split("::").last.downcase
|
77
76
|
|
78
|
-
cls.define_singleton_method("#{config_type}_name") do |name=self.name|
|
77
|
+
cls.define_singleton_method("#{config_type}_name") do |name = self.name|
|
79
78
|
@config_name ||= name.split("::").last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
|
80
79
|
end
|
81
|
-
cls.define_method(:opt_name){ self.class.send("#{config_type}_name") }
|
82
|
-
cls.define_method(:location){ @location }
|
80
|
+
cls.define_method(:opt_name) { self.class.send("#{config_type}_name") }
|
81
|
+
cls.define_method(:location) { @location }
|
83
82
|
end
|
84
83
|
|
85
|
-
def initialize(location, params={})
|
84
|
+
def initialize(location, params = {})
|
86
85
|
if !self.class.ancestors.include?(Middleware) && !location.parent.nil?
|
87
86
|
raise "#{opt_name} must be set at the top level"
|
88
87
|
end
|
88
|
+
|
89
89
|
@location = location
|
90
|
-
@params = case
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
90
|
+
@params = case schema
|
91
|
+
when TypedStruct::Validation
|
92
|
+
schema.validate!(params)
|
93
|
+
when Array
|
94
|
+
default, validation = schema
|
95
|
+
params ? validation.validate!(params) : default
|
96
|
+
when nil
|
97
|
+
nil
|
98
|
+
else
|
99
|
+
schema.new(params).to_h
|
100
|
+
end
|
101
101
|
end
|
102
102
|
end
|
103
103
|
end
|