1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-09-04 10:06:24 -07:00

Replace internal self-generated captcha tests with hcaptcha

This commit is contained in:
Eggbertx 2022-12-20 13:13:08 -08:00
parent c180fec5eb
commit 89457e47f7
9 changed files with 153 additions and 155 deletions

1
go.mod
View file

@ -9,7 +9,6 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/lib/pq v1.10.6
github.com/mattn/go-sqlite3 v1.14.15
github.com/mojocn/base64Captcha v1.3.5
github.com/rs/zerolog v1.28.0
github.com/tdewolff/minify v2.3.6+incompatible
github.com/tdewolff/parse v2.3.4+incompatible // indirect

5
go.sum
View file

@ -11,8 +11,6 @@ github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8/go.mod h1:0QBxkXxN+
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
@ -21,8 +19,6 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0=
github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
@ -40,7 +36,6 @@ gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/9
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=

View file

@ -154,6 +154,7 @@ func BuildBoardPages(board *gcsql.Board) error {
defer boardPageFile.Close()
// Render board page template to the file,
// packaging the board/section list, threads, and board info
captchaCfg := config.GetSiteConfig().Captcha
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
"webroot": criticalCfg.WebRoot,
"boards": gcsql.AllBoards,
@ -163,6 +164,8 @@ func BuildBoardPages(board *gcsql.Board) error {
"currentPage": 1,
"board": board,
"board_config": boardConfig,
"useCaptcha": captchaCfg.UseCaptcha(),
"captcha": captchaCfg,
}, boardPageFile, "text/html"); err != nil {
errEv.Err(err).
Str("page", "board.html").
@ -204,6 +207,7 @@ func BuildBoardPages(board *gcsql.Board) error {
defer currentPageFile.Close()
// Render the boardpage template
captchaCfg := config.GetSiteConfig().Captcha
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
"webroot": criticalCfg.WebRoot,
"boards": gcsql.AllBoards,
@ -213,6 +217,8 @@ func BuildBoardPages(board *gcsql.Board) error {
"currentPage": catalog.currentPage,
"board": board,
"board_config": boardCfg,
"useCaptcha": captchaCfg.UseCaptcha(),
"captcha": captchaCfg,
}, currentPageFile, "text/html"); err != nil {
errEv.Err(err).
Caller().Send()

View file

@ -91,6 +91,7 @@ func BuildThreadPages(op *gcsql.Post) error {
}
// render thread page
captchaCfg := config.GetSiteConfig().Captcha
if err = serverutil.MinifyTemplate(gctemplates.ThreadPage, map[string]interface{}{
"webroot": criticalCfg.WebRoot,
"boards": gcsql.AllBoards,
@ -99,6 +100,8 @@ func BuildThreadPages(op *gcsql.Post) error {
"sections": gcsql.AllSections,
"posts": posts[1:],
"op": posts[0],
"useCaptcha": captchaCfg.UseCaptcha(),
"captcha": captchaCfg,
}, threadPageFile, "text/html"); err != nil {
errEv.Err(err).
Caller().Send()

View file

@ -35,10 +35,7 @@ var (
"MaxLogDays": 14,
// BoardConfig
"DateTimeFormat": "Mon, January 02, 2006 3:04:05 PM",
"CaptchaWidth": 240,
"CaptchaHeight": 80,
"CaptchaMinutesTimeout": 15,
"DateTimeFormat": "Mon, January 02, 2006 3:04:05 PM",
// PostConfig
"NewThreadDelay": 30,
@ -219,14 +216,7 @@ func (gcfg *GochanConfig) ValidateValues() error {
gcfg.DateTimeFormat = defaults["DateTimeFormat"].(string)
changed = true
}
if gcfg.CaptchaWidth == 0 {
gcfg.CaptchaWidth = defaults["CaptchaWidth"].(int)
changed = true
}
if gcfg.CaptchaHeight == 0 {
gcfg.CaptchaHeight = defaults["CaptchaHeight"].(int)
changed = true
}
if gcfg.EnableGeoIP {
if gcfg.GeoIPDBlocation == "" {
return &ErrInvalidValue{Field: "GeoIPDBlocation", Value: "", Details: "GeoIPDBlocation must be set in gochan.json if EnableGeoIP is true"}
@ -314,6 +304,18 @@ type SiteConfig struct {
MinifyJS bool `description:"If checked, gochan will minify js and json files when building"`
GeoIPDBlocation string `description:"Specifies the location of the GeoIP database file. If you're using CloudFlare, you can set it to cf to rely on CloudFlare for GeoIP information."`
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
Captcha CaptchaConfig
}
type CaptchaConfig struct {
Type string // may or may not be used, possibly for specifying which service (e.g. "hcaptcha","recaptcha")
SiteKey string
AccountSecret string
}
func (cc *CaptchaConfig) UseCaptcha() bool {
return cc.SiteKey != "" && cc.AccountSecret != ""
}
type BoardCooldowns struct {
@ -336,10 +338,6 @@ type BoardConfig struct {
DateTimeFormat string `description:"The format used for dates. See <a href=\"https://golang.org/pkg/time/#Time.Format\">here</a> for more info."`
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
UseCaptcha bool
CaptchaWidth int
CaptchaHeight int
CaptchaMinutesTimeout int
MaxBoardPages int
ShowPosterID bool
EnableSpoileredImages bool

View file

@ -1,134 +1,108 @@
package posting
import (
"encoding/json"
"errors"
"fmt"
"image/color"
"net/http"
"strconv"
"net/url"
"time"
"github.com/gochan-org/gochan/pkg/building"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gctemplates"
"github.com/gochan-org/gochan/pkg/gcutil"
"github.com/gochan-org/gochan/pkg/serverutil"
"github.com/mojocn/base64Captcha"
)
var (
captchaString *base64Captcha.DriverString
driver *base64Captcha.DriverString
ErrNoCaptchaToken = errors.New("missing required CAPTCHA")
ErrUnsupportedCaptcha = errors.New("unsupported captcha type set in configuration (currently only hcaptcha is supported)")
validCaptchaTypes = []string{"hcaptcha"}
)
type captchaJSON struct {
CaptchaID string `json:"id"`
Base64String string `json:"image"`
Result string `json:"-"`
TempPostIndex string `json:"-"`
EmailCmd string `json:"-"`
type CaptchaResult struct {
Hostname string `json:"hostname"`
Credit bool `json:"credit"`
Success bool `json:"success"`
Timestamp time.Time `json:"challenge_ts"`
}
// InitCaptcha prepares the captcha driver for use
func InitCaptcha() {
boardConfig := config.GetBoardConfig("")
if !boardConfig.UseCaptcha {
var typeIsValid bool
captchaCfg := config.GetSiteConfig().Captcha
if !captchaCfg.UseCaptcha() {
return
}
driver = base64Captcha.NewDriverString(
boardConfig.CaptchaHeight, boardConfig.CaptchaWidth, int(0), int(0), int(6),
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
&color.RGBA{0, 0, 0, 0}, nil, nil).ConvertFonts()
}
// ServeCaptcha handles requests to /captcha if UseCaptcha is enabled in gochan.json
func ServeCaptcha(writer http.ResponseWriter, request *http.Request) {
boardConfig := config.GetBoardConfig("")
if !boardConfig.UseCaptcha {
return
}
var err error
if err = request.ParseForm(); err != nil {
gcutil.LogError(err).Msg("Failed parsing request form")
serverutil.ServeErrorPage(writer, "Error parsing request form: "+err.Error())
return
}
tempPostIndexStr := request.FormValue("temppostindex")
var tempPostIndex int
if tempPostIndex, err = strconv.Atoi(tempPostIndexStr); err != nil {
tempPostIndexStr = "-1"
tempPostIndex = 0
}
emailCommand := request.FormValue("emailcmd")
id, b64 := getCaptchaImage()
captchaStruct := captchaJSON{id, b64, "", tempPostIndexStr, emailCommand}
useJSON := request.FormValue("json") == "1"
if useJSON {
writer.Header().Add("Content-Type", "application/json")
str, _ := gcutil.MarshalJSON(captchaStruct, false)
serverutil.MinifyWriter(writer, []byte(str), "application/json")
return
}
if request.FormValue("reload") == "Reload" {
request.Form.Del("reload")
request.Form.Add("didreload", "1")
ServeCaptcha(writer, request)
return
}
writer.Header().Add("Content-Type", "text/html")
captchaID := request.FormValue("captchaid")
boardIDstr := request.FormValue("boardid")
boardID, err := strconv.Atoi(boardIDstr)
if err != nil {
gcutil.LogError(err).
Str("ip", gcutil.GetRealIP(request)).
Str("boardid", boardIDstr).Send()
serverutil.ServeError(writer, fmt.Sprintf("Invalid boardid value %q", boardIDstr),
useJSON, map[string]interface{}{
"boardid": boardIDstr,
})
serverutil.ServeErrorPage(writer, fmt.Sprintf("Invalid boardid value %q", boardIDstr))
return
}
captchaAnswer := request.FormValue("captchaanswer")
if captchaID != "" && request.FormValue("didreload") != "1" {
goodAnswer := base64Captcha.DefaultMemStore.Verify(captchaID, captchaAnswer, true)
if goodAnswer {
if tempPostIndex > -1 && tempPostIndex < len(gcsql.TempPosts) {
// came from a /post redirect, insert the specified temporary post
// and redirect to the thread
gcsql.TempPosts[tempPostIndex].Insert(emailCommand != "sage", boardID, false, false, false, false)
building.BuildBoards(false, boardID)
building.BuildFrontPage()
url := gcsql.TempPosts[tempPostIndex].WebPath()
// move the end Post to the current index and remove the old end Post. We don't
// really care about order as long as tempPost validation doesn't get jumbled up
gcsql.TempPosts[tempPostIndex] = gcsql.TempPosts[len(gcsql.TempPosts)-1]
gcsql.TempPosts = gcsql.TempPosts[:len(gcsql.TempPosts)-1]
http.Redirect(writer, request, url, http.StatusFound)
return
}
} else {
captchaStruct.Result = "Incorrect CAPTCHA"
for _, vType := range validCaptchaTypes {
if captchaCfg.Type == vType {
typeIsValid = true
}
}
if err = serverutil.MinifyTemplate(gctemplates.Captcha, captchaStruct, writer, "text/html"); err != nil {
gcutil.LogError(err).
Str("template", "captcha").Send()
fmt.Fprint(writer, "Error executing captcha template: ", err.Error())
if !typeIsValid {
fmt.Printf("Unrecognized Captcha.Type value in configuration: %q, valid values: %v\n",
captchaCfg.Type, validCaptchaTypes)
gcutil.LogFatal().
Str("captchaType", captchaCfg.Type).
Msg("Unsupported captcha type set in configuration")
}
}
func getCaptchaImage() (captchaID, chaptchaB64 string) {
boardConfig := config.GetBoardConfig("")
if !boardConfig.UseCaptcha {
// SubmitCaptchaResponse parses the incoming captcha form values, submits them, and returns the results
func SubmitCaptchaResponse(request *http.Request) (bool, error) {
captchaCfg := config.GetSiteConfig().Captcha
if !captchaCfg.UseCaptcha() {
return true, nil // captcha isn't required, skip the test
}
var token string
switch captchaCfg.Type {
case "hcaptcha":
token = request.PostFormValue("h-captcha-response")
default:
}
if token == "" {
return false, ErrNoCaptchaToken
}
params := url.Values{
"secret": []string{captchaCfg.AccountSecret},
"response": []string{token},
}
resp, err := http.PostForm("https://hcaptcha.com/siteverify", params)
if err != nil {
return false, err
}
defer resp.Body.Close()
var vals CaptchaResult
if err = json.NewDecoder(resp.Body).Decode(&vals); err != nil {
return false, err
}
return vals.Success, nil
}
// ServeCaptcha handles requests to /captcha if the captcha is properly configured
func ServeCaptcha(writer http.ResponseWriter, request *http.Request) {
captchaCfg := config.GetSiteConfig().Captcha
if request.Method == "GET" && request.FormValue("needcaptcha") != "" {
fmt.Fprint(writer, captchaCfg.UseCaptcha())
return
}
captcha := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)
captchaID, chaptchaB64, _ = captcha.Generate()
return
if !captchaCfg.UseCaptcha() {
serverutil.ServeErrorPage(writer, "This site is not set up to require a CAPTCHA test")
return
}
if request.Method == "POST" {
result, err := SubmitCaptchaResponse(request)
if err != nil {
serverutil.ServeErrorPage(writer, "Error checking results: "+err.Error())
}
fmt.Println("Success:", result)
}
err := serverutil.MinifyTemplate(gctemplates.Captcha, map[string]interface{}{
"webroot": config.GetSystemCriticalConfig().WebRoot,
"siteKey": captchaCfg.SiteKey,
}, writer, "text/html")
if err != nil {
serverutil.ServeErrorPage(writer, "Error serving CAPTCHA: "+err.Error())
}
}

View file

@ -212,6 +212,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
delay, err = gcsql.SinceLastThread(post.IP)
tooSoon = delay < boardConfig.Cooldowns.NewThread
} else {
// replying to a thread
delay, err = gcsql.SinceLastPost(post.IP)
tooSoon = delay < boardConfig.Cooldowns.Reply
}
@ -236,24 +237,16 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
}
post.Sanitize()
if boardConfig.UseCaptcha {
captchaID := request.FormValue("captchaid")
captchaAnswer := request.FormValue("captchaanswer")
if captchaID == "" && captchaAnswer == "" {
// browser isn't using JS, save post data to tempPosts and show captcha
request.Form.Add("temppostindex", strconv.Itoa(len(gcsql.TempPosts)))
request.Form.Add("emailcmd", emailCommand)
gcsql.TempPosts = append(gcsql.TempPosts, post)
ServeCaptcha(writer, request)
return
}
captchaSuccess, err := SubmitCaptchaResponse(request)
if err != nil {
serverutil.ServeErrorPage(writer, "Error submitting captcha response:"+err.Error())
errEv.Err(err).
Caller().Send()
return
}
boardExists := gcsql.DoesBoardExistByID(boardID)
if !boardExists {
serverutil.ServeErrorPage(writer, "Board does not exist (invalid boardid)")
if !captchaSuccess {
serverutil.ServeErrorPage(writer, "Missing or invalid captcha response")
errEv.Msg("Missing or invalid captcha response")
return
}

View file

@ -1,18 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<title>Gochan CAPTCHA</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{with .board -}}
{{with $.op -}}
<title>{{$.op.TitleText}}</title>
{{- else}}
<title>/{{$.board.Dir}}/ - {{$.board.Title}}</title>
{{end}}
{{- else}}<title>{{with $.page_title}}{{$.page_title}} - {{end}}{{.site_config.SiteName}}</title>{{end}}
<link rel="stylesheet" href="{{.webroot}}css/global.css" />
<link id="theme" rel="stylesheet" href="{{.webroot}}css/{{.board_config.DefaultStyle}}" />
<link rel="shortcut icon" href="{{.webroot}}favicon.png">
<script type="text/javascript" src="{{.webroot}}js/consts.js"></script>
<script type="text/javascript" src="{{.webroot}}js/gochan.js"></script>
</head>
<body>
{{with .Result}}{{$.Result}}{{end}}
<form action="/captcha" method="POST">
<img src="{{.Base64String}}" /><br />
<input type="text" name="captchaanswer" autocomplete="off" />
<input type="hidden" name="captchaid" value="{{.CaptchaID}}" />
{{with .EmailCmd}}<input type="hidden" name="emailcmd" value="{{$.EmailCmd}}" />{{end}}
{{with .TempPostIndex}}<input type="hidden" name="temppostindex" value="{{$.TempPostIndex}}" />{{end}}
<input type="submit" value="Submit" /><br />
<input type="submit" name="reload" value="Reload" />
<div id="topbar">
<a href="{{$.webroot}}" class="topbar-item">home</a>
{{range $i, $board := .boards}}<a href="{{$.webroot}}{{$board.Dir}}/" class="topbar-item" title="{{$board.Title}}">/{{$board.Dir}}/</a>{{end}}
</div>
{{with $.page_title }}<header>
<h1 id="board-title">{{$.page_title}}</h1>
{{with $.include_dashboard_link}}<a href="{{$.webroot}}manage" class="board-subtitle">Return to dashboard</a><br/>{{end}}
</header>{{end}}
<div id="content">
<header>
<h1 id="board-title">hCAPTCHA test</h1>
</header><br />
<form method="POST" action="{{.webroot}}captcha">
<div class="h-captcha" data-sitekey="{{.siteKey}}"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<input type="submit" value="Post">
</form>
<div id="footer">
Powered by <a href="http://github.com/gochan-org/gochan/">Gochan {{version}}</a><br />
</div>
</div>
</body>
</html>

View file

@ -17,6 +17,12 @@
<tr><th class="postblock">Message</th><td><textarea rows="4" cols="48" name="postmsg" id="postmsg"></textarea></td></tr>
<tr><th class="postblock">File</th><td><input name="imagefile" type="file" accept="image/jpeg,image/png,image/gif,video/webm,video/mp4"><input type="checkbox" id="spoiler" name="spoiler"/><label for="spoiler">Spoiler</label></td></tr>
<tr><th class="postblock">Password</th><td><input type="password" id="postpassword" name="postpassword" size="14" /> (for post/file deletion)</td></tr>
{{if .useCaptcha -}}
<tr><th class="postblock">CAPTCHA</th><td>
<div class="h-captcha" data-sitekey="{{.captcha.SiteKey}}"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
</td></tr>
{{- end}}
</table><input type="password" name="dummy2" style="display:none"/>
</form>
</div>{{end}}