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")) }