diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/index.html | 66 | ||||
| -rw-r--r-- | server/route.html | 10 | ||||
| -rw-r--r-- | server/server.go | 138 | ||||
| -rw-r--r-- | server/server_test.go | 80 |
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)) + } +} |
