commit 0ddeff0c905644b0be92f9a7598a7da44febc7db Author: Bhasher Date: Fri Dec 22 21:29:33 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1dffd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.sqlite diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..019af81 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +# Dockerfile +FROM golang:latest + +WORKDIR /app + +COPY . . + +RUN go build -o main . + +CMD ["/app/main"] diff --git a/create.html b/create.html new file mode 100644 index 0000000..618b247 --- /dev/null +++ b/create.html @@ -0,0 +1,17 @@ + +
+

Your Tracking Pixel Link

+ + +
+ + + \ No newline at end of file diff --git a/dashboard.html b/dashboard.html new file mode 100644 index 0000000..39d412b --- /dev/null +++ b/dashboard.html @@ -0,0 +1,28 @@ + + + + + Dashboard + + + +

Tracking Pixel Dashboard

+ + + + + + + + {{range .}} + + + + + + + {{end}} +
TitleTotal ViewsUnique ViewsLink
{{.Title}}{{.TotalViews}}{{.UniqueViews}}link
+ + + \ No newline at end of file diff --git a/db.go b/db.go new file mode 100644 index 0000000..64ced72 --- /dev/null +++ b/db.go @@ -0,0 +1,66 @@ +package main + +import ( + "database/sql" + "log" +) + +var db *sql.DB + +func initDB(filepath string) *sql.DB { + db, err := sql.Open("sqlite3", filepath) + if err != nil { + log.Fatal(err) + } + + // Create tables if they do not exist + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS pixels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT UNIQUE, + title TEXT + ); + CREATE TABLE IF NOT EXISTS stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pixel_id INTEGER, + view_time DATETIME DEFAULT CURRENT_TIMESTAMP, + ip TEXT, + user_agent TEXT, + fingerprint TEXT, + FOREIGN KEY(pixel_id) REFERENCES pixels(id) + ); + `) + if err != nil { + log.Fatal(err) + } + + return db +} + +func addStat(pixelID int, ip, userAgent string, fingerprint string) error { + statement, err := db.Prepare("INSERT INTO stats (pixel_id, ip, user_agent, fingerprint) VALUES (?, ?, ?, ?)") + if err != nil { + return err + } + _, err = statement.Exec(pixelID, ip, userAgent, fingerprint) + return err +} + +func getPixelIDFromUUID(uuid string) (int, error) { + var id int + row := db.QueryRow("SELECT id FROM pixels WHERE uuid = ?", uuid) + err := row.Scan(&id) + if err != nil { + return 0, err + } + return id, nil +} + +func savePixel(title, uuid string) error { + statement, err := db.Prepare("INSERT INTO pixels (title, uuid) VALUES (?, ?)") + if err != nil { + return err + } + _, err = statement.Exec(title, uuid) + return err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5a95183 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module trakr + +go 1.21.4 + +require ( + github.com/google/uuid v1.5.0 + github.com/mattn/go-sqlite3 v1.14.19 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b1bc515 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..f34eea0 --- /dev/null +++ b/handlers.go @@ -0,0 +1,134 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "html/template" + "log" + "net/http" + "strings" + + "github.com/google/uuid" +) + +func newPixelPageHandler(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFiles("new.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = tmpl.Execute(w, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func createPixelHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed) + return + } + + if err := r.ParseForm(); err != nil { + http.Error(w, "Error parsing form", http.StatusInternalServerError) + return + } + + title := r.FormValue("title") + + pixelUUID := uuid.New().String() + + err := savePixel(title, pixelUUID) + if err != nil { + http.Error(w, "Error saving pixel to the database", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) +} + +func pixelHandler(w http.ResponseWriter, r *http.Request) { + pathParts := strings.Split(r.URL.Path, "/") + if len(pathParts) < 3 { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + uuid := pathParts[2] + + // Get the pixel ID from the database + pixelID, err := getPixelIDFromUUID(uuid) + if err != nil { + log.Println("Error getting pixel ID:", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + addr := strings.Split(r.RemoteAddr, ":") + + rawFingerprint := fmt.Sprintf( + "%s|%s|%s", + addr[0], + r.Header.Get("User-Agent"), + r.Header.Get("Accept-Language"), + ) + hash := sha256.Sum256([]byte(rawFingerprint)) + fingerprint := string(hash[:]) + + err = addStat(pixelID, addr[0], r.UserAgent(), fingerprint) + if err != nil { + log.Println("Error updating stats:", err) + } + + // Serve the pixel image + w.Header().Set("Content-Type", "image/gif") + w.Write(onePixelGIF) +} + +func dashboardHandler(w http.ResponseWriter, r *http.Request) { + query := ` + SELECT p.title, p.uuid, COUNT(s.id) AS total_views, COUNT(DISTINCT s.ip) AS unique_views + FROM pixels p + LEFT JOIN stats s ON p.id = s.pixel_id + GROUP BY p.id;` + + rows, err := db.Query(query) + if err != nil { + http.Error(w, "Error fetching data", http.StatusInternalServerError) + log.Println("Error fetching data:", err) + return + } + defer rows.Close() + + var stats []PixelStats + for rows.Next() { + var s PixelStats + if err := rows.Scan(&s.Title, &s.UUID, &s.TotalViews, &s.UniqueViews); err != nil { + http.Error(w, "Error reading data", http.StatusInternalServerError) + log.Println("Error reading data:", err) + return + } + stats = append(stats, s) + } + + // Handle any errors encountered during iteration + if err = rows.Err(); err != nil { + http.Error(w, "Error iterating data", http.StatusInternalServerError) + log.Println("Error iterating data:", err) + return + } + + // Render the template + tmpl, err := template.ParseFiles("dashboard.html") + if err != nil { + http.Error(w, "Error loading template", http.StatusInternalServerError) + return + } + + err = tmpl.Execute(w, stats) + if err != nil { + http.Error(w, "Error executing template", http.StatusInternalServerError) + return + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2f2a0fb --- /dev/null +++ b/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + _ "github.com/mattn/go-sqlite3" +) + +var onePixelGIF = []byte{ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, + 0x01, 0x00, 0x80, 0xff, 0x00, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, + 0x01, 0x00, 0x3b, +} + +func main() { + db = initDB("data.sqlite") + + http.HandleFunc("/new", newPixelPageHandler) + http.HandleFunc("/create-pixel", createPixelHandler) + http.HandleFunc("/p/", pixelHandler) + http.HandleFunc("/dashboard", dashboardHandler) + + addr := ":8080" + + fmt.Printf("Server is up and running on http://localhost%s\n", addr) + + log.Fatal(http.ListenAndServe(addr, nil)) +} + +type PixelStats struct { + Title string + TotalViews int + UniqueViews int + UUID string +} diff --git a/new.html b/new.html new file mode 100644 index 0000000..0fd30b4 --- /dev/null +++ b/new.html @@ -0,0 +1,63 @@ + + + + + + Create New Tracking Pixel + + + + +
+

Create New Tracking Pixel

+
+ + +
+
+ + + \ No newline at end of file