forked from tslocum/twins
245 lines
6.1 KiB
Go
245 lines
6.1 KiB
Go
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"))
|
|
}
|