package main
import (
"context"
"crypto/tls"
"fmt"
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"io"
"log"
"net/http"
"nightfall-server/cfg"
"nightfall-server/gmi2html"
"os"
"path"
"strings"
"time"
)
const STYLE=`
body {
max-width: 650px;
margin: 40px auto;
padding: 0 10px;
font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
color: #444
}
h1, h2, h3 {
line-height: 1.2;
}
pre {
font-family: monospace;
font-size: initial;
line-height: initial;
overflow-x: auto;
}
districts[type=file] {
background-color: initial;
border: initial;
}
blockquote {
margin-left: 0;
margin-right: 0;
padding: 0 15px;
border-left: .2em solid;
}
img {
max-width: 100%;
}
@media (prefers-color-scheme: dark) {
body {
color: white;
background: #444
}
a:link{
color:#5bf
}
a:visited{
color:#ccf
}
}
`
func httpToHTTPSHandler() *http.ServeMux {
handleRedirect := func(w http.ResponseWriter, r *http.Request) {
newURI := "https://" + r.Host + r.URL.String()
http.Redirect(w, r, newURI, http.StatusFound)
}
mux := &http.ServeMux{}
mux.HandleFunc("/", handleRedirect)
return mux
}
func startWebServer(conf *cfg.Config) {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasPrefix(r.URL.Path, "/gemini/"):
geminiURL := r.URL.String()[8:len(r.URL.String())]
client := gemini.Client{}
ctx := context.Background()
resp, err := client.Get(ctx, "gemini://" + geminiURL)
if err != nil {
if _, err = w.Write([]byte("<h1>Host not found :(</h1>")); err != nil {
return
}
break
}
switch resp.Status {
case gemini.StatusRedirect:
case gemini.StatusPermanentRedirect:
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if _, err := w.Write([]byte("<style>" + STYLE + "</style>" + gmi2html.Convert("=> " + resp.Meta, geminiURL))); err != nil {
return
}
break
case gemini.StatusNotFound:
if _, err := w.Write([]byte("<h1>Page not found</h1>")); err != nil {
return
}
break
case gemini.StatusSuccess:
if strings.HasPrefix(resp.Meta, "text") {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if b, err := io.ReadAll(resp.Body); err == nil {
p := strings.Split(r.URL.Path, "/")
var up string
if len(p) > 0 {
if p[0] == "/" {
p = p[1:]
}
if p[len(p)-1] == "" {
p = p[:len(p)-1]
}
if len(p) > 3 {
up = "<p><a href=\""+strings.Join(p[:len(p)-1], "/")+"/\">Go up</a></p>"
}
}
if _, err = w.Write([]byte("<style>" + STYLE + "</style>" + up + gmi2html.Convert(string(b), geminiURL))); err != nil {
return
}
}
} else {
w.Header().Set("Content-Type", resp.Meta)
b, err := io.ReadAll(resp.Body)
if err != nil {
return
}
if _, err = w.Write(b); err != nil {
return
}
}
break
default:
if _, err = w.Write([]byte("<h1>Uhh error :(</h1>")); err != nil {
return
}
}
break
default:
http.FileServer(http.Dir(path.Join(conf.Var, "html"))).ServeHTTP(w, r)
}
})
switch conf.Env {
case "PROD":
go func() {
m := &autocert.Manager{
Cache: autocert.DirCache(conf.CertCache),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("nightfall.city"),
}
customCfg := &tls.Config{
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
fmt.Println("SERVER NAME:", hello.ServerName)
return m.GetCertificate(hello)
},
NextProtos: []string{
"h2", "http/1.1", // enable HTTP/2
acme.ALPNProto, // enable tls-alpn ACME challenges
},
}
srv := &http.Server{
Addr: ":https",
TLSConfig: customCfg,
Handler: mux,
}
fmt.Printf("Starting HTTP server on :443\n")
err := srv.ListenAndServeTLS("", "")
if err != nil {
log.Fatalf("httpsSrv.ListendAndServeTLS() failed with %s", err)
}
}()
log.Fatal(http.ListenAndServe(":80", httpToHTTPSHandler()))
default:
log.Fatal(http.ListenAndServe(":8080", mux))
}
}
func startGeminiServer(conf *cfg.Config) {
certificates := &certificate.Store{}
certificates.Register("nightfall.city")
if err := certificates.Load(conf.GmiCertCache); err != nil {
log.Fatal(err)
}
mux := &gemini.Mux{}
mux.Handle("/", gemini.FileServer(os.DirFS(path.Join(conf.Var, "gmi/"))))
server := &gemini.Server{
Handler: gemini.LoggingMiddleware(mux),
ReadTimeout: 30 * time.Second,
WriteTimeout: 1 * time.Minute,
GetCertificate: certificates.Get,
}
if err := server.ListenAndServe(context.Background()); err != nil {
log.Fatal(err)
}
}
func main() {
conf := cfg.New()
go startWebServer(conf)
startGeminiServer(conf)
}