mirror of
https://github.com/tinygo-org/net.git
synced 2026-08-16 02:33:29 +00:00
Upgrade net package from Go 1.21.4 to Go 1.26.2
Backport upstream Go standard library changes to TinyGo's net package. Unmodified files (18) replaced directly from Go 1.26.2 source. Modified files (22) merged via 3-way diff, preserving all TinyGo netdev adaptations, TINYGO markers, and embedded-device constraints. Notable upstream changes included: - net/http: cookie handling improvements, fs enhancements, reverse proxy updates, chunked encoding fixes - net/http: ServeMux routing and pattern matching updates - net: IP parsing and MAC address handling improvements - Various doc link syntax modernization (e.g. [Dial], [Buffers]) TinyGo-only files (netdev.go, tlssock.go) unchanged. All TINYGO comment markers preserved.
This commit is contained in:
+175
-51
@@ -1,6 +1,4 @@
|
||||
// TINYGO: The following is copied from Go 1.21.4 official implementation.
|
||||
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// TINYGO: The following is copied from Go 1.26.2 official implementation.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@@ -11,6 +9,7 @@ package http
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"internal/godebug"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
@@ -26,12 +25,12 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Dir implements FileSystem using the native file system restricted to a
|
||||
// A Dir implements [FileSystem] using the native file system restricted to a
|
||||
// specific directory tree.
|
||||
//
|
||||
// While the FileSystem.Open method takes '/'-separated paths, a Dir's string
|
||||
// value is a filename on the native file system, not a URL, so it is separated
|
||||
// by filepath.Separator, which isn't necessarily '/'.
|
||||
// While the [FileSystem.Open] method takes '/'-separated paths, a Dir's string
|
||||
// value is a directory path on the native file system, not a URL, so it is separated
|
||||
// by [filepath.Separator], which isn't necessarily '/'.
|
||||
//
|
||||
// Note that Dir could expose sensitive files and directories. Dir will follow
|
||||
// symlinks pointing out of the directory tree, which can be especially dangerous
|
||||
@@ -68,19 +67,27 @@ func mapOpenError(originalErr error, name string, sep rune, stat func(string) (f
|
||||
return originalErr
|
||||
}
|
||||
|
||||
// Open implements FileSystem using os.Open, opening files for reading rooted
|
||||
// errInvalidUnsafePath is returned by Dir.Open when the call to
|
||||
// filepath.Localize fails. filepath.Localize returns an error if the path
|
||||
// cannot be represented by the operating system.
|
||||
var errInvalidUnsafePath = errors.New("http: invalid or unsafe file path")
|
||||
|
||||
// Open implements [FileSystem] using [os.Open], opening files for reading rooted
|
||||
// and relative to the directory d.
|
||||
func (d Dir) Open(name string) (File, error) {
|
||||
// TINYGO: internal/safefilepath isn't avail until 1.20, so keep pre 1.20
|
||||
// TINYGO: code here until TinyGo min Go version is >= 1.20.
|
||||
if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
|
||||
return nil, errors.New("http: invalid character in file path")
|
||||
path := path.Clean("/" + name)[1:]
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
path, err := filepath.Localize(path)
|
||||
if err != nil {
|
||||
return nil, errInvalidUnsafePath
|
||||
}
|
||||
dir := string(d)
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
|
||||
fullName := filepath.Join(dir, path)
|
||||
f, err := os.Open(fullName)
|
||||
if err != nil {
|
||||
return nil, mapOpenError(err, fullName, filepath.Separator, os.Stat)
|
||||
@@ -91,18 +98,18 @@ func (d Dir) Open(name string) (File, error) {
|
||||
// A FileSystem implements access to a collection of named files.
|
||||
// The elements in a file path are separated by slash ('/', U+002F)
|
||||
// characters, regardless of host operating system convention.
|
||||
// See the FileServer function to convert a FileSystem to a Handler.
|
||||
// See the [FileServer] function to convert a FileSystem to a [Handler].
|
||||
//
|
||||
// This interface predates the fs.FS interface, which can be used instead:
|
||||
// the FS adapter function converts an fs.FS to a FileSystem.
|
||||
// This interface predates the [fs.FS] interface, which can be used instead:
|
||||
// the [FS] adapter function converts an fs.FS to a FileSystem.
|
||||
type FileSystem interface {
|
||||
Open(name string) (File, error)
|
||||
}
|
||||
|
||||
// A File is returned by a FileSystem's Open method and can be
|
||||
// served by the FileServer implementation.
|
||||
// A File is returned by a [FileSystem]'s Open method and can be
|
||||
// served by the [FileServer] implementation.
|
||||
//
|
||||
// The methods should behave the same as those on an *os.File.
|
||||
// The methods should behave the same as those on an [*os.File].
|
||||
type File interface {
|
||||
io.Closer
|
||||
io.Reader
|
||||
@@ -153,6 +160,8 @@ func dirList(w ResponseWriter, r *Request, f File) {
|
||||
sort.Slice(dirs, func(i, j int) bool { return dirs.name(i) < dirs.name(j) })
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, "<!doctype html>\n")
|
||||
fmt.Fprintf(w, "<meta name=\"viewport\" content=\"width=device-width\">\n")
|
||||
fmt.Fprintf(w, "<pre>\n")
|
||||
for i, n := 0, dirs.len(); i < n; i++ {
|
||||
name := dirs.name(i)
|
||||
@@ -168,8 +177,42 @@ func dirList(w ResponseWriter, r *Request, f File) {
|
||||
fmt.Fprintf(w, "</pre>\n")
|
||||
}
|
||||
|
||||
// GODEBUG=httpservecontentkeepheaders=1 restores the pre-1.23 behavior of not deleting
|
||||
// Cache-Control, Content-Encoding, Etag, or Last-Modified headers on ServeContent errors.
|
||||
var httpservecontentkeepheaders = godebug.New("httpservecontentkeepheaders")
|
||||
|
||||
// serveError serves an error from ServeFile, ServeFileFS, and ServeContent.
|
||||
// Because those can all be configured by the caller by setting headers like
|
||||
// Etag, Last-Modified, and Cache-Control to send on a successful response,
|
||||
// the error path needs to clear them, since they may not be meant for errors.
|
||||
func serveError(w ResponseWriter, text string, code int) {
|
||||
h := w.Header()
|
||||
|
||||
nonDefault := false
|
||||
for _, k := range []string{
|
||||
"Cache-Control",
|
||||
"Content-Encoding",
|
||||
"Etag",
|
||||
"Last-Modified",
|
||||
} {
|
||||
if !h.has(k) {
|
||||
continue
|
||||
}
|
||||
if httpservecontentkeepheaders.Value() == "1" {
|
||||
nonDefault = true
|
||||
} else {
|
||||
h.Del(k)
|
||||
}
|
||||
}
|
||||
if nonDefault {
|
||||
httpservecontentkeepheaders.IncNonDefault()
|
||||
}
|
||||
|
||||
Error(w, text, code)
|
||||
}
|
||||
|
||||
// ServeContent replies to the request using the content in the
|
||||
// provided ReadSeeker. The main benefit of ServeContent over io.Copy
|
||||
// provided ReadSeeker. The main benefit of ServeContent over [io.Copy]
|
||||
// is that it handles Range requests properly, sets the MIME type, and
|
||||
// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
|
||||
// and If-Range requests.
|
||||
@@ -177,7 +220,7 @@ func dirList(w ResponseWriter, r *Request, f File) {
|
||||
// If the response's Content-Type header is not set, ServeContent
|
||||
// first tries to deduce the type from name's file extension and,
|
||||
// if that fails, falls back to reading the first block of the content
|
||||
// and passing it to DetectContentType.
|
||||
// and passing it to [DetectContentType].
|
||||
// The name is otherwise unused; in particular it can be empty and is
|
||||
// never sent in the response.
|
||||
//
|
||||
@@ -188,11 +231,17 @@ func dirList(w ResponseWriter, r *Request, f File) {
|
||||
//
|
||||
// The content's Seek method must work: ServeContent uses
|
||||
// a seek to the end of the content to determine its size.
|
||||
// Note that [*os.File] implements the [io.ReadSeeker] interface.
|
||||
//
|
||||
// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
|
||||
// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
|
||||
//
|
||||
// Note that *os.File implements the io.ReadSeeker interface.
|
||||
// If an error occurs when serving the request (for example, when
|
||||
// handling an invalid range request), ServeContent responds with an
|
||||
// error message. By default, ServeContent strips the Cache-Control,
|
||||
// Content-Encoding, ETag, and Last-Modified headers from error responses.
|
||||
// The GODEBUG setting httpservecontentkeepheaders=1 causes ServeContent
|
||||
// to preserve these headers.
|
||||
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
|
||||
sizeFunc := func() (int64, error) {
|
||||
size, err := content.Seek(0, io.SeekEnd)
|
||||
@@ -244,7 +293,7 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
|
||||
ctype = DetectContentType(buf[:n])
|
||||
_, err := content.Seek(0, io.SeekStart) // rewind to output whole file
|
||||
if err != nil {
|
||||
Error(w, "seeker can't seek", StatusInternalServerError)
|
||||
serveError(w, "seeker can't seek", StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -255,12 +304,12 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
|
||||
|
||||
size, err := sizeFunc()
|
||||
if err != nil {
|
||||
Error(w, err.Error(), StatusInternalServerError)
|
||||
serveError(w, err.Error(), StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if size < 0 {
|
||||
// Should never happen but just to be sure
|
||||
Error(w, "negative content size computed", StatusInternalServerError)
|
||||
serveError(w, "negative content size computed", StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -282,7 +331,7 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
|
||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", size))
|
||||
fallthrough
|
||||
default:
|
||||
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
|
||||
serveError(w, err.Error(), StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -308,7 +357,7 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
|
||||
// multipart responses."
|
||||
ra := ranges[0]
|
||||
if _, err := content.Seek(ra.start, io.SeekStart); err != nil {
|
||||
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
|
||||
serveError(w, err.Error(), StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
sendSize = ra.length
|
||||
@@ -345,10 +394,35 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
|
||||
}
|
||||
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
if w.Header().Get("Content-Encoding") == "" {
|
||||
|
||||
// We should be able to unconditionally set the Content-Length here.
|
||||
//
|
||||
// However, there is a pattern observed in the wild that this breaks:
|
||||
// The user wraps the ResponseWriter in one which gzips data written to it,
|
||||
// and sets "Content-Encoding: gzip".
|
||||
//
|
||||
// The user shouldn't be doing this; the serveContent path here depends
|
||||
// on serving seekable data with a known length. If you want to compress
|
||||
// on the fly, then you shouldn't be using ServeFile/ServeContent, or
|
||||
// you should compress the entire file up-front and provide a seekable
|
||||
// view of the compressed data.
|
||||
//
|
||||
// However, since we've observed this pattern in the wild, and since
|
||||
// setting Content-Length here breaks code that mostly-works today,
|
||||
// skip setting Content-Length if the user set Content-Encoding.
|
||||
//
|
||||
// If this is a range request, always set Content-Length.
|
||||
// If the user isn't changing the bytes sent in the ResponseWrite,
|
||||
// the Content-Length will be correct.
|
||||
// If the user is changing the bytes sent, then the range request wasn't
|
||||
// going to work properly anyway and we aren't worse off.
|
||||
//
|
||||
// A possible future improvement on this might be to look at the type
|
||||
// of the ResponseWriter, and always set Content-Length if it's one
|
||||
// that we recognize.
|
||||
if len(ranges) > 0 || w.Header().Get("Content-Encoding") == "" {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
|
||||
}
|
||||
|
||||
w.WriteHeader(code)
|
||||
|
||||
if r.Method != "HEAD" {
|
||||
@@ -449,8 +523,7 @@ func checkIfUnmodifiedSince(r *Request, modtime time.Time) condResult {
|
||||
// The Last-Modified header truncates sub-second precision so
|
||||
// the modtime needs to be truncated too.
|
||||
modtime = modtime.Truncate(time.Second)
|
||||
// TINYGO: time.Compare not until Go 1.20
|
||||
if modtime.Before(t) || modtime.Equal(t) {
|
||||
if ret := modtime.Compare(t); ret <= 0 {
|
||||
return condTrue
|
||||
}
|
||||
return condFalse
|
||||
@@ -501,8 +574,7 @@ func checkIfModifiedSince(r *Request, modtime time.Time) condResult {
|
||||
// The Last-Modified header truncates sub-second precision so
|
||||
// the modtime needs to be truncated too.
|
||||
modtime = modtime.Truncate(time.Second)
|
||||
// TINYGO: time.Compare not until Go 1.20
|
||||
if modtime.Before(t) || modtime.Equal(t) {
|
||||
if ret := modtime.Compare(t); ret <= 0 {
|
||||
return condFalse
|
||||
}
|
||||
return condTrue
|
||||
@@ -618,7 +690,7 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec
|
||||
f, err := fs.Open(name)
|
||||
if err != nil {
|
||||
msg, code := toHTTPError(err)
|
||||
Error(w, msg, code)
|
||||
serveError(w, msg, code)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
@@ -626,7 +698,7 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec
|
||||
d, err := f.Stat()
|
||||
if err != nil {
|
||||
msg, code := toHTTPError(err)
|
||||
Error(w, msg, code)
|
||||
serveError(w, msg, code)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -639,11 +711,16 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec
|
||||
localRedirect(w, r, path.Base(url)+"/")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if url[len(url)-1] == '/' {
|
||||
localRedirect(w, r, "../"+path.Base(url))
|
||||
} else if url[len(url)-1] == '/' {
|
||||
base := path.Base(url)
|
||||
if base == "/" || base == "." {
|
||||
// The FileSystem maps a path like "/" or "/./" to a file instead of a directory.
|
||||
msg := "http: attempting to traverse a non-directory"
|
||||
serveError(w, msg, StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
localRedirect(w, r, "../"+base)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,6 +773,9 @@ func toHTTPError(err error) (msg string, httpStatus int) {
|
||||
if errors.Is(err, fs.ErrPermission) {
|
||||
return "403 Forbidden", StatusForbidden
|
||||
}
|
||||
if errors.Is(err, errInvalidUnsafePath) {
|
||||
return "404 page not found", StatusNotFound
|
||||
}
|
||||
// Default:
|
||||
return "500 Internal Server Error", StatusInternalServerError
|
||||
}
|
||||
@@ -716,17 +796,17 @@ func localRedirect(w ResponseWriter, r *Request, newPath string) {
|
||||
// If the provided file or directory name is a relative path, it is
|
||||
// interpreted relative to the current directory and may ascend to
|
||||
// parent directories. If the provided name is constructed from user
|
||||
// input, it should be sanitized before calling ServeFile.
|
||||
// input, it should be sanitized before calling [ServeFile].
|
||||
//
|
||||
// As a precaution, ServeFile will reject requests where r.URL.Path
|
||||
// contains a ".." path element; this protects against callers who
|
||||
// might unsafely use filepath.Join on r.URL.Path without sanitizing
|
||||
// might unsafely use [filepath.Join] on r.URL.Path without sanitizing
|
||||
// it and then use that filepath.Join result as the name argument.
|
||||
//
|
||||
// As another special case, ServeFile redirects any request where r.URL.Path
|
||||
// ends in "/index.html" to the same path, without the final
|
||||
// "index.html". To avoid such redirects either modify the path or
|
||||
// use ServeContent.
|
||||
// use [ServeContent].
|
||||
//
|
||||
// Outside of those two special cases, ServeFile does not use
|
||||
// r.URL.Path for selecting the file or directory to serve; only the
|
||||
@@ -738,18 +818,51 @@ func ServeFile(w ResponseWriter, r *Request, name string) {
|
||||
// here and ".." may not be wanted.
|
||||
// Note that name might not contain "..", for example if code (still
|
||||
// incorrectly) used filepath.Join(myDir, r.URL.Path).
|
||||
Error(w, "invalid URL path", StatusBadRequest)
|
||||
serveError(w, "invalid URL path", StatusBadRequest)
|
||||
return
|
||||
}
|
||||
dir, file := filepath.Split(name)
|
||||
serveFile(w, r, Dir(dir), file, false)
|
||||
}
|
||||
|
||||
// ServeFileFS replies to the request with the contents
|
||||
// of the named file or directory from the file system fsys.
|
||||
// The files provided by fsys must implement [io.Seeker].
|
||||
//
|
||||
// If the provided name is constructed from user input, it should be
|
||||
// sanitized before calling [ServeFileFS].
|
||||
//
|
||||
// As a precaution, ServeFileFS will reject requests where r.URL.Path
|
||||
// contains a ".." path element; this protects against callers who
|
||||
// might unsafely use [filepath.Join] on r.URL.Path without sanitizing
|
||||
// it and then use that filepath.Join result as the name argument.
|
||||
//
|
||||
// As another special case, ServeFileFS redirects any request where r.URL.Path
|
||||
// ends in "/index.html" to the same path, without the final
|
||||
// "index.html". To avoid such redirects either modify the path or
|
||||
// use [ServeContent].
|
||||
//
|
||||
// Outside of those two special cases, ServeFileFS does not use
|
||||
// r.URL.Path for selecting the file or directory to serve; only the
|
||||
// file or directory provided in the name argument is used.
|
||||
func ServeFileFS(w ResponseWriter, r *Request, fsys fs.FS, name string) {
|
||||
if containsDotDot(r.URL.Path) {
|
||||
// Too many programs use r.URL.Path to construct the argument to
|
||||
// serveFile. Reject the request under the assumption that happened
|
||||
// here and ".." may not be wanted.
|
||||
// Note that name might not contain "..", for example if code (still
|
||||
// incorrectly) used filepath.Join(myDir, r.URL.Path).
|
||||
serveError(w, "invalid URL path", StatusBadRequest)
|
||||
return
|
||||
}
|
||||
serveFile(w, r, FS(fsys), name, false)
|
||||
}
|
||||
|
||||
func containsDotDot(v string) bool {
|
||||
if !strings.Contains(v, "..") {
|
||||
return false
|
||||
}
|
||||
for _, ent := range strings.FieldsFunc(v, isSlashRune) {
|
||||
for ent := range strings.FieldsFuncSeq(v, isSlashRune) {
|
||||
if ent == ".." {
|
||||
return true
|
||||
}
|
||||
@@ -835,9 +948,9 @@ func (f ioFile) Readdir(count int) ([]fs.FileInfo, error) {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// FS converts fsys to a FileSystem implementation,
|
||||
// for use with FileServer and NewFileTransport.
|
||||
// The files provided by fsys must implement io.Seeker.
|
||||
// FS converts fsys to a [FileSystem] implementation,
|
||||
// for use with [FileServer] and [NewFileTransport].
|
||||
// The files provided by fsys must implement [io.Seeker].
|
||||
func FS(fsys fs.FS) FileSystem {
|
||||
return ioFS{fsys}
|
||||
}
|
||||
@@ -850,17 +963,28 @@ func FS(fsys fs.FS) FileSystem {
|
||||
// "index.html".
|
||||
//
|
||||
// To use the operating system's file system implementation,
|
||||
// use http.Dir:
|
||||
// use [http.Dir]:
|
||||
//
|
||||
// http.Handle("/", http.FileServer(http.Dir("/tmp")))
|
||||
//
|
||||
// To use an fs.FS implementation, use http.FS to convert it:
|
||||
//
|
||||
// http.Handle("/", http.FileServer(http.FS(fsys)))
|
||||
// To use an [fs.FS] implementation, use [http.FileServerFS] instead.
|
||||
func FileServer(root FileSystem) Handler {
|
||||
return &fileHandler{root}
|
||||
}
|
||||
|
||||
// FileServerFS returns a handler that serves HTTP requests
|
||||
// with the contents of the file system fsys.
|
||||
// The files provided by fsys must implement [io.Seeker].
|
||||
//
|
||||
// As a special case, the returned file server redirects any request
|
||||
// ending in "/index.html" to the same path, without the final
|
||||
// "index.html".
|
||||
//
|
||||
// http.Handle("/", http.FileServerFS(fsys))
|
||||
func FileServerFS(root fs.FS) Handler {
|
||||
return FileServer(FS(root))
|
||||
}
|
||||
|
||||
func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
upath := r.URL.Path
|
||||
if !strings.HasPrefix(upath, "/") {
|
||||
@@ -898,7 +1022,7 @@ func parseRange(s string, size int64) ([]httpRange, error) {
|
||||
}
|
||||
var ranges []httpRange
|
||||
noOverlap := false
|
||||
for _, ra := range strings.Split(s[len(b):], ",") {
|
||||
for ra := range strings.SplitSeq(s[len(b):], ",") {
|
||||
ra = textproto.TrimString(ra)
|
||||
if ra == "" {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user