diff --git a/README.md b/README.md index 01212f9..d0c0792 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,17 @@ mounts: # When proxy is defined, these requests are proxied to a HTTP/HTTPS address. proxy: http://archive.ubuntu.com/ubuntu/dists/bionic-updates/main/installer-amd64/current/images/hwe-netboot/ubuntu-installer/amd64/ # When true, the proxy path defined above gets a suffix to the Path prefix appended to it. - proxyAppendSuffix: true + appendSuffix: true + + - path: /subdir + # When true, all paths starting with this prefix use this mount. + pathIsPrefix: true + # Provides a path on the host to find the files. + # So that localDir: /tftpboot path: /subdir and client request: /subdir/file.x so that the host + # path becomes /tfptboot/file.x + localDir: /tftpboot + # When true, the localDir path defined above gets a suffix to the Path prefix appended to it. + appendSuffix: true - path: /install.ipxe # The templating context provides access to: .LocalIP, .RemoteIP, .HttpBaseUrl and .Manifest. diff --git a/httpd/handler.go b/httpd/handler.go index b363fff..cb4aa67 100644 --- a/httpd/handler.go +++ b/httpd/handler.go @@ -3,17 +3,20 @@ package httpd import ( "bytes" _ "embed" - mfest "github.com/DSpeichert/netbootd/manifest" - "github.com/DSpeichert/netbootd/static" - "github.com/Masterminds/sprig" "io" "net" "net/http" "net/http/httputil" "net/url" + "os" + "path/filepath" "strings" "text/template" "time" + + mfest "github.com/DSpeichert/netbootd/manifest" + "github.com/DSpeichert/netbootd/static" + "github.com/Masterminds/sprig" ) type Handler struct { @@ -129,8 +132,41 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } rp.ServeHTTP(w, r) return + } else if mount.LocalDir != "" { + path := filepath.Join(mount.LocalDir, mount.Path) + + if mount.AppendSuffix { + path = filepath.Join(mount.LocalDir, strings.TrimPrefix(r.URL.Path, mount.Path)) + } + + if !strings.HasPrefix(path, mount.LocalDir) { + h.server.logger.Error(). + Err(err). + Msgf("Requested path is invalid: %q", path) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + f, err := os.Open(path) + if err != nil { + h.server.logger.Error(). + Err(err). + Msgf("Could not get file from local dir: %q", path) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + stat, err := f.Stat() + if err != nil { + h.server.logger.Error(). + Err(err). + Msgf("could not stat file: %q", path) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.ServeContent(w, r, r.URL.Path, stat.ModTime(), f) + return } else { - // mount has neither .Path nor .Proxy defined + // mount has neither .Path, .Proxy nor .LocalDir defined h.server.logger.Error(). Str("path", r.RequestURI). Str("client", raddr.String()). diff --git a/manifest/io.go b/manifest/io.go index ccc2c7e..051c0ac 100644 --- a/manifest/io.go +++ b/manifest/io.go @@ -2,12 +2,18 @@ package manifest import ( "encoding/json" + "fmt" "gopkg.in/yaml.v2" + "path/filepath" ) func ManifestFromJson(content []byte) (manifest Manifest, err error) { err = json.Unmarshal(content, &manifest) - return + if err != nil { + return manifest, err + } + + return manifest, manifest.Validate() } func (m *Manifest) ToJson() ([]byte, error) { @@ -16,7 +22,23 @@ func (m *Manifest) ToJson() ([]byte, error) { func ManifestFromYaml(content []byte) (manifest Manifest, err error) { err = yaml.Unmarshal(content, &manifest) - return + if err != nil { + return manifest, err + } + + return manifest, manifest.Validate() +} + +func (m Manifest) Validate() error { + for _, mount := range m.Mounts { + if mount.LocalDir != "" { + if !filepath.IsAbs(mount.LocalDir) { + return fmt.Errorf("localDir needs to be absolute path") + } + } + } + + return nil } func (m *Manifest) ToYaml() ([]byte, error) { diff --git a/manifest/schema.go b/manifest/schema.go index 62b1e3a..c2d5074 100644 --- a/manifest/schema.go +++ b/manifest/schema.go @@ -40,13 +40,18 @@ type Mount struct { // The proxy destination used when handling requests. // Mutually exclusive with Content option. Proxy string - // If PathIsPrefix is true and ProxyAppendSuffix is true, the suffix to Path Prefix will also be appended to Proxy. + // If PathIsPrefix is true and AppendSuffix is true, the suffix to Path Prefix will also be appended to Proxy Or LocalDir. // Otherwise, it will be many to one proxy. - ProxyAppendSuffix bool `yaml:"proxyAppendSuffix"` + AppendSuffix bool `yaml:"appendSuffix"` // Provides content template (passed through template/text) to serve. // Mutually exclusive with Proxy option. Content string + + // Provides a path on the host to find the files. + // So that LocalDir: /tftpboot path: /subdir and client requests: /subdir/file.x the path on the host + // becomes /tfptboot/file.x + LocalDir string `yaml:"localDir"` } func (m Mount) ProxyDirector() (func(req *http.Request), error) { @@ -72,7 +77,7 @@ func (m Mount) ProxyDirector() (func(req *http.Request), error) { req.Header.Set("User-Agent", "") } - if m.ProxyAppendSuffix { + if m.AppendSuffix { req.URL.Path = target.Path + strings.TrimPrefix(req.URL.Path, m.Path) req.URL.RawPath = target.RawPath + strings.TrimPrefix(req.URL.RawPath, m.Path) } else { diff --git a/tftpd/handler.go b/tftpd/handler.go index 1e1f4ac..7f96a2b 100644 --- a/tftpd/handler.go +++ b/tftpd/handler.go @@ -5,15 +5,18 @@ import ( _ "embed" "errors" "fmt" - mfest "github.com/DSpeichert/netbootd/manifest" - "github.com/DSpeichert/netbootd/static" - "github.com/Masterminds/sprig" - "github.com/pin/tftp" "io" "net/http" "net/url" + "os" + "path/filepath" "strings" "text/template" + + mfest "github.com/DSpeichert/netbootd/manifest" + "github.com/DSpeichert/netbootd/static" + "github.com/Masterminds/sprig" + "github.com/pin/tftp" ) func (server *Server) tftpReadHandler(filename string, rf io.ReaderFrom) error { @@ -64,7 +67,7 @@ func (server *Server) tftpReadHandler(filename string, rf io.ReaderFrom) error { if mount.Proxy != "" { url := mount.Proxy - if mount.ProxyAppendSuffix { + if mount.AppendSuffix { url = url + strings.TrimPrefix(filename, mount.Path) } @@ -156,6 +159,53 @@ func (server *Server) tftpReadHandler(filename string, rf io.ReaderFrom) error { return err } + server.logger.Info(). + Err(err). + Str("path", filename). + Str("client", raddr.IP.String()). + Int64("sent", n). + Msg("transfer finished") + } else if mount.LocalDir != "" { + path := filepath.Join(mount.LocalDir, mount.Path) + + if mount.AppendSuffix { + path = filepath.Join(mount.LocalDir, strings.TrimPrefix(filename, mount.Path)) + } + + if !strings.HasPrefix(path, mount.LocalDir) { + err := fmt.Errorf("requested path is invalid") + server.logger.Error(). + Err(err). + Msgf("Requested path is invalid: %q", path) + return err + } + + f, err := os.Open(path) + if err != nil { + server.logger.Error(). + Err(err). + Msgf("Could not get file from local dir: %q", filename) + + return err + } + + stat, err := f.Stat() + if err != nil { + server.logger.Error(). + Err(err). + Msgf("Could not stat file: %q", path) + return err + } + + rf.(tftp.OutgoingTransfer).SetSize(int64(stat.Size())) + + n, err := rf.ReadFrom(f) + if err != nil { + server.logger.Error(). + Msgf("ReadFrom failed: %v", err) + return err + } + server.logger.Info(). Err(err). Str("path", filename).