Class: Unicorn
- Inherits:
-
Object
- Object
- Unicorn
- Defined in:
- lib/unicorn-lockdown.rb
Class Attribute Summary collapse
-
.app_name ⇒ Object
The name of the application.
-
.dev_unveil ⇒ Object
The hash of additional unveil paths to use if in the development environment.
-
.email ⇒ Object
The address to email for crash and unhandled exception notifications.
-
.master_execpledge ⇒ Object
The pledge string to use for the master process’s spawned processes by default.
-
.master_pledge ⇒ Object
The pledge string to use for the master process.
-
.pledge ⇒ Object
The pledge string to use for worker processes.
-
.request_logger ⇒ Object
A File instance open for writing.
-
.server ⇒ Object
The Unicorn::HttpServer instance in use.
-
.unicorn_lockdown_prefix ⇒ Object
readonly
The prefix for unicorn lockdown files.
-
.unveil ⇒ Object
The hash of unveil paths to use.
Class Method Summary collapse
-
.lockdown(configurator, opts) ⇒ Object
Helper method that sets up all necessary code for unveil/pledge support.
-
.write_request(email_message) ⇒ Object
Helper method to write request information to the request logger.
Class Attribute Details
.app_name ⇒ Object
The name of the application. All applications are given unique names. This name is used to construct the log file, listening socket, and process name.
59 60 61 |
# File 'lib/unicorn-lockdown.rb', line 59 def app_name @app_name end |
.dev_unveil ⇒ Object
The hash of additional unveil paths to use if in the development environment.
81 82 83 |
# File 'lib/unicorn-lockdown.rb', line 81 def dev_unveil @dev_unveil end |
.email ⇒ Object
The address to email for crash and unhandled exception notifications.
84 85 86 |
# File 'lib/unicorn-lockdown.rb', line 84 def email @email end |
.master_execpledge ⇒ Object
The pledge string to use for the master process’s spawned processes by default.
69 70 71 |
# File 'lib/unicorn-lockdown.rb', line 69 def master_execpledge @master_execpledge end |
.master_pledge ⇒ Object
The pledge string to use for the master process.
72 73 74 |
# File 'lib/unicorn-lockdown.rb', line 72 def master_pledge @master_pledge end |
.pledge ⇒ Object
The pledge string to use for worker processes.
75 76 77 |
# File 'lib/unicorn-lockdown.rb', line 75 def pledge @pledge end |
.request_logger ⇒ Object
A File instance open for writing. This is unique per worker process. Workers should write all new requests to this file before handling the request. If a worker process crashes, the master process will send an notification email with the previously logged request information, to enable programmers to debug and fix the issue.
66 67 68 |
# File 'lib/unicorn-lockdown.rb', line 66 def request_logger @request_logger end |
.server ⇒ Object
The Unicorn::HttpServer instance in use. This is only set once when the unicorn server is started, before forking the first worker.
54 55 56 |
# File 'lib/unicorn-lockdown.rb', line 54 def server @server end |
.unicorn_lockdown_prefix ⇒ Object (readonly)
The prefix for unicorn lockdown files
87 88 89 |
# File 'lib/unicorn-lockdown.rb', line 87 def unicorn_lockdown_prefix @unicorn_lockdown_prefix end |
.unveil ⇒ Object
The hash of unveil paths to use.
78 79 80 |
# File 'lib/unicorn-lockdown.rb', line 78 def unveil @unveil end |
Class Method Details
.lockdown(configurator, opts) ⇒ Object
Helper method that sets up all necessary code for unveil/pledge support. This should be called inside the appropriate unicorn.conf file. The configurator should be self in the top level scope of the unicorn.conf file, and this takes options:
Options:
- :app (required)
-
The name of the application
:email : The email to notify for worker crashes
- :pledge
-
The string to use when pledging worker processes after loading the app
- :master_pledge
-
The string to use when pledging the master process before spawning worker processes
- :master_execpledge
-
The pledge string for processes spawned by the master process (i.e. worker processes before loading the app)
- :unveil
-
A hash of unveil paths, passed to Pledge.unveil.
- :dev_unveil
-
A hash of unveil paths to use in development, in addition to the ones in :unveil.
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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 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 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 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 |
# File 'lib/unicorn-lockdown.rb', line 117 def lockdown(configurator, opts) Unicorn.app_name = opts.fetch(:app) Unicorn.email = opts[:email] Unicorn.master_pledge = opts[:master_pledge] Unicorn.master_execpledge = opts[:master_execpledge] Unicorn.pledge = opts[:pledge] Unicorn.unveil = opts[:unveil] Unicorn.dev_unveil = opts[:dev_unveil] configurator.instance_exec do listen "#{Unicorn.unicorn_lockdown_prefix}/www/sockets/#{Unicorn.app_name}.sock" # Buffer all client bodies in memory. This assumes an Nginx limit of 10MB, # by using 11MB this ensures that client bodies are always buffered in # memory, preventing file uploading causing a program crash if the # pledge does not allow wpath and cpath. client_body_buffer_size(11*1024*1024) # Run all worker processes with unique memory layouts worker_exec true # :nocov: # Only change the log path if daemonizing. # Otherwise, continue to log to stdout/stderr. if Unicorn::Configurator::RACKUP[:daemonize] log_path = "#{Unicorn.unicorn_lockdown_prefix}/log/unicorn/#{Unicorn.app_name}.log" stdout_path log_path stderr_path log_path end # :nocov: after_fork do |server, worker| server.logger.info("worker=#{worker.nr} spawned pid=#{$$}") # Set the request logger for the worker process after forking. Unicorn.request_logger = File.open(server.request_filename($$), "wb") Unicorn.request_logger.sync = true end if wrap_app = Unicorn.email require 'rack/email_exceptions' end after_worker_ready do |server, worker| server.logger.info("worker=#{worker.nr} ready") # If an notification email address is setup, wrap the entire app in # a middleware that will notify about any exceptions raised when # processing that aren't caught by other middleware. if wrap_app server.instance_exec do @app = Rack::EmailExceptions.new(@app, Unicorn.app_name, Unicorn.email) end end unveil = if Unicorn.dev_unveil && ENV['RACK_ENV'] == 'development' Unicorn.unveil.merge(Unicorn.dev_unveil) else Hash[Unicorn.unveil] end # Don't allow loading files in rack and mail gems if not using rubygems if defined?(Gem) && Gem.respond_to?(:loaded_specs) # Allow read access to the rack gem directory, as rack autoloads constants. if defined?(Rack) && Gem.loaded_specs['rack'] unveil['rack'] = :gem end # If using the mail library, allow read access to the mail gem directory, # as mail autoloads constants. if defined?(Mail) && Gem.loaded_specs['mail'] unveil['mail'] = :gem end end # Restrict access to the file system based on the specified unveil. Pledge.unveil(unveil) # Pledge after unveiling, because unveiling requires a separate pledge. Pledge.pledge(Unicorn.pledge) end # the last time there was a worker crash and the request information # file was empty. Set by default to 10 minutes ago, so the first # crash will always receive an email. last_empty_crash = Time.now - 600 after_worker_exit do |server, worker, status| m = "reaped #{status.inspect} worker=#{worker.nr rescue 'unknown'}" if status.success? server.logger.info(m) else server.logger.error(m) end # Email about worker process crashes. This is necessary so that # programmers are notified about any pledge violations. Pledge # violations immediately abort the process, and are bugs in the # application that should be fixed. This can also catch other # crashes such as SIGSEGV or SIGBUS. file = server.request_filename(status.pid) if File.exist?(file) if !status.success? && Unicorn.email if File.size(file).zero? # If a crash happens and the request information file is empty, # it is generally because the crash happened during initialization, # in which case it will generally continue to crash in a loop until the # problem is fixed. In that case, only send an email if there hasn't # been a similar crash in the last 5 minutes. This rate-limits the # crash notification emails to 1 every 5 minutes instead of potentially # multiple times per second. if Time.now - last_empty_crash > 300 last_empty_crash = Time.now else skip_email = true end end unless skip_email # If the request filename exists and the worker process crashed, # send a notification email. Process.waitpid(Process.fork do # Load net/smtp early require 'net/smtp' # When setting the email, first get the contents of the email # from the request file. body = File.read(file) # Then use a restrictive pledge Pledge.pledge(ENV['UNICORN_LOCKDOWN_WORKER_CRASH_PLEDGE'] || 'inet prot_exec') # If body empty, crash happened before a request was received, # try to at least provide the application name in this case. if body.empty? body = "Subject: [#{Unicorn.app_name}] Unicorn Worker Process Crash\r\n\r\nNo email content provided for app: #{Unicorn.app_name}" end # :nocov: # Don't verify localhost hostname, to avoid SSL errors raised in newer versions of net/smtp smtp_params = Net::SMTP.method(:start).parameters.include?([:key, :tls_verify]) ? {tls_verify: false, tls_hostname: 'localhost'} : {} # :nocov: # Finally send an email to localhost via SMTP. Net::SMTP.start('127.0.0.1', **smtp_params){|s| s.(body, Unicorn.email, Unicorn.email)} end) end end # Remove any request logger file if it exists. File.delete(file) end end end end |
.write_request(email_message) ⇒ Object
Helper method to write request information to the request logger. email_message
should be an email message including headers and body. This should be called at the top of the Roda route block for the application (or at some early point before processing in other web frameworks).
94 95 96 97 98 99 |
# File 'lib/unicorn-lockdown.rb', line 94 def write_request() request_logger.seek(0, IO::SEEK_SET) request_logger.truncate(0) request_logger.syswrite() request_logger.fsync end |