commit ce71c2f66bcd76eb68e5b603722b12a132453adc Author: Timmy Welch Date: Wed May 1 18:09:02 2024 -0700 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0129f76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +*.html + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +*~ +*.pyc +env +*.jpg +build +dist +ImageHash.egg-info/ +.eggs +.DS_Store +.python-version +.coverage + +tmp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7227991 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + args: [--markdown-linebreak-ext=.gitignore] + - id: end-of-file-fixer + - id: check-yaml +- repo: https://github.com/tekwizely/pre-commit-golang + rev: v1.0.0-rc.1 + hooks: + - id: go-mod-tidy + - id: go-imports + args: [-w] +- repo: https://github.com/golangci/golangci-lint + rev: v1.53.3 + hooks: + - id: golangci-lint diff --git a/cmd/hash/main.go b/cmd/hash/main.go new file mode 100644 index 0000000..174772a --- /dev/null +++ b/cmd/hash/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "flag" + "fmt" + "image" + "image/draw" + _ "image/gif" + _ "image/jpeg" + "image/png" + "log" + "os" + "strings" + + _ "github.com/spakin/netpbm" + + "gitea.narnian.us/lordwelch/goimagehash" + "github.com/anthonynsimon/bild/transform" + _ "github.com/gen2brain/avif" + _ "golang.org/x/image/bmp" + _ "golang.org/x/image/tiff" + _ "golang.org/x/image/webp" +) + +func fmtImage(im image.Image) string { + gray, ok := im.(*image.Gray) + str := &strings.Builder{} + + for y := 0; y < im.Bounds().Dy(); y++ { + str.WriteString("[ ") + for x := 0; x < im.Bounds().Dx(); x++ { + if ok { + fmt.Fprintf(str, "%03d, ", gray.GrayAt(x, y).Y) + } else { + col := im.At(x, y) + r, g, b, _ := col.RGBA() + if uint8(r) == 0x0015 && uint8(g) == 0x0013 && uint8(b) == 0x0012 { + fmt.Fprintf(os.Stderr, "RGB: { %04x, %04x, %04x }\n", uint8(r), uint8(g), uint8(b)) + } + fmt.Fprintf(str, "{ %04x, %04x, %04x }, ", uint8(r), uint8(g), uint8(b)) + } + } + str.WriteString("]\n") + } + return str.String() +} + +func debugImage(im image.Image, width, height int) { + // gray := image.NewGray(image.Rect(0, 0, im.Bounds().Dx(), im.Bounds().Dy())) + // gray.Pix = transforms.Rgb2Gray(im) + // i_resize := imaging.Resize(im, width, height, imaging.Linear) + resized := transform.Resize(im, 8, 8, transform.Lanczos) + r_gray := image.NewGray(image.Rect(0, 0, resized.Bounds().Dx(), resized.Bounds().Dy())) + draw.Draw(r_gray, resized.Bounds(), resized, resized.Bounds().Min, draw.Src) + + // fmt.Fprintln(os.Stderr, "rgb") + // fmt.Println(fmtImage(im)) + // fmt.Fprintln(os.Stderr, "grayscale") + // fmt.Println(fmtImage(gray)) + // fmt.Println("resized") + fmt.Println(fmtImage(r_gray)) +} + +func main() { + imPath := flag.String("file", "", "image file to hash") + flag.Parse() + if imPath == nil || *imPath == "" { + flag.Usage() + os.Exit(1) + } + + file, err := os.Open(*imPath) + if err != nil { + log.Printf("Failed to open file %s: %s", *imPath, err) + return + } + defer file.Close() + im, format, err := image.Decode(file) + if err != nil { + msg := fmt.Sprintf("Failed to decode Image: %s", err) + log.Println(msg) + return + } + + if format == "webp" { + im = goimagehash.FancyUpscale(im.(*image.YCbCr)) + } + + 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) + return + } + dhash, err = goimagehash.DifferenceHash(im) + if err != nil { + msg := fmt.Sprintf("Failed to dhash Image: %s", err) + log.Println(msg) + return + } + phash, err = goimagehash.PerceptionHash(im) + if err != nil { + msg := fmt.Sprintf("Failed to phash Image: %s", err) + log.Println(msg) + return + } + gray := goimagehash.ToGray(im) + file2, err := os.Create("tmp.png") + if err != nil { + log.Printf("Failed to open file %s: %s", "tmp.png", err) + return + } + err = png.Encode(file2, gray) + if err != nil { + panic(err) + } + file2.Close() + debugImage(gray, 9, 8) + + fmt.Fprintf(os.Stderr, "ahash: %s\n", ahash.String()) + fmt.Fprintf(os.Stderr, "dhash: %s\n", dhash.String()) + fmt.Fprintf(os.Stderr, "phash: %s\n", phash.String()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6f1bcbd --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module gitea.narnian.us/lordwelch/image-hasher + +go 1.21 + +toolchain go1.22.0 + +require ( + gitea.narnian.us/lordwelch/goimagehash v0.0.0-20240502010648-cb5a8237c420 + github.com/anthonynsimon/bild v0.13.0 + github.com/corona10/goimagehash v1.1.0 + github.com/gen2brain/avif v0.3.1 + github.com/google/uuid v1.3.0 + github.com/spakin/netpbm v1.3.0 + github.com/zitadel/oidc v1.13.4 + golang.org/x/image v0.7.0 +) + +require ( + github.com/ebitengine/purego v0.7.1 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/tetratelabs/wazero v1.7.1 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/sys v0.19.0 // indirect +) + +require ( + github.com/disintegration/imaging v1.6.2 + github.com/golang/protobuf v1.5.3 // indirect + github.com/gorilla/schema v1.2.0 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e79ccdd --- /dev/null +++ b/go.sum @@ -0,0 +1,136 @@ +gitea.narnian.us/lordwelch/goimagehash v0.0.0-20240502010648-cb5a8237c420 h1:yOLLICl64x5lMeYYhUABETfsd4ZO0tQjBSVfVLKbuz8= +gitea.narnian.us/lordwelch/goimagehash v0.0.0-20240502010648-cb5a8237c420/go.mod h1:usqHLOGYaIIBV579DJAlZapMEUImOdzleurWyeahfDI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8= +github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI= +github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= +github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gen2brain/avif v0.3.1 h1:womS2LKvhS/dSR3zIKUxtJW+riGlY48akGWqc+YgHtE= +github.com/gen2brain/avif v0.3.1/go.mod h1:s9sI2zo2cF6EdyRVCtnIfwL/Qb3k0TkOIEsz6ovK1ms= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= +github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= +github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spakin/netpbm v1.3.0 h1:eDX7VvrkN5sHXW0luZXRA4AKDlLmu0E5sNxJ7VSTwxc= +github.com/spakin/netpbm v1.3.0/go.mod h1:Q+ep6vNv1G44qSWp0wt3Y9o1m/QXjmaXZIFC0PMVpq0= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tetratelabs/wazero v1.7.1 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8= +github.com/tetratelabs/wazero v1.7.1/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM= +github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= +github.com/zitadel/oidc v1.13.4 h1:+k2GKqP9Ld9S2MSFlj+KaNsoZ3J9oy+Ezw51EzSFuC8= +github.com/zitadel/oidc v1.13.4/go.mod h1:3h2DhUcP02YV6q/CA/BG4yla0o6rXjK+DkJGK/dwJfw= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= +golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..9cadd9d --- /dev/null +++ b/main.go @@ -0,0 +1,377 @@ +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) + } +} diff --git a/privdrop.go b/privdrop.go new file mode 100644 index 0000000..9d606fe --- /dev/null +++ b/privdrop.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "syscall" + "unsafe" +) + +type capHeader struct { + version uint32 + pid int +} + +type capData struct { + effective uint32 + permitted uint32 + inheritable uint32 +} + +type caps struct { + hdr capHeader + data [2]capData +} + +func getCaps() (caps, error) { + var c caps + + // Get capability version + if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&c.hdr)), uintptr(unsafe.Pointer(nil)), 0); errno != 0 { + return c, fmt.Errorf("SYS_CAPGET: %v", errno) + } + + // Get current capabilities + if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&c.hdr)), uintptr(unsafe.Pointer(&c.data[0])), 0); errno != 0 { + return c, fmt.Errorf("SYS_CAPGET: %v", errno) + } + + return c, nil +} + +// mustDropPrivileges executes the program in a child process, dropping root +// privileges, but retaining the CAP_SYS_TIME capability to change the system +// clock. +func mustDropPrivileges() { + if os.Getenv("PRIVILEGES_DROPPED") == "1" { + return + } + + // From include/uapi/linux/capability.h: + // Allow setting the real-time clock + const CAP_SYS_TIME = 25 + + caps, err := getCaps() + if err != nil { + log.Fatal(err) + } + + // Add CAP_SYS_TIME to the permitted and inheritable capability mask, + // otherwise we will not be able to add it to the ambient capability mask. + caps.data[0].permitted |= 1 << uint(CAP_SYS_TIME) + caps.data[0].inheritable |= 1 << uint(CAP_SYS_TIME) + + if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&caps.hdr)), uintptr(unsafe.Pointer(&caps.data[0])), 0); errno != 0 { + log.Fatalf("SYS_CAPSET: %v", errno) + } + + cmd := exec.Command(os.Args[0]) + cmd.Env = append(os.Environ(), "PRIVILEGES_DROPPED=1") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: 65534, + Gid: 65534, + }, + AmbientCaps: []uintptr{CAP_SYS_TIME}, + } + log.Fatal(cmd.Run()) +}