Dispatcher Paragon Mobile Print SDK - iOS

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 hello

In web browser

images/download/attachments/284926569/07_27_47-version-1-modificationdate-1591010294307-api-v2.jpg

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 Version
data.append(contentsOf: [0x01, 0x01])
// Operation ID
data.append(contentsOf: [0x00, 0x02])
// Request ID
data.append(contentsOf: [0x00, 0x00, 0x00, 0x01])
// Operational attributes - signature
data.append(0x01)
// Operational attributes
// Charset tag
data.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 tag
data.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 tag
data.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 tag
data.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 - signature
data.append(0x02)
// Job attributes
// Integer tag
data.append(contentsOf: [0x21, 0x00, UInt8("copies".count)])
data.append("copies".data(using: .utf8)!)
data.append(contentsOf: [0x00, 0x04, 0x00, 0x00, 0x00, 0x01])
 
 
// Keyword tag
data.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 - signature
data.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.default
let 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.authenticationMethod
switch 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:SecIdentity
public var trust:SecTrust
public var certArray:NSArray
}
 
 
public func extractIdentity(certData:NSData, certPassword:String) -> IdentityAndTrust {
var identityAndTrust:IdentityAndTrust!
var securityError:OSStatus = errSecSuccess
var items: CFArray?
let certOptions: Dictionary = [ kSecImportExportPassphrase as String : certPassword ];
// import certificate to read its entries
securityError = SecPKCS12Import(certData, certOptions as CFDictionary, &items);
if securityError == errSecSuccess {
let certItems:CFArray = (items as CFArray?)!;
let certItemsArray:Array = certItems as Array
let dict:AnyObject? = certItemsArray.first;
if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {
// grab the identity
let identityPointer:AnyObject? = certEntry["identity"];
let secIdentityRef:SecIdentity = (identityPointer as! SecIdentity?)!;
// grab the trust
let trustPointer:AnyObject? = certEntry["trust"];
let trustRef:SecTrust = trustPointer as! SecTrust;
// grab the certificate chain
var certRef: SecCertificate?
SecIdentityCopyCertificate(secIdentityRef, &certRef);
let certArray:NSMutableArray = NSMutableArray();
certArray.add(certRef as SecCertificate?);
identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: certArray);
}
}
return identityAndTrust;
}