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{} })
|
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
|
// 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,
|
// 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.
|
// unless the Address field is specified, and in that case it is used instead.
|
||||||
@@ -75,7 +97,7 @@ func init() {
|
|||||||
type HTTPServerRes struct {
|
type HTTPServerRes struct {
|
||||||
traits.Base // add the base methods without re-implementation
|
traits.Base // add the base methods without re-implementation
|
||||||
traits.Edgeable // XXX: add autoedge support
|
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
|
init *engine.Init
|
||||||
|
|
||||||
@@ -171,6 +193,75 @@ func (obj *HTTPServerRes) getShutdownTimeout() *uint64 {
|
|||||||
return obj.Timeout // might be nil
|
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.
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
func (obj *HTTPServerRes) Validate() error {
|
func (obj *HTTPServerRes) Validate() error {
|
||||||
if obj.getAddress() == "" {
|
if obj.getAddress() == "" {
|
||||||
@@ -305,6 +396,8 @@ func (obj *HTTPServerRes) Watch(ctx context.Context) error {
|
|||||||
defer obj.conn.Close()
|
defer obj.conn.Close()
|
||||||
|
|
||||||
obj.serveMux = http.NewServeMux() // do it here in case Watch restarts!
|
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())
|
obj.serveMux.HandleFunc("/", obj.handler())
|
||||||
|
|
||||||
readTimeout := uint64(0)
|
readTimeout := uint64(0)
|
||||||
@@ -622,21 +715,39 @@ func (obj *HTTPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error
|
|||||||
// these two resources be merged, aka, does this resource support doing so? Will
|
// these two resources be merged, aka, does this resource support doing so? Will
|
||||||
// resource allow itself to be grouped _into_ this obj?
|
// resource allow itself to be grouped _into_ this obj?
|
||||||
func (obj *HTTPServerRes) GroupCmp(r engine.GroupableRes) error {
|
func (obj *HTTPServerRes) GroupCmp(r engine.GroupableRes) error {
|
||||||
res1, ok1 := r.(*HTTPFileRes) // different from what we usually do!
|
res, ok := r.(HTTPServerGroupableRes) // different from what we usually do!
|
||||||
if ok1 {
|
if !ok {
|
||||||
// If the http file resource has the Server field specified,
|
return fmt.Errorf("resource is not the right kind")
|
||||||
// then it must match against our name field if we want it to
|
}
|
||||||
// group with us.
|
|
||||||
if res1.Server != "" && res1.Server != obj.Name() {
|
// If the http resource has the parent name field specified, then it
|
||||||
return fmt.Errorf("resource groups with a different server name")
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("resource is not the right kind")
|
|
||||||
}
|
|
||||||
|
|
||||||
// readHandler handles all the incoming download requests from clients.
|
// readHandler handles all the incoming download requests from clients.
|
||||||
func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) {
|
func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) {
|
||||||
// TODO: we could statically pre-compute some stuff here...
|
// TODO: we could statically pre-compute some stuff here...
|
||||||
@@ -648,103 +759,51 @@ func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) {
|
|||||||
}
|
}
|
||||||
// TODO: would this leak anything security sensitive in our log?
|
// TODO: would this leak anything security sensitive in our log?
|
||||||
obj.init.Logf("URL: %s", req.URL)
|
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?
|
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||||
|
if obj.init.Debug {
|
||||||
//var handle io.Reader // TODO: simplify?
|
obj.init.Logf("Path: %s", requestPath)
|
||||||
var handle io.ReadSeeker
|
}
|
||||||
|
|
||||||
// Look through the autogrouped resources!
|
// Look through the autogrouped resources!
|
||||||
// TODO: can we improve performance by only searching here once?
|
// TODO: can we improve performance by only searching here once?
|
||||||
for _, x := range obj.GetGroup() { // grouped elements
|
for _, x := range obj.GetGroup() { // grouped elements
|
||||||
res, ok := x.(*HTTPFileRes) // convert from Res
|
res, ok := x.(HTTPServerGroupableRes) // convert from Res
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if requestPath != res.getPath() {
|
if obj.init.Debug {
|
||||||
continue // not me
|
obj.init.Logf("Got grouped resource: %s", res.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.init.Debug {
|
err := res.AcceptHTTP(req)
|
||||||
obj.init.Logf("Got grouped file: %s", res.String())
|
if err == nil {
|
||||||
}
|
res.ServeHTTP(w, req)
|
||||||
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)
|
|
||||||
return
|
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...
|
// Look in root if we have one, and we haven't got a file yet...
|
||||||
if obj.Root != "" && handle == nil {
|
err := obj.AcceptHTTP(req)
|
||||||
|
if err == nil {
|
||||||
p := filepath.Join(obj.Root, requestPath) // normal unsafe!
|
obj.ServeHTTP(w, req)
|
||||||
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
|
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 {
|
if obj.init.Debug {
|
||||||
obj.init.Logf("Got file at root: %s", p)
|
obj.init.Logf("Could not serve Root: %+v", err)
|
||||||
}
|
|
||||||
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...
|
// We never found something to serve...
|
||||||
if handle == nil {
|
|
||||||
if obj.init.Debug || true { // XXX: maybe we should always do this?
|
if obj.init.Debug || true { // XXX: maybe we should always do this?
|
||||||
obj.init.Logf("File not found: %s", requestPath)
|
obj.init.Logf("File not found: %s", requestPath)
|
||||||
}
|
}
|
||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
return
|
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?
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPFileRes is a file that exists within an http server. The name is used as
|
// HTTPFileRes is a file that exists within an http server. The name is used as
|
||||||
@@ -812,6 +871,56 @@ func (obj *HTTPFileRes) getContent() (io.ReadSeeker, error) {
|
|||||||
return bytes.NewReader([]byte(obj.Data)), nil
|
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.
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
func (obj *HTTPFileRes) Validate() error {
|
func (obj *HTTPFileRes) Validate() error {
|
||||||
if obj.getPath() == "" {
|
if obj.getPath() == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user