164 lines
4.5 KiB
Go
164 lines
4.5 KiB
Go
// SPDX-FileCopyrightText: 2021 Amolith <amolith@secluded.site>
|
|
//
|
|
// SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Serves the webpage created by createRoot()
|
|
func (m Ring) root(writer http.ResponseWriter, request *http.Request) {
|
|
if m.modify("ring") {
|
|
log.Println("Ring modified, clearing field and re-parsing")
|
|
m.parseList()
|
|
}
|
|
if m.modify("index") {
|
|
log.Println("Index modified, clearing field and re-parsing")
|
|
m.parseIndex()
|
|
}
|
|
var table string
|
|
for _, member := range m.ring {
|
|
table = table + " <tr>\n"
|
|
table = table + " <td>" + member.id + "</td>\n"
|
|
table = table + " <td>" + link(member.url) + "</td>\n"
|
|
table = table + " </tr>\n"
|
|
}
|
|
m.index.Execute(writer, template.HTML(table))
|
|
}
|
|
|
|
type MemberSelector struct {
|
|
id string // exact match
|
|
urlpart string
|
|
}
|
|
|
|
func parseQuery(query url.Values) (*MemberSelector, error) {
|
|
urlpart := query.Get("urlpart")
|
|
if urlpart == "" {
|
|
urlpart = query.Get("host")
|
|
}
|
|
n_valid := 0
|
|
if urlpart != "" {
|
|
n_valid += 1
|
|
}
|
|
id := query.Get("id")
|
|
if id != "" {
|
|
n_valid += 1
|
|
}
|
|
if n_valid != 1 {
|
|
return nil, fmt.Errorf("Please specify urlpart=xxx or id=xxx")
|
|
}
|
|
|
|
return &MemberSelector{
|
|
id: id,
|
|
urlpart: urlpart,
|
|
}, nil
|
|
}
|
|
|
|
func (m Ring) match(selector *MemberSelector, item RingMember) bool {
|
|
if selector.id != "" {
|
|
return item.id == selector.id
|
|
}
|
|
if selector.urlpart != "" {
|
|
return strings.Contains(item.url, selector.urlpart)
|
|
}
|
|
panic("unreachable")
|
|
}
|
|
|
|
// Redirects the visitor to the next member, wrapping around the list if the
|
|
// next would be out-of-bounds, and ensuring the destination returns a 200 OK
|
|
// status before performing the redirect.
|
|
func (m Ring) next(writer http.ResponseWriter, request *http.Request) {
|
|
if m.modify("ring") {
|
|
log.Println("Ring modified, clearing field and re-parsing")
|
|
m.parseList()
|
|
}
|
|
success := false
|
|
query := request.URL.Query()
|
|
selector, err := parseQuery(query)
|
|
if err != nil {
|
|
http.Error(writer, fmt.Errorf("invalid query: %w", err).Error(), http.StatusBadRequest)
|
|
}
|
|
length := len(m.ring)
|
|
for i, item := range m.ring {
|
|
if m.match(selector, item) {
|
|
for j := i + 1; j < length+i; j++ {
|
|
dest := m.ring[j%length].url
|
|
log.Println("Checking '" + dest + "'")
|
|
if isEndpointOnline(dest) {
|
|
log.Println("Redirecting visitor to '" + dest + "'")
|
|
http.Redirect(writer, request, dest, http.StatusFound)
|
|
success = true
|
|
break
|
|
}
|
|
log.Println("Something went wrong accessing '" + dest + "', skipping site")
|
|
}
|
|
}
|
|
}
|
|
if !success {
|
|
http.Error(writer, "Ring member '"+query.Encode()+"' not found.", 404)
|
|
}
|
|
}
|
|
|
|
// Redirects the visitor to the previous member, wrapping around the list if the
|
|
// next would be out-of-bounds, and ensuring the destination returns a 200 OK
|
|
// status before performing the redirect.
|
|
func (m Ring) previous(writer http.ResponseWriter, request *http.Request) {
|
|
if m.modify("ring") {
|
|
log.Println("Ring modified, clearing field and re-parsing")
|
|
m.parseList()
|
|
}
|
|
query := request.URL.Query()
|
|
selector, err := parseQuery(query)
|
|
if err != nil {
|
|
http.Error(writer, fmt.Errorf("invalid query: %w", err).Error(), http.StatusBadRequest)
|
|
}
|
|
length := len(m.ring)
|
|
for index, item := range m.ring {
|
|
if m.match(selector, item) {
|
|
// from here to start of list
|
|
for i := index - 1; i > 0; i-- {
|
|
dest := m.ring[i].url
|
|
if isEndpointOnline(dest) {
|
|
log.Println("Redirecting visitor to '" + dest + "'")
|
|
http.Redirect(writer, request, dest, http.StatusFound)
|
|
return
|
|
}
|
|
}
|
|
// from end of list to here
|
|
for i := length - 1; i > index; i-- {
|
|
dest := m.ring[i].url
|
|
if isEndpointOnline(dest) {
|
|
log.Println("Redirecting visitor to '" + dest + "'")
|
|
http.Redirect(writer, request, dest, http.StatusFound)
|
|
return
|
|
}
|
|
}
|
|
http.Error(writer, `It would appear that either none of the ring members are accessible
|
|
(unlikely) or the backend is broken (more likely). In either case,
|
|
please email amolith@secluded.site and let him (me) know what's up.`, 500)
|
|
return
|
|
}
|
|
}
|
|
http.Error(writer, "Ring member '"+query.Encode()+"' not )found.", 404)
|
|
}
|
|
|
|
// Redirects the visitor to a random member
|
|
func (m Ring) random(writer http.ResponseWriter, request *http.Request) {
|
|
if m.modify("ring") {
|
|
log.Println("Ring modified, clearing field and re-parsing")
|
|
m.parseList()
|
|
}
|
|
rand.Seed(time.Now().Unix())
|
|
dest := "https://" + m.ring[rand.Intn(len(m.ring)-1)].url
|
|
http.Redirect(writer, request, dest, http.StatusFound)
|
|
}
|