Code Monkey home page Code Monkey logo

shove's Introduction

When push comes to shove...

Go Report Card Written in Emacs Pipeline Status

Background

This is the replacement for Pulsus which has been steadily serving up to 100M push notifications. But, given that it was still using the binary APNS protocol it was due for an upgrade.

Overview

Design:

  • Asynchronous: a push client can just fire & forget.
  • Multiple workers per push service.
  • Less moving parts: when using Redis, you can push directly to the queue, bypassing the need for the Shove server to be up and running.

Supported push services:

  • APNS
  • Email: supports automatic creation of email digests in case the rate limit is exceeded
  • FCM
  • Telegram: supports squashing multiple messages into one in case the rate limit is exceeded
  • Webhook: issue arbitrary webhook posts
  • Web Push

Features:

  • Feedback: asynchronously receive information on invalid device tokens.
  • Queueing: both in-memory and persistent via Redis.
  • Exponential back-off in case of failure.
  • Prometheus support.
  • Squashing of messages in case rate limits are exceeded.

Why?

Usage

Running

Usage:

$ shove -h
Usage of ./shove:
  -api-addr string
        API address to listen to (default ":8322")
  -apns-certificate-path string
        APNS certificate path
  -apns-sandbox-certificate-path string
        APNS sandbox certificate path
  -apns-workers int
        The number of workers pushing APNS messages (default 4)
  -email-host string
        Email host
  -email-port int
        Email port (default 25)
  -email-rate-amount int
        Email max. rate (amount)
  -email-rate-per int
        Email max. rate (per seconds)
  -email-tls
        Use TLS
  -email-tls-insecure
        Skip TLS verification
  -fcm-api-key string
        FCM API key
  -fcm-workers int
        The number of workers pushing FCM messages (default 4)
  -queue-redis string
        Use Redis queue (Redis URL)
  -telegram-bot-token string
        Telegram bot token
  -telegram-rate-amount int
        Telegram max. rate (amount)
  -telegram-rate-per int
        Telegram max. rate (per seconds)
  -telegram-workers int
        The number of workers pushing Telegram messages (default 2)
  -webhook-workers int
        The number of workers pushing Webhook messages
  -webpush-vapid-private-key string
        VAPID public key
  -webpush-vapid-public-key string
        VAPID public key
  -webpush-workers int
        The number of workers pushing Web messages (default 8)

Start the server:

$ shove \
    -api-addr localhost:8322 \
    -queue-redis redis://redis:6379 \
    -fcm-api-key $FCM_API_KEY \
    -apns-certificate-path /etc/shove/apns/production/bundle.pem -apns-sandbox-certificate-path /etc/shove/apns/sandbox/bundle.pem \
    -webpush-vapid-public-key=$VAPID_PUBLIC_KEY -webpush-vapid-private-key=$VAPID_PRIVATE_KEY \
    -telegram-bot-token $TELEGRAM_BOT_TOKEN

APNS

Push an APNS notification:

$ curl  -i  --data '{"service": "apns", "headers": {"apns-priority": 10, "apns-topic": "com.shove.app"}, "payload": {"aps": { "alert": "hi"}}, "token": "81b8ecff8cb6d22154404d43b9aeaaf6219dfbef2abb2fe313f3725f4505cb47"}' http://localhost:8322/api/push/apns

A successful push results in:

HTTP/1.1 202 Accepted
Date: Tue, 07 May 2019 19:00:15 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8

OK

FCM

Push an FCM notification:

$ curl  -i  --data '{"to": "feE8R6apOdA:AA91PbGHMX5HUoB-tbcqBO_e75NbiOc2AiFbGL3rrYtc99Z5ejbGmCCvOhKW5liqfOzRGOXxto5l7y6b_0dCc-AQ2_bXOcDkcPZgsXGbZvmEjaZA72DfVkZ2pfRrcpcc_9IiiRT5NYC", "notification": {"title": "Hello"}}' http://localhost:8322/api/push/fcm

Webhook

Push a Webhook call, containing arbitrary body content:

$ curl  -i  --data '{"url": "http://localhost:8000/api/webhook", "headers": {"foo": "bar"}, "body": "Hello world!"}' http://localhost:8322/api/push/webhook

Or, post JSON:

$ curl  -i  --data '{"url": "http://localhost:8000/api/webhook", "headers": {"foo": "bar"}, "data": {"hello": "world!"}}' http://localhost:8322/api/push/webhook

WebPush

Push a WebPush notification:

$ curl  -i  --data '{"subscription": {"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/gAAAAAc4BA....UrjGlg","keys":{"auth":"Hbj3ap...al9ew","p256dh":"BeKdTC3...KLGBJlgF"}}, "headers": {"ttl": 3600, "urgency": "high"}, "token": "use-this-for-feedback-instead-of-subscription", "payload": {"hello":"world"}}' http://localhost:8322/api/push/webpush

The subscription (serialized as a JSON string) is used for receiving feedback. Alternatively, you can specify an optional token parameter as done in the example above.

Telegram

Push a Telegram notification:

$ curl  -i  --data '{"method": "sendMessage", "payload": {"chat_id": "12345678", "text": "Hello!"}}' http://localhost:8322/api/push/telegram

Note that the Telegram Bot API documents chat_id as "Integer or String" -- Shove requires strings to be passed. For users that disconnected from your bot the chat ID will be communicated back through the feedback mechanism. Here, the token will equal the unreachable chat ID.

Receive Feedback

Outdated/invalid tokens are communicated back. To receive those, you can periodically query the feedback channel to receive token feedback, and remove those from your database:

$ curl -X POST 'http://localhost:8322/api/feedback'

{
  "feedback": [
    {"service":"apns-sandbox",
     "token":"881becff86cbd221544044d3b9aeaaf6314dfbef2abb2fe313f3725f4505cb47",
     "reason":"invalid"}
  ]
}

Email

In order to keep your SMTP server safe from being blacklisted, the email service supports rate limitting. When the rate is exceeded, multiple mails are automatically digested.

$ shove \
    -email-host localhost \
    -email-port 1025 \
    -api-addr localhost:8322 \
    -email-rate-amount 3 \
    -email-rate-per 10 \
    -queue-redis redis://localhost:6379

Push an email:

$ curl -i -X POST --data @./scripts/email.json http://localhost:8322/api/push/email

If you send too many emails, you'll notice that they are digested, and at a later time, one digest mail is being sent:

2021/03/23 21:15:57 Using Redis queue at redis://localhost:6379
2021/03/23 21:15:57 Initializing Email service
2021/03/23 21:15:57 Serving on localhost:8322
2021/03/23 21:15:57 Shove server started
2021/03/23 21:15:57 email: Worker started
2021/03/23 21:15:57 email: Digester started
2021/03/23 21:15:58 email: Sending email
2021/03/23 21:15:59 email: Sending email
2021/03/23 21:15:59 email: Sending email
2021/03/23 21:16:00 email: Rate to [email protected] exceeded, email digested
2021/03/23 21:16:12 email: Rate to [email protected] exceeded, email digested
2021/03/23 21:16:18 email: Sending digest email

Redis Queues

Shove is being used to push a high volume of notifications in a production environment, consisting of various microservices interacting together. In such a scenario, it is important that the various services are not too tightly coupled to one another. For that purpose, Shove offers the ability to post notifications directly to a Redis queue.

Posting directly to the Redis queue, instead of using the HTTP service endpoints, has the advantage that you can take Shove offline without disturbing the operation of the clients pushing the notifications.

Shove intentionally tries to make as little assumptions on the notification payloads being pushed, as they are mostly handed over as is to the upstream services. So, when using Shove this way, the client is responsible for handing over a raw payload. Here's an example:

package main

import (
	"encoding/json"
	"gitlab.com/pennersr/shove/pkg/shove"
	"log"
	"os"
)

type FCMNotification struct {
	To       string            `json:"to"`
	Data     map[string]string `json:"data,omitempty"`
}

func main() {
	redisURL := os.Getenv("REDIS_URL")
	if redisURL == "" {
		redis_URL = "redis://localhost:6379"
	}
	client := shove.NewRedisClient(redisURL)

	notification := FCMNotification{
		To:   "token....",
		Data: map[string]string{},
	}

	raw, err := json.Marshal(notification)
	if err != nil {
		log.Fatal(err)
	}
	err = client.PushRaw("fcm", raw)
	if err != nil {
		log.Fatal(err)
	}
}

Status

Used in production, over at:

shove's People

Contributors

pennersr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

shove's Issues

Redis queue docs

I think it would be really useful to provide more docs about the redis queue support in the readme, as that is the biggest feature for me.

For example, what do I need to do to add an item to the redis queue? What is the stream name?

WebPush using VAPID, error: illegal base64 data at input byte 0

Hello. Launched your project to send WebPush notifications. Checking sending using VAPID.
curl -i --data '{"subscription": {"endpoint":"https://fcm.googleapis.com/fcm/send/cUGz6wv7OH0:APA91bHqwewkZFJ5oouwv0jZOYpwlsN3jg7n_83dnzbL5SCU5xNkTlcA9ozP0eOIHP3mkASJIkO1Idakd7j9atSugofRZEowo9N8YpTE9Qars6B6wCLkdpe5PKmNX0BP5fNTkKoRamZA","keys":{"auth": "ZmdkuTkUN0NTvsQqV7SUIA", "p256dh": "BBYQ484rGdY-HfEjAjGQUHH0k1KDmgsy6CnVcbr2l0SYZK1amCTVvNcbUCJazcawAMPOCcNkG0Omu3m2Scb9WoU"}}, "headers": {"ttl": 3600, "urgency": "high"}, "token": "token-1", "payload": {"hello":"world"}}' http://localhost:8322/api/push/webpush

I see an error in the console:
Shove error sending: illegal base64 data at input byte 0

If you make an error in the p256dh key, an error will occur:
Unmarshal Error: Public key is not a valid point on the curve

That is, presumably the first error occurs below this line:
https://github.com/SherClockHolmes/webpush-go/blob/358c5ab877144efa20d3e889826d31d45ea3bec2/webpush.go#L101

There may be a problem with incorrectly passing JSON to the library.

What am I doing wrong? Does WebPush using VAPID work correctly for you?

Build / setup steps for CentOS 7

FWIW, for anyone looking to find build instruction. Below steps are tested in DigitalOcean CentOS 7 droplet.

# Prepare...
# Current directory...
[root@centos-s-1vcpu-1gb-nyc3-01 ~]# pwd
/root
# Check CentOS version...
[root@centos-s-1vcpu-1gb-nyc3-01 ~]# cat /etc/centos-release
CentOS Linux release 7.9.2009 (Core)
# go to /opt/
[root@centos-s-1vcpu-1gb-nyc3-01 ~]# cd /opt/
# Download shove source from git...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# wget https://github.com/pennersr/shove/archive/master.zip
# Rename file...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# mv master.zip shove.zip
# Unzip...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# unzip shove.zip
# Check files and folder...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# ls
shove-master  shove.zip
# Rename folder...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# mv shove-master/ shove
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# ls
shove  shove.zip

# Install go...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# wget https://golang.org/dl/go1.15.6.linux-amd64.tar.gz
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# tar -C /usr/local -xzf go1.15.6.linux-amd64.tar.gz
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# export PATH=$PATH:/usr/local/go/bin
# Check go version...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# go version
go version go1.15.6 linux/amd64

# Install redis...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# yum install epel-release
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# yum install redis
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# systemctl start redis.service
# Check redis...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# redis-cli -h localhost ping
PONG

# Build shove...
[root@centos-s-1vcpu-1gb-nyc3-01 opt]# cd shove
[root@centos-s-1vcpu-1gb-nyc3-01 shove]# mkdir build
[root@centos-s-1vcpu-1gb-nyc3-01 shove]# go build -o build ./...
[root@centos-s-1vcpu-1gb-nyc3-01 shove]# cd build
[root@centos-s-1vcpu-1gb-nyc3-01 build]# chmod +x shove
[root@centos-s-1vcpu-1gb-nyc3-01 build]# mkdir -p /usr/local/shove/bin
[root@centos-s-1vcpu-1gb-nyc3-01 build]# mv shove /usr/local/shove/bin/

# Setup firewall and open port 8322; otherwise couldn't access through IP:8322
[root@centos-s-1vcpu-1gb-nyc3-01 build]# yum install firewalld
[root@centos-s-1vcpu-1gb-nyc3-01 build]# systemctl enable firewalld
[root@centos-s-1vcpu-1gb-nyc3-01 build]# reboot
[root@centos-s-1vcpu-1gb-nyc3-01 ~]# firewall-cmd --permanent --add-port=8322/tcp
success
[root@centos-s-1vcpu-1gb-nyc3-01 ~]# firewall-cmd --reload


# Now execute something like:
/usr/local/shove/bin/shove -api-addr x.x.x.x:8322 -fcm-api-key xxxxxxx

Using Redis queue throws 404

Apologies, if this any issue with the calling syntax.

Setup: Using FCM alone with shove

It works without Redis, but with Redis, it throws 404. Also, it misses FCM workers started message

Works without Redis

shove -api-addr x.x.x.x:8322 -fcm-api-key xxxxxx
2020/12/29 15:00:53 Using non-persistent in-memory queue
2020/12/29 15:00:53 Initializing FCM service
2020/12/29 15:00:53 Shove server started
2020/12/29 15:00:53 FCM workers started
curl  -i  --data '{"to": "XXXXX", "notification": {"title": "Hello"}}' http://x.x.x.x:8322/api/push/fcm

HTTP/1.1 202 Accepted
Date: Tue, 29 Dec 2020 14:47:44 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8

OK

Not working with Redis

shove -api-addr x.x.x.x:8322 -queue-redis redis://redis:6379 -fcm-api-key xxxxx
2020/12/29 15:04:46 Using Redis queue at redis://redis:6379
2020/12/29 15:04:46 Initializing FCM service
2020/12/29 15:04:46 Shove server started
curl  -i  --data '{"to": "XXXXX", "notification": {"title": "Hello"}}' http://x.x.x.x:8322/api/push/fcm


HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 29 Dec 2020 15:10:09 GMT
Content-Length: 19

404 page not found

Other attempts

Instead of redis://redis:6379, tried redis://localhost:6379 and localhost:6379 too.

feedback API hangs and times out

Setup: Using FCM alone with shove (with and without Redis)

curl -X POST 'http://x.x.x.x:8322/api/feedback'

curl: (7) Failed to connect to x.x.x.x port 8322: Connection timed out

Email service config to support disabling of TLS

Hi, I would very like to use this project as my push notification service in production.
I'm currently using the email service. However due to some network restriction from my department, I am only able to send email without TLS.

I've looked through the implementation and saw the the email service uses https://github.com/jordan-wright/email package.
As you've already encoded the data nicely in body I can recreate the *Email object and send it using the email package.
With the extra config at the end &tls.Config{InsecureSkipVerify: true} I was able to send the email out successfully.

It would be great if we could include additional settings to configure the email client when sending out the email.
I added the snippet which I am using now. Do let me know if it make sense and whether I can help to contribute to the core codes.

func (ec EmailConfig) send(from string, to []string, body []byte, fc services.FeedbackCollector) error {
        t := time.Now()
        email, _ := jwemail.NewEmailFromReader(bytes.NewReader(body))
        err := email.SendWithStartTLS(fmt.Sprintf("%s:%d", ec.EmailHost, ec.EmailPort), nil, &tls.Config{InsecureSkipVerify: true})
        duration := time.Since(t)
        fc.CountPush(serviceID, err == nil, duration)

        if err != nil {
                ec.Log.Printf("[ERROR] Send failed: %s", err)
                return err
        }
        return nil
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.