diff --git a/.gitignore b/.gitignore index aaadf73..bde3ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.dll *.so *.dylib +**.db # Test binary, built with `go test -c` *.test @@ -29,4 +30,4 @@ go.work.sum # Editor/IDE # .idea/ -# .vscode/ +.vscode/ diff --git a/app/backend/binary.go b/app/backend/binary.go new file mode 100644 index 0000000..b110003 --- /dev/null +++ b/app/backend/binary.go @@ -0,0 +1,114 @@ +package backend + +import ( + "binaryserver/app/db" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/launcher" +) + +var browser *rod.Browser + +func asciiToBinary(text string) string { + var result []string + for _, char := range text { + binary := fmt.Sprintf("%08b", char) // 8-bit binary format + result = append(result, binary) + } + return strings.Join(result, " ") +} + +func binaryToASCII(binary string) (string, error) { + // Ensure the length of the binary string is a multiple of 8 + if len(binary)%8 != 0 { + return "", fmt.Errorf("binary string length must be a multiple of 8") + } + + var sb strings.Builder + for i := 0; i < len(binary); i += 8 { + byteStr := binary[i : i+8] + b, err := strconv.ParseUint(byteStr, 2, 8) + if err != nil { + return "", fmt.Errorf("invalid binary sequence at position %d: %v", i, err) + } + sb.WriteByte(byte(b)) + } + return sb.String(), nil +} + +func instagramHandleExists(username string, bypass ...bool) string { + records, _ := db.GetRecords(db.QueryOptions{Where: fmt.Sprintf(`handle='%v'`, username)}) + if len(records) > 0 && len(bypass) == 0 { + return records[0].URL + } + url := fmt.Sprintf("https://www.instagram.com/%s/", username) + + page := browser.MustPage(url) + + // Wait until body is loaded + page.MustWaitLoad().MustWaitIdle() + + body := page.MustElement("body").MustText() + + record := db.NewRecord(username, url) + + // Instagram shows this for non-existent users + if strings.Contains(body, "Sorry, this page isn't available") { + record.URL = "" + db.SaveRecord(record) + return "" + } + db.SaveRecord(record) + return url +} + +func BinaryHandler(w http.ResponseWriter, req *http.Request) { + binaryInput := strings.ReplaceAll(req.URL.Query().Get("binary"), " ", "") + var text string + var handle string + var err error + if binaryInput == "" { + asciiInput := req.URL.Query().Get("text") + if asciiInput == "" { + fmt.Fprintf(w, "Could not encode or decode!\n") + return + } + text = asciiToBinary(asciiInput) + fmt.Fprintf(w, "%v\n", GetFancyResponse(`

`+text+`

`, "")) + return + } else { + text, err = binaryToASCII(binaryInput) + text = strings.ReplaceAll(text, "@", "") + handle = text + } + + if err != nil { + fmt.Fprintf(w, "Could not decode!\n") + return + } + + var url string + if url = instagramHandleExists(text); url != "" { + text = `
` + text + `
` + } else { + text = `

` + text + `

` + } + if url == "" { + handle = "" + } + + fmt.Fprintf(w, "%v\n", GetFancyResponse(text, handle)) +} + +func init() { + path, _ := launcher.New(). + Headless(true). + // Uncomment below to see browser + // Set("headless", "false"). + Launch() + browser = rod.New().ControlURL(path).MustConnect() +} diff --git a/app/backend/page.go b/app/backend/page.go new file mode 100644 index 0000000..17ca29b --- /dev/null +++ b/app/backend/page.go @@ -0,0 +1,51 @@ +package backend + +func GetFancyResponse(input, handle string) string { + var hasHandle string + if handle != "" { + hasHandle = `Report Broken Link` + } + return ` + + + + + + Decoded Binary + + + +
+ ` + input + ` + ` + hasHandle + ` +
+ + + ` +} diff --git a/app/backend/report.go b/app/backend/report.go new file mode 100644 index 0000000..bac9f84 --- /dev/null +++ b/app/backend/report.go @@ -0,0 +1,20 @@ +package backend + +import ( + "binaryserver/app/db" + "fmt" + "net/http" +) + +func ReportHandler(w http.ResponseWriter, req *http.Request) { + handle := req.URL.Query().Get("handle") + records, _ := db.GetRecords(db.QueryOptions{Where: fmt.Sprintf(`handle='%v'`, handle)}) + if len(records) > 0 { + if result := instagramHandleExists(records[0].Handle, true); result == "" && records[0].URL != "" { + fmt.Fprintf(w, "Link does seem to be bad. Fixing...\n") + db.DeleteRecord(records[0]) + } else { + fmt.Fprintf(w, "Link seems to be active and working!\n") + } + } +} diff --git a/app/db/db.go b/app/db/db.go new file mode 100644 index 0000000..073467d --- /dev/null +++ b/app/db/db.go @@ -0,0 +1,39 @@ +package db + +import ( + "database/sql" + + _ "modernc.org/sqlite" +) + +var conn *sql.DB + +func HandleConn(testing ...bool) *sql.DB { + if len(testing) > 0 { + conn, _ = sql.Open("sqlite", "file::memory:?cache=shared") + buildSchema() + return conn + } + if conn == nil { + conn, _ = sql.Open("sqlite", "records.db") + buildSchema() + return conn + } + return conn +} + +func buildSchema() error { + c := HandleConn() + _, err := c.Exec(` + +CREATE TABLE IF NOT EXISTS records +( + UUID TEXT NOT NULL, + Handle TEXT NOT NULL UNIQUE, + URL TEXT NOT NULL, + PRIMARY KEY (UUID) +); + +`) + return err +} diff --git a/app/db/record.go b/app/db/record.go new file mode 100644 index 0000000..3744bdc --- /dev/null +++ b/app/db/record.go @@ -0,0 +1,77 @@ +package db + +import ( + "binaryserver/app/utils" + + "github.com/google/uuid" +) + +type Record struct { + UUID uuid.UUID `json:"uuid"` + Handle string `json:"handle"` + URL string `json:"url"` +} + +func NewRecord(handle, url string) Record { + return Record{ + UUID: utils.GetUUID(), + Handle: handle, + URL: url, + } +} + +func SaveRecord(record Record) error { + conn := HandleConn() + stmt, err := conn.Prepare(`INSERT INTO records + VALUES(?, ?, ?)`) + if err != nil { + return err + } + _, err = stmt.Exec(record.UUID.String(), record.Handle, record.URL) + if err != nil { + return err + } + defer stmt.Close() + return nil +} + +func GetRecords(opts QueryOptions) ([]Record, error) { + conn := HandleConn() + var records []Record + stmt, err := opts.Build(conn, "records") + if err != nil { + return nil, err + } + row, err := stmt.Query() + stmt.Close() + if err != nil { + return nil, err + } + defer row.Close() + for { + if err != nil { + return nil, err + } + if !row.Next() { + break + } + record := Record{} + row.Scan(&record.UUID, &record.Handle, &record.URL) + records = append(records, record) + } + return records, nil +} + +func DeleteRecord(record Record) error { + stmt, err := conn.Prepare(`DELETE FROM records + WHERE UUID = ?`) + if err != nil { + return err + } + defer stmt.Close() + _, err = stmt.Exec(record.UUID.String()) + if err != nil { + return err + } + return nil +} diff --git a/app/db/record_test.go b/app/db/record_test.go new file mode 100644 index 0000000..66e140d --- /dev/null +++ b/app/db/record_test.go @@ -0,0 +1,18 @@ +package db + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRecords(t *testing.T) { + HandleConn(true) + rec := NewRecord("test", "https://instagram.com/handle") + err := SaveRecord(rec) + assert.Nil(t, err) + records, err := GetRecords(QueryOptions{Where: `handle='test'`}) + assert.Nil(t, err) + assert.Greater(t, len(records), 0) + assert.Equal(t, "https://instagram.com/handle", records[0].URL) +} diff --git a/app/db/types.go b/app/db/types.go new file mode 100644 index 0000000..4bffcef --- /dev/null +++ b/app/db/types.go @@ -0,0 +1,50 @@ +package db + +import ( + "database/sql" + "fmt" + "strings" +) + +type QueryOptions struct { + Select []string + Where string + OrderBy string + Limit uint + Offset uint +} + +func (o *QueryOptions) Build(conn *sql.DB, table string) (*sql.Stmt, error) { + sel := "*" + where := "" + orderby := "" + limit := "" + if o.Select != nil { + sel = strings.Join(o.Select, ", ") + } + if o.Where != "" { + where = fmt.Sprintf( + `WHERE +%v`, o.Where) + } + if o.OrderBy != "" { + orderby = fmt.Sprintf( + `ORDER BY + %v`, o.OrderBy) + } + if o.Limit != 0 && o.Offset != 0 { + limit = fmt.Sprintf( + `LIMIT %v OFFSET %v;`, o.Limit, o.Offset) + } else if o.Limit != 0 { + limit = fmt.Sprintf( + `LIMIT %v`, o.Limit) + } + return conn.Prepare(fmt.Sprintf( + `SELECT + %v +FROM + %v +%v +%v +%v;`, sel, table, where, orderby, limit)) +} diff --git a/app/utils/utils.go b/app/utils/utils.go new file mode 100644 index 0000000..e428057 --- /dev/null +++ b/app/utils/utils.go @@ -0,0 +1,12 @@ +package utils + +import "github.com/google/uuid" + +func GetUUID() uuid.UUID { + uuid.SetClockSequence(uuid.ClockSequence()) + if uid, err := uuid.NewV7(); err == nil { + return uid + } + uid, _ := uuid.NewUUID() + return uid +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aa25c20 --- /dev/null +++ b/go.mod @@ -0,0 +1,31 @@ +module binaryserver + +go 1.24.2 + +require ( + github.com/go-rod/rod v0.116.2 + github.com/google/uuid v1.6.0 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/stretchr/testify v1.10.0 + modernc.org/sqlite v1.38.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/ysmood/fetchup v0.2.3 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.9.0 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/sys v0.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.65.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0b07da1 --- /dev/null +++ b/go.sum @@ -0,0 +1,75 @@ +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= +modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA= +modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc= +modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= +modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..db663c0 --- /dev/null +++ b/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "binaryserver/app/backend" + "net/http" +) + +func main() { + http.HandleFunc("/report", backend.ReportHandler) + http.HandleFunc("/", backend.BinaryHandler) + http.ListenAndServe(":8080", nil) +}