added request body reader
This commit is contained in:
@@ -45,7 +45,7 @@ func (srv *Server) serve() {
|
||||
srv.logger.Info("listener socket closed")
|
||||
return
|
||||
}
|
||||
srv.logger.Warn(err)
|
||||
srv.logger.Error(err)
|
||||
continue
|
||||
}
|
||||
conn := newConnection(srv, i, accepted.(*net.TCPConn))
|
||||
|
||||
@@ -19,9 +19,13 @@ type SharedLocation struct {
|
||||
WritePassword string `toml:"write_password"`
|
||||
}
|
||||
|
||||
var ErrSharePathEmpty = errors.New("share.path is empty")
|
||||
var ErrShareUrlEmpty = errors.New("share.url is empty")
|
||||
var ErrShareUrlDoesntStartWithSlash = errors.New("share.url doesn't start with slash")
|
||||
|
||||
func (sh *SharedLocation) normalize(configDir string) error {
|
||||
if sh.Path == "" {
|
||||
return errors.New("share.path is empty")
|
||||
return ErrSharePathEmpty
|
||||
}
|
||||
// get absolute path for configDir
|
||||
configDirAbs, err := filepath.Abs(configDir)
|
||||
@@ -42,10 +46,10 @@ func (sh *SharedLocation) normalize(configDir string) error {
|
||||
sh.Path = filepath.Join(configDirAbs, shPathOsSep)
|
||||
|
||||
if sh.Url == "" {
|
||||
return errors.New("share.url is empty")
|
||||
return ErrShareUrlEmpty
|
||||
}
|
||||
if sh.Url[0] != '/' {
|
||||
return errors.New("share.url doesn't start with slash: '" + sh.Url + "'")
|
||||
return ErrShareUrlDoesntStartWithSlash
|
||||
}
|
||||
// remove trailing slash
|
||||
sh.Url = strings.TrimRight(sh.Url, "/")
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"serveg/internal/html"
|
||||
"serveg/internal/http"
|
||||
"serveg/internal/log"
|
||||
"strconv"
|
||||
@@ -52,12 +53,11 @@ func (conn *connection) handle() {
|
||||
conn.req, err = conn.http.ReadRequest()
|
||||
if err != nil {
|
||||
// invalid request
|
||||
conn.logConnError(err)
|
||||
if httpErr, ok := errors.AsType[*http.HttpError](err); ok {
|
||||
conn.log.Info("sending error response")
|
||||
conn.log.Warn(err)
|
||||
res = conn.http.CreateErrorResponse(conn.req, true, httpErr)
|
||||
} else {
|
||||
conn.log.Info("lingering close")
|
||||
conn.logError(err)
|
||||
conn.lingeringClose()
|
||||
return
|
||||
}
|
||||
@@ -68,31 +68,31 @@ func (conn *connection) handle() {
|
||||
switch conn.req.Method {
|
||||
case "GET", "HEAD":
|
||||
res = conn.serveFileOrDir()
|
||||
case "PUT":
|
||||
case "PUT", "OPTIONS":
|
||||
res = conn.handleUpload()
|
||||
default:
|
||||
res = conn.createErrorResponse(false, http.StatusMethodNotAllowed, conn.req.Method)
|
||||
}
|
||||
}
|
||||
|
||||
conn.log.Info("sending response: ", res.Status,
|
||||
" content-length: ", res.Headers["content-length"])
|
||||
conn.log.Trace("response headers: ", res.Headers)
|
||||
err = conn.http.SendResponse(res)
|
||||
if err != nil {
|
||||
conn.logConnError(err)
|
||||
conn.logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// it is set by CreateResponse
|
||||
if res.Headers["connection"] == "close" {
|
||||
conn.log.Info("lingering close")
|
||||
conn.lingeringClose()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *connection) logConnError(err error) {
|
||||
func (conn *connection) logError(err error) {
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
conn.log.Info("time out")
|
||||
} else if errors.Is(err, io.EOF) ||
|
||||
@@ -101,7 +101,7 @@ func (conn *connection) logConnError(err error) {
|
||||
|
||||
conn.log.Info("closed by client")
|
||||
} else {
|
||||
conn.log.Warn(err)
|
||||
conn.log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ func (conn *connection) logConnError(err error) {
|
||||
// so close() doesn't send RST and destroy the response before it's read.
|
||||
// Limits: 500 ms, 32 KB
|
||||
func (conn *connection) lingeringClose() {
|
||||
conn.log.Info("lingering close")
|
||||
if conn.socket.CloseWrite() != nil {
|
||||
return
|
||||
}
|
||||
@@ -116,6 +117,12 @@ func (conn *connection) lingeringClose() {
|
||||
_, _ = io.Copy(io.Discard, io.LimitReader(conn.socket, 32*1024))
|
||||
}
|
||||
|
||||
func (conn *connection) createErrorResponse(
|
||||
wantsClose bool, status http.StatusCode, msg string,
|
||||
) *http.Response {
|
||||
return conn.http.CreateErrorResponse(conn.req, wantsClose, http.NewHttpError(status, msg))
|
||||
}
|
||||
|
||||
func (conn *connection) serveFileOrDir() *http.Response {
|
||||
path, share, urlStatus := conn.srv.Conf.Share.MapUrl(conn.req.Url)
|
||||
// TODO implement authorization
|
||||
@@ -137,9 +144,15 @@ func (conn *connection) serveFileOrDir() *http.Response {
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *connection) createErrorResponse(wantsClose bool, status http.StatusCode, msg string,
|
||||
) *http.Response {
|
||||
return conn.http.CreateErrorResponse(conn.req, wantsClose, http.NewHttpError(status, msg))
|
||||
func (conn *connection) serveDirListing(dirPath string) *http.Response {
|
||||
htmlBytes, err := html.GenDirListingHtml(&conn.log, dirPath,
|
||||
conn.req.Url, conn.srv.Conf.HtmlTitle)
|
||||
if err != nil {
|
||||
return conn.createErrorResponse(false, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return conn.http.CreateResponse(http.StatusOK, conn.req, false).
|
||||
WithBodyBuf(htmlBytes, http.TextHtml)
|
||||
}
|
||||
|
||||
func (conn *connection) serveFile(filePath string) *http.Response {
|
||||
@@ -176,7 +189,7 @@ func (conn *connection) serveFile(filePath string) *http.Response {
|
||||
if conn.req.Method != "HEAD" {
|
||||
// file will be closed by SendResponse()
|
||||
fileOwnershipTransferred = true
|
||||
res.AttachBodyReader(f, size, http.GetMimeType(fileName))
|
||||
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.
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"serveg/internal/http"
|
||||
"serveg/internal/log"
|
||||
sh "serveg/internal/string_helper"
|
||||
)
|
||||
|
||||
const DateTimeTZ = "2006-01-02 15:04:05 UTC-07"
|
||||
|
||||
func htmlEscape(s string) string {
|
||||
sb := sh.StringBuilder{Buf: make([]byte, 0, len(s))}
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch b {
|
||||
case '&':
|
||||
sb.AddS("&")
|
||||
case '<':
|
||||
sb.AddS("<")
|
||||
case '>':
|
||||
sb.AddS(">")
|
||||
case '"':
|
||||
sb.AddS(""")
|
||||
case '\'':
|
||||
sb.AddS("'")
|
||||
default:
|
||||
sb.AddB(b)
|
||||
}
|
||||
}
|
||||
return sb.StringUnsafe()
|
||||
}
|
||||
|
||||
type listingRow struct {
|
||||
name string // html-escaped
|
||||
url string // url-encoded
|
||||
mtime string // formatted with DateTimeTZ
|
||||
size string // formatted with HumanBytes
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func (conn *connection) serveDirListing(dirPath string) *http.Response {
|
||||
htmlBytes, err := genDirListingHtml(&conn.log, dirPath,
|
||||
conn.req.Url, conn.srv.Conf.HtmlTitle)
|
||||
if err != nil {
|
||||
return conn.createErrorResponse(false, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
res := conn.http.CreateResponse(http.StatusOK, conn.req, false)
|
||||
res.AttachBodyBuf(htmlBytes, http.TextHtml)
|
||||
return res
|
||||
}
|
||||
|
||||
func genDirListingHtml(logger *log.LoggerContext, dirPath, reqUrl, pageTitle string) ([]byte, error) {
|
||||
dirEntries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dirs, files []listingRow
|
||||
var estimatedLen = 460 // html for page without entries
|
||||
for _, e := range dirEntries {
|
||||
einf, err := e.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
row := listingRow{
|
||||
name: htmlEscape(e.Name()),
|
||||
url: http.PercentEcodeUrl(path.Join(reqUrl, e.Name())),
|
||||
mtime: einf.ModTime().Format(DateTimeTZ),
|
||||
isDir: e.IsDir(),
|
||||
}
|
||||
if row.isDir {
|
||||
row.size = "DIR"
|
||||
dirs = append(dirs, row)
|
||||
} else {
|
||||
row.size = sh.HumanBytes(einf.Size())
|
||||
files = append(files, row)
|
||||
}
|
||||
|
||||
estimatedLen += len(row.name) + len(row.url) + len(row.mtime) + len(row.size)
|
||||
estimatedLen += 100 // html for row
|
||||
}
|
||||
|
||||
sb := sh.StringBuilder{Buf: make([]byte, 0, estimatedLen)}
|
||||
sb.AddS(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>`)
|
||||
sb.AddS(htmlEscape(pageTitle + " " + reqUrl))
|
||||
sb.AddS(`
|
||||
</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 2em; }
|
||||
td { padding: .2em 1em .2em 0; }
|
||||
td.size, td.date { color: #666; white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>`)
|
||||
sb.AddS("<h1>").AddS(htmlEscape(reqUrl)).AddS("</h1>")
|
||||
sb.AddS(`<table id="listing">`)
|
||||
sb.AddS(`<tr><td><a href="`)
|
||||
sb.AddS(http.PercentEcodeUrl(path.Dir(reqUrl)))
|
||||
sb.AddS(`">../</a></td></tr>`)
|
||||
|
||||
for i := 0; i < len(dirs); i++ {
|
||||
genListingRowHtml(&sb, &dirs[i])
|
||||
}
|
||||
for i := 0; i < len(files); i++ {
|
||||
genListingRowHtml(&sb, &files[i])
|
||||
}
|
||||
|
||||
sb.AddS(`
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
|
||||
realLen := sb.Len()
|
||||
if estimatedLen < realLen {
|
||||
logger.Warn("genDirListingHtml(): size prediction failed. estimatedLen: ",
|
||||
estimatedLen, " realLen: ", realLen)
|
||||
}
|
||||
return sb.Buf, nil
|
||||
}
|
||||
|
||||
func genListingRowHtml(sb *sh.StringBuilder, row *listingRow) {
|
||||
sb.AddS(`<tr>`)
|
||||
sb.AddS(`<td><a href="`).AddS(row.url).AddB('"')
|
||||
if !row.isDir {
|
||||
// tell browser to download file instead of opening in new tab
|
||||
sb.AddS(" download")
|
||||
}
|
||||
sb.AddB('>').AddS(row.name).AddS(`</a></td>`)
|
||||
sb.AddS(`<td class="size">`).AddS(row.size).AddS(`</td>`)
|
||||
sb.AddS(`<td class="date">`).AddS(row.mtime).AddS(`</td>`)
|
||||
sb.AddS(`</tr>`)
|
||||
}
|
||||
Reference in New Issue
Block a user