websub-hub/cmd/hubserver/main.go

228 lines
5.0 KiB
Go

package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"os"
"strconv"
"strings"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func randStringBytes(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
type Subscriber struct {
Callback string
LeaseSeconds int64
Secret string
}
type subscriptionHandler struct {
Subscribers map[string][]Subscriber
}
func (handler *subscriptionHandler) handlePublish(w http.ResponseWriter, r *http.Request) error {
topic := r.Form.Get("hub.topic")
log.Printf("Topic = %s\n", topic)
client := &http.Client{}
res, err := client.Get(topic)
if err != nil {
return err
}
defer res.Body.Close()
feedContent, err := ioutil.ReadAll(res.Body)
if subs, e := handler.Subscribers[topic]; e {
for _, sub := range subs {
req, err := http.NewRequest("POST", sub.Callback, strings.NewReader(string(feedContent)))
if err != nil {
log.Printf("While creating request to %s: %s", sub.Callback, err)
continue
}
req.Header.Add("Content-Type", res.Header.Get("Content-Type"))
req.Header.Add("Link",
fmt.Sprintf(
"<%s>; rel=hub, <%s>; rel=self",
"https://hub.stuifzandapp.com/",
topic,
))
if sub.Secret != "" {
mac := hmac.New(sha1.New, []byte(sub.Secret))
mac.Write(feedContent)
signature := mac.Sum(nil)
req.Header.Add("X-Hub-Signature", fmt.Sprintf("sha1=%x", signature))
}
res, err = client.Do(req)
if err != nil {
log.Printf("While POSTing to %s: %s", sub.Callback, err)
continue
}
}
}
return nil
}
func (handler *subscriptionHandler) handleSubscription(w http.ResponseWriter, r *http.Request) error {
log.Println(r.Form.Encode())
callback := r.Form.Get("hub.callback")
topic := r.Form.Get("hub.topic")
leaseSecondsStr := r.Form.Get("hub.lease_seconds")
leaseSeconds, err := strconv.ParseInt(leaseSecondsStr, 10, 64)
if leaseSecondsStr != "" && err != nil {
http.Error(w, "hub.lease_seconds is used, but not valid", 400)
return nil
}
secret := r.Form.Get("hub.secret")
callbackURL, err := url.Parse(callback)
if err != nil {
return err
}
topicURL, err := url.Parse(topic)
if err != nil {
return err
}
w.WriteHeader(202)
fmt.Fprint(w, "Accepted")
go func() {
ourChallenge := randStringBytes(12)
validationURL := *callbackURL
q := validationURL.Query()
q.Add("hub.mode", "subscribe")
q.Add("hub.topic", topicURL.String())
q.Add("hub.challenge", ourChallenge)
q.Add("hub.lease_seconds", leaseSecondsStr)
validationURL.RawQuery = q.Encode()
log.Println(validationURL)
if validateURL(validationURL.String(), ourChallenge) {
handler.addSubscriberCallback(topicURL.String(), Subscriber{callbackURL.String(), leaseSeconds, secret})
}
}()
return nil
}
func validateURL(url, challenge string) bool {
client := http.Client{}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
log.Println(err)
return false
}
res, err := client.Do(req)
if err != nil {
log.Println(err)
return false
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Println(err)
return false
}
return strings.Contains(string(body), challenge)
}
func (handler *subscriptionHandler) addSubscriberCallback(topic string, subscriber Subscriber) {
defer handler.save()
if subs, e := handler.Subscribers[topic]; e {
for i, sub := range subs {
if sub.Callback == subscriber.Callback {
handler.Subscribers[topic][i] = subscriber
return
}
}
}
// not found create a new subscription
handler.Subscribers[topic] = append(handler.Subscribers[topic], subscriber)
}
func (handler *subscriptionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
http.Error(w, "Bad Request", 400)
return
}
err := r.ParseForm()
if err != nil {
http.Error(w, "Bad Request", 400)
return
}
mode := r.Form.Get("hub.mode")
if mode == "subscribe" {
handler.handleSubscription(w, r)
return
} else if mode == "unsubcribe" {
return
} else if mode == "publish" {
handler.handlePublish(w, r)
} else {
http.Error(w, "Unknown hub.mode", 400)
return
}
}
func (handler *subscriptionHandler) load() error {
file, err := os.Open("./subscription.json")
if err != nil {
if os.IsExist(err) {
return err
} else {
handler.Subscribers = make(map[string][]Subscriber)
return nil
}
}
defer file.Close()
dec := json.NewDecoder(file)
dec.Decode(handler.Subscribers)
return nil
}
func (handler *subscriptionHandler) save() error {
file, err := os.Create("./subscription.json")
if err != nil {
return err
}
defer file.Close()
dec := json.NewEncoder(file)
dec.SetIndent("", " ")
dec.Encode(handler.Subscribers)
return nil
}
func main() {
handler := &subscriptionHandler{}
handler.load()
http.Handle("/", handler)
log.Fatal(http.ListenAndServe(":80", nil))
}