Tuesday, February 4, 2014

OSX - Safari Push Notifications

Recently Apple has expanded the push notification service to its computers, as the OSX users among you may have noticed. After some discussions we tried to implement this on our server, so for our website too.

Unfortunately Apple provides just some php samples files, which is of course a help, but as our server is written in Java it was a bit complex to get it running. I'll provide in this post the main parts of the source needed to get it.

This sample code uses the default ZIP functions of java and the signing functions provided by BouncyCastle. Most parts of the code are kind of obvious and therefore omitted (any case, if you need them just drop me a message)

Let's start with the Zipper:
private class Zipper {

private final ByteArrayOutputStream bos;
private final ZipOutputStream zipfile;
private final Map manifest;

public Zipper() {
bos = new ByteArrayOutputStream();
zipfile = new ZipOutputStream(bos);
manifest = new HashMap();
}

public byte[] getManifest() throws UnsupportedEncodingException {
Set keys = manifest.keySet();
int i = 0;
StringBuilder sb = new StringBuilder();
for (String key : keys) {
sb.append(i <= 0 ? "{" : ",");
sb.append("\"" + key + "\": \"" + manifest.get(key) + "\"");
i++;
}
sb.append("}");
return sb.toString().getBytes("UTF8");
}

public byte[] finalizeZip() throws IOException {
zipfile.finish();
bos.flush();
zipfile.close();
manifest.clear();
return bos.toByteArray();
}

public void addFileToZip(String path, String filename, byte[] file, boolean addToManifest) throws IOException {
if (file != null) {
String completeFilename = path.length() > 0 ? path + "/" + filename : filename;
ZipEntry zipEntry = new ZipEntry(completeFilename);
CRC32 crc = new CRC32();
crc.update(file);
zipEntry.setCrc(crc.getValue());
zipfile.putNextEntry(zipEntry);
zipfile.write(file, 0, file.length);
zipfile.flush();
zipfile.closeEntry();

if (addToManifest) {
try {
manifest.put(completeFilename, SHAsum(file));
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} else {
LOGGER.error("File " + filename + " was null!");
}
}

}

It is done as a private class within the class that actually returns the ZIP to the client. It does the zipping and immediately calculates the checksums that are needed for the manifest. For completeness here the simple Checksum creation methods

public static String SHAsum(byte[] convertme) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return byteArray2Hex(md.digest(convertme));
}

private static String byteArray2Hex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String formattedstring = formatter.toString();
formatter.close();
return formattedstring;

}

This manifest will be created with the method "getManifest()" in the zipper class and signed using the next class which is again private and called PKCS7Encrypter

private class PKCS7Encrypter {

private final byte[] _store;
private final String _storepass;

public PKCS7Encrypter(byte[] store, String storepass) {
_store = store;
_storepass = storepass;
}

private KeyStore getKeystore() throws KeyStoreException, NoSuchAlgorithmException, CertificateException,
IOException {
if (_store == null) {
LOGGER.error("Could not find store file (.p12)");
return null;
}
// First load the keystore object by providing the p12 file path
KeyStore clientStore = KeyStore.getInstance("PKCS12");
// replace testPass with the p12 password/pin
clientStore.load(new ByteArrayInputStream(_store), _storepass.toCharArray());
return clientStore;
}

private X509CertificateHolder getCert(KeyStore keystore, String aliaz) throws GeneralSecurityException,
IOException {
java.security.cert.Certificate c = keystore.getCertificate(aliaz);
return new X509CertificateHolder(c.getEncoded());
}

private PrivateKey getPrivateKey(KeyStore keystore, String aliaz) throws GeneralSecurityException, IOException {
return (PrivateKey) keystore.getKey(aliaz, _storepass.toCharArray());
}

public byte[] sign(byte[] dataToSign) throws IOException, GeneralSecurityException, OperatorCreationException,
CMSException {
KeyStore clientStore = getKeystore();
if (clientStore == null) {
return null;
}
Enumeration aliases = clientStore.aliases();
String aliaz = "";
while (aliases.hasMoreElements()) {
aliaz = aliases.nextElement();
if (clientStore.isKeyEntry(aliaz)) {
break;
}
}

CMSTypedData msg = new CMSProcessableByteArray(dataToSign); // Data to sign

X509CertificateHolder x509Certificate = getCert(clientStore, aliaz);
List certList = new ArrayList();
certList.add(x509Certificate); // Adding the X509 Certificate

Store certs = new JcaCertStore(certList);

CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
// Initializing the the BC's Signer
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(
getPrivateKey(clientStore, aliaz));

gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder()
.setProvider("BC").build()).build(sha1Signer, x509Certificate));
// adding the certificate
gen.addCertificates(certs);
// Getting the signed data
CMSSignedData sigData = gen.generate(msg, false);
return sigData.getEncoded();
}


}

This class needs to be fed with the .p12-file that you can extract from the Mac OSX keystore. It shouldn't be just the private key, but the certificate with private key; and the password used to export it.

With this methods (and some simple methods to create the rest of the .pushPackage file you'll now be able to create the ZIP and send it as a response

// Create the ZIP file
Zipper zip = new Zipper();
zip.addFileToZip("icon.iconset", "icon_16x16.png", getResource("images/icons/icon_16x16.png"));
zip.addFileToZip("icon.iconset", "icon_16x16@2x.png", getResource("images/icons/icon_16x16@2x.png"));
zip.addFileToZip("icon.iconset", "icon_32x32.png", getResource("images/icons/icon_32x32.png"));
zip.addFileToZip("icon.iconset", "icon_32x32@2x.png", getResource("images/icons/icon_32x32@2x.png"));
zip.addFileToZip("icon.iconset", "icon_128x128.png", getResource("images/icons/icon_128x128.png"));
zip.addFileToZip("icon.iconset", "icon_128x128@2x.png", getResource("images/icons/icon_128x128@2x.png"));

zip.addFileToZip("", "website.json", getWebsiteJson(""));
byte[] manifest = zip.getManifest();
zip.addFileToZip("", "manifest.json", manifest, false);
try {
PKCS7Encrypter encrypter = new PKCS7Encrypter(getResource(STOREPATH), STOREPASS);
zip.addFileToZip("", "signature", encrypter.sign(manifest));
} catch (Exception e) {
LOGGER.error("Signature Error: " + e.getLocalizedMessage());
e.printStackTrace();
}


getBinaryFile(zip.finalizeZip(), "MyPage.pushpackage", response);

One last thing is missing! Java by default does not accept keys in the length we'll need them for this usage. So you'll end up in getting "java.security.InvalidKeyException:illegal Key Size" exceptions.

Fortunately Oracle provides a solution to this. You'll need to download the "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 6" zip(be aware of taking the right ones for your JDK). Behind this download you'll find 2 files which are "local_policy.jar" and "US_export_policy.jar". These need to end up in your "$JAVA_HOME/jre/lib/security" folder. Both of them will already be in this folder and need to be replaced (Save the original files before doing that)

BTW When you test all this implement the logging REST callback provided by Apple.
@SuppressWarnings("unchecked")
@RequestMapping(method = RequestMethod.POST, value = "/log")
public void logErrors(@PathVariable("version") String version, @RequestBody Map json,
HttpServletRequest request, HttpServletResponse response) throws IOException {
if (LOGGER.isDebugEnabled())
LOGGER.debug("logErrors()");

Object logs = json.get("logs");
if (logs != null) {
for (String logEntry : (ArrayList) logs) {
LOGGER.error("Safari Push messages error: " + logEntry);
}
}

}
This will save you hours of searching for errors

Tuesday, September 24, 2013

CAS: Sample Client for Java

As the client for Java is missing at the jasig homepage I'll provide my solution here. Of course it has the "Domain" addition that I described in my earlier posts, but it is simple and quite obvious how to remove it.

So, here it is:

package cas;

import java.io.IOException;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;

public final class Client {
private static final Logger LOG = Logger.getLogger(Client.class.getName());

private Client() {
// static-only access
}

public static String getTicket(final String server, final String domain, final String username,
final String password, final String service) {
notNull(server, "server must not be null");
notNull(domain, "domain must not be null");
notNull(username, "username must not be null");
notNull(password, "password must not be null");
notNull(service, "service must not be null");

return getServiceTicket(server, getTicketGrantingTicket(server, domain, username, password), service);
}

private static String getServiceTicket(final String server, final String ticketGrantingTicket, final String service) {
if (ticketGrantingTicket == null)
return null;
final HttpClient client = new HttpClient();
final PostMethod post = new PostMethod(server + "/" + ticketGrantingTicket);
post.setRequestBody(new NameValuePair[] { new NameValuePair("service", service) });
try {
client.executeMethod(post);
final String response = post.getResponseBodyAsString();
switch (post.getStatusCode()) {
case 200:
return response;
default:
LOG.warning("Invalid response code (" + post.getStatusCode() + ") from CAS server!");
LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())));
break;
}
}
catch (final IOException e) {
LOG.warning(e.getMessage());
}
finally {
post.releaseConnection();
}
return null;
}

private static String getTicketGrantingTicket(final String server, final String domain, final String username,
final String password) {
final HttpClient client = new HttpClient();
final PostMethod post = new PostMethod(server);
post.setRequestBody(new NameValuePair[] { new NameValuePair("domain", domain),
new NameValuePair("username", username), new NameValuePair("password", password) });
try {
client.executeMethod(post);
final String response = post.getResponseBodyAsString();
switch (post.getStatusCode()) {
case 201: {
final Matcher matcher = Pattern.compile(".*action=\".*/(.*?)\".*").matcher(response);

if (matcher.matches())
return matcher.group(1);

LOG.warning("Successful ticket granting request, but no ticket found!");
LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())));
break;
}
default:
LOG.warning("Invalid response code (" + post.getStatusCode() + ") from CAS server!");
LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())));
break;
}
}
catch (final IOException e) {
LOG.warning(e.getMessage());
}
finally {
post.releaseConnection();
}
return null;
}

private static void notNull(final Object object, final String message) {
if (object == null)
throw new IllegalArgumentException(message);
}

public static void main(final String[] args) {
final String server = "https://login.myServer.org/v1/tickets";
final String domain = "www.myserver.org";
final String username = "JohnDoe";
final String password = "mySecret";

final String service = "https://www.myServiceServer.org/";

LOG.info(getTicket(server, domain, username, password, service));
}
}

btw: I have implemented the same procedure for iOS and put it on GitHub

Wednesday, September 18, 2013

Install CocoaPods on OSX-Mavericks (DP8)

Today I tried to upgrade CocoaPods in order to prepare my APPs to the upcoming changes in iOS7. Unfortunately I got an error

ERROR:  Error installing cocoapods:
ERROR: Failed to build gem native extension.


    /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby extconf.rb

I'm currently using "xCode5 GM" which is using the "OSX 10.8 SDK" which apparently is delivered without ruby version 2.0.

The solution was to download "xCode5 DP6" (containing the SDK 10.9), switch the command line tools to the version supplied by this release ("xcode-select") and run the following commands as a shell script

sys_rb_usr=/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr
sdk_rb_usr=`xcode-select -p`/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr

sudo cp -r $sdk_rb_usr/include $sys_rb_usr/include

And then update CocoaPods.

Wednesday, July 3, 2013

TestflightSDK - Beta testing for iOS and Android

We now introduced a new Testing system for our APPs in our company: Testflight

This system allows us to distribute our BETAs and RCs to our dedicated testers without bothering them any time we have a new version to come to us, connect the device and so on.

Beside this very useful function we can embed the TestFlightSDK directly in our APPs to monitor how the user is actually moving in the APP (right now only for iOS, but Android is coming soon)

Install the SDK
That's not very tricky... One way would be to download it and add it to your xCode, or the way I prefer is using Cocoapods to automatically inject it to my project.

As we only need it during development time we don't want to add the "Testflight.h" header in every single class where we use the monitoring. So we inject it using the "", and not in any case, but only during development

#ifdef __OBJC__
    #import
    #import
    #ifdef TESTFLIGHT
        #import
    #endif
#endif

Now we can enable it by going to the targets build settings and add a "-DTESTFLIGHT" to the "other C Flags" in the section "Apple LLVM compiler 4.2 - Language"

Activate it
That's quite simple too. Just go to your AppDelegate and add the following lines

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#ifdef TESTFLIGHT
    // !!!: Use the next line only during beta
    [TestFlight setDeviceIdentifier:[[[UIDevice currentDevice] identifierForVendor] UUIDString]];
    [TestFlight takeOff:@""];
#endif
    return YES;
}

And the basic features are already activated.

You see we are again using the makro to remove TestFlightSDK on release time

Checkpoints
Nothing simpler and useful than that. Basically with this function you can check if users are passing points in the code. Set a Checkpoint as follows

#ifdef TESTFLIGHT
    [TestFlight passCheckpoint:@""];
#endif

In your TestFlight console you'll get a notification whenever a user passes this point in the execution

Feedback
Nice feature, even though the standard form is not usable in most cases. Fortunately they offer the possibility to use a custom form and just send the text to them (or better to your page on their website).

There are still a couple of interesting features in this SDK like questionnaires and remote logging that are worth to explore, and as soon as I had the time to do that I'll post them here.

Update March 2014:
TestFlight has been bought by Apple, and one of the first things they did was to announce that the Android support will be terminated by march 21th :-(

Friday, June 28, 2013

UIWebView detect scrolling

Today I'm trying to show and hide a View depending on the scrolling direction of a UIWebView.
Unfortunately just setting the delegate of the UIWebView in the storyboard (or in the code) is not enough  to get the method

- (void)scrollViewDidScroll:(UIScrollView *)scrollView

called.

That's because its not the delegate of the UIWebView that you want in this case but the delegate of its containing UIScrollview. So in your init method just set the following and you're done.

[self.eventUIWebView.scrollView setDelegate:self];

Friday, June 14, 2013

iOS7 - Beta

I know that it's a beta, and therefore bugs are present, but in some way we as developers are forced to test it and report those back to Apple in order for them to fix the issues.

So, here is my list of bug and unwanted behaviours

1) A strange thing that i found is in the Photo-APP. I made some pictures to see them in the APP. Then I tried to delete the picture before the last one, but the APP always deletes the last picture. This doesn't seem to happen by deleting arbitrary pics, but only the pre-last. (Fixed in Beta 3)

2) In the Contacts-APP the list of initial letters (right side of the screen) doesn't seem to be aligned correctly

3) Skype... isn't working at all

4) Don't put a picture with a light background in the lock screen. It just disables you from seeing the controls

5) In some Apps (for me the synology apps) that use the popup to ask for the credentials, there are just no inputfields. This means that there is no space to enter them and you're basically not able to login

6) Now in the latest Beta 5 you'll see all the birthdays one day earlier than they really are. My contacts APP is telling me that my birthday is the 18. of September, but in the Calendar I see it on the right date. So by restoring it for displaying in the contacts they'll make some kind of mistake

I'll put more of those as soon as I find them.

All in all I like the new look and feel of the OS and even the new functions are nice, however there is still a lot of work for Apple until they can distribute it to the market.

Wednesday, June 12, 2013

Mac OSX Mavericks - Issues

Since yesterday I'm now using the new Beta of OSX-Mavericks an I'm quite happy with it.

The good things:

  • Now you can have 2 full screen apps on two screens
  • You have the Maps App on OSX
  • Auto-Update in the App store
  • You can answer iMessages directly from the notifications


The bad things:

  • Skype quit to work (This can be fixed by moving to an older version)
  • XMarks for Safari does not work
  • Safari in general seems to have some issues. If you fast switch between Safari and other Apps after some time, and even not really reproducible Safari just doesn't react when u enter a new URL. After a Safari restart everything's fine again
  • I've a Problem getting the Xcode Command line tools working; Apparently I miss the ruby.h file - Found a fix for that one


I'll keep you updated if I find some more issues

Golang setup PATH

Quite recently we startet in the company to use and write some Go programs. I love Go. It's easy to learn, read and modify. One of the m...