Module: Itsi::Server::Config

Includes:
CidrToRegex
Defined in:
lib/itsi/server/config.rb,
lib/itsi/server/config/dsl.rb,
lib/itsi/server/config/option.rb,
lib/itsi/server/config/middleware.rb,
lib/itsi/server/config/options/bind.rb,
lib/itsi/server/config/typed_struct.rb,
lib/itsi/server/config/options/watch.rb,
lib/itsi/server/config/config_helpers.rb,
lib/itsi/server/config/middleware/csp.rb,
lib/itsi/server/config/middleware/run.rb,
lib/itsi/server/config/middleware/cors.rb,
lib/itsi/server/config/middleware/etag.rb,
lib/itsi/server/config/middleware/grpc.rb,
lib/itsi/server/config/options/include.rb,
lib/itsi/server/config/options/nodelay.rb,
lib/itsi/server/config/options/preload.rb,
lib/itsi/server/config/options/threads.rb,
lib/itsi/server/config/options/workers.rb,
lib/itsi/server/config/middleware/proxy.rb,
lib/itsi/server/config/options/daemonize.rb,
lib/itsi/server/config/options/log_level.rb,
lib/itsi/server/config/options/log_format.rb,
lib/itsi/server/config/options/log_target.rb,
lib/itsi/server/config/options/reuse_port.rb,
lib/itsi/server/config/middleware/auth_jwt.rb,
lib/itsi/server/config/middleware/location.rb,
lib/itsi/server/config/middleware/max_body.rb,
lib/itsi/server/config/middleware/redirect.rb,
lib/itsi/server/config/options/stream_body.rb,
lib/itsi/server/config/middleware/deny_list.rb,
lib/itsi/server/config/middleware/allow_list.rb,
lib/itsi/server/config/middleware/auth_basic.rb,
lib/itsi/server/config/middleware/rate_limit.rb,
lib/itsi/server/config/options/reuse_address.rb,
lib/itsi/server/config/middleware/compression.rb,
lib/itsi/server/config/middleware/rackup_file.rb,
lib/itsi/server/config/options/listen_backlog.rb,
lib/itsi/server/config/middleware/auth_api_key.rb,
lib/itsi/server/config/middleware/endpoint/get.rb,
lib/itsi/server/config/middleware/endpoint/put.rb,
lib/itsi/server/config/middleware/log_requests.rb,
lib/itsi/server/config/middleware/token_source.rb,
lib/itsi/server/config/options/fiber_scheduler.rb,
lib/itsi/server/config/options/request_timeout.rb,
lib/itsi/server/config/middleware/cache_control.rb,
lib/itsi/server/config/middleware/endpoint/post.rb,
lib/itsi/server/config/middleware/static_assets.rb,
lib/itsi/server/config/options/hooks/after_fork.rb,
lib/itsi/server/config/options/pin_worker_cores.rb,
lib/itsi/server/config/options/recv_buffer_size.rb,
lib/itsi/server/config/options/send_buffer_size.rb,
lib/itsi/server/config/options/shutdown_timeout.rb,
lib/itsi/server/config/middleware/endpoint/patch.rb,
lib/itsi/server/config/middleware/error_response.rb,
lib/itsi/server/config/options/hooks/after_start.rb,
lib/itsi/server/config/options/hooks/before_fork.rb,
lib/itsi/server/config/options/scheduler_threads.rb,
lib/itsi/server/config/middleware/endpoint/delete.rb,
lib/itsi/server/config/middleware/request_headers.rb,
lib/itsi/server/config/middleware/static_response.rb,
lib/itsi/server/config/options/auto_reload_config.rb,
lib/itsi/server/config/options/log_target_filters.rb,
lib/itsi/server/config/middleware/rate_limit_store.rb,
lib/itsi/server/config/middleware/response_headers.rb,
lib/itsi/server/config/options/header_read_timeout.rb,
lib/itsi/server/config/options/worker_memory_limit.rb,
lib/itsi/server/config/middleware/endpoint/endpoint.rb,
lib/itsi/server/config/options/hooks/before_restart.rb,
lib/itsi/server/config/options/hooks/before_shutdown.rb,
lib/itsi/server/config/options/multithreaded_reactor.rb,
lib/itsi/server/config/middleware/endpoint/controller.rb,
lib/itsi/server/config/options/redirect_http_to_https.rb,
lib/itsi/server/config/middleware/intrusion_protection.rb,
lib/itsi/server/config/options/oob_gc_responses_threshold.rb,
lib/itsi/server/config/options/hooks/after_memory_limit_reached.rb,
lib/itsi/server/config/options/ruby_thread_request_backlog_size.rb

Defined Under Namespace

Modules: ConfigHelpers, TypedStruct Classes: AfterFork, AfterMemoryLimitReached, AfterStart, AllowList, AuthApiKey, AuthBasic, AuthJwt, AutoReloadConfig, BeforeFork, BeforeRestart, BeforeShutdown, Bind, CacheControl, Compress, Controller, Cors, Csp, DSL, Daemonize, Delete, DenyList, Endpoint, Etag, FiberScheduler, Get, Grpc, HeaderReadTimeout, Include, IntrusionProtection, ListenBacklog, Location, LogFormat, LogLevel, LogRequests, LogTarget, LogTargetFilters, MaxBody, Middleware, MultithreadedReactor, Nodelay, OobGcResponsesThreshold, Option, Patch, PinWorkerCores, Post, Preload, Proxy, Put, RackupFile, RateLimit, RecvBufferSize, Redirect, RedirectHttpToHttps, RequestHeaders, RequestTimeout, ResponseHeaders, ReuseAddress, ReusePort, RubyThreadRequestBacklogSize, Run, SchedulerThreads, SendBufferSize, ShutdownTimeout, StaticAssets, StaticResponse, StreamBody, Threads, Watch, WorkerMemoryLimit, Workers

Constant Summary collapse

ITSI_DEFAULT_CONFIG_FILE =
"Itsi.rb"
HeaderSource =
TypedStruct.new do
  {
    name: Type(String) & Required(),
    prefix: Type(String)
  }
end
HeaderSourceOuter =
TypedStruct.new do
  {
    header: Type(HeaderSource)
  }
end
QuerySource =
TypedStruct.new do
  {
    query: Type(String) & Required()
  }
end
TokenSource =
TypedStruct.new do
  Or(
    Type(HeaderSourceOuter),
    Type(QuerySource)
  )
end
InlineContentSource =
TypedStruct.new do
  {
    inline: Type(String)
  }
end
FileContentSource =
TypedStruct.new do
  {
    file: Type(String)
  }
end
ContentSource =
TypedStruct.new do
  Or(Type(InlineContentSource), Type(FileContentSource))
end
ErrorResponse =
TypedStruct.new do
  {
    code: Type(Integer) & Required(),
    plaintext: Type(ContentSource),
    html: Type(ContentSource),
    json: Type(ContentSource),
    default: Enum(["plaintext", "html", "json"]) & Required()
  }
end
ErrorResponseDef =
TypedStruct.new do
  Or(Enum(%w[internal_server_error not_found unauthorized forbidden payload_too_large
  too_many_requests bad_gateway service_unavailable gateway_timeout]), Type(ErrorResponse)) & Required()
end
RateLimitKey =
TypedStruct.new do
  {
    parameter: Or(
      Hash(Enum(["header"]), Hash(Enum(["name"]), Type(String))) & Required(),
      Hash(Enum(["query"]), Type(String)) & Required()
    )
  }
end
RateLimitStore =
TypedStruct.new do
  {
    redis: Type(TypedStruct.new do
      {
        connection_url: Type(String)& Required()
      }
    end) & Required()
  }
end

Class Method Summary collapse

Methods included from CidrToRegex

#build_regex_from_parts, #cidr_to_regex, #part_to_range_regex, #range_to_regex

Class Method Details

.build_config(args, config_file_path, builder_proc = nil) ⇒ Object

The configuration used when launching the Itsi server are evaluated in the following precedence:

  1. CLI Args.

  2. Itsi.rb file.

  3. Default values.


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/itsi/server/config.rb', line 36

def self.build_config(args, config_file_path, builder_proc = nil)
  args.transform_keys!(&:to_sym)

  itsifile_config, errors = \
    if builder_proc
      DSL.evaluate(&builder_proc)
    elsif args[:static]
      DSL.evaluate do
        rate_limit key: "address", store_config: "in_memory", requests: 5, seconds: 10
        etag type: "strong", algorithm: "md5", min_body_size: 1024 * 1024
        compress min_size: 1024 * 1024, level: "fastest", algorithms: %w[zstd gzip br deflate],
                  mime_types: %w[all], compress_streams: true
        log_requests before: { level: "DEBUG", format: "[{request_id}] {method} {path_and_query} - {addr} " },
                      after: { level: "DEBUG",
                              format: "[{request_id}] └─ {status} in {response_time}" }
        nodelay false
        static_assets \
          relative_path: true,
          allowed_extensions: [],
          root_dir: ".",
          not_found_behavior: { error: "not_found" },
          auto_index: true,
          try_html_extension: true,
          max_file_size_in_memory: 1024 * 1024, # 1MB
          max_files_in_memory: 1000,
          file_check_interval: 1,
          serve_hidden_files: false,
          headers: {
            "X-Content-Type-Options" => "nosniff"
          }
      end
    elsif File.exist?(config_file_path.to_s)
      DSL.evaluate(config_file_path)
    elsif File.exist?("./config.ru")
      DSL.evaluate do
        preload true
        rackup_file args.fetch(:rackup_file, "./config.ru")
      end
    else
      DSL.evaluate {}
    end

  itsifile_config.transform_keys!(&:to_sym)

  # We'll preload while we load config, if enabled.
  middleware_loader = itsifile_config.fetch(:middleware_loader, -> {})
  preload = args.fetch(:preload) { itsifile_config.fetch(:preload, false) }

  case preload
  # If we preload everything, then we'll load middleware and default rack app ahead of time
  when true
    begin
      Itsi.log_debug("Preloading middleware and default rack app")
      preloaded_middleware = middleware_loader.call
      middleware_loader = -> { preloaded_middleware }
    rescue Exception => e # rubocop:disable Lint/RescueException
      errors << [e, e.backtrace[0]]
    end
  # If we're just preloading a specific gem group, we'll do that here too
  when Symbol
    Itsi.log_debug("Preloading gem group #{preload}")
    Bundler.require(preload)
  end

  if itsifile_config[:daemonize] && !@daemonized
    @daemonized = true
    Itsi.log_info("Itsi is running in the background. Writing pid to #{Itsi::Server::Config.pid_file_path}")
    Itsi.log_info("To stop Itsi, run 'itsi stop' from this directory.")
    Process.daemon(true, false)
    Server.write_pid
  end

  srv_config = {
    workers: args.fetch(:workers) { itsifile_config.fetch(:workers, 1) },
    worker_memory_limit: args.fetch(:worker_memory_limit) { itsifile_config.fetch(:worker_memory_limit, nil) },
    silence: args.fetch(:silence) { itsifile_config.fetch(:silence, false) },
    shutdown_timeout: args.fetch(:shutdown_timeout) { itsifile_config.fetch(:shutdown_timeout, 5) },
    hooks: if args[:hooks] && itsifile_config[:hooks]
             args[:hooks].merge(itsifile_config[:hooks])
           else
             itsifile_config.fetch(
               :hooks, args[:hooks]
             )
           end,
    preload: !!preload,
    request_timeout: itsifile_config.fetch(:request_timeout, nil),
    header_read_timeout: args.fetch(:header_read_timeout) { itsifile_config.fetch(:header_read_timeout, nil) },
    notify_watchers: itsifile_config.fetch(:notify_watchers, nil),
    threads: args.fetch(:threads) { itsifile_config.fetch(:threads, 1) },
    scheduler_threads: args.fetch(:scheduler_threads) { itsifile_config.fetch(:scheduler_threads, nil) },
    streamable_body: args.fetch(:streamable_body) { itsifile_config.fetch(:streamable_body, false) },
    multithreaded_reactor: args.fetch(:multithreaded_reactor) do
      itsifile_config.fetch(:multithreaded_reactor, nil)
    end,
    pin_worker_cores: args.fetch(:pin_worker_cores) { itsifile_config.fetch(:pin_worker_cores, true) },
    scheduler_class: args.fetch(:scheduler_class) { itsifile_config.fetch(:scheduler_class, nil) },
    oob_gc_responses_threshold: args.fetch(:oob_gc_responses_threshold) do
      itsifile_config.fetch(:oob_gc_responses_threshold, nil)
    end,
    ruby_thread_request_backlog_size: args.fetch(:ruby_thread_request_backlog_size) do
      itsifile_config.fetch(:ruby_thread_request_backlog_size, nil)
    end,
    log_level: args.fetch(:log_level) { itsifile_config.fetch(:log_level, nil) },
    log_format: args.fetch(:log_format) { itsifile_config.fetch(:log_format, nil) },
    log_target: args.fetch(:log_target) { itsifile_config.fetch(:log_target, nil) },
    log_target_filters: args.fetch(:log_target_filters) { itsifile_config.fetch(:log_target_filters, nil) },
    binds: args.fetch(:binds) { itsifile_config.fetch(:binds, ["http://0.0.0.0:3000"]) },
    middleware_loader: middleware_loader,
    listeners: args.fetch(:listeners) { nil },
    reuse_address: itsifile_config.fetch(:reuse_address, true),
    reuse_port: itsifile_config.fetch(:reuse_port, true),
    listen_backlog: itsifile_config.fetch(:listen_backlog, 1024),
    nodelay: itsifile_config.fetch(:nodelay, true),
    recv_buffer_size: itsifile_config.fetch(:recv_buffer_size, 262_144),
    send_buffer_size: itsifile_config.fetch(:send_buffer_size, 262_144)
  }.transform_keys(&:to_s)

  [srv_config, errors_to_error_lines(errors)]
rescue StandardError => e
  [{}, errors_to_error_lines([[e, e.backtrace[0]]])]
end

.config_file_path(config_file_path = nil) ⇒ Object

Find config file path, if it exists.


224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/itsi/server/config.rb', line 224

def self.config_file_path(config_file_path = nil)
  config_file_path ||= \
    if File.exist?(ITSI_DEFAULT_CONFIG_FILE)
      ITSI_DEFAULT_CONFIG_FILE
    elsif File.exist?("config/#{ITSI_DEFAULT_CONFIG_FILE}")
      "config/#{ITSI_DEFAULT_CONFIG_FILE}"
    end
  # Options pass through unless we've specified a config file
  return unless File.exist?(config_file_path.to_s)

  config_file_path
end

.errors_to_error_lines(errors) ⇒ Object


176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/itsi/server/config.rb', line 176

def self.errors_to_error_lines(errors)
  return unless errors

  errors.flat_map do |(error, message)|
    location = message[/(.*?):in/, 1]
    file, lineno = location.split(":")
    lineno = lineno.to_i
    err_message = error.is_a?(NoMethodError) && error.respond_to?(:detailed_message) ? error.detailed_message : error.message
    file_lines = IO.readlines(file)
    info_lines = \
      if error.is_a?(SyntaxError)
        []
      else

        ([lineno - 2, 0].max...[file_lines.length, lineno.succ.succ].min).map do |currline|
          if currline == lineno - 1
            line = file_lines[currline][0...-1]
            padding = line[/^\s+/]&.length || 0

            [
              " \e[31m#{currline.succ.to_s.rjust(3)} | #{line}\e[0m",
              "     | #{" " * padding}\e[33m^^^\e[0m "
            ]
          else
            " #{currline.succ.to_s.rjust(3)} | #{file_lines[currline][0...-1]}"
          end
        end.flatten
      end
    [
      err_message,
      "   --> #{File.expand_path(file)}:#{lineno}",
      *info_lines
    ]
  end
end

.pid_file_pathObject


237
238
239
240
241
242
243
# File 'lib/itsi/server/config.rb', line 237

def self.pid_file_path
  if Dir.exist?("tmp")
    File.join("tmp", "itsi.pid")
  else
    ".itsi.pid"
  end
end

.prep_reexec!Object


13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/itsi/server/config.rb', line 13

def self.prep_reexec!
  @argv ||= ARGV[0...ARGV.index("--listeners")]

  auto_suppress_fork_darwin_fork_safety_warnings = [
    ENV["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"].nil? || ENV["PGGSSENCMODE"].nil?,
    RUBY_PLATFORM =~ /darwin/,
    !ENV.key?("ITSI_DISABLE_AUTO_DISABLE_DARWIN_FORK_SAFETY_WARNINGS"),
    $PROGRAM_NAME =~ /itsi$/
  ].all?
  return unless auto_suppress_fork_darwin_fork_safety_warnings

  env = ENV.to_h.merge("OBJC_DISABLE_INITIALIZE_FORK_SAFETY" => "YES", "PGGSSENCMODE" => "disable")
  if ENV["BUNDLE_BIN_PATH"]
    exec env, "bundle", "exec", $PROGRAM_NAME, *@argv
  else
    exec env, $PROGRAM_NAME, *@argv
  end
end

.reload_exec(listener_info) ⇒ Object

Reloads the entire process using exec, passing in any active file descriptors and previous invocation arguments


215
216
217
218
219
220
221
# File 'lib/itsi/server/config.rb', line 215

def self.reload_exec(listener_info)
  if ENV["BUNDLE_BIN_PATH"]
    exec "bundle", "exec", $PROGRAM_NAME, *@argv, "--listeners", listener_info
  else
    exec $PROGRAM_NAME, *@argv, "--listeners", listener_info
  end
end

.test!(cli_params) ⇒ Object


158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/itsi/server/config.rb', line 158

def self.test!(cli_params)
  config, errors = build_config(cli_params, Itsi::Server::Config.config_file_path(cli_params[:config_file]))
  unless errors.any?
    begin
      config["middleware_loader"][]
    rescue Exception => e # rubocop:disable Lint/RescueException
      errors = [e]
    end
  end

  if errors.any?
    Itsi.log_error("Config file is invalid")
    puts errors
  else
    Itsi.log_info("Config file is valid")
  end
end

.write_defaultObject

Write a default config file, if one doesn’t exist.


246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/itsi/server/config.rb', line 246

def self.write_default
  if File.exist?(ITSI_DEFAULT_CONFIG_FILE)
    puts "#{ITSI_DEFAULT_CONFIG_FILE} already exists."
    return
  end

  default_config = IO.read("#{__dir__}/default_config/Itsi.rb")

  default_config << \
    if File.exist?("./config.ru")
      <<~RUBY
        # You can mount several Ruby apps as either
        # 1. rackup files
        # 2. inline rack apps
        # 3. inline Ruby endpoints
        #
        # 1. rackup_file
        rackup_file "./config.ru"
        #
        # 2. inline rack app
        # require 'rack'
        # run(Rack::Builder.app do
        #   use Rack::CommonLogger
        #   run ->(env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
        # end)
        #
        # 3. Endpoints
        # endpoint "/" do |req|
        #   req.ok "Hello from Itsi"
        # end
      RUBY
    else
      <<~RUBY
        # You can mount several Ruby apps as either
        # 1. rackup files
        # 2. inline rack apps
        # 3. inline Ruby endpoints
        #
        # 1. rackup_file
        # Use `rackup_file` to specify the Rack app file name.
        #
        # 2. inline rack app
        # require 'rack'
        # run(Rack::Builder.app do
        #   use Rack::CommonLogger
        #   run ->(env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
        # end)
        #
        # 3. Endpoint
        endpoint "/" do |req|
          req.ok "Hello from Itsi"
        end
      RUBY
    end

  File.open(ITSI_DEFAULT_CONFIG_FILE, "w") do |file|
    file.write(default_config)
  end
end