From b5bad4defc6c75b9b969658229ce5fd2f3a46107 Mon Sep 17 00:00:00 2001 From: Ben Busby Date: Tue, 21 Jan 2025 13:46:29 -0700 Subject: Rewrite project, add daily update of services list The project was rewritten from Elixir to Go, primarily because: - I don't write Elixir anymore and don't want to maintain a project in a language I no longer write - I already write Go for other projects, including my day job, so it's a safer bet for a project that I want to maintain long term - Go allows me to build portable executables that will make it easier for others to run farside on their own machines The Go version of Farsside also has a built in task to fetch the latest services{-full}.json file from the repo and ingest it, which makes running a farside server a lot simpler. It also automatically fetches the latest instance state from https://farside.link unless configured as a primary farside node, which will allow others to use farside without increasing traffic to all instances that are queried by farside (just to the farside node itself). --- server/index.html | 66 ++++++++++++++++++++++++ server/route.html | 10 ++++ server/server.go | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++ server/server_test.go | 80 +++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 server/index.html create mode 100644 server/route.html create mode 100644 server/server.go create mode 100644 server/server_test.go (limited to 'server') diff --git a/server/index.html b/server/index.html new file mode 100644 index 0000000..0455489 --- /dev/null +++ b/server/index.html @@ -0,0 +1,66 @@ + + + + + Farside + + + +
+
+

Farside [SourceHut, GitHub]

+
+

Updated: {{ .LastUpdated }}

+
+ +
+
+
+ + diff --git a/server/route.html b/server/route.html new file mode 100644 index 0000000..fd5ef41 --- /dev/null +++ b/server/route.html @@ -0,0 +1,10 @@ + + Farside Redirect + + + + + Redirecting to {{ .InstanceURL }}... + diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..0a0b683 --- /dev/null +++ b/server/server.go @@ -0,0 +1,138 @@ +package server + +import ( + _ "embed" + "encoding/json" + "html/template" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/benbusby/farside/db" + "github.com/benbusby/farside/services" +) + +//go:embed index.html +var indexHTML string + +//go:embed route.html +var routeHTML string + +type indexData struct { + LastUpdated time.Time + ServiceList []services.Service +} + +type routeData struct { + InstanceURL string +} + +func home(w http.ResponseWriter, r *http.Request) { + serviceList := db.GetServiceList() + data := indexData{ + LastUpdated: db.LastUpdate, + ServiceList: serviceList, + } + + tmpl, err := template.New("").Parse(indexHTML) + if err != nil { + log.Println(err) + http.Error(w, "Error parsing template", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html") + + err = tmpl.Execute(w, data) + if err != nil { + log.Println(err) + http.Error(w, "Error executing template", http.StatusInternalServerError) + } +} + +func state(w http.ResponseWriter, r *http.Request) { + storedServices := db.GetServiceList() + jsonData, _ := json.Marshal(storedServices) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) +} + +func baseRouting(w http.ResponseWriter, r *http.Request) { + routing(w, r, false) +} + +func jsRouting(w http.ResponseWriter, r *http.Request) { + r.URL.Path = strings.Replace(r.URL.Path, "/_", "", 1) + routing(w, r, true) +} + +func routing(w http.ResponseWriter, r *http.Request, jsEnabled bool) { + value := r.PathValue("routing") + if len(value) == 0 { + value = r.URL.Path + } + + url, _ := url.Parse(value) + path := strings.TrimPrefix(url.Path, "/") + segments := strings.Split(path, "/") + + target, err := services.MatchRequest(segments[0]) + if err != nil { + log.Printf("Error during match request: %v\n", err) + http.Error(w, "No routing found for "+target, http.StatusBadRequest) + return + } + + instance, err := db.GetInstance(target) + if err != nil { + log.Printf("Error fetching instance from db: %v\n", err) + http.Error( + w, + "Error fetching instance for "+target, + http.StatusInternalServerError) + return + } + + if len(segments) > 1 { + targetPath := strings.Join(segments[1:], "/") + instance = instance + "/" + targetPath + } + + w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + + if jsEnabled { + data := routeData{ + InstanceURL: instance, + } + tmpl, _ := template.New("").Parse(routeHTML) + w.Header().Set("Content-Type", "text/html") + _ = tmpl.Execute(w, data) + } else { + http.Redirect(w, r, instance, http.StatusFound) + } +} + +func RunServer() { + mux := http.NewServeMux() + mux.HandleFunc("/{$}", home) + mux.HandleFunc("/state/{$}", state) + mux.HandleFunc("/{routing...}", baseRouting) + mux.HandleFunc("/_/{routing...}", jsRouting) + + port := os.Getenv("FARSIDE_PORT") + if len(port) == 0 { + port = "4001" + } + + log.Println("Starting server on http://localhost:" + port) + + err := http.ListenAndServe(":"+port, mux) + if err != nil { + log.Fatal(err) + } +} diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 0000000..96ee083 --- /dev/null +++ b/server/server_test.go @@ -0,0 +1,80 @@ +package server + +import ( + "io" + "log" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + + "github.com/benbusby/farside/db" +) + +const breezewikiTestSite = "https://breezewikitest.com" + +func TestMain(m *testing.M) { + err := db.InitializeDB() + if err != nil { + log.Fatalln("Failed to initialize database", err) + } + + err = db.SetInstances("breezewiki", []string{breezewikiTestSite}) + if err != nil { + log.Fatalln("Failed to set instances in db") + } + + exitCode := m.Run() + + _ = db.CloseDB() + os.Exit(exitCode) +} + +func TestBaseRouting(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/fandom.com", nil) + w := httptest.NewRecorder() + + baseRouting(w, req) + + res := w.Result() + defer res.Body.Close() + + if res.StatusCode != http.StatusFound { + t.Fatalf("Incorrect resp code (%d) in base routing", res.StatusCode) + } + + expectedHost, _ := url.Parse(breezewikiTestSite) + redirect, err := res.Location() + if err != nil { + t.Fatalf("Error retrieving direct from request: %v\n", err) + } else if redirect.Host != expectedHost.Host { + t.Fatalf("Incorrect redirect site -- expected: %s, actual: %s\n", + expectedHost.Host, + redirect.Host) + } +} + +func TestJSRouting(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/_/fandom.com", nil) + w := httptest.NewRecorder() + + jsRouting(w, req) + + res := w.Result() + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + t.Fatalf("Incorrect resp code (%d) in base routing", res.StatusCode) + } + + data, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("Error reading response body: %v", err) + } + + if !strings.Contains(string(data), breezewikiTestSite) { + t.Fatalf("%s not found in response body (%s)", breezewikiTestSite, string(data)) + } +} -- cgit v1.2.3