itsi 0.1.14 → 0.1.18
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 +124 -109
- data/Cargo.toml +6 -0
- data/crates/itsi_error/Cargo.toml +1 -0
- data/crates/itsi_error/src/lib.rs +100 -10
- data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
- data/crates/itsi_server/Cargo.toml +8 -10
- data/crates/itsi_server/src/default_responses/html/401.html +68 -0
- data/crates/itsi_server/src/default_responses/html/403.html +68 -0
- data/crates/itsi_server/src/default_responses/html/404.html +68 -0
- data/crates/itsi_server/src/default_responses/html/413.html +71 -0
- data/crates/itsi_server/src/default_responses/html/429.html +68 -0
- data/crates/itsi_server/src/default_responses/html/500.html +71 -0
- data/crates/itsi_server/src/default_responses/html/502.html +71 -0
- data/crates/itsi_server/src/default_responses/html/503.html +68 -0
- data/crates/itsi_server/src/default_responses/html/504.html +69 -0
- data/crates/itsi_server/src/default_responses/html/index.html +238 -0
- data/crates/itsi_server/src/default_responses/json/401.json +6 -0
- data/crates/itsi_server/src/default_responses/json/403.json +6 -0
- data/crates/itsi_server/src/default_responses/json/404.json +6 -0
- data/crates/itsi_server/src/default_responses/json/413.json +6 -0
- data/crates/itsi_server/src/default_responses/json/429.json +6 -0
- data/crates/itsi_server/src/default_responses/json/500.json +6 -0
- data/crates/itsi_server/src/default_responses/json/502.json +6 -0
- data/crates/itsi_server/src/default_responses/json/503.json +6 -0
- data/crates/itsi_server/src/default_responses/json/504.json +6 -0
- data/crates/itsi_server/src/default_responses/mod.rs +11 -0
- data/crates/itsi_server/src/lib.rs +58 -26
- data/crates/itsi_server/src/prelude.rs +2 -0
- data/crates/itsi_server/src/ruby_types/README.md +21 -0
- data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +8 -6
- data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/crates/itsi_server/src/ruby_types/{itsi_grpc_stream → itsi_grpc_response_stream}/mod.rs +121 -73
- data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +103 -40
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +8 -5
- data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +4 -4
- data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +37 -17
- data/crates/itsi_server/src/ruby_types/itsi_server.rs +4 -3
- data/crates/itsi_server/src/ruby_types/mod.rs +6 -13
- data/crates/itsi_server/src/server/{bind.rs → binds/bind.rs} +23 -4
- data/crates/itsi_server/src/server/{listener.rs → binds/listener.rs} +24 -10
- data/crates/itsi_server/src/server/binds/mod.rs +4 -0
- data/crates/itsi_server/src/server/{tls.rs → binds/tls.rs} +9 -4
- data/crates/itsi_server/src/server/http_message_types.rs +97 -0
- data/crates/itsi_server/src/server/io_stream.rs +2 -1
- data/crates/itsi_server/src/server/middleware_stack/middleware.rs +28 -16
- data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +17 -8
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +47 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +13 -9
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +50 -29
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +5 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +37 -48
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +25 -20
- data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +14 -7
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +125 -95
- data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +9 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +1 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +25 -19
- data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +4 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +9 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +260 -62
- data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +29 -22
- data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +6 -6
- data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +6 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +4 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +51 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +31 -13
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +13 -8
- data/crates/itsi_server/src/server/middleware_stack/mod.rs +101 -69
- data/crates/itsi_server/src/server/mod.rs +3 -9
- data/crates/itsi_server/src/server/process_worker.rs +21 -3
- data/crates/itsi_server/src/server/request_job.rs +2 -2
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +8 -3
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +26 -26
- data/crates/itsi_server/src/server/signal.rs +24 -41
- data/crates/itsi_server/src/server/size_limited_incoming.rs +101 -0
- data/crates/itsi_server/src/server/thread_worker.rs +59 -28
- data/crates/itsi_server/src/services/itsi_http_service.rs +239 -0
- data/crates/itsi_server/src/services/mime_types.rs +1416 -0
- data/crates/itsi_server/src/services/mod.rs +6 -0
- data/crates/itsi_server/src/services/password_hasher.rs +83 -0
- data/crates/itsi_server/src/{server → services}/rate_limiter.rs +35 -31
- data/crates/itsi_server/src/{server → services}/static_file_server.rs +521 -181
- data/crates/itsi_tracing/src/lib.rs +145 -55
- data/{Itsi.rb → foo/Itsi.rb} +6 -9
- data/gems/scheduler/Cargo.lock +7 -0
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/test/helpers/test_helper.rb +0 -1
- data/gems/scheduler/test/test_address_resolve.rb +0 -1
- data/gems/scheduler/test/test_network_io.rb +1 -1
- data/gems/scheduler/test/test_process_wait.rb +0 -1
- data/gems/server/Cargo.lock +124 -109
- data/gems/server/exe/itsi +65 -19
- data/gems/server/itsi-server.gemspec +4 -3
- data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
- data/gems/server/lib/itsi/http_request.rb +116 -17
- data/gems/server/lib/itsi/http_response.rb +2 -0
- data/gems/server/lib/itsi/passfile.rb +109 -0
- data/gems/server/lib/itsi/server/config/dsl.rb +160 -101
- data/gems/server/lib/itsi/server/config.rb +58 -23
- data/gems/server/lib/itsi/server/default_app/default_app.rb +25 -29
- data/gems/server/lib/itsi/server/default_app/index.html +113 -89
- data/gems/server/lib/itsi/server/{Itsi.rb → default_config/Itsi-rackup.rb} +1 -1
- data/gems/server/lib/itsi/server/default_config/Itsi.rb +107 -0
- data/gems/server/lib/itsi/server/grpc/grpc_call.rb +246 -0
- data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +100 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
- data/gems/server/lib/itsi/server/route_tester.rb +107 -0
- data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
- data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
- data/gems/server/lib/itsi/server/typed_handlers.rb +17 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +82 -12
- data/gems/server/lib/ruby_lsp/itsi/addon.rb +111 -0
- data/gems/server/lib/shell_completions/completions.rb +26 -0
- data/gems/server/test/helpers/test_helper.rb +2 -1
- data/lib/itsi/version.rb +1 -1
- data/sandbox/README.md +5 -0
- data/sandbox/itsi_file/Gemfile +4 -2
- data/sandbox/itsi_file/Gemfile.lock +48 -6
- data/sandbox/itsi_file/Itsi.rb +326 -129
- data/sandbox/itsi_file/call.json +1 -0
- data/sandbox/itsi_file/echo_client/Gemfile +10 -0
- data/sandbox/itsi_file/echo_client/Gemfile.lock +27 -0
- data/sandbox/itsi_file/echo_client/README.md +95 -0
- data/sandbox/itsi_file/echo_client/echo_client.rb +164 -0
- data/sandbox/itsi_file/echo_client/gen_proto.sh +17 -0
- data/sandbox/itsi_file/echo_client/lib/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_client/lib/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_client/run_client.rb +64 -0
- data/sandbox/itsi_file/echo_client/test_compressions.sh +20 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile +10 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile.lock +79 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo.proto +26 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_service_nonitsi/server.rb +52 -0
- data/sandbox/itsi_sandbox_async/config.ru +0 -1
- data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile.lock +76 -2
- data/sandbox/itsi_sandbox_rails/app/controllers/home_controller.rb +15 -0
- data/sandbox/itsi_sandbox_rails/config/environments/development.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/environments/production.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/routes.rb +2 -0
- data/sandbox/itsi_sinatra/app.rb +0 -1
- data/sandbox/static_files/.env +1 -0
- data/sandbox/static_files/404.html +25 -0
- data/sandbox/static_files/_DSC0102.NEF.jpg +0 -0
- data/sandbox/static_files/about.html +68 -0
- data/sandbox/static_files/tiny.html +1 -0
- data/sandbox/static_files/writebook.zip +0 -0
- data/tasks.txt +28 -33
- metadata +87 -26
- data/crates/itsi_error/src/from.rs +0 -68
- data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +0 -147
- data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +0 -19
- data/crates/itsi_server/src/server/itsi_service.rs +0 -172
- data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +0 -72
- data/crates/itsi_server/src/server/types.rs +0 -43
- data/gems/server/lib/itsi/server/grpc_interface.rb +0 -213
- data/sandbox/itsi_file/public/assets/index.html +0 -1
- /data/crates/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
- /data/crates/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +0 -0
- /data/crates/itsi_server/src/{server → services}/cache_store.rs +0 -0
@@ -2,12 +2,16 @@
|
|
2
2
|
|
3
3
|
require "stringio"
|
4
4
|
require "socket"
|
5
|
+
require "uri"
|
6
|
+
require_relative 'http_request/response_status_shortcodes'
|
5
7
|
|
6
8
|
module Itsi
|
7
9
|
class HttpRequest
|
10
|
+
include Server::TypedHandlers::ParamParser
|
11
|
+
include ResponseStatusShortcodes
|
8
12
|
attr_accessor :hijacked
|
9
13
|
|
10
|
-
EMPTY_IO = StringIO.new("")
|
14
|
+
EMPTY_IO = StringIO.new("")
|
11
15
|
RACK_HEADER_MAP = StandardHeaders::ALL.map do |header|
|
12
16
|
rack_form = if header == "content-type"
|
13
17
|
"CONTENT_TYPE"
|
@@ -28,7 +32,7 @@ module Itsi
|
|
28
32
|
{
|
29
33
|
"SERVER_SOFTWARE" => "Itsi",
|
30
34
|
"SCRIPT_NAME" => script_name,
|
31
|
-
"REQUEST_METHOD" =>
|
35
|
+
"REQUEST_METHOD" => request_method,
|
32
36
|
"PATH_INFO" => path,
|
33
37
|
"REQUEST_PATH" => path,
|
34
38
|
"QUERY_STRING" => query_string,
|
@@ -49,39 +53,134 @@ module Itsi
|
|
49
53
|
"rack.run_once" => false,
|
50
54
|
"rack.hijack?" => true,
|
51
55
|
"rack.multipart.buffer_size" => 16_384,
|
52
|
-
"rack.hijack" =>
|
56
|
+
"rack.hijack" => method(:hijack)
|
53
57
|
}.tap do |r|
|
54
58
|
headers.each do |(k, v)|
|
55
|
-
r[
|
59
|
+
r[case k
|
60
|
+
when "content-type" then "CONTENT_TYPE"
|
61
|
+
when "content-length" then "CONTENT_LENGTH"
|
62
|
+
when "accept" then "HTTP_ACCEPT"
|
63
|
+
when "accept-encoding" then "HTTP_ACCEPT_ENCODING"
|
64
|
+
when "accept-language" then "HTTP_ACCEPT_LANGUAGE"
|
65
|
+
when "user-agent" then "HTTP_USER_AGENT"
|
66
|
+
when "referer" then "HTTP_REFERER"
|
67
|
+
when "origin" then "HTTP_ORIGIN"
|
68
|
+
when "cookie" then "HTTP_COOKIE"
|
69
|
+
when "authorization" then "HTTP_AUTHORIZATION"
|
70
|
+
when "x-forwarded-for" then "HTTP_X_FORWARDED_FOR"
|
71
|
+
when "x-forwarded-proto" then "HTTP_X_FORWARDED_PROTO"
|
72
|
+
else RACK_HEADER_MAP[k]
|
73
|
+
end
|
74
|
+
] = v
|
56
75
|
end
|
57
76
|
end
|
58
77
|
end
|
59
78
|
|
60
|
-
def respond(
|
61
|
-
|
79
|
+
def respond(
|
80
|
+
_body = nil, _status = 200, _headers = nil,
|
81
|
+
json: nil,
|
82
|
+
html: nil,
|
83
|
+
text: nil,
|
84
|
+
xml: nil,
|
85
|
+
hijack: false,
|
86
|
+
as: nil,
|
87
|
+
status: _status,
|
88
|
+
headers: _headers,
|
89
|
+
body: _body,
|
90
|
+
&blk
|
91
|
+
)
|
92
|
+
|
93
|
+
if json
|
94
|
+
validate!(json, as: as) if as
|
95
|
+
body = json.to_json
|
96
|
+
headers ||= {}
|
97
|
+
headers["Content-Type"] ||= "application/json"
|
98
|
+
elsif html
|
99
|
+
body = html
|
100
|
+
headers ||= {}
|
101
|
+
headers["Content-Type"] ||= "text/html"
|
102
|
+
elsif xml
|
103
|
+
body = xml
|
104
|
+
headers ||= {}
|
105
|
+
headers["Content-Type"] ||= "application/xml"
|
106
|
+
elsif text
|
107
|
+
body = text
|
108
|
+
headers ||= {}
|
109
|
+
headers["Content-Type"] ||= "text/plain"
|
110
|
+
end
|
111
|
+
|
62
112
|
response.respond(status: status, headers: headers, body: body, hijack: hijack, &blk)
|
63
113
|
end
|
64
114
|
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
app_sock
|
74
|
-
end
|
115
|
+
def hijack
|
116
|
+
self.hijacked = true
|
117
|
+
UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
|
118
|
+
server_sock.autoclose = false
|
119
|
+
self.response.hijack(server_sock.fileno)
|
120
|
+
server_sock.sync = true
|
121
|
+
app_sock.sync = true
|
122
|
+
app_sock
|
75
123
|
end
|
76
124
|
end
|
77
125
|
|
78
126
|
def build_input_io
|
79
127
|
case body
|
80
|
-
when nil then
|
128
|
+
when nil then EMPTY_IO
|
81
129
|
when String then StringIO.new(body)
|
82
130
|
when Array then File.open(body.first, "rb")
|
83
131
|
else body
|
84
132
|
end
|
85
133
|
end
|
134
|
+
|
135
|
+
def validate!(params, as: nil)
|
136
|
+
as ? apply_schema!(params, as) : params
|
137
|
+
end
|
138
|
+
|
139
|
+
def params(schema=nil)
|
140
|
+
params = case
|
141
|
+
when url_encoded? then URI.decode_www_form(build_input_io.read).to_h
|
142
|
+
when json? then JSON.parse(build_input_io.read)
|
143
|
+
when multipart?
|
144
|
+
Rack::Multipart::Parser.parse(
|
145
|
+
build_input_io,
|
146
|
+
content_length,
|
147
|
+
content_type,
|
148
|
+
Rack::Multipart::Parser::TEMPFILE_FACTORY,
|
149
|
+
Rack::Multipart::Parser::BUFSIZE,
|
150
|
+
Rack::Utils.default_query_parser
|
151
|
+
).params
|
152
|
+
else
|
153
|
+
{}
|
154
|
+
end
|
155
|
+
|
156
|
+
params.merge!(query_params).merge!(url_params)
|
157
|
+
|
158
|
+
yield schema ? apply_schema!(params, schema) : params
|
159
|
+
|
160
|
+
rescue StandardError => e
|
161
|
+
if response.json?
|
162
|
+
respond(json: {error: e.message}, status: 400)
|
163
|
+
else
|
164
|
+
respond(e.message, 400)
|
165
|
+
end
|
166
|
+
ensure
|
167
|
+
clean_temp_files(params)
|
168
|
+
end
|
169
|
+
|
170
|
+
def clean_temp_files(params)
|
171
|
+
case params
|
172
|
+
when Hash
|
173
|
+
if params.key?(:tempfile)
|
174
|
+
params[:tempfile].unlink
|
175
|
+
else
|
176
|
+
params.each_value { |v| clean_temp_files(v) }
|
177
|
+
end
|
178
|
+
when Array then params.each { |v| clean_temp_files(v) }
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def query_params
|
183
|
+
URI.decode_www_form(query_string).to_h
|
184
|
+
end
|
86
185
|
end
|
87
186
|
end
|
@@ -10,6 +10,8 @@ module Itsi
|
|
10
10
|
def respond _body=nil, _status=200, _header=nil, status: _status, headers: _header, body: _body, hijack: false, &blk
|
11
11
|
self.status = status
|
12
12
|
|
13
|
+
body = body.to_s unless body.is_a?(String)
|
14
|
+
|
13
15
|
if headers
|
14
16
|
headers.each do |key, value|
|
15
17
|
if value.is_a?(Array)
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
|
4
|
+
module Passfile
|
5
|
+
require 'io/console'
|
6
|
+
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def load(filename)
|
10
|
+
if filename.nil? || filename.strip.empty?
|
11
|
+
puts "Error: a valid filename is required."
|
12
|
+
return nil
|
13
|
+
end
|
14
|
+
|
15
|
+
creds = {}
|
16
|
+
if File.exist?(filename)
|
17
|
+
File.foreach(filename) do |line|
|
18
|
+
line.chomp!
|
19
|
+
next if line.empty?
|
20
|
+
|
21
|
+
user, pass = line.split(':', 2)
|
22
|
+
creds[user] = pass
|
23
|
+
end
|
24
|
+
end
|
25
|
+
creds
|
26
|
+
end
|
27
|
+
|
28
|
+
def save(creds, filename)
|
29
|
+
File.open(filename, 'w', 0o600) do |f|
|
30
|
+
creds.each do |u, p|
|
31
|
+
f.puts "#{u}:#{p}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def echo(filename, algorithm)
|
37
|
+
return unless (creds = load(filename))
|
38
|
+
print "Enter username: "
|
39
|
+
username = $stdin.gets.chomp
|
40
|
+
|
41
|
+
print "Enter password: "
|
42
|
+
|
43
|
+
password = $stdin.noecho(&:gets).chomp
|
44
|
+
puts
|
45
|
+
|
46
|
+
print "Confirm password: "
|
47
|
+
password_confirm = $stdin.noecho(&:gets).chomp
|
48
|
+
puts
|
49
|
+
|
50
|
+
if password != password_confirm
|
51
|
+
puts "Error: Passwords do not match!"
|
52
|
+
exit(1)
|
53
|
+
end
|
54
|
+
|
55
|
+
puts "#{username}:#{Itsi.create_password_hash(password, algorithm)}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def add(filename, algorithm)
|
59
|
+
return unless (creds = load(filename))
|
60
|
+
print "Enter username: "
|
61
|
+
username = $stdin.gets.chomp
|
62
|
+
|
63
|
+
print "Enter password: "
|
64
|
+
|
65
|
+
password = $stdin.noecho(&:gets).chomp
|
66
|
+
puts
|
67
|
+
|
68
|
+
print "Confirm password: "
|
69
|
+
password_confirm = $stdin.noecho(&:gets).chomp
|
70
|
+
puts
|
71
|
+
|
72
|
+
if password != password_confirm
|
73
|
+
puts "Error: Passwords do not match!"
|
74
|
+
exit(1)
|
75
|
+
end
|
76
|
+
|
77
|
+
creds[username] = Itsi.create_password_hash(password, algorithm)
|
78
|
+
|
79
|
+
save(creds, filename)
|
80
|
+
|
81
|
+
puts "User '#{username}' added."
|
82
|
+
end
|
83
|
+
|
84
|
+
def remove(filename)
|
85
|
+
return unless (creds = load(filename))
|
86
|
+
|
87
|
+
print "Enter username to remove: "
|
88
|
+
username = $stdin.gets.chomp
|
89
|
+
|
90
|
+
if creds.key?(username)
|
91
|
+
creds.delete(username)
|
92
|
+
save(creds, filename)
|
93
|
+
puts "User '#{username}' removed."
|
94
|
+
else
|
95
|
+
puts "Warning: User '#{username}' not found."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def list(filename)
|
100
|
+
puts "Current credentials in '#{filename}':"
|
101
|
+
return unless (creds = load(filename))
|
102
|
+
creds.each do |u, p|
|
103
|
+
puts "#{u}:#{p}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|