/*
* Copyright 2009, Mahmood Ali.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Mahmood Ali. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.notnoop.apns.internal;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import com.notnoop.exceptions.InvalidSSLConfig;
import com.notnoop.exceptions.NetworkIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class Utilities {
private static Logger logger = LoggerFactory.getLogger(Utilities.class);
public static final String SANDBOX_GATEWAY_HOST = "gateway.sandbox.push.apple.com";
public static final int SANDBOX_GATEWAY_PORT = 2195;
public static final String SANDBOX_FEEDBACK_HOST = "feedback.sandbox.push.apple.com";
public static final int SANDBOX_FEEDBACK_PORT = 2196;
public static final String PRODUCTION_GATEWAY_HOST = "gateway.push.apple.com";
public static final int PRODUCTION_GATEWAY_PORT = 2195;
public static final String PRODUCTION_FEEDBACK_HOST = "feedback.push.apple.com";
public static final int PRODUCTION_FEEDBACK_PORT = 2196;
public static final int MAX_PAYLOAD_LENGTH = 2048;
private Utilities() { throw new AssertionError("Uninstantiable class"); }
public static SSLSocketFactory newSSLSocketFactory(final InputStream cert, final String password,
final String ksType, final String ksAlgorithm) throws InvalidSSLConfig {
final SSLContext context = newSSLContext(cert, password, ksType, ksAlgorithm);
return context.getSocketFactory();
}
public static SSLContext newSSLContext(final InputStream cert, final String password,
final String ksType, final String ksAlgorithm) throws InvalidSSLConfig {
try {
final KeyStore ks = KeyStore.getInstance(ksType);
ks.load(cert, password.toCharArray());
return newSSLContext(ks, password, ksAlgorithm);
} catch (final Exception e) {
throw new InvalidSSLConfig(e);
}
}
public static SSLContext newSSLContext(final KeyStore ks, final String password,
final String ksAlgorithm) throws InvalidSSLConfig {
try {
// Get a KeyManager and initialize it
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(ksAlgorithm);
kmf.init(ks, password.toCharArray());
// Get a TrustManagerFactory with the DEFAULT KEYSTORE, so we have all
// the certificates in cacerts trusted
final TrustManagerFactory tmf = TrustManagerFactory.getInstance(ksAlgorithm);
tmf.init((KeyStore)null);
// Get the SSLContext to help create SSLSocketFactory
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return sslContext;
} catch (final GeneralSecurityException e) {
throw new InvalidSSLConfig(e);
}
}
private static final Pattern pattern = Pattern.compile("[ -]");
public static byte[] decodeHex(final String deviceToken) {
final String hex = pattern.matcher(deviceToken).replaceAll("");
final byte[] bts = new byte[hex.length() / 2];
for (int i = 0; i < bts.length; i++) {
bts[i] = (byte) (charVal(hex.charAt(2 * i)) * 16 + charVal(hex.charAt(2 * i + 1)));
}
return bts;
}
private static int charVal(final char a) {
if ('0' <= a && a <= '9') {
return (a - '0');
} else if ('a' <= a && a <= 'f') {
return (a - 'a') + 10;
} else if ('A' <= a && a <= 'F') {
return (a - 'A') + 10;
} else {
throw new RuntimeException("Invalid hex character: " + a);
}
}
private static final char base[] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
public static String encodeHex(final byte[] bytes) {
final char[] chars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; ++i) {
final int b = (bytes[i]) & 0xFF;
chars[2 * i] = base[b >>> 4];
chars[2 * i + 1] = base[b & 0xF];
}
return new String(chars);
}
public static byte[] toUTF8Bytes(final String s) {
try {
return s.getBytes("UTF-8");
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public static byte[] marshall(final byte command, final byte[] deviceToken, final byte[] payload) {
final ByteArrayOutputStream boas = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(boas);
try {
dos.writeByte(command);
dos.writeShort(deviceToken.length);
dos.write(deviceToken);
dos.writeShort(payload.length);
dos.write(payload);
return boas.toByteArray();
} catch (final IOException e) {
throw new AssertionError();
}
}
public static byte[] marshallEnhanced(final byte command, final int identifier,
final int expiryTime, final byte[] deviceToken, final byte[] payload) {
final ByteArrayOutputStream boas = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(boas);
try {
dos.writeByte(command);
dos.writeInt(identifier);
dos.writeInt(expiryTime);
dos.writeShort(deviceToken.length);
dos.write(deviceToken);
dos.writeShort(payload.length);
dos.write(payload);
return boas.toByteArray();
} catch (final IOException e) {
throw new AssertionError();
}
}
public static Map<byte[], Integer> parseFeedbackStreamRaw(final InputStream in) {
final Map<byte[], Integer> result = new HashMap<byte[], Integer>();
final DataInputStream data = new DataInputStream(in);
while (true) {
try {
final int time = data.readInt();
final int dtLength = data.readUnsignedShort();
final byte[] deviceToken = new byte[dtLength];
data.readFully(deviceToken);
result.put(deviceToken, time);
} catch (final EOFException e) {
break;
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
return result;
}
public static Map<String, Date> parseFeedbackStream(final InputStream in) {
final Map<String, Date> result = new HashMap<String, Date>();
final Map<byte[], Integer> raw = parseFeedbackStreamRaw(in);
for (final Map.Entry<byte[], Integer> entry : raw.entrySet()) {
final byte[] dtArray = entry.getKey();
final int time = entry.getValue(); // in seconds
final Date date = new Date(time * 1000L); // in ms
final String dtString = encodeHex(dtArray);
result.put(dtString, date);
}
return result;
}
public static void close(final Closeable closeable) {
logger.debug("close {}", closeable);
try {
if (closeable != null) {
closeable.close();
}
} catch (final IOException e) {
logger.debug("error while closing resource", e);
}
}
public static void close(final Socket closeable) {
logger.debug("close {}", closeable);
try {
if (closeable != null) {
closeable.close();
}
} catch (final IOException e) {
logger.debug("error while closing socket", e);
}
}
public static void sleep(final int delay) {
try {
Thread.sleep(delay);
} catch (final InterruptedException e1) {
Thread.currentThread().interrupt();
}
}
public static byte[] copyOf(final byte[] bytes) {
final byte[] copy = new byte[bytes.length];
System.arraycopy(bytes, 0, copy, 0, bytes.length);
return copy;
}
public static byte[] copyOfRange(final byte[] original, final int from, final int to) {
final int newLength = to - from;
if (newLength < 0) {
throw new IllegalArgumentException(from + " > " + to);
}
final byte[] copy = new byte[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
public static void wrapAndThrowAsRuntimeException(final Exception e) throws NetworkIOException {
if (e instanceof IOException) {
throw new NetworkIOException((IOException)e);
} else if (e instanceof NetworkIOException) {
throw (NetworkIOException)e;
} else if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
@SuppressWarnings({"PointlessArithmeticExpression", "PointlessBitwiseExpression"})
public static int parseBytes(final int b1, final int b2, final int b3, final int b4) {
return ((b1 << 3 * 8) & 0xFF000000)
| ((b2 << 2 * 8) & 0x00FF0000)
| ((b3 << 1 * 8) & 0x0000FF00)
| ((b4 << 0 * 8) & 0x000000FF);
}
// @see http://stackoverflow.com/questions/119328/how-do-i-truncate-a-java-string-to-fit-in-a-given-number-of-bytes-once-utf-8-enc
public static String truncateWhenUTF8(final String s, final int maxBytes) {
int b = 0;
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
// ranges from http://en.wikipedia.org/wiki/UTF-8
int skip = 0;
int more;
if (c <= 0x007f) {
more = 1;
}
else if (c <= 0x07FF) {
more = 2;
} else if (c <= 0xd7ff) {
more = 3;
} else if (c <= 0xDFFF) {
// surrogate area, consume next char as well
more = 4;
skip = 1;
} else {
more = 3;
}
if (b + more > maxBytes) {
return s.substring(0, i);
}
b += more;
i += skip;
}
return s;
}
}