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

Start adding ahash fingerprinting support

This commit is contained in:
Eggbertx 2024-02-12 15:22:58 -08:00
parent 0d68fc78cf
commit 5e7a648e37
7 changed files with 170 additions and 7 deletions

1
go.mod
View file

@ -28,6 +28,7 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/devedge/imagehash v0.0.0-20180324030135-7061aa3b4066 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect

2
go.sum
View file

@ -29,6 +29,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/devedge/imagehash v0.0.0-20180324030135-7061aa3b4066 h1:+QbuEqZjC9bIWKkf73EQ5oaIA7g/lB6fE7uFuEV0SeY=
github.com/devedge/imagehash v0.0.0-20180324030135-7061aa3b4066/go.mod h1:FdoOQDHSR0xYTQCl+G8ZhqsB5dKYbyxVWUoUFW7F0Lw=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=

View file

@ -186,6 +186,8 @@ type SiteConfig struct {
GeoIPType string
GeoIPOptions map[string]any
Captcha CaptchaConfig
FingerprinterOptions map[string]map[string]any
}
type CaptchaConfig struct {

View file

@ -54,12 +54,15 @@ type Board struct {
// FileBan contains the information associated with a specific file ban.
// table: DBPREFIXfile_ban
type FileBan struct {
ID int // sql: `id`
BoardID *int // sql: `board_id`
StaffID int // sql: `staff_id`
StaffNote string // sql: `staff_note`
IssuedAt time.Time // sql: `issued_at`
Checksum string // sql: `checksum`
ID int // sql: `id`
BoardID *int // sql: `board_id`
StaffID int // sql: `staff_id`
StaffNote string // sql: `staff_note`
IssuedAt time.Time // sql: `issued_at`
Checksum string // sql: `checksum`
Fingerprinter *string // sql: `fingerprinter`
BanIP bool // sql: `ban_ip`
BanIPMessage *string // sql: `ban_ip_message`
}
type filenameOrUsernameBanBase struct {

View file

@ -0,0 +1,105 @@
package fingerprinting
import (
"fmt"
"image"
"path"
"strings"
"github.com/devedge/imagehash"
"github.com/gochan-org/gochan/pkg/gcsql"
)
type ahashHandler struct {
hashVideoThumb bool // if true and the upload is a video, hash the thumb
hashLength int
}
func (ah *ahashHandler) Init(options map[string]any) error {
var ok bool
for key, val := range options {
switch strings.ToLower(key) {
case "hashvideothumb":
fallthrough
case "hashvideothumbnail":
ah.hashVideoThumb, ok = val.(bool)
if !ok {
return fmt.Errorf("invalid value type for %q, expected boolean, got %T", key, val)
}
case "hashlength":
ah.hashLength, ok = val.(int)
if !ok {
return fmt.Errorf("invalid value type for %q, expected voolean, got %T", key, val)
}
}
}
if ah.hashLength < 1 {
ah.hashLength = defaultHashLength
}
return nil
}
func (ah *ahashHandler) getImage(source *FingerprintSource) (image.Image, error) {
if source.Img == nil {
if source.FilePath == "" {
file, _, err := source.Request.FormFile("imagefile")
if err != nil {
return nil, err
}
source.Img, _, err = image.Decode(file)
return source.Img, err
}
}
return nil, nil
}
func (ah *ahashHandler) CheckFile(source *FingerprintSource, board string) (*gcsql.FileBan, error) {
img, err := ah.getImage(source)
if err != nil {
return nil, err
}
ba, err := imagehash.Ahash(img, ah.hashLength)
if err != nil {
return nil, err
}
const query = `SELECT id,board_id,staff_id,staff_note,issued_at,checksum,fingerprinter,ban_ip,ban_message
FROM DBPREFIXfile_ban WHERE fingerprinter = 'ahash' AND checksum = ? LIMIT 1`
var fileBan gcsql.FileBan
err = gcsql.QueryRowSQL(query, []any{fmt.Sprintf("%x", ba)}, []any{
&fileBan.ID, &fileBan.BoardID, &fileBan.StaffID, &fileBan.StaffNote,
&fileBan.IssuedAt, &fileBan.Checksum, &fileBan.Fingerprinter,
&fileBan.BanIP, &fileBan.BanIPMessage,
})
return &fileBan, err
}
func (ah *ahashHandler) IsCompatible(upload *gcsql.Upload) bool {
switch strings.ToLower(path.Ext(upload.OriginalFilename)) {
case ".jpg":
fallthrough
case ".jpeg":
fallthrough
case ".png":
fallthrough
case ".gif":
fallthrough
case ".tif":
fallthrough
case ".tiff":
fallthrough
case ".bmp":
fallthrough
case ".webp":
return true
case ".mp4":
fallthrough
case ".webm":
return ah.hashVideoThumb
}
return false
}
func (ahashHandler) Close() error {
return nil
}

View file

@ -0,0 +1,50 @@
package fingerprinting
import (
"fmt"
"image"
"net/http"
"github.com/gochan-org/gochan/pkg/gcsql"
)
const (
defaultHashLength = 16
)
var (
fingerprinterHandlers map[string]UploadFingerprinter
)
type UploadFingerprinter interface {
// Init initializes the fingerprinter, and accepts options assumed to be in gochan.json
Init(options map[string]any) error
// IsCompatible returns true if the fingerprinter is able to handle the incoming upload
IsCompatible(upload *gcsql.Upload) bool
// CheckFile scans the incoming file and scans it against files in DBPREFIXfile_ban
// with the fingerprinter's id. It returns true if the file is banned
CheckFile(source *FingerprintSource, board string) (*gcsql.FileBan, error)
// Close closes the fingerprinter. This may or may not be necessary
Close() error
}
type FingerprintSource struct {
FilePath string
Img image.Image
Request *http.Request
}
// RegisterFingerprinter registers the given id. It must be in the configuration,
// in the FingerprinterOptions map
func RegisterFingerprinter(id string, handler UploadFingerprinter) error {
_, ok := fingerprinterHandlers[id]
if ok {
return fmt.Errorf("a fingerprinter has already been registered to the ID %q", id)
}
fingerprinterHandlers[id] = handler
return nil
}
func init() {
RegisterFingerprinter("ahash", &ahashHandler{})
}

View file

@ -85,7 +85,7 @@ func processImage(upload *gcsql.Upload, post *gcsql.Post, board string, filePath
return err
}
if err = syscall.Symlink(path.Join(documentRoot, "spoiler.png"), thumbPath); err != nil {
errEv.Err(err).
errEv.Err(err).Caller().
Str("thumbPath", thumbPath).
Msg("Error creating symbolic link to thumbnail path")
return err