diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/apns/develop.p12 b/apns/develop.p12 new file mode 100644 index 0000000..9c18df2 Binary files /dev/null and b/apns/develop.p12 differ diff --git a/apns/product.p12 b/apns/product.p12 new file mode 100644 index 0000000..f33b806 Binary files /dev/null and b/apns/product.p12 differ diff --git a/apns/voip.p12 b/apns/voip.p12 new file mode 100644 index 0000000..b0104f6 Binary files /dev/null and b/apns/voip.p12 differ diff --git a/apns/wfdemo_apns.p12 b/apns/wfdemo_apns.p12 new file mode 100644 index 0000000..3fa8d7c Binary files /dev/null and b/apns/wfdemo_apns.p12 differ diff --git a/apns/wfdemo_voip.p12 b/apns/wfdemo_voip.p12 new file mode 100755 index 0000000..ff698db Binary files /dev/null and b/apns/wfdemo_voip.p12 differ diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..5bf251c --- /dev/null +++ b/mvnw @@ -0,0 +1,225 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +echo $MAVEN_PROJECTBASEDIR +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..019bd74 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,143 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7d9df60 --- /dev/null +++ b/pom.xml @@ -0,0 +1,147 @@ + + + 4.0.0 + + cn.wildfirechat + push + 0.0.1-SNAPSHOT + jar + + push + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.6.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.xiaomi.push + mipush-sdk-server + 2.2.18 + system + ${project.basedir}/src/main/libs/MiPush_SDK_Server_2_2_19.jar + + + + + com.meizu.flyme + push-server-sdk + 1.2.7.20180307_release + + + + + com.google.code.gson + gson + 2.8.2 + + + + commons-io + commons-io + 2.5 + + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + + org.slf4j + slf4j-api + 1.7.5 + + + + org.slf4j + slf4j-log4j12 + 1.7.5 + + + + + commons-httpclient + commons-httpclient + 3.1 + + + + uk.org.lidalia + slf4j-test + 1.0.0-jdk6 + test + + + + com.google.code.findbugs + annotations + 2.0.3 + provided + + + + org.mockito + mockito-all + 1.9.5 + jar + test + + + + + com.fasterxml.jackson.core + jackson-core + 2.9.4 + + + com.fasterxml.jackson.core + jackson-databind + 2.9.4 + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.4 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/src/main/java/cn/wildfirechat/push/PushApplication.java b/src/main/java/cn/wildfirechat/push/PushApplication.java new file mode 100644 index 0000000..efe66de --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/PushApplication.java @@ -0,0 +1,11 @@ +package cn.wildfirechat.push; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PushApplication { + public static void main(String[] args) { + SpringApplication.run(PushApplication.class, args); + } +} diff --git a/src/main/java/cn/wildfirechat/push/PushController.java b/src/main/java/cn/wildfirechat/push/PushController.java new file mode 100644 index 0000000..392f02d --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/PushController.java @@ -0,0 +1,28 @@ +package cn.wildfirechat.push; + +import cn.wildfirechat.push.android.AndroidPushService; +import cn.wildfirechat.push.ios.IOSPushService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class PushController { + + @Autowired + private AndroidPushService mAndroidPushService; + + @Autowired + private IOSPushService mIOSPushService; + + @PostMapping(value = "/android/push", produces = "application/json;charset=UTF-8" ) + public Object androidPush(@RequestBody PushMessage pushMessage) { + return mAndroidPushService.push(pushMessage); + } + + @PostMapping(value = "/ios/push", produces = "application/json;charset=UTF-8" ) + public Object iOSPush(@RequestBody PushMessage pushMessage) { + return mIOSPushService.push(pushMessage); + } +} diff --git a/src/main/java/cn/wildfirechat/push/PushMessage.java b/src/main/java/cn/wildfirechat/push/PushMessage.java new file mode 100644 index 0000000..b530ca3 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/PushMessage.java @@ -0,0 +1,161 @@ +package cn.wildfirechat.push; + + +public class PushMessage { + public String sender; + public String senderName; + public int convType; + public String target; + public String targetName; + public int line; + public int cntType; + public long serverTime; + //消息的类型,普通消息通知栏;voip要透传。 + public int pushMessageType; + //推送类型,android推送分为小米/华为/魅族等。ios分别为开发和发布。 + public int pushType; + public String pushContent; + public int unReceivedMsg; + public int mentionedType; + public String packageName; + public String deviceToken; + public String voipDeviceToken; + public String language; + + + public String getSender() { + return sender; + } + + public void setSender(String sender) { + this.sender = sender; + } + + public String getSenderName() { + return senderName; + } + + public void setSenderName(String senderName) { + this.senderName = senderName; + } + + public int getConvType() { + return convType; + } + + public void setConvType(int convType) { + this.convType = convType; + } + + public String getTarget() { + return target; + } + + public void setTarget(String target) { + this.target = target; + } + + public String getTargetName() { + return targetName; + } + + public void setTargetName(String targetName) { + this.targetName = targetName; + } + + public int getLine() { + return line; + } + + public void setLine(int line) { + this.line = line; + } + + public int getCntType() { + return cntType; + } + + public void setCntType(int cntType) { + this.cntType = cntType; + } + + public long getServerTime() { + return serverTime; + } + + public void setServerTime(long serverTime) { + this.serverTime = serverTime; + } + + public int getPushMessageType() { + return pushMessageType; + } + + public void setPushMessageType(int pushMessageType) { + this.pushMessageType = pushMessageType; + } + + public int getPushType() { + return pushType; + } + + public void setPushType(int pushType) { + this.pushType = pushType; + } + + public String getPushContent() { + return pushContent; + } + + public void setPushContent(String pushContent) { + this.pushContent = pushContent; + } + + public int getUnReceivedMsg() { + return unReceivedMsg; + } + + public void setUnReceivedMsg(int unReceivedMsg) { + this.unReceivedMsg = unReceivedMsg; + } + + public int getMentionedType() { + return mentionedType; + } + + public void setMentionedType(int mentionedType) { + this.mentionedType = mentionedType; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getDeviceToken() { + return deviceToken; + } + + public void setDeviceToken(String deviceToken) { + this.deviceToken = deviceToken; + } + + public String getVoipDeviceToken() { + return voipDeviceToken; + } + + public void setVoipDeviceToken(String voipDeviceToken) { + this.voipDeviceToken = voipDeviceToken; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } +} diff --git a/src/main/java/cn/wildfirechat/push/PushMessageType.java b/src/main/java/cn/wildfirechat/push/PushMessageType.java new file mode 100644 index 0000000..535c24c --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/PushMessageType.java @@ -0,0 +1,7 @@ +package cn.wildfirechat.push; + +public interface PushMessageType { + int PUSH_MESSAGE_TYPE_NORMAL = 0; + int PUSH_MESSAGE_TYPE_VOIP_INVITE = 1; + int PUSH_MESSAGE_TYPE_VOIP_BYE = 2; +} diff --git a/src/main/java/cn/wildfirechat/push/android/AndroidPushService.java b/src/main/java/cn/wildfirechat/push/android/AndroidPushService.java new file mode 100644 index 0000000..744a58d --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/android/AndroidPushService.java @@ -0,0 +1,7 @@ +package cn.wildfirechat.push.android; + +import cn.wildfirechat.push.PushMessage; + +public interface AndroidPushService { + Object push(PushMessage pushMessage); +} diff --git a/src/main/java/cn/wildfirechat/push/android/AndroidPushServiceImpl.java b/src/main/java/cn/wildfirechat/push/android/AndroidPushServiceImpl.java new file mode 100644 index 0000000..2ebd7f2 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/android/AndroidPushServiceImpl.java @@ -0,0 +1,45 @@ +package cn.wildfirechat.push.android; + +import cn.wildfirechat.push.PushMessage; +import cn.wildfirechat.push.android.hms.HMSPush; +import cn.wildfirechat.push.android.meizu.MeiZuPush; +import cn.wildfirechat.push.android.xiaomi.XiaomiPush; +import com.google.gson.Gson; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class AndroidPushServiceImpl implements AndroidPushService { + private static final Logger LOG = LoggerFactory.getLogger(AndroidPushServiceImpl.class); + @Autowired + private HMSPush hmsPush; + + @Autowired + private MeiZuPush meiZuPush; + + @Autowired + private XiaomiPush xiaomiPush; + + + @Override + public Object push(PushMessage pushMessage) { + LOG.info("Android push {}", new Gson().toJson(pushMessage)); + switch (pushMessage.getPushType()) { + case AndroidPushType.ANDROID_PUSH_TYPE_XIAOMI: + xiaomiPush.push(pushMessage); + break; + case AndroidPushType.ANDROID_PUSH_TYPE_HUAWEI: + hmsPush.push(pushMessage); + break; + case AndroidPushType.ANDROID_PUSH_TYPE_MEIZU: + meiZuPush.push(pushMessage); + break; + default: + LOG.info("unknown push type"); + break; + } + return "ok"; + } +} diff --git a/src/main/java/cn/wildfirechat/push/android/AndroidPushType.java b/src/main/java/cn/wildfirechat/push/android/AndroidPushType.java new file mode 100644 index 0000000..f52ec04 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/android/AndroidPushType.java @@ -0,0 +1,7 @@ +package cn.wildfirechat.push.android; + +public interface AndroidPushType { + int ANDROID_PUSH_TYPE_XIAOMI = 1; + int ANDROID_PUSH_TYPE_HUAWEI = 2; + int ANDROID_PUSH_TYPE_MEIZU = 3; +} diff --git a/src/main/java/cn/wildfirechat/push/android/hms/HMSConfig.java b/src/main/java/cn/wildfirechat/push/android/hms/HMSConfig.java new file mode 100644 index 0000000..ca61c29 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/android/hms/HMSConfig.java @@ -0,0 +1,29 @@ +package cn.wildfirechat.push.android.hms; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@ConfigurationProperties(prefix="hms") +@PropertySource(value = "classpath:hms.properties") +public class HMSConfig { + private String appSecret; + private String appId; + + public String getAppSecret() { + return appSecret; + } + + public void setAppSecret(String appSecret) { + this.appSecret = appSecret; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } +} diff --git a/src/main/java/cn/wildfirechat/push/android/hms/HMSPush.java b/src/main/java/cn/wildfirechat/push/android/hms/HMSPush.java new file mode 100644 index 0000000..4a16e03 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/android/hms/HMSPush.java @@ -0,0 +1,136 @@ +package cn.wildfirechat.push.android.hms; + + +import cn.wildfirechat.push.PushMessage; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.google.gson.Gson; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.text.MessageFormat; +import java.util.List; + +@Component +public class HMSPush { + private static final Logger LOG = LoggerFactory.getLogger(HMSPush.class); + private static final String tokenUrl = "https://login.vmall.com/oauth2/token"; //获取认证Token的URL + private static final String apiUrl = "https://api.push.hicloud.com/pushsend.do"; //应用级消息下发API + private String accessToken;//下发通知消息的认证Token + private long tokenExpiredTime; //accessToken的过期时间 + + @Autowired + private HMSConfig mConfig; + + //获取下发通知消息的认证Token + private void refreshToken() throws IOException { + LOG.info("hms refresh token"); + String msgBody = MessageFormat.format( + "grant_type=client_credentials&client_secret={0}&client_id={1}", + URLEncoder.encode(mConfig.getAppSecret(), "UTF-8"), mConfig.getAppId()); + String response = httpPost(tokenUrl, msgBody, 5000, 5000); + JSONObject obj = JSONObject.parseObject(response); + accessToken = obj.getString("access_token"); + tokenExpiredTime = System.currentTimeMillis() + obj.getLong("expires_in") - 5*60*1000; + LOG.info("hms refresh token with result {}", response); + } + + //发送Push消息 + public void push(PushMessage pushMessage) { + if (tokenExpiredTime <= System.currentTimeMillis()) { + try { + refreshToken(); + } catch (IOException e) { + e.printStackTrace(); + } + } + /*PushManager.requestToken为客户端申请token的方法,可以调用多次以防止申请token失败*/ + /*PushToken不支持手动编写,需使用客户端的onToken方法获取*/ + JSONArray deviceTokens = new JSONArray();//目标设备Token + deviceTokens.add(pushMessage.getDeviceToken()); + + + JSONObject msg = new JSONObject(); + msg.put("type", 1);//3: 通知栏消息,异步透传消息请根据接口文档设置 + msg.put("body", new Gson().toJson(pushMessage));//通知栏消息body内容 + + JSONObject hps = new JSONObject();//华为PUSH消息总结构体 + hps.put("msg", msg); + + JSONObject payload = new JSONObject(); + payload.put("hps", hps); + + LOG.info("send push to HMS {}", payload); + + try { + String postBody = MessageFormat.format( + "access_token={0}&nsp_svc={1}&nsp_ts={2}&device_token_list={3}&payload={4}", + URLEncoder.encode(accessToken,"UTF-8"), + URLEncoder.encode("openpush.message.api.send","UTF-8"), + URLEncoder.encode(String.valueOf(System.currentTimeMillis() / 1000),"UTF-8"), + URLEncoder.encode(deviceTokens.toString(),"UTF-8"), + URLEncoder.encode(payload.toString(),"UTF-8")); + + String postUrl = apiUrl + "?nsp_ctx=" + URLEncoder.encode("{\"ver\":\"1\", \"appId\":\"" + mConfig.getAppId() + "\"}", "UTF-8"); + String response = httpPost(postUrl, postBody, 5000, 5000); + LOG.info("Push to {} response {}", pushMessage.getDeviceToken(), response); + } catch (IOException e) { + e.printStackTrace(); + LOG.info("Push to {} with exception", pushMessage.getDeviceToken(), e); + } + } + + public String httpPost(String httpUrl, String data, int connectTimeout, int readTimeout) throws IOException { + OutputStream outPut = null; + HttpURLConnection urlConnection = null; + InputStream in = null; + + try { + URL url = new URL(httpUrl); + urlConnection = (HttpURLConnection)url.openConnection(); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoOutput(true); + urlConnection.setDoInput(true); + urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + urlConnection.setConnectTimeout(connectTimeout); + urlConnection.setReadTimeout(readTimeout); + urlConnection.connect(); + + // POST data + outPut = urlConnection.getOutputStream(); + outPut.write(data.getBytes("UTF-8")); + outPut.flush(); + + // read response + if (urlConnection.getResponseCode() < 400) { + in = urlConnection.getInputStream(); + } else { + in = urlConnection.getErrorStream(); + } + + List lines = IOUtils.readLines(in, urlConnection.getContentEncoding()); + StringBuffer strBuf = new StringBuffer(); + for (String line : lines) { + strBuf.append(line); + } + LOG.info(strBuf.toString()); + return strBuf.toString(); + } + finally { + IOUtils.closeQuietly(outPut); + IOUtils.closeQuietly(in); + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } +} diff --git a/src/main/java/cn/wildfirechat/push/android/meizu/MeiZuConfig.java b/src/main/java/cn/wildfirechat/push/android/meizu/MeiZuConfig.java new file mode 100644 index 0000000..b494a76 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/android/meizu/MeiZuConfig.java @@ -0,0 +1,29 @@ +package cn.wildfirechat.push.android.meizu; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@ConfigurationProperties(prefix="meizu") +@PropertySource(value = "classpath:meizu.properties") +public class MeiZuConfig { + private String appSecret; + private long appId; + + public String getAppSecret() { + return appSecret; + } + + public void setAppSecret(String appSecret) { + this.appSecret = appSecret; + } + + public long getAppId() { + return appId; + } + + public void setAppId(long appId) { + this.appId = appId; + } +} diff --git a/src/main/java/cn/wildfirechat/push/android/meizu/MeiZuPush.java b/src/main/java/cn/wildfirechat/push/android/meizu/MeiZuPush.java new file mode 100644 index 0000000..eb01009 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/android/meizu/MeiZuPush.java @@ -0,0 +1,68 @@ +package cn.wildfirechat.push.android.meizu; + +import cn.wildfirechat.push.PushMessage; +import com.meizu.push.sdk.server.IFlymePush; +import com.meizu.push.sdk.server.constant.ResultPack; +import com.meizu.push.sdk.server.model.push.PushResult; +import com.meizu.push.sdk.server.model.push.VarnishedMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class MeiZuPush { + private static final Logger LOG = LoggerFactory.getLogger(MeiZuPush.class); + private IFlymePush flymePush; + + @PostConstruct + public void init() { + this.flymePush = new IFlymePush(mConfig.getAppSecret()); + } + + @Autowired + private MeiZuConfig mConfig; + + public void push(PushMessage pushMessage) { + //组装透传消息 + VarnishedMessage message = new VarnishedMessage.Builder() + .appId(mConfig.getAppId()) + .title("WildfireChat") + .content(pushMessage.pushContent) + .validTime(1) + .build(); + + //目标用户 + List pushIds = new ArrayList(); + pushIds.add(pushMessage.getDeviceToken()); + + try { + // 1 调用推送服务 + ResultPack result = flymePush.pushMessage(message, pushIds); + if (result.isSucceed()) { + // 2 调用推送服务成功 (其中map为设备的具体推送结果,一般业务针对超速的code类型做处理) + PushResult pushResult = result.value(); + String msgId = pushResult.getMsgId();//推送消息ID,用于推送流程明细排查 + Map> targetResultMap = pushResult.getRespTarget();//推送结果,全部推送成功,则map为empty + LOG.info("push result:" + pushResult); + if (targetResultMap != null && !targetResultMap.isEmpty()) { + System.err.println("push fail token:" + targetResultMap); + } + } else { + // 调用推送接口服务异常 eg: appId、appKey非法、推送消息非法..... + // result.code(); //服务异常码 + // result.comment();//服务异常描述 + LOG.info(String.format("pushMessage error code:%s comment:%s", result.code(), result.comment())); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiConfig.java b/src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiConfig.java new file mode 100644 index 0000000..6d6e952 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiConfig.java @@ -0,0 +1,20 @@ +package cn.wildfirechat.push.android.xiaomi; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@ConfigurationProperties(prefix="xiaomi") +@PropertySource(value = "classpath:xiaomi.properties") +public class XiaomiConfig { + private String appSecret; + + public String getAppSecret() { + return appSecret; + } + + public void setAppSecret(String appSecret) { + this.appSecret = appSecret; + } +} diff --git a/src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiPush.java b/src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiPush.java new file mode 100644 index 0000000..799d52b --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiPush.java @@ -0,0 +1,71 @@ +package cn.wildfirechat.push.android.xiaomi; + + +import cn.wildfirechat.push.PushMessage; +import cn.wildfirechat.push.PushMessageType; +import com.google.gson.Gson; +import com.xiaomi.xmpush.server.Constants; +import com.xiaomi.xmpush.server.Message; +import com.xiaomi.xmpush.server.Result; +import com.xiaomi.xmpush.server.Sender; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.json.simple.parser.ParseException; + + +import java.io.IOException; + +import static com.xiaomi.xmpush.server.Message.NOTIFY_TYPE_ALL; + +@Component +public class XiaomiPush { + private static final Logger LOG = LoggerFactory.getLogger(XiaomiPush.class); + @Autowired + private XiaomiConfig mConfig; + + + public void push(PushMessage pushMessage) { + Constants.useOfficial(); + Sender sender = new Sender(mConfig.getAppSecret()); + + Message message; + if(pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_NORMAL) { + //voip + long timeToLive = 60 * 1000; // 1 min + message = new Message.Builder() + .payload(new Gson().toJson(pushMessage)) + .restrictedPackageName(pushMessage.getPackageName()) + .passThrough(1) //透传 + .timeToLive(timeToLive) + .enableFlowControl(false) + .build(); + } else { + long timeToLive = 600 * 1000;//10 min + message = new Message.Builder() + .payload(new Gson().toJson(pushMessage)) + .title("新消息提醒") + .description(pushMessage.pushContent) + .notifyType(NOTIFY_TYPE_ALL) + .restrictedPackageName(pushMessage.getPackageName()) + .passThrough(0) + .timeToLive(timeToLive) + .enableFlowControl(true) + .build(); + } + + Result result = null; + try { + result = sender.send(message, pushMessage.getDeviceToken(), 3); + } catch (IOException e) { + e.printStackTrace(); + } catch (ParseException e) { + e.printStackTrace(); + } + + LOG.info("Server response: MessageId: " + result.getMessageId() + + " ErrorCode: " + result.getErrorCode().toString() + + " Reason: " + result.getReason()); + } +} diff --git a/src/main/java/cn/wildfirechat/push/ios/ApnsConfig.java b/src/main/java/cn/wildfirechat/push/ios/ApnsConfig.java new file mode 100644 index 0000000..63bd9f4 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/ios/ApnsConfig.java @@ -0,0 +1,86 @@ +package cn.wildfirechat.push.ios; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@ConfigurationProperties(prefix="apns") +@PropertySource(value = "classpath:apns.properties") +public class ApnsConfig { + String productCerPath; + String productCerPwd; + + String developCerPath; + String developCerPwd; + + String voipCerPath; + String voipCerPwd; + + String alert; + String voipAlert; + + public String getProductCerPath() { + return productCerPath; + } + + public void setProductCerPath(String productCerPath) { + this.productCerPath = productCerPath; + } + + public String getProductCerPwd() { + return productCerPwd; + } + + public void setProductCerPwd(String productCerPwd) { + this.productCerPwd = productCerPwd; + } + + public String getDevelopCerPath() { + return developCerPath; + } + + public void setDevelopCerPath(String developCerPath) { + this.developCerPath = developCerPath; + } + + public String getDevelopCerPwd() { + return developCerPwd; + } + + public void setDevelopCerPwd(String developCerPwd) { + this.developCerPwd = developCerPwd; + } + + public String getVoipCerPath() { + return voipCerPath; + } + + public void setVoipCerPath(String voipCerPath) { + this.voipCerPath = voipCerPath; + } + + public String getVoipCerPwd() { + return voipCerPwd; + } + + public void setVoipCerPwd(String voipCerPwd) { + this.voipCerPwd = voipCerPwd; + } + + public String getAlert() { + return alert; + } + + public void setAlert(String alert) { + this.alert = alert; + } + + public String getVoipAlert() { + return voipAlert; + } + + public void setVoipAlert(String voipAlert) { + this.voipAlert = voipAlert; + } +} diff --git a/src/main/java/cn/wildfirechat/push/ios/ApnsServer.java b/src/main/java/cn/wildfirechat/push/ios/ApnsServer.java new file mode 100644 index 0000000..790ef0c --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/ios/ApnsServer.java @@ -0,0 +1,187 @@ +package cn.wildfirechat.push.ios; + +import cn.wildfirechat.push.PushMessage; +import cn.wildfirechat.push.PushMessageType; +import com.notnoop.apns.*; +import com.notnoop.exceptions.ApnsDeliveryErrorException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; +import java.util.Date; +import java.util.Map; + +import static com.notnoop.apns.DeliveryError.INVALID_TOKEN; + +@Component +public class ApnsServer implements ApnsDelegate { + private static final Logger LOG = LoggerFactory.getLogger(ApnsServer.class); + @Override + public void messageSent(ApnsNotification message, boolean resent) { + LOG.info("APNS push sent:{}", message.getDeviceToken()); + } + + @Override + public void messageSendFailed(ApnsNotification message, Throwable e) { + LOG.info("APNS push failure:{}", e.getMessage()); + if(e instanceof ApnsDeliveryErrorException) { + ApnsDeliveryErrorException apnsDeliveryErrorException = (ApnsDeliveryErrorException)e; + LOG.info("APNS error code:{}", apnsDeliveryErrorException.getDeliveryError()); + if (apnsDeliveryErrorException.getDeliveryError() == INVALID_TOKEN) { + if (message.getDeviceId() != null) { + LOG.error("Invalide token!!!"); + } else { + LOG.error("APNS ERROR without deviceId:{}", message); + } + } + + } + } + + @Override + public void connectionClosed(DeliveryError e, int messageIdentifier) { + LOG.info("111"); + } + + @Override + public void cacheLengthExceeded(int newCacheLength) { + LOG.info("111"); + } + + @Override + public void notificationsResent(int resendCount) { + LOG.info("111"); + } + + ApnsService productSvc; + ApnsService developSvc; + ApnsService voipSvc; + + @Autowired + private ApnsConfig mConfig; + + @PostConstruct + private void init() { + if (StringUtils.isEmpty(mConfig.alert)) { + mConfig.alert = "default"; + } + + if (StringUtils.isEmpty(mConfig.voipAlert)) { + mConfig.alert = "default"; + } + + productSvc = APNS.newService() + .asBatched(3, 10) + .withAppleDestination(true) + .withCert(mConfig.productCerPath, mConfig.productCerPwd) + .withDelegate(this) + .build(); + + developSvc = APNS.newService() + .asBatched(3, 10) + .withAppleDestination(false) + .withCert(mConfig.developCerPath, mConfig.developCerPwd) + .withDelegate(this) + .build(); + + voipSvc = APNS.newService() + .withAppleDestination(true) + .withCert(mConfig.voipCerPath, mConfig.voipCerPwd) + .withDelegate(this) + .build(); + + productSvc.start(); + developSvc.start(); + voipSvc.start(); + } + + + public void pushMessage(PushMessage pushMessage) { + + ApnsService service = developSvc; + if (pushMessage.getPushType() == IOSPushType.IOS_PUSH_TYPE_DISTRIBUTION) { + if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_NORMAL || StringUtils.isEmpty(pushMessage.getVoipDeviceToken())) { + service = productSvc; + } else { + service = voipSvc; + } + } + + + if (service == null) { + LOG.error("Service not exist!!!!"); + return; + } + String sound = mConfig.alert; + + String pushContent = pushMessage.getPushContent(); + if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_INVITE) { + pushContent = "通话邀请"; + sound = mConfig.voipAlert; + } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE) { + pushContent = "通话结束"; + sound = null; + } + + int badge = pushMessage.getUnReceivedMsg(); + if (badge <= 0) { + badge = 1; + } + + String title; + String body; + //todo 这里需要加上语言的处理,客户端会上报自己的语言,在DeviceInfo那个类中 +// if (pushMessage.language == "zh_CN") { +// +// } else if(pushMessage.language == "US_EN") { +// +// } + if (pushMessage.convType == 1) { + title = pushMessage.targetName; + if (StringUtils.isEmpty(title)) { + title = "群聊"; + } + + if (StringUtils.isEmpty(pushMessage.senderName)) { + body = pushContent; + } else { + body = pushMessage.senderName + ":" + pushContent; + } + + if (pushMessage.mentionedType == 1) { + if (StringUtils.isEmpty(pushMessage.senderName)) { + body = "有人在群里@了你"; + } else { + body = pushMessage.senderName + "在群里@了你"; + } + } else if(pushMessage.mentionedType == 2) { + if (StringUtils.isEmpty(pushMessage.senderName)) { + body = "有人在群里@了大家"; + } else { + body = pushMessage.senderName + "在群里@了大家"; + } + } + } else { + if (StringUtils.isEmpty(pushMessage.senderName)) { + title = "消息"; + } else { + title = pushMessage.senderName; + } + body = pushContent; + } + + final String payload = APNS.newPayload().alertBody(body).badge(badge).alertTitle(title).sound(sound).build(); + final ApnsNotification goodMsg = service.push(service == voipSvc ? pushMessage.getVoipDeviceToken() : pushMessage.getDeviceToken(), payload, null); + LOG.info("Message id: " + goodMsg.getIdentifier()); + + + //检查key到期日期 + final Map inactiveDevices = service.getInactiveDevices(); + for (final Map.Entry ent : inactiveDevices.entrySet()) { + LOG.info("Inactive " + ent.getKey() + " at date " + ent.getValue()); + } + } +} diff --git a/src/main/java/cn/wildfirechat/push/ios/IOSPushService.java b/src/main/java/cn/wildfirechat/push/ios/IOSPushService.java new file mode 100644 index 0000000..0611f68 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/ios/IOSPushService.java @@ -0,0 +1,7 @@ +package cn.wildfirechat.push.ios; + +import cn.wildfirechat.push.PushMessage; + +public interface IOSPushService { + Object push(PushMessage pushMessage); +} diff --git a/src/main/java/cn/wildfirechat/push/ios/IOSPushServiceImpl.java b/src/main/java/cn/wildfirechat/push/ios/IOSPushServiceImpl.java new file mode 100644 index 0000000..c553d13 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/ios/IOSPushServiceImpl.java @@ -0,0 +1,22 @@ +package cn.wildfirechat.push.ios; + +import cn.wildfirechat.push.PushMessage; +import com.google.gson.Gson; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class IOSPushServiceImpl implements IOSPushService { + private static final Logger LOG = LoggerFactory.getLogger(IOSPushServiceImpl.class); + @Autowired + public ApnsServer apnsServer; + + @Override + public Object push(PushMessage pushMessage) { + LOG.info("iOS push {}", new Gson().toJson(pushMessage)); + apnsServer.pushMessage(pushMessage); + return "OK"; + } +} diff --git a/src/main/java/cn/wildfirechat/push/ios/IOSPushType.java b/src/main/java/cn/wildfirechat/push/ios/IOSPushType.java new file mode 100644 index 0000000..6562ca9 --- /dev/null +++ b/src/main/java/cn/wildfirechat/push/ios/IOSPushType.java @@ -0,0 +1,6 @@ +package cn.wildfirechat.push.ios; + +public interface IOSPushType { + int IOS_PUSH_TYPE_DISTRIBUTION = 0; + int IOS_PUSH_TYPE_DEVELOPEMENT = 1; +} diff --git a/src/main/java/com/notnoop/apns/APNS.java b/src/main/java/com/notnoop/apns/APNS.java new file mode 100755 index 0000000..9b8f68c --- /dev/null +++ b/src/main/java/com/notnoop/apns/APNS.java @@ -0,0 +1,57 @@ +/* + * 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; + +/** + * The main class to interact with the APNS Service. + * + * Provides an interface to create the {@link ApnsServiceBuilder} and + * {@code ApnsNotification} payload. + * + */ +public final class APNS { + + private APNS() { throw new AssertionError("Uninstantiable class"); } + + /** + * Returns a new Payload builder + */ + public static PayloadBuilder newPayload() { + return new PayloadBuilder(); + } + + /** + * Returns a new APNS Service for sending iPhone notifications + */ + public static ApnsServiceBuilder newService() { + return new ApnsServiceBuilder(); + } +} diff --git a/src/main/java/com/notnoop/apns/ApnsDelegate.java b/src/main/java/com/notnoop/apns/ApnsDelegate.java new file mode 100755 index 0000000..f48738c --- /dev/null +++ b/src/main/java/com/notnoop/apns/ApnsDelegate.java @@ -0,0 +1,92 @@ +/* + * 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; + +/** + * A delegate that gets notified of the status of notification delivery to the + * Apple Server. + * + * The delegate doesn't get notified when the notification actually arrives at + * the phone. + */ +public interface ApnsDelegate { + + /** + * Called when message was successfully sent to the Apple servers + * + * @param message the notification that was sent + * @param resent whether the notification was resent after an error + */ + public void messageSent(ApnsNotification message, boolean resent); + + /** + * Called when the delivery of the message failed for any reason + * + * If message is null, then your notification has been rejected by Apple but + * it has been removed from the cache so it is not possible to identify + * which notification caused the error. In this case subsequent + * notifications may be lost. If this happens you should consider increasing + * your cacheLength value to prevent data loss. + * + * @param message the notification that was attempted to be sent + * @param e the cause and description of the failure + */ + public void messageSendFailed(ApnsNotification message, Throwable e); + + /** + * The connection was closed and/or an error packet was received while + * monitoring was turned on. + * + * @param e the delivery error + * @param messageIdentifier id of the message that failed + */ + public void connectionClosed(DeliveryError e, int messageIdentifier); + + /** + * The resend cache needed a bigger size (while resending messages) + * + * @param newCacheLength new size of the resend cache. + */ + public void cacheLengthExceeded(int newCacheLength); + + /** + * A number of notifications has been queued for resending due to a error-response + * packet being received. + * + * @param resendCount the number of messages being queued for resend + */ + public void notificationsResent(int resendCount); + + /** + * A no operation delegate that does nothing! + */ + public final static ApnsDelegate EMPTY = new ApnsDelegateAdapter(); +} diff --git a/src/main/java/com/notnoop/apns/ApnsDelegateAdapter.java b/src/main/java/com/notnoop/apns/ApnsDelegateAdapter.java new file mode 100755 index 0000000..d10a675 --- /dev/null +++ b/src/main/java/com/notnoop/apns/ApnsDelegateAdapter.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * A no operation delegate that does nothing! + */ +public class ApnsDelegateAdapter implements ApnsDelegate { + + public void messageSent(ApnsNotification message, boolean resent) { + } + + public void messageSendFailed(ApnsNotification message, Throwable e) { + } + + public void connectionClosed(DeliveryError e, int messageIdentifier) { + } + + public void cacheLengthExceeded(int newCacheLength) { + } + + public void notificationsResent(int resendCount) { + } +} diff --git a/src/main/java/com/notnoop/apns/ApnsNotification.java b/src/main/java/com/notnoop/apns/ApnsNotification.java new file mode 100755 index 0000000..4cdeae3 --- /dev/null +++ b/src/main/java/com/notnoop/apns/ApnsNotification.java @@ -0,0 +1,76 @@ +/* + * 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; + +/** + * Represents an APNS notification to be sent to Apple service. + */ +public interface ApnsNotification { + + /** + * Returns the binary representation of the device token. + */ + public byte[] getDeviceToken(); + + /** + * Returns the binary representation of the payload. + * + */ + public byte[] getPayload(); + + /** + * Returns the identifier of the current message. The + * identifier is an application generated identifier. + * + * @return the notification identifier + */ + public int getIdentifier(); + + /** + * Returns the expiry date of the notification, a fixed UNIX + * epoch date expressed in seconds + * + * @return the expiry date of the notification + */ + public int getExpiry(); + + /** + * Returns the binary representation of the message as expected by the + * APNS server. + * + * The returned array can be used to sent directly to the APNS server + * (on the wire/socket) without any modification. + */ + public byte[] marshall(); + + + public String getDeviceId(); +} diff --git a/src/main/java/com/notnoop/apns/ApnsService.java b/src/main/java/com/notnoop/apns/ApnsService.java new file mode 100755 index 0000000..11bc4ef --- /dev/null +++ b/src/main/java/com/notnoop/apns/ApnsService.java @@ -0,0 +1,140 @@ +/* + * 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; + +import java.util.Collection; +import java.util.Date; +import java.util.Map; + +import com.notnoop.exceptions.NetworkIOException; + +/** + * Represents the connection and interface to the Apple APNS servers. + * + * The service is created by {@link ApnsServiceBuilder} like: + * + *
+ *   ApnsService = APNS.newService()
+ *                  .withCert("/path/to/certificate.p12", "MyCertPassword")
+ *                  .withSandboxDestination()
+ *                  .build()
+ * 
+ */ +public interface ApnsService { + + /** + * Sends a push notification with the provided {@code payload} to the + * iPhone of {@code deviceToken}. + * + * The payload needs to be a valid JSON object, otherwise it may fail + * silently. It is recommended to use {@link PayloadBuilder} to create + * one. + * + * @param deviceToken the destination iPhone device token + * @param payload The payload message + * @throws NetworkIOException if a network error occurred while + * attempting to send the message + */ + ApnsNotification push(String deviceToken, String payload, String deviceId) throws NetworkIOException; + + EnhancedApnsNotification push(String deviceToken, String payload, Date expiry, String deviceId) throws NetworkIOException; + + /** + * Sends a push notification with the provided {@code payload} to the + * iPhone of {@code deviceToken}. + * + * The payload needs to be a valid JSON object, otherwise it may fail + * silently. It is recommended to use {@link PayloadBuilder} to create + * one. + * + * @param deviceToken the destination iPhone device token + * @param payload The payload message + * @throws NetworkIOException if a network error occurred while + * attempting to send the message + */ + ApnsNotification push(byte[] deviceToken, byte[] payload, String deviceId) throws NetworkIOException; + + EnhancedApnsNotification push(byte[] deviceToken, byte[] payload, int expiry, String deviceId) throws NetworkIOException; + + /** + * Sends the provided notification {@code message} to the desired + * destination. + * @throws NetworkIOException if a network error occurred while + * attempting to send the message + */ + void push(ApnsNotification message) throws NetworkIOException; + + /** + * Starts the service. + * + * The underlying implementation may prepare its connections or + * data structures to be able to send the messages. + * + * This method is a blocking call, even if the service represents + * a Non-blocking push service. Once the service is returned, it is ready + * to accept push requests. + * + * @throws NetworkIOException if a network error occurred while + * starting the service + */ + void start(); + + /** + * Stops the service and frees any allocated resources it created for this + * service. + * + * The underlying implementation should close all connections it created, + * and possibly stop any threads as well. + */ + void stop(); + + /** + * Returns the list of devices that reported failed-delivery + * attempts to the Apple Feedback services. + * + * The result is map, mapping the device tokens as Hex Strings + * mapped to the timestamp when APNs determined that the + * application no longer exists on the device. + * @throws NetworkIOException if a network error occurred + * while retrieving invalid device connection + */ + Map getInactiveDevices() throws NetworkIOException; + + /** + * Test that the service is setup properly and the Apple servers + * are reachable. + * + * @throws NetworkIOException if the Apple servers aren't reachable + * or the service cannot send notifications for now + */ + void testConnection() throws NetworkIOException; + +} diff --git a/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java b/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java new file mode 100755 index 0000000..abc01f3 --- /dev/null +++ b/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java @@ -0,0 +1,760 @@ +/* + * 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; + +import com.notnoop.apns.internal.ApnsConnection; +import com.notnoop.apns.internal.ApnsConnectionImpl; +import com.notnoop.apns.internal.ApnsFeedbackConnection; +import com.notnoop.apns.internal.ApnsPooledConnection; +import com.notnoop.apns.internal.ApnsServiceImpl; +import com.notnoop.apns.internal.BatchApnsService; +import com.notnoop.apns.internal.QueuedApnsService; +import com.notnoop.apns.internal.SSLContextBuilder; +import com.notnoop.apns.internal.Utilities; +import com.notnoop.exceptions.InvalidSSLConfig; +import com.notnoop.exceptions.RuntimeIOException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import java.security.KeyStore; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; + +import static com.notnoop.apns.internal.Utilities.PRODUCTION_FEEDBACK_HOST; +import static com.notnoop.apns.internal.Utilities.PRODUCTION_FEEDBACK_PORT; +import static com.notnoop.apns.internal.Utilities.PRODUCTION_GATEWAY_HOST; +import static com.notnoop.apns.internal.Utilities.PRODUCTION_GATEWAY_PORT; +import static com.notnoop.apns.internal.Utilities.SANDBOX_FEEDBACK_HOST; +import static com.notnoop.apns.internal.Utilities.SANDBOX_FEEDBACK_PORT; +import static com.notnoop.apns.internal.Utilities.SANDBOX_GATEWAY_HOST; +import static com.notnoop.apns.internal.Utilities.SANDBOX_GATEWAY_PORT; +import static java.util.concurrent.Executors.defaultThreadFactory; + +/** + * The class is used to create instances of {@link ApnsService}. + * + * Note that this class is not synchronized. If multiple threads access a + * {@code ApnsServiceBuilder} instance concurrently, and at least on of the + * threads modifies one of the attributes structurally, it must be + * synchronized externally. + * + * Starting a new {@code ApnsService} is easy: + * + *
+ *   ApnsService = APNS.newService()
+ *    .withCert("/path/to/certificate.p12", "MyCertPassword")
+ *    .withSandboxDestination()
+ *    .build()
+ * 
+ */ +public class ApnsServiceBuilder { + private static final String KEYSTORE_TYPE = "PKCS12"; + private static final String KEY_ALGORITHM = ((java.security.Security.getProperty("ssl.KeyManagerFactory.algorithm") == null)? "sunx509" : java.security.Security.getProperty("ssl.KeyManagerFactory.algorithm")); + + private SSLContext sslContext; + + private int readTimeout; + private int connectTimeout; + + private String gatewayHost; + private int gatewayPort = -1; + + private String feedbackHost; + private int feedbackPort; + private int pooledMax = 1; + private int cacheLength = ApnsConnection.DEFAULT_CACHE_LENGTH; + private boolean autoAdjustCacheLength = true; + private ExecutorService executor; + + private ReconnectPolicy reconnectPolicy = ReconnectPolicy.Provided.EVERY_HALF_HOUR.newObject(); + private boolean isQueued; + private ThreadFactory queueThreadFactory; + + private boolean isBatched; + private int batchWaitTimeInSec; + private int batchMaxWaitTimeInSec; + private ScheduledExecutorService batchThreadPoolExecutor; + + private ApnsDelegate delegate = ApnsDelegate.EMPTY; + private Proxy proxy; + private String proxyUsername; + private String proxyPassword; + private boolean errorDetection = true; + private ThreadFactory errorDetectionThreadFactory; + + /** + * Constructs a new instance of {@code ApnsServiceBuilder} + */ + public ApnsServiceBuilder() { sslContext = null; } + + /** + * Specify the certificate used to connect to Apple APNS + * servers. This relies on the path (absolute or relative to + * working path) to the keystore (*.p12) containing the + * certificate, along with the given password. + * + * The keystore needs to be of PKCS12 and the keystore + * needs to be encrypted using the SunX509 algorithm. Both + * of these settings are the default. + * + * This library does not support password-less p12 certificates, due to a + * Oracle Java library + * Bug 6415637. There are three workarounds: use a password-protected + * certificate, use a different boot Java SDK implementation, or construct + * the `SSLContext` yourself! Needless to say, the password-protected + * certificate is most recommended option. + * + * @param fileName the path to the certificate + * @param password the password of the keystore + * @return this + * @throws RuntimeIOException if it {@code fileName} cannot be + * found or read + * @throws InvalidSSLConfig if fileName is invalid Keystore + * or the password is invalid + */ + public ApnsServiceBuilder withCert(String fileName, String password) + throws RuntimeIOException, InvalidSSLConfig { + FileInputStream stream = null; + try { + stream = new FileInputStream(fileName); + return withCert(stream, password); + } catch (FileNotFoundException e) { + throw new RuntimeIOException(e); + } finally { + Utilities.close(stream); + } + } + + /** + * Specify the certificate used to connect to Apple APNS + * servers. This relies on the stream of keystore (*.p12) + * containing the certificate, along with the given password. + * + * The keystore needs to be of PKCS12 and the keystore + * needs to be encrypted using the SunX509 algorithm. Both + * of these settings are the default. + * + * This library does not support password-less p12 certificates, due to a + * Oracle Java library + * Bug 6415637. There are three workarounds: use a password-protected + * certificate, use a different boot Java SDK implementation, or constract + * the `SSLContext` yourself! Needless to say, the password-protected + * certificate is most recommended option. + * + * @param stream the keystore represented as input stream + * @param password the password of the keystore + * @return this + * @throws InvalidSSLConfig if stream is invalid Keystore + * or the password is invalid + */ + public ApnsServiceBuilder withCert(InputStream stream, String password) + throws InvalidSSLConfig { + assertPasswordNotEmpty(password); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(stream, password, KEYSTORE_TYPE) + .withDefaultTrustKeyStore() + .build()); + } + + /** + * Specify the certificate used to connect to Apple APNS + * servers. This relies on a keystore (*.p12) + * containing the certificate, along with the given password. + * + * This library does not support password-less p12 certificates, due to a + * Oracle Java library + * Bug 6415637. There are three workarounds: use a password-protected + * certificate, use a different boot Java SDK implementation, or construct + * the `SSLContext` yourself! Needless to say, the password-protected + * certificate is most recommended option. + * + * @param keyStore the keystore + * @param password the password of the keystore + * @return this + * @throws InvalidSSLConfig if stream is invalid Keystore + * or the password is invalid + */ + public ApnsServiceBuilder withCert(KeyStore keyStore, String password) + throws InvalidSSLConfig { + assertPasswordNotEmpty(password); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(keyStore, password) + .withDefaultTrustKeyStore() + .build()); + } + + /** + * Specify the certificate store used to connect to Apple APNS + * servers. This relies on the stream of keystore (*.p12 | *.jks) + * containing the keys and certificates, along with the given + * password and alias. + * + * The keystore can be either PKCS12 or JKS and the keystore + * needs to be encrypted using the SunX509 algorithm. + * + * This library does not support password-less p12 certificates, due to a + * Oracle Java library + * Bug 6415637. There are three workarounds: use a password-protected + * certificate, use a different boot Java SDK implementation, or constract + * the `SSLContext` yourself! Needless to say, the password-protected + * certificate is most recommended option. + * + * @param stream the keystore represented as input stream + * @param password the password of the keystore + * @param alias the alias identifing the key to be used + * @return this + * @throws InvalidSSLConfig if stream is an invalid Keystore, + * the password is invalid or the alias is not found + */ + public ApnsServiceBuilder withCert(InputStream stream, String password, String alias) + throws InvalidSSLConfig { + assertPasswordNotEmpty(password); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(stream, password, KEYSTORE_TYPE, alias) + .withDefaultTrustKeyStore() + .build()); + } + + /** + * Specify the certificate store used to connect to Apple APNS + * servers. This relies on the stream of keystore (*.p12 | *.jks) + * containing the keys and certificates, along with the given + * password and alias. + * + * The keystore can be either PKCS12 or JKS and the keystore + * needs to be encrypted using the SunX509 algorithm. + * + * This library does not support password-less p12 certificates, due to a + * Oracle Java library + * Bug 6415637. There are three workarounds: use a password-protected + * certificate, use a different boot Java SDK implementation, or constract + * the `SSLContext` yourself! Needless to say, the password-protected + * certificate is most recommended option. + * + * @param keyStore the keystore + * @param password the password of the keystore + * @param alias the alias identifing the key to be used + * @return this + * @throws InvalidSSLConfig if stream is an invalid Keystore, + * the password is invalid or the alias is not found + */ + public ApnsServiceBuilder withCert(KeyStore keyStore, String password, String alias) + throws InvalidSSLConfig { + assertPasswordNotEmpty(password); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(keyStore, password, alias) + .withDefaultTrustKeyStore() + .build()); + } + + private void assertPasswordNotEmpty(String password) { + if (password == null || password.length() == 0) { + throw new IllegalArgumentException("Passwords must be specified." + + "Oracle Java SDK does not support passwordless p12 certificates"); + } + } + + /** + * Specify the SSLContext that should be used to initiate the + * connection to Apple Server. + * + * Most clients would use {@link #withCert(InputStream, String)} + * or {@link #withCert(String, String)} instead. But some + * clients may need to represent the Keystore in a different + * format than supported. + * + * @param sslContext Context to be used to create secure connections + * @return this + */ + public ApnsServiceBuilder withSSLContext(SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + + /** + * Specify the timeout value to be set in new setSoTimeout in created + * sockets, for both feedback and push connections, in milliseconds. + * @param readTimeout timeout value to be set in new setSoTimeout + * @return this + */ + public ApnsServiceBuilder withReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + /** + * Specify the timeout value to use for connectionTimeout in created + * sockets, for both feedback and push connections, in milliseconds. + * @param connectTimeout timeout value to use for connectionTimeout + * @return this + */ + public ApnsServiceBuilder withConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + /** + * Specify the gateway server for sending Apple iPhone + * notifications. + * + * Most clients should use {@link #withSandboxDestination()} + * or {@link #withProductionDestination()}. Clients may use + * this method to connect to mocking tests and such. + * + * @param host hostname the notification gateway of Apple + * @param port port of the notification gateway of Apple + * @return this + */ + public ApnsServiceBuilder withGatewayDestination(String host, int port) { + this.gatewayHost = host; + this.gatewayPort = port; + return this; + } + + /** + * Specify the Feedback for getting failed devices from + * Apple iPhone Push servers. + * + * Most clients should use {@link #withSandboxDestination()} + * or {@link #withProductionDestination()}. Clients may use + * this method to connect to mocking tests and such. + * + * @param host hostname of the feedback server of Apple + * @param port port of the feedback server of Apple + * @return this + */ + public ApnsServiceBuilder withFeedbackDestination(String host, int port) { + this.feedbackHost = host; + this.feedbackPort = port; + return this; + } + + /** + * Specify to use Apple servers as iPhone gateway and feedback servers. + * + * If the passed {@code isProduction} is true, then it connects to the + * production servers, otherwise, it connects to the sandbox servers + * + * @param isProduction determines which Apple servers should be used: + * production or sandbox + * @return this + */ + public ApnsServiceBuilder withAppleDestination(boolean isProduction) { + if (isProduction) { + return withProductionDestination(); + } else { + return withSandboxDestination(); + } + } + + /** + * Specify to use the Apple sandbox servers as iPhone gateway + * and feedback servers. + * + * This is desired when in testing and pushing notifications + * with a development provision. + * + * @return this + */ + public ApnsServiceBuilder withSandboxDestination() { + return withGatewayDestination(SANDBOX_GATEWAY_HOST, SANDBOX_GATEWAY_PORT) + .withFeedbackDestination(SANDBOX_FEEDBACK_HOST, SANDBOX_FEEDBACK_PORT); + } + + /** + * Specify to use the Apple Production servers as iPhone gateway + * and feedback servers. + * + * This is desired when sending notifications to devices with + * a production provision (whether through App Store or Ad hoc + * distribution). + * + * @return this + */ + public ApnsServiceBuilder withProductionDestination() { + return withGatewayDestination(PRODUCTION_GATEWAY_HOST, PRODUCTION_GATEWAY_PORT) + .withFeedbackDestination(PRODUCTION_FEEDBACK_HOST, PRODUCTION_FEEDBACK_PORT); + } + + /** + * Specify the reconnection policy for the socket connection. + * + * Note: This option has no effect when using non-blocking + * connections. + */ + public ApnsServiceBuilder withReconnectPolicy(ReconnectPolicy rp) { + this.reconnectPolicy = rp; + return this; + } + + /** + * Specify if the notification cache should auto adjust. + * Default is true + * + * @param autoAdjustCacheLength the notification cache should auto adjust. + * @return this + */ + public ApnsServiceBuilder withAutoAdjustCacheLength(boolean autoAdjustCacheLength) { + this.autoAdjustCacheLength = autoAdjustCacheLength; + return this; + } + + /** + * Specify the reconnection policy for the socket connection. + * + * Note: This option has no effect when using non-blocking + * connections. + */ + public ApnsServiceBuilder withReconnectPolicy(ReconnectPolicy.Provided rp) { + this.reconnectPolicy = rp.newObject(); + return this; + } + + /** + * Specify the address of the SOCKS proxy the connection should + * use. + * + *

Read the + * Java Networking and Proxies guide to understand the + * proxies complexity. + * + *

Be aware that this method only handles SOCKS proxies, not + * HTTPS proxies. Use {@link #withProxy(Proxy)} instead. + * + * @param host the hostname of the SOCKS proxy + * @param port the port of the SOCKS proxy server + * @return this + */ + public ApnsServiceBuilder withSocksProxy(String host, int port) { + Proxy proxy = new Proxy(Proxy.Type.SOCKS, + new InetSocketAddress(host, port)); + return withProxy(proxy); + } + + /** + * Specify the proxy and the authentication parameters to be used + * to establish the connections to Apple Servers. + * + *

Read the + * Java Networking and Proxies guide to understand the + * proxies complexity. + * + * @param proxy the proxy object to be used to create connections + * @param proxyUsername a String object representing the username of the proxy server + * @param proxyPassword a String object representing the password of the proxy server + * @return this + */ + public ApnsServiceBuilder withAuthProxy(Proxy proxy, String proxyUsername, String proxyPassword) { + this.proxy = proxy; + this.proxyUsername = proxyUsername; + this.proxyPassword = proxyPassword; + return this; + } + + /** + * Specify the proxy to be used to establish the connections + * to Apple Servers + * + *

Read the + * Java Networking and Proxies guide to understand the + * proxies complexity. + * + * @param proxy the proxy object to be used to create connections + * @return this + */ + public ApnsServiceBuilder withProxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + + /** + * Specify the number of notifications to cache for error purposes. + * Default is 100 + * + * @param cacheLength Number of notifications to cache for error purposes + * @return this + */ + public ApnsServiceBuilder withCacheLength(int cacheLength) { + this.cacheLength = cacheLength; + return this; + } + + /** + * Specify the socket to be used as underlying socket to connect + * to the APN service. + * + * This assumes that the socket connects to a SOCKS proxy. + * + * @deprecated use {@link ApnsServiceBuilder#withProxy(Proxy)} instead + * @param proxySocket the underlying socket for connections + * @return this + */ + @Deprecated + public ApnsServiceBuilder withProxySocket(Socket proxySocket) { + return this.withProxy(new Proxy(Proxy.Type.SOCKS, + proxySocket.getRemoteSocketAddress())); + } + + /** + * Constructs a pool of connections to the notification servers. + * + * Apple servers recommend using a pooled connection up to + * 15 concurrent persistent connections to the gateways. + * + * Note: This option has no effect when using non-blocking + * connections. + */ + public ApnsServiceBuilder asPool(int maxConnections) { + return asPool(Executors.newFixedThreadPool(maxConnections), maxConnections); + } + + /** + * Constructs a pool of connections to the notification servers. + * + * Apple servers recommend using a pooled connection up to + * 15 concurrent persistent connections to the gateways. + * + * Note: This option has no effect when using non-blocking + * connections. + * + * Note: The maxConnections here is used as a hint to how many connections + * get created. + */ + public ApnsServiceBuilder asPool(ExecutorService executor, int maxConnections) { + this.pooledMax = maxConnections; + this.executor = executor; + return this; + } + + /** + * Constructs a new thread with a processing queue to process + * notification requests. + * + * @return this + */ + public ApnsServiceBuilder asQueued() { + return asQueued(Executors.defaultThreadFactory()); + } + + /** + * Constructs a new thread with a processing queue to process + * notification requests. + * + * @param threadFactory + * thread factory to use for queue processing + * @return this + */ + public ApnsServiceBuilder asQueued(ThreadFactory threadFactory) { + this.isQueued = true; + this.queueThreadFactory = threadFactory; + return this; + } + + /** + * Construct service which will process notification requests in batch. + * After each request batch will wait waitTimeInSec (set as 5sec) for more request to come + * before executing but not more than maxWaitTimeInSec (set as 10sec) + * + * Note: It is not recommended to use pooled connection + */ + public ApnsServiceBuilder asBatched() { + return asBatched(5, 10); + } + + /** + * Construct service which will process notification requests in batch. + * After each request batch will wait waitTimeInSec for more request to come + * before executing but not more than maxWaitTimeInSec + * + * Note: It is not recommended to use pooled connection + * + * @param waitTimeInSec + * time to wait for more notification request before executing + * batch + * @param maxWaitTimeInSec + * maximum wait time for batch before executing + */ + public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec) { + return asBatched(waitTimeInSec, maxWaitTimeInSec, (ThreadFactory)null); + } + + /** + * Construct service which will process notification requests in batch. + * After each request batch will wait waitTimeInSec for more request to come + * before executing but not more than maxWaitTimeInSec + * + * Each batch creates new connection and close it after finished. + * In case reconnect policy is specified it will be applied by batch processing. + * E.g.: {@link ReconnectPolicy.Provided#EVERY_HALF_HOUR} will reconnect the connection in case batch is running for more than half an hour + * + * Note: It is not recommended to use pooled connection + * + * @param waitTimeInSec + * time to wait for more notification request before executing + * batch + * @param maxWaitTimeInSec + * maximum wait time for batch before executing + * @param threadFactory + * thread factory to use for batch processing + */ + public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec, ThreadFactory threadFactory) { + return asBatched(waitTimeInSec, maxWaitTimeInSec, new ScheduledThreadPoolExecutor(1, threadFactory != null ? threadFactory : defaultThreadFactory())); + } + + /** + * Construct service which will process notification requests in batch. + * After each request batch will wait waitTimeInSec for more request to come + * before executing but not more than maxWaitTimeInSec + * + * Each batch creates new connection and close it after finished. + * In case reconnect policy is specified it will be applied by batch processing. + * E.g.: {@link ReconnectPolicy.Provided#EVERY_HALF_HOUR} will reconnect the connection in case batch is running for more than half an hour + * + * Note: It is not recommended to use pooled connection + * + * @param waitTimeInSec + * time to wait for more notification request before executing + * batch + * @param maxWaitTimeInSec + * maximum wait time for batch before executing + * @param batchThreadPoolExecutor + * executor for batched processing (may be null) + */ + public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec, ScheduledExecutorService batchThreadPoolExecutor) { + this.isBatched = true; + this.batchWaitTimeInSec = waitTimeInSec; + this.batchMaxWaitTimeInSec = maxWaitTimeInSec; + this.batchThreadPoolExecutor = batchThreadPoolExecutor; + return this; + } + + /** + * Sets the delegate of the service, that gets notified of the + * status of message delivery. + * + * Note: This option has no effect when using non-blocking + * connections. + */ + public ApnsServiceBuilder withDelegate(ApnsDelegate delegate) { + this.delegate = delegate == null ? ApnsDelegate.EMPTY : delegate; + return this; + } + + /** + * Disables the enhanced error detection, enabled by the + * enhanced push notification interface. Error detection is + * enabled by default. + * + * This setting is desired when the application shouldn't spawn + * new threads. + * + * @return this + */ + public ApnsServiceBuilder withNoErrorDetection() { + this.errorDetection = false; + return this; + } + + /** + * Provide a custom source for threads used for monitoring connections. + * + * This setting is desired when the application must obtain threads from a + * controlled environment Google App Engine. + * @param threadFactory + * thread factory to use for error detection + * @return this + */ + public ApnsServiceBuilder withErrorDetectionThreadFactory(ThreadFactory threadFactory) { + this.errorDetectionThreadFactory = threadFactory; + return this; + } + + /** + * Returns a fully initialized instance of {@link ApnsService}, + * according to the requested settings. + * + * @return a new instance of ApnsService + */ + public ApnsService build() { + checkInitialization(); + ApnsService service; + + SSLSocketFactory sslFactory = sslContext.getSocketFactory(); + ApnsFeedbackConnection feedback = new ApnsFeedbackConnection(sslFactory, feedbackHost, feedbackPort, proxy, readTimeout, connectTimeout, proxyUsername, proxyPassword); + + ApnsConnection conn = new ApnsConnectionImpl(sslFactory, gatewayHost, + gatewayPort, proxy, proxyUsername, proxyPassword, reconnectPolicy, + delegate, errorDetection, errorDetectionThreadFactory, cacheLength, + autoAdjustCacheLength, readTimeout, connectTimeout); + if (pooledMax != 1) { + conn = new ApnsPooledConnection(conn, pooledMax, executor); + } + + service = new ApnsServiceImpl(conn, feedback); + + if (isQueued) { + service = new QueuedApnsService(service, queueThreadFactory); + } + + if (isBatched) { + service = new BatchApnsService(conn, feedback, batchWaitTimeInSec, batchMaxWaitTimeInSec, batchThreadPoolExecutor); + } + + service.start(); + + return service; + } + + private void checkInitialization() { + if (sslContext == null) + throw new IllegalStateException( + "SSL Certificates and attribute are not initialized\n" + + "Use .withCert() methods."); + if (gatewayHost == null || gatewayPort == -1) + throw new IllegalStateException( + "The Destination APNS server is not stated\n" + + "Use .withDestination(), withSandboxDestination(), " + + "or withProductionDestination()."); + } +} diff --git a/src/main/java/com/notnoop/apns/DeliveryError.java b/src/main/java/com/notnoop/apns/DeliveryError.java new file mode 100755 index 0000000..4ba33f6 --- /dev/null +++ b/src/main/java/com/notnoop/apns/DeliveryError.java @@ -0,0 +1,81 @@ +/* + * 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; + +/** + * Errors in delivery that may get reported by Apple APN servers + */ +public enum DeliveryError { + /** + * Connection closed without any error. + * + * This may occur if the APN service faces an invalid simple + * APNS notification while running in enhanced mode + */ + NO_ERROR(0), + PROCESSING_ERROR(1), + MISSING_DEVICE_TOKEN(2), + MISSING_TOPIC(3), + MISSING_PAYLOAD(4), + INVALID_TOKEN_SIZE(5), + INVALID_TOPIC_SIZE(6), + INVALID_PAYLOAD_SIZE(7), + INVALID_TOKEN(8), + + NONE(255), + UNKNOWN(254); + + private final byte code; + DeliveryError(int code) { + this.code = (byte)code; + } + + /** The status code as specified by Apple */ + public int code() { + return code; + } + + /** + * Returns the appropriate {@code DeliveryError} enum + * corresponding to the Apple provided status code + * + * @param code status code provided by Apple + * @return the appropriate DeliveryError + */ + public static DeliveryError ofCode(int code) { + for (DeliveryError e : DeliveryError.values()) { + if (e.code == code) + return e; + } + + return UNKNOWN; + } +} diff --git a/src/main/java/com/notnoop/apns/EnhancedApnsNotification.java b/src/main/java/com/notnoop/apns/EnhancedApnsNotification.java new file mode 100755 index 0000000..b5b2661 --- /dev/null +++ b/src/main/java/com/notnoop/apns/EnhancedApnsNotification.java @@ -0,0 +1,191 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import com.notnoop.apns.internal.Utilities; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.UnsupportedEncodingException; + +/** + * Represents an APNS notification to be sent to Apple service. + */ +public class EnhancedApnsNotification implements ApnsNotification { + + private static final Logger LOGGER = LoggerFactory.getLogger(EnhancedApnsNotification.class); + private final static byte COMMAND = 1; + private static AtomicInteger nextId = new AtomicInteger(0); + private final int identifier; + private final int expiry; + private final byte[] deviceToken; + private final byte[] payload; + private String deviceId; + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public static int INCREMENT_ID() { + return nextId.incrementAndGet(); + } + + /** + * The infinite future for the purposes of Apple expiry date + */ + public final static int MAXIMUM_EXPIRY = Integer.MAX_VALUE; + + /** + * Constructs an instance of {@code ApnsNotification}. + * + * The message encodes the payload with a {@code UTF-8} encoding. + * + * @param dtoken The Hex of the device token of the destination phone + * @param payload The payload message to be sent + */ + public EnhancedApnsNotification( + int identifier, int expiryTime, + String dtoken, String payload) { + this.identifier = identifier; + this.expiry = expiryTime; + this.deviceToken = Utilities.decodeHex(dtoken); + this.payload = Utilities.toUTF8Bytes(payload); + } + + /** + * Constructs an instance of {@code ApnsNotification}. + * + * @param dtoken The binary representation of the destination device token + * @param payload The binary representation of the payload to be sent + */ + public EnhancedApnsNotification( + int identifier, int expiryTime, + byte[] dtoken, byte[] payload) { + this.identifier = identifier; + this.expiry = expiryTime; + this.deviceToken = Utilities.copyOf(dtoken); + this.payload = Utilities.copyOf(payload); + } + + /** + * Returns the binary representation of the device token. + * + */ + public byte[] getDeviceToken() { + return Utilities.copyOf(deviceToken); + } + + /** + * Returns the binary representation of the payload. + * + */ + public byte[] getPayload() { + return Utilities.copyOf(payload); + } + + public int getIdentifier() { + return identifier; + } + + public int getExpiry() { + return expiry; + } + + private byte[] marshall; + /** + * Returns the binary representation of the message as expected by the + * APNS server. + * + * The returned array can be used to sent directly to the APNS server + * (on the wire/socket) without any modification. + */ + public byte[] marshall() { + if (marshall == null) { + marshall = Utilities.marshallEnhanced(COMMAND, identifier, + expiry, deviceToken, payload); + } + return marshall.clone(); + } + + @Override + public String getDeviceId() { + return deviceId; + } + + /** + * Returns the length of the message in bytes as it is encoded on the wire. + * + * Apple require the message to be of length 255 bytes or less. + * + * @return length of encoded message in bytes + */ + public int length() { + int length = 1 + 4 + 4 + 2 + deviceToken.length + 2 + payload.length; + final int marshalledLength = marshall().length; + assert marshalledLength == length; + return length; + } + + @Override + public int hashCode() { + return (21 + + 31 * identifier + + 31 * expiry + + 31 * Arrays.hashCode(deviceToken) + + 31 * Arrays.hashCode(payload)); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof EnhancedApnsNotification)) + return false; + EnhancedApnsNotification o = (EnhancedApnsNotification)obj; + return (identifier == o.identifier + && expiry == o.expiry + && Arrays.equals(this.deviceToken, o.deviceToken) + && Arrays.equals(this.payload, o.payload)); + } + + @Override + @SuppressFBWarnings("DE_MIGHT_IGNORE") + public String toString() { + String payloadString; + try { + payloadString = new String(payload, "UTF-8"); + } catch (UnsupportedEncodingException ex) { + LOGGER.debug("UTF-8 charset not found on the JRE", ex); + payloadString = "???"; + } + return "Message(Id="+identifier+"; Token="+Utilities.encodeHex(deviceToken)+"; Payload="+payloadString+")"; + } +} diff --git a/src/main/java/com/notnoop/apns/PayloadBuilder.java b/src/main/java/com/notnoop/apns/PayloadBuilder.java new file mode 100755 index 0000000..798c22a --- /dev/null +++ b/src/main/java/com/notnoop/apns/PayloadBuilder.java @@ -0,0 +1,535 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.notnoop.apns.internal.Utilities; + +/** + * Represents a builder for constructing Payload requests, as + * specified by Apple Push Notification Programming Guide. + */ +public final class PayloadBuilder { + private static final ObjectMapper mapper = new ObjectMapper(); + + private final Map root; + private final Map aps; + private final Map customAlert; + + /** + * Constructs a new instance of {@code PayloadBuilder} + */ + PayloadBuilder() { + root = new HashMap(); + aps = new HashMap(); + customAlert = new HashMap(); + } + + /** + * Sets the alert body text, the text the appears to the user, + * to the passed value + * + * @param alert the text to appear to the user + * @return this + */ + public PayloadBuilder alertBody(final String alert) { + customAlert.put("body", alert); + return this; + } + + /** + * Sets the alert title text, the text the appears to the user, + * to the passed value. + * + * Used on iOS 8.2, iWatch and also Safari + * + * @param title the text to appear to the user + * @return this + */ + public PayloadBuilder alertTitle(final String title) { + customAlert.put("title", title); + return this; + } + + /** + * The key to a title string in the Localizable.strings file for the current localization. + * + * @param key the localizable message title key + * @return this + */ + public PayloadBuilder localizedTitleKey(final String key) { + customAlert.put("title-loc-key", key); + return this; + } + + /** + * Sets the arguments for the localizable title key. + * + * @param arguments the arguments to the localized alert message + * @return this + */ + public PayloadBuilder localizedTitleArguments(final Collection arguments) { + customAlert.put("title-loc-args", arguments); + return this; + } + + /** + * Sets the arguments for the localizable title key. + * + * @param arguments the arguments to the localized alert message + * @return this + */ + public PayloadBuilder localizedTitleArguments(final String... arguments) { + return localizedTitleArguments(Arrays.asList(arguments)); + } + + /** + * Sets the alert action text + * + * @param action The label of the action button + * @return this + */ + public PayloadBuilder alertAction(final String action) { + customAlert.put("action", action); + return this; + } + + /** + * Sets the "url-args" key that are paired with the placeholders + * inside the urlFormatString value of your website.json file. + * The order of the placeholders in the URL format string determines + * the order of the values supplied by the url-args array. + * + * @param urlArgs the values to be paired with the placeholders inside + * the urlFormatString value of your website.json file. + * @return this + */ + public PayloadBuilder urlArgs(final String... urlArgs){ + aps.put("url-args", urlArgs); + return this; + } + + /** + * Sets the alert sound to be played. + * + * Passing {@code null} disables the notification sound. + * + * @param sound the file name or song name to be played + * when receiving the notification + * @return this + */ + public PayloadBuilder sound(final String sound) { + if (sound != null) { + aps.put("sound", sound); + } else { + aps.remove("sound"); + } + return this; + } + + /** + * Sets the category of the notification for iOS8 notification + * actions. See 13 minutes into "What's new in iOS Notifications" + * + * Passing {@code null} removes the category. + * + * @param category the name of the category supplied to the app + * when receiving the notification + * @return this + */ + public PayloadBuilder category(final String category) { + if (category != null) { + aps.put("category", category); + } else { + aps.remove("category"); + } + return this; + } + + /** + * Sets the notification badge to be displayed next to the + * application icon. + * + * The passed value is the value that should be displayed + * (it will be added to the previous badge number), and + * a badge of 0 clears the badge indicator. + * + * @param badge the badge number to be displayed + * @return this + */ + public PayloadBuilder badge(final int badge) { + aps.put("badge", badge); + return this; + } + + /** + * Requests clearing of the badge number next to the application + * icon. + * + * This is an alias to {@code badge(0)}. + * + * @return this + */ + public PayloadBuilder clearBadge() { + return badge(0); + } + + /** + * Sets the value of action button (the right button to be + * displayed). The default value is "View". + * + * The value can be either the simple String to be displayed or + * a localizable key, and the iPhone will show the appropriate + * localized message. + * + * A {@code null} actionKey indicates no additional button + * is displayed, just the Cancel button. + * + * @param actionKey the title of the additional button + * @return this + */ + public PayloadBuilder actionKey(final String actionKey) { + customAlert.put("action-loc-key", actionKey); + return this; + } + + /** + * Set the notification view to display an action button. + * + * This is an alias to {@code actionKey(null)} + * + * @return this + */ + public PayloadBuilder noActionButton() { + return actionKey(null); + } + + /** + * Sets the notification type to be a 'newstand' notification. + * + * A Newstand Notification targets the Newstands app so that the app + * updates the subscription info and content. + * + * @return this + */ + public PayloadBuilder forNewsstand() { + aps.put("content-available", 1); + return this; + } + + /** + * With iOS7 it is possible to have the application wake up before the user opens the app. + * + * The same key-word can also be used to send 'silent' notifications. With these 'silent' notification + * a different app delegate is being invoked, allowing the app to perform background tasks. + * + * @return this + */ + public PayloadBuilder instantDeliveryOrSilentNotification() { + aps.put("content-available", 1); + return this; + } + + /** + * Set the notification localized key for the alert body + * message. + * + * @param key the localizable message body key + * @return this + */ + public PayloadBuilder localizedKey(final String key) { + customAlert.put("loc-key", key); + return this; + } + + /** + * Sets the arguments for the alert message localizable message. + * + * The iPhone doesn't localize the arguments. + * + * @param arguments the arguments to the localized alert message + * @return this + */ + public PayloadBuilder localizedArguments(final Collection arguments) { + customAlert.put("loc-args", arguments); + return this; + } + + /** + * Sets the arguments for the alert message localizable message. + * + * The iPhone doesn't localize the arguments. + * + * @param arguments the arguments to the localized alert message + * @return this + */ + public PayloadBuilder localizedArguments(final String... arguments) { + return localizedArguments(Arrays.asList(arguments)); + } + + /** + * Sets the launch image file for the push notification + * + * @param launchImage the filename of the image file in the + * application bundle. + * @return this + */ + public PayloadBuilder launchImage(final String launchImage) { + customAlert.put("launch-image", launchImage); + return this; + } + + /** + * Sets any application-specific custom fields. The values + * are presented to the application and the iPhone doesn't + * display them automatically. + * + * This can be used to pass specific values (urls, ids, etc) to + * the application in addition to the notification message + * itself. + * + * @param key the custom field name + * @param value the custom field value + * @return this + */ + public PayloadBuilder customField(final String key, final Object value) { + root.put(key, value); + return this; + } + + public PayloadBuilder mdm(final String s) { + return customField("mdm", s); + } + + /** + * Set any application-specific custom fields. These values + * are presented to the application and the iPhone doesn't + * display them automatically. + * + * This method *adds* the custom fields in the map to the + * payload, and subsequent calls add but doesn't reset the + * custom fields. + * + * @param values the custom map + * @return this + */ + public PayloadBuilder customFields(final Map values) { + root.putAll(values); + return this; + } + + /** + * Returns the length of payload bytes once marshaled to bytes + * + * @return the length of the payload + */ + public int length() { + return copy().buildBytes().length; + } + + /** + * Returns true if the payload built so far is larger than + * the size permitted by Apple (which is 2048 bytes). + * + * @return true if the result payload is too long + */ + public boolean isTooLong() { + return length() > Utilities.MAX_PAYLOAD_LENGTH; + } + + /** + * Shrinks the alert message body so that the resulting payload + * message fits within the passed expected payload length. + * + * This method performs best-effort approach, and its behavior + * is unspecified when handling alerts where the payload + * without body is already longer than the permitted size, or + * if the break occurs within word. + * + * @param payloadLength the expected max size of the payload + * @return this + */ + public PayloadBuilder resizeAlertBody(final int payloadLength) { + return resizeAlertBody(payloadLength, ""); + } + + /** + * Shrinks the alert message body so that the resulting payload + * message fits within the passed expected payload length. + * + * This method performs best-effort approach, and its behavior + * is unspecified when handling alerts where the payload + * without body is already longer than the permitted size, or + * if the break occurs within word. + * + * @param payloadLength the expected max size of the payload + * @param postfix for the truncated body, e.g. "..." + * @return this + */ + public PayloadBuilder resizeAlertBody(final int payloadLength, final String postfix) { + int currLength = length(); + if (currLength <= payloadLength) { + return this; + } + + // now we are sure that truncation is required + String body = (String)customAlert.get("body"); + + final int acceptableSize = Utilities.toUTF8Bytes(body).length + - (currLength - payloadLength + + Utilities.toUTF8Bytes(postfix).length); + body = Utilities.truncateWhenUTF8(body, acceptableSize) + postfix; + + // set it back + customAlert.put("body", body); + + // calculate the length again + currLength = length(); + + if(currLength > payloadLength) { + // string is still too long, just remove the body as the body is + // anyway not the cause OR the postfix might be too long + customAlert.remove("body"); + } + + return this; + } + + /** + * Shrinks the alert message body so that the resulting payload + * message fits within require Apple specification (2048 bytes). + * + * This method performs best-effort approach, and its behavior + * is unspecified when handling alerts where the payload + * without body is already longer than the permitted size, or + * if the break occurs within word. + * + * @return this + */ + public PayloadBuilder shrinkBody() { + return shrinkBody(""); + } + + /** + * Shrinks the alert message body so that the resulting payload + * message fits within require Apple specification (2048 bytes). + * + * This method performs best-effort approach, and its behavior + * is unspecified when handling alerts where the payload + * without body is already longer than the permitted size, or + * if the break occurs within word. + * + * @param postfix for the truncated body, e.g. "..." + * + * @return this + */ + public PayloadBuilder shrinkBody(final String postfix) { + return resizeAlertBody(Utilities.MAX_PAYLOAD_LENGTH, postfix); + } + + /** + * Returns the JSON String representation of the payload + * according to Apple APNS specification + * + * @return the String representation as expected by Apple + */ + public String build() { + if (!root.containsKey("mdm")) { + insertCustomAlert(); + root.put("aps", aps); + } + try { + return mapper.writeValueAsString(root); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + private void insertCustomAlert() { + switch (customAlert.size()) { + case 0: + aps.remove("alert"); + break; + case 1: + if (customAlert.containsKey("body")) { + aps.put("alert", customAlert.get("body")); + break; + } + // else follow through + //$FALL-THROUGH$ + default: + aps.put("alert", customAlert); + } + } + + /** + * Returns the bytes representation of the payload according to + * Apple APNS specification + * + * @return the bytes as expected by Apple + */ + public byte[] buildBytes() { + return Utilities.toUTF8Bytes(build()); + } + + @Override + public String toString() { + return build(); + } + + private PayloadBuilder(final Map root, + final Map aps, + final Map customAlert) { + this.root = new HashMap(root); + this.aps = new HashMap(aps); + this.customAlert = new HashMap(customAlert); + } + + /** + * Returns a copy of this builder + * + * @return a copy of this builder + */ + public PayloadBuilder copy() { + return new PayloadBuilder(root, aps, customAlert); + } + + /** + * @return a new instance of Payload Builder + */ + public static PayloadBuilder newPayload() { + return new PayloadBuilder(); + } +} diff --git a/src/main/java/com/notnoop/apns/ReconnectPolicy.java b/src/main/java/com/notnoop/apns/ReconnectPolicy.java new file mode 100755 index 0000000..bff7dd9 --- /dev/null +++ b/src/main/java/com/notnoop/apns/ReconnectPolicy.java @@ -0,0 +1,118 @@ +/* + * 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; + +import com.notnoop.apns.internal.ReconnectPolicies; + +/** + * Represents the reconnection policy for the library. + * + * Each object should be used exclusively for one + * {@code ApnsService} only. + */ +public interface ReconnectPolicy { + /** + * Returns {@code true} if the library should initiate a new + * connection for sending the message. + * + * The library calls this method at every message push. + * + * @return true if the library should be reconnected + */ + public boolean shouldReconnect(); + + /** + * Callback method to be called whenever the library + * makes a new connection + */ + public void reconnected(); + + /** + * Returns a deep copy of this reconnection policy, if needed. + * + * Subclasses may return this instance if the object is immutable. + */ + public ReconnectPolicy copy(); + + /** + * Types of the library provided reconnection policies. + * + * This should capture most of the commonly used cases. + */ + public enum Provided { + /** + * Only reconnect if absolutely needed, e.g. when the connection is dropped. + *

+ * Apple recommends using a persistent connection. This improves the latency of sending push notification messages. + *

+ * The down-side is that once the connection is closed ungracefully (e.g. because Apple server drops it), the library wouldn't + * detect such failure and not warn against the messages sent after the drop before the detection. + */ + NEVER { + @Override + public ReconnectPolicy newObject() { + return new ReconnectPolicies.Never(); + } + }, + + /** + * Makes a new connection if the current connection has lasted for more than half an hour. + *

+ * This is the recommended mode. + *

+ * This is the sweat-spot in my experiments between dropped connections while minimizing latency. + */ + EVERY_HALF_HOUR { + @Override + public ReconnectPolicy newObject() { + return new ReconnectPolicies.EveryHalfHour(); + } + }, + + /** + * Makes a new connection for every message being sent. + * + * This option ensures that each message is actually + * delivered to Apple. + * + * If you send a lot of messages though, + * Apple may consider your requests to be a DoS attack. + */ + EVERY_NOTIFICATION { + @Override + public ReconnectPolicy newObject() { + return new ReconnectPolicies.Always(); + } + }; + + abstract ReconnectPolicy newObject(); + } +} diff --git a/src/main/java/com/notnoop/apns/SimpleApnsNotification.java b/src/main/java/com/notnoop/apns/SimpleApnsNotification.java new file mode 100755 index 0000000..6c2dad7 --- /dev/null +++ b/src/main/java/com/notnoop/apns/SimpleApnsNotification.java @@ -0,0 +1,172 @@ +/* + * 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; + +import java.util.Arrays; + +import com.notnoop.apns.internal.Utilities; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.UnsupportedEncodingException; + +/** + * Represents an APNS notification to be sent to Apple service. This is for legacy use only + * and should not be used in new development. + * https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/LegacyFormat.html + * + * This SimpleApnsNotification also only has limited error handling (by the APNS closing the connection + * when a bad message was received) This prevents us from location the malformed notification. + * + * As push messages sent after a malformed notification are discarded by APNS messages will get lost + * and not be delivered with the SimpleApnsNotification. + * + * @deprecated use EnhancedApnsNotification instead. + */ +@SuppressWarnings("deprecation") +@Deprecated +public class SimpleApnsNotification implements ApnsNotification { + + private static final Logger LOGGER = LoggerFactory.getLogger(SimpleApnsNotification.class); + private final static byte COMMAND = 0; + private final byte[] deviceToken; + private final byte[] payload; + + /** + * Constructs an instance of {@code ApnsNotification}. + * + * The message encodes the payload with a {@code UTF-8} encoding. + * + * @param dtoken The Hex of the device token of the destination phone + * @param payload The payload message to be sent + */ + public SimpleApnsNotification(String dtoken, String payload) { + this.deviceToken = Utilities.decodeHex(dtoken); + this.payload = Utilities.toUTF8Bytes(payload); + } + + /** + * Constructs an instance of {@code ApnsNotification}. + * + * @param dtoken The binary representation of the destination device token + * @param payload The binary representation of the payload to be sent + */ + public SimpleApnsNotification(byte[] dtoken, byte[] payload) { + this.deviceToken = Utilities.copyOf(dtoken); + this.payload = Utilities.copyOf(payload); + } + + /** + * Returns the binary representation of the device token. + * + */ + public byte[] getDeviceToken() { + return Utilities.copyOf(deviceToken); + } + + /** + * Returns the binary representation of the payload. + * + */ + public byte[] getPayload() { + return Utilities.copyOf(payload); + } + + private byte[] marshall; + /** + * Returns the binary representation of the message as expected by the + * APNS server. + * + * The returned array can be used to sent directly to the APNS server + * (on the wire/socket) without any modification. + */ + public byte[] marshall() { + if (marshall == null) + marshall = Utilities.marshall(COMMAND, deviceToken, payload); + return marshall.clone(); + } + + /** + * Returns the length of the message in bytes as it is encoded on the wire. + * + * Apple require the message to be of length 255 bytes or less. + * + * @return length of encoded message in bytes + */ + public int length() { + int length = 1 + 2 + deviceToken.length + 2 + payload.length; + final int marshalledLength = marshall().length; + assert marshalledLength == length; + return length; + } + + @Override + public int hashCode() { + return 21 + + 31 * Arrays.hashCode(deviceToken) + + 31 * Arrays.hashCode(payload); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SimpleApnsNotification)) + return false; + SimpleApnsNotification o = (SimpleApnsNotification)obj; + return Arrays.equals(this.deviceToken, o.deviceToken) + && Arrays.equals(this.payload, o.payload); + } + + public int getIdentifier() { + return -1; + } + + public int getExpiry() { + return -1; + } + + @Override + @SuppressFBWarnings("DE_MIGHT_IGNORE") + public String toString() { + String payloadString; + try { + payloadString = new String(payload, "UTF-8"); + } catch (UnsupportedEncodingException ex) { + LOGGER.debug("UTF-8 charset not found on the JRE", ex); + payloadString = "???"; + } + return "Message(Token="+Utilities.encodeHex(deviceToken)+"; Payload="+payloadString+")"; + } + + @Override + public String getDeviceId() { + return null; + } +} diff --git a/src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java b/src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java new file mode 100755 index 0000000..cf7d957 --- /dev/null +++ b/src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * A delegate that also gets notified just before a notification is being delivered to the + * Apple Server. + */ +public interface StartSendingApnsDelegate extends ApnsDelegate { + + /** + * Called when message is about to be sent to the Apple servers. + * + * @param message the notification that is about to be sent + * @param resent whether the notification is being resent after an error + */ + public void startSending(ApnsNotification message, boolean resent); + +} diff --git a/src/main/java/com/notnoop/apns/internal/AbstractApnsService.java b/src/main/java/com/notnoop/apns/internal/AbstractApnsService.java new file mode 100755 index 0000000..5a721e9 --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/AbstractApnsService.java @@ -0,0 +1,90 @@ +/* + * 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.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import com.notnoop.apns.ApnsNotification; +import com.notnoop.apns.ApnsService; +import com.notnoop.apns.EnhancedApnsNotification; +import com.notnoop.exceptions.NetworkIOException; + +abstract class AbstractApnsService implements ApnsService { + private ApnsFeedbackConnection feedback; + private AtomicInteger c = new AtomicInteger(); + + public AbstractApnsService(ApnsFeedbackConnection feedback) { + this.feedback = feedback; + } + + public EnhancedApnsNotification push(String deviceToken, String payload, String deviceId) throws NetworkIOException { + EnhancedApnsNotification notification = + new EnhancedApnsNotification(c.incrementAndGet(), EnhancedApnsNotification.MAXIMUM_EXPIRY, deviceToken, payload); + notification.setDeviceId(deviceId); + push(notification); + return notification; + } + + public EnhancedApnsNotification push(String deviceToken, String payload, Date expiry, String deviceId) throws NetworkIOException { + EnhancedApnsNotification notification = + new EnhancedApnsNotification(c.incrementAndGet(), (int)(expiry.getTime() / 1000), deviceToken, payload); + notification.setDeviceId(deviceId); + push(notification); + return notification; + } + + public EnhancedApnsNotification push(byte[] deviceToken, byte[] payload, String deviceId) throws NetworkIOException { + EnhancedApnsNotification notification = + new EnhancedApnsNotification(c.incrementAndGet(), EnhancedApnsNotification.MAXIMUM_EXPIRY, deviceToken, payload); + notification.setDeviceId(deviceId); + push(notification); + return notification; + } + + public EnhancedApnsNotification push(byte[] deviceToken, byte[] payload, int expiry, String deviceId) throws NetworkIOException { + EnhancedApnsNotification notification = + new EnhancedApnsNotification(c.incrementAndGet(), expiry, deviceToken, payload); + notification.setDeviceId(deviceId); + push(notification); + return notification; + } + + public abstract void push(ApnsNotification message) throws NetworkIOException; + + public Map getInactiveDevices() throws NetworkIOException { + return feedback.getInactiveDevices(); + } +} diff --git a/src/main/java/com/notnoop/apns/internal/ApnsConnection.java b/src/main/java/com/notnoop/apns/internal/ApnsConnection.java new file mode 100755 index 0000000..757a81b --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/ApnsConnection.java @@ -0,0 +1,52 @@ +/* + * 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.Closeable; + +import com.notnoop.apns.ApnsNotification; +import com.notnoop.exceptions.NetworkIOException; + +public interface ApnsConnection extends Closeable { + + //Default number of notifications to keep for error purposes + public static final int DEFAULT_CACHE_LENGTH = 100; + + void sendMessage(ApnsNotification m) throws NetworkIOException; + + void testConnection() throws NetworkIOException; + + ApnsConnection copy(); + + void setCacheLength(int cacheLength); + + int getCacheLength(); +} diff --git a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java new file mode 100755 index 0000000..825aea3 --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java @@ -0,0 +1,412 @@ +/* + * 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.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocketFactory; +import com.notnoop.apns.ApnsDelegate; +import com.notnoop.apns.StartSendingApnsDelegate; +import com.notnoop.apns.ApnsNotification; +import com.notnoop.apns.DeliveryError; +import com.notnoop.apns.EnhancedApnsNotification; +import com.notnoop.apns.ReconnectPolicy; +import com.notnoop.exceptions.ApnsDeliveryErrorException; +import com.notnoop.exceptions.NetworkIOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ApnsConnectionImpl implements ApnsConnection { + + private static final Logger logger = LoggerFactory.getLogger(ApnsConnectionImpl.class); + + private final SocketFactory factory; + private final String host; + private final int port; + private final int readTimeout; + private final int connectTimeout; + private final Proxy proxy; + private final String proxyUsername; + private final String proxyPassword; + private final ReconnectPolicy reconnectPolicy; + private final ApnsDelegate delegate; + private int cacheLength; + private final boolean errorDetection; + private final ThreadFactory threadFactory; + private final boolean autoAdjustCacheLength; + private final ConcurrentLinkedQueue cachedNotifications, notificationsBuffer; + private Socket socket; + private final AtomicInteger threadId = new AtomicInteger(0); + + public ApnsConnectionImpl(SocketFactory factory, String host, int port) { + this(factory, host, port, new ReconnectPolicies.Never(), ApnsDelegate.EMPTY); + } + + private ApnsConnectionImpl(SocketFactory factory, String host, int port, ReconnectPolicy reconnectPolicy, ApnsDelegate delegate) { + this(factory, host, port, null, null, null, reconnectPolicy, delegate); + } + + private ApnsConnectionImpl(SocketFactory factory, String host, int port, Proxy proxy, String proxyUsername, String proxyPassword, + ReconnectPolicy reconnectPolicy, ApnsDelegate delegate) { + this(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy, delegate, false, null, + ApnsConnection.DEFAULT_CACHE_LENGTH, true, 0, 0); + } + + public ApnsConnectionImpl(SocketFactory factory, String host, int port, Proxy proxy, String proxyUsername, String proxyPassword, + ReconnectPolicy reconnectPolicy, ApnsDelegate delegate, boolean errorDetection, ThreadFactory tf, int cacheLength, + boolean autoAdjustCacheLength, int readTimeout, int connectTimeout) { + this.factory = factory; + this.host = host; + this.port = port; + this.reconnectPolicy = reconnectPolicy; + this.delegate = delegate == null ? ApnsDelegate.EMPTY : delegate; + this.proxy = proxy; + this.errorDetection = errorDetection; + this.threadFactory = tf == null ? defaultThreadFactory() : tf; + this.cacheLength = cacheLength; + this.autoAdjustCacheLength = autoAdjustCacheLength; + this.readTimeout = readTimeout; + this.connectTimeout = connectTimeout; + this.proxyUsername = proxyUsername; + this.proxyPassword = proxyPassword; + cachedNotifications = new ConcurrentLinkedQueue(); + notificationsBuffer = new ConcurrentLinkedQueue(); + } + + private ThreadFactory defaultThreadFactory() { + return new ThreadFactory() { + ThreadFactory wrapped = Executors.defaultThreadFactory(); + @Override + public Thread newThread( Runnable r ) + { + Thread result = wrapped.newThread(r); + result.setName("MonitoringThread-"+threadId.incrementAndGet()); + result.setDaemon(true); + return result; + } + }; + } + + public synchronized void close() { + Utilities.close(socket); + } + + private void monitorSocket(final Socket socketToMonitor) { + logger.debug("Launching Monitoring Thread for socket {}", socketToMonitor); + + Thread t = threadFactory.newThread(new Runnable() { + final static int EXPECTED_SIZE = 6; + + @SuppressWarnings("InfiniteLoopStatement") + @Override + public void run() { + logger.debug("Started monitoring thread"); + try { + InputStream in; + try { + in = socketToMonitor.getInputStream(); + } catch (IOException ioe) { + logger.warn("The value of socket is null", ioe); + in = null; + } + + byte[] bytes = new byte[EXPECTED_SIZE]; + while (in != null && readPacket(in, bytes)) { + logger.debug("Error-response packet {}", Utilities.encodeHex(bytes)); + // Quickly close socket, so we won't ever try to send push notifications + // using the defective socket. + Utilities.close(socketToMonitor); + + int command = bytes[0] & 0xFF; + if (command != 8) { + throw new IOException("Unexpected command byte " + command); + } + int statusCode = bytes[1] & 0xFF; + DeliveryError e = DeliveryError.ofCode(statusCode); + + int id = Utilities.parseBytes(bytes[2], bytes[3], bytes[4], bytes[5]); + + logger.debug("Closed connection cause={}; id={}", e, id); + delegate.connectionClosed(e, id); + + Queue tempCache = new LinkedList(); + ApnsNotification notification = null; + boolean foundNotification = false; + + while (!cachedNotifications.isEmpty()) { + notification = cachedNotifications.poll(); + logger.debug("Candidate for removal, message id {}", notification.getIdentifier()); + + if (notification.getIdentifier() == id) { + logger.debug("Bad message found {}", notification.getIdentifier()); + foundNotification = true; + break; + } + tempCache.add(notification); + } + + if (foundNotification) { + logger.debug("delegate.messageSendFailed, message id {}", notification.getIdentifier()); + delegate.messageSendFailed(notification, new ApnsDeliveryErrorException(e)); + } else { + cachedNotifications.addAll(tempCache); + int resendSize = tempCache.size(); + logger.warn("Received error for message that wasn't in the cache..."); + if (autoAdjustCacheLength) { + cacheLength = cacheLength + (resendSize / 2); + delegate.cacheLengthExceeded(cacheLength); + } + logger.debug("delegate.messageSendFailed, unknown id"); + delegate.messageSendFailed(null, new ApnsDeliveryErrorException(e)); + } + + int resendSize = 0; + + while (!cachedNotifications.isEmpty()) { + + resendSize++; + final ApnsNotification resendNotification = cachedNotifications.poll(); + logger.debug("Queuing for resend {}", resendNotification.getIdentifier()); + notificationsBuffer.add(resendNotification); + } + logger.debug("resending {} notifications", resendSize); + delegate.notificationsResent(resendSize); + } + logger.debug("Monitoring input stream closed by EOF"); + + } catch (IOException e) { + // An exception when reading the error code is non-critical, it will cause another retry + // sending the message. Other than providing a more stable network connection to the APNS + // server we can't do much about it - so let's not spam the application's error log. + logger.info("Exception while waiting for error code", e); + delegate.connectionClosed(DeliveryError.UNKNOWN, -1); + } finally { + Utilities.close(socketToMonitor); + drainBuffer(); + } + } + + /** + * Read a packet like in.readFully(bytes) does - but do not throw an exception and return false if nothing + * could be read at all. + * @param in the input stream + * @param bytes the array to be filled with data + * @return true if a packet as been read, false if the stream was at EOF right at the beginning. + * @throws IOException When a problem occurs, especially EOFException when there's an EOF in the middle of the packet. + */ + private boolean readPacket(final InputStream in, final byte[] bytes) throws IOException { + final int len = bytes.length; + int n = 0; + while (n < len) { + try { + int count = in.read(bytes, n, len - n); + if (count < 0) { + throw new EOFException("EOF after reading "+n+" bytes of new packet."); + } + n += count; + } catch (IOException ioe) { + if (n == 0) + return false; + throw new IOException("Error after reading "+n+" bytes of packet", ioe); + } + } + return true; + } + }); + t.start(); + } + + private synchronized Socket getOrCreateSocket(boolean resend) throws NetworkIOException { + if (reconnectPolicy.shouldReconnect()) { + logger.debug("Reconnecting due to reconnectPolicy dictating it"); + Utilities.close(socket); + socket = null; + } + + if (socket == null || socket.isClosed()) { + try { + if (proxy == null) { + socket = factory.createSocket(host, port); + logger.debug("Connected new socket {}", socket); + } else if (proxy.type() == Proxy.Type.HTTP) { + TlsTunnelBuilder tunnelBuilder = new TlsTunnelBuilder(); + socket = tunnelBuilder.build((SSLSocketFactory) factory, proxy, proxyUsername, proxyPassword, host, port); + logger.debug("Connected new socket through http tunnel {}", socket); + } else { + boolean success = false; + Socket proxySocket = null; + try { + proxySocket = new Socket(proxy); + proxySocket.connect(new InetSocketAddress(host, port), connectTimeout); + socket = ((SSLSocketFactory) factory).createSocket(proxySocket, host, port, false); + success = true; + } finally { + if (!success) { + Utilities.close(proxySocket); + } + } + logger.debug("Connected new socket through socks tunnel {}", socket); + } + + socket.setSoTimeout(readTimeout); + socket.setKeepAlive(true); + + if (errorDetection) { + monitorSocket(socket); + } + + reconnectPolicy.reconnected(); + logger.debug("Made a new connection to APNS"); + } catch (IOException e) { + logger.error("Couldn't connect to APNS server", e); + // indicate to clients whether this is a resend or initial send + throw new NetworkIOException(e, resend); + } + } + return socket; + } + + int DELAY_IN_MS = 1000; + private static final int RETRIES = 3; + + public synchronized void sendMessage(ApnsNotification m) throws NetworkIOException { + sendMessage(m, false); + drainBuffer(); + } + + private synchronized void sendMessage(ApnsNotification m, boolean fromBuffer) throws NetworkIOException { + logger.debug("sendMessage {} fromBuffer: {}", m, fromBuffer); + + if (delegate instanceof StartSendingApnsDelegate) { + ((StartSendingApnsDelegate) delegate).startSending(m, fromBuffer); + } + + int attempts = 0; + while (true) { + try { + attempts++; + Socket socket = getOrCreateSocket(fromBuffer); + socket.getOutputStream().write(m.marshall()); + socket.getOutputStream().flush(); + cacheNotification(m); + + delegate.messageSent(m, fromBuffer); + + //logger.debug("Message \"{}\" sent", m); + attempts = 0; + break; + } catch (SSLHandshakeException e) { + // No use retrying this, it's dead Jim + throw new NetworkIOException(e); + } catch (IOException e) { + Utilities.close(socket); + if (attempts >= RETRIES) { + logger.error("Couldn't send message after " + RETRIES + " retries." + m, e); + delegate.messageSendFailed(m, e); + Utilities.wrapAndThrowAsRuntimeException(e); + } + // The first failure might be due to closed connection (which in turn might be caused by + // a message containing a bad token), so don't delay for the first retry. + // + // Additionally we don't want to spam the log file in this case, only after the second retry + // which uses the delay. + + if (attempts != 1) { + logger.info("Failed to send message " + m + "... trying again after delay", e); + Utilities.sleep(DELAY_IN_MS); + } + } + } + } + + private synchronized void drainBuffer() { + logger.debug("draining buffer"); + while (!notificationsBuffer.isEmpty()) { + final ApnsNotification notification = notificationsBuffer.poll(); + try { + sendMessage(notification, true); + } + catch (NetworkIOException ex) { + // at this point we are retrying the submission of messages but failing to connect to APNS, therefore + // notify the client of this + delegate.messageSendFailed(notification, ex); + } + } + } + + private void cacheNotification(ApnsNotification notification) { + cachedNotifications.add(notification); + while (cachedNotifications.size() > cacheLength) { + cachedNotifications.poll(); + logger.debug("Removing notification from cache " + notification); + } + } + + public ApnsConnectionImpl copy() { + return new ApnsConnectionImpl(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy.copy(), delegate, + errorDetection, threadFactory, cacheLength, autoAdjustCacheLength, readTimeout, connectTimeout); + } + + public void testConnection() throws NetworkIOException { + ApnsConnectionImpl testConnection = null; + try { + testConnection = + new ApnsConnectionImpl(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy.copy(), delegate); + final ApnsNotification notification = new EnhancedApnsNotification(0, 0, new byte[]{0}, new byte[]{0}); + testConnection.sendMessage(notification); + } finally { + if (testConnection != null) { + testConnection.close(); + } + } + } + + public void setCacheLength(int cacheLength) { + this.cacheLength = cacheLength; + } + + public int getCacheLength() { + return cacheLength; + } +} diff --git a/src/main/java/com/notnoop/apns/internal/ApnsFeedbackConnection.java b/src/main/java/com/notnoop/apns/internal/ApnsFeedbackConnection.java new file mode 100755 index 0000000..26db208 --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/ApnsFeedbackConnection.java @@ -0,0 +1,121 @@ +/* + * 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.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import java.util.Date; +import java.util.Map; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.notnoop.exceptions.NetworkIOException; + +public class ApnsFeedbackConnection { + private static final Logger logger = LoggerFactory.getLogger(ApnsFeedbackConnection.class); + + private final SocketFactory factory; + private final String host; + private final int port; + private final Proxy proxy; + private final int readTimeout; + private final int connectTimeout; + private final String proxyUsername; + private final String proxyPassword; + + public ApnsFeedbackConnection(final SocketFactory factory, final String host, final int port) { + this(factory, host, port, null, 0, 0, null, null); + } + + public ApnsFeedbackConnection(final SocketFactory factory, final String host, final int port, + final Proxy proxy, int readTimeout, int connectTimeout, final String proxyUsername, final String proxyPassword) { + this.factory = factory; + this.host = host; + this.port = port; + this.proxy = proxy; + this.readTimeout = readTimeout; + this.connectTimeout = connectTimeout; + this.proxyUsername = proxyUsername; + this.proxyPassword = proxyPassword; + } + + int DELAY_IN_MS = 1000; + private static final int RETRIES = 3; + + public Map getInactiveDevices() throws NetworkIOException { + int attempts = 0; + while (true) { + try { + attempts++; + final Map result = getInactiveDevicesImpl(); + + attempts = 0; + return result; + } catch (final Exception e) { + logger.warn("Failed to retrieve invalid devices", e); + if (attempts >= RETRIES) { + logger.error("Couldn't get feedback connection", e); + Utilities.wrapAndThrowAsRuntimeException(e); + } + Utilities.sleep(DELAY_IN_MS); + } + } + } + + public Map getInactiveDevicesImpl() throws IOException { + Socket proxySocket = null; + Socket socket = null; + try { + if (proxy == null) { + socket = factory.createSocket(host, port); + } else if (proxy.type() == Proxy.Type.HTTP) { + TlsTunnelBuilder tunnelBuilder = new TlsTunnelBuilder(); + socket = tunnelBuilder.build((SSLSocketFactory) factory, proxy, proxyUsername, proxyPassword, host, port); + } else { + proxySocket = new Socket(proxy); + proxySocket.connect(new InetSocketAddress(host, port), connectTimeout); + socket = ((SSLSocketFactory) factory).createSocket(proxySocket, host, port, false); + } + socket.setSoTimeout(readTimeout); + socket.setKeepAlive(true); + final InputStream stream = socket.getInputStream(); + return Utilities.parseFeedbackStream(stream); + } finally { + Utilities.close(socket); + Utilities.close(proxySocket); + } + } + +} diff --git a/src/main/java/com/notnoop/apns/internal/ApnsPooledConnection.java b/src/main/java/com/notnoop/apns/internal/ApnsPooledConnection.java new file mode 100755 index 0000000..e27d170 --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/ApnsPooledConnection.java @@ -0,0 +1,121 @@ +/* + * 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.util.concurrent.*; +import com.notnoop.apns.ApnsNotification; +import com.notnoop.exceptions.NetworkIOException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ApnsPooledConnection implements ApnsConnection { + private static final Logger logger = LoggerFactory.getLogger(ApnsPooledConnection.class); + + private final ApnsConnection prototype; + private final int max; + + private final ExecutorService executors; + private final ConcurrentLinkedQueue prototypes; + + public ApnsPooledConnection(ApnsConnection prototype, int max) { + this(prototype, max, Executors.newFixedThreadPool(max)); + } + + public ApnsPooledConnection(ApnsConnection prototype, int max, ExecutorService executors) { + this.prototype = prototype; + this.max = max; + + this.executors = executors; + this.prototypes = new ConcurrentLinkedQueue(); + } + + private final ThreadLocal uniquePrototype = + new ThreadLocal() { + protected ApnsConnection initialValue() { + ApnsConnection newCopy = prototype.copy(); + prototypes.add(newCopy); + return newCopy; + } + }; + + public void sendMessage(final ApnsNotification m) throws NetworkIOException { + Future future = executors.submit(new Callable() { + public Void call() throws Exception { + uniquePrototype.get().sendMessage(m); + return null; + } + }); + try { + future.get(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } catch (ExecutionException ee) { + if (ee.getCause() instanceof NetworkIOException) { + throw (NetworkIOException) ee.getCause(); + } + } + } + + public ApnsConnection copy() { + // TODO: Should copy executor properly.... What should copy do + // really?! + return new ApnsPooledConnection(prototype, max); + } + + public void close() { + executors.shutdown(); + try { + executors.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.warn("pool termination interrupted", e); + } + for (ApnsConnection conn : prototypes) { + Utilities.close(conn); + } + Utilities.close(prototype); + } + + public void testConnection() { + prototype.testConnection(); + } + + public synchronized void setCacheLength(int cacheLength) { + for (ApnsConnection conn : prototypes) { + conn.setCacheLength(cacheLength); + } + } + + @SuppressFBWarnings(value = "UG_SYNC_SET_UNSYNC_GET", justification = "prototypes is a MT-safe container") + public int getCacheLength() { + return prototypes.peek().getCacheLength(); + } +} diff --git a/src/main/java/com/notnoop/apns/internal/ApnsServiceImpl.java b/src/main/java/com/notnoop/apns/internal/ApnsServiceImpl.java new file mode 100755 index 0000000..387f5a8 --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/ApnsServiceImpl.java @@ -0,0 +1,59 @@ +/* + * 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 com.notnoop.apns.ApnsNotification; +import com.notnoop.exceptions.NetworkIOException; + +public class ApnsServiceImpl extends AbstractApnsService { + private ApnsConnection connection; + + public ApnsServiceImpl(ApnsConnection connection, ApnsFeedbackConnection feedback) { + super(feedback); + this.connection = connection; + } + + @Override + public void push(ApnsNotification msg) throws NetworkIOException { + connection.sendMessage(msg); + } + + public void start() { + } + + public void stop() { + Utilities.close(connection); + } + + public void testConnection() { + connection.testConnection(); + } +} diff --git a/src/main/java/com/notnoop/apns/internal/BatchApnsService.java b/src/main/java/com/notnoop/apns/internal/BatchApnsService.java new file mode 100755 index 0000000..d7dcbef --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/BatchApnsService.java @@ -0,0 +1,143 @@ +/* + * 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 static java.util.concurrent.Executors.defaultThreadFactory; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import com.notnoop.apns.ApnsNotification; +import com.notnoop.exceptions.NetworkIOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BatchApnsService extends AbstractApnsService { + + private static final Logger logger = LoggerFactory.getLogger(BatchApnsService.class); + + /** + * How many seconds to wait for more messages before batch is send. + * Each message reset the wait time + * + * @see #maxBatchWaitTimeInSec + */ + private int batchWaitTimeInSec = 5; + + /** + * How many seconds can be batch delayed before execution. + * This time is not exact amount after which the batch will run its roughly the time + */ + private int maxBatchWaitTimeInSec = 10; + + private long firstMessageArrivedTime; + + private ApnsConnection prototype; + + private Queue batch = new ConcurrentLinkedQueue(); + + private ScheduledExecutorService scheduleService; + private ScheduledFuture taskFuture; + + private Runnable batchRunner = new SendMessagesBatch(); + + public BatchApnsService(ApnsConnection prototype, ApnsFeedbackConnection feedback, int batchWaitTimeInSec, int maxBachWaitTimeInSec, ThreadFactory tf) { + this(prototype, feedback, batchWaitTimeInSec, maxBachWaitTimeInSec, + new ScheduledThreadPoolExecutor(1, + tf != null ? tf : defaultThreadFactory())); + } + + public BatchApnsService(ApnsConnection prototype, ApnsFeedbackConnection feedback, int batchWaitTimeInSec, int maxBachWaitTimeInSec, ScheduledExecutorService executor) { + super(feedback); + this.prototype = prototype; + this.batchWaitTimeInSec = batchWaitTimeInSec; + this.maxBatchWaitTimeInSec = maxBachWaitTimeInSec; + this.scheduleService = executor != null ? executor : new ScheduledThreadPoolExecutor(1, defaultThreadFactory()); + } + + public void start() { + // no code + } + + public void stop() { + Utilities.close(prototype); + if (taskFuture != null) { + taskFuture.cancel(true); + } + scheduleService.shutdownNow(); + } + + public void testConnection() throws NetworkIOException { + prototype.testConnection(); + } + + @Override + public void push(ApnsNotification message) throws NetworkIOException { + if (batch.isEmpty()) { + firstMessageArrivedTime = System.nanoTime(); + } + + long sinceFirstMessageSec = (System.nanoTime() - firstMessageArrivedTime) / 1000 / 1000 / 1000; + + if (taskFuture != null && sinceFirstMessageSec < maxBatchWaitTimeInSec) { + taskFuture.cancel(false); + } + + batch.add(message); + + if (taskFuture == null || taskFuture.isDone()) { + taskFuture = scheduleService.schedule(batchRunner, batchWaitTimeInSec, TimeUnit.SECONDS); + } + } + + class SendMessagesBatch implements Runnable { + public void run() { + ApnsConnection newConnection = prototype.copy(); + try { + ApnsNotification msg; + while ((msg = batch.poll()) != null) { + try { + newConnection.sendMessage(msg); + } catch (NetworkIOException e) { + logger.warn("Network exception sending message msg "+ msg.getIdentifier(), e); + } + } + } finally { + Utilities.close(newConnection); + } + } + } +} diff --git a/src/main/java/com/notnoop/apns/internal/QueuedApnsService.java b/src/main/java/com/notnoop/apns/internal/QueuedApnsService.java new file mode 100755 index 0000000..21cc820 --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/QueuedApnsService.java @@ -0,0 +1,126 @@ +/* + * 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.util.Date; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.notnoop.apns.ApnsNotification; +import com.notnoop.apns.ApnsService; +import com.notnoop.exceptions.NetworkIOException; + +public class QueuedApnsService extends AbstractApnsService { + + private static final Logger logger = LoggerFactory.getLogger(QueuedApnsService.class); + + private ApnsService service; + private BlockingQueue queue; + private AtomicBoolean started = new AtomicBoolean(false); + + public QueuedApnsService(ApnsService service) { + this(service, null); + } + + public QueuedApnsService(ApnsService service, final ThreadFactory tf) { + super(null); + this.service = service; + this.queue = new LinkedBlockingQueue(); + this.threadFactory = tf == null ? Executors.defaultThreadFactory() : tf; + this.thread = null; + } + + @Override + public void push(ApnsNotification msg) { + if (!started.get()) { + throw new IllegalStateException("service hasn't be started or was closed"); + } + queue.add(msg); + } + + private final ThreadFactory threadFactory; + private Thread thread; + private volatile boolean shouldContinue; + + public void start() { + if (started.getAndSet(true)) { + // I prefer if we throw a runtime IllegalStateException here, + // but I want to maintain semantic backward compatibility. + // So it is returning immediately here + return; + } + + service.start(); + shouldContinue = true; + thread = threadFactory.newThread(new Runnable() { + public void run() { + while (shouldContinue) { + try { + ApnsNotification msg = queue.take(); + service.push(msg); + } catch (InterruptedException e) { + // ignore + } catch (NetworkIOException e) { + // ignore: failed connect... + } catch (Exception e) { + // weird if we reached here - something wrong is happening, but we shouldn't stop the service anyway! + logger.warn("Unexpected message caught... Shouldn't be here", e); + } + } + } + }); + thread.start(); + } + + public void stop() { + started.set(false); + shouldContinue = false; + thread.interrupt(); + service.stop(); + } + + @Override + public Map getInactiveDevices() throws NetworkIOException { + return service.getInactiveDevices(); + } + + public void testConnection() throws NetworkIOException { + service.testConnection(); + } + +} diff --git a/src/main/java/com/notnoop/apns/internal/ReconnectPolicies.java b/src/main/java/com/notnoop/apns/internal/ReconnectPolicies.java new file mode 100755 index 0000000..b6a70eb --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/ReconnectPolicies.java @@ -0,0 +1,67 @@ +/* + * 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 com.notnoop.apns.ReconnectPolicy; + +public final class ReconnectPolicies { + + public static class Never implements ReconnectPolicy { + + public boolean shouldReconnect() { return false; } + public void reconnected() { } + public Never copy() { return this; } + } + + public static class Always implements ReconnectPolicy { + public boolean shouldReconnect() { return true; } + public void reconnected() { } + public Always copy() { return this; } + } + + public static class EveryHalfHour implements ReconnectPolicy { + private static final long PERIOD = 30 * 60 * 1000; + + private long lastRunning = System.currentTimeMillis(); + + public boolean shouldReconnect() { + return System.currentTimeMillis() - lastRunning > PERIOD; + } + + public void reconnected() { + lastRunning = System.currentTimeMillis(); + } + + public EveryHalfHour copy() { + return new EveryHalfHour(); + } + } +} diff --git a/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java b/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java new file mode 100755 index 0000000..5175bfd --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java @@ -0,0 +1,179 @@ +/* + * 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 com.notnoop.exceptions.InvalidSSLConfig; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; + +public class SSLContextBuilder { + private String algorithm = "sunx509"; + private KeyManagerFactory keyManagerFactory; + private TrustManager[] trustManagers; + + public SSLContextBuilder withAlgorithm(String algorithm) { + this.algorithm = algorithm; + return this; + } + + public SSLContextBuilder withDefaultTrustKeyStore() throws InvalidSSLConfig { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm); + trustManagerFactory.init((KeyStore)null); + trustManagers = trustManagerFactory.getTrustManagers(); + return this; + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } + } + + public SSLContextBuilder withTrustKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType) throws InvalidSSLConfig { + try { + final KeyStore ks = KeyStore.getInstance(keyStoreType); + ks.load(keyStoreStream, keyStorePassword.toCharArray()); + return withTrustKeyStore(ks, keyStorePassword); + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } catch (IOException e) { + throw new InvalidSSLConfig(e); + } + + } + public SSLContextBuilder withTrustKeyStore(KeyStore keyStore, String keyStorePassword) throws InvalidSSLConfig { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm); + trustManagerFactory.init(keyStore); + trustManagers = trustManagerFactory.getTrustManagers(); + return this; + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } + } + + public SSLContextBuilder withTrustManager(TrustManager trustManager) { + trustManagers = new TrustManager[] { trustManager }; + return this; + } + + public SSLContextBuilder withCertificateKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType) throws InvalidSSLConfig { + try { + final KeyStore ks = KeyStore.getInstance(keyStoreType); + ks.load(keyStoreStream, keyStorePassword.toCharArray()); + return withCertificateKeyStore(ks, keyStorePassword); + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } catch (IOException e) { + throw new InvalidSSLConfig(e); + } + } + + public SSLContextBuilder withCertificateKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType, String keyAlias) throws InvalidSSLConfig { + try { + final KeyStore ks = KeyStore.getInstance(keyStoreType); + ks.load(keyStoreStream, keyStorePassword.toCharArray()); + return withCertificateKeyStore(ks, keyStorePassword, keyAlias); + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } catch (IOException e) { + throw new InvalidSSLConfig(e); + } + } + + public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keyStorePassword) throws InvalidSSLConfig { + try { + keyManagerFactory = KeyManagerFactory.getInstance(algorithm); + keyManagerFactory.init(keyStore, keyStorePassword.toCharArray()); + return this; + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } + } + + public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keyStorePassword, String keyAlias) throws InvalidSSLConfig { + try { + if (!keyStore.containsAlias(keyAlias)) { + throw new InvalidSSLConfig("No key with alias " + keyAlias); + } + KeyStore singleKeyKeyStore = getKeyStoreWithSingleKey(keyStore, keyStorePassword, keyAlias); + return withCertificateKeyStore(singleKeyKeyStore, keyStorePassword); + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } catch (IOException e) { + throw new InvalidSSLConfig(e); + } + } + + /* + * Workaround for keystores containing multiple keys. Java will take the first key that matches + * and this way we can still offer configuration for a keystore with multiple keys and a selection + * based on alias. Also much easier than making a subclass of a KeyManagerFactory + */ + private KeyStore getKeyStoreWithSingleKey(KeyStore keyStore, String keyStorePassword, String keyAlias) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { + KeyStore singleKeyKeyStore = KeyStore.getInstance(keyStore.getType(), keyStore.getProvider()); + final char[] password = keyStorePassword.toCharArray(); + singleKeyKeyStore.load(null, password); + Key key = keyStore.getKey(keyAlias, password); + Certificate[] chain = keyStore.getCertificateChain(keyAlias); + singleKeyKeyStore.setKeyEntry(keyAlias, key, password, chain); + return singleKeyKeyStore; + } + + public SSLContext build() throws InvalidSSLConfig { + if (keyManagerFactory == null) { + throw new InvalidSSLConfig("Missing KeyManagerFactory"); + } + + if (trustManagers == null) { + throw new InvalidSSLConfig("Missing TrustManagers"); + } + + try { + final SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagers, null); + return sslContext; + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } + } +} diff --git a/src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java b/src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java new file mode 100755 index 0000000..5e0d68c --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java @@ -0,0 +1,147 @@ +/* + * 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.IOException; +import java.net.InetSocketAddress; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.Socket; +import javax.net.ssl.SSLSocketFactory; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.httpclient.ConnectMethod; +import org.apache.commons.httpclient.NTCredentials; +import org.apache.commons.httpclient.ProxyClient; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.AuthScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Establishes a TLS connection using an HTTP proxy. See RFC 2817 5.2. This class does + * not support proxies requiring a "Proxy-Authorization" header. + */ +public final class TlsTunnelBuilder { + + private static final Logger logger = LoggerFactory.getLogger(TlsTunnelBuilder.class); + + public Socket build(SSLSocketFactory factory, Proxy proxy, String proxyUsername, String proxyPassword, String host, int port) + throws IOException { + boolean success = false; + Socket proxySocket = null; + try { + logger.debug("Attempting to use proxy : " + proxy.toString()); + InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); + proxySocket = makeTunnel(host, port, proxyUsername, proxyPassword, proxyAddress); + + // Handshake with the origin server. + if(proxySocket == null) { + throw new ProtocolException("Unable to create tunnel through proxy server."); + } + Socket socket = factory.createSocket(proxySocket, host, port, true /* auto close */); + success = true; + return socket; + } finally { + if (!success) { + Utilities.close(proxySocket); + } + } + } + + @SuppressFBWarnings(value = "VA_FORMAT_STRING_USES_NEWLINE", + justification = "use as according to RFC, not platform-linefeed") + Socket makeTunnel(String host, int port, String proxyUsername, + String proxyPassword, InetSocketAddress proxyAddress) throws IOException { + if(host == null || port < 0 || host.isEmpty() || proxyAddress == null){ + throw new ProtocolException("Incorrect parameters to build tunnel."); + } + logger.debug("Creating socket for Proxy : " + proxyAddress.getAddress() + ":" + proxyAddress.getPort()); + Socket socket; + try { + ProxyClient client = new ProxyClient(); + client.getParams().setParameter("http.useragent", "java-apns"); + client.getHostConfiguration().setHost(host, port); + String proxyHost = proxyAddress.getAddress().toString().substring(0, proxyAddress.getAddress().toString().indexOf("/")); + client.getHostConfiguration().setProxy(proxyHost, proxyAddress.getPort()); + + + ProxyClient.ConnectResponse response = client.connect(); + socket = response.getSocket(); + if (socket == null) { + ConnectMethod method = response.getConnectMethod(); + // Read the proxy's HTTP response. + if(method.getStatusLine().getStatusCode() == 407) { + // Proxy server returned 407. We will now try to connect with auth Header + if(proxyUsername != null && proxyPassword != null) { + socket = AuthenticateProxy(method, client,proxyHost, proxyAddress.getPort(), + proxyUsername, proxyPassword); + } else { + throw new ProtocolException("Socket not created: " + method.getStatusLine()); + } + } + } + + } catch (Exception e) { + throw new ProtocolException("Error occurred while creating proxy socket : " + e.toString()); + } + if (socket != null) { + logger.debug("Socket for proxy created successfully : " + socket.getRemoteSocketAddress().toString()); + } + return socket; + } + + private Socket AuthenticateProxy(ConnectMethod method, ProxyClient client, + String proxyHost, int proxyPort, + String proxyUsername, String proxyPassword) throws IOException { + if("ntlm".equalsIgnoreCase(method.getProxyAuthState().getAuthScheme().getSchemeName())) { + // If Auth scheme is NTLM, set NT credentials with blank host and domain name + client.getState().setProxyCredentials(new AuthScope(proxyHost, proxyPort), + new NTCredentials(proxyUsername, proxyPassword,"","")); + } else { + // If Auth scheme is Basic/Digest, set regular Credentials + client.getState().setProxyCredentials(new AuthScope(proxyHost, proxyPort), + new UsernamePasswordCredentials(proxyUsername, proxyPassword)); + } + + ProxyClient.ConnectResponse response = client.connect(); + Socket socket = response.getSocket(); + + if (socket == null) { + method = response.getConnectMethod(); + throw new ProtocolException("Proxy Authentication failed. Socket not created: " + + method.getStatusLine()); + } + return socket; + } + +} + diff --git a/src/main/java/com/notnoop/apns/internal/Utilities.java b/src/main/java/com/notnoop/apns/internal/Utilities.java new file mode 100755 index 0000000..1849098 --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/Utilities.java @@ -0,0 +1,296 @@ +/* + * 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.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"); } + + 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 parseFeedbackStreamRaw(final InputStream in) { + final Map result = new HashMap(); + + 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 parseFeedbackStream(final InputStream in) { + final Map result = new HashMap(); + + final Map raw = parseFeedbackStreamRaw(in); + for (final Map.Entry 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; + } + +} diff --git a/src/main/java/com/notnoop/exceptions/ApnsDeliveryErrorException.java b/src/main/java/com/notnoop/exceptions/ApnsDeliveryErrorException.java new file mode 100755 index 0000000..c9b3bea --- /dev/null +++ b/src/main/java/com/notnoop/exceptions/ApnsDeliveryErrorException.java @@ -0,0 +1,61 @@ +/* + * 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. + */ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.notnoop.exceptions; + +import com.notnoop.apns.DeliveryError; + +/** + * + * @author kkirch + */ +public class ApnsDeliveryErrorException extends ApnsException { + + private final DeliveryError deliveryError; + + public ApnsDeliveryErrorException(DeliveryError error) { + this.deliveryError = error; + } + + @Override + public String getMessage() { + return "Failed to deliver notification with error code " + deliveryError.code(); + } + + public DeliveryError getDeliveryError() { + return deliveryError; + } + + +} diff --git a/src/main/java/com/notnoop/exceptions/ApnsException.java b/src/main/java/com/notnoop/exceptions/ApnsException.java new file mode 100755 index 0000000..f1725e5 --- /dev/null +++ b/src/main/java/com/notnoop/exceptions/ApnsException.java @@ -0,0 +1,44 @@ +/* + * 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.exceptions; + +/** + * Base class for all the exceptions thrown in Apns Library + */ +public abstract class ApnsException extends RuntimeException { + private static final long serialVersionUID = -4756693306121825229L; + + public ApnsException() { super(); } + public ApnsException(String message) { super(message); } + public ApnsException(Throwable cause) { super(cause); } + public ApnsException(String m, Throwable c) { super(m, c); } + +} diff --git a/src/main/java/com/notnoop/exceptions/InvalidSSLConfig.java b/src/main/java/com/notnoop/exceptions/InvalidSSLConfig.java new file mode 100755 index 0000000..be97578 --- /dev/null +++ b/src/main/java/com/notnoop/exceptions/InvalidSSLConfig.java @@ -0,0 +1,64 @@ +/* + * 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.exceptions; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +/** + * Signals that the the provided SSL context settings (e.g. + * keystore path, password, encryption type, etc) are invalid + * + * This Exception can be caused by any of the following: + * + *

    + *
  1. {@link KeyStoreException}
  2. + *
  3. {@link NoSuchAlgorithmException}
  4. + *
  5. {@link CertificateException}
  6. + *
  7. {@link IOException}
  8. + *
  9. {@link UnrecoverableKeyException}
  10. + *
  11. {@link KeyManagementException}
  12. + *
+ * + */ +public class InvalidSSLConfig extends ApnsException { + private static final long serialVersionUID = -7283168775864517167L; + + public InvalidSSLConfig() { super(); } + public InvalidSSLConfig(String message) { super(message); } + public InvalidSSLConfig(Throwable cause) { super(cause); } + public InvalidSSLConfig(String m, Throwable c) { super(m, c); } + +} diff --git a/src/main/java/com/notnoop/exceptions/NetworkIOException.java b/src/main/java/com/notnoop/exceptions/NetworkIOException.java new file mode 100755 index 0000000..df84eb5 --- /dev/null +++ b/src/main/java/com/notnoop/exceptions/NetworkIOException.java @@ -0,0 +1,69 @@ +/* + * 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.exceptions; + +import java.io.IOException; + +/** + * Thrown to indicate that that a network operation has failed: + * (e.g. connectivity problems, domain cannot be found, network + * dropped). + */ +public class NetworkIOException extends ApnsException { + private static final long serialVersionUID = 3353516625486306533L; + + private boolean resend; + + public NetworkIOException() { super(); } + public NetworkIOException(String message) { super(message); } + public NetworkIOException(IOException cause) { super(cause); } + public NetworkIOException(String m, IOException c) { super(m, c); } + public NetworkIOException(IOException cause, boolean resend) { + super(cause); + this.resend = resend; + } + + /** + * Identifies whether an exception was thrown during a resend of a + * message or not. In this case a resend refers to whether the + * message is being resent from the buffer of messages internal. + * This would occur if we sent 5 messages quickly to APNS: + * 1,2,3,4,5 and the 3 message was rejected. We would + * then need to resend 4 and 5. If a network exception was + * triggered when doing this, then the resend flag will be + * {@code true}. + * @return {@code true} for an exception trigger during a resend, otherwise {@code false}. + */ + public boolean isResend() { + return resend; + } + +} diff --git a/src/main/java/com/notnoop/exceptions/RuntimeIOException.java b/src/main/java/com/notnoop/exceptions/RuntimeIOException.java new file mode 100755 index 0000000..1a447bd --- /dev/null +++ b/src/main/java/com/notnoop/exceptions/RuntimeIOException.java @@ -0,0 +1,50 @@ +/* + * 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.exceptions; + +import java.io.IOException; + +/** + * Signals that an I/O exception of some sort has occurred. This + * class is the general class of exceptions produced by failed or + * interrupted I/O operations. + * + * This is a RuntimeException, unlike the java.io.IOException + */ +public class RuntimeIOException extends ApnsException { + private static final long serialVersionUID = 8665285084049041306L; + + public RuntimeIOException() { super(); } + public RuntimeIOException(String message) { super(message); } + public RuntimeIOException(IOException cause) { super(cause); } + public RuntimeIOException(String m, IOException c) { super(m, c); } + +} diff --git a/src/main/libs/MiPush_SDK_Server_2_2_19.jar b/src/main/libs/MiPush_SDK_Server_2_2_19.jar new file mode 100644 index 0000000..2aea5d1 Binary files /dev/null and b/src/main/libs/MiPush_SDK_Server_2_2_19.jar differ diff --git a/src/main/resources/apns.properties b/src/main/resources/apns.properties new file mode 100644 index 0000000..85c9a0d --- /dev/null +++ b/src/main/resources/apns.properties @@ -0,0 +1,9 @@ +apns.product_cer_path=apns/product.p12 +apns.product_cer_pwd=123456 +apns.develop_cer_path=apns/develop.p12 +apns.develop_cer_pwd=123456 +apns.voip_cer_path=apns/voip.p12 +apns.voip_cer_pwd=123456 + +apns.alert=default +apns.voip_alert=ring.caf \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..a30a91f --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8085 \ No newline at end of file diff --git a/src/main/resources/hms.properties b/src/main/resources/hms.properties new file mode 100644 index 0000000..d5900b4 --- /dev/null +++ b/src/main/resources/hms.properties @@ -0,0 +1,2 @@ +hms.appSecret=a4e5e6a0c8a5d8424aba5a8f0aae3d0c +hms.appId=100221325 \ No newline at end of file diff --git a/src/main/resources/meizu.properties b/src/main/resources/meizu.properties new file mode 100644 index 0000000..46eadd1 --- /dev/null +++ b/src/main/resources/meizu.properties @@ -0,0 +1,2 @@ +meizu.appSecret=098f6939499a44328fe201eb82d01fb2 +meizu.appId=113616 \ No newline at end of file diff --git a/src/main/resources/xiaomi.properties b/src/main/resources/xiaomi.properties new file mode 100644 index 0000000..3072ae4 --- /dev/null +++ b/src/main/resources/xiaomi.properties @@ -0,0 +1 @@ +xiaomi.appSecret=66nAHUMwmGz042clVI5bVg== \ No newline at end of file diff --git a/src/test/java/cn/wildfirechat/push/PushApplicationTests.java b/src/test/java/cn/wildfirechat/push/PushApplicationTests.java new file mode 100644 index 0000000..dc48b44 --- /dev/null +++ b/src/test/java/cn/wildfirechat/push/PushApplicationTests.java @@ -0,0 +1,16 @@ +package cn.wildfirechat.push; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class PushApplicationTests { + + @Test + public void contextLoads() { + } + +}