xolo-server 1.0.0

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +177 -0
  3. data/README.md +7 -0
  4. data/bin/xoloserver +106 -0
  5. data/data/com.pixar.xoloserver.plist +29 -0
  6. data/data/uninstall-pkgs-by-id.zsh +103 -0
  7. data/lib/xolo/server/app.rb +133 -0
  8. data/lib/xolo/server/command_line.rb +216 -0
  9. data/lib/xolo/server/configuration.rb +739 -0
  10. data/lib/xolo/server/constants.rb +70 -0
  11. data/lib/xolo/server/helpers/auth.rb +257 -0
  12. data/lib/xolo/server/helpers/client_data.rb +415 -0
  13. data/lib/xolo/server/helpers/file_transfers.rb +265 -0
  14. data/lib/xolo/server/helpers/jamf_pro.rb +156 -0
  15. data/lib/xolo/server/helpers/log.rb +97 -0
  16. data/lib/xolo/server/helpers/maintenance.rb +401 -0
  17. data/lib/xolo/server/helpers/notification.rb +145 -0
  18. data/lib/xolo/server/helpers/pkg_signing.rb +141 -0
  19. data/lib/xolo/server/helpers/progress_streaming.rb +252 -0
  20. data/lib/xolo/server/helpers/title_editor.rb +92 -0
  21. data/lib/xolo/server/helpers/titles.rb +145 -0
  22. data/lib/xolo/server/helpers/versions.rb +160 -0
  23. data/lib/xolo/server/log.rb +286 -0
  24. data/lib/xolo/server/mixins/changelog.rb +315 -0
  25. data/lib/xolo/server/mixins/title_jamf_access.rb +1668 -0
  26. data/lib/xolo/server/mixins/title_ted_access.rb +519 -0
  27. data/lib/xolo/server/mixins/version_jamf_access.rb +1541 -0
  28. data/lib/xolo/server/mixins/version_ted_access.rb +373 -0
  29. data/lib/xolo/server/object_locks.rb +102 -0
  30. data/lib/xolo/server/routes/auth.rb +89 -0
  31. data/lib/xolo/server/routes/jamf_pro.rb +89 -0
  32. data/lib/xolo/server/routes/maint.rb +174 -0
  33. data/lib/xolo/server/routes/title_editor.rb +71 -0
  34. data/lib/xolo/server/routes/titles.rb +285 -0
  35. data/lib/xolo/server/routes/uploads.rb +93 -0
  36. data/lib/xolo/server/routes/versions.rb +261 -0
  37. data/lib/xolo/server/routes.rb +168 -0
  38. data/lib/xolo/server/title.rb +1143 -0
  39. data/lib/xolo/server/version.rb +902 -0
  40. data/lib/xolo/server.rb +205 -0
  41. data/lib/xolo-server.rb +8 -0
  42. metadata +243 -0
@@ -0,0 +1,415 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # main module
11
+ module Xolo
12
+
13
+ module Server
14
+
15
+ module Helpers
16
+
17
+ # Constants and methods for maintaining the client data package
18
+ #
19
+ # This is used as a 'helper' in the Sinatra server
20
+ #
21
+ # This means methods here are available in all routes, views, and helpers
22
+ # the Sinatra server app.
23
+ #
24
+ # The client data package is a Jamf::JPackage that installs a JSON file on all
25
+ # managed Macs. This JSON file contains data about all titles and versions, and
26
+ # any other data that the xolo client needs to know about.
27
+ #
28
+ # It is updated automatically by the server when titles or versions are changed.
29
+ #
30
+ # It is used so that the xolo client can know what it needs to know about titles and
31
+ # versions without having to query the server or do anything over a network other
32
+ # than using the jamf binary.
33
+ #
34
+ # The downside is that the client data package is likely to be somewhat out of date,
35
+ # but that is a tradeoff for the simplicity and security of the client.
36
+ #
37
+ # The client data package is installed in /Library/Application Support/xolo/client-data.json
38
+ # it contains a JSON object with a 'titles' key, which is an object with keys for each title.
39
+ # The data provided is that produced by the Title#to_h and Version#to_h methods.
40
+ module ClientData
41
+
42
+ # Constants
43
+ #
44
+ ##############################
45
+ ##############################
46
+
47
+ CLIENT_DATA_STR = 'client-data'
48
+
49
+ # The name of the Jamf Package object that contains the xolo-client-data
50
+ # NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY
51
+ CLIENT_DATA_PACKAGE_NAME = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{CLIENT_DATA_STR}"
52
+
53
+ # The name of the package file that installs the xolo-client-data JSON file
54
+ CLIENT_DATA_COMPONENT_PACKAGE_FILE = "#{CLIENT_DATA_PACKAGE_NAME}-component.pkg"
55
+
56
+ CLIENT_DATA_PACKAGE_FILE = "#{CLIENT_DATA_PACKAGE_NAME}.pkg"
57
+
58
+ # The package identifier for the xolo-client-data package
59
+ CLIENT_DATA_PACKAGE_IDENTIFIER = "com.pixar.xolo.#{CLIENT_DATA_STR}"
60
+
61
+ # The name of the Jamf::Policy object that installs the xolo-client-data package
62
+ # automatically on all managed Macs
63
+ # NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY
64
+ CLIENT_DATA_AUTO_POLICY_NAME = "#{CLIENT_DATA_PACKAGE_NAME}-auto"
65
+
66
+ # The name of the Jamf::Policy object that installs the xolo-client-data package
67
+ # manually on a managed Mac
68
+ CLIENT_DATA_MANUAL_POLICY_NAME = "#{CLIENT_DATA_PACKAGE_NAME}-manual"
69
+
70
+ # The name of the client-data JSON file in the xolo-client-data package
71
+ # this is the file that is installed onto managed Macs in
72
+ # /Library/Application Support/xolo/
73
+ CLIENT_DATA_FILE = 'client-data.json'
74
+
75
+ # The trgger event for the manual policy to update the client data JSON file
76
+ CLIENT_DATA_MANUAL_POLICY_TRIGGER = 'update-xolo-client-data'
77
+
78
+ # Module methods
79
+ #
80
+ # These are available as module methods but not as 'helper'
81
+ # methods in sinatra routes & views.
82
+ #
83
+ ##############################
84
+ ##############################
85
+
86
+ # when this module is included
87
+ ##############################
88
+ def self.included(includer)
89
+ Xolo.verbose_include includer, self
90
+ end
91
+
92
+ # when this module is extended
93
+ def self.extended(extender)
94
+ Xolo.verbose_extend extender, self
95
+ end
96
+
97
+ # A mutex for the client data update process
98
+ #
99
+ # TODO: use Concrrent Ruby instead of Mutex
100
+ #
101
+ # @return [Mutex] the mutex
102
+ #####################
103
+ def self.client_data_mutex
104
+ @client_data_mutex ||= Mutex.new
105
+ end
106
+
107
+ # Instance methods
108
+ #
109
+ # These are available directly in sinatra routes and views
110
+ #
111
+ ##############################
112
+ ##############################
113
+
114
+ # @return [Jamf::JPackage] the xolo-client-data package object
115
+ #####################
116
+ def client_data_jpackage
117
+ return @client_data_jpackage if @client_data_jpackage
118
+
119
+ @client_data_jpackage = Jamf::JPackage.fetch packageName: CLIENT_DATA_PACKAGE_NAME, cnx: jamf_cnx
120
+ rescue Jamf::NoSuchItemError
121
+ @client_data_jpackage = create_client_data_jamf_package
122
+ end
123
+
124
+ # update the xolo-client-data package and the policy that installs it
125
+ #
126
+ # This package installs a JSON file with data about all titles and versions
127
+ # for use by the xolo client on managed Macs.
128
+ #
129
+ # This process is protected by a mutex to prevent multiple updates at the same time.
130
+ #
131
+ # @return [void]
132
+ #####################
133
+ def update_client_data
134
+ # don't do anything if we are in developer/test mode
135
+ if Xolo::Server.config.developer_mode?
136
+ log_debug 'Jamf: Skipping client-data update in developer mode'
137
+ return
138
+ end
139
+
140
+ log_info 'Jamf: Updating client-data package'
141
+
142
+ # TODO: Use Concurrent Ruby instead of Mutex
143
+ mutex = Xolo::Server::Helpers::ClientData.client_data_mutex
144
+
145
+ until mutex.try_lock
146
+ progress 'Waiting for another client data update to finish', log: :info
147
+ sleep 5
148
+ end
149
+
150
+ new_pkg = create_new_client_data_pkg_file
151
+ upload_to_dist_point client_data_jpackage, new_pkg
152
+
153
+ create_client_data_policies_if_needed
154
+
155
+ flush_client_data_policy_logs
156
+ ensure
157
+ mutex.unlock if mutex&.owned?
158
+ end
159
+
160
+ # Create and return the xolo-client-data package in Jamf Pro
161
+ #
162
+ # @return [Jamf::JPackage]
163
+ #####################
164
+ def create_client_data_jamf_package
165
+ progress "Jamf: Creating package object '#{CLIENT_DATA_PACKAGE_NAME}'"
166
+
167
+ info = "Installs the xolo client data JSON file into /Library/Application Support/xolo/#{CLIENT_DATA_FILE}"
168
+
169
+ # Create the package
170
+ pkg = Jamf::JPackage.create(
171
+ cnx: jamf_cnx,
172
+ packageName: CLIENT_DATA_PACKAGE_NAME,
173
+ fileName: CLIENT_DATA_PACKAGE_FILE,
174
+ categoryId: jamf_xolo_category_id,
175
+ info: info
176
+ )
177
+
178
+ pkg.save
179
+ # .pkg files are not uploaded here, but in the upload_client_data_package method
180
+
181
+ log_debug "Jamf: Created package '#{CLIENT_DATA_PACKAGE_NAME}'"
182
+
183
+ pkg
184
+ rescue StandardError => e
185
+ raise "Jamf: Error creating Jamf::JPackage '#{CLIENT_DATA_PACKAGE_NAME}': #{e.class}: #{e}"
186
+ end
187
+
188
+ # Create the xolo-client-data policies in Jamf Pro
189
+ #
190
+ # @return [void]
191
+ #####################
192
+ def create_client_data_policies_if_needed
193
+ all_pol_names = Jamf::Policy.all_names(cnx: jamf_cnx)
194
+
195
+ unless all_pol_names.include? CLIENT_DATA_AUTO_POLICY_NAME
196
+ create_client_data_policy CLIENT_DATA_AUTO_POLICY_NAME
197
+ end
198
+
199
+ return if all_pol_names.include? CLIENT_DATA_MANUAL_POLICY_NAME
200
+
201
+ create_client_data_policy CLIENT_DATA_MANUAL_POLICY_NAME
202
+ end
203
+
204
+ # Create a xolo-client-data install policy in Jamf Pro
205
+ #
206
+ # @param
207
+ #
208
+ # @return [void]
209
+ #####################
210
+ def create_client_data_policy(pol_name)
211
+ progress "Jamf: Creating policy '#{pol_name}'"
212
+
213
+ # Create the policy and set common attributes
214
+ pol = Jamf::Policy.create name: pol_name, cnx: jamf_cnx
215
+ pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
216
+ pol.add_package CLIENT_DATA_PACKAGE_NAME
217
+
218
+ # scope to all computers
219
+ pol.scope.set_all_targets
220
+
221
+ # exclude the forced exclusion group if any
222
+ if valid_forced_exclusion_group_name
223
+ pol.scope.set_exclusions :computer_groups, [valid_forced_exclusion_group_name]
224
+ log_info "Jamf: Excluded computer group: #{Xolo::Server.config.forced_exclusion} from policy '#{pol_name}'"
225
+ end
226
+
227
+ # Set the trigger event and frequency
228
+ if pol_name == CLIENT_DATA_AUTO_POLICY_NAME
229
+ pol.set_trigger_event :checkin, true
230
+ pol.set_trigger_event :custom, Xolo::BLANK
231
+ pol.frequency = :daily
232
+ elsif pol_name == CLIENT_DATA_MANUAL_POLICY_NAME
233
+ pol.set_trigger_event :checkin, false
234
+ pol.set_trigger_event :custom, CLIENT_DATA_MANUAL_POLICY_TRIGGER
235
+ pol.frequency = :ongoing
236
+ else
237
+ err_msg = "Jamf: Invalid policy name '#{pol_name}' must be #{CLIENT_DATA_AUTO_POLICY_NAME} or #{CLIENT_DATA_MANUAL_POLICY_NAME}"
238
+ log_err err_msg, alert: true
239
+ return
240
+ end
241
+ pol.enable
242
+
243
+ pol.save
244
+ log_info "Jamf: Created policy '#{pol_name}'"
245
+ end
246
+
247
+ # Flush the logs for the xolo-client-data policies
248
+ #
249
+ # @return [void]
250
+ #####################
251
+ def flush_client_data_policy_logs
252
+ progress "Jamf: Flushing logs for policy #{CLIENT_DATA_AUTO_POLICY_NAME}", log: :info
253
+ pol = Jamf::Policy.fetch name: CLIENT_DATA_AUTO_POLICY_NAME, cnx: jamf_cnx
254
+ pol.flush_logs
255
+ end
256
+
257
+ # Create the xolo-client-data package installer file.
258
+ # The xolo client executable is deployed as a separate thing in a Xolo Title
259
+ #
260
+ # @return [Pathname] the path to the new package file
261
+ #####################
262
+ def create_new_client_data_pkg_file
263
+ pkg_version = Time.now.strftime '%Y%m%d.%H%M%S.%6N'
264
+ work_dir_prefix = "#{CLIENT_DATA_PACKAGE_NAME}-#{pkg_version}"
265
+
266
+ pkg_work_dir = Pathname.new(Dir.mktmpdir(work_dir_prefix))
267
+
268
+ # The client data JSON file
269
+ root_dir = pkg_work_dir + 'pkgroot'
270
+ xolo_client_dir = root_dir + 'Library' + 'Application Support' + 'xolo'
271
+ xolo_client_dir.mkpath
272
+ client_data_file = xolo_client_dir + CLIENT_DATA_FILE
273
+ client_data_file.pix_save JSON.pretty_generate(client_data_hash)
274
+
275
+ # build the component package
276
+ progress "Jamf: Creating new client-data pkg file '#{CLIENT_DATA_PACKAGE_FILE}'", log: :info
277
+
278
+ unlock_signing_keychain
279
+
280
+ component_pkg_file = build_component_client_data_pkg_file(root_dir, pkg_version, pkg_work_dir)
281
+
282
+ build_dist_client_data_pkg_file(component_pkg_file, pkg_version, pkg_work_dir)
283
+ end
284
+
285
+ # Build the component install pkg with pkgbuild
286
+ # NOTE: no need to shellescape the paths, since we are using the
287
+ # array version of Open3.capture2e
288
+ #
289
+ # @return [Pathname] the path to the new package file
290
+ #####################
291
+ def build_component_client_data_pkg_file(root_dir, pkg_version, pkg_work_dir)
292
+ outfile = pkg_work_dir + CLIENT_DATA_COMPONENT_PACKAGE_FILE
293
+
294
+ cmd = ['/usr/bin/pkgbuild']
295
+ cmd << '--root'
296
+ cmd << root_dir.to_s
297
+ cmd << '--identifier'
298
+ cmd << CLIENT_DATA_PACKAGE_IDENTIFIER
299
+ cmd << '--version'
300
+ cmd << pkg_version
301
+ cmd << '--install-location'
302
+ cmd << '/'
303
+ cmd << '--sign'
304
+ cmd << Xolo::Server.config.pkg_signing_identity
305
+ cmd << '--keychain'
306
+ cmd << Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
307
+ cmd << outfile.to_s
308
+
309
+ log_debug "Command to build component pkg '#{CLIENT_DATA_COMPONENT_PACKAGE_FILE}': #{cmd.join(' ')}"
310
+
311
+ stdouterr, exit_status = Open3.capture2e(*cmd)
312
+ raise "Error creating #{CLIENT_DATA_PACKAGE_FILE}: #{stdouterr}" unless exit_status.success?
313
+
314
+ outfile
315
+ end
316
+
317
+ # Build the distribution package for the xolo-client-data JSON file
318
+ # NOTE: no need to shellescape the paths, since we are using the
319
+ # array version of Open3.capture2e
320
+ #
321
+ # @return [Pathname] the path to the new package file
322
+ #####################
323
+ def build_dist_client_data_pkg_file(component_pkg_file, pkg_version, pkg_work_dir)
324
+ pkg_file = pkg_work_dir + CLIENT_DATA_PACKAGE_FILE
325
+
326
+ cmd = ['/usr/bin/productbuild']
327
+ cmd << '--package'
328
+ cmd << component_pkg_file.to_s
329
+ cmd << '--identifier'
330
+ cmd << CLIENT_DATA_PACKAGE_IDENTIFIER
331
+ cmd << '--version'
332
+ cmd << pkg_version
333
+ cmd << '--sign'
334
+ cmd << Xolo::Server.config.pkg_signing_identity
335
+ cmd << '--keychain'
336
+ cmd << Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
337
+ cmd << pkg_file.to_s
338
+
339
+ log_debug "Command to build distribution pkg '#{CLIENT_DATA_PACKAGE_FILE}': #{cmd.join(' ')}"
340
+
341
+ stdouterr, exit_status = Open3.capture2e(*cmd)
342
+ raise "Error creating #{CLIENT_DATA_PACKAGE_FILE}: #{stdouterr}" unless exit_status.success?
343
+
344
+ pkg_file
345
+ end
346
+
347
+ # @return [Hash] the data to put in the xolo-client-data JSON file
348
+ #####################
349
+ def client_data_hash
350
+ cdh = {
351
+ titles: {}
352
+ }
353
+ all_title_objects.each do |title|
354
+ cdh[:titles][title.title] = title.to_h
355
+ cdh[:titles][title.title][:versions] = title.version_objects.map(&:to_h)
356
+
357
+ # the client uses the version_script to determine if a title is installed
358
+ cdh[:titles][title.title][:version_script] = title.version_script_contents if title.version_script
359
+
360
+ # add the forced_exclusion_group_name if any
361
+ if Xolo::Server.config.forced_exclusion
362
+ cdh[:titles][title.title][:excluded_groups] << Xolo::Server.config.forced_exclusion
363
+ end
364
+
365
+ # add the frozen group name to the excluded_groups array
366
+ cdh[:titles][title.title][:excluded_groups] << title.jamf_frozen_group_name if title.jamf_frozen_group_name
367
+ end
368
+ # TESTING
369
+ # outfile = Pathname.new('/tmp/client-data.json')
370
+ # outfile.pix_save JSON.pretty_generate(cdh)
371
+
372
+ cdh
373
+ end
374
+
375
+ # @return [Pathname] the path to the client executable 'xolo' in the ruby gem
376
+ #####################
377
+ def client_app_source
378
+ # parent 1 == helpers
379
+ # parent 2 == server
380
+ # parent 3 == xolo
381
+ # parent 4 == lib
382
+ # parent 5 == root
383
+ @client_app ||= Pathname.new(__FILE__).expand_path.parent.parent.parent.parent.parent + 'data' + 'client' + 'xolo'
384
+ end
385
+
386
+ # temp
387
+ #####################
388
+ def client_data_testing
389
+ this_file = Pathname.new(__FILE__).expand_path
390
+ log_debug "this_file: #{this_file}"
391
+ # parent 1 == helpers
392
+ # parent 2 == server
393
+ # parent 3 == xolo
394
+ # parent 4 == lib
395
+ # parent 5 == root
396
+ data_dir = this_file.parent.parent.parent.parent.parent + 'data'
397
+ log_debug "data_dir: #{data_dir}"
398
+ log_debug "data_dir exists? #{data_dir.exist?}"
399
+ log_debug "data_dir children: #{data_dir.children}"
400
+ client_dir = data_dir + 'client'
401
+ log_debug "client_dir: #{client_dir}"
402
+ log_debug "client_dir exists? #{client_dir.exist?}"
403
+ log_debug "client_dir children: #{client_dir.children}"
404
+ client_app = client_dir + 'xolo'
405
+ log_debug "client_app: #{client_app}"
406
+ log_debug "client_app exists? #{client_app.exist?}"
407
+ end
408
+
409
+ end # JamfPro
410
+
411
+ end # Helpers
412
+
413
+ end # Server
414
+
415
+ end # module Xolo
@@ -0,0 +1,265 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # main module
11
+ module Xolo
12
+
13
+ module Server
14
+
15
+ module Helpers
16
+
17
+ module FileTransfers
18
+
19
+ # Constants
20
+ #######################
21
+ #######################
22
+
23
+ # Module Methods
24
+ #######################
25
+ #######################
26
+
27
+ # when this module is included
28
+ def self.included(includer)
29
+ Xolo.verbose_include includer, self
30
+ end
31
+
32
+ # Instance Methods
33
+ #######################
34
+ ######################
35
+
36
+ # upload a file for testing ... anything
37
+ #################################
38
+ def process_incoming_testfile
39
+ progress 'starting test file upload', log: :debug
40
+
41
+ params[:file][:filename]
42
+ tempfile = Pathname.new params[:file][:tempfile].path
43
+
44
+ progress "1/3 TempFile is #{tempfile} size is #{tempfile.size}... is it still uploading?", log: :debug
45
+ sleep 2
46
+ progress "2/3 TempFile is #{tempfile} size is #{tempfile.size}... is it still uploading?", log: :debug
47
+ sleep 2
48
+ progress "3/3 TempFile is #{tempfile} size is #{tempfile.size}... is it still uploading?", log: :debug
49
+ progress 'all done', log: :debug
50
+ end
51
+
52
+ # Store an uploaded self service icon in the title's
53
+ # directory. It'll be added to Policies and Patch Policies as needed
54
+ # (increasing the bloat in the database, of course)
55
+ #################################
56
+ def process_incoming_ssvc_icon
57
+ filename = params[:file][:filename]
58
+ tempfile = Pathname.new params[:file][:tempfile].path
59
+
60
+ log_info "Processing uploaded SelfService icon for #{params[:title]}"
61
+ title = instantiate_title params[:title]
62
+ title.save_ssvc_icon(tempfile, filename)
63
+ title.configure_pol_for_self_service if title.self_service
64
+ rescue => e
65
+ msg = "#{e.class}: #{e}"
66
+ log_error msg
67
+ e.backtrace.each { |line| log_error "..#{line}" }
68
+
69
+ halt 400, { status: 400, error: msg }
70
+ end
71
+
72
+ # Handle an uploaded pkg installer
73
+ # TODO: wrap this in a thread, it might be very slow for large pkgs.
74
+ # TODO: Also, when threaded, how to report errors?
75
+ # TODO: Split this into smaller methods
76
+ #############################
77
+ def process_incoming_pkg
78
+ log_info "Processing uploaded installer package for version '#{params[:version]}' of title '#{params[:title]}'"
79
+
80
+ # the Xolo::Server::Version that owns this pkg
81
+ version = instantiate_version title: params[:title], version: params[:version]
82
+ version.lock
83
+
84
+ # is this a re-upload? True if upload_date as any value in it
85
+ if version.upload_date.pix_empty?
86
+ action = 'Uploading'
87
+ re_uploading = false
88
+ else
89
+ re_uploading = true
90
+ action = 'Re-uploading'
91
+ version.log_change msg: 'Re-uploading pkg file'
92
+ end
93
+
94
+ # the original uploaded filename
95
+ orig_filename = params[:file][:filename]
96
+ log_debug "Incoming pkg file '#{orig_filename}' "
97
+ file_extname = validate_uploaded_pkg(orig_filename)
98
+
99
+ # Set the jamf_pkg_file, now that we know the extension
100
+ uploaded_pkg_name = "#{version.jamf_pkg_name}#{file_extname}"
101
+ log_debug "Jamf: Package filename will be '#{uploaded_pkg_name}'"
102
+ version.jamf_pkg_file = uploaded_pkg_name
103
+
104
+ # The tempfile created by Sinatra when the pkg was uploaded from xadm
105
+ tempfile = Pathname.new params[:file][:tempfile].path
106
+
107
+ # The uploaded tmpfile will be staged here before uploading again to
108
+ # the Jamf Dist Point(s)
109
+ staged_pkg = Xolo::Server::Title.title_dir(params[:title]) + uploaded_pkg_name
110
+
111
+ # remove any old one that might be there
112
+ staged_pkg.delete if staged_pkg.file?
113
+
114
+ if need_to_sign?(tempfile)
115
+ # This will put the signed pkg into the staged_pkg location
116
+ sign_uploaded_pkg(tempfile, staged_pkg)
117
+ log_debug "Signing complete, deleting temp file '#{tempfile}'"
118
+ tempfile.delete if tempfile.file?
119
+ else
120
+ log_debug "Uploaded .pkg file doesn't need signing, moving tempfile to '#{staged_pkg.basename}'"
121
+ # Put the signed pkg into the staged_pkg location
122
+ tempfile.rename staged_pkg
123
+ end
124
+
125
+ # upload the pkg with the uploader tool defined in config
126
+ # This will set the checksum and manifest in the JPackage object
127
+ upload_to_dist_point(version.jamf_package, staged_pkg)
128
+
129
+ if re_uploading
130
+ # These must be set before calling wait_to_enable_reinstall_policy
131
+ version.reupload_date = Time.now
132
+ version.reuploaded_by = session[:admin]
133
+
134
+ # This will make the version start a thread
135
+ # that will wait some period of time (to allow for pkg uploads
136
+ # to complete) before enabling the reinstall policy
137
+ version.wait_to_enable_reinstall_policy
138
+ else
139
+ version.upload_date = Time.now
140
+ version.uploaded_by = session[:admin]
141
+ end
142
+
143
+ # make note if the pkg is a Distribution package
144
+ version.dist_pkg = pkg_is_distribution?(staged_pkg)
145
+
146
+ # save the manifest just in case
147
+ version.manifest_file.pix_atomic_write(version.jamf_package.manifest)
148
+
149
+ # save the checksum just in case
150
+ version.sha_512 = version.jamf_package.checksum
151
+
152
+ # don't save the admins local path to the pkg, just the filename they uploaded
153
+ version.pkg_to_upload = orig_filename
154
+
155
+ # save/update the local data file, since we've done stuff to update it
156
+ version.save_local_data
157
+
158
+ # log the upload
159
+ version.log_change msg: "#{action} pkg file '#{staged_pkg.basename}'"
160
+
161
+ # remove the staged pkg and the tempfile
162
+ staged_pkg.delete
163
+ tempfile.delete if tempfile.file?
164
+ rescue => e
165
+ msg = "#{e.class}: #{e}"
166
+ log_error msg
167
+ e.backtrace.each { |line| log_error "..#{line}" }
168
+ halt 400, { status: 400, error: msg }
169
+ ensure
170
+ version.unlock
171
+ end
172
+
173
+ # Check if a package is a Distribution package, if not,
174
+ # it is a component package and can't be used for
175
+ # MDM deployment.
176
+ #
177
+ # @param pkg_file [Pathname, String] The path to the .pkg
178
+ #
179
+ # @return [Boolean] true if the pkg is a Distribution package
180
+ ###########################################
181
+ def pkg_is_distribution?(pkg_file)
182
+ pkg_file = Pathname.new(pkg_file)
183
+ raise ArgumentError, "pkg_file does not exist or not a file: #{pkg_file}" unless pkg_file.file?
184
+
185
+ tmpdir = Pathname.new(Dir.mktmpdir)
186
+ workdir = tmpdir + "#{pkg_file.basename}-expanded"
187
+
188
+ system "/usr/sbin/pkgutil --expand #{pkg_file.to_s.shellescape} #{workdir.to_s.shellescape}"
189
+
190
+ workdir.children.map(&:basename).map(&:to_s).include? 'Distribution'
191
+ ensure
192
+ tmpdir.rmtree
193
+ end
194
+
195
+ # upload a staged pkg to the dist point(s)
196
+ # This will also update the checksum and manifest.
197
+ #
198
+ # @param jpkg [Jamf::JPackage] The package object for which the pkg is being uploaded
199
+ # @param pkg_file [Pathname] The path to .pkg file being uploaded
200
+ #
201
+ # @return [void]
202
+ ###########################################
203
+ def upload_to_dist_point(jpkg, pkg_file)
204
+ if Xolo::Server.config.upload_tool.to_s.downcase == 'api'
205
+ jpkg.upload pkg_file # this will update the checksum and manifest automatically, and save back to the server
206
+ log_info "Jamf: Uploaded #{pkg_file.basename} to primary dist point via API, with new checksum and manifest"
207
+ else
208
+ log_debug "Jamf: Regenerating manifest for package '#{jpkg.packageName}' from #{pkg_file.basename}"
209
+ jpkg.generate_manifest(pkg_file)
210
+
211
+ log_debug "Jamf: Recalculating checksum for package '#{jpkg.packageName}' from #{pkg_file.basename}"
212
+ jpkg.recalculate_checksum(pkg_file)
213
+
214
+ log_info "Jamf: Saving package '#{jpkg.packageName}' with new checksum and manifest"
215
+ jpkg.save
216
+ upload_via_tool(jpkg, pkg_file)
217
+ end
218
+ end
219
+
220
+ # upload the pkg with the uploader tool defined in config
221
+ #
222
+ # @param version [Xolo::Server::Version] The version object
223
+ # @param staged_pkg [Pathname] The path to the staged pkg
224
+ #
225
+ # @return [void]
226
+ ###########################################
227
+ def upload_via_tool(jpkg, pkg_file)
228
+ log_info "Jamf: Uploading #{pkg_file.basename} to dist point(s) via upload tool"
229
+
230
+ tool = Shellwords.escape Xolo::Server.config.upload_tool.to_s
231
+ jpkg_name = Shellwords.escape jpkg.packageName
232
+ pkg = Shellwords.escape pkg_file.to_s
233
+ cmd = "#{tool} #{jpkg_name} #{pkg}"
234
+
235
+ stdouterr, exit_status = Open3.capture2e(cmd)
236
+ return if exit_status.success?
237
+
238
+ msg = "Uploader tool failed to upload #{pkg_file.basename} to dist point(s): #{stdouterr}"
239
+ log_error msg
240
+ raise msg
241
+ end
242
+
243
+ # Confirm and return the extension of the originally uplaoded file,
244
+ # either .pkg or .zip
245
+ #
246
+ # @param filename [String] The original name of the file uploaded to Xolo.
247
+ #
248
+ # @return [String] either '.pkg' or '.zip'
249
+ ###############################
250
+ def validate_uploaded_pkg(filename)
251
+ log_debug "Validating pkg file ext for '#{filename}'"
252
+
253
+ file_extname = Pathname.new(filename).extname
254
+ return file_extname if Xolo::OK_PKG_EXTS.include? file_extname
255
+
256
+ raise "Bad filename '#{filename}'. Package files must end in .pkg or .zip (for old-style bundle packages)"
257
+ end
258
+
259
+ end # FileTransfers
260
+
261
+ end # Helpers
262
+
263
+ end # Server
264
+
265
+ end # module Xolo