YSoft SafeQ Mobile Print SDK - iOS
The document describes API for integrating with YSoft SafeQ. 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
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;
}