Compare commits
5 Commits
dbfe957f63
...
6d51145dad
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d51145dad | |||
| dc557efde9 | |||
| 2d39edac8e | |||
| c894e4da13 | |||
| e55232e00b |
|
|
@ -15,7 +15,7 @@ import (
|
|||
|
||||
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)
|
||||
|
||||
if len(tokens) != 2 {
|
||||
|
|
@ -33,7 +33,7 @@ func (b *memoryBackend) cachedCheckAuthToken(conn redis.Conn, header string, r *
|
|||
return true, nil
|
||||
}
|
||||
|
||||
authorized, err = b.checkAuthToken(header, r)
|
||||
authorized, err = checkAuthToken(header, tokenEndpoint, r)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (b *memoryBackend) checkAuthToken(header string, token *auth.TokenResponse) (bool, error) {
|
||||
tokenEndpoint := b.TokenEndpoint
|
||||
func checkAuthToken(header string, tokenEndpoint string, token *auth.TokenResponse) (bool, error) {
|
||||
|
||||
req, err := buildValidateAuthTokenRequest(tokenEndpoint, header)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
|
@ -49,6 +50,22 @@ type Feed struct {
|
|||
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 {
|
||||
conn := h.pool.Get()
|
||||
defer conn.Close()
|
||||
|
|
@ -231,7 +248,12 @@ func (h *hubIncomingBackend) run() error {
|
|||
select {
|
||||
case <-ticker.C:
|
||||
log.Println("Getting feeds for WebSub")
|
||||
feeds := h.GetFeeds()
|
||||
subscribeRuns.Add(1)
|
||||
|
||||
feeds, err := h.Feeds()
|
||||
if err != nil {
|
||||
}
|
||||
|
||||
for _, feed := range feeds {
|
||||
log.Printf("Looking at %s\n", feed.URL)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
log.Printf("Error while subscribing: %s", err)
|
||||
resubscribeErrors.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"p83.nl/go/ekster/pkg/websub"
|
||||
)
|
||||
|
||||
type incomingHandler struct {
|
||||
|
|
@ -85,8 +84,9 @@ func (h *incomingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
// match signature
|
||||
sig := r.Header.Get("X-Hub-Signature")
|
||||
if sig != "" {
|
||||
if err := isHubSignatureValid(sig, feedContent, secret); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Error in signature: %s", err), 400)
|
||||
if err := websub.ValidateHubSignature(sig, feedContent, []byte(secret)); err != nil {
|
||||
log.Printf("could not validate signature: %+v", err)
|
||||
http.Error(w, fmt.Sprintf("could not validate signature: %s", err), 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -94,32 +94,9 @@ func (h *incomingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
ct := r.Header.Get("Content-Type")
|
||||
err = h.Backend.UpdateFeed(feed, ct, bytes.NewBuffer(feedContent))
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
return b.cachedCheckAuthToken(conn, header, r)
|
||||
return cachedCheckAuthToken(conn, header, b.TokenEndpoint, r)
|
||||
}
|
||||
|
||||
func (b *memoryBackend) Debug() {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,86 @@ type micropubHandler struct {
|
|||
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) {
|
||||
var item microsub.Item
|
||||
|
||||
|
|
@ -59,66 +139,29 @@ func parseIncomingItem(r *http.Request) (*microsub.Item, error) {
|
|||
return &item, nil
|
||||
}
|
||||
|
||||
/*
|
||||
* URLs needed:
|
||||
* - / 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")
|
||||
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||
sourceID = authHeader[7:]
|
||||
}
|
||||
|
||||
func getChannelFromAuthorization(r *http.Request, conn redis.Conn) (string, error) {
|
||||
// backward compatible
|
||||
sourceID := r.URL.Query().Get("source_id")
|
||||
if sourceID != "" {
|
||||
channel, err := redis.String(conn.Do("HGET", "sources", sourceID))
|
||||
if err != nil {
|
||||
channel, err = redis.String(conn.Do("HGET", "token:"+sourceID, "channel"))
|
||||
if err != nil {
|
||||
http.Error(w, "unauthorized", 401)
|
||||
return
|
||||
}
|
||||
return "", errors.Wrapf(err, "could not get channel for sourceID: %s", sourceID)
|
||||
}
|
||||
|
||||
item, err := parseIncomingItem(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
item.Read = false
|
||||
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
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
// 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 {
|
||||
return "", errors.Wrap(err, "could not get channel for token")
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not get channel from authorization")
|
||||
}
|
||||
|
|
|
|||
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