The document describes API for integrating with Dispatcher Paragon. The document contains reference to source code.
The document describes requests for delivering job to Mobile Integration Gateway (MiG).
Delivering Print job via MiG
MiG = Mobile Integration Gateway which operates typically on port 8050. This services has full implementation of IPPS according to Apple's IPP documentation and it also comply with Mopria standard. It was the first server solution certified by Mopria for IPP delivery. IPP is described in RFC https://tools.ietf.org/html/rfc2911- RFC is quite big, we will need just a subset.
To check whether MiG is running just enter following url to browser: https://safeq-server:8050/
Result should be web page with the string:
MIG helloIn web browser
Note: If you do not have valid certificate installed, we recommend to use Firefox for testing purpose, because it allows you to continue even with not valid certificate on HTTPS.
The job upload to MiG is just one POST request delivered to url: https://safeq-server:8050/ipp/print .
The request should be composed from following parts
- HTTP Header:
- Content-Type: application/ipp
- Accept: text/html, image/gif, image/jpeg, *; q=.2, /; q=.2
- Authorization: Basic BASE64_encoded_password
- HTTP Body:
- IPP encoded payload
Following sample code is based on URLRequest (see Apple documentation for more details: https://developer.apple.com/documentation/foundation/urlrequest). Here you may choose different stack, based on your application.
Following Swift code will construct the header:
var urlRequest = URLRequest(url: uploadURL)urlRequest.httpMethod = "POST"urlRequest.setValue("application/ipp", forHTTPHeaderField: "Content-Type")urlRequest.setValue("text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2", forHTTPHeaderField: "Accept")urlRequest.setValue("Basic \(self.token)", forHTTPHeaderField: "Authorization")The request body is mix of binary and text protocol known as IPP. We recommend you to read Petr Barton's his thesis written in 2016.
You can find the thesis here: https://is.muni.cz/th/wyl4x/?lang=en
PDF: https://is.muni.cz/th/wyl4x/thesis.pdf
Attachment here: https://is.muni.cz/th/wyl4x/attachement.zip
The sample implementation in Java class is here: android-printservice/app/src/main/java/com/ysoft/safeqprintservice/IppRequest.java
Code could serve as inspiration here is code written in Swift. Code is not optimized, it could be simplified. The point is to demonstrate structure of packet.
To serialize the IPP to request body you can use following code:
var data = Data()// IPP Versiondata.append(contentsOf: [0x01, 0x01])// Operation IDdata.append(contentsOf: [0x00, 0x02])// Request IDdata.append(contentsOf: [0x00, 0x00, 0x00, 0x01])// Operational attributes - signaturedata.append(0x01)// Operational attributes// Charset tagdata.append(contentsOf: [0x47, 0x00, UInt8("attributes-charset".count)])data.append("attributes-charset".data(using: .utf8)!)data.append(contentsOf: [0x00, UInt8("us-ascii".count)])data.append("us-ascii".data(using: .utf8)!)// Natural language tagdata.append(contentsOf: [0x48, 0x00, UInt8("attributes-natural-language".count)])data.append("attributes-natural-language".data(using: .utf8)!)data.append(contentsOf: [0x00, UInt8("en-us".count)])data.append("en-us".data(using: .utf8)!)// Name without language tagdata.append(contentsOf: [0x42, 0x00, UInt8("job-name".count)])data.append("job-name".data(using: .utf8)!)data.append(contentsOf: [0x00, UInt8(filename.count)])data.append(filename.data(using: .utf8)!)// Boolean tagdata.append(contentsOf: [0x22, 0x00, UInt8("ipp-attribute-fidelity".count)])data.append("ipp-attribute-fidelity".data(using: .utf8)!)data.append(contentsOf: [0x00, 0x01, 0x01])// Job attributes - signaturedata.append(0x02)// Job attributes// Integer tagdata.append(contentsOf: [0x21, 0x00, UInt8("copies".count)])data.append("copies".data(using: .utf8)!)data.append(contentsOf: [0x00, 0x04, 0x00, 0x00, 0x00, 0x01])// Keyword tagdata.append(contentsOf: [0x44, 0x00, UInt8("sides".count)])data.append("sides".data(using: .utf8)!)data.append(contentsOf: [0x00, UInt8("one-sided".count)])data.append("one-sided".data(using: .utf8)!)// End attributes - signaturedata.append(0x03)To invoke the delivery it's sufficient to call:
do {fileUrl.startAccessingSecurityScopedResource()data.append(try Data(contentsOf: fileUrl))fileUrl.stopAccessingSecurityScopedResource()let configuration = URLSessionConfiguration.defaultlet session = URLSession(configuration: configuration, delegate: self, delegateQueue:OperationQueue.main)session.uploadTask(with: urlRequest, from: data, completionHandler: self.uploadCompletionHandler).resume()} catch {print(error)return false}Response codes
200 OK - job was accepted by MiG
401 Unauthorized - authorization header is missing or credentials are incorrect
404 Not found - request URL is incorrect and it's not pointing to working MiG /ipp/print
Client certificate support
Set up Nginx proxy before MiG. Replace mig-server-address by your server.
listen [::]:443 ssl;listen 443 ssl;ssl_certificate /etc/.../fullchain.pem;ssl_certificate_key /etc/.../privkey.pem;ssl_dhparam /etc/.../ssl-dhparams.pem;location ^~ /ipp/print {proxy_set_header SSL_CLIENT_CERT $ssl_client_cert;proxy_pass http://mig-server-address:8050;}ssl_client_certificate /etc/nginx/ssl/ca.cer;ssl_verify_client on;The application must implement urlSession handler to recognize NSURLAuthenticationMethodClientCertificate.
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {let authenticationMethod = challenge.protectionSpace.authenticationMethodswitch authenticationMethod {case NSURLAuthenticationMethodClientCertificate:print("handle client certificates")handleClientCertificate(didReceive: challenge, completionHandler: completionHandler)....To send the client certificate to server (after initial response). It's necessary to load certificate in P12 format.
Client certificate might be protected by password, in order to open the certificate it's necessary to specify the password.
The next step is to create identityTrust which loads certificate and password.
To complete the client certificate loading, it's necessary to invoke completionHandler.
func handleClientCertificate(didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!let localCertPath = documentsUrl.appendingPathComponent( "client.p12")print(localCertPath)let localCertData = try? Data(contentsOf: localCertPath)if localCertData != nil{let clientCertificatePassword = UserDefaults.standard.string(forKey: "CLIENT_CERTIFICATE_PASSWORD_KEY") ?? ""let identityAndTrust:IdentityAndTrust = extractIdentity(certData: localCertData! as NSData, certPassword: clientCertificatePassword)let urlCredential:URLCredential = URLCredential(identity: identityAndTrust.identityRef,certificates: identityAndTrust.certArray as [AnyObject],persistence: URLCredential.Persistence.forSession);completionHandler(URLSession.AuthChallengeDisposition.useCredential, urlCredential);return}challenge.sender?.cancel(challenge)completionHandler(URLSession.AuthChallengeDisposition.rejectProtectionSpace, nil)}Additional code to work with TrustIdentity
public struct IdentityAndTrust {public var identityRef:SecIdentitypublic var trust:SecTrustpublic var certArray:NSArray}public func extractIdentity(certData:NSData, certPassword:String) -> IdentityAndTrust {var identityAndTrust:IdentityAndTrust!var securityError:OSStatus = errSecSuccessvar items: CFArray?let certOptions: Dictionary = [ kSecImportExportPassphrase as String : certPassword ];// import certificate to read its entriessecurityError = SecPKCS12Import(certData, certOptions as CFDictionary, &items);if securityError == errSecSuccess {let certItems:CFArray = (items as CFArray?)!;let certItemsArray:Array = certItems as Arraylet dict:AnyObject? = certItemsArray.first;if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {// grab the identitylet identityPointer:AnyObject? = certEntry["identity"];let secIdentityRef:SecIdentity = (identityPointer as! SecIdentity?)!;// grab the trustlet trustPointer:AnyObject? = certEntry["trust"];let trustRef:SecTrust = trustPointer as! SecTrust;// grab the certificate chainvar certRef: SecCertificate?SecIdentityCopyCertificate(secIdentityRef, &certRef);let certArray:NSMutableArray = NSMutableArray();certArray.add(certRef as SecCertificate?);identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: certArray);}}return identityAndTrust;}