diff --git a/.gitignore b/.gitignore index 63082bc..611c651 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,4 @@ key.pem config.yaml bin/* .DS_Store -*.cert -*.key coverage.out diff --git a/.travis.yml b/.travis.yml index 9c47908..4605dca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,9 @@ go: env: global: - secure: ad5iOvyzCMdxp664YZD92Qse8EAB8R8jgoV8CspLBVbwfqPv2S+2IF2pHrxqd6o81y2sTgAr0VFOx9q9gVD3JZpT0vjpPIzscGNVYtup8bDBDFYIf3vwt6RVvJsSTYGQIQ6HulZ97CKB5W2pCI2q9pOrZqFzQG9lHA4J4m+XzAzH7eNx4PprboEwKKhESI4191ii47phWKcalcul00wo/oeM1u/Bmmg/rcGyQgzUHg3zFh+1avQp7z4yAMFmScRnJDJFABg9s9TVeQu8FQXUWTBv4tUQJrpPs60zRSxpt3ap9Yx67pkuVYpfZz9o9u6FLa8UC+zWVh+oHYXfYWrSmCcsyfLjy5Wg2Zt3Mx92Aqq8uiaGaLSxcn5pL9xeIK6X0uWixp2wzXMdjE9RkGRaPhTm/Ze4F810JyNR+i0AkM8py/gPEkVXkeIw+UGS/mo5bq4a5ntLKRbl1vePOIPUtpV8nlgBblNB6jqd3/wINRcCoNhNiKtr+uvUm1jKKK+hu4q9YncwG3gpB+u0DRR2KIPSgdUlzL3V43UMWIC12eEjVrS+xkMo7a2gfUXMzwmpMhQHZ2QMb0VMOgIde1X4nlOL0GHsrHJAp8AmjQF6xbv2gcxzs98zU9qKp/qm4QETGQvHaMXhvE8ogzx1EgXaJ0xn9SIAw11TBT6dpHLdcGI= + - secure: ad5iOvyzCMdxp664YZD92Qse8EAB8R8jgoV8CspLBVbwfqPv2S+2IF2pHrxqd6o81y2sTgAr0VFOx9q9gVD3JZpT0vjpPIzscGNVYtup8bDBDFYIf3vwt6RVvJsSTYGQIQ6HulZ97CKB5W2pCI2q9pOrZqFzQG9lHA4J4m+XzAzH7eNx4PprboEwKKhESI4191ii47phWKcalcul00wo/oeM1u/Bmmg/rcGyQgzUHg3zFh+1avQp7z4yAMFmScRnJDJFABg9s9TVeQu8FQXUWTBv4tUQJrpPs60zRSxpt3ap9Yx67pkuVYpfZz9o9u6FLa8UC+zWVh+oHYXfYWrSmCcsyfLjy5Wg2Zt3Mx92Aqq8uiaGaLSxcn5pL9xeIK6X0uWixp2wzXMdjE9RkGRaPhTm/Ze4F810JyNR+i0AkM8py/gPEkVXkeIw+UGS/mo5bq4a5ntLKRbl1vePOIPUtpV8nlgBblNB6jqd3/wINRcCoNhNiKtr+uvUm1jKKK+hu4q9YncwG3gpB+u0DRR2KIPSgdUlzL3V43UMWIC12eEjVrS+xkMo7a2gfUXMzwmpMhQHZ2QMb0VMOgIde1X4nlOL0GHsrHJAp8AmjQF6xbv2gcxzs98zU9qKp/qm4QETGQvHaMXhvE8ogzx1EgXaJ0xn9SIAw11TBT6dpHLdcGI= + - secure: nnOFadWZodcsJeRHwcPE7wjUk4xZ7oNjrJUPvs1yBDKGOCfga/KbmWpQ85PZiJmmxA7WO1PXpRV0lf+lHKDuAJKiXbQZNZOW8BoFSagRy7/lSicDqeQyJX2zJsjYbM11ZqOCH1uiTMBsNsbaGJTGtwHrbQ84zXJk2WXvDfrvNqlo85cXDHtxkdsqfETgY3S7Gis7VUKmoJKHuhF8ADvsLjaXpg/kgnrhEkTz9AD4oxscMUq3/vkf5wPN9a4uEXDJamKK6N2CKfwGZLwVPGMRIrIL+/gc1vyALGpl5xjC/rMuErESVfzKcUfLgENXL4JEv2IcqfInDqhVL56YG87xlYb4Wsxsm0u1IYDl+v8Frm35SPAz7C5Iha+/Kh1G1ip9AZKkpSS7IxpFOfRe6hmhvXIX00cNCo51ZMp7xBkahiktHveXj7UrqX72y949/+QXgbC0AvV2+4lYnEDydkphlkXH9MDeAVtkbYGUss06U0q19GuL0O8+iZPyBfajdAdS0V0nx7zIYYW42UVuJdRcndcEkaK7PQd0fZlwwZUSXQgqEg8zd+VzxC7tAgtS8LlOU2Ma3Vf4pZiScaZNqpJXPIyD/jPSqNSYQe6WJ6FKRPJw+1ZwN2kAeQMIt1th+08I+23dyolpbULPTkOFudsZoyFkSPhEcDBKNEQdrL7ggTw= + - secure: MVVH7ilA3r3eLb1I9/KTn3hWaEWOhY8yeF1g2zO09r0oRwEr+NKba/Ey2TpBmkcbFt+CiKMrefzoG9dwHyHm9dFH/1OLBrRAXK3RLV6qyMNw708yOkc7NfH5xK7X7F2u0vWwagK3aVkhTxXGrIQvaEK5jJ2tK3K1uDO5TzUC4TM0hLsgGvyTah89LJBhM4k0OEcAIVmzPO58Ql+RZV3nw03LDtcLofVFNqApCAUJNPrArt9TP/UraPPg/R8WtAS/PMY6IsMRKWj3LjN/J089zfQgiHH6p2wPBQ0n2R2zgisnxjAz5wt6/Dkvo09UqkVWFGX4p6N4t7kxAZoqhRRt+t38qdDip5iloclHGO5eI3/dr30V88Y5ionLL81WsBRqFuMmWrEb86maSMsXWl5yM1qB76Rsh/sPMDRk99Wf9RqhHedJxek6reoUcTBZl9kgXJDlqpjEogyq6qgL0jCCqgl0N6FZAYE/5SvW82MCukZZmko2UXBS/GmU6fhBRGquzOczL43YvVqqV8olZ2qXIdeeX5KeO/SHMUvW3oUnENa6V49K6ZX1f7KG8p8jERTNuDfsdgvZdMSYR2EeDQ7RTU6fhoc3BX4cFyTrS1qImswoBvLCEr54awQXDhgWddZsIWQzCxw6TmD7u25G3YHTsM5Wvs2Gj4z0aJeXFlLSfSQ= install: - go get -t -v ./... diff --git a/certificate/certificate-valid.p12 b/certificate/certificate-valid.p12 new file mode 100644 index 0000000..761b614 Binary files /dev/null and b/certificate/certificate-valid.p12 differ diff --git a/certificate/certificate-valid.pem b/certificate/certificate-valid.pem new file mode 100644 index 0000000..2939d7c --- /dev/null +++ b/certificate/certificate-valid.pem @@ -0,0 +1,59 @@ +Bag Attributes + localKeyID: 8C 1A 9F 00 66 BD 24 42 B9 5D 1E EB FE 5E 8B CA 04 3D 73 83 + friendlyName: APNS/2 Private Key +subject=/C=NZ/ST=Wellington/L=Wellington/O=Internet Widgits Pty Ltd/OU=9ZEH62KRVV/CN=APNS/2 Development IOS Push Services: com.sideshow.Apns2 +issuer=/C=NZ/ST=Wellington/L=Wellington/O=APNS/2 Inc./OU=APNS/2 Worldwide Developer Relations/CN=APNS/2 Worldwide Developer Relations Certification Authority +-----BEGIN CERTIFICATE----- +MIID6zCCAtMCAQIwDQYJKoZIhvcNAQELBQAwgcMxCzAJBgNVBAYTAk5aMRMwEQYD +VQQIEwpXZWxsaW5ndG9uMRMwEQYDVQQHEwpXZWxsaW5ndG9uMRQwEgYDVQQKEwtB +UE5TLzIgSW5jLjEtMCsGA1UECxMkQVBOUy8yIFdvcmxkd2lkZSBEZXZlbG9wZXIg +UmVsYXRpb25zMUUwQwYDVQQDEzxBUE5TLzIgV29ybGR3aWRlIERldmVsb3BlciBS +ZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTYwMTA4MDgzNDMw +WhcNMjYwMTA1MDgzNDMwWjCBsjELMAkGA1UEBhMCTloxEzARBgNVBAgTCldlbGxp +bmd0b24xEzARBgNVBAcTCldlbGxpbmd0b24xITAfBgNVBAoTGEludGVybmV0IFdp +ZGdpdHMgUHR5IEx0ZDETMBEGA1UECxMKOVpFSDYyS1JWVjFBMD8GA1UEAxM4QVBO +Uy8yIERldmVsb3BtZW50IElPUyBQdXNoIFNlcnZpY2VzOiBjb20uc2lkZXNob3cu +QXBuczIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY0c1TKB5oZPwQ +7t1CwMIrvqB6GIU3tPy6RhckZXTkOB8YeBWJ7UKfCz8HGHFVomBP0T5OUbeqQzqW +YJbQzZ8a6ZMszbL0lO4X9++3Oi5/TtAwOUOK8rOFN25m2KfsayHQZ/4vWStK2Fwm +5aJbGLlpH/b/7z1D4vhmMgoBuT1IuyhGiyFxlZ9EtTloFvsqM1E5fYZOSZACyXTa +K4vdgbQMgUVsI714FAgLTlK0UeiRkmKm3pdbtfVbrthzI+IHXKItUIy+Fn20PRMh +dSnaztSz7tgBWCIx22qvcYogHWiOgUYIM772zE2y8UVOr8DsiRlsOHSA7EI4MJcQ +G2FUq2Z/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGyfyO2HMgcdeBcz3bt5BILX +f7RA2/UmVIwcKR1qotTsF+PnBmcILeyOQgDe9tGU5cRc79kDt3JRmMYROFIMgFRf +Wf22uOKtho7GQQaKvG+bkgMVdYFRlBHnF+KeqKH81qb9p+CT4Iw0GehIL1DijFLR +VIAIBYpz4oBPCIE1ISVT+Fgaf3JAh59kbPbNw9AIDxaBtP8EuzSTNwfbxoGbCobS +Wi1U8IsCwQFt8tM1m4ZXD1CcZIrGdryeAhVkvKIJRiU5QYWI2nqZN+JqQucm9ad0 +mYO5mJkIobUa4+ZJhCPKEdmgpFbRGk0wVuaDM9Cv6P2srsYAjaO4y3VP0GvNKRI= +-----END CERTIFICATE----- +Bag Attributes + localKeyID: 8C 1A 9F 00 66 BD 24 42 B9 5D 1E EB FE 5E 8B CA 04 3D 73 83 + friendlyName: APNS/2 Private Key +Key Attributes: +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA2NHNUygeaGT8EO7dQsDCK76gehiFN7T8ukYXJGV05DgfGHgV +ie1Cnws/BxhxVaJgT9E+TlG3qkM6lmCW0M2fGumTLM2y9JTuF/fvtzouf07QMDlD +ivKzhTduZtin7Gsh0Gf+L1krSthcJuWiWxi5aR/2/+89Q+L4ZjIKAbk9SLsoRosh +cZWfRLU5aBb7KjNROX2GTkmQAsl02iuL3YG0DIFFbCO9eBQIC05StFHokZJipt6X +W7X1W67YcyPiB1yiLVCMvhZ9tD0TIXUp2s7Us+7YAVgiMdtqr3GKIB1ojoFGCDO+ +9sxNsvFFTq/A7IkZbDh0gOxCODCXEBthVKtmfwIDAQABAoIBAQCW8ZCI+OAae1tE +ipZ9F2bWP3LHLXTo8FYVdCA+VWeITk3PoiIUkJmV0aWCUhDstgto5doDej5sCTur +Xvj/ynaerMeqJFYWkewjwZcgLyAZvwuO1v7fp9E0x/9TGDfnjjnPNeaundxW0cNt +zOY3l0HVHsy9Jpe3QDcAJovy4Tv5+hFY4kDxUBGsyjvhScVgKg5tLkJclm3sOu/L +GyLqpwNI3OJAdMIuVD4N2BZ1aOEap6mp2y8Ie0/R4YWcaZ5A4Pw7xUPl6SXc9uua +/78QTERtPC6ejyCBiE05a8m3Q3iud3Xtnlyws2KwhgBAfE6M4zR/f3OQB7ZIXMhy +ZpmZZw5xAoGBAPYn84IrlIQetWQfvPdM7Kzgh6UDHCugnlCDghwYpRJGi8hMfuZV +xNIrYAJzLYDQ01lFJRJgWXTcbqz9NBz1nhg+cNOz1/KY+38eudee6DNYmztP7jDP +2jnaS+dtjC8hAXObnFqG+NilMDLLu6aRmrJaImbjSrfyLiE6mvJ7u81nAoGBAOF9 +g93wZ0mL1rk2s5WwHGTNU/HaOtmWS4z7kA7f4QaRub+MwppZmmDZPHpiZX7BPcZz +iOPQh+xn7IqRGoQWBLykBVt8zZFoLZJoCR3n63lex5A4p/0Pp1gFZrR+xX8PYVos +3yeeiWyPKsXXNc0s5QwHZcX6Wb8EHThTXGCBetcpAoGAMeQJC9IPaPPcae2w3CLA +OY3MkFpgBEuqqsDsxwsLsfeQb0lp0v+BQ+O8suJrT5eDrq1ABUh3+SKQYAl13YS+ +xUUqkw35b9cn6iztF9HCWF3WIKBjs4r9PQqMpdxjNE4pQChC+Wov16ErcrAuWWVb +iFiSbm4U/9FbHisFqq3/c3MCgYB+vzSuPgFw37+0oEDVtQZgyuGSop5NzCNvfb/9 +/G3aaXNFbnO8mv0hzzoleMWgODLnJ+4cUAz3H3tgcCu9bzr+Zhv0zvQl9a8YCo6F +VuWPdW0rbg1PO8tOuMqATnno79ZC/9H3zS9l7BuY1V2SlNeyqT3VyOFFc6SREpps +TJul8QKBgAxnQB8MA7zPULu1clyaJLdtEdRPkKWN7lKYptc0e/VHfSsKxseWkfqi +zgXZ51kQTrT6Zb6HYRfwC1mMXHWRKRyYjAnCxVim6YQd+KVT49iRDDAiIFoMGA4i +vvcIlneqOZZPDIoKJ60IjO/DZHWkw5mLjaIrT+qQ3XAGdJA13hcm +-----END RSA PRIVATE KEY----- diff --git a/certificate/localhost.cert b/certificate/localhost.cert new file mode 100644 index 0000000..2471fc2 --- /dev/null +++ b/certificate/localhost.cert @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIJALbZEDvUQrFKMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xNjAzMjgwMzMwNDFaFw0yNjAzMjYwMzMwNDFaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMj1+xg4jVLzVnB5j7n1ul30WEE4BCzcNFxg5AOB5H5q+wje0YYiVFg6PQyv +GCipqIRXVRdVQ1hHSeunYGKe8lq3Sb1X8PUJ12v9uRbpS9DK1Owqk8rsPDu6sVTL +qKKgH1Z8yazzaS0AbXuA5e9gO/RzijbnpEP+quM4dueiMPVEJyLq+EoIQY+MM8MP +8dZzL4XZl7wL4UsCN7rPcO6W3tlnT0iO3h9c/Ym2hFhz+KNJ9KRRCvtPGZESigtK +bHsXH099WDo8v/Wp5/evBw/+JD0opxmCfHIBALHt9v53RvvsDZ1t33Rpu5C8znEY +Y2Ay7NgxhqjqoWJqA48lJeA0clsCAwEAAaNQME4wHQYDVR0OBBYEFC0bTU1Xofeh +NKIelashIsqKidDYMB8GA1UdIwQYMBaAFC0bTU1XofehNKIelashIsqKidDYMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAiJL8IMTwNX9XqQWYDFgkG4 +AnrVwQhreAqC9rSxDCjqqnMHPHGzcCeDMLAMoh0kOy20nowUGNtCZ0uBvnX2q1bN +g1jt+GBcLJDR3LL4CpNOlm3YhOycuNfWMxTA7BXkmnSrZD/7KhArsBEY8aulxwKJ +HRgNlIwe1oFD1YdX1BS5pp4t25B6Vq4A3FMMUkVoWE688nE168hvQgwjrHkgHhwe +eN8lGE2DhFraXnWmDMdwaHD3HRFGhyppIFN+f7BqbWX9gM+T2YRTfObIXLWbqJLD +3Mk/NkxqVcg4eY54wJ1ufCUGAYAIaY6fQqiNUz8nhwK3t45NBVT9y/uJXqnTLyY= +-----END CERTIFICATE----- diff --git a/certificate/localhost.key b/certificate/localhost.key new file mode 100644 index 0000000..4d28833 --- /dev/null +++ b/certificate/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAyPX7GDiNUvNWcHmPufW6XfRYQTgELNw0XGDkA4Hkfmr7CN7R +hiJUWDo9DK8YKKmohFdVF1VDWEdJ66dgYp7yWrdJvVfw9QnXa/25FulL0MrU7CqT +yuw8O7qxVMuooqAfVnzJrPNpLQBte4Dl72A79HOKNuekQ/6q4zh256Iw9UQnIur4 +SghBj4wzww/x1nMvhdmXvAvhSwI3us9w7pbe2WdPSI7eH1z9ibaEWHP4o0n0pFEK ++08ZkRKKC0psexcfT31YOjy/9ann968HD/4kPSinGYJ8cgEAse32/ndG++wNnW3f +dGm7kLzOcRhjYDLs2DGGqOqhYmoDjyUl4DRyWwIDAQABAoIBAGTKqsN9KbSfA42q +CqI0UuLouJMNa1qsnz5uAi6YKWgWdA4A44mpEjCmFRSVhUJvxWuK+cyYIQzXxIWD +D16nZdqF72AeCWZ9JySsvvZ00GfKM3y35iRy08sJWgOzmcLnGJCiSeyKsQe3HTJC +dhDXbXqvsHTVPZg01LTeDxUiTffU8NMKqR2AecQ2sTDwXEhAnTyAtnzl/XaBgFzu +U6G7FzGM5y9bxkfQVkvy+DEJkHGNOjzwcVfByyVl610ixmG1vmxVj9PbWmIPsUV8 +ySmjhvDQbOfoxW0h9vTlTqGtQcBw962osnDDMWFCdM7lzO0T7RRnPVGIRpCJOKhq +keqHKwECgYEA8wwI/iZughoTXTNG9LnQQ/WAtsqO80EjMTUheo5I1kOzmUz09pyh +iAsUDoN0/26tZ5WNjlnyZu7dvTc/x3dTZpmNnoo8gcVbQNECDRzqfuQ9PPXm1SN5 +6peBqAvBv78hjV05aXzPG/VBbeig7l299EarEA+a/oH3KrgDoqVqE0ECgYEA06vA +YJmgg4fZRucAYoaYsLz9Z9rCFjTe1PBTmUJkbOR8vFIHHTTEWi/SuxXL0wDSeoE2 +7BQm86gCC7/KgRdrzoBqZ5qS9Mv2dsLgY635VSgjjfZkVLiH1VRRpSQObYnfoysg +gatcHSKMExd4SLQByAuImXP+L5ayDBcEJfbqSpsCgYB78Is1b0uzNLDjOh7Y9Vhr +D2qPzEORcIoNsdZctOoXuXaAmmngyIbm5R9ZN1gWWc47oFwLV3rxWqXgs6fmg8cX +7v309vFcC9Q4/Vxaa4B5LNK9n3gTAIBPTOtlUnl+2my1tfBtBqRm0W6IKbTHWS5g +vxjEm/CiEIyGUEgqTMgHAQKBgBKuXdQoutng63QufwIzDtbKVzMLQ4XiNKhmbXph +OavCnp+gPbB+L7Yl8ltAmTSOJgVZ0hcT0DxA361Zx+2Mu58GBl4OblnchmwE1vj1 +KcQyPrEQxdoUTyiswGfqvrs8J9imvb+z9/U6T1KAB8Wi3WViXzPr4MsiaaRXg642 +FIdxAoGAZ7/735dkhJcyOfs+LKsLr68JSstoorXOYvdMu1+JGa9iLuhnHEcMVWC8 +IuihzPfloZtMbGYkZJn8l3BeGd8hmfFtgTgZGPoVRetft2LDFLnPxp2sEH5OFLsQ +R+K/kAOul8eStWuMXOFA9pMzGkGEgIFJMJOyaJON3kedQI8deCM= +-----END RSA PRIVATE KEY----- diff --git a/config/apns.yaml b/config/config.yaml similarity index 100% rename from config/apns.yaml rename to config/config.yaml diff --git a/gorush/config_test.go b/gorush/config_test.go new file mode 100644 index 0000000..1811042 --- /dev/null +++ b/gorush/config_test.go @@ -0,0 +1,45 @@ +package gopush + +import ( + "github.com/stretchr/testify/assert" + "io/ioutil" + "log" + "os" + "testing" +) + +// Test file is missing +func TestMissingFile(t *testing.T) { + filename := "test" + _, err := LoadConfYaml(filename) + + assert.NotNil(t, err) +} + +// Test wrong json format +func TestWrongYAMLormat(t *testing.T) { + content := []byte(`Wrong format`) + + filename := "tempfile" + + if err := ioutil.WriteFile(filename, content, 0644); err != nil { + log.Fatalf("WriteFile %s: %v", filename, err) + } + + // clean up + defer os.Remove(filename) + + // parse JSON format error + _, err := LoadConfYaml(filename) + + assert.NotNil(t, err) +} + +// Test config file. +func TestReadConfig(t *testing.T) { + config, err := LoadConfYaml("../config/config.yaml") + + assert.Nil(t, err) + assert.Equal(t, "8088", config.Core.Port) + assert.False(t, config.Android.Enabled) +} diff --git a/gorush/notification.go b/gorush/notification.go index 4f66347..ff0ac79 100644 --- a/gorush/notification.go +++ b/gorush/notification.go @@ -13,7 +13,21 @@ type ExtendJSON struct { Value string `json:"val"` } -type alert struct { +const ( + // PriorityLow will tell APNs to send the push message at a time that takes + // into account power considerations for the device. Notifications with this + // priority might be grouped and delivered in bursts. They are throttled, and + // in some cases are not delivered. + ApnsPriorityLow = 5 + + // PriorityHigh will tell APNs to send the push message immediately. + // Notifications with this priority must trigger an alert, sound, or badge on + // the target device. It is an error to use this priority for a push + // notification that contains only the content-available key. + ApnsPriorityHigh = 10 +) + +type Alert struct { Action string `json:"action,omitempty"` ActionLocKey string `json:"action-loc-key,omitempty"` Body string `json:"body,omitempty"` @@ -48,18 +62,16 @@ type RequestPushNotification struct { Topic string `json:"topic,omitempty"` Badge int `json:"badge,omitempty"` Sound string `json:"sound,omitempty"` - Expiry int `json:"expiry,omitempty"` - Retry int `json:"retry,omitempty"` Category string `json:"category,omitempty"` URLArgs []string `json:"url-args,omitempty"` Extend []ExtendJSON `json:"extend,omitempty"` - Alert alert `json:"alert,omitempty"` + Alert Alert `json:"alert,omitempty"` // meta IDs []uint64 `json:"seq_id,omitempty"` } -func InitAPNSClient() { +func InitAPNSClient() error { if PushConf.Ios.Enabled { var err error @@ -68,7 +80,7 @@ func InitAPNSClient() { if err != nil { log.Println("Cert Error:", err) - return + return err } if PushConf.Ios.Production { @@ -77,6 +89,8 @@ func InitAPNSClient() { ApnsClient = apns.NewClient(CertificatePemIos).Development() } } + + return nil } func pushNotification(notification RequestPushNotification) bool { @@ -100,92 +114,104 @@ func pushNotification(notification RequestPushNotification) bool { return success } +// The iOS Notification Payload +// ref: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html +func GetIOSNotification(req RequestPushNotification) *apns.Notification { + notification := &apns.Notification{} + + if len(req.ApnsID) > 0 { + notification.ApnsID = req.ApnsID + } + + if len(req.Topic) > 0 { + notification.Topic = req.Topic + } + + if len(req.Priority) > 0 && req.Priority == "normal" { + notification.Priority = apns.PriorityLow + } + + payload := payload.NewPayload().Alert(req.Message) + + if req.Badge > 0 { + payload.Badge(req.Badge) + } + + if len(req.Sound) > 0 { + payload.Sound(req.Sound) + } + + if req.ContentAvailable { + payload.ContentAvailable() + } + + if len(req.Extend) > 0 { + for _, extend := range req.Extend { + payload.Custom(extend.Key, extend.Value) + } + } + + // Alert dictionary + + if len(req.Alert.Title) > 0 { + payload.AlertTitle(req.Alert.Title) + } + + if len(req.Alert.TitleLocKey) > 0 { + payload.AlertTitleLocKey(req.Alert.TitleLocKey) + } + + // Need send PR to apns2 repo. + // if len(req.Alert.LocArgs) > 0 { + // payload.AlertLocArgs(req.Alert.LocArgs) + // } + + if len(req.Alert.TitleLocArgs) > 0 { + payload.AlertTitleLocArgs(req.Alert.TitleLocArgs) + } + + if len(req.Alert.Body) > 0 { + payload.AlertBody(req.Alert.Body) + } + + if len(req.Alert.LaunchImage) > 0 { + payload.AlertLaunchImage(req.Alert.LaunchImage) + } + + if len(req.Alert.LocKey) > 0 { + payload.AlertLocKey(req.Alert.LocKey) + } + + if len(req.Alert.Action) > 0 { + payload.AlertAction(req.Alert.Action) + } + + if len(req.Alert.ActionLocKey) > 0 { + payload.AlertActionLocKey(req.Alert.ActionLocKey) + } + + // General + + if len(req.Category) > 0 { + payload.Category(req.Category) + } + + if len(req.URLArgs) > 0 { + payload.URLArgs(req.URLArgs) + } + + notification.Payload = payload + + return notification +} + func pushNotificationIos(req RequestPushNotification) bool { - // The Remote Notification Payload - // https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html + notification := GetIOSNotification(req) + for _, token := range req.Tokens { - notification := &apns.Notification{} notification.DeviceToken = token - if len(req.ApnsID) > 0 { - notification.ApnsID = req.ApnsID - } - - if len(req.Topic) > 0 { - notification.Topic = req.Topic - } - - if len(req.Priority) > 0 && req.Priority == "normal" { - notification.Priority = apns.PriorityLow - } - - payload := payload.NewPayload().Alert(req.Message) - - if req.Badge > 0 { - payload.Badge(req.Badge) - } - - if len(req.Sound) > 0 { - payload.Sound(req.Sound) - } - - if req.ContentAvailable { - payload.ContentAvailable() - } - - if len(req.Extend) > 0 { - for _, extend := range req.Extend { - payload.Custom(extend.Key, extend.Value) - } - } - - // Alert dictionary - - if len(req.Alert.Title) > 0 { - payload.AlertTitle(req.Alert.Title) - } - - if len(req.Alert.TitleLocKey) > 0 { - payload.AlertTitleLocKey(req.Alert.TitleLocKey) - } - - if len(req.Alert.LocArgs) > 0 { - payload.AlertTitleLocArgs(req.Alert.LocArgs) - } - - if len(req.Alert.Body) > 0 { - payload.AlertBody(req.Alert.Body) - } - - if len(req.Alert.LaunchImage) > 0 { - payload.AlertLaunchImage(req.Alert.LaunchImage) - } - - if len(req.Alert.LocKey) > 0 { - payload.AlertLocKey(req.Alert.LocKey) - } - - if len(req.Alert.Action) > 0 { - payload.AlertAction(req.Alert.Action) - } - - if len(req.Alert.ActionLocKey) > 0 { - payload.AlertActionLocKey(req.Alert.ActionLocKey) - } - - // General - - if len(req.Category) > 0 { - payload.Category(req.Category) - } - - if len(req.URLArgs) > 0 { - payload.URLArgs(req.URLArgs) - } - - notification.Payload = payload - // send ios notification res, err := ApnsClient.Push(notification) @@ -197,17 +223,15 @@ func pushNotificationIos(req RequestPushNotification) bool { if res.Sent() { log.Println("APNs ID:", res.ApnsID) - return true } } return true } -func pushNotificationAndroid(req RequestPushNotification) bool { - - // HTTP Connection Server Reference for Android - // https://developers.google.com/cloud-messaging/http-server-ref +// HTTP Connection Server Reference for Android +// https://developers.google.com/cloud-messaging/http-server-ref +func GetAndroidNotification(req RequestPushNotification) gcm.HttpMessage { notification := gcm.HttpMessage{} notification.RegistrationIds = req.Tokens @@ -255,21 +279,30 @@ func pushNotificationAndroid(req RequestPushNotification) bool { notification.Notification.Body = req.Message } + return notification +} + +func pushNotificationAndroid(req RequestPushNotification) bool { + + notification := GetAndroidNotification(req) + res, err := gcm.SendHttp(PushConf.Android.ApiKey, notification) + log.Printf("Success count: %d, Failure count: %d", res.Success, res.Failure) + if err != nil { - log.Println(err) + log.Println("GCM Server Error Message: " + err.Error()) return false } if res.Error != "" { - log.Println("GCM Error Message: " + res.Error) + log.Println("GCM Http Error Message: " + res.Error) + + return false } if res.Success > 0 { - log.Printf("Success count: %d, Failure count: %d", res.Success, res.Failure) - return true } diff --git a/gorush/notification_test.go b/gorush/notification_test.go new file mode 100644 index 0000000..96166b3 --- /dev/null +++ b/gorush/notification_test.go @@ -0,0 +1,158 @@ +package gopush + +import ( + "encoding/json" + "github.com/buger/jsonparser" + "github.com/google/go-gcm" + "github.com/stretchr/testify/assert" + "log" + "testing" +) + +func TestIOSNotificationStructure(t *testing.T) { + var dat map[string]interface{} + + test := "test" + message := "Welcome notification Server" + req := RequestPushNotification{ + ApnsID: test, + Topic: test, + Priority: "normal", + Message: message, + Badge: 1, + Sound: test, + ContentAvailable: true, + Extend: []ExtendJSON{ + { + Key: "key1", + Value: "1", + }, + { + Key: "key2", + Value: "2", + }, + }, + Category: test, + URLArgs: []string{"a", "b"}, + } + + notification := GetIOSNotification(req) + + dump, _ := json.Marshal(notification.Payload) + data := []byte(string(dump)) + + if err := json.Unmarshal(data, &dat); err != nil { + log.Println(err) + panic(err) + } + + alert, _ := jsonparser.GetString(data, "aps", "alert") + badge, _ := jsonparser.GetInt(data, "aps", "badge") + sound, _ := jsonparser.GetString(data, "aps", "sound") + contentAvailable, _ := jsonparser.GetInt(data, "aps", "content-available") + category, _ := jsonparser.GetString(data, "aps", "category") + key1 := dat["key1"].(string) + key2 := dat["key2"].(string) + aps := dat["aps"].(map[string]interface{}) + urlArgs := aps["url-args"].([]interface{}) + + assert.Equal(t, test, notification.ApnsID) + assert.Equal(t, test, notification.Topic) + assert.Equal(t, ApnsPriorityLow, notification.Priority) + assert.Equal(t, message, alert) + assert.Equal(t, 1, int(badge)) + assert.Equal(t, test, sound) + assert.Equal(t, 1, int(contentAvailable)) + assert.Equal(t, "1", key1) + assert.Equal(t, "2", key2) + assert.Equal(t, test, category) + assert.Contains(t, urlArgs, "a") + assert.Contains(t, urlArgs, "b") +} + +func TestIOSAlertNotificationStructure(t *testing.T) { + var dat map[string]interface{} + + test := "test" + req := RequestPushNotification{ + Alert: Alert{ + Action: test, + ActionLocKey: test, + Body: test, + LaunchImage: test, + LocArgs: []string{"a", "b"}, + LocKey: test, + Title: test, + TitleLocArgs: []string{"a", "b"}, + TitleLocKey: test, + }, + } + + notification := GetIOSNotification(req) + + dump, _ := json.Marshal(notification.Payload) + data := []byte(string(dump)) + + if err := json.Unmarshal(data, &dat); err != nil { + log.Println(err) + panic(err) + } + + action, _ := jsonparser.GetString(data, "aps", "alert", "action") + actionLocKey, _ := jsonparser.GetString(data, "aps", "alert", "action-loc-key") + body, _ := jsonparser.GetString(data, "aps", "alert", "body") + launchImage, _ := jsonparser.GetString(data, "aps", "alert", "launch-image") + locKey, _ := jsonparser.GetString(data, "aps", "alert", "loc-key") + title, _ := jsonparser.GetString(data, "aps", "alert", "title") + titleLocKey, _ := jsonparser.GetString(data, "aps", "alert", "title-loc-key") + aps := dat["aps"].(map[string]interface{}) + alert := aps["alert"].(map[string]interface{}) + titleLocArgs := alert["title-loc-args"].([]interface{}) + + assert.Equal(t, test, action) + assert.Equal(t, test, actionLocKey) + assert.Equal(t, test, body) + assert.Equal(t, test, launchImage) + assert.Equal(t, test, locKey) + assert.Equal(t, test, title) + assert.Equal(t, test, titleLocKey) + assert.Contains(t, titleLocArgs, "a") + assert.Contains(t, titleLocArgs, "b") +} + +func TestAndroidNotificationStructure(t *testing.T) { + + test := "test" + req := RequestPushNotification{ + Tokens: []string{"a", "b"}, + Message: "Welcome", + To: test, + Priority: "high", + CollapseKey: "1", + ContentAvailable: true, + DelayWhileIdle: true, + TimeToLive: 100, + RestrictedPackageName: test, + DryRun: true, + Data: map[string]interface{}{ + "a": "1", + "b": "2", + }, + Notification: gcm.Notification{ + Title: test, + }, + } + + notification := GetAndroidNotification(req) + + assert.Equal(t, test, notification.To) + assert.Equal(t, "high", notification.Priority) + assert.Equal(t, "1", notification.CollapseKey) + assert.True(t, notification.ContentAvailable) + assert.True(t, notification.DelayWhileIdle) + assert.Equal(t, 100, int(notification.TimeToLive)) + assert.Equal(t, test, notification.RestrictedPackageName) + assert.True(t, notification.DryRun) + assert.Equal(t, test, notification.Notification.Title) + assert.Equal(t, "Welcome", notification.Notification.Body) +} diff --git a/gorush/server.go b/gorush/server.go index 5fc9e0e..e325cf9 100644 --- a/gorush/server.go +++ b/gorush/server.go @@ -56,10 +56,13 @@ func GetMainEngine() *gin.Engine { return r } -func RunHTTPServer() { +func RunHTTPServer() error { + var err error if PushConf.Core.SSL && PushConf.Core.CertPath != "" && PushConf.Core.KeyPath != "" { - GetMainEngine().RunTLS(":"+PushConf.Core.Port, PushConf.Core.CertPath, PushConf.Core.KeyPath) + err = GetMainEngine().RunTLS(":"+PushConf.Core.Port, PushConf.Core.CertPath, PushConf.Core.KeyPath) } else { - GetMainEngine().Run(":" + PushConf.Core.Port) + err = GetMainEngine().Run(":" + PushConf.Core.Port) } + + return err } diff --git a/gorush/server_test.go b/gorush/server_test.go index 596cd92..6518389 100644 --- a/gorush/server_test.go +++ b/gorush/server_test.go @@ -3,10 +3,14 @@ package gopush import ( "github.com/appleboy/gofight" "github.com/buger/jsonparser" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" + "io/ioutil" "net/http" - "testing" + "os" "runtime" + "testing" + "time" ) var go_version = runtime.Version() @@ -16,6 +20,56 @@ func initTest() { PushConf.Core.Mode = "test" } +func testRequest(t *testing.T, url string) { + resp, err := http.Get(url) + defer resp.Body.Close() + assert.NoError(t, err) + + _, ioerr := ioutil.ReadAll(resp.Body) + assert.NoError(t, ioerr) + assert.Equal(t, "200 OK", resp.Status, "should get a 200") +} + +func TestPrintGoPushVersion(t *testing.T) { + PrintGoPushVersion() +} + +func TestRunNormalServer(t *testing.T) { + initTest() + + router := gin.New() + + go func() { + assert.NoError(t, RunHTTPServer()) + }() + // have to wait for the goroutine to start and run the server + // otherwise the main thread will complete + time.Sleep(5 * time.Millisecond) + + assert.Error(t, router.Run(":8088")) + testRequest(t, "http://localhost:8088/api/status") +} + +// func TestRunTLSServer(t *testing.T) { +// initTest() + +// PushConf.Core.SSL = true +// PushConf.Core.Port = "8087" +// PushConf.Core.CertPath = "../certificate/localhost.cert" +// PushConf.Core.KeyPath = "../certificate/localhost.key" +// router := gin.New() + +// go func() { +// assert.NoError(t, RunHTTPServer()) +// }() +// // have to wait for the goroutine to start and run the server +// // otherwise the main thread will complete +// time.Sleep(5 * time.Millisecond) + +// assert.Error(t, router.Run(":8087")) +// testRequest(t, "https://localhost:8087/api/status") +// } + func TestRootHandler(t *testing.T) { initTest() @@ -48,7 +102,7 @@ func TestAPIStatusHandler(t *testing.T) { }) } -func TestPushHandler(t *testing.T) { +func TestMissingParameterPushHandler(t *testing.T) { initTest() r := gofight.New() @@ -63,3 +117,157 @@ func TestPushHandler(t *testing.T) { assert.Equal(t, http.StatusBadRequest, r.Code) }) } + +func TestDisabledIosPushHandler(t *testing.T) { + initTest() + + PushConf.Ios.Enabled = false + InitAPNSClient() + + r := gofight.New() + + r.POST("/api/push"). + SetJSON(gofight.D{ + "tokens": []string{"11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7"}, + "platform": 1, + "message": "Welcome", + }). + Run(GetMainEngine(), func(r gofight.HttpResponse, rq gofight.HttpRequest) { + + assert.Equal(t, http.StatusOK, r.Code) + }) +} + +func TestMissingIosCertificate(t *testing.T) { + initTest() + + PushConf.Ios.Enabled = true + PushConf.Ios.PemKeyPath = "test" + err := InitAPNSClient() + + assert.Error(t, err) +} + +func TestIosPushDevelopment(t *testing.T) { + initTest() + + PushConf.Ios.Enabled = true + PushConf.Ios.PemKeyPath = "../certificate/certificate-valid.pem" + InitAPNSClient() + + r := gofight.New() + + r.POST("/api/push"). + SetJSON(gofight.D{ + "tokens": []string{"11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7"}, + "platform": 1, + "message": "Welcome", + }). + Run(GetMainEngine(), func(r gofight.HttpResponse, rq gofight.HttpRequest) { + + assert.Equal(t, http.StatusOK, r.Code) + }) +} + +func TestIosPushProduction(t *testing.T) { + initTest() + + PushConf.Ios.Enabled = true + PushConf.Ios.Production = true + PushConf.Ios.PemKeyPath = "../certificate/certificate-valid.pem" + InitAPNSClient() + + r := gofight.New() + + r.POST("/api/push"). + SetJSON(gofight.D{ + "tokens": []string{"11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7"}, + "platform": 1, + "message": "Welcome", + }). + Run(GetMainEngine(), func(r gofight.HttpResponse, rq gofight.HttpRequest) { + + assert.Equal(t, http.StatusOK, r.Code) + }) +} + +func TestDisabledAndroidPushHandler(t *testing.T) { + initTest() + + PushConf.Android.Enabled = false + + r := gofight.New() + + r.POST("/api/push"). + SetJSON(gofight.D{ + "tokens": []string{"aaaaaa", "bbbbb"}, + "platform": 2, + "message": "Welcome", + }). + Run(GetMainEngine(), func(r gofight.HttpResponse, rq gofight.HttpRequest) { + + assert.Equal(t, http.StatusOK, r.Code) + }) +} + +func TestAndroidWrongAPIKey(t *testing.T) { + initTest() + + PushConf.Android.Enabled = true + PushConf.Android.ApiKey = os.Getenv("ANDROID_API_KEY") + "a" + + r := gofight.New() + + r.POST("/api/push"). + SetJSON(gofight.D{ + "tokens": []string{"aaaaaa", "bbbbb"}, + "platform": 2, + "message": "Welcome", + }). + Run(GetMainEngine(), func(r gofight.HttpResponse, rq gofight.HttpRequest) { + + assert.Equal(t, http.StatusOK, r.Code) + }) +} + +func TestAndroidWrongToken(t *testing.T) { + initTest() + + PushConf.Android.Enabled = true + PushConf.Android.ApiKey = os.Getenv("ANDROID_API_KEY") + + r := gofight.New() + + r.POST("/api/push"). + SetJSON(gofight.D{ + "tokens": []string{"aaaaaa", "bbbbb"}, + "platform": 2, + "message": "Welcome", + }). + Run(GetMainEngine(), func(r gofight.HttpResponse, rq gofight.HttpRequest) { + + assert.Equal(t, http.StatusOK, r.Code) + }) +} + +func TestAndroidRightToken(t *testing.T) { + initTest() + + PushConf.Android.Enabled = true + PushConf.Android.ApiKey = os.Getenv("ANDROID_API_KEY") + + android_token := os.Getenv("ANDROID_TEST_TOKEN") + + r := gofight.New() + + r.POST("/api/push"). + SetJSON(gofight.D{ + "tokens": []string{android_token, "bbbbb"}, + "platform": 2, + "message": "Welcome", + }). + Run(GetMainEngine(), func(r gofight.HttpResponse, rq gofight.HttpRequest) { + + assert.Equal(t, http.StatusOK, r.Code) + }) +}