Skip to content

Commit

Permalink
Move captcha into subpackage.
Browse files Browse the repository at this point in the history
  • Loading branch information
robertabcd committed Sep 18, 2020
1 parent 4e99ade commit 1ed585a
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 56 deletions.
22 changes: 22 additions & 0 deletions captcha/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package captcha

type Config struct {
Enabled bool
InsertSecret string
ExpireSecs int
Recaptcha RecaptchaConfig
Redis RedisConfig
}

type RecaptchaConfig struct {
SiteKey string
Secret string
}

// See https://godoc.org/github.com/go-redis/redis#Options
type RedisConfig struct {
Network string
Addr string
Password string
DB int
}
80 changes: 51 additions & 29 deletions captcha.go → captcha/handler.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package captcha

import (
"encoding/json"
Expand All @@ -9,6 +9,7 @@ import (
"time"

"github.com/go-redis/redis"
"github.com/gorilla/mux"
"github.com/ptt/pttweb/page"
"github.com/rvelhote/go-recaptcha"
)
Expand All @@ -32,50 +33,70 @@ var (
}
)

var captchaRedis *redis.Client

type CaptchaErr struct {
error
SetCaptchaPage func(p *page.Captcha)
}

func initCaptchaRedisServer(c *CaptchaRedisConfig) error {
captchaRedis = redis.NewClient(&redis.Options{
Network: c.Network,
Addr: c.Addr,
Password: c.Password,
DB: c.DB,
type Handler struct {
config *Config
router *mux.Router
redisClient *redis.Client
}

func Install(cfg *Config, r *mux.Router) error {
redisClient := redis.NewClient(&redis.Options{
Network: cfg.Redis.Network,
Addr: cfg.Redis.Addr,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
})
h := &Handler{
config: cfg,
router: r,
redisClient: redisClient,
}
h.installRoutes(r)
return nil
}

func handleCaptcha(c *Context, w http.ResponseWriter) error {
p, err := handleCaptchaInternal(c, w)
func (h *Handler) installRoutes(r *mux.Router) {
r.Path(`/captcha`).
Handler(page.ErrorWrapper(h.handleCaptcha)).
Name("captcha")

r.Path(`/captcha/insert`).
Handler(page.ErrorWrapper(h.handleCaptchaInsert)).
Name("captcha_insert")
}

func (h *Handler) handleCaptcha(ctx page.Context, w http.ResponseWriter) error {
p, err := h.handleCaptchaInternal(ctx, w)
if err != nil {
return err
}
return page.ExecutePage(w, p)
}

func handleCaptchaInternal(c *Context, w http.ResponseWriter) (*page.Captcha, error) {
func (h *Handler) handleCaptchaInternal(ctx page.Context, w http.ResponseWriter) (*page.Captcha, error) {
p := &page.Captcha{
Handle: c.R.FormValue(CaptchaHandle),
RecaptchaSiteKey: config.RecaptchaSiteKey,
Handle: ctx.Request().FormValue(CaptchaHandle),
RecaptchaSiteKey: h.config.Recaptcha.SiteKey,
}
if u, err := router.Get("captcha").URLPath(); err != nil {
if u, err := h.router.Get("captcha").URLPath(); err != nil {
return nil, err
} else {
q := make(url.Values)
q.Set(CaptchaHandle, c.R.FormValue(CaptchaHandle))
q.Set(CaptchaHandle, ctx.Request().FormValue(CaptchaHandle))
p.PostAction = u.String() + "?" + q.Encode()
}
// Check if the handle is valid.
if _, err := fetchVerificationKey(p.Handle); err != nil {
if _, err := h.fetchVerificationKey(p.Handle); err != nil {
return translateCaptchaErr(p, err)
}
if response := c.R.PostFormValue(GRecaptchaResponse); response != "" {
if response := ctx.Request().PostFormValue(GRecaptchaResponse); response != "" {
r := recaptcha.Recaptcha{
PrivateKey: config.RecaptchaSecret,
PrivateKey: h.config.Recaptcha.Secret,
URL: RecaptchaURL,
}
verifyResp, errs := r.Verify(response, "")
Expand All @@ -84,7 +105,7 @@ func handleCaptchaInternal(c *Context, w http.ResponseWriter) (*page.Captcha, er
return translateCaptchaErr(p, ErrCaptchaVerifyFailed)
} else if verifyResp.Success {
var err error
p.VerificationKey, err = fetchVerificationKey(p.Handle)
p.VerificationKey, err = h.fetchVerificationKey(p.Handle)
if err != nil {
return translateCaptchaErr(p, err)
}
Expand All @@ -104,11 +125,11 @@ func translateCaptchaErr(p *page.Captcha, err error) (*page.Captcha, error) {
return p, err
}

func fetchVerificationKey(handle string) (string, error) {
func (h *Handler) fetchVerificationKey(handle string) (string, error) {
if len(handle) > MaxCaptchaHandleLength {
return "", ErrCaptchaHandleNotFound
}
data, err := captchaRedis.Get(handle).Result()
data, err := h.redisClient.Get(handle).Result()
if err == redis.Nil {
return "", ErrCaptchaHandleNotFound
} else if err != nil {
Expand All @@ -121,15 +142,16 @@ func fetchVerificationKey(handle string) (string, error) {
return e.Verify, nil
}

func handleCaptchaInsert(c *Context, w http.ResponseWriter) error {
secret := c.R.FormValue("secret")
handle := c.R.FormValue("handle")
verify := c.R.FormValue("verify")
func (h *Handler) handleCaptchaInsert(ctx page.Context, w http.ResponseWriter) error {
req := ctx.Request()
secret := req.FormValue("secret")
handle := req.FormValue("handle")
verify := req.FormValue("verify")
if secret == "" || handle == "" || verify == "" || len(handle) > MaxCaptchaHandleLength {
w.WriteHeader(http.StatusBadRequest)
return nil
}
if secret != config.CaptchaInsertSecret {
if secret != h.config.InsertSecret {
w.WriteHeader(http.StatusForbidden)
return nil
}
Expand All @@ -140,8 +162,8 @@ func handleCaptchaInsert(c *Context, w http.ResponseWriter) error {
if err != nil {
return err
}
expire := time.Duration(config.CaptchaExpireSecs) * time.Second
r, err := captchaRedis.SetNX(handle, data, expire).Result()
expire := time.Duration(h.config.ExpireSecs) * time.Second
r, err := h.redisClient.SetNX(handle, data, expire).Result()
if err != nil {
return err
}
Expand Down
30 changes: 18 additions & 12 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package main

import "errors"
import (
"errors"

"github.com/ptt/pttweb/captcha"
)

type PttwebConfig struct {
Bind []string
Expand Down Expand Up @@ -30,15 +34,7 @@ type PttwebConfig struct {
RecaptchaSecret string
CaptchaInsertSecret string
CaptchaExpireSecs int
CaptchaRedisConfig *CaptchaRedisConfig
}

// See https://godoc.org/github.com/go-redis/redis#Options
type CaptchaRedisConfig struct {
Network string
Addr string
Password string
DB int
CaptchaRedisConfig *captcha.RedisConfig
}

const (
Expand All @@ -62,6 +58,16 @@ func (c *PttwebConfig) CheckAndFillDefaults() error {
return nil
}

func (c *PttwebConfig) IsCaptchaConfigured() bool {
return c.RecaptchaSiteKey != "" && c.RecaptchaSecret != "" && c.CaptchaRedisConfig != nil
func (c *PttwebConfig) captchaConfig() *captcha.Config {
enabled := c.RecaptchaSiteKey != "" && c.RecaptchaSecret != "" && c.CaptchaRedisConfig != nil
return &captcha.Config{
Enabled: enabled,
InsertSecret: c.CaptchaInsertSecret,
ExpireSecs: c.CaptchaExpireSecs,
Recaptcha: captcha.RecaptchaConfig{
SiteKey: c.RecaptchaSiteKey,
Secret: c.RecaptchaSecret,
},
Redis: *c.CaptchaRedisConfig,
}
}
4 changes: 4 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ type Context struct {
isCrawler bool
}

func (c *Context) Request() *http.Request {
return c.R
}

func (c *Context) MergeFromRequest(r *http.Request) error {
c.R = r
_, c.isOver18CheckSkipped = routesSkipOver18[mux.CurrentRoute(r).GetName()]
Expand Down
23 changes: 23 additions & 0 deletions page/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package page

import (
"net/http"
)

type Context interface {
Request() *http.Request
}

type context struct {
req *http.Request
}

func newContext(req *http.Request) (*context, error) {
return &context{
req: req,
}, nil
}

func (c *context) Request() *http.Request {
return c.req
}
78 changes: 78 additions & 0 deletions page/wrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package page

import (
"fmt"
"log"
"net/http"

"github.com/ptt/pttweb/pttbbs"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)

func setCommonResponseHeaders(w http.ResponseWriter) {
h := w.Header()
h.Set("Server", "Cryophoenix")
h.Set("Content-Type", "text/html; charset=utf-8")
}

type ErrorWrapper func(Context, http.ResponseWriter) error

func (fn ErrorWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
setCommonResponseHeaders(w)

if err := clarifyRemoteError(handleRequest(w, r, fn)); err != nil {
if pg, ok := err.(Page); ok {
if err = ExecutePage(w, pg); err != nil {
log.Println("Failed to emit error page:", err)
}
return
}
internalError(w, err)
}
}

func clarifyRemoteError(err error) error {
if err == pttbbs.ErrNotFound {
return newNotFoundError(err)
}

switch grpc.Code(err) {
case codes.NotFound, codes.PermissionDenied:
return newNotFoundError(err)
}

return err
}

func internalError(w http.ResponseWriter, err error) {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
ExecutePage(w, &Error{
Title: `500 - Internal Server Error`,
ContentHtml: `500 - Internal Server Error / Server Too Busy.`,
})
}

func handleRequest(w http.ResponseWriter, r *http.Request, f func(Context, http.ResponseWriter) error) error {
ctx, err := newContext(r)
if err != nil {
return err
}
return f(ctx, w)
}

type NotFoundError struct {
NotFound
UnderlyingErr error
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("not found error page: %v", e.UnderlyingErr)
}

func newNotFoundError(err error) *NotFoundError {
return &NotFoundError{
UnderlyingErr: err,
}
}
20 changes: 5 additions & 15 deletions pttweb.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/ptt/pttweb/atomfeed"
"github.com/ptt/pttweb/cache"
"github.com/ptt/pttweb/captcha"
"github.com/ptt/pttweb/page"
manpb "github.com/ptt/pttweb/proto/man"
"github.com/ptt/pttweb/pttbbs"
Expand Down Expand Up @@ -124,13 +125,6 @@ func main() {
},
}

// Init captcha redis server.
if config.IsCaptchaConfigured() {
if err := initCaptchaRedisServer(config.CaptchaRedisConfig); err != nil {
log.Fatal("initCaptchaRedisServer:", err)
}
}

// Load templates
if err := page.LoadTemplates(config.TemplateDirectory, templateFuncMap()); err != nil {
log.Fatal("cannot load templates:", err)
Expand Down Expand Up @@ -237,14 +231,10 @@ func createRouter() *mux.Router {
Name("manentry")

// Captcha
if config.IsCaptchaConfigured() {
r.Path(ReplaceVars(`/captcha`)).
Handler(ErrorWrapper(handleCaptcha)).
Name("captcha")

r.Path(ReplaceVars(`/captcha/insert`)).
Handler(ErrorWrapper(handleCaptchaInsert)).
Name("captcha_insert")
if cfg := config.captchaConfig(); cfg.Enabled {
if err := captcha.Install(cfg, r); err != nil {
log.Fatal("captcha.Install:", err)
}
}

return r
Expand Down

0 comments on commit 1ed585a

Please sign in to comment.