chook 1.0.1.b2 → 1.1.5
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 +5 -5
- data/CHANGES.md +56 -0
- data/README.md +363 -127
- data/bin/chook-server +31 -1
- data/data/chook.conf.example +183 -0
- data/data/com.pixar.chook-server.plist +20 -0
- data/data/sample_handlers/RestAPIOperation.rb +11 -11
- data/data/sample_handlers/SmartGroupComputerMembershipChange.rb +3 -6
- data/data/sample_jsons/SmartGroupComputerMembershipChange.json +3 -1
- data/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +3 -1
- data/lib/chook/configuration.rb +27 -8
- data/lib/chook/event.rb +6 -1
- data/lib/chook/event/handled_event.rb +36 -9
- data/lib/chook/event/handled_event/handlers.rb +260 -98
- data/lib/chook/event/handled_event_logger.rb +86 -0
- data/lib/chook/event_handling.rb +1 -0
- data/lib/chook/foundation.rb +3 -0
- data/lib/chook/procs.rb +17 -1
- data/lib/chook/server.rb +73 -72
- data/lib/chook/server/auth.rb +164 -0
- data/lib/chook/server/log.rb +215 -0
- data/lib/chook/server/public/css/chook.css +133 -0
- data/lib/chook/server/public/imgs/ChookLogoAlMcWhiggin.png +0 -0
- data/lib/chook/server/public/js/chook.js +126 -0
- data/lib/chook/server/public/js/logstream.js +101 -0
- data/lib/chook/server/routes.rb +28 -0
- data/lib/chook/server/routes/handle_by_name.rb +65 -0
- data/lib/chook/server/routes/handle_webhook_event.rb +27 -3
- data/lib/chook/server/routes/handlers.rb +52 -0
- data/lib/chook/server/routes/home.rb +48 -1
- data/lib/chook/server/routes/log.rb +105 -0
- data/lib/chook/server/routes/login_logout.rb +48 -0
- data/lib/chook/server/views/admin.haml +11 -0
- data/lib/chook/server/views/bak.haml +48 -0
- data/lib/chook/server/views/config.haml +15 -0
- data/lib/chook/server/views/handlers.haml +63 -0
- data/lib/chook/server/views/layout.haml +64 -0
- data/lib/chook/server/views/logstream.haml +33 -0
- data/lib/chook/server/views/sketch_admin +44 -0
- data/lib/chook/subject.rb +13 -2
- data/lib/chook/subject/dep_device.rb +81 -0
- data/lib/chook/subject/policy_finished.rb +43 -0
- data/lib/chook/subject/smart_group.rb +6 -0
- data/lib/chook/version.rb +1 -1
- metadata +79 -19
data/lib/chook/event_handling.rb
CHANGED
data/lib/chook/foundation.rb
CHANGED
data/lib/chook/procs.rb
CHANGED
@@ -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
|
-
|
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'
|
data/lib/chook/server.rb
CHANGED
@@ -22,9 +22,15 @@
|
|
22
22
|
### language governing permissions and limitations under the Apache License.
|
23
23
|
###
|
24
24
|
###
|
25
|
-
|
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
|
-
|
36
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
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
|
-
|
69
|
-
|
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 :
|
72
|
-
|
79
|
+
set :logger, Log.startup(@log_level)
|
80
|
+
set :server, :thin
|
73
81
|
set :bind, '0.0.0.0'
|
74
|
-
set :
|
75
|
-
set :
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
97
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
102
|
+
def self.uptime
|
103
|
+
@start_time ? "#{humanize_secs(Time.now - @start_time)} ago" : 'Not Running'
|
104
|
+
end
|
112
105
|
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|