From ef9544ff7daad3f9b338644cb09d2c0c9065a469 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 25 Oct 2023 00:14:32 -0400 Subject: [PATCH] Beeg refactor for database and users and auth --- .gitignore | 2 + cmd/cli.go | 109 +++++++++ cmd/willow.go | 181 ++++++++++++++ db/db.go | 51 ++++ db/project.go | 83 +++++++ db/release.go | 62 +++++ db/sql/schema.sql | 46 ++++ db/users.go | 84 +++++++ git.go => git/git.go | 75 ++++-- go.mod | 39 ++- go.sum | 98 +++++--- justfile | 35 ++- main.go | 228 ------------------ project/project.go | 201 +++++++++++++++ releases.go | 85 ------- rss.go => rss/rss.go | 29 ++- users/users.go | 92 +++++++ {static => ws/static}/home.html | 0 {static => ws/static}/home.html.license | 0 ws/static/login.html | 30 +++ .../static/login.html.license | 0 {static => ws/static}/new.html | 0 .../static/new.html.license | 0 {static => ws/static}/select-release.html | 0 ws/static/select-release.html.license | 3 + ws.go => ws/ws.go | 61 +++-- 26 files changed, 1186 insertions(+), 408 deletions(-) create mode 100644 cmd/cli.go create mode 100644 cmd/willow.go create mode 100644 db/db.go create mode 100644 db/project.go create mode 100644 db/release.go create mode 100644 db/sql/schema.sql create mode 100644 db/users.go rename git.go => git/git.go (64%) delete mode 100644 main.go create mode 100644 project/project.go delete mode 100644 releases.go rename rss.go => rss/rss.go (55%) create mode 100644 users/users.go rename {static => ws/static}/home.html (100%) rename {static => ws/static}/home.html.license (100%) create mode 100644 ws/static/login.html rename static/new.html.license => ws/static/login.html.license (100%) rename {static => ws/static}/new.html (100%) rename static/select-release.html.license => ws/static/new.html.license (100%) rename {static => ws/static}/select-release.html (100%) create mode 100644 ws/static/select-release.html.license rename ws.go => ws/ws.go (72%) diff --git a/.gitignore b/.gitignore index 2306181..dd5b9c4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,7 @@ /willow /*.csv /data/ +/*.sqlite +/config.toml /.idea/ diff --git a/cmd/cli.go b/cmd/cli.go new file mode 100644 index 0000000..a8a7895 --- /dev/null +++ b/cmd/cli.go @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "database/sql" + "fmt" + "os" + "syscall" + + "git.sr.ht/~amolith/willow/users" + "golang.org/x/term" +) + +// createUser is a CLI that creates a new user with the specified username +func createUser(dbConn *sql.DB, username string) { + fmt.Println("Creating user", username) + + fmt.Print("Enter password: ") + password, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + fmt.Println("Error reading password:", err) + os.Exit(1) + } + fmt.Println() + + fmt.Print("Confirm password: ") + passwordConfirmation, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + fmt.Println("Error reading password confirmation:", err) + os.Exit(1) + } + fmt.Println() + + if string(password) != string(passwordConfirmation) { + fmt.Println("Passwords do not match") + os.Exit(1) + } + err = users.Register(dbConn, username, string(password)) + if err != nil { + fmt.Println("Error creating user:", err) + os.Exit(1) + } + + fmt.Println("\nUser", username, "created successfully") + os.Exit(0) +} + +// deleteUser is a CLI that deletes a user with the specified username +func deleteUser(dbConn *sql.DB, username string) { + fmt.Println("Deleting user", username) + err := users.Delete(dbConn, username) + if err != nil { + fmt.Println("Error deleting user:", err) + os.Exit(1) + } + + fmt.Printf("User %s deleted successfully\n", username) + os.Exit(0) +} + +// listUsers is a CLI that lists all users in the database +func listUsers(dbConn *sql.DB) { + fmt.Println("Listing all users") + + dbUsers, err := users.GetUsers(dbConn) + if err != nil { + fmt.Println("Error retrieving users from the database:", err) + os.Exit(1) + } + + if len(dbUsers) == 0 { + fmt.Println("- No users found") + } else { + for _, u := range dbUsers { + fmt.Println("-", u) + } + } + os.Exit(0) +} + +// checkAuthorised is a CLI that checks whether the provided user/password +// combo is authorised. +func checkAuthorised(dbConn *sql.DB, username string) { + fmt.Printf("Checking whether password for user %s is correct\n", username) + + fmt.Print("Enter password: ") + password, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + fmt.Println("Error reading password:", err) + os.Exit(1) + } + fmt.Println() + + authorised, err := users.Authorised(dbConn, username, string(password)) + if err != nil { + fmt.Println("Error checking authorisation:", err) + os.Exit(1) + } + + if authorised { + fmt.Println("User is authorised") + } else { + fmt.Println("User is not authorised") + } + os.Exit(0) +} diff --git a/cmd/willow.go b/cmd/willow.go new file mode 100644 index 0000000..cdf414a --- /dev/null +++ b/cmd/willow.go @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + "log" + "net/http" + "os" + "sync" + + "git.sr.ht/~amolith/willow/db" + "git.sr.ht/~amolith/willow/project" + "git.sr.ht/~amolith/willow/ws" + + "github.com/BurntSushi/toml" + flag "github.com/spf13/pflag" +) + +type ( + Config struct { + Server server + CSVLocation string + DBConn string + // TODO: Make cache location configurable + // CacheLocation string + FetchInterval int + } + + server struct { + Listen string + } +) + +var ( + flagConfig = flag.StringP("config", "c", "config.toml", "Path to config file") + flagAddUser = flag.StringP("add", "a", "", "Username of account to add") + flagDeleteUser = flag.StringP("deleteuser", "d", "", "Username of account to delete") + flagCheckAuthorised = flag.StringP("validatecredentials", "v", "", "Username of account to check") + flagListUsers = flag.BoolP("listusers", "l", false, "List all users") + config Config + req = make(chan struct{}) + res = make(chan []project.Project) + manualRefresh = make(chan struct{}) +) + +//goland:noinspection GoUnusedFunction +func main() { + flag.Parse() + + err := checkConfig() + if err != nil { + log.Fatalln(err) + } + + fmt.Println("Opening database at", config.DBConn) + + dbConn, err := db.Open(config.DBConn) + if err != nil { + fmt.Println("Error opening database:", err) + os.Exit(1) + } + + fmt.Println("Verifying database schema") + err = db.VerifySchema(dbConn) + if err != nil { + fmt.Println("Error verifying database schema:", err) + fmt.Println("Attempting to load schema") + err = db.LoadSchema(dbConn) + if err != nil { + fmt.Println("Error loading schema:", err) + os.Exit(1) + } + } + fmt.Println("Database schema verified") + + if len(*flagAddUser) > 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 { + createUser(dbConn, *flagAddUser) + os.Exit(0) + } else if len(*flagAddUser) == 0 && len(*flagDeleteUser) > 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 { + deleteUser(dbConn, *flagDeleteUser) + os.Exit(0) + } else if len(*flagAddUser) == 0 && len(*flagDeleteUser) == 0 && *flagListUsers && len(*flagCheckAuthorised) == 0 { + listUsers(dbConn) + os.Exit(0) + } else if len(*flagAddUser) == 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) > 0 { + checkAuthorised(dbConn, *flagCheckAuthorised) + os.Exit(0) + } + + fmt.Println("Starting refresh loop") + go project.RefreshLoop(dbConn, config.FetchInterval, &manualRefresh, &req, &res) + + var mutex sync.Mutex + + wsHandler := ws.Handler{ + DbConn: dbConn, + Mutex: &mutex, + Req: &req, + Res: &res, + ManualRefresh: &manualRefresh, + } + + mux := http.NewServeMux() + mux.HandleFunc("/", wsHandler.RootHandler) + mux.HandleFunc("/static", ws.StaticHandler) + mux.HandleFunc("/new", wsHandler.NewHandler) + mux.HandleFunc("/login", wsHandler.LoginHandler) + + httpServer := &http.Server{ + Addr: config.Server.Listen, + Handler: mux, + } + + fmt.Println("Starting web server on", config.Server.Listen) + if err := httpServer.ListenAndServe(); errors.Is(err, http.ErrServerClosed) { + fmt.Println("Web server closed") + os.Exit(0) + } else { + fmt.Println(err) + os.Exit(1) + } +} + +func checkConfig() error { + file, err := os.Open(*flagConfig) + if err != nil { + if os.IsNotExist(err) { + file, err = os.Create(*flagConfig) + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(`# Path to SQLite database +DBConn = "willow.sqlite" +# How often to fetch new releases in seconds +FetchInterval = 3600 + +[Server] +# Address to listen on +Listen = "127.0.0.1:1313" + `) + if err != nil { + return err + } + + fmt.Println("Config file created at", *flagConfig) + fmt.Println("Please edit it and restart the server") + os.Exit(0) + } else { + return err + } + } + defer file.Close() + + _, err = toml.DecodeFile(*flagConfig, &config) + if err != nil { + return err + } + + if config.FetchInterval < 10 { + fmt.Println("Fetch interval is set to", config.FetchInterval, "seconds, but the minimum is 10, using 10") + config.FetchInterval = 10 + } + + if config.Server.Listen == "" { + fmt.Println("No listen address specified, using 127.0.0.1:1313") + config.Server.Listen = "127.0.0.1:1313" + } + + if config.DBConn == "" { + fmt.Println("No SQLite path specified, using \"willow.sqlite\"") + config.DBConn = "willow.sqlite" + } + + return nil +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..8fc694a --- /dev/null +++ b/db/db.go @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: Apache-2.0 + +package db + +import ( + "database/sql" + "embed" + + _ "modernc.org/sqlite" +) + +// Embed the schema into the binary +// +//go:embed sql +var embeddedSQL embed.FS + +// Open opens a connection to the SQLite database +func Open(dbPath string) (*sql.DB, error) { + return sql.Open("sqlite", dbPath) +} + +func VerifySchema(dbConn *sql.DB) error { + tables := []string{ + "users", + "sessions", + "projects", + } + + for _, table := range tables { + name := "" + err := dbConn.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name) + if err != nil { + return err + } + } + return nil +} + +// LoadSchema loads the schema into the database +func LoadSchema(dbConn *sql.DB) error { + schema, err := embeddedSQL.ReadFile("sql/schema.sql") + if err != nil { + return err + } + + _, err = dbConn.Exec(string(schema)) + + return err +} diff --git a/db/project.go b/db/project.go new file mode 100644 index 0000000..dbac019 --- /dev/null +++ b/db/project.go @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: Apache-2.0 + +package db + +import "database/sql" + +// CreateProject adds a project to the database +func CreateProject(db *sql.DB, url, name, forge, running string) error { + _, err := db.Exec("INSERT INTO projects (url, name, forge, version) VALUES (?, ?, ?, ?)", url, name, forge, running) + return err +} + +// DeleteProject deletes a project from the database +func DeleteProject(db *sql.DB, url string) error { + _, err := db.Exec("DELETE FROM projects WHERE url = ?", url) + if err != nil { + return err + } + _, err = db.Exec("DELETE FROM releases WHERE project_url = ?", url) + return err +} + +// GetProject returns a project from the database +func GetProject(db *sql.DB, url string) (map[string]string, error) { + var name, forge, version string + err := db.QueryRow("SELECT name, forge, version FROM projects WHERE url = ?", url).Scan(&name, &forge, &version) + if err != nil { + return nil, err + } + project := map[string]string{ + "name": name, + "url": url, + "forge": forge, + "version": version, + } + return project, nil +} + +// UpdateProject updates an existing project in the database +func UpdateProject(db *sql.DB, url, name, forge, running string) error { + _, err := db.Exec("UPDATE projects SET name=?, forge=?, version=? WHERE url=?", name, forge, running, url) + return err +} + +// UpsertProject adds or updates a project in the database +func UpsertProject(db *sql.DB, url, name, forge, running string) error { + _, err := db.Exec(`INSERT INTO projects (url, name, forge, version) + VALUES (?, ?, ?, ?) + ON CONFLICT(url) DO + UPDATE SET + name = excluded.name, + forge = excluded.forge, + version = excluded.version;`, url, name, forge, running) + return err +} + +// GetProjects returns a list of all projects in the database +func GetProjects(db *sql.DB) ([]map[string]string, error) { + rows, err := db.Query("SELECT name, url, forge, version FROM projects") + if err != nil { + return nil, err + } + defer rows.Close() + + var projects []map[string]string + for rows.Next() { + var name, url, forge, version string + err = rows.Scan(&name, &url, &forge, &version) + if err != nil { + return nil, err + } + project := map[string]string{ + "name": name, + "url": url, + "forge": forge, + "version": version, + } + projects = append(projects, project) + } + return projects, nil +} diff --git a/db/release.go b/db/release.go new file mode 100644 index 0000000..86bf399 --- /dev/null +++ b/db/release.go @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: Apache-2.0 + +package db + +import "database/sql" + +// AddRelease adds a release for a project with a given URL to the database + +// DeleteRelease deletes a release for a project with a given URL from the database + +// UpdateRelease updates a release for a project with a given URL in the database + +// UpsertRelease adds or updates a release for a project with a given URL in the +// database +func UpsertRelease(db *sql.DB, projectURL, releaseURL, tag, content, date string) error { + _, err := db.Exec(`INSERT INTO releases (project_url, release_url, tag, content, date) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(release_url) DO + UPDATE SET + release_url = excluded.release_url, + content = excluded.content, + tag = excluded.tag, + content = excluded.content, + date = excluded.date;`, projectURL, releaseURL, tag, content, date) + return err +} + +// GetRelease returns a release for a project with a given URL from the database + +// GetReleases returns all releases for a project with a given URL from the database +func GetReleases(db *sql.DB, projectURL string) ([]map[string]string, error) { + rows, err := db.Query(`SELECT project_url, release_url, tag, content, date FROM releases WHERE project_url = ?`, projectURL) + if err != nil { + return nil, err + } + defer rows.Close() + + releases := make([]map[string]string, 0) + for rows.Next() { + var ( + projectURL string + releaseURL string + tag string + content string + date string + ) + err := rows.Scan(&projectURL, &releaseURL, &tag, &content, &date) + if err != nil { + return nil, err + } + releases = append(releases, map[string]string{ + "projectURL": projectURL, + "releaseURL": releaseURL, + "tag": tag, + "content": content, + "date": date, + }) + } + return releases, nil +} diff --git a/db/sql/schema.sql b/db/sql/schema.sql new file mode 100644 index 0000000..30ae5d1 --- /dev/null +++ b/db/sql/schema.sql @@ -0,0 +1,46 @@ +-- SPDX-FileCopyrightText: Amolith +-- +-- SPDX-License-Identifier: CC0-1.0 + +-- Create table of users with username, password hash, salt, and creation +-- timestamp +CREATE TABLE users ( + username VARCHAR(255) NOT NULL, + hash VARCHAR(255) NOT NULL, + salt VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (username) +); + +-- Create table of sessions with session GUID, username, and timestamp of when +-- the session was created +CREATE TABLE sessions ( + token VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL, + expires TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (token) +); + +-- Create table of tracked projects with URL, name, forge, running version, and +-- timestamp of when the project was added +CREATE TABLE projects ( + url VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + forge VARCHAR(255) NOT NULL, + version VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (url) +); + +-- Create table of project releases with the project URL and the release tags, +-- contents, URLs, and dates +CREATE TABLE releases ( + project_url VARCHAR(255) NOT NULL, + release_url VARCHAR(255) NOT NULL, + tag VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + date TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (release_url) +); \ No newline at end of file diff --git a/db/users.go b/db/users.go new file mode 100644 index 0000000..e687a31 --- /dev/null +++ b/db/users.go @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: Apache-2.0 + +package db + +import ( + "database/sql" + "time" +) + +// DeleteUser deletes specific user from the database and returns an error if it +// fails +func DeleteUser(db *sql.DB, user string) error { + _, err := db.Exec("DELETE FROM users WHERE username = ?", user) + return err +} + +// CreateUser creates a new user in the database and returns an error if it fails +func CreateUser(db *sql.DB, username, hash, salt string) error { + _, err := db.Exec("INSERT INTO users (username, hash, salt) VALUES (?, ?, ?)", username, hash, salt) + return err +} + +// GetUser returns a user's hash and salt from the database as strings and +// returns an error if it fails +func GetUser(db *sql.DB, username string) (string, string, error) { + var hash, salt string + err := db.QueryRow("SELECT hash, salt FROM users WHERE username = ?", username).Scan(&hash, &salt) + return hash, salt, err +} + +// GetUsers returns a list of all users in the database as a slice of strings +// and returns an error if it fails +func GetUsers(db *sql.DB) ([]string, error) { + rows, err := db.Query("SELECT username FROM users") + if err != nil { + return nil, err + } + defer rows.Close() + + var users []string + for rows.Next() { + var user string + err = rows.Scan(&user) + if err != nil { + return nil, err + } + users = append(users, user) + } + + return users, nil +} + +// GetSession accepts a session ID and returns the username associated with it +// and an error +func GetSession(db *sql.DB, session string) (string, time.Time, error) { + var username string + var expiresString string + err := db.QueryRow("SELECT username, expires FROM sessions WHERE token = ?", session).Scan(&username, &expiresString) + if err != nil { + return "", time.Time{}, err + } + + expires, err := time.Parse(time.RFC3339, expiresString) + if err != nil { + return "", time.Time{}, err + } + return username, expires, nil +} + +// InvalidateSession invalidates a session by setting the expiration date to the +// provided time. +func InvalidateSession(db *sql.DB, session string, expiry time.Time) error { + _, err := db.Exec("UPDATE sessions SET expires = ? WHERE token = ?", expiry.Format(time.RFC3339), session) + return err +} + +// CreateSession creates a new session in the database and returns an error if +// it fails +func CreateSession(db *sql.DB, username, token string, expiry time.Time) error { + _, err := db.Exec("INSERT INTO sessions (token, username, expires) VALUES (?, ?, ?)", token, username, expiry.Format(time.RFC3339)) + return err +} diff --git a/git.go b/git/git.go similarity index 64% rename from git.go rename to git/git.go index cc0ef53..40ca6b9 100644 --- a/git.go +++ b/git/git.go @@ -2,19 +2,36 @@ // // SPDX-License-Identifier: Apache-2.0 -package main +package git import ( "errors" + "fmt" + "net/url" "os" "sort" "strings" + "time" + + "github.com/microcosm-cc/bluemonday" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/transport" ) +type Release struct { + Tag string + Content string + URL string + Date time.Time +} + +var ( + bmUGC = bluemonday.UGCPolicy() + bmStrict = bluemonday.StrictPolicy() +) + // listRemoteTags lists all tags in a remote repository, whether HTTP(S) or SSH. // func listRemoteTags(url string) (tags []string, err error) { // // TODO: Implement listRemoteTags @@ -22,37 +39,51 @@ import ( // return nil, nil // } -// fetchReleases fetches all releases in a remote repository, whether HTTP(S) or SSH. -func getGitReleases(p project) (project, error) { - r, err := minimalClone(p.URL) +// GetReleases fetches all releases in a remote repository, whether HTTP(S) or +// SSH. +func GetReleases(gitURI, forge string) ([]Release, error) { + r, err := minimalClone(gitURI) if err != nil { - return p, err + return nil, err } tagRefs, err := r.Tags() if err != nil { - return p, err + return nil, err } + + parsedURI, err := url.Parse(gitURI) + if err != nil { + fmt.Println("Error parsing URI: " + err.Error()) + } + + var httpURI string + if parsedURI.Scheme != "" { + httpURI = parsedURI.Host + parsedURI.Path + } + + releases := make([]Release, 0) + err = tagRefs.ForEach(func(tagRef *plumbing.Reference) error { obj, err := r.TagObject(tagRef.Hash()) - switch err { - case plumbing.ErrObjectNotFound: + switch { + case errors.Is(err, plumbing.ErrObjectNotFound): // This is a lightweight tag, not an annotated tag, skip it return nil - case nil: - url := "" + case err == nil: + tagURL := "" tagName := bmStrict.Sanitize(tagRef.Name().Short()) - switch p.Forge { + switch forge { case "sourcehut": - url = p.URL + "/refs/" + tagName + tagURL = "https://" + httpURI + "/refs/" + tagName case "gitlab": - url = p.URL + "/-/releases/" + tagName + tagURL = "https://" + httpURI + "/-/releases/" + tagName default: - url = "" + tagURL = "" } - p.Releases = append(p.Releases, release{ + releases = append(releases, Release{ Tag: tagName, Content: bmUGC.Sanitize(obj.Message), - URL: url, + URL: tagURL, Date: obj.Tagger.When, }) default: @@ -61,12 +92,12 @@ func getGitReleases(p project) (project, error) { return nil }) if err != nil { - return p, err + return nil, err } - sort.Slice(p.Releases, func(i, j int) bool { return p.Releases[i].Date.After(p.Releases[j].Date) }) + sort.Slice(releases, func(i, j int) bool { return releases[i].Date.After(releases[j].Date) }) - return p, nil + return releases, nil } // minimalClone clones a repository with a depth of 1 and no checkout. @@ -86,7 +117,7 @@ func minimalClone(url string) (r *git.Repository, err error) { Depth: 1, Tags: git.AllTags, }) - if err == git.NoErrAlreadyUpToDate { + if errors.Is(err, git.NoErrAlreadyUpToDate) { return r, nil } return r, err @@ -101,8 +132,8 @@ func minimalClone(url string) (r *git.Repository, err error) { return r, err } -// removeRepo removes a repository from the local filesystem. -func removeRepo(url string) (err error) { +// RemoveRepo removes a repository from the local filesystem. +func RemoveRepo(url string) (err error) { path, err := stringifyRepo(url) if err != nil { return err diff --git a/go.mod b/go.mod index b7870c4..136159d 100644 --- a/go.mod +++ b/go.mod @@ -8,41 +8,58 @@ go 1.20 require ( github.com/BurntSushi/toml v1.3.2 - github.com/go-git/go-git/v5 v5.8.0 + github.com/go-git/go-git/v5 v5.9.0 github.com/microcosm-cc/bluemonday v1.0.25 github.com/mmcdole/gofeed v1.2.1 github.com/spf13/pflag v1.0.5 + golang.org/x/crypto v0.14.0 + golang.org/x/term v0.13.0 + modernc.org/sqlite v1.26.0 ) require ( + dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect github.com/PuerkitoBio/goquery v1.8.1 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/uuid v1.3.0 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect github.com/mmcdole/goxpp v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sergi/go-diff v1.3.1 // indirect - github.com/skeema/knownhosts v1.2.0 // indirect + github.com/skeema/knownhosts v1.2.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.12.0 // indirect - golang.org/x/sys v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect - golang.org/x/tools v0.11.0 // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/net v0.16.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/tools v0.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.24.1 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.6.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3d546e9..57d593c 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,12 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= +github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= @@ -19,44 +21,51 @@ github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd3 github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +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/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= -github.com/go-git/go-git/v5 v5.8.0 h1:Rc543s6Tyq+YcyPwZRvU4jzZGM8rB/wWu94TnTIYALQ= -github.com/go-git/go-git/v5 v5.8.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= +github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY= +github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +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/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s= @@ -68,18 +77,22 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= -github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -95,12 +108,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 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/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -111,8 +124,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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= @@ -126,20 +139,22 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -148,22 +163,45 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= +modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= +modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw= +modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= diff --git a/justfile b/justfile index f4676d9..a8b7320 100644 --- a/justfile +++ b/justfile @@ -2,25 +2,40 @@ # # SPDX-License-Identifier: CC0-1.0 -default: reuse lint test staticcheck +default: fmt lint staticcheck test vuln reuse -reuse: - reuse lint +fmt: + # Formatting all Go source code + go install mvdan.cc/gofumpt@latest + gofumpt -l -w . lint: - # Linting Go code - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + # Linting Go source code golangci-lint run -test: - # Running tests - go test -v ./... - staticcheck: # Performing static analysis go install honnef.co/go/tools/cmd/staticcheck@latest staticcheck ./... +test: + # Running tests + go test -v ./... + +vuln: + # Checking for vulnerabilities + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... + +reuse: + # Linting licenses and copyright headers + reuse lint + clean: # Cleaning up - rm -rf willow + rm -rf willow out/ + +clean-all: + # Removing build artifacts, willow.sqlite, config.toml, and data/ directory + + rm -rf willow out willow.sqlite config.toml data diff --git a/main.go b/main.go deleted file mode 100644 index cb83e6c..0000000 --- a/main.go +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-FileCopyrightText: Amolith -// -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "encoding/csv" - "errors" - "fmt" - "github.com/BurntSushi/toml" - "log" - "net/http" - "os" - "sort" - "strings" - "time" - - "github.com/microcosm-cc/bluemonday" - flag "github.com/spf13/pflag" -) - -type ( - Model struct { - Projects []project - } - - project struct { - URL string - Name string - Forge string - Running string - Releases []release - } - - release struct { - Tag string - Content string - URL string - Date time.Time - } - - Config struct { - Server server - CSVLocation string - // TODO: Make cache location configurable - // CacheLocation string - FetchInterval int - } - - server struct { - Listen string - } -) - -var ( - flagConfig *string = flag.StringP("config", "c", "config.toml", "Path to config file") - config Config - req = make(chan struct{}) - manualRefresh = make(chan struct{}) - res = make(chan []project) - m = Model{ - Projects: []project{}, - } - bmUGC = bluemonday.UGCPolicy() - bmStrict = bluemonday.StrictPolicy() -) - -func main() { - - flag.Parse() - - err := checkConfig() - if err != nil { - log.Fatalln(err) - } - - err = checkCSV() - if err != nil { - log.Fatalln(err) - } - - reader := csv.NewReader(strings.NewReader(config.CSVLocation)) - - records, err := reader.ReadAll() - if err != nil { - log.Fatalln(err) - } - - m.Projects = []project{} - if len(records) > 0 { - for i, record := range records { - if i == 0 { - continue - } - m.Projects = append(m.Projects, project{ - URL: record[0], - Name: record[1], - Forge: record[2], - Running: record[3], - Releases: []release{}, - }) - } - } - - go refreshLoop(manualRefresh, req, res) - - mux := http.NewServeMux() - - httpServer := &http.Server{ - Addr: config.Server.Listen, - Handler: mux, - } - - mux.HandleFunc("/", rootHandler) - mux.HandleFunc("/static", staticHandler) - mux.HandleFunc("/new", newHandler) - - if err := httpServer.ListenAndServe(); errors.Is(err, http.ErrServerClosed) { - log.Println("Web server closed") - } else { - log.Fatalln(err) - } -} - -func refreshLoop(manualRefresh, req chan struct{}, res chan []project) { - ticker := time.NewTicker(time.Second * 3600) - - fetch := func() []project { - projects := make([]project, len(m.Projects)) - copy(projects, m.Projects) - for i, project := range projects { - project, err := getReleases(project) - if err != nil { - fmt.Println(err) - continue - } - projects[i] = project - } - sort.Slice(projects, func(i, j int) bool { return strings.ToLower(projects[i].Name) < strings.ToLower(projects[j].Name) }) - return projects - } - - projects := fetch() - - for { - select { - case <-ticker.C: - projects = fetch() - case <-manualRefresh: - ticker.Reset(time.Second * 3600) - projects = fetch() - case <-req: - projectsCopy := make([]project, len(projects)) - copy(projectsCopy, projects) - res <- projectsCopy - } - } -} - -func checkConfig() error { - file, err := os.Open(*flagConfig) - if err != nil { - if os.IsNotExist(err) { - file, err = os.Create(*flagConfig) - if err != nil { - return err - } - defer file.Close() - - _, err = file.WriteString("# Location of the CSV file containing the projects\nCSVLocation = \"projects.csv\"\n# How often to fetch new releases in seconds\nFetchInterval = 3600\n\n[Server]\n# Address to listen on\nListen = \"127.0.0.1:1313\"\n") - if err != nil { - return err - } - - fmt.Println("Config file created at", *flagConfig) - fmt.Println("Please edit it and restart the server") - os.Exit(0) - } else { - return err - } - } - defer file.Close() - - _, err = toml.DecodeFile(*flagConfig, &config) - if err != nil { - return err - } - - if config.CSVLocation == "" { - fmt.Println("No CSV location specified, using projects.csv") - config.CSVLocation = "projects.csv" - } - - if config.FetchInterval < 10 { - fmt.Println("Fetch interval is set to", config.FetchInterval, "seconds, but the minimum is 10, using 10") - config.FetchInterval = 10 - } - - if config.Server.Listen == "" { - fmt.Println("No listen address specified, using 127.0.0.1:1313") - config.Server.Listen = "127.0.0.1:1313" - } - - return nil -} - -func checkCSV() error { - file, err := os.Open(config.CSVLocation) - if err != nil { - if os.IsNotExist(err) { - file, err = os.Create(config.CSVLocation) - if err != nil { - return err - } - defer file.Close() - - _, err = file.WriteString("url,name,forge,running\nhttps://git.sr.ht/~amolith/earl,earl,sourcehut,v0.0.1-rc0\n") - if err != nil { - return err - } - } else { - return err - } - } - defer file.Close() - return nil -} diff --git a/project/project.go b/project/project.go new file mode 100644 index 0000000..3ee1a3c --- /dev/null +++ b/project/project.go @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: Apache-2.0 + +package project + +import ( + "database/sql" + "fmt" + "log" + "sort" + "strings" + "time" + + "git.sr.ht/~amolith/willow/db" + "git.sr.ht/~amolith/willow/git" + "git.sr.ht/~amolith/willow/rss" +) + +type Project struct { + URL string + Name string + Forge string + Running string + Releases []Release +} + +type Release struct { + URL string + Tag string + Content string + Date time.Time +} + +// GetReleases returns a list of all releases for a project from the database +func GetReleases(dbConn *sql.DB, proj Project) (Project, error) { + ret, err := db.GetReleases(dbConn, proj.URL) + if err != nil { + return proj, err + } + + if len(ret) == 0 { + return fetchReleases(proj) + } + + for _, row := range ret { + proj.Releases = append(proj.Releases, Release{ + Tag: row["tag"], + Content: row["content"], + URL: row["release_url"], + Date: time.Time{}, + }) + } + sort.Slice(proj.Releases, func(i, j int) bool { + return proj.Releases[i].Date.After(proj.Releases[j].Date) + }) + return proj, nil +} + +// fetchReleases fetches releases from a project's forge given its URI +func fetchReleases(p Project) (Project, error) { + var err error + switch p.Forge { + case "github", "gitea", "forgejo": + rssReleases, err := rss.GetReleases(p.URL) + if err != nil { + return p, err + } + for _, release := range rssReleases { + p.Releases = append(p.Releases, Release{ + Tag: release.Tag, + Content: release.Content, + URL: release.URL, + Date: release.Date, + }) + } + default: + gitReleases, err := git.GetReleases(p.URL, p.Forge) + if err != nil { + return p, err + } + for _, release := range gitReleases { + p.Releases = append(p.Releases, Release{ + Tag: release.Tag, + Content: release.Content, + URL: release.URL, + Date: release.Date, + }) + } + } + sort.Slice(p.Releases, func(i, j int) bool { + return p.Releases[i].Date.After(p.Releases[j].Date) + }) + return p, err +} + +func Track(dbConn *sql.DB, manualRefresh *chan struct{}, name, url, forge, release string) { + err := db.UpsertProject(dbConn, url, name, forge, release) + if err != nil { + fmt.Println("Error upserting project:", err) + } + *manualRefresh <- struct{}{} +} + +func Untrack(dbConn *sql.DB, manualRefresh *chan struct{}, url string) { + err := db.DeleteProject(dbConn, url) + if err != nil { + fmt.Println("Error deleting project:", err) + } + + *manualRefresh <- struct{}{} + + err = git.RemoveRepo(url) + if err != nil { + log.Println(err) + } +} + +func RefreshLoop(dbConn *sql.DB, interval int, manualRefresh, req *chan struct{}, res *chan []Project) { + ticker := time.NewTicker(time.Second * time.Duration(interval)) + + fetch := func() []Project { + projectsList, err := GetProjects(dbConn) + if err != nil { + fmt.Println("Error getting projects:", err) + } + for i, p := range projectsList { + p, err := fetchReleases(p) + if err != nil { + fmt.Println(err) + continue + } + projectsList[i] = p + } + sort.Slice(projectsList, func(i, j int) bool { + return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name) + }) + for i := range projectsList { + for j := range projectsList[i].Releases { + err = db.UpsertRelease(dbConn, projectsList[i].URL, projectsList[i].Releases[j].URL, projectsList[i].Releases[j].Tag, projectsList[i].Releases[j].Content, projectsList[i].Releases[j].Date.Format("2006-01-02 15:04:05")) + if err != nil { + fmt.Println("Error upserting release:", err) + continue + } + } + } + return projectsList + } + + projects := fetch() + + for { + select { + case <-ticker.C: + projects = fetch() + case <-*manualRefresh: + ticker.Reset(time.Second * 3600) + projects = fetch() + case <-*req: + projectsCopy := make([]Project, len(projects)) + copy(projectsCopy, projects) + *res <- projectsCopy + } + } +} + +// GetProject returns a project from the database +func GetProject(dbConn *sql.DB, url string) (Project, error) { + var p Project + projectDB, err := db.GetProject(dbConn, url) + if err != nil { + return p, err + } + p = Project{ + URL: projectDB["url"], + Name: projectDB["name"], + Forge: projectDB["forge"], + Running: projectDB["version"], + } + return p, err +} + +// GetProjects returns a list of all projects from the database +func GetProjects(dbConn *sql.DB) ([]Project, error) { + projectsDB, err := db.GetProjects(dbConn) + if err != nil { + return nil, err + } + + projects := make([]Project, len(projectsDB)) + for i, p := range projectsDB { + projects[i] = Project{ + URL: p["url"], + Name: p["name"], + Forge: p["forge"], + Running: p["version"], + } + } + + return projects, nil +} diff --git a/releases.go b/releases.go deleted file mode 100644 index e50def6..0000000 --- a/releases.go +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: Amolith -// -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "encoding/csv" - "log" - "os" -) - -func getReleases(p project) (project, error) { - var err error - switch p.Forge { - case "github", "gitea", "forgejo": - p, err = getRSSReleases(p) - // case "gitlab": - // // TODO: maybe use GitLab's API? - default: - p, err = getGitReleases(p) - } - return p, err -} - -func track(name, url, forge, release string) { - projectExists := false - for i := range m.Projects { - if m.Projects[i].URL == url { - projectExists = true - m.Projects[i].Running = release - } - } - - if !projectExists { - m.Projects = append(m.Projects, project{ - URL: url, - Name: name, - Forge: forge, - Running: release, - }) - } - - manualRefresh <- struct{}{} - - writeCSV() -} - -func untrack(url string) { - for i := range m.Projects { - if m.Projects[i].URL == url { - m.Projects = append(m.Projects[:i], m.Projects[i+1:]...) - break - } - } - - manualRefresh <- struct{}{} - - writeCSV() - err := removeRepo(url) - if err != nil { - log.Println(err) - } -} - -func writeCSV() { - file, err := os.OpenFile(config.CSVLocation, os.O_RDWR|os.O_CREATE, 0o600) - if err != nil { - log.Fatalln(err) - } - defer file.Close() - - writer := csv.NewWriter(file) - - if err := writer.Write([]string{"url", "name", "forge", "running"}); err != nil { - log.Fatalln(err) - } - for _, project := range m.Projects { - if err := writer.Write([]string{project.URL, project.Name, project.Forge, project.Running}); err != nil { - log.Fatalln(err) - } - } - - writer.Flush() -} diff --git a/rss.go b/rss/rss.go similarity index 55% rename from rss.go rename to rss/rss.go index 2c98c3a..2302630 100644 --- a/rss.go +++ b/rss/rss.go @@ -2,24 +2,41 @@ // // SPDX-License-Identifier: Apache-2.0 -package main +package rss import ( "fmt" + "time" + + "github.com/microcosm-cc/bluemonday" "github.com/mmcdole/gofeed" ) -func getRSSReleases(p project) (project, error) { +type Release struct { + Tag string + Content string + URL string + Date time.Time +} + +var ( + bmUGC = bluemonday.UGCPolicy() + bmStrict = bluemonday.StrictPolicy() +) + +func GetReleases(feedURL string) ([]Release, error) { fp := gofeed.NewParser() - feed, err := fp.ParseURL(p.URL + "/releases.atom") + feed, err := fp.ParseURL(feedURL + "/releases.atom") if err != nil { fmt.Println(err) - return p, err + return nil, err } + releases := make([]Release, 0) + for _, item := range feed.Items { - p.Releases = append(p.Releases, release{ + releases = append(releases, Release{ Tag: bmStrict.Sanitize(item.Title), Content: bmUGC.Sanitize(item.Content), URL: bmStrict.Sanitize(item.Link), @@ -30,5 +47,5 @@ func getRSSReleases(p project) (project, error) { // TODO: Doesn't seem to work? // sort.Slice(p.Releases, func(i, j int) bool { return p.Releases[i].Date.After(p.Releases[j].Date) }) - return p, nil + return releases, nil } diff --git a/users/users.go b/users/users.go new file mode 100644 index 0000000..043edbd --- /dev/null +++ b/users/users.go @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: Apache-2.0 + +package users + +import ( + "crypto/rand" + "database/sql" + "encoding/base64" + "time" + + "git.sr.ht/~amolith/willow/db" + "golang.org/x/crypto/argon2" +) + +// argonHash accepts two strings for the user's password and a random salt, +// hashes the password using the salt, and returns the hash as a base64-encoded +// string. +func argonHash(password, salt string) (string, error) { + decodedSalt, err := base64.StdEncoding.DecodeString(salt) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(argon2.IDKey([]byte(password), decodedSalt, 2, 64*1024, 4, 64)), nil +} + +// generateSalt generates a random salt and returns it as a base64-encoded +// string. +func generateSalt() (string, error) { + salt := make([]byte, 16) + _, err := rand.Read(salt) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(salt), nil +} + +// Register accepts a username and password, hashes the password and stores the +// hash and salt in the database. +func Register(dbConn *sql.DB, username, password string) error { + salt, err := generateSalt() + if err != nil { + return err + } + + hash, err := argonHash(password, salt) + if err != nil { + return err + } + + return db.CreateUser(dbConn, username, hash, salt) +} + +// Delete removes a user from the database. +func Delete(dbConn *sql.DB, username string) error { return db.DeleteUser(dbConn, username) } + +// Authorised accepts a username string, a token string, and returns true if the +// user is authorised, false if not, and an error if one is encountered. +func Authorised(dbConn *sql.DB, username, token string) (bool, error) { + dbHash, dbSalt, err := db.GetUser(dbConn, username) + if err != nil { + return false, err + } + + providedHash, err := argonHash(token, dbSalt) + if err != nil { + return false, err + } + + return dbHash == providedHash, nil +} + +// GetSession accepts a session cookie string and returns the username +func GetSession(dbConn *sql.DB, session string) (string, time.Time, error) { + return db.GetSession(dbConn, session) +} + +// InvalidateSession invalidates a session by setting the expiration date to the +// current time. +func InvalidateSession(dbConn *sql.DB, session string) error { + return db.InvalidateSession(dbConn, session, time.Now()) +} + +// CreateSession accepts a username and a token and creates a session in the +// database. +func CreateSession(dbConn *sql.DB, username, token string, expiry time.Time) error { + return db.CreateSession(dbConn, username, token, expiry) +} + +// GetUsers returns a list of all users in the database as a slice of strings. +func GetUsers(dbConn *sql.DB) ([]string, error) { return db.GetUsers(dbConn) } diff --git a/static/home.html b/ws/static/home.html similarity index 100% rename from static/home.html rename to ws/static/home.html diff --git a/static/home.html.license b/ws/static/home.html.license similarity index 100% rename from static/home.html.license rename to ws/static/home.html.license diff --git a/ws/static/login.html b/ws/static/login.html new file mode 100644 index 0000000..bcb3b4d --- /dev/null +++ b/ws/static/login.html @@ -0,0 +1,30 @@ + + + + Willow + + + + + + +

Willow

+
+
+ + +
+
+ + +
+ +
+

Source code

+ + diff --git a/static/new.html.license b/ws/static/login.html.license similarity index 100% rename from static/new.html.license rename to ws/static/login.html.license diff --git a/static/new.html b/ws/static/new.html similarity index 100% rename from static/new.html rename to ws/static/new.html diff --git a/static/select-release.html.license b/ws/static/new.html.license similarity index 100% rename from static/select-release.html.license rename to ws/static/new.html.license diff --git a/static/select-release.html b/ws/static/select-release.html similarity index 100% rename from static/select-release.html rename to ws/static/select-release.html diff --git a/ws/static/select-release.html.license b/ws/static/select-release.html.license new file mode 100644 index 0000000..f0b540a --- /dev/null +++ b/ws/static/select-release.html.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Amolith + +SPDX-License-Identifier: Apache-2.0 diff --git a/ws.go b/ws/ws.go similarity index 72% rename from ws.go rename to ws/ws.go index ef0be82..82cf5fa 100644 --- a/ws.go +++ b/ws/ws.go @@ -2,31 +2,55 @@ // // SPDX-License-Identifier: Apache-2.0 -package main +package ws import ( + "database/sql" "embed" "fmt" "io" "net/http" "net/url" "strings" + "sync" "text/template" + + "git.sr.ht/~amolith/willow/project" + "github.com/microcosm-cc/bluemonday" ) +type Handler struct { + DbConn *sql.DB + Mutex *sync.Mutex + Req *chan struct{} + ManualRefresh *chan struct{} + Res *chan []project.Project +} + //go:embed static var fs embed.FS -func rootHandler(w http.ResponseWriter, r *http.Request) { - req <- struct{}{} - data := <-res +// bmUGC = bluemonday.UGCPolicy() +var bmStrict = bluemonday.StrictPolicy() + +func (h Handler) RootHandler(w http.ResponseWriter, r *http.Request) { + if !h.isAuthorised(r) { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + *h.Req <- struct{}{} + data := <-*h.Res tmpl := template.Must(template.ParseFS(fs, "static/home.html")) if err := tmpl.Execute(w, data); err != nil { fmt.Println(err) } } -func newHandler(w http.ResponseWriter, r *http.Request) { +func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) { + if !h.isAuthorised(r) { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } params := r.URL.Query() action := bmStrict.Sanitize(params.Get("action")) if r.Method == http.MethodGet { @@ -36,7 +60,7 @@ func newHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(err) } } else if action != "delete" { - submittedURL := bmStrict.Sanitize(params.Get("submittedURL")) + submittedURL := bmStrict.Sanitize(params.Get("url")) if submittedURL == "" { w.WriteHeader(http.StatusBadRequest) _, err := w.Write([]byte("No URL provided")) @@ -62,22 +86,20 @@ func newHandler(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte("No name provided")) if err != nil { fmt.Println(err) - } } - proj := project{ + proj := project.Project{ URL: submittedURL, Name: name, Forge: forge, } - proj, err := getReleases(proj) + proj, err := project.GetReleases(h.DbConn, proj) if err != nil { w.WriteHeader(http.StatusBadRequest) _, err := w.Write([]byte(fmt.Sprintf("Error getting releases: %s", err))) if err != nil { fmt.Println(err) - } } tmpl := template.Must(template.ParseFS(fs, "static/select-release.html")) @@ -85,17 +107,16 @@ func newHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(err) } } else if action == "delete" { - submittedURL := params.Get("submittedURL") + submittedURL := params.Get("url") if submittedURL == "" { w.WriteHeader(http.StatusBadRequest) _, err := w.Write([]byte("No URL provided")) if err != nil { fmt.Println(err) - } } - untrack(submittedURL) + project.Untrack(h.DbConn, h.ManualRefresh, submittedURL) http.Redirect(w, r, "/", http.StatusSeeOther) } } @@ -111,7 +132,7 @@ func newHandler(w http.ResponseWriter, r *http.Request) { releaseValue := bmStrict.Sanitize(r.FormValue("release")) if nameValue != "" && urlValue != "" && forgeValue != "" && releaseValue != "" { - track(nameValue, urlValue, forgeValue, releaseValue) + project.Track(h.DbConn, h.ManualRefresh, nameValue, urlValue, forgeValue, releaseValue) http.Redirect(w, r, "/", http.StatusSeeOther) return } @@ -126,13 +147,21 @@ func newHandler(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte("No data provided")) if err != nil { fmt.Println(err) - } } } } -func staticHandler(writer http.ResponseWriter, request *http.Request) { +func (h Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { + // TODO: do this +} + +func (h Handler) isAuthorised(r *http.Request) bool { + // TODO: do this + return false +} + +func StaticHandler(writer http.ResponseWriter, request *http.Request) { resource := strings.TrimPrefix(request.URL.Path, "/") // if path ends in .css, set content type to text/css if strings.HasSuffix(resource, ".css") {