Skip to content

Instantly share code, notes, and snippets.

@1f604
Last active June 18, 2021 23:13
Show Gist options
  • Save 1f604/c6181b81b50adfaa1096c4d42e4e0346 to your computer and use it in GitHub Desktop.
Save 1f604/c6181b81b50adfaa1096c4d42e4e0346 to your computer and use it in GitHub Desktop.
golang push notification apple apns http2 token-based
// Code to send push notifications. Most of the code is taken from https://github.com/sideshow/apns2/
// I wrote this because I didn't want to use any 3rd party dependencies.
package main
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"math/big"
"net"
"net/http"
"strings"
"time"
"golang.org/x/net/http2"
)
var (
/* Make sure to change these variables. See Apple documentation on how to send push notification by command line. */
g_devicetoken = "your device token here" //64 hex char device token
g_authkey_location = "/path/to/Authkey.p8"
g_key_id = "ABCDE12345"
g_team_id = "EDCBA54321"
g_topic = "com.yourteam.yourappname" //App ID
/* You don't need to change these variables */
g_payload = map[string]interface{}{
"alert": "here is an alert!",
"sound": "default",
"badge": 9,
}
g_host = "https://api.sandbox.push.apple.com"
g_token_timeout int64 = 3000 //expire time for bearer tokens in seconds. Should be less than 3600 and more than 1200.
g_token_last_generated int64 = 0
g_bearer_token = ""
// TLSDialTimeout is the maximum amount of time a dial will wait for a connect
// to complete.
TLSDialTimeout = 20 * time.Second
// HTTPClientTimeout specifies a time limit for requests made by the
// HTTPClient. The timeout includes connection time, any redirects,
// and reading the response body.
HTTPClientTimeout = 60 * time.Second
// TCPKeepAlive specifies the keep-alive period for an active network
// connection. If zero, keep-alives are not enabled.
TCPKeepAlive = 60 * time.Second
)
var privkey *ecdsa.PrivateKey
// Creates new bearer token. Takes about 3ms in the worst case.
// Bearer token expires every hour. But Apple demands that you wait at least 20 mins between generating new tokens.
func GenerateBearerTokenIfExpired() string {
if (time.Now().Unix() - g_token_last_generated) < g_token_timeout {
return g_bearer_token
}
g_token_last_generated = time.Now().Unix()
jwt_header_bytes, _ := json.Marshal(map[string]interface{}{
"alg": "ES256",
"kid": g_key_id,
})
jwt_claims_bytes, _ := json.Marshal(map[string]interface{}{
"iss": g_team_id,
"iat": g_token_last_generated,
})
jwt_header := JWTBase64Encode(jwt_header_bytes)
jwt_claims := JWTBase64Encode(jwt_claims_bytes)
jwt_body := jwt_header + "." + jwt_claims
ret, _ := SignBytes([]byte(jwt_body))
bearer_token := jwt_body + "." + ret
header := bearer_token
return header
}
//source: dgrijalva/jwt-go/token.go
// Encode JWT specific base64url encoding with padding stripped
func JWTBase64Encode(seg []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
}
// AuthKeyFromFile loads a .p8 certificate from a local file and returns a
// *ecdsa.PrivateKey.
func AuthKeyFromFile(filename string) (*ecdsa.PrivateKey, error) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
return AuthKeyFromBytes(bytes)
}
// AuthKeyFromBytes loads a .p8 certificate from an in memory byte array and
// returns an *ecdsa.PrivateKey.
func AuthKeyFromBytes(bytes []byte) (*ecdsa.PrivateKey, error) {
block, err := pem.Decode(bytes)
if block == nil {
panic(err)
}
key, err1 := x509.ParsePKCS8PrivateKey(block.Bytes) //This function takes 11ms on my machine. So, make sure it's not run frequently.
if err1 != nil {
panic(err1)
}
switch pk := key.(type) {
case *ecdsa.PrivateKey:
return pk, nil
default:
panic("error: key of wrong type")
}
}
// Create and return a digital signature of a hash of the specified data.
// The equivalent command at the command line is:
// echo -n 'Make America Great Again!' | openssl dgst -sha256 -sign Authkey.p8
func SignBytes(b []byte) (string, error) {
hash := sha256.Sum256(b)
if privkey == nil {
panic("privkey is nil")
}
r, s, err := ecdsa.Sign(rand.Reader, privkey, hash[:])
if err != nil {
panic(err)
}
asn1Data := []*big.Int{r, s}
sbytes, err := asn1.Marshal(asn1Data)
if err != nil {
panic(err)
}
ret := JWTBase64Encode(sbytes)
return ret, nil
}
func setHeaders(r *http.Request) {
r.Header.Set("Content-Type", "application/json; charset=utf-8")
r.Header.Set("apns-topic", g_topic)
r.Header.Set("apns-push-type", "alert")
r.Header.Set("authorization", fmt.Sprintf("bearer %v", g_bearer_token))
}
type Response struct {
// The HTTP status code returned by APNs.
StatusCode int
// The APNs error string indicating the reason for the notification failure (if
// any). The error code is specified as a string.
Reason string
// The APNs ApnsID value from the Notification. If you didn't set an ApnsID on the
// Notification, this will be a new unique UUID which has been created by APNs.
ApnsID string
// If the value of StatusCode is 410, this is the last time at which APNs
// confirmed that the device token was no longer valid for the topic.
// Represents device uninstall time.
Timestamp time.Time
}
// DialTLS is the default dial function for creating TLS connections for
// non-proxied HTTPS requests.
var DialTLS = func(network, addr string, cfg *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: TLSDialTimeout,
KeepAlive: TCPKeepAlive,
}
return tls.DialWithDialer(dialer, network, addr, cfg)
}
func sendPush(bearerToken string) {
n := map[string]interface{}{
"aps": g_payload,
}
payload, err := json.Marshal(n)
if err != nil {
panic(err)
}
Host := g_host
DeviceToken := g_devicetoken
url := fmt.Sprintf("%v/3/device/%v", Host, DeviceToken)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
panic(err)
}
setHeaders(req)
var ctx context.Context
transport := &http2.Transport{
DialTLS: DialTLS,
}
HTTPClient := http.Client{
Transport: transport,
Timeout: HTTPClientTimeout,
}
if ctx != nil {
req = req.WithContext(ctx)
}
httpRes, err := HTTPClient.Do(req)
if err != nil {
panic(err)
}
defer httpRes.Body.Close()
response := &Response{}
response.StatusCode = httpRes.StatusCode
response.ApnsID = httpRes.Header.Get("apns-id")
decoder := json.NewDecoder(httpRes.Body)
err1 := decoder.Decode(&response)
if err1 != nil && err1 != io.EOF {
panic(err1)
}
fmt.Println(response)
}
func main() {
start := time.Now()
privkey, _ = AuthKeyFromFile(g_authkey_location)
duration := time.Since(start)
fmt.Println("program startup duration:", duration)
for {
start := time.Now()
g_bearer_token = GenerateBearerTokenIfExpired()
duration = time.Since(start)
fmt.Println("time taken to generate bearer token:", duration)
start = time.Now()
sendPush(g_bearer_token)
duration = time.Since(start)
fmt.Println("time taken to send push notification:", duration)
fmt.Println("Press the Enter Key to send another push notification!")
fmt.Scanln() // wait for Enter Key
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment