1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-09-05 11:06:23 -07:00

Merge fingerprinting package into uploads package

This commit is contained in:
Eggbertx 2024-02-14 15:18:11 -08:00
parent 5e7a648e37
commit 0795a0a8a8
6 changed files with 93 additions and 166 deletions

2
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/Eggbertx/durationutil v1.0.0
github.com/aquilax/tripcode v1.0.1
github.com/cjoudrey/gluahttp v0.0.0-20201111170219-25003d9adfa9
github.com/devedge/imagehash v0.0.0-20180324030135-7061aa3b4066
github.com/disintegration/imaging v1.6.2
github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8
github.com/go-sql-driver/mysql v1.7.1
@ -28,7 +29,6 @@ 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

View file

@ -179,7 +179,6 @@ type SiteConfig struct {
MaxRecentPosts int
RecentPostsWithNoFile bool
EnableAppeals bool
MaxLogDays int
MinifyHTML bool
MinifyJS bool
@ -187,7 +186,8 @@ type SiteConfig struct {
GeoIPOptions map[string]any
Captcha CaptchaConfig
FingerprinterOptions map[string]map[string]any
FingerprintVideoThumbnails bool
FingerprintHashLength int
}
type CaptchaConfig struct {

View file

@ -23,7 +23,13 @@ import (
)
var (
uploadHandlers map[string]UploadHandler
uploadHandlers map[string]UploadHandler
imageExtensions = []string{
".bmp", ".gif", ".jpg", ".jpeg", ".png", ".webp",
}
videoExtensions = []string{
".mp4", ".webm",
}
)
type UploadHandler func(upload *gcsql.Upload, post *gcsql.Post, board string, filePath string, thumbPath string, catalogThumbPath string, infoEv *zerolog.Event, accessEv *zerolog.Event, errEv *zerolog.Event) error
@ -35,13 +41,12 @@ func RegisterUploadHandler(ext string, handler UploadHandler) {
func init() {
uploadHandlers = make(map[string]UploadHandler)
RegisterUploadHandler(".gif", processImage)
RegisterUploadHandler(".jpg", processImage)
RegisterUploadHandler(".jpeg", processImage)
RegisterUploadHandler(".png", processImage)
RegisterUploadHandler(".webp", processImage)
RegisterUploadHandler(".mp4", processVideo)
RegisterUploadHandler(".webm", processVideo)
for _, ext := range imageExtensions {
uploadHandlers[ext] = processImage
}
for _, ext := range videoExtensions {
uploadHandlers[ext] = processVideo
}
}
// AttachUploadFromRequest reads an incoming HTTP request and processes any incoming files.

View file

@ -0,0 +1,77 @@
package uploads
import (
"fmt"
"image"
"net/http"
"path"
"github.com/devedge/imagehash"
"github.com/disintegration/imaging"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql"
)
const (
defaultFingerprintHashLength = 16
)
type FingerprintSource struct {
FilePath string
Img image.Image
Request *http.Request
}
func fingerprintImage(img image.Image, board string) (*gcsql.FileBan, error) {
hashLength := config.GetSiteConfig().FingerprintHashLength
if hashLength < 1 {
hashLength = defaultFingerprintHashLength
}
ba, err := imagehash.Ahash(img, 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
if 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,
}); err != nil {
return nil, err
}
if fileBan.ID == 0 {
// no matches
return nil, nil
}
return &fileBan, err
}
func fingerprintFile(filePath string, board string) (*gcsql.FileBan, error) {
img, err := imaging.Open(filePath)
if err != nil {
return nil, err
}
return fingerprintImage(img, board)
}
func canFingerprint(filename string) bool {
siteCfg := config.GetSiteConfig()
ext := path.Ext(filename)
for _, iExt := range imageExtensions {
if iExt == ext {
return true
}
}
if siteCfg.FingerprintVideoThumbnails {
for _, vExt := range videoExtensions {
if vExt == ext {
return true
}
}
}
return false
}

View file

@ -1,105 +0,0 @@
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

@ -1,50 +0,0 @@
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{})
}