summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
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))
+ }
+}