378 lines
10 KiB
Go
378 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"image"
|
|
_ "image/gif"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/image/bmp"
|
|
_ "golang.org/x/image/tiff"
|
|
_ "golang.org/x/image/vp8"
|
|
_ "golang.org/x/image/vp8l"
|
|
_ "golang.org/x/image/webp"
|
|
|
|
"github.com/corona10/goimagehash"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/disintegration/imaging"
|
|
"github.com/zitadel/oidc/pkg/client/rp"
|
|
httphelper "github.com/zitadel/oidc/pkg/http"
|
|
"github.com/zitadel/oidc/pkg/oidc"
|
|
)
|
|
|
|
const (
|
|
h_1 uint64 = 0b11111111 << (8 * iota)
|
|
h_2
|
|
h_3
|
|
h_4
|
|
h_5
|
|
h_6
|
|
h_7
|
|
h_8
|
|
)
|
|
|
|
const (
|
|
shift_1 = (8 * iota)
|
|
shift_2
|
|
shift_3
|
|
shift_4
|
|
shift_5
|
|
shift_6
|
|
shift_7
|
|
shift_8
|
|
)
|
|
|
|
type Cover map[string][]string // IDs is a map of domain to ID eg IDs['comicvine.gamespot.com'] = []string{"1235"}
|
|
|
|
// type Cover struct {
|
|
// AHash uint64
|
|
// DHash uint64
|
|
// PHash uint64
|
|
// IDs map[string][]string // IDs is a map of domain to ID eg IDs['comicvine.gamespot.com'] = []string{"1235"}
|
|
// }
|
|
|
|
type Server struct {
|
|
httpServer *http.Server
|
|
mux *http.ServeMux
|
|
BaseURL *url.URL
|
|
token chan<- *oidc.Tokens
|
|
ahash [8]map[uint8]uint32
|
|
dhash [8]map[uint8]uint32
|
|
phash [8]map[uint8]uint32
|
|
fAhash map[uint64]uint32
|
|
fDhash map[uint64]uint32
|
|
fPhash map[uint64]uint32
|
|
IDToCover map[string]uint32 // IDToCover is a map of domain:id to an index to covers eg IDToCover['comicvine.gamespot.com:12345'] = 0
|
|
covers []Cover
|
|
// hashes are a uint64 split into 8 pieces or a unint64 for quick lookup, the value is an index to covers
|
|
}
|
|
|
|
var key = []byte(uuid.New().String())[:16]
|
|
|
|
func main() {
|
|
// mustDropPrivileges()
|
|
startServer()
|
|
}
|
|
|
|
func (s *Server) authenticated(w http.ResponseWriter, r *http.Request) (string, bool) {
|
|
return strings.TrimSpace("lordwelch"), true
|
|
}
|
|
|
|
func (s *Server) setupOauthHandlers() error {
|
|
redirectURI := *s.BaseURL
|
|
redirectURI.Path = "/oauth/callback"
|
|
successURI := *s.BaseURL
|
|
successURI.Path = "/success"
|
|
failURI := *s.BaseURL
|
|
failURI.RawQuery = url.Values{"auth": []string{"fail"}}.Encode()
|
|
|
|
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
|
|
|
options := []rp.Option{
|
|
rp.WithCookieHandler(cookieHandler),
|
|
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
|
}
|
|
|
|
provider, err := rp.NewRelyingPartyOIDC(os.Getenv("COMICHASHER_PROVIDER_URL"), os.Getenv("COMICHASHER_CLIENT_ID"), os.Getenv("COMICHASHER_CLIENT_SECRET"), redirectURI.String(), strings.Split(os.Getenv("COMICHASHER_SCOPES"), ","), options...)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating provider: %w", err)
|
|
}
|
|
|
|
// generate some state (representing the state of the user in your application,
|
|
// e.g. the page where he was before sending him to login
|
|
state := func() string {
|
|
return uuid.New().String()
|
|
}
|
|
|
|
// register the AuthURLHandler at your preferred path
|
|
// the AuthURLHandler creates the auth request and redirects the user to the auth server
|
|
// including state handling with secure cookie and the possibility to use PKCE
|
|
s.mux.Handle("/login", rp.AuthURLHandler(state, provider))
|
|
|
|
// for demonstration purposes the returned userinfo response is written as JSON object onto response
|
|
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) {
|
|
s.token <- tokens
|
|
w.Header().Add("location", successURI.String())
|
|
w.WriteHeader(301)
|
|
}
|
|
|
|
// register the CodeExchangeHandler at the callbackPath
|
|
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
|
|
// with the returned tokens from the token endpoint
|
|
s.mux.Handle(redirectURI.Path, rp.CodeExchangeHandler(marshalUserinfo, provider))
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) setupAppHandlers() {
|
|
s.mux.HandleFunc("/add_cover", s.add_cover)
|
|
s.mux.HandleFunc("/get_cover", s.get_cover)
|
|
s.mux.HandleFunc("/match_cover_hash", s.match_cover_hash)
|
|
}
|
|
|
|
func (s *Server) get_cover(w http.ResponseWriter, r *http.Request) {
|
|
user, authed := s.authenticated(w, r)
|
|
if !authed || user == "" {
|
|
http.Error(w, "Invalid Auth", http.StatusForbidden)
|
|
return
|
|
}
|
|
var (
|
|
values = r.URL.Query()
|
|
domain = strings.TrimSpace(values.Get("domain"))
|
|
id = strings.TrimSpace(values.Get("id"))
|
|
)
|
|
if id == "" {
|
|
log.Println("No ID Provided")
|
|
http.Error(w, "No ID Provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if domain == "" {
|
|
log.Println("No domain Provided")
|
|
http.Error(w, "No domain Provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if index, ok := s.IDToCover[domain+":"+id]; ok {
|
|
covers, err := json.Marshal(s.covers[index])
|
|
if err == nil {
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(covers)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) match_cover_hash(w http.ResponseWriter, r *http.Request) {
|
|
user, authed := s.authenticated(w, r)
|
|
if !authed || user == "" {
|
|
http.Error(w, "Invalid Auth", http.StatusForbidden)
|
|
return
|
|
}
|
|
var (
|
|
values = r.URL.Query()
|
|
ahashStr = strings.TrimSpace(values.Get("ahash"))
|
|
dhashStr = strings.TrimSpace(values.Get("dhash"))
|
|
phashStr = strings.TrimSpace(values.Get("phash"))
|
|
ahash uint64
|
|
dhash uint64
|
|
phash uint64
|
|
err error
|
|
)
|
|
if ahash, err = strconv.ParseUint(ahashStr, 16, 64); err != nil && ahashStr != "" {
|
|
log.Printf("could not parse ahash: %s", ahashStr)
|
|
http.Error(w, "parse fail", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if dhash, err = strconv.ParseUint(dhashStr, 16, 64); err != nil && dhashStr != "" {
|
|
log.Printf("could not parse dhash: %s", dhashStr)
|
|
http.Error(w, "parse fail", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if phash, err = strconv.ParseUint(phashStr, 16, 64); err != nil && phashStr != "" {
|
|
log.Printf("could not parse phash: %s", phashStr)
|
|
http.Error(w, "parse fail", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if index, ok := s.fAhash[ahash]; ok {
|
|
covers, err := json.Marshal(s.covers[index])
|
|
if err == nil {
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(covers)
|
|
return
|
|
}
|
|
}
|
|
if index, ok := s.fDhash[dhash]; ok {
|
|
covers, err := json.Marshal(s.covers[index])
|
|
if err == nil {
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(covers)
|
|
return
|
|
}
|
|
}
|
|
if index, ok := s.fPhash[phash]; ok {
|
|
covers, err := json.Marshal(s.covers[index])
|
|
if err == nil {
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(covers)
|
|
return
|
|
}
|
|
}
|
|
w.Header().Add("Content-Type", "application/json")
|
|
fmt.Fprintln(w, "{\"msg\":\"No hashes found\"}")
|
|
}
|
|
|
|
func (s *Server) add_cover(w http.ResponseWriter, r *http.Request) {
|
|
user, authed := s.authenticated(w, r)
|
|
if !authed || user == "" {
|
|
http.Error(w, "Invalid Auth", http.StatusForbidden)
|
|
return
|
|
}
|
|
var (
|
|
values = r.URL.Query()
|
|
domain = strings.TrimSpace(values.Get("domain"))
|
|
id = strings.TrimSpace(values.Get("id"))
|
|
)
|
|
if id == "" {
|
|
log.Println("No ID Provided")
|
|
http.Error(w, "No ID Provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if domain == "" {
|
|
log.Println("No domain Provided")
|
|
http.Error(w, "No domain Provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
im, format, err := image.Decode(r.Body)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("Failed to decode Image: %s", err)
|
|
log.Println(msg)
|
|
http.Error(w, msg, http.StatusBadRequest)
|
|
return
|
|
}
|
|
log.Printf("Decoded %s image from %s", format, user)
|
|
im = &goimagehash.YCbCr{YCbCr: im.(*image.YCbCr)}
|
|
i := imaging.Resize(im, 9, 8, imaging.Linear)
|
|
bmp.Encode(w, i)
|
|
fmt.Println(im.Bounds())
|
|
|
|
var (
|
|
ahash *goimagehash.ImageHash
|
|
dhash *goimagehash.ImageHash
|
|
phash *goimagehash.ImageHash
|
|
)
|
|
|
|
ahash, err = goimagehash.AverageHash(im)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("Failed to ahash Image: %s", err)
|
|
log.Println(msg)
|
|
http.Error(w, msg, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
dhash, err = goimagehash.DifferenceHash(im)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("Failed to dhash Image: %s", err)
|
|
log.Println(msg)
|
|
http.Error(w, msg, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
phash, err = goimagehash.PerceptionHash(im)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("Failed to phash Image: %s", err)
|
|
log.Println(msg)
|
|
http.Error(w, msg, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
fmt.Printf("%#064b\n", ahash.GetHash())
|
|
fmt.Printf("%#064b\n", dhash.GetHash())
|
|
fmt.Printf("%#064b\n", phash.GetHash())
|
|
|
|
s.covers = append(s.covers, make(Cover))
|
|
|
|
s.covers[len(s.covers)-1][domain] = append(s.covers[len(s.covers)-1][domain], id)
|
|
|
|
s.IDToCover[domain+":"+id] = uint32(len(s.covers) - 1)
|
|
|
|
s.mapHashes(uint32(len(s.covers)-1), ahash, dhash, phash)
|
|
}
|
|
|
|
func (s *Server) mapHashes(index uint32, ahash, dhash, phash *goimagehash.ImageHash) {
|
|
s.fAhash[ahash.GetHash()] = index
|
|
s.fDhash[dhash.GetHash()] = index
|
|
s.fPhash[phash.GetHash()] = index
|
|
for i, partial_hash := range SplitHash(ahash.GetHash()) {
|
|
s.ahash[i][partial_hash] = index
|
|
}
|
|
for i, partial_hash := range SplitHash(dhash.GetHash()) {
|
|
s.dhash[i][partial_hash] = index
|
|
}
|
|
for i, partial_hash := range SplitHash(phash.GetHash()) {
|
|
s.phash[i][partial_hash] = index
|
|
}
|
|
}
|
|
|
|
func (s *Server) initHashes() {
|
|
for i := range s.ahash {
|
|
s.ahash[i] = make(map[uint8]uint32)
|
|
}
|
|
for i := range s.dhash {
|
|
s.dhash[i] = make(map[uint8]uint32)
|
|
}
|
|
for i := range s.phash {
|
|
s.phash[i] = make(map[uint8]uint32)
|
|
}
|
|
s.fAhash = make(map[uint64]uint32)
|
|
s.fDhash = make(map[uint64]uint32)
|
|
s.fPhash = make(map[uint64]uint32)
|
|
s.IDToCover = make(map[string]uint32)
|
|
}
|
|
|
|
func SplitHash(hash uint64) [8]uint8 {
|
|
return [8]uint8{
|
|
uint8((hash & h_8) >> shift_8),
|
|
uint8((hash & h_7) >> shift_7),
|
|
uint8((hash & h_6) >> shift_6),
|
|
uint8((hash & h_5) >> shift_5),
|
|
uint8((hash & h_4) >> shift_4),
|
|
uint8((hash & h_3) >> shift_3),
|
|
uint8((hash & h_2) >> shift_2),
|
|
uint8((hash & h_1) >> shift_1),
|
|
}
|
|
}
|
|
|
|
// func (s *Server) CoverByID(id string) uint32 {
|
|
// v,ok :=s.IDToCover[id]
|
|
// return 0
|
|
// }
|
|
func (s *Server) FindHashes() {
|
|
}
|
|
|
|
func startServer() {
|
|
mux := http.NewServeMux()
|
|
server := Server{
|
|
token: make(chan *oidc.Tokens),
|
|
mux: mux,
|
|
httpServer: &http.Server{
|
|
Addr: ":8080",
|
|
Handler: mux,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
MaxHeaderBytes: 1 << 20,
|
|
},
|
|
}
|
|
server.initHashes()
|
|
// server.setupOauthHandlers()
|
|
server.setupAppHandlers()
|
|
err := server.httpServer.ListenAndServe()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|