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:
@@ -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() == "" {
|
||||
|
||||
Reference in New Issue
Block a user