Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Getting some basic device command jazz together #2

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ gem 'bcrypt', '~> 3.1.7'

gem 'faraday'
gem 'table_parser'
gem 'apns', github: 'jamesdaniels/apns'

group :development do
gem 'foreman'
Expand Down
7 changes: 7 additions & 0 deletions backend/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
GIT
remote: git://github.com/jamesdaniels/apns.git
revision: 3d73997816a692de8fe96d5d98b5f76239f8744f
specs:
apns (1.0.0)

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -275,6 +281,7 @@ PLATFORMS

DEPENDENCIES
CFPropertyList
apns!
bcrypt (~> 3.1.7)
brakeman
capybara
Expand Down
8 changes: 6 additions & 2 deletions backend/app/controllers/mdm_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ class MdmController < ApplicationController

def queue
fail PermissionDenied unless device_udid_matches? && valid_mdm_signature?
pending_command = DeviceCommand.update_and_issue_next_command! parsed_response, device
puts parsed_request.inspect
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take out the puts

pending_command = DeviceCommand.update_and_issue_next_command! parsed_request, device
pending_command.update(
state: 'received',
received_at: Time.now
) if pending_command
send_data pending_command.try(&:to_plist), type: :xml
end

def check_in
fail PermissionDenied unless device_udid_matches? && valid_mdm_signature?
Rails.logger.info parsed_request
device_registration.update_from_check_in! parsed_request
head :ok
end
Expand Down
83 changes: 83 additions & 0 deletions backend/app/models/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,87 @@ class Command < ActiveRecord::Base
has_many :device_commands, dependent: :destroy
has_many :devices, through: :device_commands

serialize :settings, Hash

def plist_values
{RequestType: type.gsub(/Command$/, '')}
end

def parse_response!(response, device)
end

end

class DeviceInformationCommand < Command

GeneralQueries = %w(UDID Languages Locales DeviceID OrganizationInfo LastCloudBackupDate).freeze
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yuck!

DeviceInformationQueries = %w(DeviceName OSVersion BuildVersion ModelName Model ProductName SerialNumber DeviceCapacity AvailableDeviceCapacity BatteryLevel CellularTechnology IMEI MEID ModemFirmwareVersion IsSupervised IsDeviceLocatorServiceEnabled IsActivationLockEnabled IsDoNotDisturbInEffect DeviceID EASDeviceIdentifier IsCloudBackupEnabled).freeze
NetworkInformationQueries = %w(ICCID BluetoothMAC WiFiMAC EthernetMACs CurrentCarrierNetwork SIMCarrierNetwork SubscriberCarrierNetwork CarrierSettingsVersion PhoneNumber VoiceRoamingEnabled DataRoamingEnabled IsRoaming PersonalHotspotEnabled SubscriberMCC SubscriberMNC CurrentMCC CurrentMNC).freeze
ItunesStoreAccountQueries = %w(iTunesStoreAccountIsActive iTunesStoreAccountHash)

def plist_values
super.merge({
Queries: GeneralQueries | DeviceInformationQueries | NetworkInformationQueries | ItunesStoreAccountQueries
})
end

def parse_response!(response, device)
response = response['QueryResponses']
device_model = DeviceModel.find_or_create_by(model: response['ProductName'])
major, minor, patch = response['OSVersion'].split('.')
patch ||= 0
device_firmware = DeviceFirmware.find_or_create_by(buildid: response['BuildVersion'], major: major, minor: minor, patch: patch)
device_model_firmware = DeviceModelFirmware.find_or_create_by(device_firmware: device_firmware, device_model: device_model)
device.update(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really don't like persisting all the values like this; should go with a K/V store.

name: response['DeviceName'],
imei: response['IMEI'],
iccid: response['ICCID'],
meid: response['MEID'],
phone_number: response['PhoneNumber'],
subscriber_carrier: response['SubscriberCarrierNetwork'],
sim_carrier: response['SIMCarrierNetwork'],
wifi_mac_address: response['WiFiMAC'],
last_cloud_backup_at: response['LastCloudBackupDate'],
supervised: response['IsSupervised'],
roaming: response['IsRoaming'],
do_not_disturb: response['IsDoNotDisturbInEffect'],
device_locator_service_enabled: response['IsDeviceLocatorServiceEnabled'],
cloud_backup_enabled: response['IsCloudBackupEnabled'],
activation_lock_enabled: response['IsActivationLockEnabled'],
eas_identifier: response['EASDeviceIdentifier'],
roaming_enabled: response['DataRoamingEnabled'],
bluetooth_mac_address: response['BluetoothMAC'],
battery_level: response['BatteryLevel'],
remaining_storage_capacity: response['AvailableDeviceCapacity'],
storage_capacity: response['DeviceCapacity'],
device_model_firmware: device_model_firmware,
last_checkin_at: Time.now,
itunes_store_account_hash: response['iTunesStoreAccountHash'],
itunes_store_account_active: response['iTunesStoreAccountIsActive'],
personal_hotspot_enabled: response['PersonalHotspotEnabled'],
subscriber_mcc: response['SubscriberMCC'],
subscriber_mnc: response['SubscriberMNC'],
current_mcc: response['CurrentMCC'],
current_mnc: response['CurrentMNC']
)
end

end

class SecurityInfoCommand < Command

def parse_response!(response, device)
response = response['SecurityInfo']
device.update(
passcode_compliant: response['PasscodeCompliant'],
passcode_compliant_with_profiles: response['PasscodeCompliantWithProfiles'],
passcode_present: response['PasscodePresent'],
block_level_encryption_enabled: response['HardwareEncryptionCaps'] && response['HardwareEncryptionCaps'] & 1 == 1,
file_level_encryption_enabled: response['HardwareEncryptionCaps'] && response['HardwareEncryptionCaps'] & 2 == 2,
full_disk_encryption_enabled: response['FDE_Enabled'],
full_disk_encryption_has_personal_recovery_key: response['FDE_HasPersonalRecoveryKey'],
full_disk_encryption_has_institutional_recovery_key: response['FDE_HasInstitutionalRecoveryKey']
)
end

end
4 changes: 3 additions & 1 deletion backend/app/models/device.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class Device < ActiveRecord::Base
has_many :device_commands, dependent: :destroy
has_many :commands, through: :device_commands

has_many :device_users, dependent: :destroy

validates :udid, presence: true

def self.find_or_create_from_challenge_response!(challenge_response)
Expand All @@ -15,7 +17,7 @@ def self.find_or_create_from_challenge_response!(challenge_response)
device.name = challenge_response['DEVICE_NAME']
device.iccid = challenge_response['ICCID']
device.imei = challenge_response['IMEI']
#device.meid = challenge_response['MEID']
device.meid = challenge_response['MEID']
device_model = DeviceModel.where(model: challenge_response['PRODUCT']).first_or_create!
device_firmware = DeviceFirmware.where(buildid: challenge_response['VERSION']).first_or_create!
device.device_model_firmware = DeviceModelFirmware.where(device_model_id: device_model, device_firmware_id: device_firmware).first_or_create!
Expand Down
45 changes: 28 additions & 17 deletions backend/app/models/device_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,47 @@ class DeviceCommand < ActiveRecord::Base

belongs_to :device
belongs_to :command
belongs_to :device_registration
belongs_to :device_user_registration

def self.update_and_issue_next_command!(parsed_response, device)
status = parsed_response['Status']
def self.update_and_issue_next_command!(parsed_request, device)
status = parsed_request['Status']
if parsed_request['UserID']
device_user = device.device_users.find_by(user_id: parsed_request['UserID'])
else
device_user = nil
end
if status != 'Idle'
device_command = device.device_commands.find(parsed_response['CommmandUUID'])
if status != 'NotNow'
device_command = device.device_commands.find(parsed_request['CommandUUID'])
if status == 'NotNow'
# set device state to busy
else
device_command.update_from_response! parsed_response
device_command.update_from_response! parsed_request
end
end
nil
if status == 'NotNow'
# TODO look up command that are guaranteed
else
# TODO don't look up received on guaranteed
device.device_commands.find_by(
state: %w(received pending),
device_user_id: device_user
)
end
end

def update_from_response!(parsed_response)
def update_from_response!(parsed_request)
command.parse_response! parsed_request, device
update(state: parsed_request['Status'].downcase)
end

def to_plist
plist = CFPropertyList::List.new
plist.value = CFPropertyList.guess plist_values
plist.value = CFPropertyList.guess({
CommandUUID: id.to_s,
Command: command.plist_values
})
plist.to_str CFPropertyList::List::FORMAT_XML
end

def plist_values
{
'CommandUUID' => id,
'Command' => {
'RequestType' => type
}
}
end

end
74 changes: 69 additions & 5 deletions backend/app/models/device_registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ class DeviceRegistration < ActiveRecord::Base

belongs_to :device

AccessRights = %i(configuration_profile_inspection configuration_profile_management device_lock_and_unlock device_erase device_information_queries network_information_queries provisioning_profile_inspection provisioning_profile_management application_inspection restriction_queries security_queries setting_management application_management).freeze
has_many :device_user_registrations, dependent: :destroy
has_many :device_users, through: :device_user_registrations

AccessRights = %w(configuration_profile_inspection configuration_profile_management device_lock_and_unlock device_erase device_information_queries network_information_queries provisioning_profile_inspection provisioning_profile_management application_inspection restriction_queries security_queries setting_management application_management).freeze

def self.create_from_request(request)
create do |device_registration|
Expand All @@ -31,6 +34,49 @@ def scep_challenge=(new_scep_challenge)
self.scep_challenge_digest = Password.create @scep_challenge
end

def send_push
send_apns_notifications [
APNS::MdmNotification.new(
Base64.decode64(apns_token).unpack('H*')[0],
apns_push_magic
)
]
end

def send_apns_notifications(notifications)
apns_connection do |ssl|
notifications.each do |notification|
ssl.write notification.packaged_notification
end
end
end

def apns_connection(feedback = false)

context = OpenSSL::SSL::SSLContext.new
context.cert = ApnsCert
context.key = ApnsKey

if feedback
host = 'feedback.push.apple.com'
port = 2196
else
host = 'gateway.push.apple.com'
port = 2195
end

sock = TCPSocket.new host, port
ssl = OpenSSL::SSL::SSLSocket.new sock, context
ssl.connect

yield(ssl)

ssl.flush
ssl.close
sock.close
end


def to_plist(payload_url)
plist = CFPropertyList::List.new
plist.value = CFPropertyList.guess plist_values(payload_url)
Expand All @@ -40,7 +86,7 @@ def to_plist(payload_url)
def access_rights
AccessRights.each_with_index.map do |right, index|
if true
(1 + index) ** 2
2 ** index
else
0
end
Expand Down Expand Up @@ -139,9 +185,27 @@ def plist_values(payload_url)
def update_from_check_in!(parsed_request)
case parsed_request['MessageType']
when 'TokenUpdate'
self.apns_push_magic = parsed_request['PushMagic']
self.apns_token = Base64.encode64 parsed_request['Token']
save!
if parsed_request['UserID']
device_user = DeviceUser.find_or_create_by(
user_id: parsed_request['UserID'],
device_id: device_id
)
device_user.user_long_name = parsed_request['UserLongName']
device_user.user_short_name = parsed_request['UserShortName']
device_user.save!

device_user_registration = DeviceUserRegistration.find_or_create_by(
device_user: device_user,
device_registration: self
)
device_user_registration.apns_push_magic = parsed_request['PushMagic']
device_user_registration.apns_token = Base64.encode64 parsed_request['Token']
device_user_registration.save!
else
self.apns_push_magic = parsed_request['PushMagic']
self.apns_token = Base64.encode64 parsed_request['Token']
save!
end
when 'Authenticate'
self.state = 'managed'
transaction do
Expand Down
8 changes: 8 additions & 0 deletions backend/app/models/device_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class DeviceUser < ActiveRecord::Base

belongs_to :device

has_many :device_user_registrations, dependent: :destroy
has_many :device_registrations, through: :device_user_registrations

end
15 changes: 15 additions & 0 deletions backend/app/models/device_user_registration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class DeviceUserRegistration < ActiveRecord::Base

belongs_to :device_registration
belongs_to :device_user

def send_push
device_registration.send_apns_notifications [
APNS::MdmNotification.new(
Base64.decode64(apns_token).unpack('H*')[0],
apns_push_magic
)
]
end

end
4 changes: 1 addition & 3 deletions backend/config/initializers/certificates.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


if Rails.env.test?

ScepKey = OpenSSL::PKey::RSA.new 2048
Expand Down Expand Up @@ -53,7 +51,7 @@
ScepCert = OpenSSL::X509::Certificate.new File.read(scep_cert_path)

apns_key_path = File.join(Rails.root, 'ssl', 'apnskey.pem')
ApnsKey = OpenSSL::PKey::RSA.new File.read(apns_key_path), ENV['APNS_PASSPHRASE']
ApnsKey = OpenSSL::PKey::RSA.new File.read(apns_key_path), 'ad50c8b9b35ec4384e7f2b9dcaebe1cb'

apns_cert_path = File.join(Rails.root, 'ssl', 'apnscert.pem')
ApnsCert = OpenSSL::X509::Certificate.new File.read(apns_cert_path)
Expand Down
23 changes: 23 additions & 0 deletions backend/db/migrate/20150612034325_device_details.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class DeviceDetails < ActiveRecord::Migration
def change
add_column :devices, :meid, :string
add_column :devices, :phone_number, :string
add_column :devices, :subscriber_carrier, :string
add_column :devices, :sim_carrier, :string
add_column :devices, :wifi_mac_address, :string
add_column :devices, :last_cloud_backup_at, :datetime
add_column :devices, :supervised, :boolean
add_column :devices, :roaming, :boolean
add_column :devices, :do_not_disturb, :boolean
add_column :devices, :device_locator_service_enabled, :boolean
add_column :devices, :cloud_backup_enabled, :boolean
add_column :devices, :activation_lock_enabled, :boolean
add_column :devices, :eas_identifier, :string
add_column :devices, :roaming_enabled, :boolean
add_column :devices, :bluetooth_mac_address, :string
add_column :devices, :battery_level, :float
add_column :devices, :remaining_storage_capacity, :float
add_column :devices, :storage_capacity, :float
add_column :devices, :last_checkin_at, :datetime
end
end
Loading