mirror of
https://codeberg.org/gigirassy/pinata
synced 2026-08-30 23:37:40 +00:00
Update main.go
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -37,7 +39,7 @@ var httpClient = &http.Client{
|
||||
|
||||
var copyBufPool = sync.Pool{
|
||||
New: func() any {
|
||||
b := make([]byte, 32*1024) // 32KB
|
||||
b := make([]byte, 32*1024)
|
||||
return &b
|
||||
},
|
||||
}
|
||||
@@ -223,10 +225,81 @@ func clearBookmarksCookie(w http.ResponseWriter) {
|
||||
http.SetCookie(w, c)
|
||||
}
|
||||
|
||||
// ---------- CSS and HTML (no JS) ----------
|
||||
// ---------- theme helpers ----------
|
||||
|
||||
// validate and normalize a hex color; returns "#rrggbb" or empty string if invalid
|
||||
func normalizeHexColor(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
// allow with or without leading '#'
|
||||
if strings.HasPrefix(s, "#") {
|
||||
s = s[1:]
|
||||
}
|
||||
if len(s) != 6 {
|
||||
return ""
|
||||
}
|
||||
for _, r := range s {
|
||||
if !(('0' <= r && r <= '9') || ('a' <= r && r <= 'f') || ('A' <= r && r <= 'F')) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return "#" + strings.ToLower(s)
|
||||
}
|
||||
|
||||
// hex to rgba string with alpha
|
||||
func hexToRGBA(hex string, alpha float64) string {
|
||||
hex = strings.TrimPrefix(hex, "#")
|
||||
if len(hex) != 6 {
|
||||
return "rgba(124,58,237,0.12)" // fallback purple-ish
|
||||
}
|
||||
rv, _ := strconv.ParseUint(hex[0:2], 16, 8)
|
||||
gv, _ := strconv.ParseUint(hex[2:4], 16, 8)
|
||||
bv, _ := strconv.ParseUint(hex[4:6], 16, 8)
|
||||
return fmt.Sprintf("rgba(%d,%d,%d,%.2f)", rv, gv, bv, alpha)
|
||||
}
|
||||
|
||||
// get theme variables from cookies; returns accent (hex) and imgScale (float like "1.00")
|
||||
func getThemeVars(r *http.Request) (string, string) {
|
||||
// Default accent
|
||||
accent := "#7c3aed"
|
||||
imgScale := "1.00" // default 100%
|
||||
if c, err := r.Cookie("pinata_accent"); err == nil {
|
||||
if val := normalizeHexColor(c.Value); val != "" {
|
||||
accent = val
|
||||
}
|
||||
}
|
||||
if c2, err := r.Cookie("pinata_img_scale"); err == nil {
|
||||
// expect integer percent
|
||||
if p, err := strconv.Atoi(c2.Value); err == nil {
|
||||
if p < 50 {
|
||||
p = 50
|
||||
}
|
||||
if p > 200 {
|
||||
p = 200
|
||||
}
|
||||
// convert to scale
|
||||
scale := float64(p) / 100.0
|
||||
imgScale = fmt.Sprintf("%.2f", scale)
|
||||
}
|
||||
}
|
||||
return accent, imgScale
|
||||
}
|
||||
|
||||
// ---------- CSS (uses CSS vars; defaults are present but overridden per-request via inline style) ----------
|
||||
const cssContent = `
|
||||
:root{--bg:#0b0f17;--muted:#94a3b8;--text:#e6e6ff;--accent:#7c3aed;--card-shadow:rgba(0,0,0,0.6)}
|
||||
*{box-sizing:border-box}html,body{height:100%}body{margin:0;padding:20px;background:linear-gradient(180deg,#071020 0%,#0b0f17 100%);color:var(--text);font-family:ui-monospace,Menlo,Monaco,monospace}
|
||||
:root{
|
||||
--bg:#0b0f17;
|
||||
--muted:#94a3b8;
|
||||
--text:#e6e6ff;
|
||||
--accent:#7c3aed; /* default; overridden by inline style */
|
||||
--accent-rgba: rgba(124,58,237,0.12);
|
||||
--img-scale: 1;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{margin:0;padding:20px;background:linear-gradient(180deg,#071020 0%,var(--bg) 100%);color:var(--text);font-family:ui-monospace,Menlo,Monaco,monospace}
|
||||
a{color:inherit}
|
||||
.header{display:flex;gap:12px;align-items:center;margin-bottom:18px;flex-wrap:wrap}
|
||||
.brand{font-size:20px;font-weight:700;color:var(--accent);text-decoration:none}
|
||||
@@ -238,9 +311,9 @@ button[type="submit"],.btn-save{background:linear-gradient(90deg,var(--accent),#
|
||||
.btn-save{font-weight:600}
|
||||
.img-container { column-width: 260px; column-gap: 16px; width: 100%; max-width: 1400px; margin-top: 18px; }
|
||||
.card { display:inline-block; width:100%; margin:0 0 16px; border-radius:10px; overflow:hidden; background:linear-gradient(180deg,rgba(255,255,255,0.01),rgba(255,255,255,0.02)); box-shadow:0 6px 18px rgba(3,7,18,0.6); border:1px solid rgba(124,58,237,0.06); break-inside: avoid; -webkit-column-break-inside: avoid; -moz-column-break-inside: avoid; min-height:0; position:relative; }
|
||||
.card img { display:block; width:100%; height:auto; object-fit:cover; background:#08101a; }
|
||||
.card img { display:block; width:100%; height:auto; object-fit:cover; background:#08101a; transform-origin: top center; transform: scale(var(--img-scale)); }
|
||||
.card-controls { position:absolute; top:8px; right:8px; display:flex; gap:8px; align-items:center; }
|
||||
.btn-save-mini { background: rgba(124,58,237,0.12); color: var(--text); border: none; padding:6px; border-radius:999px; cursor:pointer; font-weight:700; }
|
||||
.btn-save-mini { background: rgba(0,0,0,0.45); border:1px solid rgba(255,255,255,0.06); color: var(--text); padding:6px; border-radius:999px; cursor:pointer; font-weight:700; display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; text-decoration:none; }
|
||||
.magnifier{background:rgba(0,0,0,0.45);border:1px solid rgba(255,255,255,0.06);color:var(--text);padding:6px;border-radius:999px;font-size:14px;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;text-decoration:none}
|
||||
.bookmarks{margin-left:12px;color:var(--muted);font-size:14px}
|
||||
.bookmark-list{margin-top:10px;display:flex;gap:8px;flex-wrap:wrap}
|
||||
@@ -255,18 +328,85 @@ button[type="submit"],.btn-save{background:linear-gradient(90deg,var(--accent),#
|
||||
`
|
||||
|
||||
// ---------- handlers ----------
|
||||
|
||||
func styleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/css; charset=utf8")
|
||||
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
||||
_, _ = io.WriteString(w, cssContent)
|
||||
}
|
||||
|
||||
// settings POST handler: sets accent color and image scale cookies
|
||||
func settingsPostHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
accent := normalizeHexColor(r.FormValue("accent"))
|
||||
scaleStr := r.FormValue("scale") // expected as integer percent like "100"
|
||||
if accent == "" {
|
||||
accent = "#7c3aed"
|
||||
}
|
||||
percent := 100
|
||||
if ss := strings.TrimSpace(scaleStr); ss != "" {
|
||||
if p, err := strconv.Atoi(ss); err == nil {
|
||||
if p < 50 {
|
||||
p = 50
|
||||
}
|
||||
if p > 200 {
|
||||
p = 200
|
||||
}
|
||||
percent = p
|
||||
}
|
||||
}
|
||||
// set cookies (non-encrypted, not sensitive)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "pinata_accent",
|
||||
Value: accent,
|
||||
Path: "/",
|
||||
MaxAge: 60 * 60 * 24 * 365 * 5,
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "pinata_img_scale",
|
||||
Value: strconv.Itoa(percent),
|
||||
Path: "/",
|
||||
MaxAge: 60 * 60 * 24 * 365 * 5,
|
||||
})
|
||||
next := r.FormValue("next")
|
||||
if next == "" {
|
||||
next = "/"
|
||||
}
|
||||
http.Redirect(w, r, next, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// Index (front) - server-rendered bookmarks and settings form (no JS)
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf8")
|
||||
_, _ = io.WriteString(w, `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Pinata - Search</title><link rel="stylesheet" href="/static/style.css"></head><body>`)
|
||||
accent, imgScale := getThemeVars(r)
|
||||
// produce small inline style that overrides css vars
|
||||
accentRgba := hexToRGBA(accent, 0.12)
|
||||
inlineStyle := fmt.Sprintf(`<style>:root{--accent:%s;--accent-rgba:%s;--img-scale:%s;}</style>`, html.EscapeString(accent), html.EscapeString(accentRgba), html.EscapeString(imgScale))
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = io.WriteString(w, `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Pinata - Search</title><link rel="stylesheet" href="/static/style.css">`+inlineStyle+`</head><body>`)
|
||||
_, _ = io.WriteString(w, `<div class="header"><a class="brand" href="/">Pinata</a><div class="search-box"></div></div>`)
|
||||
_, _ = io.WriteString(w, `<div style="color:#94a3b8; margin-bottom:12px;">Search images from Pinterest — submit a search to view results.</div>`)
|
||||
_, _ = io.WriteString(w, `<div style="color:var(--muted); margin-bottom:12px;">Search images from Pinterest — submit a search to view results.</div>`)
|
||||
_, _ = io.WriteString(w, `<form class="search-block" method="get" action="/search"><input type="text" name="q" placeholder="Search Image" required maxlength="64"><button type="submit">Search</button></form>`)
|
||||
|
||||
// Settings form (color + scale)
|
||||
_, _ = io.WriteString(w, `<div style="margin-top:12px;"><form method="post" action="/settings" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">`)
|
||||
_, _ = io.WriteString(w, `<label style="font-size:14px;color:var(--muted);">Accent: <input type="color" name="accent" value="`+html.EscapeString(accent)+`" style="margin-left:6px;"></label>`)
|
||||
_, _ = io.WriteString(w, `<label style="font-size:14px;color:var(--muted);">Image scale: <select name="scale" style="margin-left:6px;">`)
|
||||
// options: 75,100,125,150
|
||||
opts := []int{75, 100, 125, 150}
|
||||
for _, v := range opts {
|
||||
sel := ""
|
||||
if fmt.Sprintf("%.2f", float64(v)/100.0) == imgScale {
|
||||
sel = ` selected`
|
||||
}
|
||||
_, _ = io.WriteString(w, `<option value="`+strconv.Itoa(v)+`"`+sel+`>`+strconv.Itoa(v)+`%</option>`)
|
||||
}
|
||||
_, _ = io.WriteString(w, `</select></label>`)
|
||||
_, _ = io.WriteString(w, `<input type="hidden" name="next" value="/"> <button type="submit" class="btn-save">Apply</button></form></div>`)
|
||||
|
||||
// bookmarks shown only on index
|
||||
if bookmarkingEnabled {
|
||||
items := readBookmarksFromReq(r)
|
||||
_, _ = io.WriteString(w, `<div class="bookmarks"><div style="font-size:14px;color:var(--muted);margin-top:8px">Saved bookmarks</div><div class="bookmark-list">`)
|
||||
@@ -280,15 +420,15 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, `<form method="post" action="/bookmark_remove" style="display:inline;margin:0 0 0 6px;"><input type="hidden" name="type" value="`+html.EscapeString(e.Type)+`"><input type="hidden" name="value" value="`+html.EscapeString(e.Value)+`"><button class="bookmark-remove-btn" type="submit" title="Remove">✕</button></form></span>`)
|
||||
}
|
||||
_, _ = io.WriteString(w, `</div>`)
|
||||
|
||||
_, _ = io.WriteString(w, `<div class="export-form"><form method="get" action="/bookmarks/export"><button type="submit" class="btn-save">Export JSON</button></form>`)
|
||||
_, _ = io.WriteString(w, `<form method="post" action="/bookmarks/import" enctype="multipart/form-data" style="margin-left:8px;"><input type="file" name="file" accept="application/json" required><button type="submit" class="btn-save" style="margin-left:8px">Import JSON</button></form></div>`)
|
||||
_, _ = io.WriteString(w, `</div>`)
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(w, `<div class="footer-note">Powered by Pinata • Reverse search via Tineye</div></body></html>`)
|
||||
_, _ = io.WriteString(w, `<div class="footer-note">Powered by Pinata • Reverse image search uses Tineye</div></body></html>`)
|
||||
}
|
||||
|
||||
// searchHandler: streaming results, include inline style variables from cookies
|
||||
func searchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
||||
if len(q) < 1 || len(q) > 64 {
|
||||
@@ -343,8 +483,14 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
accent, imgScale := getThemeVars(r)
|
||||
accentRgba := hexToRGBA(accent, 0.12)
|
||||
inlineStyle := fmt.Sprintf(`<style>:root{--accent:%s;--accent-rgba:%s;--img-scale:%s;}</style>`, html.EscapeString(accent), html.EscapeString(accentRgba), html.EscapeString(imgScale))
|
||||
|
||||
// Start streaming HTML
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = io.WriteString(w, `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>`+html.EscapeString(q)+` - Pinata</title><link rel="stylesheet" href="/static/style.css"></head><body>`)
|
||||
_, _ = io.WriteString(w, `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>`+html.EscapeString(q)+` - Pinata</title><link rel="stylesheet" href="/static/style.css">`+inlineStyle+`</head><body>`)
|
||||
// header: inline search and Save-search form
|
||||
_, _ = io.WriteString(w, `<div class="header" style="margin-bottom:8px;"><a class="brand" href="/">Pinata</a><div class="search-box">`)
|
||||
_, _ = io.WriteString(w, `<form class="search-inline" method="get" action="/search"><input type="text" name="q" value="`+html.EscapeString(q)+`" maxlength="64"><button type="submit">Search</button></form>`)
|
||||
if bookmarkingEnabled {
|
||||
@@ -400,6 +546,7 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
esc := url.QueryEscape(u)
|
||||
b64 := base64.StdEncoding.EncodeToString([]byte(u))
|
||||
|
||||
// card
|
||||
_, _ = io.WriteString(w, `<div class="card">`)
|
||||
_, _ = io.WriteString(w, `<a href="/image_proxy?url=`+esc+`" style="display:block;"><img loading="lazy" src="/image_proxy?url=`+esc+`" alt="image"></a>`)
|
||||
_, _ = io.WriteString(w, `<div class="card-controls">`)
|
||||
@@ -410,8 +557,9 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
next := "/search?q=" + url.QueryEscape(q)
|
||||
_, _ = io.WriteString(w, `<form method="post" action="/bookmark_image" style="display:inline;margin:0;"><input type="hidden" name="url" value="`+html.EscapeString(u)+`"><input type="hidden" name="next" value="`+html.EscapeString(next)+`"><button class="btn-save-mini" type="submit" title="Save image">❤</button></form>`)
|
||||
}
|
||||
_, _ = io.WriteString(w, `</div>`)
|
||||
_, _ = io.WriteString(w, `</div>`)
|
||||
_, _ = io.WriteString(w, `</div>`) // card-controls
|
||||
_, _ = io.WriteString(w, `</div>`) // card
|
||||
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
@@ -442,10 +590,88 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
next := "/search?q=" + qenc + "&bookmark=" + benc + cenc
|
||||
_, _ = io.WriteString(w, `<div class="pagination"><a href="`+html.EscapeString(next)+`">Next page</a></div>`)
|
||||
}
|
||||
_, _ = io.WriteString(w, `<div class="footer-note">Powered by Pinata • Reverse search via Tineye</div></body></html>`)
|
||||
_, _ = io.WriteString(w, `<div class="footer-note">Powered by Pinata • Reverse image search uses Tineye</div></body></html>`)
|
||||
}
|
||||
|
||||
// ---------- bookmark handlers ----------
|
||||
// ---------- secure image proxy (only https i.pinimg.com) ----------
|
||||
func imageProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uq := r.URL.Query().Get("url")
|
||||
if uq == "" {
|
||||
http.Error(w, "url required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
orig, err := url.QueryUnescape(uq)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid url", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
parsed, err := url.Parse(orig)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid url", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// require https and exact host
|
||||
if parsed.Scheme != "https" {
|
||||
http.Error(w, "proxy allowed for https only", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !strings.EqualFold(parsed.Hostname(), "i.pinimg.com") {
|
||||
http.Error(w, "proxy allowed only for i.pinimg.com", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", parsed.String(), nil)
|
||||
if err != nil {
|
||||
http.Error(w, "failed", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0")
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to fetch", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||
w.Header().Set("Content-Type", ct)
|
||||
}
|
||||
if cc := resp.Header.Get("Cache-Control"); cc != "" {
|
||||
w.Header().Set("Cache-Control", cc)
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
bufPtr := copyBufPool.Get().(*[]byte)
|
||||
buf := *bufPtr
|
||||
_, _ = io.CopyBuffer(w, resp.Body, buf)
|
||||
copyBufPool.Put(bufPtr)
|
||||
}
|
||||
|
||||
func revsearchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if disableReverse {
|
||||
http.Error(w, "reverse disabled", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
b64 := r.URL.Query().Get("b64")
|
||||
if b64 == "" {
|
||||
http.Error(w, "b64 required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
bs, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid b64", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
orig := string(bs)
|
||||
if !(strings.HasPrefix(orig, "http://") || strings.HasPrefix(orig, "https://")) {
|
||||
http.Error(w, "invalid url", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tineye := "https://tineye.com/search?url=" + url.QueryEscape(orig)
|
||||
http.Redirect(w, r, tineye, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ---------- bookmark handlers (unchanged from previous) ----------
|
||||
|
||||
func bookmarkPostHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !bookmarkingEnabled {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
@@ -637,98 +863,11 @@ func bookmarksImportHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ---------- image proxy & revsearch ----------
|
||||
func imageProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uq := r.URL.Query().Get("url")
|
||||
if uq == "" {
|
||||
http.Error(w, "url required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
orig, err := url.QueryUnescape(uq)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid url", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(orig)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid url", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Require https scheme
|
||||
if parsed.Scheme != "https" {
|
||||
http.Error(w, "proxy allowed for https only", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Require exact host i.pinimg.com (case insensitive)
|
||||
if !strings.EqualFold(parsed.Hostname(), "i.pinimg.com") {
|
||||
http.Error(w, "proxy allowed only for i.pinimg.com", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Build request with timeout
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", parsed.String(), nil)
|
||||
if err != nil {
|
||||
http.Error(w, "failed", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to fetch", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Copy select headers and stream body using pooled buffer
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||
w.Header().Set("Content-Type", ct)
|
||||
}
|
||||
if cc := resp.Header.Get("Cache-Control"); cc != "" {
|
||||
w.Header().Set("Cache-Control", cc)
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
bufPtr := copyBufPool.Get().(*[]byte)
|
||||
buf := *bufPtr
|
||||
_, _ = io.CopyBuffer(w, resp.Body, buf)
|
||||
copyBufPool.Put(bufPtr)
|
||||
}
|
||||
|
||||
|
||||
func revsearchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if disableReverse {
|
||||
http.Error(w, "reverse disabled", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
b64 := r.URL.Query().Get("b64")
|
||||
if b64 == "" {
|
||||
http.Error(w, "b64 required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
bs, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid b64", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
orig := string(bs)
|
||||
if !(strings.HasPrefix(orig, "http://") || strings.HasPrefix(orig, "https://")) {
|
||||
http.Error(w, "invalid url", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tineye := "https://tineye.com/search?url=" + url.QueryEscape(orig)
|
||||
http.Redirect(w, r, tineye, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ---------- main ----------
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/static/style.css", styleHandler)
|
||||
mux.HandleFunc("/settings", settingsPostHandler)
|
||||
mux.HandleFunc("/", indexHandler)
|
||||
mux.HandleFunc("/search", searchHandler)
|
||||
mux.HandleFunc("/image_proxy", imageProxyHandler)
|
||||
|
||||
Reference in New Issue
Block a user