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.
255 lines
6.3 KiB
255 lines
6.3 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, "" |
|
} |
|
|
|
pathBytes := []byte(r.URL.Path) |
|
strippedPath := r.URL.Path |
|
if strippedPath[0] == '/' { |
|
strippedPath = strippedPath[1:] |
|
} |
|
|
|
if host, ok := config.Hosts[r.URL.Hostname()]; ok { |
|
if strings.HasSuffix(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, "" |
|
} |
|
|
|
if host.css != nil { |
|
w.Write(host.css) |
|
} else { |
|
w.Write(cssBytes) |
|
} |
|
return status, int64(len(cssBytes)), "" |
|
} |
|
|
|
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 { |
|
result = data |
|
if fileExt == ".htm" || fileExt == ".html" { |
|
// HTML content type already set |
|
} else if customType := config.Types[filepath.Ext(filePath)]; customType != "" { |
|
contentType = customType |
|
} else { |
|
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")) |
|
}
|
|
|