├── .gitignore ├── .gitmodules ├── README.md ├── client-reporting ├── client-reporting.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ │ └── gblack.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── gblack.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints.xcbkptlist │ │ └── xcschemes │ │ ├── client-reporting.xcscheme │ │ └── xcschememanagement.plist └── client-reporting │ ├── client-reporting-Prefix.pch │ ├── client_reporting.h │ └── client_reporting.m ├── images ├── certPortal.png ├── commandSuccess.png ├── deviceEnroll.jpg ├── enrollConfig.png └── enrollExport.png ├── references ├── CommandReference.pdf └── InsideAppleMDM.pdf ├── scripts ├── README_WINDOWS.txt ├── make_certs.bat ├── make_certs.sh ├── server.cnf ├── vendor_signing.bat └── vendor_signing.sh └── server ├── Example.mobileconfig ├── LICENSE ├── Manifest.plist.template ├── README ├── device.py ├── favicon.ico ├── problems.py ├── server.py ├── static ├── devices.mustache.html ├── dist │ ├── LICENSE │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── site-template.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── jquery-1.11.0.min.js │ │ ├── main.js │ │ ├── mustache.min.js │ │ ├── populate.js │ │ └── setup.js └── index.html └── xactn.log /.gitignore: -------------------------------------------------------------------------------- 1 | # Python related files 2 | server/*.pyc 3 | 4 | # Logs/storage with possible sensitive information 5 | server/xactn.log 6 | server/devicelist.pickle 7 | 8 | # Certs with sensitive information 9 | server/CA.crt 10 | server/Enroll.mobileconfig 11 | server/Identity.p12 12 | server/PushCert.pem 13 | server/Server.* 14 | server/identity.crt 15 | 16 | # Custom payloads 17 | server/Manifest.plist 18 | server/MyApp.* 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor"] 2 | path = vendor 3 | url = git://github.com/grinich/mdmvendorsign 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview[![analytics](http://www.google-analytics.com/collect?v=1&t=pageview&_s=1&dl=https%3A%2F%2Fgithub.com%2Fproject-imas%2Fmdm-server&_u=MAC~&cid=1757014354.1393964045&tid=UA-38868530-1)]() 2 | 3 | Instructions and code for setting up a simple iOS Mobile Device Management (MDM) server. MDM allows for OS level control of multiple devices from a centralized location. A remote administrator can install/remove apps, install/revoke certificates, lock the device, change password requirements, etc. 4 | 5 | # Prerequisites 6 | 7 | * Publicly accessible Linux/Unix server 8 | * Apple Enterprise Account 9 | * Apple Developer Account 10 | * Python 2.7 (See Server Setup for libraries) 11 | * openssl command-line 12 | * Java SDK (java/javac) 13 | * Apple's iPhone Configuration Utility 14 | * [OS X Version](http://support.apple.com/kb/dl1465) 15 | * [Windows Version](http://support.apple.com/kb/DL1466) 16 | 17 | # Setup 18 | 19 | 1. Create MDM Vendor CSR 20 | * Open Keychain Access. 21 | * Go to the menu bar: Keychain Access -> Certificate Assistant -> Request a Certificate From a Certificate Authority. 22 | * Use the same email as the developer account that will be used. Enter in a common name as well. 23 | * Select *Saved to disk*. 24 | 25 | 2. Upload CSR to Apple 26 | * Go to [Apple's Certificates, Identifiers & Profiles page](https://developer.apple.com/account/ios/certificate/certificateCreate.action). 27 | * Select MDM CSR under Production. If this option is disabled, you will need to contact apple to enable it. You can either email apple at devprograms@apple.com or go through the [online contact menu](http://developer.apple.com/contact/). In your message, indicate that you are looking to create an MDM Vendor Certificate and need the MDM CSR option enabled on the certificate creation page. Apple should respond within one business day according to their contact page. 28 | * When you have the MDM CSR option available, select it and hit continue. Hit continue again through Apple's description of how to create a CSR file (we already have one). 29 | * Upload the .certSigningRequest file we created in step 1 and then hit generate. A .cer file should be downloaded. Name it something like mdmvendor.cer. 30 | 31 | 3. Export MDM private key 32 | * Open your mdmvendor.cer file in Keychain Access. 33 | * Select Certificates from the left side. 34 | * You should find your certificate listed as *MDM Vendor: Common Name*. 35 | * There should be an arrow on that line that opens up show the MDM private key. 36 | * Right-click the private key, select *Export...*, and save as private.p12 37 | * Remember where you save this file, we will use it in step 5. 38 | 39 | 4. Create Push Certificate CSR 40 | * In Keychain Access, again select from the menu bar: Keychain Access -> Certificate Assistant -> Request a Certificate From a Certificate Authority. 41 | * Enter your email (can be a different email) and a common name. 42 | * Select *Saved to disk* and name it something like push.csr. 43 | 44 | 5. Extract MDM private key and MDM Vendor Certificate 45 | * Extract private key using the following command: 46 | 47 | openssl pkcs12 -in private.p12 -nocerts -out key.pem 48 | 49 | * Strip the password from the private key using the following command: 50 | 51 | openssl rsa -in key.pem out private.key 52 | 53 | * Extract certificate using the following command: 54 | 55 | openssl pkcs12 -in private.p12 -clcerts -nokeys -out cert.pem 56 | 57 | * Convert certificate to DES using the following command: 58 | 59 | openssl x509 -in cert.pem -inform PEM -out mdm.cer -outform DES 60 | 61 | * These files will be used in the next step. 62 | 63 | 6. Use the mdmvendorsign tool to create applepush.csr 64 | * We're going to use the python code located in /vendor/. If /vendor/ is currently empty, you probably forgot to init and update submodules 65 | 66 | git submodule init 67 | git submodule update 68 | 69 | * Copy private.key, push.csr, and mdm.cer into /vendor/ 70 | 71 | * Run the following command while in that directory: 72 | 73 | python mdm_vendorpython mdm_vendor_sign.py –key private.key –csr push.csr –mdm mdm.cer –out applepush.csr 74 | 75 | * This should generate applepush.csr. 76 | 77 | 7. Get Push Certificate from Apple 78 | * Go to [Apple's Push Certificates Portal](https://identity.apple.com/pushcert/) and click the Create a Certificate button. 79 | * Upload applepush.csr to create a new entry in the table. 80 | * Download the resulting push certificate. 81 | * Open the push certificate in Keychain Access. 82 | 83 | 8. Prepare Push Certificate 84 | * Find the push certificate in Keychain Access. It should look like *APSP:hexstuffhere*. 85 | * Right-click the certificate and select *Get Info*. 86 | * Copy down the User ID which should look like com.apple.mgmt.External.hexstuffhere... We will use it later on in step 9. 87 | * Right-click the certificate and select *Export...* and save it as mdm.p12 88 | * Run the following command to convert it to a pem file: 89 | 90 | openssl pkcs12 -in mdm.p12 -out PushCert.pem -nodes 91 | 92 | * Move the resulting PushCert.pem file to /server/ 93 | 94 | 9. Generate additional certs 95 | * Go to the scripts directory and run make_certs.sh. 96 | * This will generate a number of necessary certs to move forward. 97 | * Certs will be automatically moved to their proper location in /server. 98 | * We'll use identity.p12 in step 10 to create an Enroll.mobileconfig file 99 | 100 | 10. Create Enroll.mobileconfig 101 | * Open the iPhone Configuration Utilities program, select *Configuration Profiles*, and then click the *New* button. 102 | * In the General category: Pick a name to identify the cert. For Identifier, use the com.apple.mgmt.External.hexstuffhere that you copied down earlier. 103 | * In the Credentials category, click configure and find your scripts/identity.p12 file generated in step 9. For password, we either use the PEM password or the export password - if the profile does not install, try the other option. Please leave feedback with which worked. 104 | * For Mobile Device Management: 105 | * Server URL: https://YOUR_HOSTNAME_OR_IP:8080/server 106 | * Check In URL: https://YOUR_HOSTNAME_OR_IP:8080/checkin 107 | * Topic: com.apple.mgmt... string (same as General->Identifier) 108 | * Identity: identity.p12 109 | * Sign messages: Checked 110 | * Check out when removed: Unchecked 111 | * Query device for: Check all that you want 112 | * Add / Remove: Check all that you want 113 | * Security: Check all that you want 114 | * Use Development APNS server: Uncheck 115 | * When done, click Export. Choose None for security and then Export.... 116 | * Save the file as **Enroll**. You will now have an Enroll.mobileconfig file - move it to the /server directory. 117 | 118 | 11. Cleanup 119 | * Any additional files that are not in /server/ generated during this process are not necessary for running the server. Some of them may have/be private keys or other unique information, so it is probably a good idea to protect or destroy those files. 120 | * Most certs will be located in the /scripts/ folder. There may be some generated from Keychain Access that were saved by the user and may be saved elsewhere. 121 | * Please secure these files and prevent others from being able to access them. 122 | 123 | NOTE: UPDATING CERTIFICATE INSTRUCTIONS - WORK IN PROGRESS 124 | 125 | 126 | # Server Setup 127 | 128 | The server code is based on and heavily takes from [Intrepidus Group's blackhat presentation](https://intrepidusgroup.com/). Copy over the **mdm-server/server** directory you put the enrollment profile and certificates in to your server. 129 | 130 | You must have the following installed on the server: 131 | * Openssl 132 | * Recommend downloading and compiling yourself 133 | * Some Debian-based distros disable features needed by M2Crypto 134 | * Source available at [http://www.openssl.org/source/](http://www.openssl.org/source/) 135 | * Python 2.7, with the following libraries 136 | * [web.py](http://webpy.org/) 137 | * [M2Crypto](https://pypi.python.org/pypi/M2Crypto) 138 | * [PyOpenSSL](https://pypi.python.org/pypi/pyOpenSSL) 139 | * [APNSWrapper](https://pypi.python.org/pypi/APNSWrapper) 140 | * APNSWrapper appears to be inactive 141 | * On 22 October 2014, [Apple removed support for SSLv3](https://developer.apple.com/news/?id=10222014a), which APNSWrapper uses, due to the poodle vulnerability 142 | * As a temporary solution, users need to edit line 131 of connections.py of the source code of APNSWrapper 143 | * Change "SSLv3" to "TLSv1", so that the line reads: 144 | ```python 145 | ssl_version = self.ssl_module.PROTOCOL_TLSv1, 146 | ``` 147 | * After making the change, users should install the library using: 148 | 149 | ```bash 150 | python setup.py install 151 | ``` 152 | 153 | * More information will follow as we find a better solution 154 | 155 | Network Settings 156 | * Outbound access to gateway.push.apple.com:2195 157 | * Inbound access to port 8080 158 | * iOS device must also have outbound access to gateway.push.apple.com:5223 159 | 160 | If everything is setup appropriately, simply navigate to the **/server** directory and run python server.py. 161 | 162 | On the device navigate to: **https://YOUR_HOST:8080/** 163 | Once there you need to, in order: 164 | 1. Tap *here* to install the CA Cert (for Server/Identity) 165 | 2. Tap *here* to enroll in MDM (the device should appear after this step) 166 | 3. Select Command (DeviceLock is a good one to test) and check your device. Click Submit to send the command. 167 | 4. If everything works, the device should lock and you're good to go! As of right now some of the commands aren't fully implemented. Feel free to experiment with different commands! 168 | 169 | --- 170 | ![Device Enrollment Steps](images/deviceEnroll.jpg) 171 | --- 172 | 173 | You can now run those commands from any web browser, a successfull command will often looks something like the following: 174 | 175 | --- 176 | ![Command Success](images/commandSuccess.png) 177 | --- 178 | 179 | Click the "Response" button to see the plist response from apple. Click the pencil to edit the device name, device owner, and device location. 180 | 181 | 182 | When stopping the server, the standard control-c doesn't usually work. Instead use control-z to suspend the process and then use a kill command to end the process. 183 | 184 | ^z 185 | [1]+ Stopped python server.py 186 | user:~/mdm-server/server$ kill %1 187 | [1]+ Terminated python server.py 188 | user:~/mdm-server/server$ 189 | 190 | The server uses the pickle library to save devices. When the device class is updated, the pickle format may be invalidated, causing the server to error. In order to fix this, remove the devicelist.pickle file (make a backup just in case!) and re-enroll all devices. 191 | 192 | # Client Reporting 193 | 194 | The MDM server also has REST endpoints for reporting issues and geolocation data from the enrolled clients. This functionality may be used at a later point in time by a security app. The API can be imported into any project as follows: 195 | 196 | * Click on the top level Project item and add files ("option-command-a") 197 | * Navigate to client-reporting/ 198 | * Highlight the client-reporting subdirectory 199 | * Click the Add button 200 | 201 | The library provides the following functions: 202 | 203 | +(void) setHostAddress: (NSString*) host; // Set where the MDM server lives 204 | +(void) setPause : (BOOL) toggle; // Toggle whether to add a thread execution pause to allow requests to finish 205 | +(void) reportJailbreak; // Report that the device has been jailbroken 206 | +(void) reportDebugger; // Report that the application has a debugger attached 207 | +(void) reportLocation : (CLLocationCoordinate2D*) coords; // Report the lat/lon location of the device 208 | 209 | "setHostAddress" and "setPause" are meant to be set once only, and effect all "report" calls. An example usage may look like: 210 | 211 | // Code in application init 212 | [client_reporting setHostAddress:@"192.168.0.0"]; 213 | [client_reporting setPause:YES]; 214 | 215 | // Later code during execution 216 | [client_reporting reportDebugger] 217 | 218 | This client API can be coupled with the [iMAS security-check controls](git@github.com:project-imas/security-check.git) to provide accurate reporting of jailbreak and debugger detection. 219 | 220 | 221 | Apologies for the long and complex setup, we hope to eventually make things easier and simpler. Please post questions to github if you get stuck and we'll do our best to help. Enjoy! 222 | 223 | 224 | 225 | # LICENSE AND ATTRIBUTION 226 | 227 | Copyright 2013-2014 The MITRE Corporation, All Rights Reserved. 228 | 229 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License at 230 | 231 | http://www.apache.org/licenses/LICENSE-2.0 232 | 233 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 234 | 235 | 236 | This project also uses code from various sources under various licenses. 237 | 238 | [The original code from the Intrepidus Group's python server is under the BSD License found here.](server/LICENSE) 239 | 240 | [The python vendor signing code is located here and is under the MIT license.](https://github.com/grinich/mdmvendorsign) 241 | 242 | [The Softhinker certificate signing code is under the Apache License found here.](vendor-signing/LICENSE) 243 | 244 | [The website's Bootstrap code is under the MIT License found here.](server/static/dist/LICENSE) 245 | 246 | The certificate setup instructions were based on [this blog post](http://www.blueboxmoon.com/wordpress/?p=877). Our thanks to Daniel. 247 | 248 | Finally we use some free [glyphicons](http://glyphicons.com/) that are included with bootstrap. 249 | -------------------------------------------------------------------------------- /client-reporting/client-reporting.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3E90E01D1864F60400155CB7 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E90E01C1864F60400155CB7 /* Foundation.framework */; }; 11 | 3E90E0221864F60400155CB7 /* client_reporting.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3E90E0211864F60400155CB7 /* client_reporting.h */; }; 12 | 3E90E0241864F60400155CB7 /* client_reporting.m in Sources */ = {isa = PBXBuildFile; fileRef = 3E90E0231864F60400155CB7 /* client_reporting.m */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXCopyFilesBuildPhase section */ 16 | 3E90E0171864F60400155CB7 /* CopyFiles */ = { 17 | isa = PBXCopyFilesBuildPhase; 18 | buildActionMask = 2147483647; 19 | dstPath = "include/${PRODUCT_NAME}"; 20 | dstSubfolderSpec = 16; 21 | files = ( 22 | 3E90E0221864F60400155CB7 /* client_reporting.h in CopyFiles */, 23 | ); 24 | runOnlyForDeploymentPostprocessing = 0; 25 | }; 26 | /* End PBXCopyFilesBuildPhase section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 3E90E0191864F60400155CB7 /* libclient-reporting.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libclient-reporting.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 3E90E01C1864F60400155CB7 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 31 | 3E90E0201864F60400155CB7 /* client-reporting-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "client-reporting-Prefix.pch"; sourceTree = ""; }; 32 | 3E90E0211864F60400155CB7 /* client_reporting.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = client_reporting.h; sourceTree = ""; }; 33 | 3E90E0231864F60400155CB7 /* client_reporting.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = client_reporting.m; sourceTree = ""; }; 34 | /* End PBXFileReference section */ 35 | 36 | /* Begin PBXFrameworksBuildPhase section */ 37 | 3E90E0161864F60400155CB7 /* Frameworks */ = { 38 | isa = PBXFrameworksBuildPhase; 39 | buildActionMask = 2147483647; 40 | files = ( 41 | 3E90E01D1864F60400155CB7 /* Foundation.framework in Frameworks */, 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 3E90E0101864F60400155CB7 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 3E90E01E1864F60400155CB7 /* client-reporting */, 52 | 3E90E01B1864F60400155CB7 /* Frameworks */, 53 | 3E90E01A1864F60400155CB7 /* Products */, 54 | ); 55 | sourceTree = ""; 56 | }; 57 | 3E90E01A1864F60400155CB7 /* Products */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 3E90E0191864F60400155CB7 /* libclient-reporting.a */, 61 | ); 62 | name = Products; 63 | sourceTree = ""; 64 | }; 65 | 3E90E01B1864F60400155CB7 /* Frameworks */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 3E90E01C1864F60400155CB7 /* Foundation.framework */, 69 | ); 70 | name = Frameworks; 71 | sourceTree = ""; 72 | }; 73 | 3E90E01E1864F60400155CB7 /* client-reporting */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 3E90E0211864F60400155CB7 /* client_reporting.h */, 77 | 3E90E0231864F60400155CB7 /* client_reporting.m */, 78 | 3E90E01F1864F60400155CB7 /* Supporting Files */, 79 | ); 80 | path = "client-reporting"; 81 | sourceTree = ""; 82 | }; 83 | 3E90E01F1864F60400155CB7 /* Supporting Files */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 3E90E0201864F60400155CB7 /* client-reporting-Prefix.pch */, 87 | ); 88 | name = "Supporting Files"; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | 3E90E0181864F60400155CB7 /* client-reporting */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = 3E90E0271864F60400155CB7 /* Build configuration list for PBXNativeTarget "client-reporting" */; 97 | buildPhases = ( 98 | 3E90E0151864F60400155CB7 /* Sources */, 99 | 3E90E0161864F60400155CB7 /* Frameworks */, 100 | 3E90E0171864F60400155CB7 /* CopyFiles */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = "client-reporting"; 107 | productName = "client-reporting"; 108 | productReference = 3E90E0191864F60400155CB7 /* libclient-reporting.a */; 109 | productType = "com.apple.product-type.library.static"; 110 | }; 111 | /* End PBXNativeTarget section */ 112 | 113 | /* Begin PBXProject section */ 114 | 3E90E0111864F60400155CB7 /* Project object */ = { 115 | isa = PBXProject; 116 | attributes = { 117 | LastUpgradeCheck = 0460; 118 | ORGANIZATIONNAME = "Black, Gavin S."; 119 | }; 120 | buildConfigurationList = 3E90E0141864F60400155CB7 /* Build configuration list for PBXProject "client-reporting" */; 121 | compatibilityVersion = "Xcode 3.2"; 122 | developmentRegion = English; 123 | hasScannedForEncodings = 0; 124 | knownRegions = ( 125 | en, 126 | ); 127 | mainGroup = 3E90E0101864F60400155CB7; 128 | productRefGroup = 3E90E01A1864F60400155CB7 /* Products */; 129 | projectDirPath = ""; 130 | projectRoot = ""; 131 | targets = ( 132 | 3E90E0181864F60400155CB7 /* client-reporting */, 133 | ); 134 | }; 135 | /* End PBXProject section */ 136 | 137 | /* Begin PBXSourcesBuildPhase section */ 138 | 3E90E0151864F60400155CB7 /* Sources */ = { 139 | isa = PBXSourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 3E90E0241864F60400155CB7 /* client_reporting.m in Sources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXSourcesBuildPhase section */ 147 | 148 | /* Begin XCBuildConfiguration section */ 149 | 3E90E0251864F60400155CB7 /* Debug */ = { 150 | isa = XCBuildConfiguration; 151 | buildSettings = { 152 | ALWAYS_SEARCH_USER_PATHS = NO; 153 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 154 | CLANG_CXX_LIBRARY = "libc++"; 155 | CLANG_ENABLE_OBJC_ARC = YES; 156 | CLANG_WARN_CONSTANT_CONVERSION = YES; 157 | CLANG_WARN_EMPTY_BODY = YES; 158 | CLANG_WARN_ENUM_CONVERSION = YES; 159 | CLANG_WARN_INT_CONVERSION = YES; 160 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 | COPY_PHASE_STRIP = NO; 162 | GCC_C_LANGUAGE_STANDARD = gnu99; 163 | GCC_DYNAMIC_NO_PIC = NO; 164 | GCC_OPTIMIZATION_LEVEL = 0; 165 | GCC_PREPROCESSOR_DEFINITIONS = ( 166 | "DEBUG=1", 167 | "$(inherited)", 168 | ); 169 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 170 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 171 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 172 | GCC_WARN_UNUSED_VARIABLE = YES; 173 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 174 | ONLY_ACTIVE_ARCH = YES; 175 | SDKROOT = iphoneos; 176 | }; 177 | name = Debug; 178 | }; 179 | 3E90E0261864F60400155CB7 /* Release */ = { 180 | isa = XCBuildConfiguration; 181 | buildSettings = { 182 | ALWAYS_SEARCH_USER_PATHS = NO; 183 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 184 | CLANG_CXX_LIBRARY = "libc++"; 185 | CLANG_ENABLE_OBJC_ARC = YES; 186 | CLANG_WARN_CONSTANT_CONVERSION = YES; 187 | CLANG_WARN_EMPTY_BODY = YES; 188 | CLANG_WARN_ENUM_CONVERSION = YES; 189 | CLANG_WARN_INT_CONVERSION = YES; 190 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 191 | COPY_PHASE_STRIP = YES; 192 | GCC_C_LANGUAGE_STANDARD = gnu99; 193 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 194 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 195 | GCC_WARN_UNUSED_VARIABLE = YES; 196 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 197 | SDKROOT = iphoneos; 198 | VALIDATE_PRODUCT = YES; 199 | }; 200 | name = Release; 201 | }; 202 | 3E90E0281864F60400155CB7 /* Debug */ = { 203 | isa = XCBuildConfiguration; 204 | buildSettings = { 205 | DSTROOT = /tmp/client_reporting.dst; 206 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 207 | GCC_PREFIX_HEADER = "client-reporting/client-reporting-Prefix.pch"; 208 | OTHER_LDFLAGS = "-ObjC"; 209 | PRODUCT_NAME = "$(TARGET_NAME)"; 210 | SKIP_INSTALL = YES; 211 | }; 212 | name = Debug; 213 | }; 214 | 3E90E0291864F60400155CB7 /* Release */ = { 215 | isa = XCBuildConfiguration; 216 | buildSettings = { 217 | DSTROOT = /tmp/client_reporting.dst; 218 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 219 | GCC_PREFIX_HEADER = "client-reporting/client-reporting-Prefix.pch"; 220 | OTHER_LDFLAGS = "-ObjC"; 221 | PRODUCT_NAME = "$(TARGET_NAME)"; 222 | SKIP_INSTALL = YES; 223 | }; 224 | name = Release; 225 | }; 226 | /* End XCBuildConfiguration section */ 227 | 228 | /* Begin XCConfigurationList section */ 229 | 3E90E0141864F60400155CB7 /* Build configuration list for PBXProject "client-reporting" */ = { 230 | isa = XCConfigurationList; 231 | buildConfigurations = ( 232 | 3E90E0251864F60400155CB7 /* Debug */, 233 | 3E90E0261864F60400155CB7 /* Release */, 234 | ); 235 | defaultConfigurationIsVisible = 0; 236 | defaultConfigurationName = Release; 237 | }; 238 | 3E90E0271864F60400155CB7 /* Build configuration list for PBXNativeTarget "client-reporting" */ = { 239 | isa = XCConfigurationList; 240 | buildConfigurations = ( 241 | 3E90E0281864F60400155CB7 /* Debug */, 242 | 3E90E0291864F60400155CB7 /* Release */, 243 | ); 244 | defaultConfigurationIsVisible = 0; 245 | }; 246 | /* End XCConfigurationList section */ 247 | }; 248 | rootObject = 3E90E0111864F60400155CB7 /* Project object */; 249 | } 250 | -------------------------------------------------------------------------------- /client-reporting/client-reporting.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client-reporting/client-reporting.xcodeproj/project.xcworkspace/xcuserdata/gblack.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/mdm-server/21bb47cf7a3fb7e18cda8ae1a99a367f2ea4c4b5/client-reporting/client-reporting.xcodeproj/project.xcworkspace/xcuserdata/gblack.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /client-reporting/client-reporting.xcodeproj/xcuserdata/gblack.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /client-reporting/client-reporting.xcodeproj/xcuserdata/gblack.xcuserdatad/xcschemes/client-reporting.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 42 | 43 | 44 | 45 | 51 | 52 | 54 | 55 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /client-reporting/client-reporting.xcodeproj/xcuserdata/gblack.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | client-reporting.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3E90E0181864F60400155CB7 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /client-reporting/client-reporting/client-reporting-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'client-reporting' target in the 'client-reporting' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /client-reporting/client-reporting/client_reporting.h: -------------------------------------------------------------------------------- 1 | // 2 | // client_reporting.h 3 | // client-reporting 4 | // 5 | // Created by Black, Gavin S. on 12/20/13. 6 | // Copyright (c) 2013 Black, Gavin S. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | 13 | @interface client_reporting : NSObject 14 | +(void) setHostAddress: (NSString*) host; 15 | +(void) setPause : (BOOL) toggle; 16 | 17 | 18 | +(void) reportJailbreak; 19 | +(void) reportDebugger; 20 | +(void) reportLocation : (CLLocationCoordinate2D*) coords; 21 | 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /client-reporting/client-reporting/client_reporting.m: -------------------------------------------------------------------------------- 1 | // 2 | // client_reporting.m 3 | // client-reporting 4 | // 5 | // Created by Black, Gavin S. on 12/20/13. 6 | // Copyright (c) 2013 Black, Gavin S. All rights reserved. 7 | // 8 | 9 | #import "client_reporting.h" 10 | 11 | @implementation client_reporting 12 | 13 | static NSString* mdmAddress = @""; 14 | static BOOL doPause = NO; 15 | 16 | +(void) setHostAddress: (NSString*) host { 17 | mdmAddress = [NSString stringWithFormat:@"https://%@/reporting/?type=", host]; 18 | } 19 | 20 | +(void) setPause : (BOOL) toggle { 21 | doPause = toggle; 22 | } 23 | 24 | +(void) makeCall : (NSString*) urlStr { 25 | NSURL* url = [NSURL URLWithString:urlStr]; 26 | [NSURLRequest requestWithURL:url]; 27 | if(doPause) [NSThread sleepForTimeInterval:2.5]; 28 | } 29 | 30 | +(void) reportJailbreak { 31 | [self makeCall:[NSString stringWithFormat:@"%@jailbreak", mdmAddress]]; 32 | } 33 | 34 | +(void) reportDebugger { 35 | [self makeCall:[NSString stringWithFormat:@"%@debugger", mdmAddress]]; 36 | } 37 | 38 | +(void) reportLocation : (CLLocationCoordinate2D*) coords { 39 | NSString *latitude = [NSString stringWithFormat:@"%f", coords->latitude]; 40 | NSString *longitude = [NSString stringWithFormat:@"%f", coords->longitude]; 41 | 42 | [self makeCall:[NSString stringWithFormat:@"%@location&lat=%@&lon=%@", mdmAddress, latitude, longitude]]; 43 | } 44 | 45 | 46 | 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /images/certPortal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/mdm-server/21bb47cf7a3fb7e18cda8ae1a99a367f2ea4c4b5/images/certPortal.png -------------------------------------------------------------------------------- /images/commandSuccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/mdm-server/21bb47cf7a3fb7e18cda8ae1a99a367f2ea4c4b5/images/commandSuccess.png -------------------------------------------------------------------------------- /images/deviceEnroll.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/mdm-server/21bb47cf7a3fb7e18cda8ae1a99a367f2ea4c4b5/images/deviceEnroll.jpg -------------------------------------------------------------------------------- /images/enrollConfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/mdm-server/21bb47cf7a3fb7e18cda8ae1a99a367f2ea4c4b5/images/enrollConfig.png -------------------------------------------------------------------------------- /images/enrollExport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/mdm-server/21bb47cf7a3fb7e18cda8ae1a99a367f2ea4c4b5/images/enrollExport.png -------------------------------------------------------------------------------- /references/CommandReference.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/mdm-server/21bb47cf7a3fb7e18cda8ae1a99a367f2ea4c4b5/references/CommandReference.pdf -------------------------------------------------------------------------------- /references/InsideAppleMDM.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/mdm-server/21bb47cf7a3fb7e18cda8ae1a99a367f2ea4c4b5/references/InsideAppleMDM.pdf -------------------------------------------------------------------------------- /scripts/README_WINDOWS.txt: -------------------------------------------------------------------------------- 1 | STEP 1) Download Openssl and add the folder to system path. 2 | Make sure that openSSL can be executed from any directory 3 | 4 | STEP 2) Update server.cnf with your IP address or hostname. Replace with this information. 5 | 6 | Step 3) 7 | Run make_certs.bat 8 | 9 | You will be guided with 8 different steps. 10 | 11 | After this you could follow the other guidelines give. There is no more OS dependancy. 12 | 13 | -------------------------------------------------------------------------------- /scripts/make_certs.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | choice /m "Do you have CA?" 3 | IF ERRORLEVEL 2 GOTO BUILDCA 4 | IF ERRORLEVEL 1 GOTO INBUILTCA 5 | :BUILDCA 6 | 7 | echo 1. Creating Certificate Authority (CA) 8 | echo For 'Common Name' enter something like 'MDM Test CA 9 | openssl req -new -x509 -extensions v3_ca -keyout cakey.key -out cacert.crt -days 365 10 | 11 | :CONT 12 | echo 2. Creating the Web Server private key and certificate request 13 | echo For 'Common Name' enter your server's IP address 14 | openssl genrsa 2048 > server.key 15 | openssl req -new -key server.key -out server.csr 16 | 17 | echo 3. Signing the server key with the CA. You'll the CA passphrase from step 1. 18 | openssl x509 -req -days 365 -in server.csr -CA cacert.crt -CAkey cakey.key -CAcreateserial -out server.crt -extfile .\server.cnf -extensions ssl_server 19 | 20 | echo 4. Creating the device Identity key and certificate request. 21 | openssl genrsa 2048 > identity.key 22 | openssl req -new -key identity.key -out identity.csr 23 | 24 | echo 5. Signing the identity key with the CA. You'll the CA passphrase from step 1. 25 | echo ** Give it a passphrase. You'll need to include that in the IPCU profile. 26 | openssl x509 -req -days 365 -in identity.csr -CA cacert.crt -CAkey cakey.key -CAcreateserial -out identity.crt 27 | openssl pkcs12 -export -out identity.p12 -inkey identity.key -in identity.crt -certfile cacert.crt 28 | 29 | 30 | echo 6. Copying keys and certs to server folder 31 | copy server.key ..\server\Server.key 32 | copy server.crt ..\server\Server.crt 33 | copy cacert.crt ..\server\CA.crt 34 | copy identity.crt ..\server\identity.crt 35 | copy identity.p12 ..\server\Identity.p12 36 | 37 | 38 | #echo 7. Generating keys and certs for plist generation 39 | #openssl req -inform pem -outform der -in identity.csr -out customer.der 40 | ## Rename identity.csr to be used with the iOS Provisioning Portal 41 | #rename identity.csr customer.csr 42 | 43 | #copy Identity.p12 ..\vendor-signing\com\softhinker\vendor.p12 44 | #copy customer.der ..\vendor-signing\com\softhinker\customer.der 45 | 46 | #echo 8. Making the Apple Certificate useble by python 47 | #openssl x509 -inform der -in AppleWWDRCA.cer -out intermediate.pem 48 | #openssl x509 -inform der -in AppleIncRootCertificate.cer -out root.pem 49 | 50 | #cp intermediate.pem ..\..\vendor-signing\com\softhinker\intermediate.pem 51 | #cp root.pem ..\..\vendor-signing\com\softhinker\root.pem 52 | 53 | echo DONE!! 54 | goto end 55 | 56 | :INBUILTCA 57 | echo place CA Certificate and CA Key and then press enter. NOTE: you should have the password of the certificate. 58 | 59 | @pause 60 | goto CONT 61 | 62 | :end 63 | @pause 64 | -------------------------------------------------------------------------------- /scripts/make_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "" 4 | echo "Setting up server.cnf." 5 | echo "Please enter the Hostname or IP address of your server." 6 | read IP 7 | sed -i -e "s//$IP/g" server.cnf 8 | echo "Done." 9 | echo "" 10 | 11 | echo "" 12 | echo "Setting up certificates for MDM server testing!" 13 | echo "" 14 | echo "1. Creating Certificate Authority (CA)" 15 | echo " ** For 'Common Name' enter something like 'MDM Test CA'" 16 | echo " ** Create and a remember the PEM pass phrase for use later on" 17 | echo "" 18 | openssl req -new -x509 -extensions v3_ca -keyout cakey.key -out cacert.crt -days 365 19 | 20 | echo "" 21 | echo "2. Creating the Web Server private key and certificate request" 22 | echo " ** For 'Common Name' enter your server's IP address **" 23 | echo "" 24 | openssl genrsa 2048 > server.key 25 | openssl req -new -key server.key -out server.csr 26 | 27 | echo "" 28 | echo "3. Signing the server key with the CA. You'll use the PEM pass phrase from step 1." 29 | echo "" 30 | openssl x509 -req -days 365 -in server.csr -CA cacert.crt -CAkey cakey.key -CAcreateserial -out server.crt -extfile ./server.cnf -extensions ssl_server 31 | 32 | 33 | 34 | echo "" 35 | echo "4. Creating the device Identity key and certificate request" 36 | echo " ** For 'Common Name' enter something like 'my device'" 37 | echo "" 38 | openssl genrsa 2048 > identity.key 39 | openssl req -new -key identity.key -out identity.csr 40 | 41 | echo "" 42 | echo "5. Signing the identity key with the CA. You'll the PEM pass phrase from step 1." 43 | echo " ** Create an export passphrase. You'll need to include it in the IPCU profile." 44 | echo "" 45 | openssl x509 -req -days 365 -in identity.csr -CA cacert.crt -CAkey cakey.key -CAcreateserial -out identity.crt 46 | openssl pkcs12 -export -out identity.p12 -inkey identity.key -in identity.crt -certfile cacert.crt 47 | 48 | 49 | 50 | echo "" 51 | echo "6. Copying keys and certs to server folder" 52 | # Move relevant certs to the /server/ directory 53 | mv server.key ../server/Server.key 54 | mv server.crt ../server/Server.crt 55 | mv cacert.crt ../server/CA.crt 56 | mv identity.crt ../server/identity.crt 57 | cp identity.p12 ../server/Identity.p12 58 | 59 | ####################################### 60 | # Removed with softhinker vendor code # 61 | ####################################### 62 | 63 | #echo "7. Generating keys and certs for plist generation" 64 | #echo "" 65 | #openssl req -inform pem -outform der -in identity.csr -out customer.der 66 | # Rename identity.csr to be used with the iOS Provisioning Portal 67 | #mv identity.csr customer.csr 68 | 69 | #echo "" 70 | #echo "8. Getting Apple certificates online" 71 | #curl https://developer.apple.com/certificationauthority/AppleWWDRCA.cer -ko AppleWWDRCA.cer 72 | #curl http://www.apple.com/appleca/AppleIncRootCertificate.cer -o AppleIncRootCertificate.cer 73 | #openssl x509 -inform der -in AppleWWDRCA.cer -out intermediate.pem 74 | #openssl x509 -inform der -in AppleIncRootCertificate.cer -out root.pem 75 | 76 | # Move relevant files for use in softhinker vendor-signing 77 | # Need to manually generate and move mdm.pem 78 | #mv intermediate.pem ../vendor-signing/com/softhinker/intermediate.pem 79 | #mv root.pem ../vendor-signing/com/softhinker/root.pem 80 | #mv identity.p12 ../vendor-signing/com/softhinker/vendor.p12 81 | #mv customer.der ../vendor-signing/com/softhinker/customer.der 82 | -------------------------------------------------------------------------------- /scripts/server.cnf: -------------------------------------------------------------------------------- 1 | [ ssl_server ] 2 | basicConstraints = CA:FALSE 3 | nsCertType = server 4 | keyUsage = digitalSignature, keyEncipherment 5 | extendedKeyUsage = serverAuth, nsSGC, msSGC 6 | #nsComment = "OpenSSL Certificate for SSL Web Server" 7 | subjectAltName = IP:,DNS:,URI:https://:8080/ 8 | 9 | -------------------------------------------------------------------------------- /scripts/vendor_signing.bat: -------------------------------------------------------------------------------- 1 | cd ../vendor-signing/com/softhinker 2 | javac -cp "../../lib/dom4j-1.6.1.jar;./" Test.java 3 | cd ../../ 4 | java -cp "./lib/dom4j-1.6.1.jar;./" com.softhinker.Test 5 | -------------------------------------------------------------------------------- /scripts/vendor_signing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd ../vendor-signing/com/softhinker 4 | javac -cp "../../lib/dom4j-1.6.1.jar:./" Test.java 5 | cd ../../ 6 | java -cp "./lib/dom4j-1.6.1.jar:./" com.softhinker.Test 7 | -------------------------------------------------------------------------------- /server/Example.mobileconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PayloadContent 6 | 7 | 8 | PayloadDescription 9 | Configures device restrictions. 10 | PayloadDisplayName 11 | Restrictions 12 | PayloadIdentifier 13 | com.intrepidusgroup.djs.test.simple.restrictions 14 | PayloadOrganization 15 | 16 | PayloadType 17 | com.apple.applicationaccess 18 | PayloadUUID 19 | 77D2FFB1-C010-4220-A3D9-BFE4B3EF99FD 20 | PayloadVersion 21 | 1 22 | allowAddingGameCenterFriends 23 | 24 | allowAppInstallation 25 | 26 | allowCamera 27 | 28 | allowExplicitContent 29 | 30 | allowGlobalBackgroundFetchWhenRoaming 31 | 32 | allowInAppPurchases 33 | 34 | allowMultiplayerGaming 35 | 36 | allowSafari 37 | 38 | allowScreenShot 39 | 40 | allowVideoConferencing 41 | 42 | allowVoiceDialing 43 | 44 | allowYouTube 45 | 46 | allowiTunes 47 | 48 | forceEncryptedBackup 49 | 50 | ratingApps 51 | 1000 52 | ratingMovies 53 | 1000 54 | ratingRegion 55 | us 56 | ratingTVShows 57 | 1000 58 | 59 | 60 | PayloadDescription 61 | Very simple profile to test restrictions. 62 | PayloadDisplayName 63 | Very Simple Restriction Test 64 | PayloadIdentifier 65 | com.intrepidusgroup.djs.test.simple 66 | PayloadOrganization 67 | 68 | PayloadRemovalDisallowed 69 | 70 | PayloadType 71 | Configuration 72 | PayloadUUID 73 | 2D9490F4-E890-4B37-9062-7095FEF0E482 74 | PayloadVersion 75 | 1 76 | 77 | 78 | -------------------------------------------------------------------------------- /server/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2012, Intrepidus Group 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | Neither the name of the authors, nor Intrepidus Group, nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | 30 | -------------------------------------------------------------------------------- /server/Manifest.plist.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | items 4 | 5 | 6 | assets 7 | 8 | 9 | kind 10 | software-package 11 | url 12 | https://*** SERVER_IP ***:8080/app 13 | 14 | 15 | metadata 16 | 17 | bundle-identifier 18 | *** APPLICATION BUNDLE ID (like com.example.myapp) *** 19 | bundle-version 20 | 1.0.0 21 | kind 22 | software 23 | subtitle 24 | 25 | title 26 | *** APP NAME *** 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /server/README: -------------------------------------------------------------------------------- 1 | For more documentation, please see the README file located in the root directory of this repository. 2 | 3 | 4 | -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 5 | Important Files 6 | -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 7 | 8 | 9 | [required, included] 10 | favicon.ico Replace with your website's icon 11 | server.py The server itself 12 | device.py A device class to support the server 13 | xactn.log Log of commands and responses [initially empty] 14 | 15 | certs/make_certs.sh Script to create the various certs and 16 | keys you'll need (uses OpenSSL) 17 | 18 | [required, need to get/create manually] 19 | PushCert.pem Certificate and private key (no passphrase) for APNS 20 | See README in root directory for more instructions 21 | Enroll.mobileconfig Use IPCU to create profile with MDM payload, 22 | used to enroll devices 23 | 24 | 25 | [required, created by make_cert.sh] 26 | CA.crt CA certificate used to sign the server cert 27 | [load onto device] 28 | Server.key Private key (no passphrase) for SSL server 29 | Server.crt Certificate for SSL server 30 | Identity.p12 Device identity cert (for MDM enrollment profile) 31 | 32 | 33 | [optional, to test installing custom apps] 34 | Example.mobileconfig Sample profile to install (disables certain apps, etc.) 35 | MyApp.ipa Bundle for a custom iOS app 36 | MyApp.mobileprovision Mobile provisioning profile for the custom app 37 | Manifest.plist Simple manifest for custom app 38 | [Manifest.plist.template provided] 39 | -------------------------------------------------------------------------------- /server/device.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from plistlib import * 3 | from operator import itemgetter 4 | import time, datetime, copy 5 | 6 | class device: 7 | TIMEOUT = 20 # Number of seconds before command times out 8 | WHITELIST = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 " 9 | def __init__(self, newUDID, tuple): 10 | self.UDID = newUDID 11 | self.IP = tuple[0] 12 | self.pushMagic = tuple[1] 13 | self.deviceToken = tuple[2] 14 | self.unlockToken = tuple[3] 15 | 16 | # Hard coded information to show possible features 17 | self.GEO = "42*21'29''N 71*03'49''W" 18 | 19 | # Owner and location default to unassigned 20 | self.owner = 'Unassigned' 21 | self.location = 'Unassigned' 22 | 23 | self.status = 0 # 0=ready for command (green? gray?) 24 | # 1=command in queue (yellow) 25 | # 2=error/timeout (red) 26 | # maybe have green (last command successful?) 27 | 28 | # Possible additional parameters based on query commands 29 | #self.availableCapacity 30 | #self.totalCapacity 31 | #self.installedApps 32 | 33 | self.name = '' 34 | self.customName = '' 35 | self.model = '' 36 | self.OS = '' 37 | 38 | # Dictionary to hold commands and responses that HAVE been sent 39 | # Keys are Command UUID, value is a dictionary 40 | # {'command', 'response', 'order', 'status'} 41 | self.cmdList = {} 42 | 43 | # Queue to hold commands that HAVE NOT been sent 44 | self.queue = deque() 45 | 46 | 47 | def getUDID(self): 48 | return self.UDID 49 | 50 | def getQueueInfo(self): 51 | # Returns information needed by queue function 52 | return self.pushMagic, self.deviceToken 53 | 54 | def getResponse(self, cmdUUID): 55 | return self.cmdList[cmdUUID]['response'] 56 | 57 | def sortCommands(self): 58 | temp = [] 59 | for key in self.cmdList: 60 | temp.append((self.cmdList[key]['order'], key)) 61 | return sorted(temp, reverse=True) 62 | 63 | 64 | def populate(self): 65 | # Returns info as a dictionary for use with mustache 66 | d = {} 67 | d['UDID'] = self.UDID 68 | if self.customName: 69 | d['name'] = self.customName 70 | else: 71 | d['name'] = self.name 72 | d['ip'] = self.IP 73 | d['owner'] = self.owner 74 | d['location'] = self.location 75 | d['geo'] = self.GEO 76 | d['status'] = ['success', 'warning', 'danger'][self.status] 77 | #d['icon'] = ['ok', 'refresh', 'remove'][self.status] # possible glyphicon functionality 78 | 79 | # Send back 5 most recent commands 80 | temp = self.sortCommands() 81 | d['commands'] = [] 82 | for tuple in temp[:5]: 83 | # Check for commands with variables that are not JSON serializable 84 | if 'UnlockToken' in self.cmdList[tuple[1]]['cmd']['Command']: 85 | # Remove unlocktoken from output 86 | temp_cmd = copy.deepcopy(self.cmdList[tuple[1]]) 87 | temp_cmd['cmd']['Command']['UnlockToken'] = 'Redacted by server' 88 | d['commands'].append(temp_cmd) 89 | elif 'CertificateList' in self.cmdList[tuple[1]]['response']: 90 | # Remove CertificateList data from output 91 | # TODO: Possibly implement some other method of delivering certificate data 92 | temp_cmd = copy.deepcopy(self.cmdList[tuple[1]]) 93 | for i in range(len(temp_cmd['response']['CertificateList'])): 94 | temp_cmd['response']['CertificateList'][i]['Data'] = 'Redacted by server' 95 | d['commands'].append(temp_cmd) 96 | elif 'ProfileList' in self.cmdList[tuple[1]]['response']: 97 | # Remove SignerCertificates data from output 98 | # Note that some profiles may not have SignerCertificates, in which case this 99 | # will add it to the ProfileList, though only for response output on server 100 | temp_cmd = copy.deepcopy(self.cmdList[tuple[1]]) 101 | for i in range(len(temp_cmd['response']['ProfileList'])): 102 | temp_cmd['response']['ProfileList'][i]['SignerCertificates'] = 'Redacted by server' 103 | d['commands'].append(temp_cmd) 104 | elif 'ProvisioningProfileList' in self.cmdList[tuple[1]]['response']: 105 | # Change ExpiryDate datetime objects to UTC strings for output 106 | temp_cmd = copy.deepcopy(self.cmdList[tuple[1]]) 107 | for i in range(len(temp_cmd['response']['ProvisioningProfileList'])): 108 | temp_cmd['response']['ProvisioningProfileList'][i]['ExpiryDate'] = temp_cmd['response']['ProvisioningProfileList'][i]['ExpiryDate'].strftime("%Y-%m-%d %H:%M:%S") + " UTC" 109 | d['commands'].append(temp_cmd) 110 | elif 'InstallProfile' == self.cmdList[tuple[1]]['cmd']['Command']['RequestType']: 111 | #Remove payload data from output 112 | temp_cmd = copy.deepcopy(self.cmdList[tuple[1]]) 113 | temp_cmd['cmd']['Command']['Payload'] = 'Redacted by server' 114 | d['commands'].append(temp_cmd) 115 | 116 | else: 117 | d['commands'].append(self.cmdList[tuple[1]]) 118 | 119 | return d 120 | 121 | def sanitize(self, string): 122 | # Function to remove any non-alphanumeric characters from input 123 | wl = set(self.WHITELIST) 124 | 125 | for char in set(string)-wl: 126 | string = string.replace(char, ''); 127 | 128 | return string[:32] 129 | 130 | def updateMetadata(self, newName, newOwner, newLocation): 131 | # Fuction for customizable metadata 132 | if newName: 133 | self.customName = self.sanitize(newName.strip()) 134 | if newOwner: 135 | self.owner = self.sanitize(newOwner.strip()) 136 | if newLocation: 137 | self.location = self.sanitize(newLocation.strip()) 138 | 139 | 140 | def updateInfo(self, newName, newModel, newOS): 141 | # Update class variables with data from DeviceInformation 142 | self.name = newName 143 | self.model = newModel 144 | self.OS = newOS 145 | 146 | def reenroll(self, newIP, newPush, newUnlock): 147 | self.IP = newIP 148 | self.pushMagic = newPush 149 | self.unlockToken = newUnlock 150 | 151 | def addCommand(self, cmd): 152 | # Add a new command to the queue 153 | # Update status to show command pending 154 | self.status = 1 155 | cmd['TimeStamp'] = time.time() 156 | 157 | # Update command with unlockToken if necessary 158 | if cmd['Command']['RequestType'] == 'ClearPasscode': 159 | cmd['Command']['UnlockToken'] = Data(self.unlockToken) 160 | 161 | print "ADDED COMMAND TO QUEUE:", cmd['CommandUUID'] 162 | self.queue.append(cmd) 163 | 164 | def sendCommand(self): 165 | # Pop command off queue to be sent to the device 166 | if len(self.queue) == 0: 167 | print "**No commands left in queue" 168 | return '' 169 | 170 | cmd = self.queue.popleft() 171 | self.cmdList[cmd['CommandUUID']] = {} 172 | self.cmdList[cmd['CommandUUID']]['cmd'] = cmd 173 | self.cmdList[cmd['CommandUUID']]['response'] = '' 174 | self.cmdList[cmd['CommandUUID']]['status'] = 'warning' 175 | self.cmdList[cmd['CommandUUID']]['order'] = len(self.cmdList.keys()) 176 | print "**Sending command", cmd['CommandUUID'], "and moving it from queue**" 177 | return cmd 178 | 179 | 180 | def addResponse(self, cmdUUID, response): 181 | # Add a response to correspond with a previous command 182 | print "**ADDING RESPONSE TO CMD:", cmdUUID 183 | self.cmdList[cmdUUID]['response'] = response 184 | # Check response for success/failure 185 | if response['Status'] == 'Acknowledged': 186 | self.cmdList[cmdUUID]['status'] = 'success' 187 | self.status = 0 188 | elif response['Status'] == 'Error': 189 | self.cmdList[cmdUUID]['status'] = 'danger' 190 | self.status = 2 191 | 192 | def checkTimeout(self): 193 | # Checks for command timeout 194 | now = time.time() 195 | 196 | # If we have no commands waiting, we're good 197 | if self.status != 1: 198 | return 199 | 200 | # Check queue for timed out commands 201 | if len(self.queue) > 0: 202 | for cmd in self.queue: 203 | if now - cmd['TimeStamp'] > self.TIMEOUT: 204 | # Command has time out, add it to cmd list with an error 205 | self.status = 2 206 | self.queue.remove(cmd) 207 | self.cmdList[cmd['CommandUUID']] = {} 208 | self.cmdList[cmd['CommandUUID']]['cmd'] = cmd 209 | self.cmdList[cmd['CommandUUID']]['response'] = {'Status':'TimeoutError'} 210 | self.cmdList[cmd['CommandUUID']]['status'] = 'danger' 211 | self.cmdList[cmd['CommandUUID']]['order'] = len(self.cmdList.keys()) 212 | return 213 | 214 | # Check command list for timed out commands 215 | for commandUUID in self.cmdList: 216 | if self.cmdList[commandUUID]['response'] == "" and now-self.cmdList[commandUUID]['cmd']['TimeStamp'] > self.TIMEOUT: 217 | self.status = 2 218 | print('cmdList is: %s' % self.cmdList) 219 | 220 | if self.cmdList.get('Command'): 221 | print('cmdList command is: %s' % self.cmdList.get('Command')) 222 | self.cmdList[Command['cmd']['CommandUUID']]['status'] = 'danger' 223 | self.cmdList[Command['cmd']['CommandUUID']]['response'] = {'Status':'TimeoutError'} 224 | -------------------------------------------------------------------------------- /server/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macadmins/mdm-server/21bb47cf7a3fb7e18cda8ae1a99a367f2ea4c4b5/server/favicon.ico -------------------------------------------------------------------------------- /server/problems.py: -------------------------------------------------------------------------------- 1 | problems = [] 2 | -------------------------------------------------------------------------------- /server/server.py: -------------------------------------------------------------------------------- 1 | import web, os, json, uuid, sys 2 | import cPickle as pickle 3 | from device import device # Custom device class 4 | #from plistlib import * 5 | from APNSWrapper import * 6 | from problems import * 7 | from datetime import datetime 8 | from time import sleep 9 | # needed to handle verification of signed messages from devices 10 | from M2Crypto import SMIME, X509, BIO, m2 11 | from biplist import * 12 | 13 | # 14 | # Simple, basic, bare-bones example test server 15 | # Implements Apple's Mobile Device Management (MDM) protocol 16 | # Compatible with iOS 4.x devices 17 | # 18 | # 19 | # David Schuetz, Senior Consultant, Intrepidus Group 20 | # 21 | # Copyright 2011, Intrepidus Group 22 | # http://intrepidusgroup.com 23 | 24 | # Reuse permitted under terms of BSD License (see LICENSE file). 25 | # No warranties, expressed or implied. 26 | # This is experimental software, for research only. Use at your own risk. 27 | 28 | # 29 | # Revision History: 30 | # 31 | # * August 2011 - initial release, Black Hat USA 32 | # * January 2012 - minor tweaks, including favicon, useful README, and 33 | # scripts to create certs, log file, etc. 34 | # * January 2012 - Added support for some iOS 5 functions. ShmooCon 8. 35 | # * February 2012 - Can now verify signed messages from devices 36 | # - Tweaks to CherryPy startup to avoid errors on console 37 | # * January 2014 - Support for multiple enrollments 38 | # - Supports reporting problems 39 | # * April 2014 - Support for new front end 40 | # - Tweaks and bug fixes 41 | # * May 2014 - New device class 42 | # - Rework server to use device class 43 | # - Fixes a number of problems with using multiple devices 44 | # - Support for new device-based front end 45 | 46 | 47 | # Global variable setup 48 | LOGFILE = 'xactn.log' 49 | devicesAwaitingConfiguration = {} 50 | 51 | # Dummy socket to get the hostname 52 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 53 | s.connect(('8.8.8.8', 0)) 54 | 55 | # NOTE: Will need to overwrite this if behind a firewall 56 | MY_ADDR = s.getsockname()[0] + ":8080" 57 | 58 | # Set up some smime objects to verify signed messages coming from devices 59 | sm_obj = SMIME.SMIME() 60 | x509 = X509.load_cert('identity.crt') 61 | sk = X509.X509_Stack() 62 | sk.push(x509) 63 | sm_obj.set_x509_stack(sk) 64 | 65 | st = X509.X509_Store() 66 | st.load_info('CA.crt') 67 | sm_obj.set_x509_store(st) 68 | 69 | 70 | ########################################################################### 71 | # Update this to match the UUID in the test provisioning profiles, in order 72 | # to demonstrate removal of the profile 73 | 74 | my_test_provisioning_uuid = 'REPLACE-ME-WITH-REAL-UUIDSTRING' 75 | 76 | from web.wsgiserver import CherryPyWSGIServer 77 | 78 | # Python 2.7 requires the PyOpenSSL library 79 | # Python 3.x should use be able to use the default python SSL 80 | try: 81 | from OpenSSL import SSL 82 | from OpenSSL import crypto 83 | except ImportError: 84 | SSL = None 85 | 86 | 87 | CherryPyWSGIServer.ssl_certificate = "Server.crt" 88 | CherryPyWSGIServer.ssl_private_key = "Server.key" 89 | 90 | ########################################################################### 91 | 92 | device_list = dict() 93 | 94 | global mdm_commands 95 | 96 | urls = ( 97 | '/', 'root', 98 | '/queue', 'queue_cmd_post', 99 | '/checkin', 'do_mdm', 100 | '/server', 'do_mdm', 101 | '/ServerURL', 'do_mdm', 102 | '/CheckInURL', 'do_mdm', 103 | '/enroll', 'enroll_profile', 104 | '/ca', 'mdm_ca', 105 | '/favicon.ico', 'favicon', 106 | '/manifest', 'app_manifest', 107 | '/app', 'app_ipa', 108 | '/problem', 'do_problem', 109 | '/problemjb', 'do_problem', 110 | '/poll', 'poll', 111 | '/getcommands', 'get_commands', 112 | '/devices', 'dev_tab', 113 | '/response', 'get_response', 114 | '/metadata', 'metadata', 115 | ) 116 | 117 | 118 | 119 | def setup_commands(): 120 | # Function to generate dictionary of valid commands 121 | global my_test_provisioning_uuid 122 | 123 | ret_list = dict() 124 | 125 | for cmd in ['DeviceLock', 'ProfileList', 'Restrictions', 126 | 'CertificateList', 'InstalledApplicationList', 127 | 'ProvisioningProfileList','DeviceConfigured', 128 | # new for iOS 5: 129 | 'ManagedApplicationList',]: 130 | ret_list[cmd] = dict( Command = dict( RequestType = cmd )) 131 | 132 | ret_list['SecurityInfo'] = dict( 133 | Command = dict( 134 | RequestType = 'SecurityInfo', 135 | Queries = [ 136 | 'HardwareEncryptionCaps', 'PasscodePresent', 137 | 'PasscodeCompliant', 'PasscodeCompliantWithProfiles', 138 | ] 139 | ) 140 | ) 141 | 142 | ret_list['DeviceInformation'] = dict( 143 | Command = dict( 144 | RequestType = 'DeviceInformation', 145 | Queries = [ 146 | 'AvailableDeviceCapacity', 'AwaitingConfiguration', 147 | 'BluetoothMAC', 'BuildVersion', 148 | 'CarrierSettingsVersion', 'CurrentCarrierNetwork', 149 | 'CurrentMCC', 'CurrentMNC', 'DataRoamingEnabled', 150 | 'DeviceCapacity', 'DeviceName', 'ICCID', 'IMEI', 'IsRoaming', 151 | 'IsCloudBackupEnabled', 'IsActivationLockEnabled', 'IsDoNotDisturbInEffect', 152 | 'Model', 'ModelName', 'ModemFirmwareVersion', 'OSVersion', 153 | 'OSUpdateSettings', 'LocalHostName', 'HostName', 154 | 'PhoneNumber', 'Product', 'ProductName', 'SIMCarrierNetwork', 155 | 'SIMMCC', 'SIMMNC', 'SerialNumber', 'UDID', 'WiFiMAC', 'UDID', 156 | 'UnlockToken', 'MEID', 'CellularTechnology', 'BatteryLevel', 157 | 'SubscriberCarrierNetwork', 'VoiceRoamingEnabled', 158 | 'SubscriberMCC', 'SubscriberMNC', 'DataRoaming', 'VoiceRoaming', 159 | 'JailbreakDetected' 160 | ] 161 | ) 162 | ) 163 | 164 | ret_list['ClearPasscode'] = dict( 165 | Command = dict( 166 | RequestType = 'ClearPasscode', 167 | # When ClearPasscode is used, the device specific unlock token needs to be added 168 | # UnlockToken = Data(my_UnlockToken) 169 | ) 170 | ) 171 | 172 | # commented out, and command string changed, to avoid accidentally 173 | # erasing test devices. 174 | # 175 | # ret_list['EraseDevice'] = dict( 176 | # Command = dict( 177 | # RequestType = 'DONT_EraseDevice', 178 | # ) 179 | # ) 180 | # 181 | if 'Example.mobileconfig' in os.listdir('.'): 182 | my_test_cfg_profile = open('Example.mobileconfig', 'rb').read() 183 | pl = readPlistFromString(my_test_cfg_profile) 184 | 185 | 186 | 187 | ret_list['InstallProfile'] = dict( 188 | Command = dict( 189 | RequestType = 'InstallProfile', 190 | Payload = pl 191 | ) 192 | ) 193 | 194 | ret_list['RemoveProfile'] = dict( 195 | Command = dict( 196 | RequestType = 'RemoveProfile', 197 | Identifier = pl['PayloadIdentifier'] 198 | ) 199 | ) 200 | 201 | else: 202 | print "Can't find Example.mobileconfig in current directory." 203 | 204 | 205 | if 'MyApp.mobileprovision' in os.listdir('.'): 206 | my_test_prov_profile = open('MyApp.mobileprovision', 'rb').read() 207 | 208 | ret_list['InstallProvisioningProfile'] = dict( 209 | Command = dict( 210 | RequestType = 'InstallProvisioningProfile', 211 | ProvisioningProfile = Data(my_test_prov_profile) 212 | ) 213 | ) 214 | 215 | ret_list['RemoveProvisioningProfile'] = dict( 216 | Command = dict( 217 | RequestType = 'RemoveProvisioningProfile', 218 | # need an ASN.1 parser to snarf the UUID out of the signed profile 219 | UUID = my_test_provisioning_uuid 220 | ) 221 | ) 222 | 223 | else: 224 | print "Can't find MyApp.mobileprovision in current directory." 225 | 226 | # 227 | # iOS 5: 228 | # 229 | ret_list['InstallApplication'] = dict( 230 | Command = dict( 231 | RequestType = 'InstallApplication', 232 | ManagementFlags = 4, # do not delete app when unenrolling from MDM 233 | iTunesStoreID=471966214, # iTunes Movie Trailers 234 | )) 235 | 236 | # Load PBKDF2-SHA512 password hash for automatic account creation during 237 | # SetupConfiguration stage in Setup Assistant 238 | my_password_hash_plist = open('shadowhash.plist', 'rb').read() 239 | 240 | ret_list['SetupConfiguration'] = dict( 241 | Command = { 242 | 'RequestType': 'SetupConfiguration', 243 | 'SkipPrimarySetupAccountCreation': True, 244 | 'AdminAccount': { 245 | 'shortName': 'admin', 246 | 'fullName': 'System Administrator', 247 | 'passwordHash': Data(my_password_hash_plist) 248 | } 249 | }) 250 | 251 | # Manifest.plist is just a trigger to make this command available, it is not 252 | # sent to the client, instead it is loaded from the ManifestURL instead 253 | if ('Manifest.plist' in os.listdir('.')): 254 | ret_list['InstallManagementTools'] = dict( 255 | Command = dict( 256 | RequestType = 'InstallApplication', 257 | ManifestURL = 'https://myhost.org/Manifest.plist', 258 | ManagementFlags = 1, # delete app when unenrolling from MDM 259 | Options = {'NotManaged': True}, 260 | )) 261 | print ret_list['InstallManagementTools'] 262 | else: 263 | print "Need Manifest.plist to enable InstallManagementTools." 264 | 265 | 266 | ret_list['RemoveApplication'] = dict( 267 | Command = dict( 268 | RequestType = 'RemoveApplication', 269 | Identifier = 'com.apple.movietrailers', 270 | )) 271 | 272 | ret_list['RemoveCustomApplication'] = dict( 273 | Command = dict( 274 | RequestType = 'RemoveApplication', 275 | Identifier = 'mitre.managedTest', 276 | )) 277 | 278 | # 279 | # on an ipad, you'll likely get errors for the "VoiceRoaming" part. 280 | # Since, you know...it's not a phone. 281 | # 282 | ret_list['Settings'] = dict( 283 | Command = dict( 284 | RequestType = 'Settings', 285 | Settings = [ 286 | dict( 287 | Item = 'DataRoaming', 288 | Enabled = False, 289 | ), 290 | dict( 291 | Item = 'VoiceRoaming', 292 | Enabled = True, 293 | ), 294 | ] 295 | )) 296 | 297 | # 298 | # haven't figured out how to make this one work yet. :( 299 | # 300 | # ret_list['ApplyRedemptionCode'] = dict( 301 | # Command = dict( 302 | # RequestType = 'ApplyRedemptionCode', 303 | ## do I maybe need to add an iTunesStoreID in here? 304 | # RedemptionCode = '3WABCDEFGXXX', 305 | # iTunesStoreID=471966214, # iTunes Movie Trailers 306 | # ManagementFlags = 1, 307 | # )) 308 | 309 | 310 | print ret_list.keys() 311 | return ret_list 312 | 313 | def processSignedPlist(infile): 314 | certstore_path = "/etc/ssl/certs/ca-certificates.crt" 315 | file_descriptor = infile 316 | input_bio = BIO.MemoryBuffer(file_descriptor) 317 | signer = SMIME.SMIME() 318 | cert_store = X509.X509_Store() 319 | cert_store.load_info(certstore_path) 320 | signer.set_x509_store(cert_store) 321 | try: 322 | p7 = SMIME.PKCS7(m2.pkcs7_read_bio_der(input_bio._ptr()), 1) 323 | except SMIME.SMIME_Error, e: 324 | logging.error('load pkcs7 error: ' + str(e)) 325 | sk3 = p7.get0_signers(X509.X509_Stack()) 326 | signer.set_x509_stack(sk3) 327 | data_bio = None 328 | 329 | content = signer.verify(p7, data_bio, flags=SMIME.PKCS7_NOVERIFY) 330 | pl = readPlistFromString(content) 331 | 332 | return pl 333 | 334 | class root: 335 | def GET(self): 336 | return web.redirect("/static/index.html") 337 | 338 | def queue(cmd, dev_UDID): 339 | # Function to add a command to a device queue 340 | global device_list, mdm_commands 341 | 342 | mylocal_PushMagic, mylocal_DeviceToken = device_list[dev_UDID].getQueueInfo() 343 | 344 | cmd_data = mdm_commands[cmd] 345 | cmd_data['CommandUUID'] = str(uuid.uuid4()) 346 | 347 | # Have to search through device_list using pushmagic or devtoken to get UDID 348 | for key in device_list: 349 | if device_list[key].UDID == dev_UDID: 350 | if 'InstallProfile' in cmd_data['Command']['RequestType']: 351 | pl = cmd_data['Command']['Payload'] 352 | if 'Active Directory' in pl['PayloadDisplayName']: 353 | pl['PayloadContent'][0]['ClientID'] = dev_UDID[:12] 354 | cmd_data['Command']['Payload'] = Data(writePlistToString(pl)) 355 | 356 | device_list[key].addCommand(cmd_data) 357 | print "*Adding CMD:", cmd_data['CommandUUID'], "to device:", key 358 | break 359 | 360 | store_devices() 361 | 362 | 363 | # Send request to Apple 364 | wrapper = APNSNotificationWrapper('PushCert.pem', False) 365 | message = APNSNotification() 366 | message.token(mylocal_DeviceToken) 367 | message.appendProperty(APNSProperty('mdm', mylocal_PushMagic)) 368 | wrapper.append(message) 369 | wrapper.notify() 370 | 371 | 372 | class queue_cmd_post: 373 | def POST(self): 374 | global device_list 375 | 376 | i = json.loads(web.data()) 377 | cmd = i.pop("cmd", []) 378 | dev = i.pop("dev[]", []) 379 | 380 | for UDID in dev: 381 | queue(cmd, UDID) 382 | 383 | # Update page - currently not using update() 384 | #return update() 385 | return 386 | 387 | 388 | class do_mdm: 389 | def PUT(self): 390 | global sm_obj, device_list 391 | HIGH='' 392 | LOW='' 393 | NORMAL='' 394 | 395 | i = web.data() 396 | pl = readPlistFromString(i) 397 | 398 | print pl 399 | print web.ctx.environ 400 | 401 | if 'HTTP_MDM_SIGNATURE' in web.ctx.environ: 402 | raw_sig = web.ctx.environ['HTTP_MDM_SIGNATURE'] 403 | cooked_sig = '\n'.join(raw_sig[pos:pos+76] for pos in xrange(0, len(raw_sig), 76)) 404 | 405 | signature = '\n-----BEGIN PKCS7-----\n%s\n-----END PKCS7-----\n' % cooked_sig 406 | 407 | # Verify client signature - necessary? 408 | buf = BIO.MemoryBuffer(signature) 409 | p7 = SMIME.load_pkcs7_bio(buf) 410 | data_bio = BIO.MemoryBuffer(i) 411 | try: 412 | v = sm_obj.verify(p7, data_bio) 413 | if v: 414 | print "Client signature verified." 415 | except: 416 | print "*** INVALID CLIENT MESSAGE SIGNATURE ***" 417 | 418 | print "%sReceived %4d bytes: %s" % (HIGH, len(web.data()), NORMAL), 419 | 420 | if pl.get('Status') == 'Idle': 421 | print HIGH + "Idle Status" + NORMAL 422 | 423 | print "*FETCHING CMD TO BE SENT FROM DEVICE:", pl['UDID'] 424 | rd = device_list[pl['UDID']].sendCommand() 425 | 426 | # If no commands in queue, return empty string to avoid infinite idle loop 427 | if(not rd): 428 | return '' 429 | 430 | print "%sSent: %s%s" % (HIGH, rd['Command']['RequestType'], NORMAL) 431 | 432 | elif pl.get('MessageType') == 'TokenUpdate': 433 | print HIGH+"Token Update"+NORMAL 434 | rd = do_TokenUpdate(pl) 435 | print HIGH+"Device Enrolled!"+NORMAL 436 | 437 | elif pl.get('Status') == 'Acknowledged': 438 | global devicesAwaitingConfiguration 439 | print HIGH+"Acknowledged"+NORMAL 440 | rd = dict() 441 | # A command has returned a response 442 | # Add the response to the given device 443 | print "*CALLING ADD RESPONSE TO CMD:", pl['CommandUUID'] 444 | device_list[pl['UDID']].addResponse(pl['CommandUUID'], pl) 445 | 446 | # If we grab device information, we should also update the device info 447 | if pl.get('QueryResponses'): 448 | print "DeviceInformation should update here..." 449 | p = pl['QueryResponses'] 450 | device_list[pl['UDID']].updateInfo(p['DeviceName'], p['ModelName'], p['OSVersion']) 451 | 452 | if pl.get('RequestType') and 'SetupConfiguration' in pl.get('RequestType'): 453 | print HIGH+"Device completed SetupConfiguration command!"+NORMAL 454 | print HIGH+"Sending DeviceConfigured command now."+NORMAL 455 | 456 | queue('DeviceConfigured', pl['UDID']) 457 | 458 | print HIGH+"Removing device from devicesAwaitingConfiguration list..."+NORMAL 459 | devicesAwaitingConfiguration[pl['UDID']]['status'] = 'DeviceConfigured' 460 | devicesAwaitingConfiguration[pl['UDID']]['mtime'] = int(datetime.now().strftime('%s')) 461 | print HIGH+"Removed device from devicesAwaitingConfiguration list"+NORMAL 462 | 463 | if pl.get('RequestType') and 'DeviceConfigured' in pl.get('RequestType'): 464 | devicesAwaitingConfiguration[pl['UDID']] = { 'status': 'InstallApplication', 465 | 'mtime': int(datetime.now().strftime('%s'))} 466 | print HIGH+"Sending InstallManagementTools command now."+NORMAL 467 | sleep(5) 468 | queue('InstallManagementTools', pl['UDID']) 469 | sleep(5) 470 | 471 | if pl.get('RequestType') and 'InstallApplication' in pl.get('RequestType'): 472 | devicesAwaitingConfiguration[pl['UDID']] = { 'status': 'AllDone', 473 | 'mtime': int(datetime.now().strftime('%s'))} 474 | sleep(5) 475 | 476 | # Update pickle file with new response 477 | store_devices() 478 | else: 479 | rd = dict() 480 | if pl.get('MessageType') == 'Authenticate': 481 | print HIGH+"Authenticate"+NORMAL 482 | elif pl.get('MessageType') == 'CheckOut': 483 | print HIGH+"Device leaving MDM"+ NORMAL 484 | elif pl.get('Status') == 'Error': 485 | print "*CALLING ADD RESPONSE WITH ERROR TO CMD:", pl['CommandUUID'] 486 | device_list[pl['UDID']].addResponse(pl['CommandUUID'], pl) 487 | elif pl.get('Status') == 'NotNow': 488 | print "*CALLING ADD RESPONSE WITH NotNow TO CMD:", pl['CommandUUID'] 489 | device_list[pl['UDID']].addResponse(pl['CommandUUID'], pl) 490 | 491 | now = int(datetime.now().strftime('%s')) 492 | mtime = devicesAwaitingConfiguration[pl['UDID']]['mtime'] 493 | 494 | queue('DeviceInformation', pl['UDID']) 495 | 496 | if now - mtime > 5: 497 | if 'InstallApplication' in devicesAwaitingConfiguration[udid]['status']: 498 | print "*Sending command %s again to device %s" % (pl['RequestType'], pl['UDID']) 499 | devicesAwaitingConfiguration[pl['UDID']]['mtime'] = now 500 | queue('InstallApplication', pl['UDID']) 501 | else: 502 | print HIGH+"(other)"+NORMAL 503 | print HIGH, pl, NORMAL 504 | log_data(pl) 505 | log_data(rd) 506 | 507 | out = writePlistToString(rd) 508 | #print LOW, out, NORMAL 509 | 510 | return out 511 | 512 | # POST method is called by DEP-enrolled clients as their first checkin. 513 | # Included in the POST is a signed plist containing client identifying keys: 514 | # LANGUAGE (en_us) 515 | # PRODUCT (MacBookAir5,1) 516 | # SERIAL (C02DEADBEEF2) 517 | # UDID (as reported by System Profiler "Hardware UUID") 518 | # VERSION (15C50) 519 | def POST(self): 520 | 521 | profile = web.data() 522 | 523 | clientID = processSignedPlist(profile) 524 | 525 | print "Received clientID profile:\n\n%s" % clientID 526 | 527 | devicesAwaitingConfiguration[clientID['UDID']] = { 'status': 'CheckinPost', 528 | 'mtime': int(datetime.now().strftime('%s'))} 529 | 530 | # Send the enrollment profile, currently hardcoded 531 | if 'MDM.mobileconfig' in os.listdir('.'): 532 | web.header('Content-Type', 'application/x-apple-aspen-config;charset=utf-8') 533 | web.header('Content-Disposition', 'attachment;filename="MDM.mobileconfig"') 534 | return open('MDM.mobileconfig', "rb").read() 535 | else: 536 | raise web.notfound() 537 | 538 | 539 | class get_commands: 540 | def POST(self): 541 | # Function to return static list of commands to the front page 542 | # Should be called once by document.ready 543 | global mdm_commands 544 | 545 | drop_list = [] 546 | for key in sorted(mdm_commands.iterkeys()): 547 | drop_list.append([key, key]) 548 | return json.dumps(drop_list) 549 | 550 | 551 | def update(): 552 | 553 | # DEPRICATED 554 | # Current polling endpoint is /devices 555 | # May be updated later on for intelligent updating of devices 556 | 557 | # Function to update devices on the frontend 558 | # Is called on page load and polling 559 | 560 | global problems, device_list 561 | 562 | # Create list of devices 563 | dev_list_out = [] 564 | for UDID in device_list: 565 | dev_list_out.append([device_list[UDID].IP, device_list[UDID].pushMagic]) 566 | 567 | # Format output as a dict and then return as JSON 568 | out = dict() 569 | out['dev_list'] = dev_list_out 570 | out['problems'] = '
'.join(problems) 571 | 572 | return json.dumps(out) 573 | 574 | 575 | class poll: 576 | def POST(self): 577 | 578 | # DEPRICATED 579 | # Current polling endpoint is /devices 580 | # May be updated later on for intelligent updating of devices 581 | 582 | 583 | # Polling function to update page with new data 584 | return update() 585 | 586 | 587 | class get_response: 588 | def POST(self): 589 | # Endpoint to return a reponse given a UDID and command UUID 590 | global device_list 591 | 592 | i = json.loads(web.data()) 593 | try: 594 | return device_list[i['UDID']].getResponse(i['UUID']) 595 | except: 596 | print "ERROR: Unable to lookup response by command UUID" 597 | return "ERROR: Unable to retrieve response" 598 | 599 | 600 | class dev_tab: 601 | def POST(self): 602 | # Endpoint to return list of devices with a list of device info 603 | global device_list 604 | devices = [] 605 | 606 | date_handler = lambda obj: ( 607 | obj.isoformat() 608 | if isinstance(obj, datetime) 609 | or isinstance(obj, date) 610 | else None 611 | ) 612 | 613 | for key in device_list: 614 | device_list[key].checkTimeout() 615 | devices.append(device_list[key].populate()) 616 | 617 | # A device-sorting functionality could happen here 618 | 619 | out = {} 620 | out['devices'] = devices 621 | 622 | # Shoe-horn the AwaitingConfiguration check in here since it's a very 623 | # crude polling loop as long as we have the devices page up in a browser 624 | for udid in devicesAwaitingConfiguration: 625 | 626 | print "Current status and mtime: %s - %i" % (devicesAwaitingConfiguration[udid]['status'], 627 | devicesAwaitingConfiguration[udid]['mtime']) 628 | 629 | now = int(datetime.now().strftime('%s')) 630 | mtime = devicesAwaitingConfiguration[udid]['mtime'] 631 | 632 | if now - mtime > 5: 633 | if 'AwaitingConfiguration' in devicesAwaitingConfiguration[udid]['status']: 634 | queue('SetupConfiguration', udid) 635 | # elif 'InstallApplication' in devicesAwaitingConfiguration[udid]['status']: 636 | # queue('InstallApplication', udid) 637 | 638 | # return JSON, catch datetime types with date_handler because JSON can't handle them 639 | return json.dumps(out, encoding='latin-1', default=date_handler) 640 | 641 | 642 | class metadata: 643 | def POST(self): 644 | # Endpoint to update device metadata 645 | global device_list 646 | 647 | i = json.loads(web.data()) 648 | 649 | device_list[i['UDID']].updateMetadata(i['name'], i['owner'], i['location']) 650 | 651 | store_devices() 652 | 653 | return 654 | 655 | 656 | def store_devices(): 657 | # Function to convert the device list and write to a file 658 | global device_list 659 | 660 | print "STORING DEVICES..." 661 | 662 | # Use pickle to store list of devices 663 | pickle.dump(device_list, file('devicelist.pickle', 'w')) 664 | 665 | 666 | def read_devices(): 667 | # Function to open and read the device list 668 | # Is called when the server loads 669 | global device_list 670 | 671 | try: 672 | device_list = pickle.load(file('devicelist.pickle')) 673 | print "LOADED PICKLE" 674 | except: 675 | print "NO DATA IN PICKLE FILE or PICKLE FILE DOES NOT EXIST" 676 | # Creating new pickle file if need be 677 | open('devicelist.pickle', 'a').close() 678 | 679 | 680 | def do_TokenUpdate(pl): 681 | global mdm_commands, devicesAwaitingConfiguration 682 | 683 | print "CONTENTS OF TokenUpdate plist:\n\n", pl 684 | my_PushMagic = pl['PushMagic'] 685 | print dir(pl['Token']) 686 | # my_DeviceToken = pl['Token'].data 687 | my_DeviceToken = pl['Token'] 688 | if pl.get('UnlockToken'): 689 | my_UnlockToken = pl['UnlockToken'].data 690 | else: 691 | my_UnlockToken = pl['Token'] 692 | 693 | # Check whether the device doing the TokenUpdate is currently awaiting 694 | # configuration via the AwaitingConfiguration key and if so add to list 695 | if pl.get('AwaitingConfiguration'): 696 | if pl.get('UDID') in devicesAwaitingConfiguration: 697 | devicesAwaitingConfiguration[pl['UDID']] = { 'status': 'AwaitingConfiguration', 698 | 'mtime': int(datetime.now().strftime('%s'))} 699 | 700 | newTuple = (web.ctx.ip, my_PushMagic, my_DeviceToken, my_UnlockToken) 701 | 702 | print "NEW DEVICE UDID:", pl.get('UDID') 703 | # A new device has enrolled, add a new device 704 | if pl.get('UDID') not in device_list: 705 | # Device does not already exist, create new instance of device 706 | device_list[pl.get('UDID')] = device(pl['UDID'], newTuple) 707 | print "ADDING DEVICE TO DEVICE_LIST" 708 | else: 709 | # Device exists, update information - token stays the same 710 | device_list[pl['UDID']].reenroll(web.ctx.ip, my_PushMagic, my_UnlockToken) 711 | print "DEVICE ALREADY EXISTS, UPDATE INFORMATION" 712 | 713 | 714 | # Queue a DeviceInformation command to populate fields in device_list 715 | # Is this command causing an off-by-one error with commands? 716 | queue('DeviceInformation', pl['UDID']) 717 | 718 | # Store devices in a file for persistence 719 | store_devices() 720 | 721 | # Return empty dictionary for use in do_mdm 722 | return dict() 723 | 724 | 725 | class enroll_profile: 726 | def GET(self): 727 | # Enroll an iPad/iPhone/iPod when requested 728 | if 'Enroll.mobileconfig' in os.listdir('.'): 729 | web.header('Content-Type', 'application/x-apple-aspen-config;charset=utf-8') 730 | web.header('Content-Disposition', 'attachment;filename="MDM.mobileconfig"') 731 | return open('MDM.mobileconfig', "rb").read() 732 | else: 733 | raise web.notfound() 734 | 735 | 736 | class do_problem: 737 | # DEBUG 738 | # DEPRICATED? 739 | # TODO: Problems may need to be reworked a bit 740 | # Stop storing in a .py file 741 | # What is the purpose of problems? Whats the end goal? 742 | def GET(self): 743 | global problems 744 | problem_detect = ' (' 745 | problem_detect += datetime.now().strftime("%Y-%m-%d %H:%M:%S") 746 | if web.ctx.path == "/problem": 747 | problem_detect += ') Debugger attached to ' 748 | elif web.ctx.path == "/problemjb": 749 | problem_detect += ') Jailbreak detected for ' 750 | problem_detect += web.ctx.ip 751 | 752 | problems.insert(0, problem_detect) 753 | out = "\nproblems = %s" % problems 754 | fd = open('problems.py', 'w') 755 | fd.write(out) 756 | fd.close() 757 | 758 | 759 | class mdm_ca: 760 | def GET(self): 761 | 762 | if 'CA.crt' in os.listdir('.'): 763 | web.header('Content-Type', 'application/octet-stream;charset=utf-8') 764 | web.header('Content-Disposition', 'attachment;filename="CA.crt"') 765 | return open('CA.crt', "rb").read() 766 | else: 767 | raise web.notfound() 768 | 769 | 770 | class favicon: 771 | def GET(self): 772 | if 'favicon.ico' in os.listdir('.'): 773 | web.header('Content-Type', 'image/x-icon;charset=utf-8') 774 | return open('favicon.ico', "rb").read() 775 | elif 'favicon.ico' in os.listdir('./static/'): 776 | web.header('Content-Type', 'image/x-icon;charset=utf-8') 777 | return open('/static/favicon.ico', "rb").read() 778 | else: 779 | raise web.ok 780 | 781 | 782 | class app_manifest: 783 | def GET(self): 784 | 785 | if 'Manifest.plist' in os.listdir('.'): 786 | web.header('Content-Type', 'text/xml;charset=utf-8') 787 | return open('Manifest.plist', "rb").read() 788 | else: 789 | raise web.notfound() 790 | 791 | 792 | class app_ipa: 793 | def GET(self): 794 | 795 | if 'MyApp.ipa' in os.listdir('.'): 796 | web.header('Content-Type', 'application/octet-stream;charset=utf-8') 797 | web.header('Content-Disposition', 'attachment;filename="MyApp.ipa"') 798 | return open('MyApp.ipa', "rb").read() 799 | else: 800 | return web.ok 801 | 802 | 803 | def log_data(out): 804 | fd = open(LOGFILE, "a") 805 | fd.write(datetime.now().ctime()) 806 | fd.write(" %s\n" % repr(out)) 807 | fd.close() 808 | 809 | 810 | if __name__ == "__main__": 811 | print "Starting Server" 812 | app = web.application(urls, globals()) 813 | app.internalerror = web.debugerror 814 | 815 | try: 816 | app.run() 817 | except: 818 | sys.exit(0) 819 | else: 820 | # app.run() seems to use server.py as a module 821 | # Placing these in main causes them not to run 822 | # Placing these above main causes them to run twice 823 | mdm_commands = setup_commands() 824 | read_devices() 825 | -------------------------------------------------------------------------------- /server/static/devices.mustache.html: -------------------------------------------------------------------------------- 1 | {{#devices}} 2 |
3 |
4 |

5 |
6 | 7 |
8 | 9 |
10 |
{{name}}
11 |
{{owner}}
12 |
{{location}}
13 |
14 | 15 | 16 | 17 |
18 |
Edit Metadata:
19 |
20 |
21 | Device name: 22 | Device owner: 23 | Assigned location: 24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 | 32 |

33 |
34 |
35 |
36 |
37 |
UDID:
{{UDID}}
38 |
IP:
{{ip}}
39 |
{{geo}}
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {{#commands}} 52 | 53 | 54 | 55 | 56 | 57 | {{/commands}} 58 | 59 |
StatusCommandOptions
{{cmd.Command.RequestType}}
60 |
61 |
62 | {{/devices}} 63 | -------------------------------------------------------------------------------- /server/static/dist/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2014 Twitter, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/static/dist/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default, 8 | .btn-primary, 9 | .btn-success, 10 | .btn-info, 11 | .btn-warning, 12 | .btn-danger { 13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 16 | } 17 | .btn-default:active, 18 | .btn-primary:active, 19 | .btn-success:active, 20 | .btn-info:active, 21 | .btn-warning:active, 22 | .btn-danger:active, 23 | .btn-default.active, 24 | .btn-primary.active, 25 | .btn-success.active, 26 | .btn-info.active, 27 | .btn-warning.active, 28 | .btn-danger.active { 29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 31 | } 32 | .btn:active, 33 | .btn.active { 34 | background-image: none; 35 | } 36 | .btn-default { 37 | text-shadow: 0 1px 0 #fff; 38 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 39 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 40 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 41 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 42 | background-repeat: repeat-x; 43 | border-color: #dbdbdb; 44 | border-color: #ccc; 45 | } 46 | .btn-default:hover, 47 | .btn-default:focus { 48 | background-color: #e0e0e0; 49 | background-position: 0 -15px; 50 | } 51 | .btn-default:active, 52 | .btn-default.active { 53 | background-color: #e0e0e0; 54 | border-color: #dbdbdb; 55 | } 56 | .btn-primary { 57 | background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 58 | background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); 59 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); 60 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 61 | background-repeat: repeat-x; 62 | border-color: #2b669a; 63 | } 64 | .btn-primary:hover, 65 | .btn-primary:focus { 66 | background-color: #2d6ca2; 67 | background-position: 0 -15px; 68 | } 69 | .btn-primary:active, 70 | .btn-primary.active { 71 | background-color: #2d6ca2; 72 | border-color: #2b669a; 73 | } 74 | .btn-success { 75 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 76 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 77 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 78 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 79 | background-repeat: repeat-x; 80 | border-color: #3e8f3e; 81 | } 82 | .btn-success:hover, 83 | .btn-success:focus { 84 | background-color: #419641; 85 | background-position: 0 -15px; 86 | } 87 | .btn-success:active, 88 | .btn-success.active { 89 | background-color: #419641; 90 | border-color: #3e8f3e; 91 | } 92 | .btn-info { 93 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 94 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 95 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 96 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 97 | background-repeat: repeat-x; 98 | border-color: #28a4c9; 99 | } 100 | .btn-info:hover, 101 | .btn-info:focus { 102 | background-color: #2aabd2; 103 | background-position: 0 -15px; 104 | } 105 | .btn-info:active, 106 | .btn-info.active { 107 | background-color: #2aabd2; 108 | border-color: #28a4c9; 109 | } 110 | .btn-warning { 111 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 112 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 113 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 114 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 115 | background-repeat: repeat-x; 116 | border-color: #e38d13; 117 | } 118 | .btn-warning:hover, 119 | .btn-warning:focus { 120 | background-color: #eb9316; 121 | background-position: 0 -15px; 122 | } 123 | .btn-warning:active, 124 | .btn-warning.active { 125 | background-color: #eb9316; 126 | border-color: #e38d13; 127 | } 128 | .btn-danger { 129 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 130 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 131 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 132 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 133 | background-repeat: repeat-x; 134 | border-color: #b92c28; 135 | } 136 | .btn-danger:hover, 137 | .btn-danger:focus { 138 | background-color: #c12e2a; 139 | background-position: 0 -15px; 140 | } 141 | .btn-danger:active, 142 | .btn-danger.active { 143 | background-color: #c12e2a; 144 | border-color: #b92c28; 145 | } 146 | .thumbnail, 147 | .img-thumbnail { 148 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 149 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 150 | } 151 | .dropdown-menu > li > a:hover, 152 | .dropdown-menu > li > a:focus { 153 | background-color: #e8e8e8; 154 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 155 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 156 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 157 | background-repeat: repeat-x; 158 | } 159 | .dropdown-menu > .active > a, 160 | .dropdown-menu > .active > a:hover, 161 | .dropdown-menu > .active > a:focus { 162 | background-color: #357ebd; 163 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 164 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 165 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 166 | background-repeat: repeat-x; 167 | } 168 | .navbar-default { 169 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 170 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 171 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 172 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 173 | background-repeat: repeat-x; 174 | border-radius: 4px; 175 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 176 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 177 | } 178 | .navbar-default .navbar-nav > .active > a { 179 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); 180 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); 181 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); 182 | background-repeat: repeat-x; 183 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 184 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 185 | } 186 | .navbar-brand, 187 | .navbar-nav > li > a { 188 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 189 | } 190 | .navbar-inverse { 191 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 192 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 193 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 194 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 195 | background-repeat: repeat-x; 196 | } 197 | .navbar-inverse .navbar-nav > .active > a { 198 | background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%); 199 | background-image: linear-gradient(to bottom, #222 0%, #282828 100%); 200 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); 201 | background-repeat: repeat-x; 202 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 203 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 204 | } 205 | .navbar-inverse .navbar-brand, 206 | .navbar-inverse .navbar-nav > li > a { 207 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 208 | } 209 | .navbar-static-top, 210 | .navbar-fixed-top, 211 | .navbar-fixed-bottom { 212 | border-radius: 0; 213 | } 214 | .alert { 215 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 216 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 217 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 218 | } 219 | .alert-success { 220 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 221 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 222 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 223 | background-repeat: repeat-x; 224 | border-color: #b2dba1; 225 | } 226 | .alert-info { 227 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 228 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 230 | background-repeat: repeat-x; 231 | border-color: #9acfea; 232 | } 233 | .alert-warning { 234 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 235 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 236 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 237 | background-repeat: repeat-x; 238 | border-color: #f5e79e; 239 | } 240 | .alert-danger { 241 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 242 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 243 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 244 | background-repeat: repeat-x; 245 | border-color: #dca7a7; 246 | } 247 | .progress { 248 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 249 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 250 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 251 | background-repeat: repeat-x; 252 | } 253 | .progress-bar { 254 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); 255 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); 256 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); 257 | background-repeat: repeat-x; 258 | } 259 | .progress-bar-success { 260 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 261 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 262 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 263 | background-repeat: repeat-x; 264 | } 265 | .progress-bar-info { 266 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 267 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 268 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 269 | background-repeat: repeat-x; 270 | } 271 | .progress-bar-warning { 272 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 273 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 274 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 275 | background-repeat: repeat-x; 276 | } 277 | .progress-bar-danger { 278 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 279 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 280 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 281 | background-repeat: repeat-x; 282 | } 283 | .list-group { 284 | border-radius: 4px; 285 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 286 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 287 | } 288 | .list-group-item.active, 289 | .list-group-item.active:hover, 290 | .list-group-item.active:focus { 291 | text-shadow: 0 -1px 0 #3071a9; 292 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); 293 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); 294 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); 295 | background-repeat: repeat-x; 296 | border-color: #3278b3; 297 | } 298 | .panel { 299 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 300 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 301 | } 302 | .panel-default > .panel-heading { 303 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 304 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 305 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 306 | background-repeat: repeat-x; 307 | } 308 | .panel-primary > .panel-heading { 309 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 310 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 311 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 312 | background-repeat: repeat-x; 313 | } 314 | .panel-success > .panel-heading { 315 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 316 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 317 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 318 | background-repeat: repeat-x; 319 | } 320 | .panel-info > .panel-heading { 321 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 322 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 323 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 324 | background-repeat: repeat-x; 325 | } 326 | .panel-warning > .panel-heading { 327 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 328 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 329 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 330 | background-repeat: repeat-x; 331 | } 332 | .panel-danger > .panel-heading { 333 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 334 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .well { 339 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 340 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 341 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 342 | background-repeat: repeat-x; 343 | border-color: #dcdcdc; 344 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 345 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 346 | } 347 | /*# sourceMappingURL=bootstrap-theme.css.map */ 348 | -------------------------------------------------------------------------------- /server/static/dist/css/bootstrap-theme.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins.less"],"names":[],"mappings":"AAeA;AACA;AACA;AACA;AACA;AACA;EACE,wCAAA;ECoGA,2FAAA;EACQ,mFAAA;;ADhGR,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;AACD,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;EC8FD,wDAAA;EACQ,gDAAA;;ADnER,IAAC;AACD,IAAC;EACC,sBAAA;;AAKJ;EC4PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;EAyB2C,yBAAA;EAA2B,kBAAA;;AAvBtE,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAeJ;EC2PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAgBJ;EC0PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAiBJ;ECyPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,SAAC;AACD,SAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,SAAC;AACD,SAAC;EACC,yBAAA;EACA,qBAAA;;AAkBJ;ECwPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAmBJ;ECuPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,WAAC;AACD,WAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,WAAC;AACD,WAAC;EACC,yBAAA;EACA,qBAAA;;AA2BJ;AACA;EC6CE,kDAAA;EACQ,0CAAA;;ADpCV,cAAe,KAAK,IAAG;AACvB,cAAe,KAAK,IAAG;ECmOnB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EDpOF,yBAAA;;AAEF,cAAe,UAAU;AACzB,cAAe,UAAU,IAAG;AAC5B,cAAe,UAAU,IAAG;EC6NxB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED9NF,yBAAA;;AAUF;ECiNI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;EDrPA,kBAAA;ECaA,2FAAA;EACQ,mFAAA;;ADjBV,eAOE,YAAY,UAAU;EC0MpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,wDAAA;EACQ,gDAAA;;ADLV;AACA,WAAY,KAAK;EACf,8CAAA;;AAIF;EC+LI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;;ADtOF,eAIE,YAAY,UAAU;EC2LpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,uDAAA;EACQ,+CAAA;;ADCV,eASE;AATF,eAUE,YAAY,KAAK;EACf,yCAAA;;AAKJ;AACA;AACA;EACE,gBAAA;;AAUF;EACE,6CAAA;EChCA,0FAAA;EACQ,kFAAA;;AD2CV;ECqJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAKF;ECoJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAMF;ECmJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAOF;ECkJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAgBF;ECyII,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADlIJ;EC+HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADjIJ;EC8HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADhIJ;EC6HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD/HJ;EC4HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD9HJ;EC2HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtHJ;EACE,kBAAA;EC/EA,kDAAA;EACQ,0CAAA;;ADiFV,gBAAgB;AAChB,gBAAgB,OAAO;AACvB,gBAAgB,OAAO;EACrB,6BAAA;EC4GE,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED7GF,qBAAA;;AAUF;ECjGE,iDAAA;EACQ,yCAAA;;AD0GV,cAAe;ECsFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADxFJ,cAAe;ECqFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADvFJ,cAAe;ECoFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtFJ,WAAY;ECmFR,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADrFJ,cAAe;ECkFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADpFJ,aAAc;ECiFV,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD5EJ;ECyEI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED1EF,qBAAA;EC1HA,yFAAA;EACQ,iFAAA","sourcesContent":["\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-bg, 5%); @end-color: darken(@navbar-default-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-bg; @end-color: lighten(@navbar-inverse-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n}\n\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","//\n// Mixins\n// --------------------------------------------------\n\n\n// Utilities\n// -------------------------\n\n// Clearfix\n// Source: http://nicolasgallagher.com/micro-clearfix-hack/\n//\n// For modern browsers\n// 1. The space content is one way to avoid an Opera bug when the\n// contenteditable attribute is included anywhere else in the document.\n// Otherwise it causes space to appear at the top and bottom of elements\n// that are clearfixed.\n// 2. The use of `table` rather than `block` is only necessary if using\n// `:before` to contain the top-margins of child elements.\n.clearfix() {\n &:before,\n &:after {\n content: \" \"; // 1\n display: table; // 2\n }\n &:after {\n clear: both;\n }\n}\n\n// WebKit-style focus\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n\n// Center-align a block level element\n.center-block() {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n\n// Sizing shortcuts\n.size(@width; @height) {\n width: @width;\n height: @height;\n}\n.square(@size) {\n .size(@size; @size);\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n &::-moz-placeholder { color: @color; // Firefox\n opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Text overflow\n// Requires inline-block or block for proper styling\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n// CSS image replacement\n//\n// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for\n// mixins being reused as classes with the same name, this doesn't hold up. As\n// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. Note\n// that we cannot chain the mixins together in Less, so they are repeated.\n//\n// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757\n\n// Deprecated as of v3.0.1 (will be removed in v4)\n.hide-text() {\n font: ~\"0/0\" a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n// New mixin to use as of v3.0.1\n.text-hide() {\n .hide-text();\n}\n\n\n\n// CSS3 PROPERTIES\n// --------------------------------------------------\n\n// Single side border-radius\n.border-top-radius(@radius) {\n border-top-right-radius: @radius;\n border-top-left-radius: @radius;\n}\n.border-right-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-top-right-radius: @radius;\n}\n.border-bottom-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-bottom-left-radius: @radius;\n}\n.border-left-radius(@radius) {\n border-bottom-left-radius: @radius;\n border-top-left-radius: @radius;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support the\n// standard `box-shadow` property.\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Transitions\n.transition(@transition) {\n -webkit-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n// Transformations\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n transform: rotate(@degrees);\n}\n.scale(@ratio; @ratio-y...) {\n -webkit-transform: scale(@ratio, @ratio-y);\n -ms-transform: scale(@ratio, @ratio-y); // IE9 only\n transform: scale(@ratio, @ratio-y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n transform: translate(@x, @y);\n}\n.skew(@x; @y) {\n -webkit-transform: skew(@x, @y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n transform: skew(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// User select\n// For selecting text on the page\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n\n// Resize anything\n.resizable(@direction) {\n resize: @direction; // Options: horizontal, vertical, both\n overflow: auto; // Safari fix\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Opacity\n.opacity(@opacity) {\n opacity: @opacity;\n // IE8 filter\n @opacity-ie: (@opacity * 100);\n filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n\n\n\n// GRADIENTS\n// --------------------------------------------------\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, color-stop(@start-color @start-percent), color-stop(@end-color @end-percent)); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n\n// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n\n\n\n// Retina images\n//\n// Short retina mixin for setting background-image and -size\n\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// COMPONENT MIXINS\n// --------------------------------------------------\n\n// Horizontal dividers\n// -------------------------\n// Dividers (basically an hr) within dropdowns and nav lists\n.nav-divider(@color: #e5e5e5) {\n height: 1px;\n margin: ((@line-height-computed / 2) - 1) 0;\n overflow: hidden;\n background-color: @color;\n}\n\n// Panels\n// -------------------------\n.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) {\n border-color: @border;\n\n & > .panel-heading {\n color: @heading-text-color;\n background-color: @heading-bg-color;\n border-color: @heading-border;\n\n + .panel-collapse .panel-body {\n border-top-color: @border;\n }\n }\n & > .panel-footer {\n + .panel-collapse .panel-body {\n border-bottom-color: @border;\n }\n }\n}\n\n// Alerts\n// -------------------------\n.alert-variant(@background; @border; @text-color) {\n background-color: @background;\n border-color: @border;\n color: @text-color;\n\n hr {\n border-top-color: darken(@border, 5%);\n }\n .alert-link {\n color: darken(@text-color, 10%);\n }\n}\n\n// Tables\n// -------------------------\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n\n// List Groups\n// -------------------------\n.list-group-item-variant(@state; @background; @color) {\n .list-group-item-@{state} {\n color: @color;\n background-color: @background;\n\n a& {\n color: @color;\n\n .list-group-item-heading { color: inherit; }\n\n &:hover,\n &:focus {\n color: @color;\n background-color: darken(@background, 5%);\n }\n &.active,\n &.active:hover,\n &.active:focus {\n color: #fff;\n background-color: @color;\n border-color: @color;\n }\n }\n }\n}\n\n// Button variants\n// -------------------------\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n.button-variant(@color; @background; @border) {\n color: @color;\n background-color: @background;\n border-color: @border;\n\n &:hover,\n &:focus,\n &:active,\n &.active,\n .open .dropdown-toggle& {\n color: @color;\n background-color: darken(@background, 8%);\n border-color: darken(@border, 12%);\n }\n &:active,\n &.active,\n .open .dropdown-toggle& {\n background-image: none;\n }\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &:active,\n &.active {\n background-color: @background;\n border-color: @border;\n }\n }\n\n .badge {\n color: @background;\n background-color: @color;\n }\n}\n\n// Button sizes\n// -------------------------\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n}\n\n// Pagination\n// -------------------------\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) {\n > li {\n > a,\n > span {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n }\n &:first-child {\n > a,\n > span {\n .border-left-radius(@border-radius);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius);\n }\n }\n }\n}\n\n// Labels\n// -------------------------\n.label-variant(@color) {\n background-color: @color;\n &[href] {\n &:hover,\n &:focus {\n background-color: darken(@color, 10%);\n }\n }\n}\n\n// Contextual backgrounds\n// -------------------------\n.bg-variant(@color) {\n background-color: @color;\n a&:hover {\n background-color: darken(@color, 10%);\n }\n}\n\n// Typography\n// -------------------------\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover {\n color: darken(@color, 10%);\n }\n}\n\n// Navbar vertical align\n// -------------------------\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.\n.navbar-vertical-align(@element-height) {\n margin-top: ((@navbar-height - @element-height) / 2);\n margin-bottom: ((@navbar-height - @element-height) / 2);\n}\n\n// Progress bars\n// -------------------------\n.progress-bar-variant(@color) {\n background-color: @color;\n .progress-striped & {\n #gradient > .striped();\n }\n}\n\n// Responsive utilities\n// -------------------------\n// More easily include all the states for responsive-utilities.less.\n.responsive-visibility() {\n display: block !important;\n table& { display: table; }\n tr& { display: table-row !important; }\n th&,\n td& { display: table-cell !important; }\n}\n\n.responsive-invisibility() {\n display: none !important;\n}\n\n\n// Grid System\n// -----------\n\n// Centered container element\n.container-fixed() {\n margin-right: auto;\n margin-left: auto;\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: (@gutter / -2);\n margin-right: (@gutter / -2);\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n @media (min-width: @screen-xs-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-xs-column-push(@columns) {\n @media (min-width: @screen-xs-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-xs-column-pull(@columns) {\n @media (min-width: @screen-xs-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n\n// Form validation states\n//\n// Used in forms.less to generate the form validation CSS for warnings, errors,\n// and successes.\n\n.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) {\n // Color the label and help text\n .help-block,\n .control-label,\n .radio,\n .checkbox,\n .radio-inline,\n .checkbox-inline {\n color: @text-color;\n }\n // Set the border and box shadow on specific inputs to match\n .form-control {\n border-color: @border-color;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work\n &:focus {\n border-color: darken(@border-color, 10%);\n @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%);\n .box-shadow(@shadow);\n }\n }\n // Set validation states also for addons\n .input-group-addon {\n color: @text-color;\n border-color: @border-color;\n background-color: @background-color;\n }\n // Optional feedback icon\n .form-control-feedback {\n color: @text-color;\n }\n}\n\n// Form control focus state\n//\n// Generate a customized focus state and for any input with the specified color,\n// which defaults to the `@input-focus-border` variable.\n//\n// We highly encourage you to not customize the default value, but instead use\n// this to tweak colors on an as-needed basis. This aesthetic change is based on\n// WebKit's default styles, but applicable to a wider range of browsers. Its\n// usability and accessibility should be taken into account with any change.\n//\n// Example usage: change the default blue border and shadow to white for better\n// contrast against a dark gray background.\n\n.form-control-focus(@color: @input-border-focus) {\n @color-rgba: rgba(red(@color), green(@color), blue(@color), .6);\n &:focus {\n border-color: @color;\n outline: 0;\n .box-shadow(~\"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}\");\n }\n}\n\n// Form control sizing\n//\n// Relative text size, padding, and border-radii changes for form controls. For\n// horizontal sizing, wrap controls in the predefined grid classes. ` 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 |
51 |
52 | 53 |
54 | 55 | 56 |


57 | 58 |
59 | 60 | 61 | 62 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /server/xactn.log: -------------------------------------------------------------------------------- 1 | ## transaction log 2 | ## this will get filled up with timestamped JSON strings showing commands 3 | ## issued by the server, and requests/responses from the client. 4 | ## 5 | ## Be careful, this will likely include sensitive information, like 6 | ## * Device Tokens 7 | ## * Devie Unlock Tokens 8 | ## * Push Certificate Topic (not super sensitive, but still) 9 | ## * Device information (UDID, phone numbers, blah blah blah) 10 | ## 11 | ## So if you want to send this to anyone, SCRUB IT FIRST 12 | ## 13 | 14 | --------------------------------------------------------------------------------