简体   繁体   中英

Safari Push Notifications

I would like to implement push notifications for my website (obviously only in compatible browser as Safari 7). I have read the Apple documentation and I have successfully created my package containing my icon.iconset, my certificate.p12, manifest.json and a website.json. Now I would like to ask the permission to the user when I first visit the website. If he allows it, I should send the package. Everything is pretty clear but I don't know how to go on.

How do I create my push package out of my files? How do I precisely sign it? The package should be always the same so I could sign it on my mac and upload to my server only one package.

If you have experience with this technology, please let me know :)

I have successfully create push package to ask permission on safari web push notifications using REST API in java. Also I have follow steps which provide by Apple officials on there sites.

Please follow below steps to create push package.

  1. Create your web push notification P12 certificate from your apple account.

  2. Create your REST API using https for pushPackage which contain icon.iconset, your certificate.p12, manifest.json and a website.json. p12 certificate must be web push notification certificate.

  3. How to create push package:- Please refer below java code. I have create push package using java servlet. which provide 2 web service end points.

  4. v1/pushpackage/webpushID

  5. v1/log

Servlet which process your push package request which send by safari push notification method

SafariPushPakageAPI.java /* Push Package REST API handler */

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;

import com.safari.Packager;

public class SafariPushPakageAPI extends HttpServlet{

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    public static  String ServerPath = null;
    private static final String REQUEST_PERMISSION = "/v1/pushPackages/YOUR_WEB_PUSH_ID";
    private static final String REQUEST_ERRORLOG = "/v1/log";

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doRequest(request, response);
    }
//  /v1/pushPackages/webpushID
//  /v1/log
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doRequest(request, response);
    }

    private void doRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("===>> SAFARI PUSH NOTIFICATION REQUEST");
        String path = request.getPathInfo();
        System.out.println("PATH ===>> "+path);
        if(path == null){
            doRequestPermission(request, response);
        }else if (path.equalsIgnoreCase(REQUEST_PERMISSION)){
            doRequestPermission(request, response);
        }else if (path.equalsIgnoreCase(REQUEST_ERRORLOG)){
            doRequestShowErrorLog(request, response);
        }else{
            doRequestPermission(request, response);
        }
    }

    private void doRequestPermission(HttpServletRequest request,HttpServletResponse response) {
        try{
            System.out.println("INSIDE REQUEST PERMISSION ==>>>");
            System.out.println(IOUtils.toString(request.getReader()));
            String authToken = StringUtils.isBlank(request.getParameter("token")) ? "UserTokenRT124DFGH" : StringUtils.trimToEmpty(request.getParameter("token"));
            System.out.println("=>>>>>>>>>> USER TOKEN =>>>>>>>>>> "+authToken);
            @SuppressWarnings("deprecation")
            String packagePath =request.getRealPath("pushPackage.raw/icon.iconset/"); // LOCATION WHERE YOUR PUSH PACKAGE FOLDER CONTAIN LOGOS AND website.json file
            response.setContentType("application/zip");
            response.setHeader("Content-Disposition", "attachment;filename=\"pushpackage.zip\"");
            OutputStream out = response.getOutputStream();
            out.write(Packager.createPackageFile(authToken,packagePath));
            response.flushBuffer();
        }catch(IOException ioe){
            ioe.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }   
    }

    private void doRequestShowErrorLog(HttpServletRequest request,HttpServletResponse response) {
        try{
            System.out.println("ERROR LOG STARTED");
            System.out.println(IOUtils.toString(request.getReader()));
            System.out.println("END");
        }catch(Exception e){
            e.printStackTrace();
        }
    }

}

Packager.java

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.io.IOUtils;
import org.json.JSONArray;
import org.json.JSONObject;

/**
 *
 * @author Ritesh
 */
public class Packager {

    final static String CERTIFICATE_PATH="PATH TO YOUR 12 CERTIFICATE";
    final static String CERTIFICATE_PASS="PASSWORD";

    static String getJSON(String authenticationToken) throws Exception {
        JSONObject obj = new JSONObject();
        obj.put("websiteName", "WEB SITE NAME");
        obj.put("websitePushID", "WEB PUSH ID");
        obj.put("allowedDomains", new JSONArray());

        obj.getJSONArray("allowedDomains").put("https://TEST.EXAMPLE.net");//LIST OF DOMAINS ALLOW

        obj.put("urlFormatString", "https://TEST.EXAMPLE.net/%@");
        obj.put("authenticationToken", authenticationToken);
        obj.put("webServiceURL", "https://API.EXAMPLE.COM");//callback URL WITHOUT WEB SERVICE ENDPOINT NAME

        return obj.toString();
    }

    public static byte[] createPackageFile(String authenticationToken, String path) throws Exception {

        System.out.println("packaging safari file with token: " + authenticationToken);
        ZipHandler zip = new ZipHandler();
        File dir = new File(path);

        for (File file : dir.listFiles()) {          
             InputStream is = new FileInputStream(file);
             byte[] bytes = IOUtils.toByteArray(is);
             zip.addFile("icon.iconset", file.getName(),bytes );
        }       

        zip.addFile("", "website.json", getJSON(authenticationToken).getBytes());

        byte[] manifest = zip.manifest();
        zip.addFile("", "manifest.json", manifest);

        zip.addFile("", "signature", sign(manifest));

        return zip.getBytes();

    }

    static byte[] sign(byte bytesToSign[]) throws Exception {
        return new PKCS7Signer().sign(CERTIFICATE_PATH,CERTIFICATE_PASS, bytesToSign);
    }

    /**
     * Servlet handler , should listen on the callback URL (as in webServiceURL)
     * @param requestPath
     * @param req
     * @param servletRequest
     * @param servletResponse
     * @throws Exception
     */


    public static void main(String[] args) throws Exception {
        Packager.createPackageFile("SafriNotifcation","");
    }               

}

PKCS7Signer.java which create your signature file.

import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;

public final class PKCS7Signer {

    static {
        try{
            Security.addProvider(new BouncyCastleProvider());
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    private KeyStore getKeystore(String storeLocation, String storePasswd) throws Exception {
        if (storeLocation == null) {
            System.out.println("Could not find store file (.p12)");
            return null;
        }
        // First load the keystore object by providing the p12 file path
        KeyStore clientStore = KeyStore.getInstance("PKCS12");
        // replace testPass with the p12 password/pin
        clientStore.load(new FileInputStream(storeLocation), storePasswd.toCharArray());
        return clientStore;
    }

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

    private PrivateKey getPrivateKey(KeyStore keystore, String alias, String storePasswd) throws Exception {
        return (PrivateKey) keystore.getKey(alias, storePasswd.toCharArray());
    }

    public byte[] sign(String storeLocation, String storePasswd, byte[] dataToSign) throws Exception {
        KeyStore clientStore = getKeystore(storeLocation, storePasswd);

        if (clientStore == null) {
            return null;
        }
        Enumeration aliases = clientStore.aliases();
        String alias = "";
        while (aliases.hasMoreElements()) {
            alias = (String) aliases.nextElement();
            if (clientStore.isKeyEntry(alias)) {
                break;
            }
        }

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

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

        Store certs = new JcaCertStore(certList);

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

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

please note used latest bouncy castle jars for create signature file bcprov-jdk15on-157.jar bcpkix-jdk15on-157.jar

  1. Write your javascript on your client side page.it contains simple java script to get permissions.

///

// For safari var domain="YOUR WEB PUSH ID";

function safariIniti() {

    var pResult = window.safari.pushNotification.permission(domain);

    if(pResult.permission === 'default') {
        //request permission
        requestPermissions();
    } else if (pResult.permission === 'granted') {
        console.log("Permission for " + domain + " is " + pResult.permission);
        var token = pResult.deviceToken;
        // Show subscription for debug
        console.log('Subscription details:'+token);
    } else if(pResult.permission === 'denied') {
        console.log("Permission for " + domain + " is " + pResult.permission);
    }
}

function getToken(){

    // always start with a letter (for DOM friendlyness)
    var idstr=String.fromCharCode(Math.floor((Math.random()*25)+65));
    do {                
        // between numbers and characters (48 is 0 and 90 is Z (42-48 = 90)
        var ascicode=Math.floor((Math.random()*42)+48);
        if (ascicode<58 || ascicode>64){
            // exclude all chars between : (58) and @ (64)
            idstr+=String.fromCharCode(ascicode);    
        }                
    } while (idstr.length<32);

    return (idstr);
}


function requestPermissions() {

    var tokenVal = getToken();
    window.safari.pushNotification.requestPermission('WEb service url without end points',domain,{token:tokenVal},
function(subscription) {


        console.log(subscription.permission);
        console.log("PERMISSION ====>> "+subscription.permission);
        if(subscription.permission === 'granted') {
            //TODO
        }
        else if(subscription.permission === 'denied') {
            // TODO:
        }
    });

}

Apple provides a php file that you can use to create your push package including the signature. https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NotificationProgrammingGuideForWebsites/CompanionFile.zip

Alternatively, you can use the push_package gem, https://github.com/SymmetricInfinity/push_package , which we developed when implementing safari push notifications for zeropush.com. More details are available at https://zeropush.com/blog/implementing-safari-push-notifications-in-osx-mavericks .

遵循此Apple 文档github repo ,它们包含创建 safari 推送通知所需的足够信息。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM