add chat handlers and only allow POST methods
This commit is contained in:
parent
3e9a17abb9
commit
9343d2ea69
@ -26,7 +26,7 @@ type Params struct {
|
|||||||
PowManager pow.Manager
|
PowManager pow.Manager
|
||||||
MailingList mailinglist.MailingList
|
MailingList mailinglist.MailingList
|
||||||
GlobalRoom chat.Room
|
GlobalRoom chat.Room
|
||||||
UserIDCalculator chat.UserIDCalculator
|
UserIDCalculator *chat.UserIDCalculator
|
||||||
|
|
||||||
// ListenProto and ListenAddr are passed into net.Listen to create the
|
// ListenProto and ListenAddr are passed into net.Listen to create the
|
||||||
// API's listener. Both "tcp" and "unix" protocols are explicitly
|
// API's listener. Both "tcp" and "unix" protocols are explicitly
|
||||||
@ -165,7 +165,14 @@ func (a *api) handler() http.Handler {
|
|||||||
apiMux.Handle("/mailinglist/finalize", a.mailingListFinalizeHandler())
|
apiMux.Handle("/mailinglist/finalize", a.mailingListFinalizeHandler())
|
||||||
apiMux.Handle("/mailinglist/unsubscribe", a.mailingListUnsubscribeHandler())
|
apiMux.Handle("/mailinglist/unsubscribe", a.mailingListUnsubscribeHandler())
|
||||||
|
|
||||||
|
apiMux.Handle("/chat/global/", http.StripPrefix("/chat/global", newChatHandler(
|
||||||
|
a.params.GlobalRoom,
|
||||||
|
a.params.UserIDCalculator,
|
||||||
|
a.requirePowMiddleware,
|
||||||
|
)))
|
||||||
|
|
||||||
var apiHandler http.Handler = apiMux
|
var apiHandler http.Handler = apiMux
|
||||||
|
apiHandler = allowedMethod("POST", apiHandler)
|
||||||
apiHandler = checkCSRFMiddleware(apiHandler)
|
apiHandler = checkCSRFMiddleware(apiHandler)
|
||||||
apiHandler = logMiddleware(a.params.Logger, apiHandler)
|
apiHandler = logMiddleware(a.params.Logger, apiHandler)
|
||||||
apiHandler = annotateMiddleware(apiHandler)
|
apiHandler = annotateMiddleware(apiHandler)
|
||||||
|
112
srv/api/apiutils/apiutils.go
Normal file
112
srv/api/apiutils/apiutils.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// Package apiutils contains utilities which are useful for implementing api
|
||||||
|
// endpoints.
|
||||||
|
package apiutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loggerCtxKey int
|
||||||
|
|
||||||
|
// SetRequestLogger sets the given Logger onto the given Request's Context,
|
||||||
|
// returning a copy.
|
||||||
|
func SetRequestLogger(r *http.Request, logger *mlog.Logger) *http.Request {
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx = context.WithValue(ctx, loggerCtxKey(0), logger)
|
||||||
|
return r.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRequestLogger returns the Logger which was set by SetRequestLogger onto
|
||||||
|
// this Request, or nil.
|
||||||
|
func GetRequestLogger(r *http.Request) *mlog.Logger {
|
||||||
|
ctx := r.Context()
|
||||||
|
logger, _ := ctx.Value(loggerCtxKey(0)).(*mlog.Logger)
|
||||||
|
if logger == nil {
|
||||||
|
logger = mlog.Null
|
||||||
|
}
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONResult writes the JSON encoding of the given value as the response body.
|
||||||
|
func JSONResult(rw http.ResponseWriter, r *http.Request, v interface{}) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
InternalServerError(rw, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b = append(b, '\n')
|
||||||
|
|
||||||
|
rw.Header().Set("Content-Type", "application/json")
|
||||||
|
rw.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BadRequest writes a 400 status and a JSON encoded error struct containing the
|
||||||
|
// given error as the response body.
|
||||||
|
func BadRequest(rw http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
GetRequestLogger(r).Warn(r.Context(), "bad request", err)
|
||||||
|
|
||||||
|
rw.WriteHeader(400)
|
||||||
|
JSONResult(rw, r, struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalServerError writes a 500 status and a JSON encoded error struct
|
||||||
|
// containing a generic error as the response body (though it will log the given
|
||||||
|
// one).
|
||||||
|
func InternalServerError(rw http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
GetRequestLogger(r).Error(r.Context(), "internal server error", err)
|
||||||
|
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
JSONResult(rw, r, struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}{
|
||||||
|
Error: "internal server error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrToInt parses the given string as an integer, or returns the given default
|
||||||
|
// integer if the string is empty.
|
||||||
|
func StrToInt(str string, defaultVal int) (int, error) {
|
||||||
|
if str == "" {
|
||||||
|
return defaultVal, nil
|
||||||
|
}
|
||||||
|
return strconv.Atoi(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCookie returns the namd cookie's value, or the given default value if the
|
||||||
|
// cookie is not set.
|
||||||
|
//
|
||||||
|
// This will only return an error if there was an unexpected error parsing the
|
||||||
|
// Request's cookies.
|
||||||
|
func GetCookie(r *http.Request, cookieName, defaultVal string) (string, error) {
|
||||||
|
c, err := r.Cookie(cookieName)
|
||||||
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
|
return defaultVal, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return "", fmt.Errorf("reading cookie %q: %w", cookieName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandStr returns a human-readable random string with the given number of bytes
|
||||||
|
// of randomness.
|
||||||
|
func RandStr(numBytes int) string {
|
||||||
|
b := make([]byte, numBytes)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
@ -7,32 +7,57 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *api) chatHistoryHandler() http.Handler {
|
type chatHandler struct {
|
||||||
|
*http.ServeMux
|
||||||
|
|
||||||
|
room chat.Room
|
||||||
|
userIDCalc *chat.UserIDCalculator
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChatHandler(
|
||||||
|
room chat.Room, userIDCalc *chat.UserIDCalculator,
|
||||||
|
requirePowMiddleware func(http.Handler) http.Handler,
|
||||||
|
) http.Handler {
|
||||||
|
c := &chatHandler{
|
||||||
|
ServeMux: http.NewServeMux(),
|
||||||
|
room: room,
|
||||||
|
userIDCalc: userIDCalc,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Handle("/history", c.historyHandler())
|
||||||
|
c.Handle("/user-id", requirePowMiddleware(c.userIDHandler()))
|
||||||
|
c.Handle("/append", requirePowMiddleware(c.appendHandler()))
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chatHandler) historyHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
limit, err := strToInt(r.FormValue("limit"), 0)
|
limit, err := apiutils.StrToInt(r.PostFormValue("limit"), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badRequest(rw, r, fmt.Errorf("invalid limit parameter: %w", err))
|
apiutils.BadRequest(rw, r, fmt.Errorf("invalid limit parameter: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor := r.FormValue("cursor")
|
cursor := r.PostFormValue("cursor")
|
||||||
|
|
||||||
cursor, msgs, err := a.params.GlobalRoom.History(r.Context(), chat.HistoryOpts{
|
cursor, msgs, err := c.room.History(r.Context(), chat.HistoryOpts{
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
Cursor: cursor,
|
Cursor: cursor,
|
||||||
})
|
})
|
||||||
|
|
||||||
if argErr := (chat.ErrInvalidArg{}); errors.As(err, &argErr) {
|
if argErr := (chat.ErrInvalidArg{}); errors.As(err, &argErr) {
|
||||||
badRequest(rw, r, argErr.Err)
|
apiutils.BadRequest(rw, r, argErr.Err)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
internalServerError(rw, r, err)
|
apiutils.InternalServerError(rw, r, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResult(rw, r, struct {
|
apiutils.JSONResult(rw, r, struct {
|
||||||
Cursor string `json:"cursor"`
|
Cursor string `json:"cursor"`
|
||||||
Messages []chat.Message `json:"messages"`
|
Messages []chat.Message `json:"messages"`
|
||||||
}{
|
}{
|
||||||
@ -42,7 +67,7 @@ func (a *api) chatHistoryHandler() http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *api) getUserID(r *http.Request) (chat.UserID, error) {
|
func (c *chatHandler) userID(r *http.Request) (chat.UserID, error) {
|
||||||
name := r.PostFormValue("name")
|
name := r.PostFormValue("name")
|
||||||
if l := len(name); l == 0 {
|
if l := len(name); l == 0 {
|
||||||
return chat.UserID{}, errors.New("name is required")
|
return chat.UserID{}, errors.New("name is required")
|
||||||
@ -68,21 +93,58 @@ func (a *api) getUserID(r *http.Request) (chat.UserID, error) {
|
|||||||
return chat.UserID{}, errors.New("password too long")
|
return chat.UserID{}, errors.New("password too long")
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.params.UserIDCalculator.Calculate(name, password), nil
|
return c.userIDCalc.Calculate(name, password), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *api) getUserIDHandler() http.Handler {
|
func (c *chatHandler) userIDHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
userID, err := a.getUserID(r)
|
userID, err := c.userID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badRequest(rw, r, err)
|
apiutils.BadRequest(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResult(rw, r, struct {
|
apiutils.JSONResult(rw, r, struct {
|
||||||
UserID chat.UserID `json:"userID"`
|
UserID chat.UserID `json:"userID"`
|
||||||
}{
|
}{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *chatHandler) appendHandler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, err := c.userID(r)
|
||||||
|
if err != nil {
|
||||||
|
apiutils.BadRequest(rw, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := r.PostFormValue("body")
|
||||||
|
|
||||||
|
if l := len(body); l == 0 {
|
||||||
|
apiutils.BadRequest(rw, r, errors.New("body is required"))
|
||||||
|
return
|
||||||
|
|
||||||
|
} else if l > 300 {
|
||||||
|
apiutils.BadRequest(rw, r, errors.New("body too long"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := c.room.Append(r.Context(), chat.Message{
|
||||||
|
UserID: userID,
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
apiutils.InternalServerError(rw, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiutils.JSONResult(rw, r, struct {
|
||||||
|
MessageID string `json:"messageID"`
|
||||||
|
}{
|
||||||
|
MessageID: msg.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -13,16 +15,16 @@ const (
|
|||||||
func setCSRFMiddleware(h http.Handler) http.Handler {
|
func setCSRFMiddleware(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
csrfTok, err := getCookie(r, csrfTokenCookieName, "")
|
csrfTok, err := apiutils.GetCookie(r, csrfTokenCookieName, "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalServerError(rw, r, err)
|
apiutils.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if csrfTok == "" {
|
} else if csrfTok == "" {
|
||||||
http.SetCookie(rw, &http.Cookie{
|
http.SetCookie(rw, &http.Cookie{
|
||||||
Name: csrfTokenCookieName,
|
Name: csrfTokenCookieName,
|
||||||
Value: randStr(32),
|
Value: apiutils.RandStr(32),
|
||||||
Secure: true,
|
Secure: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -34,14 +36,14 @@ func setCSRFMiddleware(h http.Handler) http.Handler {
|
|||||||
func checkCSRFMiddleware(h http.Handler) http.Handler {
|
func checkCSRFMiddleware(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
csrfTok, err := getCookie(r, csrfTokenCookieName, "")
|
csrfTok, err := apiutils.GetCookie(r, csrfTokenCookieName, "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalServerError(rw, r, err)
|
apiutils.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if csrfTok == "" || r.Header.Get(csrfTokenHeaderName) != csrfTok {
|
} else if csrfTok == "" || r.Header.Get(csrfTokenHeaderName) != csrfTok {
|
||||||
badRequest(rw, r, errors.New("invalid CSRF token"))
|
apiutils.BadRequest(rw, r, errors.New("invalid CSRF token"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ func (a *api) mailingListSubscribeHandler() http.Handler {
|
|||||||
parts[0] == "" ||
|
parts[0] == "" ||
|
||||||
parts[1] == "" ||
|
parts[1] == "" ||
|
||||||
len(email) >= 512 {
|
len(email) >= 512 {
|
||||||
badRequest(rw, r, errors.New("invalid email"))
|
apiutils.BadRequest(rw, r, errors.New("invalid email"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,11 +26,11 @@ func (a *api) mailingListSubscribeHandler() http.Handler {
|
|||||||
// just eat the error, make it look to the user like the
|
// just eat the error, make it look to the user like the
|
||||||
// verification email was sent.
|
// verification email was sent.
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
internalServerError(rw, r, err)
|
apiutils.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResult(rw, r, struct{}{})
|
apiutils.JSONResult(rw, r, struct{}{})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,25 +40,25 @@ func (a *api) mailingListFinalizeHandler() http.Handler {
|
|||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
subToken := r.PostFormValue("subToken")
|
subToken := r.PostFormValue("subToken")
|
||||||
if l := len(subToken); l == 0 || l > 128 {
|
if l := len(subToken); l == 0 || l > 128 {
|
||||||
badRequest(rw, r, errInvalidSubToken)
|
apiutils.BadRequest(rw, r, errInvalidSubToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.params.MailingList.FinalizeSubscription(subToken)
|
err := a.params.MailingList.FinalizeSubscription(subToken)
|
||||||
|
|
||||||
if errors.Is(err, mailinglist.ErrNotFound) {
|
if errors.Is(err, mailinglist.ErrNotFound) {
|
||||||
badRequest(rw, r, errInvalidSubToken)
|
apiutils.BadRequest(rw, r, errInvalidSubToken)
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if errors.Is(err, mailinglist.ErrAlreadyVerified) {
|
} else if errors.Is(err, mailinglist.ErrAlreadyVerified) {
|
||||||
// no problem
|
// no problem
|
||||||
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
internalServerError(rw, r, err)
|
apiutils.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResult(rw, r, struct{}{})
|
apiutils.JSONResult(rw, r, struct{}{})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,21 +68,21 @@ func (a *api) mailingListUnsubscribeHandler() http.Handler {
|
|||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
unsubToken := r.PostFormValue("unsubToken")
|
unsubToken := r.PostFormValue("unsubToken")
|
||||||
if l := len(unsubToken); l == 0 || l > 128 {
|
if l := len(unsubToken); l == 0 || l > 128 {
|
||||||
badRequest(rw, r, errInvalidUnsubToken)
|
apiutils.BadRequest(rw, r, errInvalidUnsubToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.params.MailingList.Unsubscribe(unsubToken)
|
err := a.params.MailingList.Unsubscribe(unsubToken)
|
||||||
|
|
||||||
if errors.Is(err, mailinglist.ErrNotFound) {
|
if errors.Is(err, mailinglist.ErrNotFound) {
|
||||||
badRequest(rw, r, errInvalidUnsubToken)
|
apiutils.BadRequest(rw, r, errInvalidUnsubToken)
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
internalServerError(rw, r, err)
|
apiutils.InternalServerError(rw, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResult(rw, r, struct{}{})
|
apiutils.JSONResult(rw, r, struct{}{})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
||||||
)
|
)
|
||||||
@ -57,7 +58,7 @@ func (lrw *logResponseWriter) WriteHeader(statusCode int) {
|
|||||||
func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler {
|
func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
r = setRequestLogger(r, logger)
|
r = apiutils.SetRequestLogger(r, logger)
|
||||||
|
|
||||||
lrw := newLogResponseWriter(rw)
|
lrw := newLogResponseWriter(rw)
|
||||||
|
|
||||||
@ -76,3 +77,15 @@ func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler {
|
|||||||
logger.Info(ctx, "handled HTTP request")
|
logger.Info(ctx, "handled HTTP request")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allowedMethod(method string, h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == method {
|
||||||
|
h.ServeHTTP(rw, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiutils.GetRequestLogger(r).WarnString(r.Context(), "method not allowed")
|
||||||
|
rw.WriteHeader(405)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *api) newPowChallengeHandler() http.Handler {
|
func (a *api) newPowChallengeHandler() http.Handler {
|
||||||
@ -12,7 +14,7 @@ func (a *api) newPowChallengeHandler() http.Handler {
|
|||||||
|
|
||||||
challenge := a.params.PowManager.NewChallenge()
|
challenge := a.params.PowManager.NewChallenge()
|
||||||
|
|
||||||
jsonResult(rw, r, struct {
|
apiutils.JSONResult(rw, r, struct {
|
||||||
Seed string `json:"seed"`
|
Seed string `json:"seed"`
|
||||||
Target uint32 `json:"target"`
|
Target uint32 `json:"target"`
|
||||||
}{
|
}{
|
||||||
@ -28,21 +30,21 @@ func (a *api) requirePowMiddleware(h http.Handler) http.Handler {
|
|||||||
seedHex := r.PostFormValue("powSeed")
|
seedHex := r.PostFormValue("powSeed")
|
||||||
seed, err := hex.DecodeString(seedHex)
|
seed, err := hex.DecodeString(seedHex)
|
||||||
if err != nil || len(seed) == 0 {
|
if err != nil || len(seed) == 0 {
|
||||||
badRequest(rw, r, errors.New("invalid powSeed"))
|
apiutils.BadRequest(rw, r, errors.New("invalid powSeed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
solutionHex := r.PostFormValue("powSolution")
|
solutionHex := r.PostFormValue("powSolution")
|
||||||
solution, err := hex.DecodeString(solutionHex)
|
solution, err := hex.DecodeString(solutionHex)
|
||||||
if err != nil || len(seed) == 0 {
|
if err != nil || len(seed) == 0 {
|
||||||
badRequest(rw, r, errors.New("invalid powSolution"))
|
apiutils.BadRequest(rw, r, errors.New("invalid powSolution"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.params.PowManager.CheckSolution(seed, solution)
|
err = a.params.PowManager.CheckSolution(seed, solution)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err))
|
apiutils.BadRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type loggerCtxKey int
|
|
||||||
|
|
||||||
func setRequestLogger(r *http.Request, logger *mlog.Logger) *http.Request {
|
|
||||||
ctx := r.Context()
|
|
||||||
ctx = context.WithValue(ctx, loggerCtxKey(0), logger)
|
|
||||||
return r.WithContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRequestLogger(r *http.Request) *mlog.Logger {
|
|
||||||
ctx := r.Context()
|
|
||||||
logger, _ := ctx.Value(loggerCtxKey(0)).(*mlog.Logger)
|
|
||||||
if logger == nil {
|
|
||||||
logger = mlog.Null
|
|
||||||
}
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonResult(rw http.ResponseWriter, r *http.Request, v interface{}) {
|
|
||||||
b, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
internalServerError(rw, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b = append(b, '\n')
|
|
||||||
|
|
||||||
rw.Header().Set("Content-Type", "application/json")
|
|
||||||
rw.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func badRequest(rw http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
getRequestLogger(r).Warn(r.Context(), "bad request", err)
|
|
||||||
|
|
||||||
rw.WriteHeader(400)
|
|
||||||
jsonResult(rw, r, struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}{
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func internalServerError(rw http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
getRequestLogger(r).Error(r.Context(), "internal server error", err)
|
|
||||||
|
|
||||||
rw.WriteHeader(500)
|
|
||||||
jsonResult(rw, r, struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}{
|
|
||||||
Error: "internal server error",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func strToInt(str string, defaultVal int) (int, error) {
|
|
||||||
if str == "" {
|
|
||||||
return defaultVal, nil
|
|
||||||
}
|
|
||||||
return strconv.Atoi(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCookie(r *http.Request, cookieName, defaultVal string) (string, error) {
|
|
||||||
c, err := r.Cookie(cookieName)
|
|
||||||
if errors.Is(err, http.ErrNoCookie) {
|
|
||||||
return defaultVal, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return "", fmt.Errorf("reading cookie %q: %w", cookieName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func randStr(numBytesEntropy int) string {
|
|
||||||
b := make([]byte, numBytesEntropy)
|
|
||||||
if _, err := rand.Read(b); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(b)
|
|
||||||
}
|
|
@ -84,7 +84,7 @@ type HistoryOpts struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o HistoryOpts) sanitize() (HistoryOpts, error) {
|
func (o HistoryOpts) sanitize() (HistoryOpts, error) {
|
||||||
if o.Limit < 0 || o.Limit > 100 {
|
if o.Limit <= 0 || o.Limit > 100 {
|
||||||
o.Limit = 100
|
o.Limit = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,8 +38,8 @@ type UserIDCalculator struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewUserIDCalculator returns a UserIDCalculator with sane defaults.
|
// NewUserIDCalculator returns a UserIDCalculator with sane defaults.
|
||||||
func NewUserIDCalculator(secret []byte) UserIDCalculator {
|
func NewUserIDCalculator(secret []byte) *UserIDCalculator {
|
||||||
return UserIDCalculator{
|
return &UserIDCalculator{
|
||||||
Secret: secret,
|
Secret: secret,
|
||||||
TimeCost: 15,
|
TimeCost: 15,
|
||||||
MemoryCost: 128 * 1024,
|
MemoryCost: 128 * 1024,
|
||||||
@ -50,7 +50,7 @@ func NewUserIDCalculator(secret []byte) UserIDCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate accepts a name and password and returns the calculated UserID.
|
// Calculate accepts a name and password and returns the calculated UserID.
|
||||||
func (c UserIDCalculator) Calculate(name, password string) UserID {
|
func (c *UserIDCalculator) Calculate(name, password string) UserID {
|
||||||
|
|
||||||
input := fmt.Sprintf("%q:%q", name, password)
|
input := fmt.Sprintf("%q:%q", name, password)
|
||||||
|
|
||||||
|
@ -11,10 +11,12 @@ import (
|
|||||||
|
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/api"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/api"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
|
||||||
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/pow"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/pow"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
||||||
|
"github.com/mediocregopher/radix/v4"
|
||||||
"github.com/tilinna/clock"
|
"github.com/tilinna/clock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -45,6 +47,13 @@ func main() {
|
|||||||
apiParams.SetupCfg(cfg)
|
apiParams.SetupCfg(cfg)
|
||||||
ctx = mctx.WithAnnotator(ctx, &apiParams)
|
ctx = mctx.WithAnnotator(ctx, &apiParams)
|
||||||
|
|
||||||
|
redisProto := cfg.String("redis-proto", "tcp", "Network protocol to connect to redis over, can be tcp or unix")
|
||||||
|
redisAddr := cfg.String("redis-addr", "127.0.0.1:6379", "Address redis is expected to listen on")
|
||||||
|
redisPoolSize := cfg.Int("redis-pool-size", 5, "Number of connections in the redis pool to keep")
|
||||||
|
|
||||||
|
chatGlobalRoomMaxMsgs := cfg.Int("chat-global-room-max-messages", 1000, "Maximum number of messages the global chat room can retain")
|
||||||
|
chatUserIDCalcSecret := cfg.String("chat-user-id-calc-secret", "", "Secret to use when calculating user ids")
|
||||||
|
|
||||||
// initialization
|
// initialization
|
||||||
err := cfg.Init(ctx)
|
err := cfg.Init(ctx)
|
||||||
|
|
||||||
@ -60,6 +69,10 @@ func main() {
|
|||||||
|
|
||||||
ctx = mctx.Annotate(ctx,
|
ctx = mctx.Annotate(ctx,
|
||||||
"dataDir", *dataDir,
|
"dataDir", *dataDir,
|
||||||
|
"redisProto", *redisProto,
|
||||||
|
"redisAddr", *redisAddr,
|
||||||
|
"redisPoolSize", *redisPoolSize,
|
||||||
|
"chatGlobalRoomMaxMsgs", *chatGlobalRoomMaxMsgs,
|
||||||
)
|
)
|
||||||
|
|
||||||
clock := clock.Realtime()
|
clock := clock.Realtime()
|
||||||
@ -92,9 +105,35 @@ func main() {
|
|||||||
|
|
||||||
ml := mailinglist.New(mlParams)
|
ml := mailinglist.New(mlParams)
|
||||||
|
|
||||||
|
redis, err := (radix.PoolConfig{
|
||||||
|
Size: *redisPoolSize,
|
||||||
|
}).New(
|
||||||
|
ctx, *redisProto, *redisAddr,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
loggerFatalErr(ctx, logger, "initializing redis pool", err)
|
||||||
|
}
|
||||||
|
defer redis.Close()
|
||||||
|
|
||||||
|
chatGlobalRoom, err := chat.NewRoom(ctx, chat.RoomParams{
|
||||||
|
Logger: logger.WithNamespace("global-chat-room"),
|
||||||
|
Redis: redis,
|
||||||
|
ID: "global",
|
||||||
|
MaxMessages: *chatGlobalRoomMaxMsgs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
loggerFatalErr(ctx, logger, "initializing global chat room", err)
|
||||||
|
}
|
||||||
|
defer chatGlobalRoom.Close()
|
||||||
|
|
||||||
|
chatUserIDCalc := chat.NewUserIDCalculator([]byte(*chatUserIDCalcSecret))
|
||||||
|
|
||||||
apiParams.Logger = logger.WithNamespace("api")
|
apiParams.Logger = logger.WithNamespace("api")
|
||||||
apiParams.PowManager = powMgr
|
apiParams.PowManager = powMgr
|
||||||
apiParams.MailingList = ml
|
apiParams.MailingList = ml
|
||||||
|
apiParams.GlobalRoom = chatGlobalRoom
|
||||||
|
apiParams.UserIDCalculator = chatUserIDCalc
|
||||||
|
|
||||||
logger.Info(ctx, "listening")
|
logger.Info(ctx, "listening")
|
||||||
a, err := api.New(apiParams)
|
a, err := api.New(apiParams)
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
"-pow-secret=${config.powSecret}"
|
"-pow-secret=${config.powSecret}"
|
||||||
"-listen-proto=${config.listenProto}"
|
"-listen-proto=${config.listenProto}"
|
||||||
"-listen-addr=${config.listenAddr}"
|
"-listen-addr=${config.listenAddr}"
|
||||||
|
"-redis-proto=unix"
|
||||||
|
"-redis-addr=${config.runDir}/redis"
|
||||||
] ++ (
|
] ++ (
|
||||||
if config.staticProxyURL == ""
|
if config.staticProxyURL == ""
|
||||||
then [ "-static-dir=${staticBuild}" ]
|
then [ "-static-dir=${staticBuild}" ]
|
||||||
|
@ -31,7 +31,7 @@ const doFetch = async (req) => {
|
|||||||
// may throw
|
// may throw
|
||||||
const solvePow = async () => {
|
const solvePow = async () => {
|
||||||
|
|
||||||
const res = await call('GET', '/api/pow/challenge');
|
const res = await call('/api/pow/challenge');
|
||||||
|
|
||||||
const worker = new Worker('/assets/solvePow.js');
|
const worker = new Worker('/assets/solvePow.js');
|
||||||
|
|
||||||
@ -46,8 +46,12 @@ const solvePow = async () => {
|
|||||||
return {seed: res.seed, solution: powSol};
|
return {seed: res.seed, solution: powSol};
|
||||||
}
|
}
|
||||||
|
|
||||||
const call = async (method, route, opts = {}) => {
|
const call = async (route, opts = {}) => {
|
||||||
const { body = {}, requiresPow = false } = opts;
|
const {
|
||||||
|
method = 'POST',
|
||||||
|
body = {},
|
||||||
|
requiresPow = false,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
if (!utils.cookies["csrf_token"])
|
if (!utils.cookies["csrf_token"])
|
||||||
throw "csrf_token cookie not set, can't make api call";
|
throw "csrf_token cookie not set, can't make api call";
|
||||||
|
@ -63,7 +63,7 @@ emailSubscribe.onclick = async () => {
|
|||||||
throw "The browser environment is not secure.";
|
throw "The browser environment is not secure.";
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.call('POST', '/api/mailinglist/subscribe', {
|
await api.call('/api/mailinglist/subscribe', {
|
||||||
body: { email: emailAddress.value },
|
body: { email: emailAddress.value },
|
||||||
requiresPow: true,
|
requiresPow: true,
|
||||||
});
|
});
|
||||||
|
@ -28,7 +28,7 @@ nofollow: true
|
|||||||
|
|
||||||
const api = await import("/assets/api.js");
|
const api = await import("/assets/api.js");
|
||||||
|
|
||||||
await api.call('POST', '/api/mailinglist/finalize', {
|
await api.call('/api/mailinglist/finalize', {
|
||||||
body: { subToken },
|
body: { subToken },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ nofollow: true
|
|||||||
|
|
||||||
const api = await import("/assets/api.js");
|
const api = await import("/assets/api.js");
|
||||||
|
|
||||||
await api.call('POST', '/api/mailinglist/unsubscribe', {
|
await api.call('/api/mailinglist/unsubscribe', {
|
||||||
body: { unsubToken },
|
body: { unsubToken },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user