Compare commits
5 Commits
dbfe957f63
...
6d51145dad
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d51145dad | |||
| dc557efde9 | |||
| 2d39edac8e | |||
| c894e4da13 | |||
| e55232e00b |
|
|
@ -15,7 +15,7 @@ import (
|
||||||
|
|
||||||
var authHeaderRegex = regexp.MustCompile("^Bearer (.+)$")
|
var authHeaderRegex = regexp.MustCompile("^Bearer (.+)$")
|
||||||
|
|
||||||
func (b *memoryBackend) cachedCheckAuthToken(conn redis.Conn, header string, r *auth.TokenResponse) (bool, error) {
|
func cachedCheckAuthToken(conn redis.Conn, header string, tokenEndpoint string, r *auth.TokenResponse) (bool, error) {
|
||||||
tokens := authHeaderRegex.FindStringSubmatch(header)
|
tokens := authHeaderRegex.FindStringSubmatch(header)
|
||||||
|
|
||||||
if len(tokens) != 2 {
|
if len(tokens) != 2 {
|
||||||
|
|
@ -33,7 +33,7 @@ func (b *memoryBackend) cachedCheckAuthToken(conn redis.Conn, header string, r *
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
authorized, err = b.checkAuthToken(header, r)
|
authorized, err = checkAuthToken(header, tokenEndpoint, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "could not check auth token")
|
return false, errors.Wrap(err, "could not check auth token")
|
||||||
}
|
}
|
||||||
|
|
@ -50,8 +50,7 @@ func (b *memoryBackend) cachedCheckAuthToken(conn redis.Conn, header string, r *
|
||||||
return authorized, nil
|
return authorized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *memoryBackend) checkAuthToken(header string, token *auth.TokenResponse) (bool, error) {
|
func checkAuthToken(header string, tokenEndpoint string, token *auth.TokenResponse) (bool, error) {
|
||||||
tokenEndpoint := b.TokenEndpoint
|
|
||||||
|
|
||||||
req, err := buildValidateAuthTokenRequest(tokenEndpoint, header)
|
req, err := buildValidateAuthTokenRequest(tokenEndpoint, header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"expvar"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
|
@ -49,6 +50,22 @@ type Feed struct {
|
||||||
ResubscribeAt int64 `redis:"resubscribe_at"`
|
ResubscribeAt int64 `redis:"resubscribe_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
subscribeRuns *expvar.Int
|
||||||
|
resubscriptions *expvar.Int
|
||||||
|
resubscribeErrors *expvar.Int
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
subscribeRuns = expvar.NewInt("subscribe.runs")
|
||||||
|
resubscriptions = expvar.NewInt("subscribe.calls")
|
||||||
|
resubscribeErrors = expvar.NewInt("subscribe.errors")
|
||||||
|
|
||||||
|
expvar.Publish("subscribe.runs", subscribeRuns)
|
||||||
|
expvar.Publish("subscribe.calls", resubscriptions)
|
||||||
|
expvar.Publish("subscribe.errors", resubscribeErrors)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *hubIncomingBackend) GetSecret(id int64) string {
|
func (h *hubIncomingBackend) GetSecret(id int64) string {
|
||||||
conn := h.pool.Get()
|
conn := h.pool.Get()
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
@ -231,7 +248,12 @@ func (h *hubIncomingBackend) run() error {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
log.Println("Getting feeds for WebSub")
|
log.Println("Getting feeds for WebSub")
|
||||||
feeds := h.GetFeeds()
|
subscribeRuns.Add(1)
|
||||||
|
|
||||||
|
feeds, err := h.Feeds()
|
||||||
|
if err != nil {
|
||||||
|
}
|
||||||
|
|
||||||
for _, feed := range feeds {
|
for _, feed := range feeds {
|
||||||
log.Printf("Looking at %s\n", feed.URL)
|
log.Printf("Looking at %s\n", feed.URL)
|
||||||
if feed.ResubscribeAt == 0 || time.Now().After(time.Unix(feed.ResubscribeAt, 0)) {
|
if feed.ResubscribeAt == 0 || time.Now().After(time.Unix(feed.ResubscribeAt, 0)) {
|
||||||
|
|
@ -239,9 +261,11 @@ func (h *hubIncomingBackend) run() error {
|
||||||
feed.Callback = fmt.Sprintf("%s/incoming/%d", h.baseURL, feed.ID)
|
feed.Callback = fmt.Sprintf("%s/incoming/%d", h.baseURL, feed.ID)
|
||||||
}
|
}
|
||||||
log.Printf("Send resubscribe for %q on %q with callback %q\n", feed.URL, feed.Hub, feed.Callback)
|
log.Printf("Send resubscribe for %q on %q with callback %q\n", feed.URL, feed.Hub, feed.Callback)
|
||||||
|
resubscriptions.Add(1)
|
||||||
err := h.Subscribe(&feed)
|
err := h.Subscribe(&feed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error while subscribing: %s", err)
|
log.Printf("Error while subscribing: %s", err)
|
||||||
|
resubscribeErrors.Add(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,14 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha1"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
"p83.nl/go/ekster/pkg/websub"
|
||||||
)
|
)
|
||||||
|
|
||||||
type incomingHandler struct {
|
type incomingHandler struct {
|
||||||
|
|
@ -85,8 +84,9 @@ func (h *incomingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// match signature
|
// match signature
|
||||||
sig := r.Header.Get("X-Hub-Signature")
|
sig := r.Header.Get("X-Hub-Signature")
|
||||||
if sig != "" {
|
if sig != "" {
|
||||||
if err := isHubSignatureValid(sig, feedContent, secret); err != nil {
|
if err := websub.ValidateHubSignature(sig, feedContent, []byte(secret)); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Error in signature: %s", err), 400)
|
log.Printf("could not validate signature: %+v", err)
|
||||||
|
http.Error(w, fmt.Sprintf("could not validate signature: %s", err), 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,32 +94,9 @@ func (h *incomingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
ct := r.Header.Get("Content-Type")
|
ct := r.Header.Get("Content-Type")
|
||||||
err = h.Backend.UpdateFeed(feed, ct, bytes.NewBuffer(feedContent))
|
err = h.Backend.UpdateFeed(feed, ct, bytes.NewBuffer(feedContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Unknown format of body: %s (%s)", ct, err), 400)
|
http.Error(w, fmt.Sprintf("could not update feed: %s (%s)", ct, err), 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func isHubSignatureValid(sig string, feedContent []byte, secret string) error {
|
|
||||||
parts := strings.Split(sig, "=")
|
|
||||||
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return fmt.Errorf("signature format is not like sha1=signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
if parts[0] != "sha1" {
|
|
||||||
return fmt.Errorf("signature format is not like sha1=signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
// verification
|
|
||||||
mac := hmac.New(sha1.New, []byte(secret))
|
|
||||||
mac.Write(feedContent)
|
|
||||||
signature := mac.Sum(nil)
|
|
||||||
|
|
||||||
if fmt.Sprintf("%x", signature) != parts[1] {
|
|
||||||
return fmt.Errorf("signature does not match feed %s %s", signature, parts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ func (b *memoryBackend) AuthTokenAccepted(header string, r *auth.TokenResponse)
|
||||||
log.Printf("could not close redis connection: %v", err)
|
log.Printf("could not close redis connection: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return b.cachedCheckAuthToken(conn, header, r)
|
return cachedCheckAuthToken(conn, header, b.TokenEndpoint, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *memoryBackend) Debug() {
|
func (b *memoryBackend) Debug() {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,86 @@ type micropubHandler struct {
|
||||||
pool *redis.Pool
|
pool *redis.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *micropubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
err := r.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not close request body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn := h.pool.Get()
|
||||||
|
defer func() {
|
||||||
|
err := conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not close redis connection: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
var channel string
|
||||||
|
|
||||||
|
channel, err = getChannelFromAuthorization(r, conn)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, "unauthorized", 401)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// no channel is found
|
||||||
|
if channel == "" {
|
||||||
|
http.Error(w, "bad request, unknown channel", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := parseIncomingItem(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Read = false
|
||||||
|
newID, err := generateItemID(conn, channel)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.ID = newID
|
||||||
|
|
||||||
|
err = h.Backend.channelAddItemWithMatcher(channel, *item)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not add item to channel %s: %v", channel, err)
|
||||||
|
}
|
||||||
|
err = h.Backend.updateChannelUnreadCount(channel)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not update channel unread content %s: %v", channel, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err = json.NewEncoder(w).Encode(map[string]string{"ok": "1"}); err != nil {
|
||||||
|
http.Error(w, "internal server error", 500)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "Method not allowed", 405)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateItemID(conn redis.Conn, channel string) (string, error) {
|
||||||
|
id, err := redis.Int(conn.Do("INCR", fmt.Sprintf("source:%s:next_id", channel)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", sha1.Sum([]byte(fmt.Sprintf("source:%s:%d", channel, id)))), nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseIncomingItem(r *http.Request) (*microsub.Item, error) {
|
func parseIncomingItem(r *http.Request) (*microsub.Item, error) {
|
||||||
var item microsub.Item
|
var item microsub.Item
|
||||||
|
|
||||||
|
|
@ -59,66 +139,29 @@ func parseIncomingItem(r *http.Request) (*microsub.Item, error) {
|
||||||
return &item, nil
|
return &item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
func getChannelFromAuthorization(r *http.Request, conn redis.Conn) (string, error) {
|
||||||
* URLs needed:
|
// backward compatible
|
||||||
* - / with endpoint urls
|
|
||||||
* - /micropub micropub endpoint
|
|
||||||
* - /auth auth endpoint
|
|
||||||
* - /token token endpoint
|
|
||||||
*/
|
|
||||||
func (h *micropubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
conn := h.pool.Get()
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "bad request", 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
sourceID := r.URL.Query().Get("source_id")
|
sourceID := r.URL.Query().Get("source_id")
|
||||||
|
if sourceID != "" {
|
||||||
authHeader := r.Header.Get("Authorization")
|
|
||||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
|
||||||
sourceID = authHeader[7:]
|
|
||||||
}
|
|
||||||
|
|
||||||
channel, err := redis.String(conn.Do("HGET", "sources", sourceID))
|
channel, err := redis.String(conn.Do("HGET", "sources", sourceID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
channel, err = redis.String(conn.Do("HGET", "token:"+sourceID, "channel"))
|
return "", errors.Wrapf(err, "could not get channel for sourceID: %s", sourceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return channel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// full micropub with indieauth
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
token := authHeader[7:]
|
||||||
|
channel, err := redis.String(conn.Do("HGET", "token:"+token, "channel"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "unauthorized", 401)
|
return "", errors.Wrap(err, "could not get channel for token")
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err := parseIncomingItem(r)
|
return channel, nil
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), 400)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Read = false
|
return "", fmt.Errorf("could not get channel from authorization")
|
||||||
id, _ := redis.Int(conn.Do("INCR", "source:"+sourceID+"next_id"))
|
|
||||||
item.ID = fmt.Sprintf("%x", sha1.Sum([]byte(fmt.Sprintf("source:%s:%d", sourceID, id))))
|
|
||||||
err = h.Backend.channelAddItemWithMatcher(channel, *item)
|
|
||||||
err = h.Backend.updateChannelUnreadCount(channel)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("could not update channel unread content %s: %v", channel, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
err = enc.Encode(map[string]string{
|
|
||||||
"ok": "1",
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Error(w, "Method not allowed", 405)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
pkg/websub/signature.go
Normal file
41
pkg/websub/signature.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package websub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateHubSignature validate a sha1 signature that could be send with the
|
||||||
|
// hub as an extra header
|
||||||
|
func ValidateHubSignature(sig string, feedContent, secret []byte) error {
|
||||||
|
parts := strings.Split(sig, "=")
|
||||||
|
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return errors.New("signature format is not like sha1=signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts[0] != "sha1" {
|
||||||
|
return errors.New("signature format is not like sha1=signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
// verification
|
||||||
|
mac := hmac.New(sha1.New, secret)
|
||||||
|
mac.Write(feedContent)
|
||||||
|
signature := mac.Sum(nil)
|
||||||
|
|
||||||
|
signature2, err := hex.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not decode signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hmac.Equal(signature, signature2) {
|
||||||
|
return fmt.Errorf("signature does not match feed %s %s", signature, parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
22
pkg/websub/signature_test.go
Normal file
22
pkg/websub/signature_test.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package websub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateHubSignature(t *testing.T) {
|
||||||
|
secret := []byte("this is a test secret")
|
||||||
|
feedContent := []byte("hello world")
|
||||||
|
|
||||||
|
mac := hmac.New(sha1.New, secret)
|
||||||
|
mac.Write(feedContent)
|
||||||
|
signature := mac.Sum(nil)
|
||||||
|
|
||||||
|
err := ValidateHubSignature(fmt.Sprintf("sha1=%x", signature), feedContent, secret)
|
||||||
|
assert.NoError(t, err, "error should be nil")
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user