package server import ( "fmt" "os" "path" "serveg/internal/html" "serveg/internal/http" "time" ) func (conn *TcpConnection) GET_HEAD(req *http.Request) *http.Response { path, share, urlStatus := conn.srv.Conf.Share.MapUrl(req.Url) // handle authorization if share != nil { access := checkSharePasswords(share, req) if access < AccessReadOnly { return conn.createErrorResponse(req, false, http.StatusUnauthorized, "reader password required"). WithHeader("www-authenticate", `Basic realm="`+share.Url+` (read)", charset="UTF-8"`) } } switch urlStatus { case UrlFile: return conn.serveFile(req, path) case UrlDir: return conn.serveDirListing(req, path) case UrlShareNotFound, UrlDoesntExistInShare: return conn.createErrorResponse(req, false, http.StatusNotFound, req.Url) case UrlForbidden: return conn.createErrorResponse(req, false, http.StatusForbidden, req.Url) case UrlIncorrect: return conn.createErrorResponse(req, false, http.StatusBadRequest, "invalid url '"+req.Url+"'") default: panic(fmt.Sprintf("unsupported UrlStatus: %d", int32(urlStatus))) } } func (conn *TcpConnection) serveDirListing(req *http.Request, dirPath string) *http.Response { htmlBytes, err := html.GenDirListingHtml(&conn.log, dirPath, req.Url, conn.srv.Conf.HtmlTitle) if err != nil { return conn.createErrorResponse(req, false, http.StatusInternalServerError, err.Error()) } return conn.http.CreateResponse(http.StatusOK, req, false). WithBodyBuf(htmlBytes, http.TextHtml) } func (conn *TcpConnection) serveFile(req *http.Request, filePath string) *http.Response { f, err := os.Open(filePath) if err != nil { return conn.createErrorResponse(req, false, http.StatusInternalServerError, err.Error()) } var fileOwnershipTransferred bool defer func() { if !fileOwnershipTransferred { _ = f.Close() } }() finf, err := f.Stat() if err != nil { return conn.createErrorResponse(req, false, http.StatusInternalServerError, err.Error()) } size := finf.Size() // time is received with second precision // if not truncate, mtime will always be > ifModifiedSince mtime := finf.ModTime().UTC().Truncate(time.Second) ifModifiedSince, errTime := time.Parse(http.HeaderTimeFormat, req.Headers["if-modified-since"]) notModified := errTime == nil && !mtime.After(ifModifiedSince) if notModified { return conn.http.CreateResponse(http.StatusNotModified, req, false) } res := conn.http.CreateResponse(http.StatusOK, req, false) fileName := path.Base(filePath) if req.Method != "HEAD" { // file will be closed by SendResponse() fileOwnershipTransferred = true res.WithBodyReader(f, size, http.GetMimeType(fileName)) } // Tell browser to not use its own caching, remember last-modified // On next request browser will send if-modified-since and let server check if file changed. res.Headers["cache-control"] = "no-cache" res.Headers["last-modified"] = mtime.Format(http.HeaderTimeFormat) res.Headers["content-disposition"] = http.ContentDisposition("inline", fileName) //TODO: accept "range" header and return StatusPartialContent // res.Headers["accept-ranges"] = "bytes" return res }