hub/internal/hub/delivery/http/hub_http.go

223 lines
5.6 KiB
Go

package http
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"golang.org/x/text/language"
"source.toby3d.me/toby3d/hub/internal/common"
"source.toby3d.me/toby3d/hub/internal/domain"
"source.toby3d.me/toby3d/hub/internal/hub"
"source.toby3d.me/toby3d/hub/internal/subscription"
"source.toby3d.me/toby3d/hub/internal/topic"
"source.toby3d.me/toby3d/hub/web/template"
)
type (
Request struct {
Callback *url.URL
Topic *url.URL
Secret domain.Secret
Mode domain.Mode
LeaseSeconds float64
}
Response struct {
Mode domain.Mode
Reason string
Topic domain.Topic
}
NewHandlerParams struct {
Hub hub.UseCase
Subscriptions subscription.UseCase
Topics topic.UseCase
Matcher language.Matcher
Name string
}
Handler struct {
hub hub.UseCase
subscriptions subscription.UseCase
topics topic.UseCase
matcher language.Matcher
name string
}
)
var DefaultRequestLeaseSeconds = time.Duration(10 * 24 * time.Hour).Seconds() // 10 days
var (
ErrHubMode = errors.New(common.HubMode + " MUST be " + domain.ModeSubscribe.String() + " or " +
domain.ModeUnsubscribe.String())
ErrHubSecret = errors.New(common.HubSecret + " SHOULD be specified when the request was made over HTTPS")
)
func NewHandler(params NewHandlerParams) *Handler {
return &Handler{
hub: params.Hub,
matcher: params.Matcher,
name: params.Name,
subscriptions: params.Subscriptions,
topics: params.Topics,
}
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
now := time.Now().UTC().Round(time.Second)
switch r.Method {
default:
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
case http.MethodPost:
req := NewRequest()
var err error
if err = req.bind(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// TODO(toby3d): send denied ping to callback if it's not accepted by hub
s := new(domain.Subscription)
req.populate(s, now)
switch req.Mode {
case domain.ModeSubscribe, domain.ModeUnsubscribe:
if _, err = h.hub.Verify(r.Context(), *s, req.Mode); err != nil {
r.Clone(context.WithValue(r.Context(), "error", err))
w.WriteHeader(http.StatusAccepted)
return
}
switch req.Mode {
case domain.ModeSubscribe:
_, err = h.subscriptions.Subscribe(r.Context(), *s)
case domain.ModeUnsubscribe:
_, err = h.subscriptions.Unsubscribe(r.Context(), *s)
}
case domain.ModePublish:
_, err = h.topics.Publish(r.Context(), req.Topic)
}
if err != nil {
r.Clone(context.WithValue(r.Context(), "error", err))
}
w.WriteHeader(http.StatusAccepted)
case "", http.MethodGet:
tags, _, _ := language.ParseAcceptLanguage(r.Header.Get(common.HeaderAcceptLanguage))
tag, _, _ := h.matcher.Match(tags...)
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
template.WriteTemplate(w, &template.Home{BaseOf: template.NewBaseOf(tag, h.name)})
}
}
func NewRequest() *Request {
return &Request{
Mode: domain.ModeUnd,
Callback: nil,
Secret: domain.Secret{},
Topic: nil,
LeaseSeconds: DefaultRequestLeaseSeconds,
}
}
func (r *Request) bind(req *http.Request) error {
var err error
if err = req.ParseForm(); err != nil {
return fmt.Errorf("cannot parse request form: %w", err)
}
if !req.PostForm.Has(common.HubMode) {
return fmt.Errorf("%s parameter is required, but not provided", common.HubMode)
}
// NOTE(toby3d): hub.mode
if r.Mode, err = domain.ParseMode(req.PostForm.Get(common.HubMode)); err != nil {
return fmt.Errorf("cannot parse %s: %w", common.HubMode, err)
}
// NOTE(toby3d): hub.topic
if !req.PostForm.Has(common.HubTopic) {
return fmt.Errorf("%s parameter is required, but not provided", common.HubTopic)
}
if r.Topic, err = url.Parse(req.PostForm.Get(common.HubTopic)); err != nil {
return fmt.Errorf("cannot parse %s: %w", common.HubTopic, err)
}
switch r.Mode {
case domain.ModePublish:
case domain.ModeSubscribe, domain.ModeUnsubscribe:
// NOTE(toby3d): hub.callback
if !req.PostForm.Has(common.HubCallback) {
return fmt.Errorf("%s parameter is required, but not provided", common.HubCallback)
}
if r.Callback, err = url.Parse(req.PostForm.Get(common.HubCallback)); err != nil {
return fmt.Errorf("cannot parse %s: %w", common.HubCallback, err)
}
// NOTE(toby3d): hub.lease_seconds
if r.Mode != domain.ModeUnsubscribe && req.PostForm.Has(common.HubLeaseSeconds) {
r.LeaseSeconds, err = strconv.ParseFloat(req.PostForm.Get(common.HubLeaseSeconds), 64)
if err != nil {
return fmt.Errorf("cannot parse %s: %w", common.HubLeaseSeconds, err)
}
}
// NOTE(toby3d): hub.secret
if !req.PostForm.Has(common.HubSecret) {
if req.TLS != nil {
return ErrHubSecret
}
return nil
}
secret, err := domain.ParseSecret(req.PostForm.Get(common.HubSecret))
if err != nil {
return fmt.Errorf("cannot parse %s: %w", common.HubSecret, err)
}
r.Secret = *secret
}
return nil
}
func (r Request) populate(s *domain.Subscription, ts time.Time) {
s.CreatedAt = ts
s.UpdatedAt = ts
s.ExpiredAt = ts.Add(time.Duration(r.LeaseSeconds) * time.Second).Round(time.Second)
s.Callback = r.Callback
s.Topic = r.Topic
s.Secret = r.Secret
}
func NewResponse(t domain.Topic, err error) *Response {
return &Response{
Mode: domain.ModeDenied,
Topic: t,
Reason: err.Error(),
}
}
func (r *Response) populate(q url.Values) {
r.Mode.AddQuery(q)
r.Topic.AddQuery(q)
q.Add(common.HubReason, r.Reason)
}