Last active
June 18, 2021 23:13
-
-
Save 1f604/c6181b81b50adfaa1096c4d42e4e0346 to your computer and use it in GitHub Desktop.
golang push notification apple apns http2 token-based
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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