chook 1.0.1.b2 → 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +56 -0
  3. data/README.md +363 -127
  4. data/bin/chook-server +31 -1
  5. data/data/chook.conf.example +183 -0
  6. data/data/com.pixar.chook-server.plist +20 -0
  7. data/data/sample_handlers/RestAPIOperation.rb +11 -11
  8. data/data/sample_handlers/SmartGroupComputerMembershipChange.rb +3 -6
  9. data/data/sample_jsons/SmartGroupComputerMembershipChange.json +3 -1
  10. data/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +3 -1
  11. data/lib/chook/configuration.rb +27 -8
  12. data/lib/chook/event.rb +6 -1
  13. data/lib/chook/event/handled_event.rb +36 -9
  14. data/lib/chook/event/handled_event/handlers.rb +260 -98
  15. data/lib/chook/event/handled_event_logger.rb +86 -0
  16. data/lib/chook/event_handling.rb +1 -0
  17. data/lib/chook/foundation.rb +3 -0
  18. data/lib/chook/procs.rb +17 -1
  19. data/lib/chook/server.rb +73 -72
  20. data/lib/chook/server/auth.rb +164 -0
  21. data/lib/chook/server/log.rb +215 -0
  22. data/lib/chook/server/public/css/chook.css +133 -0
  23. data/lib/chook/server/public/imgs/ChookLogoAlMcWhiggin.png +0 -0
  24. data/lib/chook/server/public/js/chook.js +126 -0
  25. data/lib/chook/server/public/js/logstream.js +101 -0
  26. data/lib/chook/server/routes.rb +28 -0
  27. data/lib/chook/server/routes/handle_by_name.rb +65 -0
  28. data/lib/chook/server/routes/handle_webhook_event.rb +27 -3
  29. data/lib/chook/server/routes/handlers.rb +52 -0
  30. data/lib/chook/server/routes/home.rb +48 -1
  31. data/lib/chook/server/routes/log.rb +105 -0
  32. data/lib/chook/server/routes/login_logout.rb +48 -0
  33. data/lib/chook/server/views/admin.haml +11 -0
  34. data/lib/chook/server/views/bak.haml +48 -0
  35. data/lib/chook/server/views/config.haml +15 -0
  36. data/lib/chook/server/views/handlers.haml +63 -0
  37. data/lib/chook/server/views/layout.haml +64 -0
  38. data/lib/chook/server/views/logstream.haml +33 -0
  39. data/lib/chook/server/views/sketch_admin +44 -0
  40. data/lib/chook/subject.rb +13 -2
  41. data/lib/chook/subject/dep_device.rb +81 -0
  42. data/lib/chook/subject/policy_finished.rb +43 -0
  43. data/lib/chook/subject/smart_group.rb +6 -0
  44. data/lib/chook/version.rb +1 -1
  45. metadata +79 -19
@@ -37,4 +37,5 @@ Chook::HandledSubject.generate_classes
37
37
  # events
38
38
  require 'chook/event'
39
39
  require 'chook/event/handled_event'
40
+ require 'chook/event/handled_event_logger'
40
41
  Chook::HandledEvent.generate_classes
@@ -27,6 +27,9 @@
27
27
  require 'json'
28
28
  require 'open-uri'
29
29
  require 'pathname'
30
+ require 'logger'
31
+ require 'English'
32
+ require 'securerandom'
30
33
 
31
34
  require 'chook/version'
32
35
  require 'chook/procs' # must load before configuration
@@ -30,12 +30,28 @@ module Chook
30
30
  module Procs
31
31
 
32
32
  TRUE_RE = /^\s*(true|yes)\s*$/i
33
+
33
34
  JSS_EPOCH_TO_TIME = proc { |val| Time.strptime val.to_s[0..-4], '%s' }
35
+
34
36
  STRING_TO_BOOLEAN = proc { |val| val =~ TRUE_RE ? true : false }
37
+
35
38
  STRING_TO_PATHNAME = proc { |val| Pathname.new val }
39
+
40
+ STRING_TO_LOG_LEVEL = proc do |level|
41
+ if (0..5).cover? level
42
+ level
43
+ else
44
+ lvl = Chook::Server::Log::LOG_LEVELS[level.to_sym]
45
+ lvl ? lvl : Logger::UNKNOWN
46
+ end # if..else
47
+ end
48
+
36
49
  MOBILE_USERID = proc { |_device| '-1' }
50
+
37
51
  PRODUCT = proc { |_device| nil }
38
- ALWAYS_TRUE = proc { |_boolean| True }
52
+
53
+ ALWAYS_TRUE = proc { |_boolean| true }
54
+
39
55
  COMPUTER_USERID = proc do |comp|
40
56
  id = '-1' unless comp.groups_accounts[:local_accounts].find { |acct| acct[:name] == comp.username }
41
57
  id.is_a?(Hash) ? id[:uid] : '-1'
@@ -22,9 +22,15 @@
22
22
  ### language governing permissions and limitations under the Apache License.
23
23
  ###
24
24
  ###
25
- require 'chook/event_handling'
25
+
26
26
  require 'sinatra/base'
27
+ require 'sinatra/custom_logger'
28
+ require 'haml'
27
29
  require 'openssl'
30
+ require 'chook/event_handling'
31
+ require 'chook/server/log'
32
+ require 'chook/server/auth'
33
+ require 'chook/server/routes'
28
34
 
29
35
  module Chook
30
36
 
@@ -32,90 +38,85 @@ module Chook
32
38
  # the engine of your choice.
33
39
  class Server < Sinatra::Base
34
40
 
35
- DEFAULT_SERVER_ENGINE = :webrick
36
- DEFAULT_PORT = 8000
41
+ DEFAULT_PORT = 80
42
+ DEFAULT_SSL_PORT = 443
43
+ DEFAULT_CONCURRENCY = true
44
+ DEFAULT_SESSION_EXPIRE = 24 * 60 * 60 # one day
37
45
 
38
- @server_engine = Chook::CONFIG.server_engine || DEFAULT_SERVER_ENGINE
39
- require @server_engine.to_s
40
- @server_port = Chook::CONFIG.server_port || DEFAULT_PORT
46
+ # set defaults in config
47
+ Chook.config.port ||= Chook.config.use_ssl ? DEFAULT_SSL_PORT : DEFAULT_PORT
48
+ Chook.config.admin_session_expires ||= DEFAULT_SESSION_EXPIRE
41
49
 
42
- def self.run!
43
- # trap HUPs to reload handlers
44
- Signal.trap('HUP') do
45
- Chook::HandledEvent::Handlers.load_handlers reload: true
46
- end
47
- Chook::HandledEvent::Handlers.load_handlers
48
- chook_configure
49
- case @server_engine.to_sym
50
- when :webrick
50
+ # can't use ||= here cuz nil and false have different meanings
51
+ Chook.config.concurrency = DEFAULT_CONCURRENCY if Chook.config.concurrency.nil?
52
+
53
+ # Run the server
54
+ ###################################
55
+ def self.run!(log_level: nil)
56
+ prep_to_run
57
+
58
+ if Chook.config.use_ssl
59
+ super do |server|
60
+ server.ssl = true
61
+ server.ssl_options = {
62
+ cert_chain_file: Chook.config.ssl_cert_path.to_s,
63
+ private_key_file: Chook.config.ssl_private_key_path.to_s,
64
+ verify_peer: false
65
+ }
66
+ end # super do
67
+
68
+ else # no ssl
51
69
  super
52
- when :thin
53
- if Chook::CONFIG.use_ssl
54
- super do |server|
55
- server.ssl = true
56
- server.ssl_options = {
57
- cert_chain_file: Chook::CONFIG.ssl_cert_path.to_s,
58
- private_key_file: Chook::CONFIG.ssl_private_key_path.to_s,
59
- verify_peer: false
60
- }
61
- end # super do
62
- else
63
- super
64
- end # if use ssl
65
- end # case
70
+ end # if use ssl
66
71
  end # self.run
67
72
 
68
- # Sinatra Settings
69
- def self.chook_configure
73
+ def self.prep_to_run
74
+ @start_time = Time.now
75
+ log_level ||= Chook.config.log_level
76
+ @log_level = Chook::Procs::STRING_TO_LOG_LEVEL.call log_level
77
+
70
78
  configure do
71
- set :environment, :production
72
- enable :logging, :lock
79
+ set :logger, Log.startup(@log_level)
80
+ set :server, :thin
73
81
  set :bind, '0.0.0.0'
74
- set :server, @server_engine
75
- set :port, @server_port
76
-
77
- if Chook::CONFIG.use_ssl
78
- case @server_engine.to_sym
79
- when :webrick
80
- require 'webrick/https'
81
- key = Chook::CONFIG.ssl_private_key_path.read
82
- cert = Chook::CONFIG.ssl_cert_path.read
83
- cert_name = Chook::CONFIG.ssl_cert_name
84
- set :SSLEnable, true
85
- set :SSLVerifyClient, OpenSSL::SSL::VERIFY_NONE
86
- set :SSLPrivateKey, OpenSSL::PKey::RSA.new(key, ssl_key_password)
87
- set :SSLCertificate, OpenSSL::X509::Certificate.new(cert)
88
- set :SSLCertName, [['CN', cert_name]]
89
- when :thin
90
- true
91
- end # case
92
- end # if ssl
82
+ set :port, Chook.config.port
83
+ set :show_exceptions, :after_handler if development?
84
+ set :root, "#{File.dirname __FILE__}/server"
85
+ enable :static
86
+ enable :sessions
87
+ set :sessions, expire_after: Chook.config.admin_session_expires if Chook.config.admin_user
88
+ if Chook.config.concurrency
89
+ set :threaded, true
90
+ else
91
+ enable :lock
92
+ end
93
93
  end # configure
94
- end # chook_configure
95
94
 
96
- def self.ssl_key_password
97
- path = Chook::CONFIG.ssl_private_key_pw_path
98
- raise 'No config setting for "ssl_private_key_pw_path"' unless path
99
- file = Pathname.new path
95
+ Chook::HandledEvent::Handlers.load_handlers
96
+ end # prep to run
100
97
 
101
- # if the path ends with a pipe, its a command that will
102
- # return the desired password, so remove the pipe,
103
- # execute it, and return stdout from it.
104
- if path.end_with? '|'
105
- raise 'ssl_private_key_pw_path: #{path} is not an executable file.' unless file.executable?
106
- return `#{path.chomp '|'}`.chomp
107
- end
98
+ def self.starttime
99
+ @start_time
100
+ end
108
101
 
109
- raise 'ssl_private_key_pw_path: #{path} is not a readable file.' unless file.readable?
110
- stat = file.stat
111
- raise "Password file for '#{pw}' has insecure permissions, must be 0600." unless ('%o' % stat.mode).end_with? '0600'
102
+ def self.uptime
103
+ @start_time ? "#{humanize_secs(Time.now - @start_time)} ago" : 'Not Running'
104
+ end
112
105
 
113
- # chomping an empty string removes all trailing \n's and \r\n's
114
- file.read.chomp('')
115
- end # ssl_key_password
106
+ # Very handy!
107
+ # lifted from
108
+ # http://stackoverflow.com/questions/4136248/how-to-generate-a-human-readable-time-range-using-ruby-on-rails
109
+ #
110
+ def self.humanize_secs(secs)
111
+ [[60, :second], [60, :minute], [24, :hour], [7, :day], [52.179, :week], [1_000_000, :year]].map do |count, name|
112
+ next unless secs.positive?
113
+
114
+ secs, n = secs.divmod(count)
115
+ n = n.to_i
116
+ "#{n} #{n == 1 ? name : (name.to_s + 's')}"
117
+ end.compact.reverse.join(' ')
118
+ end
116
119
 
117
120
  end # class server
118
121
 
119
122
  end # module
120
-
121
- require 'chook/server/routes'
@@ -0,0 +1,164 @@
1
+ ### Copyright 2017 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ module Chook
27
+
28
+ # the server
29
+ class Server < Sinatra::Base
30
+
31
+ # helper module for authentication
32
+ module Auth
33
+
34
+ USE_JAMF_ADMIN_USER = 'use_jamf'.freeze
35
+
36
+ def protect_via_basic_auth!
37
+ # don't protect if user isn't defined
38
+ return unless Chook.config.webhooks_user
39
+ return if webhook_user_authorized?
40
+ headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
41
+ halt 401, "Not authorized\n"
42
+ end
43
+
44
+ def webhook_user_authorized?
45
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
46
+
47
+ # gotta have basic auth presented to us
48
+ unless @auth.provided? && @auth.basic? && @auth.credentials
49
+ Chook.logger.debug "No basic auth provided on protected route: #{request.path_info} from: #{request.ip}"
50
+ return false
51
+ end
52
+
53
+ authenticate_webhooks_user @auth.credentials
54
+ end # authorized?
55
+
56
+ # webhook user auth always comes from config
57
+ def authenticate_webhooks_user(creds)
58
+ if creds.first == Chook.config.webhooks_user && creds.last == Chook::Server.webhooks_user_pw
59
+ Chook.logger.debug "Got HTTP Basic auth for webhooks user: #{Chook.config.webhooks_user}@#{request.ip}"
60
+ true
61
+ else
62
+ Chook.logger.error "FAILED auth for webhooks user: #{Chook.config.webhooks_user}@#{request.ip}"
63
+ false
64
+ end
65
+ end # authenticate_webhooks_user
66
+
67
+ # admin user auth might come from config, might come from Jamf Pro
68
+ def authenticate_admin(user, pw)
69
+ return authenticate_jamf_admin(user, pw) if Chook.config.admin_user == USE_JAMF_ADMIN_USER
70
+ authenticate_admin_user(user, pw)
71
+ end
72
+
73
+ # admin auth from config
74
+ def authenticate_admin_user(user, pw)
75
+ if user == Chook.config.admin_user && pw == Chook::Server.admin_user_pw
76
+ Chook.logger.debug "Got auth for admin user: #{user}@#{request.ip}"
77
+ session[:authed_admin] = user
78
+ true
79
+ else
80
+ Chook.logger.warn "FAILED auth for admin user: #{user}@#{request.ip}"
81
+ session[:authed_admin] = nil
82
+ false
83
+ end
84
+ end
85
+
86
+ # admin auth from jamf pro
87
+ def authenticate_jamf_admin(user, pw)
88
+ require 'ruby-jss'
89
+ JSS::APIConnection.new(
90
+ user: user,
91
+ pw: pw,
92
+ server: Chook.config.jamf_server,
93
+ port: Chook.config.jamf_port,
94
+ use_ssl: Chook.config.jamf_use_ssl,
95
+ verify_cert: Chook.config.jamf_verify_cert
96
+ )
97
+ Chook.logger.debug "Jamf Admin login for: #{user}@#{request.ip}"
98
+
99
+ session[:authed_admin] = user
100
+ true
101
+ rescue JSS::AuthenticationError
102
+ Chook.logger.warn "Jamf Admin login FAILED for: #{user}@#{request.ip}"
103
+ session[:authed_admin] = nil
104
+ false
105
+ end # authenticate_jamf_admin
106
+
107
+ end # module auth
108
+
109
+ helpers Chook::Server::Auth
110
+
111
+ # Learn the webhook or admin passwords from config.
112
+ # so we can authenticate them from the browser and the JSS
113
+ #
114
+ # This is at the Server level, since we only need read it
115
+ # once per server startup, so we store it in a server
116
+ # instance var.
117
+ ###################################
118
+ def self.webhooks_user_pw
119
+ @webhooks_user_pw ||= pw_from_conf Chook.config.webhooks_user_pw
120
+ end # self.webhooks_user_pw
121
+
122
+ def self.admin_user_pw
123
+ @admin_user_pw ||= pw_from_conf Chook.config.admin_pw
124
+ end
125
+
126
+ def self.pw_from_conf(setting)
127
+ return '' unless setting
128
+
129
+ # if the path ends with a pipe, its a command that will
130
+ # return the desired password, so remove the pipe,
131
+ # execute it, and return stdout from it.
132
+ return pw_from_command(setting) if setting.end_with? '|'
133
+
134
+ # otherwise its a file path, and read the pw from the contents
135
+ pw_from_file(setting)
136
+ end # def pw_from_conf(setting)
137
+
138
+ def self.pw_from_command(cmd)
139
+ cmd = cmd.chomp '|'
140
+ output = `#{cmd} 2>&1`.chomp
141
+ raise "Can't get password from #{setting}: #{output}" unless $CHILD_STATUS.exitstatus.zero?
142
+ output
143
+ end
144
+
145
+ def self.pw_from_file(file)
146
+ file = Pathname.new file
147
+ return nil unless file.file?
148
+ stat = file.stat
149
+ mode = format('%o', stat.mode)
150
+ raise "Password file #{setting} has insecure mode, must be 0600." unless mode.end_with?('0600')
151
+ raise "Password file #{setting} has insecure owner, must be owned by UID #{Process.euid}." unless stat.owned?
152
+ # chomping an empty string removes all trailing \n's and \r\n's
153
+ file.read.chomp('')
154
+ end
155
+
156
+
157
+ end # server
158
+
159
+ end # Chook
160
+
161
+ require 'chook/server/routes/home'
162
+ require 'chook/server/routes/handle_webhook_event'
163
+ require 'chook/server/routes/handlers'
164
+ require 'chook/server/routes/log'
@@ -0,0 +1,215 @@
1
+ ### Copyright 2017 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ # the main module
27
+ module Chook
28
+
29
+ # the server app
30
+ class Server < Sinatra::Base
31
+
32
+ # the CustomLogger helper allows us to
33
+ # use our own Logger instance as the
34
+ # Sinatra `logger` available in routes vis `set :logger...`
35
+ helpers Sinatra::CustomLogger
36
+
37
+ # This module defines our custom Logger instance from the config settings
38
+ # and makes it available in the .logger module method,
39
+ # which is used anywhere outside of a route
40
+ # (inside of a route, the #logger method is locally available)
41
+ #
42
+ # General access to the logger:
43
+ # from anywhere in Chook, as long as a server is running, the Logger
44
+ # instance is available at Chook.logger or Chook::Server::Log.logger
45
+ #
46
+ #
47
+ # Logging from inside a route:
48
+ # inside a Sinatra route, the local `logger` method returnes the
49
+ # Logger instance.
50
+ #
51
+ # Logging from an internal handler:
52
+ # In an internal WebHook handler, starting with `Chook.event_handler do |event|`
53
+ # the logger should be accesses via the event's own wrapper: event.logger
54
+ # Each message will be prepended with the event'd ID
55
+ #
56
+ # Logging from an external handler:
57
+ # from an external handler you can POST a JSON formatted log entry to
58
+ # http(s)://chookserver/log. The body must be a JSON object with 2 keys:
59
+ # 'level' and 'message', with non-empty strings.
60
+ # The level must be one of: fatal, error, warn, info, or debug. Any other
61
+ # value as the level will always be logged regardless of current logging
62
+ # level. NOTE: if your server requires authentication, you must provide it
63
+ # when using this route.
64
+ #
65
+ # Here's an example with curl, split to multi-line for clarity:
66
+ #
67
+ # curl -H "Content-Type: application/json" \
68
+ # -X POST \
69
+ # --data '{"level":"debug", "message":"It Worked"}' \
70
+ # https://user:passwd@chookserver.myorg.org:443/log
71
+ #
72
+ module Log
73
+
74
+ # Using an instance of this as the Logger target sends logfile writes
75
+ # to all registered streams as well as the file
76
+ class LogFileWithStream < File
77
+
78
+ # ServerSent Events data lines always start with this
79
+ LOGSTREAM_DATA_PFX = 'data:'.freeze
80
+
81
+ def write(str)
82
+ super # writes out to the file
83
+ flush
84
+ # send to any active streams
85
+ Chook::Server::Log.log_streams.keys.each do |active_stream|
86
+ # ignore streams closed at the client end,
87
+ # they get removed when a new stream starts
88
+ # see the route: get '/subscribe_to_log_stream'
89
+ next if active_stream.closed?
90
+
91
+ # send new data to the stream
92
+ active_stream << "#{LOGSTREAM_DATA_PFX}#{str}\n\n"
93
+ end
94
+ end
95
+ end # class
96
+
97
+ # mapping of integer levels to symbols
98
+ LOG_LEVELS = {
99
+ fatal: Logger::FATAL,
100
+ error: Logger::ERROR,
101
+ warn: Logger::WARN,
102
+ info: Logger::INFO,
103
+ debug: Logger::DEBUG
104
+ }.freeze
105
+
106
+ # log Streaming
107
+ # ServerSent Events data lines always start with this
108
+ LOGSTREAM_DATA_PFX = 'data:'.freeze
109
+
110
+ # Send this to the clients at least every LOGSTREAM_KEEPALIVE_MAX secs
111
+ # even if there's no data for the stream
112
+ LOGSTREAM_KEEPALIVE_MSG = "#{LOGSTREAM_DATA_PFX} I'm Here!\n\n".freeze
113
+ LOGSTREAM_KEEPALIVE_MAX = 10
114
+
115
+ # the clients will recognize M3_LOG_STREAM_CLOSED and stop trying
116
+ # to connect via ssh.
117
+ LOGSTREAM_CLOSED_PFX = "#{LOGSTREAM_DATA_PFX} M3_LOG_STREAM_CLOSED:".freeze
118
+
119
+ DEFAULT_FILE = Pathname.new '/var/log/chook-server.log'
120
+ DEFAULT_MAX_MEGS = 10
121
+ DEFAULT_TO_KEEP = 10
122
+ DEFAULT_LEVEL = Logger::INFO
123
+
124
+ # set defaults in config
125
+ Chook.config.log_file ||= DEFAULT_FILE
126
+ Chook.config.logs_to_keep ||= DEFAULT_TO_KEEP
127
+ Chook.config.log_max_megs ||= DEFAULT_MAX_MEGS
128
+ Chook.config.log_level ||= DEFAULT_LEVEL
129
+
130
+ # Create the logger,
131
+ # make the first log entry for this run,
132
+ # and return it so it can be used by the server
133
+ # when it does `set :logger, Log.startup(@log_level)`
134
+ def self.startup(level = Chook.config.log_level)
135
+ # create the logger using a LogFileWithStream instance
136
+ @logger =
137
+ if Chook.config.logs_to_keep && Chook.config.logs_to_keep > 0
138
+ Logger.new(
139
+ LogFileWithStream.new(Chook.config.log_file, 'a'),
140
+ Chook.config.logs_to_keep,
141
+ (Chook.config.log_max_megs * 1024 * 1024)
142
+ )
143
+ else
144
+ Logger.new(LogFileWithStream.new(Chook.config.log_file, 'a'))
145
+ end
146
+
147
+ # date and line format
148
+ @logger.datetime_format = '%Y-%m-%d %H:%M:%S'
149
+
150
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
151
+ "#{datetime}: [#{severity}] #{msg}\n"
152
+ end
153
+
154
+ # level
155
+ level &&= Chook::Procs::STRING_TO_LOG_LEVEL.call level
156
+ level ||= Chook.config.log_level
157
+ level ||= DEFAULT_LEVEL
158
+ @logger.level = level
159
+
160
+ # first startup entry
161
+ @logger.unknown "Chook Server v#{Chook::VERSION} starting up. PID: #{$PROCESS_ID}, Port: #{Chook.config.port}, SSL: #{Chook.config.use_ssl}"
162
+
163
+ # if debug, log our config
164
+ if level == Logger::DEBUG
165
+ @logger.debug 'Config: '
166
+ Chook::Configuration::CONF_KEYS.keys.each do |key|
167
+ @logger.debug " Chook.config.#{key} = #{Chook.config.send key}"
168
+ end
169
+ end
170
+
171
+ # return the logger, the server uses it as a helper
172
+ @logger
173
+ end # log
174
+
175
+ # general access to the logger as Chook::Server::Log.logger
176
+ def self.logger
177
+ @logger ||= startup
178
+ end
179
+
180
+ # a Hash of registered log streams
181
+ # streams are keys, valus are their IP addrs
182
+ # see the `get '/subscribe_to_log_stream'` route
183
+ #
184
+ def self.log_streams
185
+ @log_streams ||= {}
186
+ end
187
+
188
+ def self.clean_log_streams
189
+ log_streams.delete_if do |stream, ip|
190
+ if stream.closed?
191
+ logger.debug "Removing closed log stream for #{ip}"
192
+ true
193
+ else
194
+ false
195
+ end # if
196
+ end # delete if
197
+ end # clean_log_streams
198
+
199
+ end # module
200
+
201
+ end # server
202
+
203
+ # access from everywhere as Chook.logger
204
+ def self.logger
205
+ Server::Log.logger
206
+ end
207
+
208
+ # log an exception - multiple log lines
209
+ # the first being the error message the rest being indented backtrace
210
+ def self.log_exception(exception)
211
+ logger.error exception.to_s
212
+ exception.backtrace.each { |l| logger.error "..#{l}" }
213
+ end
214
+
215
+ end # Chook