engine: resources: http: Improve groupability of resources

This improves groupability of http resources so that we can have fancier
kinds!
This commit is contained in:
James Shubin
2024-01-16 19:43:09 -05:00
parent 9dd5dfdde2
commit 0711d05232

View File

@@ -53,6 +53,28 @@ func init() {
engine.RegisterResource(httpFileKind, func() engine.Res { return &HTTPFileRes{} })
}
// HTTPServerGroupableRes is the interface that you must implement if you want
// to allow a resource the ability to be grouped into the http server resource.
// As an added safety, the Kind must also begin with "http:", and not have more
// than one colon, or it must begin with http:server:, and not have any further
// colons to avoid accidents of unwanted grouping.
type HTTPServerGroupableRes interface {
engine.Res
// ParentName is used to limit which resources autogroup into this one.
// If it's empty then it's ignored, otherwise it must match the Name of
// the parent to get grouped.
ParentName() string
// AcceptHTTP determines whether this will respond to this request.
// Return nil to accept, or any error to pass. This should be
// deterministic (pure) and fast.
AcceptHTTP(req *http.Request) error
// ServeHTTP is the standard HTTP handler that will be used for this.
http.Handler // ServeHTTP(w http.ResponseWriter, req *http.Request)
}
// HTTPServerRes is an http server resource. It serves files, but does not
// actually apply any state. The name is used as the address to listen on,
// unless the Address field is specified, and in that case it is used instead.
@@ -75,7 +97,7 @@ func init() {
type HTTPServerRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable // XXX: add autoedge support
traits.Groupable // can have HTTPFileRes grouped into it
traits.Groupable // can have HTTPFileRes and others grouped into it
init *engine.Init
@@ -171,6 +193,75 @@ func (obj *HTTPServerRes) getShutdownTimeout() *uint64 {
return obj.Timeout // might be nil
}
// AcceptHTTP determines whether we will respond to this request. Return nil to
// accept, or any error to pass. In this particular case, it accepts for the
// Root directory handler, but it happens to be implemented with this signature
// in case it gets moved. It doesn't intentionally match the
// HTTPServerGroupableRes interface.
func (obj *HTTPServerRes) AcceptHTTP(req *http.Request) error {
// Look in root if we have one, and we haven't got a file yet...
if obj.Root == "" {
return fmt.Errorf("no Root directory")
}
return nil
}
// ServeHTTP is the standard HTTP handler that will be used here. In this
// particular case, it serves the Root directory handler, but it happens to be
// implemented with this signature in case it gets moved. It doesn't
// intentionally match the HTTPServerGroupableRes interface.
func (obj *HTTPServerRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// We only allow GET at the moment.
if req.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
requestPath := req.URL.Path // TODO: is this what we want here?
p := filepath.Join(obj.Root, requestPath) // normal unsafe!
if !strings.HasPrefix(p, obj.Root) { // root ends with /
// user might have tried a ../../etc/passwd hack
obj.init.Logf("join inconsistency: %s", p)
http.NotFound(w, req) // lie to them...
return
}
if HTTPUseSecureJoin {
var err error
p, err = securefilepath.SecureJoin(obj.Root, requestPath)
if err != nil {
obj.init.Logf("secure join fail: %s", p)
http.NotFound(w, req) // lie to them...
return
}
}
if obj.init.Debug {
obj.init.Logf("Got file at root: %s", p)
}
handle, err := os.Open(p)
if err != nil {
obj.init.Logf("could not open: %s", p)
msg, httpStatus := toHTTPError(err)
http.Error(w, msg, httpStatus)
return
}
// Determine the last-modified time if we can.
modtime := time.Now()
fi, err := handle.Stat()
if err == nil {
modtime = fi.ModTime()
}
// TODO: if Stat errors, should we fail the whole thing?
// XXX: is requestPath what we want for the name field?
http.ServeContent(w, req, requestPath, modtime, handle)
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
return
}
// Validate checks if the resource data structure was populated correctly.
func (obj *HTTPServerRes) Validate() error {
if obj.getAddress() == "" {
@@ -305,6 +396,8 @@ func (obj *HTTPServerRes) Watch(ctx context.Context) error {
defer obj.conn.Close()
obj.serveMux = http.NewServeMux() // do it here in case Watch restarts!
// TODO: We could consider having the obj.GetGroup loop here, instead of
// essentially having our own "router" API with AcceptHTTP.
obj.serveMux.HandleFunc("/", obj.handler())
readTimeout := uint64(0)
@@ -622,19 +715,37 @@ func (obj *HTTPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error
// these two resources be merged, aka, does this resource support doing so? Will
// resource allow itself to be grouped _into_ this obj?
func (obj *HTTPServerRes) GroupCmp(r engine.GroupableRes) error {
res1, ok1 := r.(*HTTPFileRes) // different from what we usually do!
if ok1 {
// If the http file resource has the Server field specified,
// then it must match against our name field if we want it to
// group with us.
if res1.Server != "" && res1.Server != obj.Name() {
return fmt.Errorf("resource groups with a different server name")
}
return nil
res, ok := r.(HTTPServerGroupableRes) // different from what we usually do!
if !ok {
return fmt.Errorf("resource is not the right kind")
}
return fmt.Errorf("resource is not the right kind")
// If the http resource has the parent name field specified, then it
// must match against our name field if we want it to group with us.
if pn := res.ParentName(); pn != "" && pn != obj.Name() {
return fmt.Errorf("resource groups with a different parent name")
}
// http:foo is okay, but file or config:etcd is not
if !strings.HasPrefix(r.Kind(), httpKind+":") {
return fmt.Errorf("not one of our children")
}
// http:server:foo is okay, but http:server:foo:bar is not
p1 := httpServerKind + ":"
s1 := strings.TrimPrefix(r.Kind(), p1)
if len(s1) != len(r.Kind()) && strings.Count(s1, ":") > 0 { // has prefix
return fmt.Errorf("maximum one resource after `%s` prefix", httpServerKind)
}
// http:foo is okay, but http:foo:bar is not
p2 := httpKind + ":"
s2 := strings.TrimPrefix(r.Kind(), p2)
if len(s2) != len(r.Kind()) && strings.Count(s2, ":") > 0 { // has prefix
return fmt.Errorf("maximum one resource after `%s` prefix", httpKind)
}
return nil
}
// readHandler handles all the incoming download requests from clients.
@@ -648,101 +759,49 @@ func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) {
}
// TODO: would this leak anything security sensitive in our log?
obj.init.Logf("URL: %s", req.URL)
if obj.init.Debug {
obj.init.Logf("Path: %s", req.URL.Path)
}
// We only allow GET at the moment.
if req.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
requestPath := req.URL.Path // TODO: is this what we want here?
//var handle io.Reader // TODO: simplify?
var handle io.ReadSeeker
if obj.init.Debug {
obj.init.Logf("Path: %s", requestPath)
}
// Look through the autogrouped resources!
// TODO: can we improve performance by only searching here once?
for _, x := range obj.GetGroup() { // grouped elements
res, ok := x.(*HTTPFileRes) // convert from Res
res, ok := x.(HTTPServerGroupableRes) // convert from Res
if !ok {
continue
}
if requestPath != res.getPath() {
continue // not me
if obj.init.Debug {
obj.init.Logf("Got grouped resource: %s", res.String())
}
if obj.init.Debug {
obj.init.Logf("Got grouped file: %s", res.String())
}
var err error
handle, err = res.getContent()
if err != nil {
obj.init.Logf("could not get content for: %s", requestPath)
msg, httpStatus := toHTTPError(err)
http.Error(w, msg, httpStatus)
err := res.AcceptHTTP(req)
if err == nil {
res.ServeHTTP(w, req)
return
}
break
if obj.init.Debug {
obj.init.Logf("Could not serve: %+v", err)
}
//continue // not me
}
// Look in root if we have one, and we haven't got a file yet...
if obj.Root != "" && handle == nil {
p := filepath.Join(obj.Root, requestPath) // normal unsafe!
if !strings.HasPrefix(p, obj.Root) { // root ends with /
// user might have tried a ../../etc/passwd hack
obj.init.Logf("join inconsistency: %s", p)
http.NotFound(w, req) // lie to them...
return
}
if HTTPUseSecureJoin {
var err error
p, err = securefilepath.SecureJoin(obj.Root, requestPath)
if err != nil {
obj.init.Logf("secure join fail: %s", p)
http.NotFound(w, req) // lie to them...
return
}
}
if obj.init.Debug {
obj.init.Logf("Got file at root: %s", p)
}
var err error
handle, err = os.Open(p)
if err != nil {
obj.init.Logf("could not open: %s", p)
msg, httpStatus := toHTTPError(err)
http.Error(w, msg, httpStatus)
return
}
}
// We never found a file...
if handle == nil {
if obj.init.Debug || true { // XXX: maybe we should always do this?
obj.init.Logf("File not found: %s", requestPath)
}
http.NotFound(w, req)
err := obj.AcceptHTTP(req)
if err == nil {
obj.ServeHTTP(w, req)
return
}
// Determine the last-modified time if we can.
modtime := time.Now()
if f, ok := handle.(*os.File); ok {
fi, err := f.Stat()
if err == nil {
modtime = fi.ModTime()
}
// TODO: if Stat errors, should we fail the whole thing?
if obj.init.Debug {
obj.init.Logf("Could not serve Root: %+v", err)
}
// XXX: is requestPath what we want for the name field?
http.ServeContent(w, req, requestPath, modtime, handle)
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
// We never found something to serve...
if obj.init.Debug || true { // XXX: maybe we should always do this?
obj.init.Logf("File not found: %s", requestPath)
}
http.NotFound(w, req)
return
}
}
@@ -812,6 +871,56 @@ func (obj *HTTPFileRes) getContent() (io.ReadSeeker, error) {
return bytes.NewReader([]byte(obj.Data)), nil
}
// ParentName is used to limit which resources autogroup into this one. If it's
// empty then it's ignored, otherwise it must match the Name of the parent to
// get grouped.
func (obj *HTTPFileRes) ParentName() string {
return obj.Server
}
// AcceptHTTP determines whether we will respond to this request. Return nil to
// accept, or any error to pass.
func (obj *HTTPFileRes) AcceptHTTP(req *http.Request) error {
requestPath := req.URL.Path // TODO: is this what we want here?
if requestPath != obj.getPath() {
return fmt.Errorf("unhandled path")
}
return nil
}
// ServeHTTP is the standard HTTP handler that will be used here.
func (obj *HTTPFileRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// We only allow GET at the moment.
if req.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
requestPath := req.URL.Path // TODO: is this what we want here?
handle, err := obj.getContent()
if err != nil {
obj.init.Logf("could not get content for: %s", requestPath)
msg, httpStatus := toHTTPError(err)
http.Error(w, msg, httpStatus)
return
}
// Determine the last-modified time if we can.
modtime := time.Now()
if f, ok := handle.(*os.File); ok {
fi, err := f.Stat()
if err == nil {
modtime = fi.ModTime()
}
// TODO: if Stat errors, should we fail the whole thing?
}
// XXX: is requestPath what we want for the name field?
http.ServeContent(w, req, requestPath, modtime, handle)
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
}
// Validate checks if the resource data structure was populated correctly.
func (obj *HTTPFileRes) Validate() error {
if obj.getPath() == "" {