// 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" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "strconv" "time" "go-common/library/log" "go-common/library/stat" "go-common/library/stat/prom" "golang.org/x/net/http2" "golang.org/x/net/proxy" ) const ( // HostDevelopment dev host. HostDevelopment = "https://api.development.push.apple.com" // HostProduction pro host. HostProduction = "https://api.push.apple.com" // StatusCodeSuccess success. StatusCodeSuccess = 200 // StatusCodeBadReq bad req. StatusCodeBadReq = 400 // StatusCodeCerErr There was an error with the certificate. StatusCodeCerErr = 403 // StatusCodeMethodErr The request used a bad :method value. Only POST requests are supported. StatusCodeMethodErr = 405 // StatusCodeNotForTopic The device token is not form the topic. StatusCodeNotForTopic = 400 // StatusCodeNoActive The device token is no longer active for the topic. StatusCodeNoActive = 410 // StatusCodePayloadTooLarge The notification payload was too large. StatusCodePayloadTooLarge = 413 // StatusCodeTooManyReq The server received too many requests for the same device token. StatusCodeTooManyReq = 429 // StatusCodeServerErr Internal server error StatusCodeServerErr = 500 // StatusCodeServerUnavailable The server is shutting down and unavailable. StatusCodeServerUnavailable = 503 ) // DefaultHost is a mutable var for testing purposes var DefaultHost = HostDevelopment // Client represents a connection with the APNs type Client struct { HTTPClient *http.Client Certificate tls.Certificate Host string BoundID string Stats stat.Stat } // func init() { // proxy.RegisterDialerType("http", func(*url.URL, proxy.Dialer) (proxy.Dialer, error) { // return &net.Dialer{}, nil // }) // } // 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. func NewClient(certificate tls.Certificate, timeout time.Duration) *Client { tlsConfig := &tls.Config{ Certificates: []tls.Certificate{certificate}, ClientAuth: tls.NoClientCert, } if len(certificate.Certificate) > 0 { tlsConfig.BuildNameToCertificate() } transport := &http2.Transport{ TLSClientConfig: tlsConfig, } // transport := &http.Transport{ // TLSClientConfig: tlsConfig, // Proxy: func(_ *http.Request) (*url.URL, error) { // return url.Parse("http://10.28.10.11:80") // }, // DialContext: (&net.Dialer{ // Timeout: 30 * time.Second, // KeepAlive: 30 * time.Second, // DualStack: true, // }).DialContext, // MaxIdleConns: 100, // IdleConnTimeout: 90 * time.Second, // TLSHandshakeTimeout: 10 * time.Second, // ExpectContinueTimeout: 1 * time.Second, // } return &Client{ HTTPClient: &http.Client{Transport: transport, Timeout: timeout}, Certificate: certificate, Host: DefaultHost, Stats: prom.HTTPClient, } } // NewClientWithProxy returns a new Client with sock5 proxy. func NewClientWithProxy(certificate tls.Certificate, timeout time.Duration, proxyAddr string) *Client { tlsConfig := &tls.Config{ Certificates: []tls.Certificate{certificate}, ClientAuth: tls.NoClientCert, } if len(certificate.Certificate) > 0 { tlsConfig.BuildNameToCertificate() } return &Client{ HTTPClient: &http.Client{Transport: proxyTransport(proxyAddr, tlsConfig, timeout), Timeout: timeout}, Certificate: certificate, Host: DefaultHost, Stats: prom.HTTPClient, } } func proxyTransport(proxyAddr string, config *tls.Config, timeout time.Duration) *http2.Transport { return &http2.Transport{ DialTLS: func(network, addr string, cfg *tls.Config) (nc net.Conn, err error) { dialer := &net.Dialer{Timeout: timeout / 2} var proxyDialer proxy.Dialer if proxyDialer, err = proxy.SOCKS5("tcp", proxyAddr, nil, dialer); err != nil { log.Error("proxy.SOCKS5(%s) error(%v)", proxyAddr, err) return nil, err } // u, _ := url.Parse("http://10.28.10.11:80") // proxyDialer, err = proxy.FromURL(u, dialer) var conn net.Conn if conn, err = proxyDialer.Dial(network, addr); err != nil { log.Error("proxyDialer.Dial(%s,%s) error(%v)", network, addr, err) if conn, err = dialer.Dial(network, addr); err != nil { log.Error("dialer.Dial(%s,%s) error(%v)", network, addr, err) return nil, err } } tlsConn := tls.Client(conn, cfg) if err = tlsConn.Handshake(); err != nil { log.Error("tlsConn.Handshake() error(%v)", err) return nil, err } if !cfg.InsecureSkipVerify { if err = tlsConn.VerifyHostname(cfg.ServerName); err != nil { log.Error("tlsConn.VerifyHostname(%s) error(%v)", cfg.ServerName, err) return nil, err } } state := tlsConn.ConnectionState() if state.NegotiatedProtocol != http2.NextProtoTLS { err = fmt.Errorf("http2: unexpected ALPN protocol(%s) expect(%s)", state.NegotiatedProtocol, http2.NextProtoTLS) return nil, err } if !state.NegotiatedProtocolIsMutual { err = errors.New("http2: could not negotiate protocol mutually") return nil, err } return tlsConn, nil }, TLSClientConfig: config, } } // 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. func (c *Client) Push(deviceToken string, payload *Payload, overTime int64) (response *Response, err error) { if c.Stats != nil { now := time.Now() defer func() { c.Stats.Timing(c.Host, int64(time.Since(now)/time.Millisecond)) log.Info("apns stats timing: %v", int64(time.Since(now)/time.Millisecond)) if err != nil { c.Stats.Incr(c.Host, "failed") } }() } var ( req *http.Request res *http.Response t = time.NewTimer(c.HTTPClient.Timeout) errCh = make(chan error, 1) url = fmt.Sprintf("%v/3/device/%v", c.Host, deviceToken) ) if req, err = http.NewRequest("POST", url, bytes.NewBuffer(payload.Marshal())); err != nil { log.Error("http.NewRequest(%s) error(%v)", url, err) return } req.Header.Set("apns-topic", c.BoundID) req.Header.Set("apns-expiration", strconv.FormatInt(overTime, 10)) req.Header.Set("apns-collapse-id", payload.TaskID) go func() { res, err = c.HTTPClient.Do(req) errCh <- err }() select { case <-t.C: err = errors.New("http.Do timeout") return case err = <-errCh: if err != nil { log.Error("c.HTTPClient.Do() error(%v)", err) return } } defer res.Body.Close() response = &Response{StatusCode: res.StatusCode, ApnsID: res.Header.Get("apns-id")} var bs []byte bs, err = ioutil.ReadAll(res.Body) if err != nil { log.Error("ioutil.ReadAll() error(%v)", err) return } else if len(bs) == 0 { return } if e := json.Unmarshal(bs, &response); e != nil { if e != io.EOF { log.Error("json decode body(%s) error(%v)", string(bs), e) } } return } // MockPush mock push. func (c *Client) MockPush(deviceToken string, payload *Payload, overTime int64) (response *Response, err error) { if c.Stats != nil { now := time.Now() defer func() { c.Stats.Timing(c.Host, int64(time.Since(now)/time.Millisecond)) // log.Info("mock apns stats timing: %v", int64(time.Since(now)/time.Millisecond)) if err != nil { c.Stats.Incr(c.Host, "apple push mock") } }() } time.Sleep(200 * time.Millisecond) response = &Response{StatusCode: StatusCodeSuccess} return }