summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorBen Busby <contact@benbusby.com>2025-01-21 13:46:29 -0700
committerBen Busby <contact@benbusby.com>2025-01-21 13:46:29 -0700
commitb5bad4defc6c75b9b969658229ce5fd2f3a46107 (patch)
treeacc460a4e15669e71dc61f0df2df5a27c6d2f965 /server
parente0e395f3c82627190897683a40e4ba28104a03f9 (diff)
downloadfarside-b5bad4defc6c75b9b969658229ce5fd2f3a46107.tar.gz
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).
Diffstat (limited to 'server')
-rw-r--r--server/index.html66
-rw-r--r--server/route.html10
-rw-r--r--server/server.go138
-rw-r--r--server/server_test.go80
4 files changed, 294 insertions, 0 deletions
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 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <title>Farside</title>
+ <style>
+ html {
+ font-family: monospace;
+ font-size: 16px;
+ color: #66397C;
+ }
+ #parent-div {
+ text-align: center;
+ }
+ #child-div {
+ text-align: left;
+ width: 50%;
+ display: inline-block;
+ }
+ hr {
+ border: 1px dashed;
+ }
+ a:link, a:visited {
+ color: #66397C;
+ }
+ @media only screen and (max-width: 1000px) {
+ #child-div {
+ width: 90%;
+ }
+ }
+ ul {
+ margin: 10px;
+ }
+ @media (prefers-color-scheme: dark) {
+ html {
+ color: #fff;
+ background: #121517;
+ }
+ a:link, a:visited {
+ color: #AA8AC1;
+ }
+ }
+ </style>
+</head>
+<body>
+ <div id="parent-div">
+ <div id="child-div">
+ <h1>Farside [<a href="https://sr.ht/~benbusby/farside">SourceHut</a>, <a href="https://github.com/benbusby/farside">GitHub</a>]</h1>
+ <hr>
+ <h3>Updated: {{ .LastUpdated }}</h2>
+ <div>
+ <ul>
+ {{ range $i, $service := .ServiceList }}
+ <li><a href="/{{ $service.Type }}">{{ $service.Type }}</a></li>
+ <ul>
+ {{ range $j, $instance := $service.Instances }}
+ <li><a href="{{ $instance }}">{{ $instance }}</li>
+ {{ end }}
+ </ul>
+ {{ end }}
+ </ul>
+ </div>
+ </div>
+ </div>
+</body>
+
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 @@
+<head>
+ <title>Farside Redirect</title>
+ <meta http-equiv="refresh" content="1; url={{ .InstanceURL }}">
+ <script>
+ history.pushState({page: 1}, "Farside Redirect");
+ </script>
+</head>
+<body>
+ <span>Redirecting to {{ .InstanceURL }}...</span>
+</body>
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))
+ }
+}