Class: Unicorn

Inherits:
Object
  • Object
show all
Defined in:
lib/unicorn-lockdown.rb

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.app_nameObject

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_unveilObject

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

.emailObject

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_execpledgeObject

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_pledgeObject

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

.pledgeObject

The pledge string to use for worker processes.



75
76
77
# File 'lib/unicorn-lockdown.rb', line 75

def pledge
  @pledge
end

.request_loggerObject

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

.serverObject

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_prefixObject (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

.unveilObject

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.send_message(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(email_message)
  request_logger.seek(0, IO::SEEK_SET)
  request_logger.truncate(0)
  request_logger.syswrite(email_message)
  request_logger.fsync
end