mirror of
https://github.com/christianselig/apollo-backend
synced 2024-11-14 16:07:42 +00:00
221 lines
6.7 KiB
Go
221 lines
6.7 KiB
Go
|
// Package apns2 is a go Apple Push Notification Service (APNs) provider that
|
|||
|
// allows you to send remote notifications to your iOS, tvOS, and OS X
|
|||
|
// apps, using the new APNs HTTP/2 network protocol.
|
|||
|
package apns2
|
|||
|
|
|||
|
import (
|
|||
|
"bytes"
|
|||
|
"crypto/tls"
|
|||
|
"encoding/json"
|
|||
|
"fmt"
|
|||
|
"io"
|
|||
|
"net"
|
|||
|
"net/http"
|
|||
|
"time"
|
|||
|
|
|||
|
"github.com/sideshow/apns2/token"
|
|||
|
"golang.org/x/net/http2"
|
|||
|
)
|
|||
|
|
|||
|
// Apple HTTP/2 Development & Production urls
|
|||
|
const (
|
|||
|
HostDevelopment = "https://api.sandbox.push.apple.com"
|
|||
|
HostProduction = "https://api.push.apple.com"
|
|||
|
)
|
|||
|
|
|||
|
// DefaultHost is a mutable var for testing purposes
|
|||
|
var DefaultHost = HostDevelopment
|
|||
|
|
|||
|
var (
|
|||
|
// 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
|
|||
|
)
|
|||
|
|
|||
|
// 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)
|
|||
|
}
|
|||
|
|
|||
|
// Client represents a connection with the APNs
|
|||
|
type Client struct {
|
|||
|
Host string
|
|||
|
Certificate tls.Certificate
|
|||
|
Token *token.Token
|
|||
|
HTTPClient *http.Client
|
|||
|
}
|
|||
|
|
|||
|
type connectionCloser interface {
|
|||
|
CloseIdleConnections()
|
|||
|
}
|
|||
|
|
|||
|
// NewClient returns a new Client with an underlying http.Client configured with
|
|||
|
// the correct APNs HTTP/2 transport settings. It does not connect to the APNs
|
|||
|
// until the first Notification is sent via the Push method.
|
|||
|
//
|
|||
|
// As per the Apple APNs Provider API, you should keep a handle on this client
|
|||
|
// so that you can keep your connections with APNs open across multiple
|
|||
|
// notifications; don’t repeatedly open and close connections. APNs treats rapid
|
|||
|
// connection and disconnection as a denial-of-service attack.
|
|||
|
//
|
|||
|
// If your use case involves multiple long-lived connections, consider using
|
|||
|
// the ClientManager, which manages clients for you.
|
|||
|
func NewClient(certificate tls.Certificate) *Client {
|
|||
|
tlsConfig := &tls.Config{
|
|||
|
Certificates: []tls.Certificate{certificate},
|
|||
|
}
|
|||
|
if len(certificate.Certificate) > 0 {
|
|||
|
tlsConfig.BuildNameToCertificate()
|
|||
|
}
|
|||
|
transport := &http2.Transport{
|
|||
|
TLSClientConfig: tlsConfig,
|
|||
|
DialTLS: DialTLS,
|
|||
|
}
|
|||
|
return &Client{
|
|||
|
HTTPClient: &http.Client{
|
|||
|
Transport: transport,
|
|||
|
Timeout: HTTPClientTimeout,
|
|||
|
},
|
|||
|
Certificate: certificate,
|
|||
|
Host: DefaultHost,
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// NewTokenClient returns a new Client with an underlying http.Client configured
|
|||
|
// with the correct APNs HTTP/2 transport settings. It does not connect to the APNs
|
|||
|
// until the first Notification is sent via the Push method.
|
|||
|
//
|
|||
|
// As per the Apple APNs Provider API, you should keep a handle on this client
|
|||
|
// so that you can keep your connections with APNs open across multiple
|
|||
|
// notifications; don’t repeatedly open and close connections. APNs treats rapid
|
|||
|
// connection and disconnection as a denial-of-service attack.
|
|||
|
func NewTokenClient(token *token.Token) *Client {
|
|||
|
transport := &http2.Transport{
|
|||
|
DialTLS: DialTLS,
|
|||
|
}
|
|||
|
return &Client{
|
|||
|
Token: token,
|
|||
|
HTTPClient: &http.Client{
|
|||
|
Transport: transport,
|
|||
|
Timeout: HTTPClientTimeout,
|
|||
|
},
|
|||
|
Host: DefaultHost,
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Development sets the Client to use the APNs development push endpoint.
|
|||
|
func (c *Client) Development() *Client {
|
|||
|
c.Host = HostDevelopment
|
|||
|
return c
|
|||
|
}
|
|||
|
|
|||
|
// Production sets the Client to use the APNs production push endpoint.
|
|||
|
func (c *Client) Production() *Client {
|
|||
|
c.Host = HostProduction
|
|||
|
return c
|
|||
|
}
|
|||
|
|
|||
|
// Push sends a Notification to the APNs gateway. If the underlying http.Client
|
|||
|
// is not currently connected, this method will attempt to reconnect
|
|||
|
// transparently before sending the notification. It will return a Response
|
|||
|
// indicating whether the notification was accepted or rejected by the APNs
|
|||
|
// gateway, or an error if something goes wrong.
|
|||
|
//
|
|||
|
// Use PushWithContext if you need better cancellation and timeout control.
|
|||
|
func (c *Client) Push(n *Notification) (*Response, error) {
|
|||
|
return c.PushWithContext(nil, n)
|
|||
|
}
|
|||
|
|
|||
|
// PushWithContext sends a Notification to the APNs gateway. Context carries a
|
|||
|
// deadline and a cancellation signal and allows you to close long running
|
|||
|
// requests when the context timeout is exceeded. Context can be nil, for
|
|||
|
// backwards compatibility.
|
|||
|
//
|
|||
|
// If the underlying http.Client is not currently connected, this method will
|
|||
|
// attempt to reconnect transparently before sending the notification. It will
|
|||
|
// return a Response indicating whether the notification was accepted or
|
|||
|
// rejected by the APNs gateway, or an error if something goes wrong.
|
|||
|
func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error) {
|
|||
|
payload, err := json.Marshal(n)
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
|
|||
|
url := fmt.Sprintf("%v/3/device/%v", c.Host, n.DeviceToken)
|
|||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
|
|||
|
if c.Token != nil {
|
|||
|
c.setTokenHeader(req)
|
|||
|
}
|
|||
|
|
|||
|
setHeaders(req, n)
|
|||
|
|
|||
|
httpRes, err := c.requestWithContext(ctx, req)
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
defer httpRes.Body.Close()
|
|||
|
|
|||
|
response := &Response{}
|
|||
|
response.StatusCode = httpRes.StatusCode
|
|||
|
response.ApnsID = httpRes.Header.Get("apns-id")
|
|||
|
|
|||
|
decoder := json.NewDecoder(httpRes.Body)
|
|||
|
if err := decoder.Decode(&response); err != nil && err != io.EOF {
|
|||
|
return &Response{}, err
|
|||
|
}
|
|||
|
return response, nil
|
|||
|
}
|
|||
|
|
|||
|
// CloseIdleConnections closes any underlying connections which were previously
|
|||
|
// connected from previous requests but are now sitting idle. It will not
|
|||
|
// interrupt any connections currently in use.
|
|||
|
func (c *Client) CloseIdleConnections() {
|
|||
|
c.HTTPClient.Transport.(connectionCloser).CloseIdleConnections()
|
|||
|
}
|
|||
|
|
|||
|
func (c *Client) setTokenHeader(r *http.Request) {
|
|||
|
c.Token.GenerateIfExpired()
|
|||
|
r.Header.Set("authorization", fmt.Sprintf("bearer %v", c.Token.Bearer))
|
|||
|
}
|
|||
|
|
|||
|
func setHeaders(r *http.Request, n *Notification) {
|
|||
|
r.Header.Set("Content-Type", "application/json; charset=utf-8")
|
|||
|
if n.Topic != "" {
|
|||
|
r.Header.Set("apns-topic", n.Topic)
|
|||
|
}
|
|||
|
if n.ApnsID != "" {
|
|||
|
r.Header.Set("apns-id", n.ApnsID)
|
|||
|
}
|
|||
|
if n.CollapseID != "" {
|
|||
|
r.Header.Set("apns-collapse-id", n.CollapseID)
|
|||
|
}
|
|||
|
if n.Priority > 0 {
|
|||
|
r.Header.Set("apns-priority", fmt.Sprintf("%v", n.Priority))
|
|||
|
}
|
|||
|
if !n.Expiration.IsZero() {
|
|||
|
r.Header.Set("apns-expiration", fmt.Sprintf("%v", n.Expiration.Unix()))
|
|||
|
}
|
|||
|
if n.PushType != "" {
|
|||
|
r.Header.Set("apns-push-type", string(n.PushType))
|
|||
|
} else {
|
|||
|
r.Header.Set("apns-push-type", string(PushTypeAlert))
|
|||
|
}
|
|||
|
|
|||
|
}
|