1
0

Initial Release

This commit is contained in:
2026-05-24 19:58:20 -07:00
commit d7b7e01e92
28 changed files with 4273 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
package routes
import (
"bakonpancakz/stickerboard/env"
"bytes"
"crypto/sha1"
"encoding/json"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"log"
"net"
"net/http"
"os"
"path"
"sync"
"time"
"golang.org/x/image/webp"
)
// Dead Simple Ratelimiting, refreshes whenever it feels like...
var uploadDebounce sync.Map
func init() {
go func() {
for {
uploadDebounce.Clear()
time.Sleep(time.Minute)
}
}()
}
// Retrieve the real IP address based on the environment variables
func getRealAddress(r *http.Request) string {
var ip string
if env.HTTP_PROXY_HEADER != "" {
ip = r.Header.Get(env.HTTP_PROXY_HEADER)
if ip != "" {
return ip
}
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}
ip = host
return ip
}
func POST_Stickers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// Rate Limiting
uploadOK := false
uploadIP := getRealAddress(r)
if _, exists := uploadDebounce.Load(uploadIP); exists {
http.Error(w, "Posted Too Recently", http.StatusTooManyRequests)
return
}
defer func() {
if !uploadOK {
uploadDebounce.Delete(uploadIP)
}
}()
uploadDebounce.Store(uploadIP, true)
// Sanity Checks
r.Body = http.MaxBytesReader(w, r.Body, env.MAX_FORM_BYTES)
if r.ContentLength > env.MAX_FORM_BYTES {
http.Error(w, "Payload Too Large", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(env.MAX_FORM_BYTES); err != nil {
http.Error(w, "Invalid Form Body", http.StatusBadRequest)
return
}
// Parse Incoming JSON
var formJSON struct {
OffsetX int `json:"offset_x"`
OffsetY int `json:"offset_y"`
ImageScale int `json:"image_scale"`
UserName string `json:"user_name"`
UserURL string `json:"user_url"`
Message string `json:"message"`
}
if err := json.Unmarshal([]byte(r.FormValue("data")), &formJSON); err != nil {
http.Error(w, "Malformed Form Data", http.StatusBadRequest)
return
}
if formJSON.ImageScale < 1 || formJSON.ImageScale > 100 || len(formJSON.Message) > 1024 {
http.Error(w, "Invalid Form Body", http.StatusBadRequest)
return
}
// Copy Incoming Image to Memory
// We're might partially or fully read it multiple times, so yes it has to
// be stored entirely in memory (@_@)
var formImage []byte
if file, _, err := r.FormFile("sticker"); err != nil {
http.Error(w, "Malformed Form Image", http.StatusBadRequest)
return
} else if data, err := io.ReadAll(file); err != nil {
http.Error(w, "Invalid Form Body", http.StatusBadRequest)
return
} else {
file.Close()
formImage = data
}
// Validate Incoming Image
var imageType = env.ImageSniffType(formImage)
var imageInfo image.Config
var imageErr error
switch imageType {
case env.IMAGE_WEBP:
imageInfo, imageErr = webp.DecodeConfig(bytes.NewReader(formImage))
case env.IMAGE_JPEG:
imageInfo, imageErr = jpeg.DecodeConfig(bytes.NewReader(formImage))
case env.IMAGE_PNG:
imageInfo, imageErr = png.DecodeConfig(bytes.NewReader(formImage))
case env.IMAGE_GIF:
imageInfo, imageErr = gif.DecodeConfig(bytes.NewReader(formImage))
default:
http.Error(w, "Unsupported Image Format", http.StatusBadRequest)
return
}
if imageErr != nil {
http.Error(w, "Invalid Image Data", http.StatusBadRequest)
return
}
if imageInfo.Height > 2048 || imageInfo.Width > 2048 {
http.Error(w, "Image dimension cannot be larger than 2048 pixels", http.StatusBadRequest)
return
}
if imageInfo.Height < 32 || imageInfo.Width < 32 {
http.Error(w, "Image dimension cannot be smaller than 32 pixels", http.StatusBadRequest)
return
}
// Validate Image Placement
scaledFloat := float32(formJSON.ImageScale) / 100
scaledWidth := int((float32(imageInfo.Width) * scaledFloat))
scaledHeight := int((float32(imageInfo.Height) * scaledFloat))
if scaledHeight > (env.CANVAS_STICKER_MAX_HEIGHT + 4) {
http.Error(w, "Image is Too Large", http.StatusBadRequest)
return
}
if formJSON.OffsetX < -scaledWidth || formJSON.OffsetX > env.CANVAS_WIDTH ||
formJSON.OffsetY < -scaledHeight || formJSON.OffsetY > env.CANVAS_HEIGHT {
http.Error(w, "Image cannot be placed off-screen", http.StatusBadRequest)
return
}
// Decode Image Frame(s) for Classification
var imageFrames = make([]image.Image, 0, 1)
var decodedStill image.Image
var decodedFrame *gif.GIF
switch imageType {
case env.IMAGE_WEBP:
decodedStill, imageErr = webp.Decode(bytes.NewReader(formImage))
case env.IMAGE_JPEG:
decodedStill, imageErr = jpeg.Decode(bytes.NewReader(formImage))
case env.IMAGE_PNG:
decodedStill, imageErr = png.Decode(bytes.NewReader(formImage))
case env.IMAGE_GIF:
decodedFrame, imageErr = gif.DecodeAll(bytes.NewReader(formImage))
default:
log.Printf("[http] Missing Decoder for Image Type: %s\n", imageType)
http.Error(w, "Unsupported Image Format", http.StatusBadRequest)
return
}
if imageErr != nil {
http.Error(w, "Invalid Image Data", http.StatusBadRequest)
return
} else {
// Copy Decoded Frames
if imageType == env.IMAGE_GIF {
for i := range decodedFrame.Image {
frame := decodedFrame.Image[i]
imageFrames = append(imageFrames, frame.SubImage(frame.Rect))
}
} else {
imageFrames = append(imageFrames, decodedStill)
decodedStill = nil
}
}
// Classify Decoded Frames
for i := range imageFrames {
pass, err := env.ModelClassifyImage(imageFrames[i])
if err != nil {
log.Println("[http] Cannot Classify Image:", err)
http.Error(w, "Model Error", http.StatusInternalServerError)
return
}
if !pass {
log.Printf("[http] Inappopriate Image Uploaded by %s\n", uploadIP)
http.Error(w, "Inappropriate Image", http.StatusBadRequest)
return
}
}
// Write Contents to Disk
imageHash := fmt.Sprintf("%X", sha1.Sum(formImage))
imagePath := path.Join(env.DATA_DIRECTORY, imageHash)
if err := os.WriteFile(imagePath, formImage, env.FILE_MODE); err != nil {
log.Println("[http] Cannot Write Image:", imagePath, err)
return
}
// Write Contents to Database
env.DatabaseMtx.Lock()
env.Database.Stickers = append(env.Database.Stickers, env.DatabaseSticker{
Created: time.Now(),
UserAddress: uploadIP,
UserName: formJSON.UserName,
UserURL: formJSON.UserURL,
Message: formJSON.Message,
Visible: true,
OffsetX: formJSON.OffsetX,
OffsetY: formJSON.OffsetY,
ImageScale: float64(formJSON.ImageScale) / 100,
ImageHeight: imageInfo.Height,
ImageWidth: imageInfo.Width,
ImageType: imageType,
ImageHash: imageHash,
})
env.DatabaseMtx.Unlock()
uploadOK = true
// Update Stickerboard
env.StickerboardRender()
w.WriteHeader(http.StatusCreated)
}