Gemini server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

244 lines
6.1 KiB

package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"code.rocketnine.space/tslocum/gmitohtml/pkg/gmitohtml"
)
var cssBytes = []byte(gmitohtml.StyleCSS)
func serveHTTPS(w http.ResponseWriter, r *http.Request) (int, int64, string) {
if r.URL.Path == "" {
// Redirect to /
u, err := url.Parse(r.URL.String())
if err != nil {
status := http.StatusInternalServerError
http.Error(w, "Failed to parse URL", status)
return status, -1, ""
}
u.Path += "/"
status := http.StatusTemporaryRedirect
http.Redirect(w, r, u.String(), status)
return status, -1, ""
} else if r.URL.Path == "/assets/style.css" {
status := http.StatusOK
w.Header().Set("Content-Type", cssType)
w.WriteHeader(status)
if r.Method == "HEAD" {
return status, 0, ""
}
w.Write(cssBytes)
return status, int64(len(cssBytes)), ""
}
pathBytes := []byte(r.URL.Path)
strippedPath := r.URL.Path
if strippedPath[0] == '/' {
strippedPath = strippedPath[1:]
}
if host, ok := config.Hosts[r.URL.Hostname()]; ok {
for _, serve := range host.Paths {
matchedRegexp := serve.r != nil && serve.r.Match(pathBytes)
matchedPrefix := serve.r == nil && strings.HasPrefix(r.URL.Path, serve.Path)
if !matchedRegexp && !matchedPrefix {
continue
}
requireInput := serve.Input != "" || serve.SensitiveInput != ""
if r.URL.RawQuery == "" && requireInput {
if serve.SensitiveInput != "" {
// TODO
}
status := http.StatusInternalServerError
http.Error(w, "Gemini to HTML conversion is not supported for this page", status)
return status, -1, serve.Log
}
if matchedRegexp || matchedPrefix {
if serve.Root == "" || serve.FastCGI != "" {
status := http.StatusInternalServerError
http.Error(w, "Gemini to HTML conversion is not supported for this page", status)
return status, -1, serve.Log
}
var filePath string
if serve.Root != "" {
root := serve.Root
if root[len(root)-1] != '/' {
root += "/"
}
requestSplit := strings.Split(r.URL.Path, "/")
if !serve.SymLinks {
for i := 1; i < len(requestSplit); i++ {
info, err := os.Lstat(path.Join(root, strings.Join(requestSplit[1:i+1], "/")))
if err != nil || info.Mode()&os.ModeSymlink == os.ModeSymlink {
http.NotFound(w, r)
return http.StatusNotFound, -1, serve.Log
}
}
}
filePath = path.Join(root, strings.Join(requestSplit[1:], "/"))
}
fi, err := os.Stat(filePath)
if err != nil {
http.NotFound(w, r)
return http.StatusNotFound, -1, serve.Log
}
mode := fi.Mode()
hasTrailingSlash := len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] == '/'
if mode.IsDir() {
if !hasTrailingSlash {
u, err := url.Parse(r.URL.String())
if err != nil {
status := http.StatusInternalServerError
http.Error(w, "Failed to parse URL", status)
return status, -1, serve.Log
}
u.Path += "/"
status := http.StatusTemporaryRedirect
http.Redirect(w, r, u.String(), status)
return status, -1, serve.Log
}
var found bool
for _, indexFile := range indexFiles {
_, err := os.Stat(path.Join(filePath, indexFile))
if err == nil || os.IsExist(err) {
filePath = path.Join(filePath, indexFile)
found = true
break
}
}
if !found {
if serve.List {
dirList, err := buildDirList(r.URL, filePath)
if err != nil {
status := http.StatusInternalServerError
http.Error(w, "Failed to build directory listing", status)
return status, -1, serve.Log
}
result := gmitohtml.Convert([]byte(dirList), r.URL.String())
status := http.StatusOK
w.Header().Set("Content-Type", htmlType)
w.WriteHeader(status)
if r.Method == "HEAD" {
return status, 0, serve.Log
}
w.Write(result)
return status, int64(len(result)), serve.Log
}
http.NotFound(w, r)
return http.StatusNotFound, -1, serve.Log
}
} else if hasTrailingSlash && len(r.URL.Path) > 1 {
u, err := url.Parse(r.URL.String())
if err != nil {
status := http.StatusInternalServerError
http.Error(w, "Failed to parse URL", status)
return status, -1, serve.Log
}
u.Path = u.Path[:len(u.Path)-1]
status := http.StatusTemporaryRedirect
http.Redirect(w, r, u.String(), status)
return status, -1, serve.Log
}
data, err := ioutil.ReadFile(filePath)
if err != nil {
status := http.StatusInternalServerError
http.Error(w, err.Error(), status)
return status, -1, serve.Log
}
var result []byte
contentType := htmlType
fileExt := strings.ToLower(filepath.Ext(filePath))
if fileExt == ".gmi" || fileExt == ".gemini" {
result = gmitohtml.Convert([]byte(data), r.URL.String())
} else if fileExt == ".htm" || fileExt == ".html" {
result = data
} else {
result = data
contentType = plainType
}
status := http.StatusOK
w.Header().Set("Content-Type", contentType)
w.WriteHeader(status)
if r.Method == "HEAD" {
return status, 0, serve.Log
}
w.Write(result)
return status, int64(len(result)), serve.Log
}
}
}
http.NotFound(w, r)
return http.StatusNotFound, -1, ""
}
type responseWriter struct {
statusCode int
header http.Header
conn *tls.Conn
wroteHeader bool
}
func newResponseWriter(conn *tls.Conn) *responseWriter {
return &responseWriter{
header: http.Header{},
conn: conn,
}
}
func (w *responseWriter) Header() http.Header {
return w.header
}
func (w *responseWriter) Write(b []byte) (int, error) {
if !w.wroteHeader {
w.wroteHeader = true
w.WriteHeader(http.StatusOK)
}
return w.conn.Write(b)
}
func (w *responseWriter) WriteHeader(statusCode int) {
if w.wroteHeader {
return
}
w.statusCode = statusCode
statusText := http.StatusText(statusCode)
if statusText == "" {
statusText = "Unknown"
}
w.conn.Write([]byte(fmt.Sprintf("HTTP/1.1 %d %s\r\n", statusCode, statusText)))
w.header.Write(w.conn)
w.conn.Write([]byte("\r\n"))
}