1
0
Fork 0
mirror of https://github.com/benbusby/farside.git synced 2025-04-25 13:10:02 +00:00

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).
This commit is contained in:
Ben Busby 2025-01-21 13:46:29 -07:00
parent e0e395f3c8
commit b5bad4defc
No known key found for this signature in database
GPG key ID: B9B7231E01D924A1
31 changed files with 1031 additions and 768 deletions

66
server/index.html Normal file
View file

@ -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>

10
server/route.html Normal file
View file

@ -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>

138
server/server.go Normal file
View file

@ -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)
}
}

80
server/server_test.go Normal file
View file

@ -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))
}
}