From 654e958d3f3ca62127f0e4350fb697c5be7860eb Mon Sep 17 00:00:00 2001 From: James Shubin Date: Sun, 25 May 2025 01:40:25 -0400 Subject: [PATCH] engine: resources: Add the proper prefix to grouped http resources Resources that can be grouped into the http:server resource must have that prefix. Grouping is basically hierarchical, and without that common prefix, it means we'd have to special-case our grouping algorithm. --- Makefile | 2 +- engine/graph/actions.go | 2 +- engine/graph/autogroup.go | 4 +- engine/graph/autogroup/base.go | 23 +- engine/graph/autogroup/util.go | 2 +- engine/resources/Makefile | 6 +- engine/resources/http.go | 1067 +---------------- engine/resources/http_server.go | 814 +++++++++++++ engine/resources/http_server_file.go | 337 ++++++ .../{http_flag.go => http_server_flag.go} | 75 +- .../{http_proxy.go => http_server_proxy.go} | 105 +- ...roxy_test.go => http_server_proxy_test.go} | 2 +- .../{http_ui.go => http_server_ui.go} | 129 +- .../{http_ui => http_server_ui}/.gitignore | 0 .../{http_ui => http_server_ui}/README.md | 6 +- .../common/common.go | 28 +- .../index.html.tmpl | 0 .../{http_ui => http_server_ui}/main.go | 12 +- .../static/.gitignore | 0 .../static/embed.go | 21 +- .../static/noembed.go | 17 +- .../static/static.go | 12 +- .../{http_ui => http_server_ui}/wasm_exec.js | 0 ...tp_ui_input.go => http_server_ui_input.go} | 170 +-- .../{http-flag0.mcl => http-server-flag0.mcl} | 8 +- ...http-proxy0.mcl => http-server-proxy0.mcl} | 6 +- .../{http-ui-kv.mcl => http-server-ui-kv.mcl} | 10 +- ...-ui-value.mcl => http-server-ui-value.mcl} | 10 +- examples/lang/{http0.mcl => http-server0.mcl} | 8 +- examples/lang/{http1.mcl => http-server1.mcl} | 4 +- lang/core/embedded/provisioner/main.mcl | 32 +- .../TestAstFunc2/send-recv-3.txtar | 8 +- misc/vim/mcl.vim | 2 +- test/test-gotest.sh | 2 +- test/test-govet.sh | 2 +- 35 files changed, 1513 insertions(+), 1413 deletions(-) create mode 100644 engine/resources/http_server.go create mode 100644 engine/resources/http_server_file.go rename engine/resources/{http_flag.go => http_server_flag.go} (78%) rename engine/resources/{http_proxy.go => http_server_proxy.go} (83%) rename engine/resources/{http_proxy_test.go => http_server_proxy_test.go} (99%) rename engine/resources/{http_ui.go => http_server_ui.go} (83%) rename engine/resources/{http_ui => http_server_ui}/.gitignore (100%) rename engine/resources/{http_ui => http_server_ui}/README.md (50%) rename engine/resources/{http_ui => http_server_ui}/common/common.go (78%) rename engine/resources/{http_ui => http_server_ui}/index.html.tmpl (100%) rename engine/resources/{http_ui => http_server_ui}/main.go (95%) rename engine/resources/{http_ui => http_server_ui}/static/.gitignore (100%) rename engine/resources/{http_ui => http_server_ui}/static/embed.go (75%) rename engine/resources/{http_ui => http_server_ui}/static/noembed.go (79%) rename engine/resources/{http_ui => http_server_ui}/static/static.go (81%) rename engine/resources/{http_ui => http_server_ui}/wasm_exec.js (100%) rename engine/resources/{http_ui_input.go => http_server_ui_input.go} (71%) rename examples/lang/{http-flag0.mcl => http-server-flag0.mcl} (82%) rename examples/lang/{http-proxy0.mcl => http-server-proxy0.mcl} (84%) rename examples/lang/{http-ui-kv.mcl => http-server-ui-kv.mcl} (81%) rename examples/lang/{http-ui-value.mcl => http-server-ui-value.mcl} (88%) rename examples/lang/{http0.mcl => http-server0.mcl} (88%) rename examples/lang/{http1.mcl => http-server1.mcl} (94%) diff --git a/Makefile b/Makefile index 01017717..81e76e92 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ SHELL = bash # a large amount of output from this `find`, can cause `make` to be much slower! GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*') MCL_FILES := $(shell find lang/ -name '*.mcl' -not -path 'old/*' -not -path 'tmp/*') -MISC_FILES := $(shell find engine/resources/http_ui/) +MISC_FILES := $(shell find engine/resources/http_server_ui/) SVERSION := $(or $(SVERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always)) VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0)) diff --git a/engine/graph/actions.go b/engine/graph/actions.go index eda1825a..517a6a00 100644 --- a/engine/graph/actions.go +++ b/engine/graph/actions.go @@ -172,7 +172,7 @@ func (obj *Engine) Process(ctx context.Context, vertex pgraph.Vertex) error { } // If we contain grouped resources, maybe someone inside wants to recv? - // This code is similar to the above and was added for http:ui stuff. + // This code is similar to the above and was added for http:server:ui. // XXX: Maybe this block isn't needed, as mentioned we need to check! if res, ok := vertex.(engine.GroupableRes); ok { process := res.GetGroup() // look through these diff --git a/engine/graph/autogroup.go b/engine/graph/autogroup.go index 94b3f2ca..c88b50c7 100644 --- a/engine/graph/autogroup.go +++ b/engine/graph/autogroup.go @@ -99,8 +99,8 @@ func (obj *wrappedGrouper) VertexCmp(v1, v2 pgraph.Vertex) error { // same. This prevents us from having a linear chain of pkg->pkg->pkg, // instead of flattening all of them into one arbitrary choice. But if // we are doing hierarchical grouping, then we want to allow this type - // of grouping, or we won't end up building any hierarchies! This change - // was added for http:ui stuff. Check this condition is really required. + // of grouping, or we won't end up building any hierarchies! This was + // added for http:server:ui. Check this condition is really required. if r1.Kind() == r2.Kind() { // XXX: needed or do we unwrap the contents? if r1.IsGrouped() { // already grouped! return fmt.Errorf("already grouped") diff --git a/engine/graph/autogroup/base.go b/engine/graph/autogroup/base.go index 2c033833..11a2b65e 100644 --- a/engine/graph/autogroup/base.go +++ b/engine/graph/autogroup/base.go @@ -58,17 +58,18 @@ func (ag *baseGrouper) Init(g *pgraph.Graph) error { ag.graph = g // pointer // We sort deterministically, first by kind, and then by name. In - // particular, longer kind chunks sort first. So http:ui:text should - // appear before http:server and http:ui. This is a hack so that if we - // are doing hierarchical automatic grouping, it gives the http:ui:text - // a chance to get grouped into http:ui, before http:ui gets grouped - // into http:server, because once that happens, http:ui:text will never - // get grouped, and this won't work properly. This works, because when - // we start comparing iteratively the list of resources, it does this - // with a O(n^2) loop that compares the X and Y zero indexes first, and - // and then continues along. If the "longer" resources appear first, - // then they'll group together first. We should probably put this into - // a new Grouper struct, but for now we might as well leave it here. + // particular, longer kind chunks sort first. So http:server:ui:input + // should appear before http:server and http:server:ui. This is a + // strategy so that if we are doing hierarchical automatic grouping, it + // gives the http:server:ui:input a chance to get grouped into + // http:server:ui, before http:server:ui gets grouped into http:server, + // because once that happens, http:server:ui:input will never get + // grouped, and this won't work properly. This works, because when we + // start comparing iteratively the list of resources, it does this with + // a O(n^2) loop that compares the X and Y zero indexes first, and then + // continues along. If the "longer" resources appear first, then they'll + // group together first. We should probably put this into a new Grouper + // struct, but for now we might as well leave it here. //vertices := ag.graph.VerticesSorted() // formerly vertices := RHVSort(ag.graph.Vertices()) diff --git a/engine/graph/autogroup/util.go b/engine/graph/autogroup/util.go index 04309dbf..273f724d 100644 --- a/engine/graph/autogroup/util.go +++ b/engine/graph/autogroup/util.go @@ -181,7 +181,7 @@ func (obj RHVSlice) Less(i, j int) bool { li := len(si) lj := len(sj) - if li != lj { // eg: http:ui vs. http:ui:text + if li != lj { // eg: http:server:ui vs. http:server:ui:text return li > lj // reverse } diff --git a/engine/resources/Makefile b/engine/resources/Makefile index e72e8492..3dab82c0 100644 --- a/engine/resources/Makefile +++ b/engine/resources/Makefile @@ -31,13 +31,13 @@ SHELL = /usr/bin/env bash .PHONY: build clean default: build -WASM_FILE = http_ui/main.wasm +WASM_FILE = http_server_ui/main.wasm build: $(WASM_FILE) -$(WASM_FILE): http_ui/main.go +$(WASM_FILE): http_server_ui/main.go @echo "Generating: wasm..." - cd http_ui/ && env GOOS=js GOARCH=wasm go build -o `basename $(WASM_FILE)` + cd http_server_ui/ && env GOOS=js GOARCH=wasm go build -o `basename $(WASM_FILE)` clean: @rm -f $(WASM_FILE) || true diff --git a/engine/resources/http.go b/engine/resources/http.go index bd80da03..c2151a80 100644 --- a/engine/resources/http.go +++ b/engine/resources/http.go @@ -30,1080 +30,15 @@ package resources import ( - "bytes" - "context" - "fmt" - "io" - "net" "net/http" "os" - "path/filepath" "strconv" - "strings" - "sync" - "time" - - "github.com/purpleidea/mgmt/engine" - "github.com/purpleidea/mgmt/engine/traits" - "github.com/purpleidea/mgmt/pgraph" - "github.com/purpleidea/mgmt/util/errwrap" - "github.com/purpleidea/mgmt/util/safepath" - - securefilepath "github.com/cyphar/filepath-securejoin" ) const ( - // HTTPUseSecureJoin specifies that we should add in a "secure join" lib - // so that we avoid the ../../etc/passwd and symlink problems. - HTTPUseSecureJoin = true - - httpKind = "http" - httpServerKind = httpKind + ":server" - httpFileKind = httpKind + ":file" + httpKind = "http" ) -func init() { - engine.RegisterResource(httpServerKind, func() engine.Res { return &HTTPServerRes{} }) - 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. -// This resource can offer up files for serving that are specified either inline -// in this resource by specifying an http root, or as http:file resources which -// will get autogrouped into this resource at runtime. The two methods can be -// combined as well. -// -// This server also supports autogrouping some more magical resources into it. -// For example, the http:flag and http:ui resources add in magic endpoints. -// -// This server is not meant as a featureful replacement for the venerable and -// modern httpd servers out there, but rather as a simple, dynamic, integrated -// alternative for bootstrapping new machines and clusters in an elegant way. -// -// TODO: add support for TLS -// XXX: Add an http:flag resource that lets an http client set a flag somewhere! -// XXX: Add a http:ui resource that functions can read data from! -// XXX: The http:ui resource can also take in values from those functions! -type HTTPServerRes struct { - traits.Base // add the base methods without re-implementation - traits.Edgeable // XXX: add autoedge support - traits.Groupable // can have HTTPFileRes and others grouped into it - - init *engine.Init - - // Address is the listen address to use for the http server. It is - // common to use `:80` (the standard) to listen on TCP port 80 on all - // addresses. - Address string `lang:"address" yaml:"address"` - - // Timeout is the maximum duration in seconds to use for unspecified - // timeouts. In other words, when this value is specified, it is used as - // the value for the other *Timeout values when they aren't used. Put - // another way, this makes it easy to set all the different timeouts - // with a single parameter. - Timeout *uint64 `lang:"timeout" yaml:"timeout"` - - // ReadTimeout is the maximum duration in seconds for reading during the - // http request. If it is zero, then there is no timeout. If this is - // unspecified, then the value of Timeout is used instead if it is set. - // For more information, see the golang net/http Server documentation. - ReadTimeout *uint64 `lang:"read_timeout" yaml:"read_timeout"` - - // WriteTimeout is the maximum duration in seconds for writing during - // the http request. If it is zero, then there is no timeout. If this is - // unspecified, then the value of Timeout is used instead if it is set. - // For more information, see the golang net/http Server documentation. - WriteTimeout *uint64 `lang:"write_timeout" yaml:"write_timeout"` - - // ShutdownTimeout is the maximum duration in seconds to wait for the - // server to shutdown gracefully before calling Close. By default it is - // nice to let client connections terminate gracefully, however it might - // take longer than we are willing to wait, particularly if one is long - // polling or running a very long download. As a result, you can set a - // timeout here. The default is zero which means it will wait - // indefinitely. The shutdown process can also be cancelled by the - // interrupt handler which this resource supports. If this is - // unspecified, then the value of Timeout is used instead if it is set. - ShutdownTimeout *uint64 `lang:"shutdown_timeout" yaml:"shutdown_timeout"` - - // Root is the root directory that we should serve files from. If it is - // not specified, then it is not used. Any http file resources will have - // precedence over anything in here, in case the same path exists twice. - // TODO: should we have a flag to determine the precedence rules here? - Root string `lang:"root" yaml:"root"` - - // TODO: should we allow adding a list of one-of files directly here? - - eventsChanMap map[engine.Res]chan error - interruptChan chan struct{} - - conn net.Listener - serveMux *http.ServeMux // can't share the global one between resources! - server *http.Server -} - -// Default returns some sensible defaults for this resource. -func (obj *HTTPServerRes) Default() engine.Res { - return &HTTPServerRes{} -} - -// getAddress returns the actual address to use. When Address is not specified, -// we use the Name. -func (obj *HTTPServerRes) getAddress() string { - if obj.Address != "" { - return obj.Address - } - return obj.Name() -} - -// getReadTimeout determines the value for ReadTimeout, because if unspecified, -// this will default to the value of Timeout. -func (obj *HTTPServerRes) getReadTimeout() *uint64 { - if obj.ReadTimeout != nil { - return obj.ReadTimeout - } - return obj.Timeout // might be nil -} - -// getWriteTimeout determines the value for WriteTimeout, because if -// unspecified, this will default to the value of Timeout. -func (obj *HTTPServerRes) getWriteTimeout() *uint64 { - if obj.WriteTimeout != nil { - return obj.WriteTimeout - } - return obj.Timeout // might be nil -} - -// getShutdownTimeout determines the value for ShutdownTimeout, because if -// unspecified, this will default to the value of Timeout. -func (obj *HTTPServerRes) getShutdownTimeout() *uint64 { - if obj.ShutdownTimeout != nil { - return obj.ShutdownTimeout - } - 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) - sendHTTPError(w, err) - return - } - defer handle.Close() // ignore error - - // 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() == "" { - return fmt.Errorf("empty address") - } - - host, _, err := net.SplitHostPort(obj.getAddress()) - if err != nil { - return errwrap.Wrapf(err, "the Address is in an invalid format: %s", obj.getAddress()) - } - if host != "" { - // TODO: should we allow fqdn's here? - ip := net.ParseIP(host) - if ip == nil { - return fmt.Errorf("the Address is not a valid IP: %s", host) - } - } - - if obj.Root != "" && !strings.HasPrefix(obj.Root, "/") { - return fmt.Errorf("the Root must be absolute") - } - if obj.Root != "" && !strings.HasSuffix(obj.Root, "/") { - return fmt.Errorf("the Root must be a dir") - } - - // XXX: validate that the autogrouped resources don't have paths that - // conflict with each other. We can only have a single unique entry for - // what handles a /whatever URL. - - return nil -} - -// Init runs some startup code for this resource. -func (obj *HTTPServerRes) Init(init *engine.Init) error { - obj.init = init // save for later - - // No need to error in Validate if Timeout is ignored, but log it. - // These are all specified, so Timeout effectively does nothing. - a := obj.ReadTimeout != nil - b := obj.WriteTimeout != nil - c := obj.ShutdownTimeout != nil - if obj.Timeout != nil && (a && b && c) { - obj.init.Logf("the Timeout param is being ignored") - } - - // NOTE: If we don't Init anything that's autogrouped, then it won't - // even get an Init call on it. - - obj.eventsChanMap = make(map[engine.Res]chan error) - - // TODO: should we do this in the engine? Do we want to decide it here? - for _, res := range obj.GetGroup() { // grouped elements - // NOTE: We build a new init, but it's not complete. We only add - // what we're planning to use, and we ignore the rest for now... - r := res // bind the variable! - - obj.eventsChanMap[r] = make(chan error) - event := func() { - select { - case obj.eventsChanMap[r] <- nil: - // send! - } - // We don't do this here (why?) we instead read from the - // above channel and then send on multiplexedChan to the - // main loop, where it runs the obj.init.Event function. - //obj.init.Event() // notify engine of an event (this can block) - } - - newInit := &engine.Init{ - Program: obj.init.Program, - Version: obj.init.Version, - Hostname: obj.init.Hostname, - - // Watch: - Running: event, - Event: event, - - // CheckApply: - Refresh: func() bool { - innerRes, ok := r.(engine.RefreshableRes) - if !ok { - panic("res does not support the Refreshable trait") - } - return innerRes.Refresh() - }, - Send: engine.GenerateSendFunc(r), - Recv: engine.GenerateRecvFunc(r), // unused - - FilteredGraph: func() (*pgraph.Graph, error) { - panic("FilteredGraph for HTTP not implemented") - }, - - Local: obj.init.Local, - World: obj.init.World, - //VarDir: obj.init.VarDir, // TODO: wrap this - - Debug: obj.init.Debug, - Logf: func(format string, v ...interface{}) { - obj.init.Logf(r.String()+": "+format, v...) - }, - } - - if err := res.Init(newInit); err != nil { - return errwrap.Wrapf(err, "autogrouped Init failed") - } - } - - obj.interruptChan = make(chan struct{}) - - return nil -} - -// Cleanup is run by the engine to clean up after the resource is done. -func (obj *HTTPServerRes) Cleanup() error { - return nil -} - -// Watch is the primary listener for this resource and it outputs events. -func (obj *HTTPServerRes) Watch(ctx context.Context) error { - // TODO: I think we could replace all this with: - //obj.conn, err := net.Listen("tcp", obj.getAddress()) - // ...but what is the advantage? - addr, err := net.ResolveTCPAddr("tcp", obj.getAddress()) - if err != nil { - return errwrap.Wrapf(err, "could not resolve address") - } - - obj.conn, err = net.ListenTCP("tcp", addr) - if err != nil { - return errwrap.Wrapf(err, "could not start listener") - } - 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) - if i := obj.getReadTimeout(); i != nil { - readTimeout = *i - } - writeTimeout := uint64(0) - if i := obj.getWriteTimeout(); i != nil { - writeTimeout = *i - } - obj.server = &http.Server{ - Addr: obj.getAddress(), - Handler: obj.serveMux, - ReadTimeout: time.Duration(readTimeout) * time.Second, - WriteTimeout: time.Duration(writeTimeout) * time.Second, - //MaxHeaderBytes: 1 << 20, XXX: should we add a param for this? - } - - multiplexedChan := make(chan error) - defer close(multiplexedChan) // closes after everyone below us is finished - - wg := &sync.WaitGroup{} - defer wg.Wait() - - for _, r := range obj.GetGroup() { // grouped elements - res := r // optional in newer golang - wg.Add(1) - go func() { - defer wg.Done() - defer close(obj.eventsChanMap[res]) // where Watch sends events - if err := res.Watch(ctx); err != nil { - select { - case multiplexedChan <- err: - case <-ctx.Done(): - } - } - }() - // wait for Watch first Running() call or immediate error... - select { - case <-obj.eventsChanMap[res]: // triggers on start or on err... - } - - wg.Add(1) - go func() { - defer wg.Done() - for { - var ok bool - var err error - select { - // receive - case err, ok = <-obj.eventsChanMap[res]: - if !ok { - return - } - } - - // send (multiplex) - select { - case multiplexedChan <- err: - case <-ctx.Done(): - return - } - } - }() - } - // we block until all the children are started first... - - obj.init.Running() // when started, notify engine that we're running - - var closeError error - closeSignal := make(chan struct{}) - - shutdownChan := make(chan struct{}) // server shutdown finished signal - wg.Add(1) - go func() { - defer wg.Done() - select { - case <-obj.interruptChan: - // TODO: should we bubble up the error from Close? - // TODO: do we need a mutex around this Close? - obj.server.Close() // kill it quickly! - case <-shutdownChan: - // let this exit - } - }() - - wg.Add(1) - go func() { - defer wg.Done() - defer close(closeSignal) - - err := obj.server.Serve(obj.conn) // blocks until Shutdown() is called! - if err == nil || err == http.ErrServerClosed { - return - } - // if this returned on its own, then closeSignal can be used... - closeError = errwrap.Wrapf(err, "the server errored") - }() - - // When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS - // immediately return ErrServerClosed. Make sure the program doesn't - // exit and waits instead for Shutdown to return. - defer func() { - defer close(shutdownChan) // signal that shutdown is finished - innerCtx := context.Background() - if i := obj.getShutdownTimeout(); i != nil && *i > 0 { - var cancel context.CancelFunc - innerCtx, cancel = context.WithTimeout(innerCtx, time.Duration(*i)*time.Second) - defer cancel() - } - err := obj.server.Shutdown(innerCtx) // shutdown gracefully - if err == context.DeadlineExceeded { - // TODO: should we bubble up the error from Close? - // TODO: do we need a mutex around this Close? - obj.server.Close() // kill it now - } - }() - - startupChan := make(chan struct{}) - close(startupChan) // send one initial signal - - var send = false // send event? - for { - if obj.init.Debug { - obj.init.Logf("Looping...") - } - - select { - case <-startupChan: - startupChan = nil - send = true - - case err, ok := <-multiplexedChan: - if !ok { // shouldn't happen - multiplexedChan = nil - continue - } - if err != nil { - return err - } - send = true - - case <-closeSignal: // something shut us down early - return closeError - - case <-ctx.Done(): // closed by the engine to signal shutdown - return nil - } - - // do all our event sending all together to avoid duplicate msgs - if send { - send = false - obj.init.Event() // notify engine of an event (this can block) - } - } -} - -// CheckApply never has anything to do for this resource, so it always succeeds. -// It does however check that certain runtime requirements (such as the Root dir -// existing if one was specified) are fulfilled. If there are any autogrouped -// resources, those will be recursively called so that they can send/recv. -func (obj *HTTPServerRes) CheckApply(ctx context.Context, apply bool) (bool, error) { - if obj.init.Debug { - obj.init.Logf("CheckApply") - } - - // XXX: We don't want the initial CheckApply to return true until the - // Watch has started up, so we must block here until that's the case... - - // Cheap runtime validation! - // XXX: maybe only do this only once to avoid repeated, unnecessary checks? - if obj.Root != "" { - fileInfo, err := os.Stat(obj.Root) - if err != nil { - return false, errwrap.Wrapf(err, "can't stat Root dir") - } - if !fileInfo.IsDir() { - return false, fmt.Errorf("the Root path is not a dir") - } - } - - checkOK := true - for _, res := range obj.GetGroup() { // grouped elements - if c, err := res.CheckApply(ctx, apply); err != nil { - return false, errwrap.Wrapf(err, "autogrouped CheckApply failed") - } else if !c { - checkOK = false - } - } - - return checkOK, nil -} - -// Cmp compares two resources and returns an error if they are not equivalent. -func (obj *HTTPServerRes) Cmp(r engine.Res) error { - // we can only compare HTTPServerRes to others of the same resource kind - res, ok := r.(*HTTPServerRes) - if !ok { - return fmt.Errorf("res is not the same kind") - } - - if obj.Address != res.Address { - return fmt.Errorf("the Address differs") - } - - if (obj.Timeout == nil) != (res.Timeout == nil) { // xor - return fmt.Errorf("the Timeout differs") - } - if obj.Timeout != nil && res.Timeout != nil { - if *obj.Timeout != *res.Timeout { // compare the values - return fmt.Errorf("the value of Timeout differs") - } - } - if (obj.ReadTimeout == nil) != (res.ReadTimeout == nil) { - return fmt.Errorf("the ReadTimeout differs") - } - if obj.ReadTimeout != nil && res.ReadTimeout != nil { - if *obj.ReadTimeout != *res.ReadTimeout { - return fmt.Errorf("the value of ReadTimeout differs") - } - } - if (obj.WriteTimeout == nil) != (res.WriteTimeout == nil) { - return fmt.Errorf("the WriteTimeout differs") - } - if obj.WriteTimeout != nil && res.WriteTimeout != nil { - if *obj.WriteTimeout != *res.WriteTimeout { - return fmt.Errorf("the value of WriteTimeout differs") - } - } - if (obj.ShutdownTimeout == nil) != (res.ShutdownTimeout == nil) { - return fmt.Errorf("the ShutdownTimeout differs") - } - if obj.ShutdownTimeout != nil && res.ShutdownTimeout != nil { - if *obj.ShutdownTimeout != *res.ShutdownTimeout { - return fmt.Errorf("the value of ShutdownTimeout differs") - } - } - - // TODO: We could do this sort of thing to skip checking Timeout when it - // is not used, but for the moment, this is overkill and not needed yet. - //a := obj.ReadTimeout != nil - //b := obj.WriteTimeout != nil - //c := obj.ShutdownTimeout != nil - //if !(obj.Timeout != nil && (a && b && c)) { - // // the Timeout param is not being ignored - //} - - if obj.Root != res.Root { - return fmt.Errorf("the Root differs") - } - - return nil -} - -// Interrupt is called to ask the execution of this resource to end early. It -// will cause the server Shutdown to end abruptly instead of leading open client -// connections terminate gracefully. It does this by causing the server Close -// method to run. -func (obj *HTTPServerRes) Interrupt() error { - close(obj.interruptChan) // this should cause obj.server.Close() to run! - return nil -} - -// Copy copies the resource. Don't call it directly, use engine.ResCopy instead. -// TODO: should this copy internal state? -func (obj *HTTPServerRes) Copy() engine.CopyableRes { - var timeout, readTimeout, writeTimeout, shutdownTimeout *uint64 - if obj.Timeout != nil { - x := *obj.Timeout - timeout = &x - } - if obj.ReadTimeout != nil { - x := *obj.ReadTimeout - readTimeout = &x - } - if obj.WriteTimeout != nil { - x := *obj.WriteTimeout - writeTimeout = &x - } - if obj.ShutdownTimeout != nil { - x := *obj.ShutdownTimeout - shutdownTimeout = &x - } - return &HTTPServerRes{ - Address: obj.Address, - Timeout: timeout, - ReadTimeout: readTimeout, - WriteTimeout: writeTimeout, - ShutdownTimeout: shutdownTimeout, - Root: obj.Root, - } -} - -// UnmarshalYAML is the custom unmarshal handler for this struct. It is -// primarily useful for setting the defaults. -func (obj *HTTPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error { - type rawRes HTTPServerRes // indirection to avoid infinite recursion - - def := obj.Default() // get the default - res, ok := def.(*HTTPServerRes) // put in the right format - if !ok { - return fmt.Errorf("could not convert to HTTPServerRes") - } - raw := rawRes(*res) // convert; the defaults go here - - if err := unmarshal(&raw); err != nil { - return err - } - - *obj = HTTPServerRes(raw) // restore from indirection with type conversion! - return nil -} - -// GroupCmp returns whether two resources can be grouped together or not. Can -// 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 { - res, ok := r.(HTTPServerGroupableRes) // different from what we usually do! - if !ok { - 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. -func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) { - // TODO: we could statically pre-compute some stuff here... - - return func(w http.ResponseWriter, req *http.Request) { - - if obj.init.Debug { - obj.init.Logf("Client: %s", req.RemoteAddr) - } - // TODO: would this leak anything security sensitive in our log? - obj.init.Logf("URL: %s", req.URL) - requestPath := req.URL.Path // TODO: is this what we want here? - 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.(HTTPServerGroupableRes) // convert from Res - if !ok { - continue - } - if obj.init.Debug { - obj.init.Logf("Got grouped resource: %s", res.String()) - } - - err := res.AcceptHTTP(req) - if err == nil { - res.ServeHTTP(w, req) - return - } - 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... - err := obj.AcceptHTTP(req) - if err == nil { - obj.ServeHTTP(w, req) - return - } - if obj.init.Debug { - obj.init.Logf("Could not serve Root: %+v", err) - } - - // 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 - } -} - -// HTTPFileRes is a file that exists within an http server. The name is used as -// the public path of the file, unless the filename field is specified, and in -// that case it is used instead. The way this works is that it autogroups at -// runtime with an existing http resource, and in doing so makes the file -// associated with this resource available for serving from that http server. -type HTTPFileRes struct { - traits.Base // add the base methods without re-implementation - traits.Edgeable // XXX: add autoedge support - traits.Groupable // can be grouped into HTTPServerRes - - init *engine.Init - - // Server is the name of the http server resource to group this into. If - // it is omitted, and there is only a single http resource, then it will - // be grouped into it automatically. If there is more than one main http - // resource being used, then the grouping behaviour is *undefined* when - // this is not specified, and it is not recommended to leave this blank! - Server string `lang:"server" yaml:"server"` - - // Filename is the name of the file this data should appear as on the - // http server. - Filename string `lang:"filename" yaml:"filename"` - - // Path is the absolute path to a file that should be used as the source - // for this file resource. It must not be combined with the data field. - // If this corresponds to a directory, then it will used as a root dir - // that will be served as long as the resource name or Filename are also - // a directory ending with a slash. - Path string `lang:"path" yaml:"path"` - - // Data is the file content that should be used as the source for this - // file resource. It must not be combined with the path field. - // TODO: should this be []byte instead? - Data string `lang:"data" yaml:"data"` -} - -// Default returns some sensible defaults for this resource. -func (obj *HTTPFileRes) Default() engine.Res { - return &HTTPFileRes{} -} - -// getPath returns the actual path we respond to. When Filename is not -// specified, we use the Name. Note that this is the filename that will be seen -// on the http server, it is *not* the source path to the actual file contents -// being sent by the server. -func (obj *HTTPFileRes) getPath() string { - if obj.Filename != "" { - return obj.Filename - } - return obj.Name() -} - -// getContent returns the content that we expect from this resource. It depends -// on whether the user specified the Path or Data fields, and whether the Path -// exists or not. -func (obj *HTTPFileRes) getContent(requestPath safepath.AbsPath) (io.ReadSeeker, error) { - if obj.Path != "" && obj.Data != "" { - // programming error! this should have been caught in Validate! - return nil, fmt.Errorf("must not specify Path and Data") - } - - if obj.Data != "" { - return bytes.NewReader([]byte(obj.Data)), nil - } - - absFile, err := obj.getContentRelative(requestPath) - if err != nil { // on error, we just assume no root/prefix stuff happens - return os.Open(obj.Path) - } - - return os.Open(absFile.Path()) -} - -// getContentRelative takes a request, and returns the absolute path to the file -// that we want to request, if it's safely under what we can provide. -func (obj *HTTPFileRes) getContentRelative(requestPath safepath.AbsPath) (safepath.AbsFile, error) { - // the location on disk of the data - srcPath, err := safepath.SmartParseIntoPath(obj.Path) // (safepath.Path, error) - if err != nil { - return safepath.AbsFile{}, err - } - srcAbsDir, ok := srcPath.(safepath.AbsDir) - if !ok { - return safepath.AbsFile{}, fmt.Errorf("the Path is not an abs dir") - } - - // the public path we respond to (might be a dir prefix or just a file) - pubPath, err := safepath.SmartParseIntoPath(obj.getPath()) // (safepath.Path, error) - if err != nil { - return safepath.AbsFile{}, err - } - pubAbsDir, ok := pubPath.(safepath.AbsDir) - if !ok { - return safepath.AbsFile{}, fmt.Errorf("the name is not an abs dir") - } - - // is the request underneath what we're providing? - if !safepath.HasPrefix(requestPath, pubAbsDir) { - return safepath.AbsFile{}, fmt.Errorf("wrong prefix") - } - - // make the delta - delta, err := safepath.StripPrefix(requestPath, pubAbsDir) // (safepath.Path, error) - if err != nil { - return safepath.AbsFile{}, err - } - relFile, ok := delta.(safepath.RelFile) - if !ok { - return safepath.AbsFile{}, fmt.Errorf("the delta is not a rel file") - } - - return safepath.JoinToAbsFile(srcAbsDir, relFile), nil // AbsFile -} - -// 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 strings.HasSuffix(obj.Path, "/") { // a dir! - if strings.HasPrefix(requestPath, obj.getPath()) { - // relative dir root - return nil - } - } - - 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? - - absPath, err := safepath.ParseIntoAbsPath(requestPath) - if err != nil { - obj.init.Logf("invalid input path: %s", requestPath) - sendHTTPError(w, err) - return - } - - handle, err := obj.getContent(absPath) - if err != nil { - obj.init.Logf("could not get content for: %s", requestPath) - sendHTTPError(w, err) - return - } - //if readSeekCloser, ok := handle.(io.ReadSeekCloser); ok { // same - // defer readSeekCloser.Close() // ignore error - //} - if closer, ok := handle.(io.Closer); ok { - defer closer.Close() // ignore error - } - - // 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() == "" { - return fmt.Errorf("empty filename") - } - // FIXME: does getPath need to start with a slash? - - if obj.Path != "" && !strings.HasPrefix(obj.Path, "/") { - return fmt.Errorf("the Path must be absolute") - } - - if obj.Path != "" && obj.Data != "" { - return fmt.Errorf("must not specify Path and Data") - } - - // NOTE: if obj.Path == "" && obj.Data == "" then we have an empty file! - - return nil -} - -// Init runs some startup code for this resource. -func (obj *HTTPFileRes) Init(init *engine.Init) error { - obj.init = init // save for later - - return nil -} - -// Cleanup is run by the engine to clean up after the resource is done. -func (obj *HTTPFileRes) Cleanup() error { - return nil -} - -// Watch is the primary listener for this resource and it outputs events. This -// particular one does absolutely nothing but block until we've received a done -// signal. -func (obj *HTTPFileRes) Watch(ctx context.Context) error { - obj.init.Running() // when started, notify engine that we're running - - select { - case <-ctx.Done(): // closed by the engine to signal shutdown - } - - //obj.init.Event() // notify engine of an event (this can block) - - return nil -} - -// CheckApply never has anything to do for this resource, so it always succeeds. -func (obj *HTTPFileRes) CheckApply(ctx context.Context, apply bool) (bool, error) { - if obj.init.Debug { - obj.init.Logf("CheckApply") - } - - return true, nil // always succeeds, with nothing to do! -} - -// Cmp compares two resources and returns an error if they are not equivalent. -func (obj *HTTPFileRes) Cmp(r engine.Res) error { - // we can only compare HTTPFileRes to others of the same resource kind - res, ok := r.(*HTTPFileRes) - if !ok { - return fmt.Errorf("res is not the same kind") - } - - if obj.Server != res.Server { - return fmt.Errorf("the Server field differs") - } - if obj.Filename != res.Filename { - return fmt.Errorf("the Filename differs") - } - if obj.Path != res.Path { - return fmt.Errorf("the Path differs") - } - if obj.Data != res.Data { - return fmt.Errorf("the Data differs") - } - - return nil -} - -// UnmarshalYAML is the custom unmarshal handler for this struct. It is -// primarily useful for setting the defaults. -func (obj *HTTPFileRes) UnmarshalYAML(unmarshal func(interface{}) error) error { - type rawRes HTTPFileRes // indirection to avoid infinite recursion - - def := obj.Default() // get the default - res, ok := def.(*HTTPFileRes) // put in the right format - if !ok { - return fmt.Errorf("could not convert to HTTPFileRes") - } - raw := rawRes(*res) // convert; the defaults go here - - if err := unmarshal(&raw); err != nil { - return err - } - - *obj = HTTPFileRes(raw) // restore from indirection with type conversion! - return nil -} - // httpError represents a specific HTTP error to send, but can be stored as an // internal golang `error` type. type httpError struct { diff --git a/engine/resources/http_server.go b/engine/resources/http_server.go new file mode 100644 index 00000000..c5b67499 --- /dev/null +++ b/engine/resources/http_server.go @@ -0,0 +1,814 @@ +// Mgmt +// Copyright (C) James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +package resources + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/purpleidea/mgmt/engine" + "github.com/purpleidea/mgmt/engine/traits" + "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/util/errwrap" + + securefilepath "github.com/cyphar/filepath-securejoin" +) + +const ( + // HTTPUseSecureJoin specifies that we should add in a "secure join" lib + // so that we avoid the ../../etc/passwd and symlink problems. + HTTPUseSecureJoin = true + + httpServerKind = httpKind + ":server" +) + +func init() { + engine.RegisterResource(httpServerKind, func() engine.Res { return &HTTPServerRes{} }) +} + +// 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. +// This resource can offer up files for serving that are specified either inline +// in this resource by specifying an http root, or as http:server:file resources +// which will get autogrouped into this resource at runtime. The two methods can +// be combined as well. +// +// This server also supports autogrouping some more magical resources into it. +// For example, the http:server:flag and http:server:ui resources add in magic +// endpoints. +// +// This server is not meant as a featureful replacement for the venerable and +// modern httpd servers out there, but rather as a simple, dynamic, integrated +// alternative for bootstrapping new machines and clusters in an elegant way. +// +// TODO: add support for TLS +// XXX: Make the http:server:ui resource that functions can read data from! +// XXX: The http:server:ui resource can also take in values from those functions +type HTTPServerRes struct { + traits.Base // add the base methods without re-implementation + traits.Edgeable // XXX: add autoedge support + traits.Groupable // can have HTTPServerFileRes and others grouped into it + + init *engine.Init + + // Address is the listen address to use for the http server. It is + // common to use `:80` (the standard) to listen on TCP port 80 on all + // addresses. + Address string `lang:"address" yaml:"address"` + + // Timeout is the maximum duration in seconds to use for unspecified + // timeouts. In other words, when this value is specified, it is used as + // the value for the other *Timeout values when they aren't used. Put + // another way, this makes it easy to set all the different timeouts + // with a single parameter. + Timeout *uint64 `lang:"timeout" yaml:"timeout"` + + // ReadTimeout is the maximum duration in seconds for reading during the + // http request. If it is zero, then there is no timeout. If this is + // unspecified, then the value of Timeout is used instead if it is set. + // For more information, see the golang net/http Server documentation. + ReadTimeout *uint64 `lang:"read_timeout" yaml:"read_timeout"` + + // WriteTimeout is the maximum duration in seconds for writing during + // the http request. If it is zero, then there is no timeout. If this is + // unspecified, then the value of Timeout is used instead if it is set. + // For more information, see the golang net/http Server documentation. + WriteTimeout *uint64 `lang:"write_timeout" yaml:"write_timeout"` + + // ShutdownTimeout is the maximum duration in seconds to wait for the + // server to shutdown gracefully before calling Close. By default it is + // nice to let client connections terminate gracefully, however it might + // take longer than we are willing to wait, particularly if one is long + // polling or running a very long download. As a result, you can set a + // timeout here. The default is zero which means it will wait + // indefinitely. The shutdown process can also be cancelled by the + // interrupt handler which this resource supports. If this is + // unspecified, then the value of Timeout is used instead if it is set. + ShutdownTimeout *uint64 `lang:"shutdown_timeout" yaml:"shutdown_timeout"` + + // Root is the root directory that we should serve files from. If it is + // not specified, then it is not used. Any http file resources will have + // precedence over anything in here, in case the same path exists twice. + // TODO: should we have a flag to determine the precedence rules here? + Root string `lang:"root" yaml:"root"` + + // TODO: should we allow adding a list of one-of files directly here? + + eventsChanMap map[engine.Res]chan error + interruptChan chan struct{} + + conn net.Listener + serveMux *http.ServeMux // can't share the global one between resources! + server *http.Server +} + +// Default returns some sensible defaults for this resource. +func (obj *HTTPServerRes) Default() engine.Res { + return &HTTPServerRes{} +} + +// getAddress returns the actual address to use. When Address is not specified, +// we use the Name. +func (obj *HTTPServerRes) getAddress() string { + if obj.Address != "" { + return obj.Address + } + return obj.Name() +} + +// getReadTimeout determines the value for ReadTimeout, because if unspecified, +// this will default to the value of Timeout. +func (obj *HTTPServerRes) getReadTimeout() *uint64 { + if obj.ReadTimeout != nil { + return obj.ReadTimeout + } + return obj.Timeout // might be nil +} + +// getWriteTimeout determines the value for WriteTimeout, because if +// unspecified, this will default to the value of Timeout. +func (obj *HTTPServerRes) getWriteTimeout() *uint64 { + if obj.WriteTimeout != nil { + return obj.WriteTimeout + } + return obj.Timeout // might be nil +} + +// getShutdownTimeout determines the value for ShutdownTimeout, because if +// unspecified, this will default to the value of Timeout. +func (obj *HTTPServerRes) getShutdownTimeout() *uint64 { + if obj.ShutdownTimeout != nil { + return obj.ShutdownTimeout + } + 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) + sendHTTPError(w, err) + return + } + defer handle.Close() // ignore error + + // 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() == "" { + return fmt.Errorf("empty address") + } + + host, _, err := net.SplitHostPort(obj.getAddress()) + if err != nil { + return errwrap.Wrapf(err, "the Address is in an invalid format: %s", obj.getAddress()) + } + if host != "" { + // TODO: should we allow fqdn's here? + ip := net.ParseIP(host) + if ip == nil { + return fmt.Errorf("the Address is not a valid IP: %s", host) + } + } + + if obj.Root != "" && !strings.HasPrefix(obj.Root, "/") { + return fmt.Errorf("the Root must be absolute") + } + if obj.Root != "" && !strings.HasSuffix(obj.Root, "/") { + return fmt.Errorf("the Root must be a dir") + } + + // XXX: validate that the autogrouped resources don't have paths that + // conflict with each other. We can only have a single unique entry for + // what handles a /whatever URL. + + return nil +} + +// Init runs some startup code for this resource. +func (obj *HTTPServerRes) Init(init *engine.Init) error { + obj.init = init // save for later + + // No need to error in Validate if Timeout is ignored, but log it. + // These are all specified, so Timeout effectively does nothing. + a := obj.ReadTimeout != nil + b := obj.WriteTimeout != nil + c := obj.ShutdownTimeout != nil + if obj.Timeout != nil && (a && b && c) { + obj.init.Logf("the Timeout param is being ignored") + } + + // NOTE: If we don't Init anything that's autogrouped, then it won't + // even get an Init call on it. + + obj.eventsChanMap = make(map[engine.Res]chan error) + + // TODO: should we do this in the engine? Do we want to decide it here? + for _, res := range obj.GetGroup() { // grouped elements + // NOTE: We build a new init, but it's not complete. We only add + // what we're planning to use, and we ignore the rest for now... + r := res // bind the variable! + + obj.eventsChanMap[r] = make(chan error) + event := func() { + select { + case obj.eventsChanMap[r] <- nil: + // send! + } + // We don't do this here (why?) we instead read from the + // above channel and then send on multiplexedChan to the + // main loop, where it runs the obj.init.Event function. + //obj.init.Event() // notify engine of an event (this can block) + } + + newInit := &engine.Init{ + Program: obj.init.Program, + Version: obj.init.Version, + Hostname: obj.init.Hostname, + + // Watch: + Running: event, + Event: event, + + // CheckApply: + Refresh: func() bool { + innerRes, ok := r.(engine.RefreshableRes) + if !ok { + panic("res does not support the Refreshable trait") + } + return innerRes.Refresh() + }, + Send: engine.GenerateSendFunc(r), + Recv: engine.GenerateRecvFunc(r), // unused + + FilteredGraph: func() (*pgraph.Graph, error) { + panic("FilteredGraph for HTTP not implemented") + }, + + Local: obj.init.Local, + World: obj.init.World, + //VarDir: obj.init.VarDir, // TODO: wrap this + + Debug: obj.init.Debug, + Logf: func(format string, v ...interface{}) { + obj.init.Logf(r.String()+": "+format, v...) + }, + } + + if err := res.Init(newInit); err != nil { + return errwrap.Wrapf(err, "autogrouped Init failed") + } + } + + obj.interruptChan = make(chan struct{}) + + return nil +} + +// Cleanup is run by the engine to clean up after the resource is done. +func (obj *HTTPServerRes) Cleanup() error { + return nil +} + +// Watch is the primary listener for this resource and it outputs events. +func (obj *HTTPServerRes) Watch(ctx context.Context) error { + // TODO: I think we could replace all this with: + //obj.conn, err := net.Listen("tcp", obj.getAddress()) + // ...but what is the advantage? + addr, err := net.ResolveTCPAddr("tcp", obj.getAddress()) + if err != nil { + return errwrap.Wrapf(err, "could not resolve address") + } + + obj.conn, err = net.ListenTCP("tcp", addr) + if err != nil { + return errwrap.Wrapf(err, "could not start listener") + } + 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) + if i := obj.getReadTimeout(); i != nil { + readTimeout = *i + } + writeTimeout := uint64(0) + if i := obj.getWriteTimeout(); i != nil { + writeTimeout = *i + } + obj.server = &http.Server{ + Addr: obj.getAddress(), + Handler: obj.serveMux, + ReadTimeout: time.Duration(readTimeout) * time.Second, + WriteTimeout: time.Duration(writeTimeout) * time.Second, + //MaxHeaderBytes: 1 << 20, XXX: should we add a param for this? + } + + multiplexedChan := make(chan error) + defer close(multiplexedChan) // closes after everyone below us is finished + + wg := &sync.WaitGroup{} + defer wg.Wait() + + for _, r := range obj.GetGroup() { // grouped elements + res := r // optional in newer golang + wg.Add(1) + go func() { + defer wg.Done() + defer close(obj.eventsChanMap[res]) // where Watch sends events + if err := res.Watch(ctx); err != nil { + select { + case multiplexedChan <- err: + case <-ctx.Done(): + } + } + }() + // wait for Watch first Running() call or immediate error... + select { + case <-obj.eventsChanMap[res]: // triggers on start or on err... + } + + wg.Add(1) + go func() { + defer wg.Done() + for { + var ok bool + var err error + select { + // receive + case err, ok = <-obj.eventsChanMap[res]: + if !ok { + return + } + } + + // send (multiplex) + select { + case multiplexedChan <- err: + case <-ctx.Done(): + return + } + } + }() + } + // we block until all the children are started first... + + obj.init.Running() // when started, notify engine that we're running + + var closeError error + closeSignal := make(chan struct{}) + + shutdownChan := make(chan struct{}) // server shutdown finished signal + wg.Add(1) + go func() { + defer wg.Done() + select { + case <-obj.interruptChan: + // TODO: should we bubble up the error from Close? + // TODO: do we need a mutex around this Close? + obj.server.Close() // kill it quickly! + case <-shutdownChan: + // let this exit + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer close(closeSignal) + + err := obj.server.Serve(obj.conn) // blocks until Shutdown() is called! + if err == nil || err == http.ErrServerClosed { + return + } + // if this returned on its own, then closeSignal can be used... + closeError = errwrap.Wrapf(err, "the server errored") + }() + + // When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS + // immediately return ErrServerClosed. Make sure the program doesn't + // exit and waits instead for Shutdown to return. + defer func() { + defer close(shutdownChan) // signal that shutdown is finished + innerCtx := context.Background() + if i := obj.getShutdownTimeout(); i != nil && *i > 0 { + var cancel context.CancelFunc + innerCtx, cancel = context.WithTimeout(innerCtx, time.Duration(*i)*time.Second) + defer cancel() + } + err := obj.server.Shutdown(innerCtx) // shutdown gracefully + if err == context.DeadlineExceeded { + // TODO: should we bubble up the error from Close? + // TODO: do we need a mutex around this Close? + obj.server.Close() // kill it now + } + }() + + startupChan := make(chan struct{}) + close(startupChan) // send one initial signal + + var send = false // send event? + for { + if obj.init.Debug { + obj.init.Logf("Looping...") + } + + select { + case <-startupChan: + startupChan = nil + send = true + + case err, ok := <-multiplexedChan: + if !ok { // shouldn't happen + multiplexedChan = nil + continue + } + if err != nil { + return err + } + send = true + + case <-closeSignal: // something shut us down early + return closeError + + case <-ctx.Done(): // closed by the engine to signal shutdown + return nil + } + + // do all our event sending all together to avoid duplicate msgs + if send { + send = false + obj.init.Event() // notify engine of an event (this can block) + } + } +} + +// CheckApply never has anything to do for this resource, so it always succeeds. +// It does however check that certain runtime requirements (such as the Root dir +// existing if one was specified) are fulfilled. If there are any autogrouped +// resources, those will be recursively called so that they can send/recv. +func (obj *HTTPServerRes) CheckApply(ctx context.Context, apply bool) (bool, error) { + if obj.init.Debug { + obj.init.Logf("CheckApply") + } + + // XXX: We don't want the initial CheckApply to return true until the + // Watch has started up, so we must block here until that's the case... + + // Cheap runtime validation! + // XXX: maybe only do this only once to avoid repeated, unnecessary checks? + if obj.Root != "" { + fileInfo, err := os.Stat(obj.Root) + if err != nil { + return false, errwrap.Wrapf(err, "can't stat Root dir") + } + if !fileInfo.IsDir() { + return false, fmt.Errorf("the Root path is not a dir") + } + } + + checkOK := true + for _, res := range obj.GetGroup() { // grouped elements + if c, err := res.CheckApply(ctx, apply); err != nil { + return false, errwrap.Wrapf(err, "autogrouped CheckApply failed") + } else if !c { + checkOK = false + } + } + + return checkOK, nil +} + +// Cmp compares two resources and returns an error if they are not equivalent. +func (obj *HTTPServerRes) Cmp(r engine.Res) error { + // we can only compare HTTPServerRes to others of the same resource kind + res, ok := r.(*HTTPServerRes) + if !ok { + return fmt.Errorf("res is not the same kind") + } + + if obj.Address != res.Address { + return fmt.Errorf("the Address differs") + } + + if (obj.Timeout == nil) != (res.Timeout == nil) { // xor + return fmt.Errorf("the Timeout differs") + } + if obj.Timeout != nil && res.Timeout != nil { + if *obj.Timeout != *res.Timeout { // compare the values + return fmt.Errorf("the value of Timeout differs") + } + } + if (obj.ReadTimeout == nil) != (res.ReadTimeout == nil) { + return fmt.Errorf("the ReadTimeout differs") + } + if obj.ReadTimeout != nil && res.ReadTimeout != nil { + if *obj.ReadTimeout != *res.ReadTimeout { + return fmt.Errorf("the value of ReadTimeout differs") + } + } + if (obj.WriteTimeout == nil) != (res.WriteTimeout == nil) { + return fmt.Errorf("the WriteTimeout differs") + } + if obj.WriteTimeout != nil && res.WriteTimeout != nil { + if *obj.WriteTimeout != *res.WriteTimeout { + return fmt.Errorf("the value of WriteTimeout differs") + } + } + if (obj.ShutdownTimeout == nil) != (res.ShutdownTimeout == nil) { + return fmt.Errorf("the ShutdownTimeout differs") + } + if obj.ShutdownTimeout != nil && res.ShutdownTimeout != nil { + if *obj.ShutdownTimeout != *res.ShutdownTimeout { + return fmt.Errorf("the value of ShutdownTimeout differs") + } + } + + // TODO: We could do this sort of thing to skip checking Timeout when it + // is not used, but for the moment, this is overkill and not needed yet. + //a := obj.ReadTimeout != nil + //b := obj.WriteTimeout != nil + //c := obj.ShutdownTimeout != nil + //if !(obj.Timeout != nil && (a && b && c)) { + // // the Timeout param is not being ignored + //} + + if obj.Root != res.Root { + return fmt.Errorf("the Root differs") + } + + return nil +} + +// Interrupt is called to ask the execution of this resource to end early. It +// will cause the server Shutdown to end abruptly instead of leading open client +// connections terminate gracefully. It does this by causing the server Close +// method to run. +func (obj *HTTPServerRes) Interrupt() error { + close(obj.interruptChan) // this should cause obj.server.Close() to run! + return nil +} + +// Copy copies the resource. Don't call it directly, use engine.ResCopy instead. +// TODO: should this copy internal state? +func (obj *HTTPServerRes) Copy() engine.CopyableRes { + var timeout, readTimeout, writeTimeout, shutdownTimeout *uint64 + if obj.Timeout != nil { + x := *obj.Timeout + timeout = &x + } + if obj.ReadTimeout != nil { + x := *obj.ReadTimeout + readTimeout = &x + } + if obj.WriteTimeout != nil { + x := *obj.WriteTimeout + writeTimeout = &x + } + if obj.ShutdownTimeout != nil { + x := *obj.ShutdownTimeout + shutdownTimeout = &x + } + return &HTTPServerRes{ + Address: obj.Address, + Timeout: timeout, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + ShutdownTimeout: shutdownTimeout, + Root: obj.Root, + } +} + +// UnmarshalYAML is the custom unmarshal handler for this struct. It is +// primarily useful for setting the defaults. +func (obj *HTTPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes HTTPServerRes // indirection to avoid infinite recursion + + def := obj.Default() // get the default + res, ok := def.(*HTTPServerRes) // put in the right format + if !ok { + return fmt.Errorf("could not convert to HTTPServerRes") + } + raw := rawRes(*res) // convert; the defaults go here + + if err := unmarshal(&raw); err != nil { + return err + } + + *obj = HTTPServerRes(raw) // restore from indirection with type conversion! + return nil +} + +// GroupCmp returns whether two resources can be grouped together or not. Can +// 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 { + res, ok := r.(HTTPServerGroupableRes) // different from what we usually do! + if !ok { + 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:server:foo is okay, but file or config:etcd is not + if !strings.HasPrefix(r.Kind(), httpServerKind+":") { + 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 := httpServerKind + ":" + //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", httpServerKind) + //} + + return nil +} + +// readHandler handles all the incoming download requests from clients. +func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) { + // TODO: we could statically pre-compute some stuff here... + + return func(w http.ResponseWriter, req *http.Request) { + + if obj.init.Debug { + obj.init.Logf("Client: %s", req.RemoteAddr) + } + // TODO: would this leak anything security sensitive in our log? + obj.init.Logf("URL: %s", req.URL) + requestPath := req.URL.Path // TODO: is this what we want here? + 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.(HTTPServerGroupableRes) // convert from Res + if !ok { + continue + } + if obj.init.Debug { + obj.init.Logf("Got grouped resource: %s", res.String()) + } + + err := res.AcceptHTTP(req) + if err == nil { + res.ServeHTTP(w, req) + return + } + 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... + err := obj.AcceptHTTP(req) + if err == nil { + obj.ServeHTTP(w, req) + return + } + if obj.init.Debug { + obj.init.Logf("Could not serve Root: %+v", err) + } + + // 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 + } +} diff --git a/engine/resources/http_server_file.go b/engine/resources/http_server_file.go new file mode 100644 index 00000000..f124459e --- /dev/null +++ b/engine/resources/http_server_file.go @@ -0,0 +1,337 @@ +// Mgmt +// Copyright (C) James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +package resources + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "github.com/purpleidea/mgmt/engine" + "github.com/purpleidea/mgmt/engine/traits" + "github.com/purpleidea/mgmt/util/safepath" +) + +const ( + httpServerFileKind = httpServerKind + ":file" +) + +func init() { + engine.RegisterResource(httpServerFileKind, func() engine.Res { return &HTTPServerFileRes{} }) +} + +// HTTPServerFileRes is a file that exists within an http server. The name is +// used as the public path of the file, unless the filename field is specified, +// and in that case it is used instead. The way this works is that it autogroups +// at runtime with an existing http resource, and in doing so makes the file +// associated with this resource available for serving from that http server. +type HTTPServerFileRes struct { + traits.Base // add the base methods without re-implementation + traits.Edgeable // XXX: add autoedge support + traits.Groupable // can be grouped into HTTPServerRes + + init *engine.Init + + // Server is the name of the http server resource to group this into. If + // it is omitted, and there is only a single http resource, then it will + // be grouped into it automatically. If there is more than one main http + // resource being used, then the grouping behaviour is *undefined* when + // this is not specified, and it is not recommended to leave this blank! + Server string `lang:"server" yaml:"server"` + + // Filename is the name of the file this data should appear as on the + // http server. + Filename string `lang:"filename" yaml:"filename"` + + // Path is the absolute path to a file that should be used as the source + // for this file resource. It must not be combined with the data field. + // If this corresponds to a directory, then it will used as a root dir + // that will be served as long as the resource name or Filename are also + // a directory ending with a slash. + Path string `lang:"path" yaml:"path"` + + // Data is the file content that should be used as the source for this + // file resource. It must not be combined with the path field. + // TODO: should this be []byte instead? + Data string `lang:"data" yaml:"data"` +} + +// Default returns some sensible defaults for this resource. +func (obj *HTTPServerFileRes) Default() engine.Res { + return &HTTPServerFileRes{} +} + +// getPath returns the actual path we respond to. When Filename is not +// specified, we use the Name. Note that this is the filename that will be seen +// on the http server, it is *not* the source path to the actual file contents +// being sent by the server. +func (obj *HTTPServerFileRes) getPath() string { + if obj.Filename != "" { + return obj.Filename + } + return obj.Name() +} + +// getContent returns the content that we expect from this resource. It depends +// on whether the user specified the Path or Data fields, and whether the Path +// exists or not. +func (obj *HTTPServerFileRes) getContent(requestPath safepath.AbsPath) (io.ReadSeeker, error) { + if obj.Path != "" && obj.Data != "" { + // programming error! this should have been caught in Validate! + return nil, fmt.Errorf("must not specify Path and Data") + } + + if obj.Data != "" { + return bytes.NewReader([]byte(obj.Data)), nil + } + + absFile, err := obj.getContentRelative(requestPath) + if err != nil { // on error, we just assume no root/prefix stuff happens + return os.Open(obj.Path) + } + + return os.Open(absFile.Path()) +} + +// getContentRelative takes a request, and returns the absolute path to the file +// that we want to request, if it's safely under what we can provide. +func (obj *HTTPServerFileRes) getContentRelative(requestPath safepath.AbsPath) (safepath.AbsFile, error) { + // the location on disk of the data + srcPath, err := safepath.SmartParseIntoPath(obj.Path) // (safepath.Path, error) + if err != nil { + return safepath.AbsFile{}, err + } + srcAbsDir, ok := srcPath.(safepath.AbsDir) + if !ok { + return safepath.AbsFile{}, fmt.Errorf("the Path is not an abs dir") + } + + // the public path we respond to (might be a dir prefix or just a file) + pubPath, err := safepath.SmartParseIntoPath(obj.getPath()) // (safepath.Path, error) + if err != nil { + return safepath.AbsFile{}, err + } + pubAbsDir, ok := pubPath.(safepath.AbsDir) + if !ok { + return safepath.AbsFile{}, fmt.Errorf("the name is not an abs dir") + } + + // is the request underneath what we're providing? + if !safepath.HasPrefix(requestPath, pubAbsDir) { + return safepath.AbsFile{}, fmt.Errorf("wrong prefix") + } + + // make the delta + delta, err := safepath.StripPrefix(requestPath, pubAbsDir) // (safepath.Path, error) + if err != nil { + return safepath.AbsFile{}, err + } + relFile, ok := delta.(safepath.RelFile) + if !ok { + return safepath.AbsFile{}, fmt.Errorf("the delta is not a rel file") + } + + return safepath.JoinToAbsFile(srcAbsDir, relFile), nil // AbsFile +} + +// 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 *HTTPServerFileRes) 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 *HTTPServerFileRes) AcceptHTTP(req *http.Request) error { + requestPath := req.URL.Path // TODO: is this what we want here? + + if strings.HasSuffix(obj.Path, "/") { // a dir! + if strings.HasPrefix(requestPath, obj.getPath()) { + // relative dir root + return nil + } + } + + if requestPath != obj.getPath() { + return fmt.Errorf("unhandled path") + } + return nil +} + +// ServeHTTP is the standard HTTP handler that will be used here. +func (obj *HTTPServerFileRes) 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? + + absPath, err := safepath.ParseIntoAbsPath(requestPath) + if err != nil { + obj.init.Logf("invalid input path: %s", requestPath) + sendHTTPError(w, err) + return + } + + handle, err := obj.getContent(absPath) + if err != nil { + obj.init.Logf("could not get content for: %s", requestPath) + sendHTTPError(w, err) + return + } + //if readSeekCloser, ok := handle.(io.ReadSeekCloser); ok { // same + // defer readSeekCloser.Close() // ignore error + //} + if closer, ok := handle.(io.Closer); ok { + defer closer.Close() // ignore error + } + + // 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 *HTTPServerFileRes) Validate() error { + if obj.getPath() == "" { + return fmt.Errorf("empty filename") + } + // FIXME: does getPath need to start with a slash? + + if obj.Path != "" && !strings.HasPrefix(obj.Path, "/") { + return fmt.Errorf("the Path must be absolute") + } + + if obj.Path != "" && obj.Data != "" { + return fmt.Errorf("must not specify Path and Data") + } + + // NOTE: if obj.Path == "" && obj.Data == "" then we have an empty file! + + return nil +} + +// Init runs some startup code for this resource. +func (obj *HTTPServerFileRes) Init(init *engine.Init) error { + obj.init = init // save for later + + return nil +} + +// Cleanup is run by the engine to clean up after the resource is done. +func (obj *HTTPServerFileRes) Cleanup() error { + return nil +} + +// Watch is the primary listener for this resource and it outputs events. This +// particular one does absolutely nothing but block until we've received a done +// signal. +func (obj *HTTPServerFileRes) Watch(ctx context.Context) error { + obj.init.Running() // when started, notify engine that we're running + + select { + case <-ctx.Done(): // closed by the engine to signal shutdown + } + + //obj.init.Event() // notify engine of an event (this can block) + + return nil +} + +// CheckApply never has anything to do for this resource, so it always succeeds. +func (obj *HTTPServerFileRes) CheckApply(ctx context.Context, apply bool) (bool, error) { + if obj.init.Debug { + obj.init.Logf("CheckApply") + } + + return true, nil // always succeeds, with nothing to do! +} + +// Cmp compares two resources and returns an error if they are not equivalent. +func (obj *HTTPServerFileRes) Cmp(r engine.Res) error { + // we can only compare HTTPServerFileRes to others of the same resource kind + res, ok := r.(*HTTPServerFileRes) + if !ok { + return fmt.Errorf("res is not the same kind") + } + + if obj.Server != res.Server { + return fmt.Errorf("the Server field differs") + } + if obj.Filename != res.Filename { + return fmt.Errorf("the Filename differs") + } + if obj.Path != res.Path { + return fmt.Errorf("the Path differs") + } + if obj.Data != res.Data { + return fmt.Errorf("the Data differs") + } + + return nil +} + +// UnmarshalYAML is the custom unmarshal handler for this struct. It is +// primarily useful for setting the defaults. +func (obj *HTTPServerFileRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes HTTPServerFileRes // indirection to avoid infinite recursion + + def := obj.Default() // get the default + res, ok := def.(*HTTPServerFileRes) // put in the right format + if !ok { + return fmt.Errorf("could not convert to HTTPServerFileRes") + } + raw := rawRes(*res) // convert; the defaults go here + + if err := unmarshal(&raw); err != nil { + return err + } + + *obj = HTTPServerFileRes(raw) // restore from indirection with type conversion! + return nil +} diff --git a/engine/resources/http_flag.go b/engine/resources/http_server_flag.go similarity index 78% rename from engine/resources/http_flag.go rename to engine/resources/http_server_flag.go index 13db670e..098dfc21 100644 --- a/engine/resources/http_flag.go +++ b/engine/resources/http_server_flag.go @@ -41,23 +41,23 @@ import ( ) const ( - httpFlagKind = httpKind + ":flag" + httpServerFlagKind = httpServerKind + ":flag" ) func init() { - engine.RegisterResource(httpFlagKind, func() engine.Res { return &HTTPFlagRes{} }) + engine.RegisterResource(httpServerFlagKind, func() engine.Res { return &HTTPServerFlagRes{} }) } -// HTTPFlagRes is a special path that exists within an http server. The name is -// used as the public path of the flag, unless the path field is specified, and -// in that case it is used instead. The way this works is that it autogroups at -// runtime with an existing http resource, and in doing so makes the flag -// associated with this resource available to cause actions when it receives a -// request on that http server. If you create a flag which responds to the same -// type of request as an http:file resource or any other kind of resource, it is -// undefined behaviour which will answer the request. The most common clash will -// happen if both are present at the same path. -type HTTPFlagRes struct { +// HTTPServerFlagRes is a special path that exists within an http server. The +// name is used as the public path of the flag, unless the path field is +// specified, and in that case it is used instead. The way this works is that it +// autogroups at runtime with an existing http resource, and in doing so makes +// the flag associated with this resource available to cause actions when it +// receives a request on that http server. If you create a flag which responds +// to the same type of request as an http:server:file resource or any other kind +// of resource, it is undefined behaviour which will answer the request. The +// most common clash will happen if both are present at the same path. +type HTTPServerFlagRes struct { traits.Base // add the base methods without re-implementation traits.Edgeable // XXX: add autoedge support traits.Groupable // can be grouped into HTTPServerRes @@ -88,13 +88,13 @@ type HTTPFlagRes struct { } // Default returns some sensible defaults for this resource. -func (obj *HTTPFlagRes) Default() engine.Res { - return &HTTPFlagRes{} +func (obj *HTTPServerFlagRes) Default() engine.Res { + return &HTTPServerFlagRes{} } // getPath returns the actual path we respond to. When Path is not specified, we // use the Name. -func (obj *HTTPFlagRes) getPath() string { +func (obj *HTTPServerFlagRes) getPath() string { if obj.Path != "" { return obj.Path } @@ -104,13 +104,13 @@ func (obj *HTTPFlagRes) getPath() string { // 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 *HTTPFlagRes) ParentName() string { +func (obj *HTTPServerFlagRes) 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 *HTTPFlagRes) AcceptHTTP(req *http.Request) error { +func (obj *HTTPServerFlagRes) 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") @@ -125,7 +125,7 @@ func (obj *HTTPFlagRes) AcceptHTTP(req *http.Request) error { } // ServeHTTP is the standard HTTP handler that will be used here. -func (obj *HTTPFlagRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { +func (obj *HTTPServerFlagRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { // We only allow POST at the moment. if req.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) @@ -166,7 +166,7 @@ func (obj *HTTPFlagRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { } // Validate checks if the resource data structure was populated correctly. -func (obj *HTTPFlagRes) Validate() error { +func (obj *HTTPServerFlagRes) Validate() error { if obj.getPath() == "" { return fmt.Errorf("empty filename") } @@ -179,7 +179,7 @@ func (obj *HTTPFlagRes) Validate() error { } // Init runs some startup code for this resource. -func (obj *HTTPFlagRes) Init(init *engine.Init) error { +func (obj *HTTPServerFlagRes) Init(init *engine.Init) error { obj.init = init // save for later obj.mutex = &sync.Mutex{} @@ -189,7 +189,7 @@ func (obj *HTTPFlagRes) Init(init *engine.Init) error { } // Cleanup is run by the engine to clean up after the resource is done. -func (obj *HTTPFlagRes) Cleanup() error { +func (obj *HTTPServerFlagRes) Cleanup() error { return nil } @@ -197,7 +197,7 @@ func (obj *HTTPFlagRes) Cleanup() error { // particular one listens for events from incoming http requests to the flag, // and notifies the engine so that CheckApply can then run and return the // correct value on send/recv. -func (obj *HTTPFlagRes) Watch(ctx context.Context) error { +func (obj *HTTPServerFlagRes) Watch(ctx context.Context) error { obj.init.Running() // when started, notify engine that we're running startupChan := make(chan struct{}) @@ -237,7 +237,7 @@ func (obj *HTTPFlagRes) Watch(ctx context.Context) error { } // CheckApply never has anything to do for this resource, so it always succeeds. -func (obj *HTTPFlagRes) CheckApply(ctx context.Context, apply bool) (bool, error) { +func (obj *HTTPServerFlagRes) CheckApply(ctx context.Context, apply bool) (bool, error) { if obj.init.Debug || true { // XXX: maybe we should always do this? obj.init.Logf("value: %+v", obj.value) } @@ -276,7 +276,7 @@ func (obj *HTTPFlagRes) CheckApply(ctx context.Context, apply bool) (bool, error // As a result, we need to run send/recv on the new graph after // autogrouping, so that we compare apples to apples, when we do the // graphsync! - if err := obj.init.Send(&HTTPFlagSends{ + if err := obj.init.Send(&HTTPServerFlagSends{ Value: &value, }); err != nil { return false, err @@ -287,9 +287,9 @@ func (obj *HTTPFlagRes) CheckApply(ctx context.Context, apply bool) (bool, error } // Cmp compares two resources and returns an error if they are not equivalent. -func (obj *HTTPFlagRes) Cmp(r engine.Res) error { - // we can only compare HTTPFlagRes to others of the same resource kind - res, ok := r.(*HTTPFlagRes) +func (obj *HTTPServerFlagRes) Cmp(r engine.Res) error { + // we can only compare HTTPServerFlagRes to others of the same resource kind + res, ok := r.(*HTTPServerFlagRes) if !ok { return fmt.Errorf("res is not the same kind") } @@ -307,28 +307,29 @@ func (obj *HTTPFlagRes) Cmp(r engine.Res) error { return nil } -// HTTPFlagSends is the struct of data which is sent after a successful Apply. -type HTTPFlagSends struct { +// HTTPServerFlagSends is the struct of data which is sent after a successful +// Apply. +type HTTPServerFlagSends struct { // Value is the received value being sent. Value *string `lang:"value"` } // Sends represents the default struct of values we can send using Send/Recv. -func (obj *HTTPFlagRes) Sends() interface{} { - return &HTTPFlagSends{ +func (obj *HTTPServerFlagRes) Sends() interface{} { + return &HTTPServerFlagSends{ Value: nil, } } // UnmarshalYAML is the custom unmarshal handler for this struct. It is // primarily useful for setting the defaults. -func (obj *HTTPFlagRes) UnmarshalYAML(unmarshal func(interface{}) error) error { - type rawRes HTTPFlagRes // indirection to avoid infinite recursion +func (obj *HTTPServerFlagRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes HTTPServerFlagRes // indirection to avoid infinite recursion - def := obj.Default() // get the default - res, ok := def.(*HTTPFlagRes) // put in the right format + def := obj.Default() // get the default + res, ok := def.(*HTTPServerFlagRes) // put in the right format if !ok { - return fmt.Errorf("could not convert to HTTPFlagRes") + return fmt.Errorf("could not convert to HTTPServerFlagRes") } raw := rawRes(*res) // convert; the defaults go here @@ -336,6 +337,6 @@ func (obj *HTTPFlagRes) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } - *obj = HTTPFlagRes(raw) // restore from indirection with type conversion! + *obj = HTTPServerFlagRes(raw) // restore from indirection with type conversion! return nil } diff --git a/engine/resources/http_proxy.go b/engine/resources/http_server_proxy.go similarity index 83% rename from engine/resources/http_proxy.go rename to engine/resources/http_server_proxy.go index 631a3ce6..6b809860 100644 --- a/engine/resources/http_proxy.go +++ b/engine/resources/http_server_proxy.go @@ -49,43 +49,44 @@ import ( ) const ( - httpProxyKind = httpKind + ":proxy" + httpServerProxyKind = httpServerKind + ":proxy" ) var ( - // httpProxyRWMutex synchronizes against reads and writes to the cache. + // httpServerProxyRWMutex synchronizes against reads and writes to the cache. // TODO: we could instead have a per-cache path individual mutex, but to // keep things simple for now, we just lumped them all together. - httpProxyRWMutex *sync.RWMutex + httpServerProxyRWMutex *sync.RWMutex ) func init() { - httpProxyRWMutex = &sync.RWMutex{} + httpServerProxyRWMutex = &sync.RWMutex{} - engine.RegisterResource(httpProxyKind, func() engine.Res { return &HTTPProxyRes{} }) + engine.RegisterResource(httpServerProxyKind, func() engine.Res { return &HTTPServerProxyRes{} }) } -// HTTPProxyRes is a resource representing a special path that exists within an -// http server. The name is used as the public path of the endpoint, unless the -// path field is specified, and in that case it is used instead. The way this -// works is that it autogroups at runtime with an existing http resource, and in -// doing so makes the path associated with this resource available when serving -// files. When something under the path is accessed, this is pulled from the -// backing http server, which makes an http client connection if needed to pull -// the authoritative file down, saves it locally for future use, and then -// returns it to the original http client caller. On a subsequent call, if the -// cache was not invalidated, the file doesn't need to be fetched from the -// network. In effect, this works as a caching http proxy. If you create this as -// a resource which responds to the same type of request as an http:file -// resource or any other kind of resource, it is undefined behaviour which will -// answer the request. The most common clash will happen if both are present at -// the same path. This particular implementation stores some file data in memory -// as a convenience instead of streaming directly to clients. This makes locking -// much easier, but is wasteful. If you plan on using this for huge files and on -// systems with low amounts of memory, you might want to optimize this. The -// resultant proxy path is determined by subtracting the `Sub` field from the -// `Path` (and request path) and then appending the result to the `Head` field. -type HTTPProxyRes struct { +// HTTPServerProxyRes is a resource representing a special path that exists +// within an http server. The name is used as the public path of the endpoint, +// unless the path field is specified, and in that case it is used instead. The +// way this works is that it autogroups at runtime with an existing http server +// resource, and in doing so makes the path associated with this resource +// available when serving files. When something under the path is accessed, this +// is pulled from the backing http server, which makes an http client connection +// if needed to pull the authoritative file down, saves it locally for future +// use, and then returns it to the original http client caller. On a subsequent +// call, if the cache was not invalidated, the file doesn't need to be fetched +// from the network. In effect, this works as a caching http proxy. If you +// create this as a resource which responds to the same type of request as an +// http:server:file resource or any other kind of resource, it is undefined +// behaviour which will answer the request. The most common clash will happen if +// both are present at the same path. This particular implementation stores some +// file data in memory as a convenience instead of streaming directly to +// clients. This makes locking much easier, but is wasteful. If you plan on +// using this for huge files and on systems with low amounts of memory, you +// might want to optimize this. The resultant proxy path is determined by +// subtracting the `Sub` field from the `Path` (and request path) and then +// appending the result to the `Head` field. +type HTTPServerProxyRes struct { traits.Base // add the base methods without re-implementation traits.Edgeable // XXX: add autoedge support traits.Groupable // can be grouped into HTTPServerRes @@ -136,13 +137,13 @@ type HTTPProxyRes struct { } // Default returns some sensible defaults for this resource. -func (obj *HTTPProxyRes) Default() engine.Res { - return &HTTPProxyRes{} +func (obj *HTTPServerProxyRes) Default() engine.Res { + return &HTTPServerProxyRes{} } // getPath returns the actual path we respond to. When Path is not specified, we // use the Name. -func (obj *HTTPProxyRes) getPath() string { +func (obj *HTTPServerProxyRes) getPath() string { if obj.Path != "" { return obj.Path } @@ -151,7 +152,7 @@ func (obj *HTTPProxyRes) getPath() string { // serveHTTP is the real implementation of ServeHTTP, but with a more ergonomic // signature. -func (obj *HTTPProxyRes) serveHTTP(ctx context.Context, requestPath string) (handlerFuncError, error) { +func (obj *HTTPServerProxyRes) serveHTTP(ctx context.Context, requestPath string) (handlerFuncError, error) { // TODO: switch requestPath to use safepath.AbsPath instead of a string result, err := obj.pathParser.parse(requestPath) @@ -237,8 +238,8 @@ func (obj *HTTPProxyRes) serveHTTP(ctx context.Context, requestPath string) (han writers := []io.Writer{w} // out to the client if obj.Cache != "" { // check in the cache... - httpProxyRWMutex.Lock() - defer httpProxyRWMutex.Unlock() + httpServerProxyRWMutex.Lock() + defer httpServerProxyRWMutex.Unlock() // store in cachePath if err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil { @@ -323,11 +324,11 @@ func (obj *HTTPProxyRes) serveHTTP(ctx context.Context, requestPath string) (han // getCachedFile pulls a file from our local cache if it exists. It returns the // correct http handler on success, which we can then run. -func (obj *HTTPProxyRes) getCachedFile(ctx context.Context, absPath string) (handlerFuncError, error) { +func (obj *HTTPServerProxyRes) getCachedFile(ctx context.Context, absPath string) (handlerFuncError, error) { // TODO: if infinite reads keep coming in, do we indefinitely-postpone // the locking so that a new file can be saved in the cache? - httpProxyRWMutex.RLock() - defer httpProxyRWMutex.RUnlock() + httpServerProxyRWMutex.RLock() + defer httpServerProxyRWMutex.RUnlock() f, err := os.Open(absPath) if err != nil { @@ -361,13 +362,13 @@ func (obj *HTTPProxyRes) getCachedFile(ctx context.Context, absPath string) (han // 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 *HTTPProxyRes) ParentName() string { +func (obj *HTTPServerProxyRes) 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 *HTTPProxyRes) AcceptHTTP(req *http.Request) error { +func (obj *HTTPServerProxyRes) AcceptHTTP(req *http.Request) error { requestPath := req.URL.Path // TODO: is this what we want here? if p := obj.getPath(); strings.HasSuffix(p, "/") { // a dir! @@ -384,7 +385,7 @@ func (obj *HTTPProxyRes) AcceptHTTP(req *http.Request) error { } // ServeHTTP is the standard HTTP handler that will be used here. -func (obj *HTTPProxyRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { +func (obj *HTTPServerProxyRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { // We only allow GET at the moment. if req.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) @@ -419,7 +420,7 @@ func (obj *HTTPProxyRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { } // Validate checks if the resource data structure was populated correctly. -func (obj *HTTPProxyRes) Validate() error { +func (obj *HTTPServerProxyRes) Validate() error { if obj.getPath() == "" { return fmt.Errorf("empty filename") } @@ -449,7 +450,7 @@ func (obj *HTTPProxyRes) Validate() error { } // Init runs some startup code for this resource. -func (obj *HTTPProxyRes) Init(init *engine.Init) error { +func (obj *HTTPServerProxyRes) Init(init *engine.Init) error { obj.init = init // save for later obj.pathParser = &pathParser{ @@ -463,14 +464,14 @@ func (obj *HTTPProxyRes) Init(init *engine.Init) error { } // Cleanup is run by the engine to clean up after the resource is done. -func (obj *HTTPProxyRes) Cleanup() error { +func (obj *HTTPServerProxyRes) Cleanup() error { return nil } // Watch is the primary listener for this resource and it outputs events. This // particular one does absolutely nothing but block until we've received a done // signal. -func (obj *HTTPProxyRes) Watch(ctx context.Context) error { +func (obj *HTTPServerProxyRes) Watch(ctx context.Context) error { obj.init.Running() // when started, notify engine that we're running select { @@ -483,7 +484,7 @@ func (obj *HTTPProxyRes) Watch(ctx context.Context) error { } // CheckApply never has anything to do for this resource, so it always succeeds. -func (obj *HTTPProxyRes) CheckApply(ctx context.Context, apply bool) (bool, error) { +func (obj *HTTPServerProxyRes) CheckApply(ctx context.Context, apply bool) (bool, error) { if obj.init.Debug { obj.init.Logf("CheckApply") } @@ -492,9 +493,9 @@ func (obj *HTTPProxyRes) CheckApply(ctx context.Context, apply bool) (bool, erro } // Cmp compares two resources and returns an error if they are not equivalent. -func (obj *HTTPProxyRes) Cmp(r engine.Res) error { - // we can only compare HTTPProxyRes to others of the same resource kind - res, ok := r.(*HTTPProxyRes) +func (obj *HTTPServerProxyRes) Cmp(r engine.Res) error { + // we can only compare HTTPServerProxyRes to others of the same resource kind + res, ok := r.(*HTTPServerProxyRes) if !ok { return fmt.Errorf("res is not the same kind") } @@ -521,13 +522,13 @@ func (obj *HTTPProxyRes) Cmp(r engine.Res) error { // UnmarshalYAML is the custom unmarshal handler for this struct. It is // primarily useful for setting the defaults. -func (obj *HTTPProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error { - type rawRes HTTPProxyRes // indirection to avoid infinite recursion +func (obj *HTTPServerProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes HTTPServerProxyRes // indirection to avoid infinite recursion - def := obj.Default() // get the default - res, ok := def.(*HTTPProxyRes) // put in the right format + def := obj.Default() // get the default + res, ok := def.(*HTTPServerProxyRes) // put in the right format if !ok { - return fmt.Errorf("could not convert to HTTPProxyRes") + return fmt.Errorf("could not convert to HTTPServerProxyRes") } raw := rawRes(*res) // convert; the defaults go here @@ -535,7 +536,7 @@ func (obj *HTTPProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error return err } - *obj = HTTPProxyRes(raw) // restore from indirection with type conversion! + *obj = HTTPServerProxyRes(raw) // restore from indirection with type conversion! return nil } diff --git a/engine/resources/http_proxy_test.go b/engine/resources/http_server_proxy_test.go similarity index 99% rename from engine/resources/http_proxy_test.go rename to engine/resources/http_server_proxy_test.go index 5bf7fdb9..5b65710e 100644 --- a/engine/resources/http_proxy_test.go +++ b/engine/resources/http_server_proxy_test.go @@ -36,7 +36,7 @@ import ( "testing" ) -func TestHttpProxyPathParser0(t *testing.T) { +func TestHttpServerProxyPathParser0(t *testing.T) { type test struct { // an individual test fail bool diff --git a/engine/resources/http_ui.go b/engine/resources/http_server_ui.go similarity index 83% rename from engine/resources/http_ui.go rename to engine/resources/http_server_ui.go index 0e16069c..39996dae 100644 --- a/engine/resources/http_ui.go +++ b/engine/resources/http_server_ui.go @@ -40,8 +40,8 @@ import ( "sync" "github.com/purpleidea/mgmt/engine" - "github.com/purpleidea/mgmt/engine/resources/http_ui/common" - "github.com/purpleidea/mgmt/engine/resources/http_ui/static" + "github.com/purpleidea/mgmt/engine/resources/http_server_ui/common" + "github.com/purpleidea/mgmt/engine/resources/http_server_ui/static" "github.com/purpleidea/mgmt/engine/traits" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/util/errwrap" @@ -50,30 +50,31 @@ import ( ) const ( - httpUIKind = httpKind + ":ui" + httpServerUIKind = httpServerKind + ":ui" - httpUIIndexHTMLTmpl = "index.html.tmpl" + httpServerUIIndexHTMLTmpl = "index.html.tmpl" ) var ( - //go:embed http_ui/index.html.tmpl - httpUIIndexHTMLTmplData string + //go:embed http_server_ui/index.html.tmpl + httpServerUIIndexHTMLTmplData string - //go:embed http_ui/wasm_exec.js - httpUIWasmExecData []byte + //go:embed http_server_ui/wasm_exec.js + httpServerUIWasmExecData []byte - //go:embed http_ui/main.wasm - httpUIMainWasmData []byte + //go:embed http_server_ui/main.wasm + httpServerUIMainWasmData []byte ) func init() { - engine.RegisterResource(httpUIKind, func() engine.Res { return &HTTPUIRes{} }) + engine.RegisterResource(httpServerUIKind, func() engine.Res { return &HTTPServerUIRes{} }) } // HTTPServerUIGroupableRes is the interface that you must implement if you want // to allow a resource the ability to be grouped into the http server ui -// resource. As an added safety, the Kind must also begin with "http:ui:", and -// not have more than one colon to avoid accidents of unwanted grouping. +// resource. As an added safety, the Kind must also begin with +// "http:server:ui:", and not have more than one colon to avoid accidents of +// unwanted grouping. type HTTPServerUIGroupableRes interface { engine.Res @@ -109,8 +110,9 @@ type HTTPServerUIGroupableRes interface { GetSort() string } -// HTTPUIResData represents some additional data to attach to the resource. -type HTTPUIResData struct { +// HTTPServerUIResData represents some additional data to attach to the +// resource. +type HTTPServerUIResData struct { // Title is the generated page title that is displayed to the user. Title string `lang:"title" yaml:"title"` @@ -121,12 +123,13 @@ type HTTPUIResData struct { Head string `lang:"head" yaml:"head"` } -// HTTPUIRes is a web UI resource that exists within an http server. The name is -// used as the public path of the ui, unless the path field is specified, and in -// that case it is used instead. The way this works is that it autogroups at -// runtime with an existing http server resource, and in doing so makes the form -// associated with this resource available for serving from that http server. -type HTTPUIRes struct { +// HTTPServerUIRes is a web UI resource that exists within an http server. The +// name is used as the public path of the ui, unless the path field is +// specified, and in that case it is used instead. The way this works is that it +// autogroups at runtime with an existing http server resource, and in doing so +// makes the form associated with this resource available for serving from that +// http server. +type HTTPServerUIRes struct { traits.Base // add the base methods without re-implementation traits.Edgeable // XXX: add autoedge support traits.Groupable // can be grouped into HTTPServerRes @@ -146,7 +149,7 @@ type HTTPUIRes struct { Path string `lang:"path" yaml:"path"` // Data represents some additional data to attach to the resource. - Data *HTTPUIResData `lang:"data" yaml:"data"` + Data *HTTPServerUIResData `lang:"data" yaml:"data"` //eventStream chan error eventsChanMap map[engine.Res]chan error @@ -162,15 +165,15 @@ type HTTPUIRes struct { } // Default returns some sensible defaults for this resource. -func (obj *HTTPUIRes) Default() engine.Res { - return &HTTPUIRes{} +func (obj *HTTPServerUIRes) Default() engine.Res { + return &HTTPServerUIRes{} } // getPath returns the actual path we respond to. When Path is not specified, we // use the Name. Note that this is the handler path that will be seen on the // root http server, and this ui application might use a querystring and/or POST // data as well. -func (obj *HTTPUIRes) getPath() string { +func (obj *HTTPServerUIRes) getPath() string { if obj.Path != "" { return obj.Path } @@ -179,7 +182,7 @@ func (obj *HTTPUIRes) getPath() string { // routerPath returns an appropriate path for our router based on what we want // to achieve using our parent prefix. -func (obj *HTTPUIRes) routerPath(p string) string { +func (obj *HTTPServerUIRes) routerPath(p string) string { if strings.HasPrefix(p, "/") { return obj.getPath() + p[1:] } @@ -190,13 +193,13 @@ func (obj *HTTPUIRes) routerPath(p string) string { // 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 *HTTPUIRes) ParentName() string { +func (obj *HTTPServerUIRes) 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 *HTTPUIRes) AcceptHTTP(req *http.Request) error { +func (obj *HTTPServerUIRes) 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") @@ -209,7 +212,7 @@ func (obj *HTTPUIRes) AcceptHTTP(req *http.Request) error { // getResByID returns the grouped resource with the id we're searching for if it // exists, otherwise nil and false. -func (obj *HTTPUIRes) getResByID(id string) (HTTPServerUIGroupableRes, bool) { +func (obj *HTTPServerUIRes) getResByID(id string) (HTTPServerUIGroupableRes, bool) { for _, x := range obj.GetGroup() { // grouped elements res, ok := x.(HTTPServerUIGroupableRes) // convert from Res if !ok { @@ -227,7 +230,7 @@ func (obj *HTTPUIRes) getResByID(id string) (HTTPServerUIGroupableRes, bool) { } // ginLogger is a helper to get structured logs out of gin. -func (obj *HTTPUIRes) ginLogger() gin.HandlerFunc { +func (obj *HTTPServerUIRes) ginLogger() gin.HandlerFunc { return func(c *gin.Context) { //start := time.Now() c.Next() @@ -248,11 +251,11 @@ func (obj *HTTPUIRes) ginLogger() gin.HandlerFunc { // getTemplate builds the super template that contains the map of each file name // so that it can be used easily to send out named, templated documents. -func (obj *HTTPUIRes) getTemplate() (*template.Template, error) { +func (obj *HTTPServerUIRes) getTemplate() (*template.Template, error) { // XXX: get this from somewhere m := make(map[string]string) //m["foo.tmpl"] = "hello from file1" // TODO: add more content? - m[httpUIIndexHTMLTmpl] = httpUIIndexHTMLTmplData // index.html.tmpl + m[httpServerUIIndexHTMLTmpl] = httpServerUIIndexHTMLTmplData // index.html.tmpl filenames := []string{} for filename := range m { @@ -283,7 +286,7 @@ func (obj *HTTPUIRes) getTemplate() (*template.Template, error) { } // ServeHTTP is the standard HTTP handler that will be used here. -func (obj *HTTPUIRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { +func (obj *HTTPServerUIRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { // XXX: do all the router bits in Init() if we can... gin.SetMode(gin.ReleaseMode) // for production @@ -306,32 +309,32 @@ func (obj *HTTPUIRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { h["program"] = obj.init.Program h["version"] = obj.init.Version h["hostname"] = obj.init.Hostname - h["embedded"] = static.HTTPUIStaticEmbedded // true or false - h["title"] = "" // key must be specified + h["embedded"] = static.HTTPServerUIStaticEmbedded // true or false + h["title"] = "" // key must be specified h["path"] = obj.getPath() if obj.Data != nil { h["title"] = obj.Data.Title // template var h["head"] = template.HTML(obj.Data.Head) } - c.HTML(http.StatusOK, httpUIIndexHTMLTmpl, h) + c.HTML(http.StatusOK, httpServerUIIndexHTMLTmpl, h) }) router.GET(obj.routerPath("/main.wasm"), func(c *gin.Context) { - c.Data(http.StatusOK, "application/wasm", httpUIMainWasmData) + c.Data(http.StatusOK, "application/wasm", httpServerUIMainWasmData) }) router.GET(obj.routerPath("/wasm_exec.js"), func(c *gin.Context) { // the version of this file has to match compiler version // the original came from: ~golang/lib/wasm/wasm_exec.js // XXX: add a test to ensure this matches the compiler version // the content-type matters or this won't work in the browser - c.Data(http.StatusOK, "text/javascript;charset=UTF-8", httpUIWasmExecData) + c.Data(http.StatusOK, "text/javascript;charset=UTF-8", httpServerUIWasmExecData) }) - if static.HTTPUIStaticEmbedded { - router.GET(obj.routerPath("/"+static.HTTPUIIndexBootstrapCSS), func(c *gin.Context) { - c.Data(http.StatusOK, "text/css;charset=UTF-8", static.HTTPUIIndexStaticBootstrapCSS) + if static.HTTPServerUIStaticEmbedded { + router.GET(obj.routerPath("/"+static.HTTPServerUIIndexBootstrapCSS), func(c *gin.Context) { + c.Data(http.StatusOK, "text/css;charset=UTF-8", static.HTTPServerUIIndexStaticBootstrapCSS) }) - router.GET(obj.routerPath("/"+static.HTTPUIIndexBootstrapJS), func(c *gin.Context) { - c.Data(http.StatusOK, "text/javascript;charset=UTF-8", static.HTTPUIIndexStaticBootstrapJS) + router.GET(obj.routerPath("/"+static.HTTPServerUIIndexBootstrapJS), func(c *gin.Context) { + c.Data(http.StatusOK, "text/javascript;charset=UTF-8", static.HTTPServerUIIndexStaticBootstrapJS) }) } @@ -492,7 +495,7 @@ func (obj *HTTPUIRes) ServeHTTP(w http.ResponseWriter, req *http.Request) { } // Validate checks if the resource data structure was populated correctly. -func (obj *HTTPUIRes) Validate() error { +func (obj *HTTPServerUIRes) Validate() error { if obj.getPath() == "" { return fmt.Errorf("empty path") } @@ -510,7 +513,7 @@ func (obj *HTTPUIRes) Validate() error { } // Init runs some startup code for this resource. -func (obj *HTTPUIRes) Init(init *engine.Init) error { +func (obj *HTTPServerUIRes) Init(init *engine.Init) error { obj.init = init // save for later //obj.eventStream = make(chan error) @@ -572,7 +575,7 @@ func (obj *HTTPUIRes) Init(init *engine.Init) error { Recv: engine.GenerateRecvFunc(r), // unused FilteredGraph: func() (*pgraph.Graph, error) { - panic("FilteredGraph for HTTP:UI not implemented") + panic("FilteredGraph for HTTP:Server:UI not implemented") }, Local: obj.init.Local, @@ -594,14 +597,14 @@ func (obj *HTTPUIRes) Init(init *engine.Init) error { } // Cleanup is run by the engine to clean up after the resource is done. -func (obj *HTTPUIRes) Cleanup() error { +func (obj *HTTPServerUIRes) Cleanup() error { return nil } // Watch is the primary listener for this resource and it outputs events. This // particular one does absolutely nothing but block until we've received a done // signal. -func (obj *HTTPUIRes) Watch(ctx context.Context) error { +func (obj *HTTPServerUIRes) Watch(ctx context.Context) error { multiplexedChan := make(chan error) defer close(multiplexedChan) // closes after everyone below us is finished @@ -708,7 +711,7 @@ func (obj *HTTPUIRes) Watch(ctx context.Context) error { // CheckApply is responsible for the Send/Recv aspects of the autogrouped // resources. It recursively calls any autogrouped children. -func (obj *HTTPUIRes) CheckApply(ctx context.Context, apply bool) (bool, error) { +func (obj *HTTPServerUIRes) CheckApply(ctx context.Context, apply bool) (bool, error) { if obj.init.Debug { obj.init.Logf("CheckApply") } @@ -726,9 +729,9 @@ func (obj *HTTPUIRes) CheckApply(ctx context.Context, apply bool) (bool, error) } // Cmp compares two resources and returns an error if they are not equivalent. -func (obj *HTTPUIRes) Cmp(r engine.Res) error { - // we can only compare HTTPUIRes to others of the same resource kind - res, ok := r.(*HTTPUIRes) +func (obj *HTTPServerUIRes) Cmp(r engine.Res) error { + // we can only compare HTTPServerUIRes to others of the same resource kind + res, ok := r.(*HTTPServerUIRes) if !ok { return fmt.Errorf("res is not the same kind") } @@ -745,13 +748,13 @@ func (obj *HTTPUIRes) Cmp(r engine.Res) error { // UnmarshalYAML is the custom unmarshal handler for this struct. It is // primarily useful for setting the defaults. -func (obj *HTTPUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error { - type rawRes HTTPUIRes // indirection to avoid infinite recursion +func (obj *HTTPServerUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes HTTPServerUIRes // indirection to avoid infinite recursion - def := obj.Default() // get the default - res, ok := def.(*HTTPUIRes) // put in the right format + def := obj.Default() // get the default + res, ok := def.(*HTTPServerUIRes) // put in the right format if !ok { - return fmt.Errorf("could not convert to HTTPUIRes") + return fmt.Errorf("could not convert to HTTPServerUIRes") } raw := rawRes(*res) // convert; the defaults go here @@ -759,14 +762,14 @@ func (obj *HTTPUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } - *obj = HTTPUIRes(raw) // restore from indirection with type conversion! + *obj = HTTPServerUIRes(raw) // restore from indirection with type conversion! return nil } // GroupCmp returns whether two resources can be grouped together or not. Can // these two resources be merged, aka, does this resource support doing so? Will // resource allow itself to be grouped _into_ this obj? -func (obj *HTTPUIRes) GroupCmp(r engine.GroupableRes) error { +func (obj *HTTPServerUIRes) GroupCmp(r engine.GroupableRes) error { res, ok := r.(HTTPServerUIGroupableRes) // different from what we usually do! if !ok { return fmt.Errorf("resource is not the right kind") @@ -778,17 +781,17 @@ func (obj *HTTPUIRes) GroupCmp(r engine.GroupableRes) error { return fmt.Errorf("resource groups with a different parent name") } - p := httpUIKind + ":" + p := httpServerUIKind + ":" - // http:ui:foo is okay, but http:file is not + // http:server:ui:foo is okay, but http:server:file is not if !strings.HasPrefix(r.Kind(), p) { return fmt.Errorf("not one of our children") } - // http:ui:foo is okay, but http:ui:foo:bar is not + // http:server:ui:foo is okay, but http:server:ui:foo:bar is not s := strings.TrimPrefix(r.Kind(), p) if len(s) != len(r.Kind()) && strings.Count(s, ":") > 0 { // has prefix - return fmt.Errorf("maximum one resource after `%s` prefix", httpUIKind) + return fmt.Errorf("maximum one resource after `%s` prefix", httpServerUIKind) } return nil diff --git a/engine/resources/http_ui/.gitignore b/engine/resources/http_server_ui/.gitignore similarity index 100% rename from engine/resources/http_ui/.gitignore rename to engine/resources/http_server_ui/.gitignore diff --git a/engine/resources/http_ui/README.md b/engine/resources/http_server_ui/README.md similarity index 50% rename from engine/resources/http_ui/README.md rename to engine/resources/http_server_ui/README.md index 37aeb800..9b50a20d 100644 --- a/engine/resources/http_ui/README.md +++ b/engine/resources/http_server_ui/README.md @@ -1,6 +1,6 @@ -This directory contains the golang wasm source for the `http_ui` resource. It -gets built automatically when you run `make` from the main project root -directory. +This directory contains the golang wasm source for the `http_server_ui` +resource. It gets built automatically when you run `make` from the main project +root directory. After it gets built, the compiled artifact gets bundled into the main project binary via go embed. diff --git a/engine/resources/http_ui/common/common.go b/engine/resources/http_server_ui/common/common.go similarity index 78% rename from engine/resources/http_ui/common/common.go rename to engine/resources/http_server_ui/common/common.go index 87fdf475..7c34e256 100644 --- a/engine/resources/http_ui/common/common.go +++ b/engine/resources/http_server_ui/common/common.go @@ -28,28 +28,30 @@ // additional permission. // Package common contains some code that is shared between the wasm and the -// http:ui packages. +// http:server:ui packages. package common const ( - // HTTPUIInputType represents the field in the "Type" map that specifies + // HTTPServerUIInputType represents the field in the "Type" map that specifies // which input type we're using. - HTTPUIInputType = "type" + HTTPServerUIInputType = "type" - // HTTPUIInputTypeText is the representation of the html "text" type. - HTTPUIInputTypeText = "text" + // HTTPServerUIInputTypeText is the representation of the html "text" + // type. + HTTPServerUIInputTypeText = "text" - // HTTPUIInputTypeRange is the representation of the html "range" type. - HTTPUIInputTypeRange = "range" + // HTTPServerUIInputTypeRange is the representation of the html "range" + // type. + HTTPServerUIInputTypeRange = "range" - // HTTPUIInputTypeRangeMin is the html input "range" min field. - HTTPUIInputTypeRangeMin = "min" + // HTTPServerUIInputTypeRangeMin is the html input "range" min field. + HTTPServerUIInputTypeRangeMin = "min" - // HTTPUIInputTypeRangeMax is the html input "range" max field. - HTTPUIInputTypeRangeMax = "max" + // HTTPServerUIInputTypeRangeMax is the html input "range" max field. + HTTPServerUIInputTypeRangeMax = "max" - // HTTPUIInputTypeRangeStep is the html input "range" step field. - HTTPUIInputTypeRangeStep = "step" + // HTTPServerUIInputTypeRangeStep is the html input "range" step field. + HTTPServerUIInputTypeRangeStep = "step" ) // Form represents the entire form containing all the desired elements. diff --git a/engine/resources/http_ui/index.html.tmpl b/engine/resources/http_server_ui/index.html.tmpl similarity index 100% rename from engine/resources/http_ui/index.html.tmpl rename to engine/resources/http_server_ui/index.html.tmpl diff --git a/engine/resources/http_ui/main.go b/engine/resources/http_server_ui/main.go similarity index 95% rename from engine/resources/http_ui/main.go rename to engine/resources/http_server_ui/main.go index e97312b7..e0636c11 100644 --- a/engine/resources/http_ui/main.go +++ b/engine/resources/http_server_ui/main.go @@ -39,7 +39,7 @@ import ( "syscall/js" "time" - "github.com/purpleidea/mgmt/engine/resources/http_ui/common" + "github.com/purpleidea/mgmt/engine/resources/http_server_ui/common" "github.com/purpleidea/mgmt/util/errwrap" ) @@ -168,7 +168,7 @@ func (obj *Main) Run() error { } //fmt.Printf("%+v\n", element) // debug - inputType, exists := x.Type[common.HTTPUIInputType] // "text" or "range" ... + inputType, exists := x.Type[common.HTTPServerUIInputType] // "text" or "range" ... if !exists { fmt.Printf("Element has no input type: %+v\n", element) continue @@ -185,14 +185,14 @@ func (obj *Main) Run() error { //el.Call("setAttribute", "name", id) el.Set("type", inputType) - if inputType == common.HTTPUIInputTypeRange { - if val, exists := x.Type[common.HTTPUIInputTypeRangeMin]; exists { + if inputType == common.HTTPServerUIInputTypeRange { + if val, exists := x.Type[common.HTTPServerUIInputTypeRangeMin]; exists { el.Set("min", val) } - if val, exists := x.Type[common.HTTPUIInputTypeRangeMax]; exists { + if val, exists := x.Type[common.HTTPServerUIInputTypeRangeMax]; exists { el.Set("max", val) } - if val, exists := x.Type[common.HTTPUIInputTypeRangeStep]; exists { + if val, exists := x.Type[common.HTTPServerUIInputTypeRangeStep]; exists { el.Set("step", val) } } diff --git a/engine/resources/http_ui/static/.gitignore b/engine/resources/http_server_ui/static/.gitignore similarity index 100% rename from engine/resources/http_ui/static/.gitignore rename to engine/resources/http_server_ui/static/.gitignore diff --git a/engine/resources/http_ui/static/embed.go b/engine/resources/http_server_ui/static/embed.go similarity index 75% rename from engine/resources/http_ui/static/embed.go rename to engine/resources/http_server_ui/static/embed.go index 46af371d..d140aa03 100644 --- a/engine/resources/http_ui/static/embed.go +++ b/engine/resources/http_server_ui/static/embed.go @@ -27,7 +27,7 @@ // additional permission if he deems it necessary to achieve the goals of this // additional permission. -//go:build httpuistatic +//go:build httpserveruistatic package static @@ -36,16 +36,19 @@ import ( ) const ( - // HTTPUIStaticEmbedded specifies whether files have been embedded. - HTTPUIStaticEmbedded = true + // HTTPServerUIStaticEmbedded specifies whether files have been + // embedded. + HTTPServerUIStaticEmbedded = true ) var ( - // HTTPUIIndexStaticBootstrapCSS is the embedded data. It is embedded. - //go:embed http_ui/static/bootstrap.min.css - HTTPUIIndexStaticBootstrapCSS []byte + // HTTPServerUIIndexStaticBootstrapCSS is the embedded data. It is + // embedded. + //go:embed http_server_ui/static/bootstrap.min.css + HTTPServerUIIndexStaticBootstrapCSS []byte - // HTTPUIIndexStaticBootstrapJS is the embedded data. It is embedded. - //go:embed http_ui/static/bootstrap.bundle.min.js - HTTPUIIndexStaticBootstrapJS []byte + // HTTPServerUIIndexStaticBootstrapJS is the embedded data. It is + // embedded. + //go:embed http_server_ui/static/bootstrap.bundle.min.js + HTTPServerUIIndexStaticBootstrapJS []byte ) diff --git a/engine/resources/http_ui/static/noembed.go b/engine/resources/http_server_ui/static/noembed.go similarity index 79% rename from engine/resources/http_ui/static/noembed.go rename to engine/resources/http_server_ui/static/noembed.go index 6bc159fb..8dec079c 100644 --- a/engine/resources/http_ui/static/noembed.go +++ b/engine/resources/http_server_ui/static/noembed.go @@ -27,19 +27,22 @@ // additional permission if he deems it necessary to achieve the goals of this // additional permission. -//go:build !httpuistatic +//go:build !httpserveruistatic package static const ( - // HTTPUIStaticEmbedded specifies whether files have been embedded. - HTTPUIStaticEmbedded = false + // HTTPServerUIStaticEmbedded specifies whether files have been + // embedded. + HTTPServerUIStaticEmbedded = false ) var ( - // HTTPUIIndexStaticBootstrapCSS is the embedded data. It is empty here. - HTTPUIIndexStaticBootstrapCSS []byte + // HTTPServerUIIndexStaticBootstrapCSS is the embedded data. It is empty + // here. + HTTPServerUIIndexStaticBootstrapCSS []byte - // HTTPUIIndexStaticBootstrapJS is the embedded data. It is empty here. - HTTPUIIndexStaticBootstrapJS []byte + // HTTPServerUIIndexStaticBootstrapJS is the embedded data. It is empty + // here. + HTTPServerUIIndexStaticBootstrapJS []byte ) diff --git a/engine/resources/http_ui/static/static.go b/engine/resources/http_server_ui/static/static.go similarity index 81% rename from engine/resources/http_ui/static/static.go rename to engine/resources/http_server_ui/static/static.go index be654b68..5aff6400 100644 --- a/engine/resources/http_ui/static/static.go +++ b/engine/resources/http_server_ui/static/static.go @@ -32,11 +32,11 @@ package static const ( - // HTTPUIIndexBootstrapCSS is the path to the bootstrap css file when - // embedded, relative to the parent directory. - HTTPUIIndexBootstrapCSS = "static/bootstrap.min.css" + // HTTPServerUIIndexBootstrapCSS is the path to the bootstrap css file + // when embedded, relative to the parent directory. + HTTPServerUIIndexBootstrapCSS = "static/bootstrap.min.css" - // HTTPUIIndexBootstrapJS is the path to the bootstrap js file when - // embedded, relative to the parent directory. - HTTPUIIndexBootstrapJS = "static/bootstrap.bundle.min.js" + // HTTPServerUIIndexBootstrapJS is the path to the bootstrap js file + // when embedded, relative to the parent directory. + HTTPServerUIIndexBootstrapJS = "static/bootstrap.bundle.min.js" ) diff --git a/engine/resources/http_ui/wasm_exec.js b/engine/resources/http_server_ui/wasm_exec.js similarity index 100% rename from engine/resources/http_ui/wasm_exec.js rename to engine/resources/http_server_ui/wasm_exec.js diff --git a/engine/resources/http_ui_input.go b/engine/resources/http_server_ui_input.go similarity index 71% rename from engine/resources/http_ui_input.go rename to engine/resources/http_server_ui_input.go index 405158d1..8234099e 100644 --- a/engine/resources/http_ui_input.go +++ b/engine/resources/http_server_ui_input.go @@ -37,37 +37,37 @@ import ( "sync" "github.com/purpleidea/mgmt/engine" - "github.com/purpleidea/mgmt/engine/resources/http_ui/common" + "github.com/purpleidea/mgmt/engine/resources/http_server_ui/common" "github.com/purpleidea/mgmt/engine/traits" "github.com/purpleidea/mgmt/util/errwrap" ) const ( - httpUIInputKind = httpUIKind + ":input" + httpServerUIInputKind = httpServerUIKind + ":input" - httpUIInputStoreKey = "key" - httpUIInputStoreSchemeLocal = "local" - httpUIInputStoreSchemeWorld = "world" + httpServerUIInputStoreKey = "key" + httpServerUIInputStoreSchemeLocal = "local" + httpServerUIInputStoreSchemeWorld = "world" - httpUIInputTypeText = common.HTTPUIInputTypeText // "text" - httpUIInputTypeRange = common.HTTPUIInputTypeRange // "range" + httpServerUIInputTypeText = common.HTTPServerUIInputTypeText // "text" + httpServerUIInputTypeRange = common.HTTPServerUIInputTypeRange // "range" ) func init() { - engine.RegisterResource(httpUIInputKind, func() engine.Res { return &HTTPUIInputRes{} }) + engine.RegisterResource(httpServerUIInputKind, func() engine.Res { return &HTTPServerUIInputRes{} }) } -// HTTPUIInputRes is a form element that exists within a http:ui resource, which -// exists within an http server. The name is used as the unique id of the field, -// unless the id field is specified, and in that case it is used instead. The -// way this works is that it autogroups at runtime with an existing http:ui -// resource, and in doing so makes the form field associated with this resource -// available as part of that ui which is itself grouped and served from the http -// server resource. -type HTTPUIInputRes struct { +// HTTPServerUIInputRes is a form element that exists within a http:server:ui +// resource, which exists within an http server. The name is used as the unique +// id of the field, unless the id field is specified, and in that case it is +// used instead. The way this works is that it autogroups at runtime with an +// existing http:server:ui resource, and in doing so makes the form field +// associated with this resource available as part of that ui which is itself +// grouped and served from the http server resource. +type HTTPServerUIInputRes struct { traits.Base // add the base methods without re-implementation traits.Edgeable // XXX: add autoedge support - traits.Groupable // can be grouped into HTTPUIRes + traits.Groupable // can be grouped into HTTPServerUIRes traits.Sendable init *engine.Init @@ -119,14 +119,14 @@ type HTTPUIInputRes struct { } // Default returns some sensible defaults for this resource. -func (obj *HTTPUIInputRes) Default() engine.Res { - return &HTTPUIInputRes{ +func (obj *HTTPServerUIInputRes) Default() engine.Res { + return &HTTPServerUIInputRes{ Type: "text://", } } // Validate checks if the resource data structure was populated correctly. -func (obj *HTTPUIInputRes) Validate() error { +func (obj *HTTPServerUIInputRes) Validate() error { if obj.GetID() == "" { return fmt.Errorf("empty id") } @@ -149,7 +149,7 @@ func (obj *HTTPUIInputRes) Validate() error { } // Init runs some startup code for this resource. -func (obj *HTTPUIInputRes) Init(init *engine.Init) error { +func (obj *HTTPServerUIInputRes) Init(init *engine.Init) error { obj.init = init // save for later u, err := url.Parse(obj.Type) @@ -159,7 +159,7 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error { if u == nil { return fmt.Errorf("can't parse Type") } - if u.Scheme != httpUIInputTypeText && u.Scheme != httpUIInputTypeRange { + if u.Scheme != httpServerUIInputTypeText && u.Scheme != httpServerUIInputTypeRange { return fmt.Errorf("unknown scheme: %s", u.Scheme) } values, err := url.ParseQuery(u.RawQuery) @@ -177,7 +177,7 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error { if u == nil { return fmt.Errorf("can't parse Store") } - if u.Scheme != httpUIInputStoreSchemeLocal && u.Scheme != httpUIInputStoreSchemeWorld { + if u.Scheme != httpServerUIInputStoreSchemeLocal && u.Scheme != httpServerUIInputStoreSchemeWorld { return fmt.Errorf("unknown scheme: %s", u.Scheme) } values, err := url.ParseQuery(u.RawQuery) @@ -188,7 +188,7 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error { obj.scheme = u.Scheme // cache for later obj.key = obj.Name() // default - x, exists := values[httpUIInputStoreKey] + x, exists := values[httpServerUIInputStoreKey] if exists && len(x) > 0 && x[0] != "" { // ignore absent or broken keys obj.key = x[0] } @@ -203,13 +203,13 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error { } // Cleanup is run by the engine to clean up after the resource is done. -func (obj *HTTPUIInputRes) Cleanup() error { +func (obj *HTTPServerUIInputRes) Cleanup() error { return nil } // getKey returns the key to be used for this resource. If the Store field is // specified, it will use that parsed part, otherwise it uses the Name. -func (obj *HTTPUIInputRes) getKey() string { +func (obj *HTTPServerUIInputRes) getKey() string { if obj.Store != "" { return obj.key } @@ -220,20 +220,20 @@ func (obj *HTTPUIInputRes) getKey() string { // 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 *HTTPUIInputRes) ParentName() string { +func (obj *HTTPServerUIInputRes) ParentName() string { return obj.Path } // GetKind returns the kind of this resource. -func (obj *HTTPUIInputRes) GetKind() string { +func (obj *HTTPServerUIInputRes) GetKind() string { // NOTE: We don't *need* to return such a specific string, and "input" // would be enough, but we might as well use this because we have it. - return httpUIInputKind + return httpServerUIInputKind } // GetID returns the actual ID we respond to. When ID is not specified, we use // the Name. -func (obj *HTTPUIInputRes) GetID() string { +func (obj *HTTPServerUIInputRes) GetID() string { if obj.ID != "" { return obj.ID } @@ -242,7 +242,7 @@ func (obj *HTTPUIInputRes) GetID() string { // SetValue stores the new value field that was obtained from submitting the // form. This receives the raw, unsafe value that you must validate first. -func (obj *HTTPUIInputRes) SetValue(ctx context.Context, vs []string) error { +func (obj *HTTPServerUIInputRes) SetValue(ctx context.Context, vs []string) error { if len(vs) != 1 { return fmt.Errorf("unexpected length of %d", len(vs)) } @@ -259,7 +259,7 @@ func (obj *HTTPUIInputRes) SetValue(ctx context.Context, vs []string) error { } // setValue is the helper version where the caller must provide the mutex. -func (obj *HTTPUIInputRes) setValue(ctx context.Context, val string) error { +func (obj *HTTPServerUIInputRes) setValue(ctx context.Context, val string) error { obj.value = val select { @@ -270,7 +270,7 @@ func (obj *HTTPUIInputRes) setValue(ctx context.Context, val string) error { return nil } -func (obj *HTTPUIInputRes) checkValue(value string) error { +func (obj *HTTPServerUIInputRes) checkValue(value string) error { // XXX: validate based on obj.Type // XXX: validate what kind of values are allowed, probably no \n, etc... return nil @@ -278,7 +278,7 @@ func (obj *HTTPUIInputRes) checkValue(value string) error { // GetValue gets a string representation for the form value, that we'll use in // our html form. -func (obj *HTTPUIInputRes) GetValue(ctx context.Context) (string, error) { +func (obj *HTTPServerUIInputRes) GetValue(ctx context.Context) (string, error) { obj.mutex.Lock() defer obj.mutex.Unlock() @@ -297,36 +297,36 @@ func (obj *HTTPUIInputRes) GetValue(ctx context.Context) (string, error) { } // GetType returns a map that you can use to build the input field in the ui. -func (obj *HTTPUIInputRes) GetType() map[string]string { +func (obj *HTTPServerUIInputRes) GetType() map[string]string { m := make(map[string]string) - if obj.typeURL.Scheme == httpUIInputTypeRange { + if obj.typeURL.Scheme == httpServerUIInputTypeRange { m = obj.rangeGetType() } - m[common.HTTPUIInputType] = obj.typeURL.Scheme + m[common.HTTPServerUIInputType] = obj.typeURL.Scheme return m } -func (obj *HTTPUIInputRes) rangeGetType() map[string]string { +func (obj *HTTPServerUIInputRes) rangeGetType() map[string]string { m := make(map[string]string) base := 10 bits := 64 - if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeMin]; exists && len(sa) > 0 { + if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeMin]; exists && len(sa) > 0 { if x, err := strconv.ParseInt(sa[0], base, bits); err == nil { - m[common.HTTPUIInputTypeRangeMin] = strconv.FormatInt(x, base) + m[common.HTTPServerUIInputTypeRangeMin] = strconv.FormatInt(x, base) } } - if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeMax]; exists && len(sa) > 0 { + if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeMax]; exists && len(sa) > 0 { if x, err := strconv.ParseInt(sa[0], base, bits); err == nil { - m[common.HTTPUIInputTypeRangeMax] = strconv.FormatInt(x, base) + m[common.HTTPServerUIInputTypeRangeMax] = strconv.FormatInt(x, base) } } - if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeStep]; exists && len(sa) > 0 { + if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeStep]; exists && len(sa) > 0 { if x, err := strconv.ParseInt(sa[0], base, bits); err == nil { - m[common.HTTPUIInputTypeRangeStep] = strconv.FormatInt(x, base) + m[common.HTTPServerUIInputTypeRangeStep] = strconv.FormatInt(x, base) } } @@ -335,18 +335,18 @@ func (obj *HTTPUIInputRes) rangeGetType() map[string]string { // GetSort returns a string that you can use to determine the global sorted // display order of all the elements in a ui. -func (obj *HTTPUIInputRes) GetSort() string { +func (obj *HTTPServerUIInputRes) GetSort() string { return obj.Sort } // Watch is the primary listener for this resource and it outputs events. This // particular one does absolutely nothing but block until we've received a done // signal. -func (obj *HTTPUIInputRes) Watch(ctx context.Context) error { - if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal { +func (obj *HTTPServerUIInputRes) Watch(ctx context.Context) error { + if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal { return obj.localWatch(ctx) } - if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld { + if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld { return obj.worldWatch(ctx) } @@ -363,7 +363,7 @@ func (obj *HTTPUIInputRes) Watch(ctx context.Context) error { return nil } -func (obj *HTTPUIInputRes) localWatch(ctx context.Context) error { +func (obj *HTTPServerUIInputRes) localWatch(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -398,7 +398,7 @@ func (obj *HTTPUIInputRes) localWatch(ctx context.Context) error { } -func (obj *HTTPUIInputRes) worldWatch(ctx context.Context) error { +func (obj *HTTPServerUIInputRes) worldWatch(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -439,16 +439,16 @@ func (obj *HTTPUIInputRes) worldWatch(ctx context.Context) error { // CheckApply performs the send/recv portion of this autogrouped resources. That // can fail, but only if the send portion fails for some reason. If we're using // the Store feature, then it also reads and writes to and from that store. -func (obj *HTTPUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, error) { +func (obj *HTTPServerUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, error) { if obj.init.Debug { obj.init.Logf("CheckApply") } // If we're in ".Value" mode, we want to look at the incoming value, and // send it onwards. This function mostly exists as a stub in this case. - // The private value gets set by obj.SetValue from the http:ui parent. - // If we're in ".Store" mode, then we're reconciling between the "World" - // and the http:ui "Web". + // The private value gets set by obj.SetValue from the http:server:ui + // parent. If we're in ".Store" mode, then we're reconciling between the + // "World" and the http:server:ui "Web". if obj.Store != "" { return obj.storeCheckApply(ctx, apply) @@ -458,14 +458,14 @@ func (obj *HTTPUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, er } -func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (bool, error) { +func (obj *HTTPServerUIInputRes) valueCheckApply(ctx context.Context, apply bool) (bool, error) { obj.mutex.Lock() value := obj.value // gets set by obj.SetValue obj.mutex.Unlock() if obj.last != nil && *obj.last == value { - if err := obj.init.Send(&HTTPUIInputSends{ + if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &value, }); err != nil { return false, err @@ -474,7 +474,7 @@ func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (boo } if !apply { - if err := obj.init.Send(&HTTPUIInputSends{ + if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &value, // XXX: arbitrary since we're in noop mode }); err != nil { return false, err @@ -489,7 +489,7 @@ func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (boo obj.init.Logf("sending: %s", value) // send - if err := obj.init.Send(&HTTPUIInputSends{ + if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &value, }); err != nil { return false, err @@ -501,11 +501,11 @@ func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (boo // storeCheckApply is a tricky function where we attempt to reconcile the state // between a third-party changing the value in the World database, and a recent -// "http:ui" change by and end user. Basically whoever runs last is the "right" -// value that we want to use. We know who sent the event from reading the -// storeEvent variable, and if it was the World, we want to cache it locally, -// and if it was the Web, then we want to push it up to the store. -func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (bool, error) { +// "http:server:ui" change by an end user. Basically whoever runs last is the +// "right" value that we want to use. We know who sent the event from reading +// the storeEvent variable, and if it was the World, we want to cache it +// locally, and if it was the Web, then we want to push it up to the store. +func (obj *HTTPServerUIInputRes) storeCheckApply(ctx context.Context, apply bool) (bool, error) { v1, exists, err := obj.storeGet(ctx, obj.getKey()) if err != nil { @@ -519,7 +519,7 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo obj.mutex.Unlock() if exists && v1 == v2 { // both sides are happy - if err := obj.init.Send(&HTTPUIInputSends{ + if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &v2, }); err != nil { return false, err @@ -528,7 +528,7 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo } if !apply { - if err := obj.init.Send(&HTTPUIInputSends{ + if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &v2, // XXX: arbitrary since we're in noop mode }); err != nil { return false, err @@ -555,7 +555,7 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo obj.init.Logf("sending: %s", value) // send - if err := obj.init.Send(&HTTPUIInputSends{ + if err := obj.init.Send(&HTTPServerUIInputSends{ Value: &value, }); err != nil { return false, err @@ -564,8 +564,8 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo return false, nil } -func (obj *HTTPUIInputRes) storeGet(ctx context.Context, key string) (string, bool, error) { - if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal { +func (obj *HTTPServerUIInputRes) storeGet(ctx context.Context, key string) (string, bool, error) { + if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal { val, err := obj.init.Local.ValueGet(ctx, key) if err != nil { return "", false, err // real error @@ -581,7 +581,7 @@ func (obj *HTTPUIInputRes) storeGet(ctx context.Context, key string) (string, bo return s, true, nil } - if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld { + if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld { val, err := obj.init.World.StrGet(ctx, key) if err != nil && obj.init.World.StrIsNotExist(err) { return "", false, nil // val doesn't exist @@ -595,13 +595,13 @@ func (obj *HTTPUIInputRes) storeGet(ctx context.Context, key string) (string, bo return "", false, nil // something else } -func (obj *HTTPUIInputRes) storeSet(ctx context.Context, key, val string) error { +func (obj *HTTPServerUIInputRes) storeSet(ctx context.Context, key, val string) error { - if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal { + if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal { return obj.init.Local.ValueSet(ctx, key, val) } - if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld { + if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld { return obj.init.World.StrSet(ctx, key, val) } @@ -609,9 +609,9 @@ func (obj *HTTPUIInputRes) storeSet(ctx context.Context, key, val string) error } // Cmp compares two resources and returns an error if they are not equivalent. -func (obj *HTTPUIInputRes) Cmp(r engine.Res) error { - // we can only compare HTTPUIInputRes to others of the same resource kind - res, ok := r.(*HTTPUIInputRes) +func (obj *HTTPServerUIInputRes) Cmp(r engine.Res) error { + // we can only compare HTTPServerUIInputRes to others of the same resource kind + res, ok := r.(*HTTPServerUIInputRes) if !ok { return fmt.Errorf("res is not the same kind") } @@ -640,13 +640,13 @@ func (obj *HTTPUIInputRes) Cmp(r engine.Res) error { // UnmarshalYAML is the custom unmarshal handler for this struct. It is // primarily useful for setting the defaults. -func (obj *HTTPUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) error { - type rawRes HTTPUIInputRes // indirection to avoid infinite recursion +func (obj *HTTPServerUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes HTTPServerUIInputRes // indirection to avoid infinite recursion - def := obj.Default() // get the default - res, ok := def.(*HTTPUIInputRes) // put in the right format + def := obj.Default() // get the default + res, ok := def.(*HTTPServerUIInputRes) // put in the right format if !ok { - return fmt.Errorf("could not convert to HTTPUIInputRes") + return fmt.Errorf("could not convert to HTTPServerUIInputRes") } raw := rawRes(*res) // convert; the defaults go here @@ -654,20 +654,20 @@ func (obj *HTTPUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) erro return err } - *obj = HTTPUIInputRes(raw) // restore from indirection with type conversion! + *obj = HTTPServerUIInputRes(raw) // restore from indirection with type conversion! return nil } -// HTTPUIInputSends is the struct of data which is sent after a successful +// HTTPServerUIInputSends is the struct of data which is sent after a successful // Apply. -type HTTPUIInputSends struct { +type HTTPServerUIInputSends struct { // Value is the text element value being sent. Value *string `lang:"value"` } // Sends represents the default struct of values we can send using Send/Recv. -func (obj *HTTPUIInputRes) Sends() interface{} { - return &HTTPUIInputSends{ +func (obj *HTTPServerUIInputRes) Sends() interface{} { + return &HTTPServerUIInputSends{ Value: nil, } } diff --git a/examples/lang/http-flag0.mcl b/examples/lang/http-server-flag0.mcl similarity index 82% rename from examples/lang/http-flag0.mcl rename to examples/lang/http-server-flag0.mcl index 39507274..dc857890 100644 --- a/examples/lang/http-flag0.mcl +++ b/examples/lang/http-server-flag0.mcl @@ -6,12 +6,12 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing } # you can add a raw file like this... -http:file "/file1" { +http:server:file "/file1" { data => "hello, world, i'm file1 and i don't exist on disk!\n", } # wget --post-data 'key=hello&whatever=bye' -O - http://127.0.0.1:8080/flag1 -http:flag "/flag1" { +http:server:flag "/flag1" { #server => ":8080", key => "key", } @@ -22,8 +22,8 @@ print "print1" { Meta:autogroup => false, } -Http:Flag["/flag1"].value -> Print["print1"].msg -Http:Flag["/flag1"].value -> Value["value1"].any +Http:Server:Flag["/flag1"].value -> Print["print1"].msg +Http:Server:Flag["/flag1"].value -> Value["value1"].any $ret = value.get_str("value1") # name of value resource $val = $ret->value diff --git a/examples/lang/http-proxy0.mcl b/examples/lang/http-server-proxy0.mcl similarity index 84% rename from examples/lang/http-proxy0.mcl rename to examples/lang/http-server-proxy0.mcl index 9a719704..9b3b27f9 100644 --- a/examples/lang/http-proxy0.mcl +++ b/examples/lang/http-server-proxy0.mcl @@ -4,7 +4,7 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing } # you can add a raw file like this... -http:file "/file1" { +http:server:file "/file1" { data => "hello, world, i'm file1 and i don't exist on disk!\n", } @@ -17,7 +17,7 @@ file "${distroarch_http_prefix}" { # root http dir } # this one is backed by the (optional) rsync -#http:file "/fedora/releases/${version}/Everything/${arch}/os/" { +#http:server:file "/fedora/releases/${version}/Everything/${arch}/os/" { # path => "${distroarch_http_prefix}", #} @@ -25,7 +25,7 @@ file "${distroarch_http_prefix}" { # root http dir # wget http://127.0.0.1:8080/fedora/releases/38/Everything/x86_64/os/Packages/c/cowsay-3.7.0-7.fc38.noarch.rpm # wget https://mirrors.xtom.de/fedora/releases/38/Everything/x86_64/os/Packages/c/cowsay-3.7.0-7.fc38.noarch.rpm -http:proxy "/fedora/releases/${version}/Everything/${arch}/os/" { # same as the http:file path +http:server:proxy "/fedora/releases/${version}/Everything/${arch}/os/" { # same as the http:server:file path cache => "${distroarch_http_prefix}", # /tmp/os/ #force => false, # if true, overwrite or change from dir->file if needed diff --git a/examples/lang/http-ui-kv.mcl b/examples/lang/http-server-ui-kv.mcl similarity index 81% rename from examples/lang/http-ui-kv.mcl rename to examples/lang/http-server-ui-kv.mcl index cdad26b2..b40c3e29 100644 --- a/examples/lang/http-ui-kv.mcl +++ b/examples/lang/http-server-ui-kv.mcl @@ -5,11 +5,11 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing } # you can add a raw file like this... -http:file "/file1" { +http:server:file "/file1" { data => "hello, world, i'm file1 and i don't exist on disk!\n", } -http:ui "/ui/" { +http:server:ui "/ui/" { #path => "/ui/", # we can override the name like this if needed data => struct{ @@ -20,18 +20,18 @@ http:ui "/ui/" { $text1_id = "text1" $range1_id = "range1" -http:ui:input $text1_id { +http:server:ui:input $text1_id { store => "world://", sort => "a", } -http:ui:input $range1_id { +http:server:ui:input $range1_id { store => "world://", type => "range://?min=0&max=5&step=1", sort => "b", } -#Http:Ui:Input[$text1_id].value -> Kv[$text1_id].value +#Http:Server:Ui:Input[$text1_id].value -> Kv[$text1_id].value #kv $text1_id { # store in world #} diff --git a/examples/lang/http-ui-value.mcl b/examples/lang/http-server-ui-value.mcl similarity index 88% rename from examples/lang/http-ui-value.mcl rename to examples/lang/http-server-ui-value.mcl index 23688325..c79efa8d 100644 --- a/examples/lang/http-ui-value.mcl +++ b/examples/lang/http-server-ui-value.mcl @@ -5,11 +5,11 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing } # you can add a raw file like this... -http:file "/file1" { +http:server:file "/file1" { data => "hello, world, i'm file1 and i don't exist on disk!\n", } -http:ui "/ui/" { +http:server:ui "/ui/" { #path => "/ui/", # we can override the name like this if needed data => struct{ @@ -30,17 +30,17 @@ $range1_val = if $ret2->ready { } else { "2" # some default } -http:ui:input $text1_id { +http:server:ui:input $text1_id { value => $text1_val, # it passes back into itself! } -http:ui:input $range1_id { +http:server:ui:input $range1_id { value => $range1_val, type => "range://?min=0&max=5&step=1", sort => "b", } -Http:Ui:Input[$text1_id].value -> Value[$text1_id].any +Http:Server:Ui:Input[$text1_id].value -> Value[$text1_id].any value $text1_id { any => "whatever", # TODO: remove the temporary placeholder here diff --git a/examples/lang/http0.mcl b/examples/lang/http-server0.mcl similarity index 88% rename from examples/lang/http0.mcl rename to examples/lang/http-server0.mcl index 86c8dd18..4f478f02 100644 --- a/examples/lang/http0.mcl +++ b/examples/lang/http-server0.mcl @@ -14,7 +14,7 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing } # you can add a raw file like this... -http:file "/file1" { +http:server:file "/file1" { data => "hello, world, i'm file1 and i don't exist on disk!\n", } @@ -26,19 +26,19 @@ file $f2 { } # you can point to it directly... -http:file "/file2" { +http:server:file "/file2" { path => $f2, Depend => File[$f2], # TODO: add autoedges } # here's a file in the middle of nowhere that still works... -http:file "/i/am/some/deeply/nested/file" { +http:server:file "/i/am/some/deeply/nested/file" { data => "how did you find me!\n", } # and this file won't autogroup with the main http server -http:file "/nope/noway" { +http:server:file "/nope/noway" { data => "i won't be seen!\n", server => "someone else!", # normally we don't use this this way } diff --git a/examples/lang/http1.mcl b/examples/lang/http-server1.mcl similarity index 94% rename from examples/lang/http1.mcl rename to examples/lang/http-server1.mcl index 45096af9..3331ce2c 100644 --- a/examples/lang/http1.mcl +++ b/examples/lang/http-server1.mcl @@ -34,12 +34,12 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing } # you can add a raw file like this... -http:file "/file1" { +http:server:file "/file1" { data => "hello, world, i'm file1 and i don't exist on disk!\n", } # this pulls in a whole folder, since path is a folder! -http:file "/secret/folder/" { +http:server:file "/secret/folder/" { path => "${root}", Depend => File["${root}"], # TODO: add autoedges diff --git a/lang/core/embedded/provisioner/main.mcl b/lang/core/embedded/provisioner/main.mcl index 1c52204a..ca267c94 100644 --- a/lang/core/embedded/provisioner/main.mcl +++ b/lang/core/embedded/provisioner/main.mcl @@ -211,7 +211,7 @@ class base($config) { $vardir = local.vardir("provisioner/") $binary_path = deploy.binary_path() - http:file "/mgmt/binary" { # TODO: support different architectures + http:server:file "/mgmt/binary" { # TODO: support different architectures path => $binary_path, # TODO: As long as binary doesn't contain private data! Before => Print["ready"], @@ -251,7 +251,7 @@ class base($config) { gzip "${abs_gz}" { input => "${abs_tar}", } - http:file "/mgmt/deploy.tar.gz" { + http:server:file "/mgmt/deploy.tar.gz" { path => "${abs_gz}", } @@ -406,7 +406,7 @@ class base:repo($config) { #Depend => Pkg[$pkgs], } - http:file "/${uid}/vmlinuz" { # when using ipxe + http:server:file "/${uid}/vmlinuz" { # when using ipxe path => $vmlinuz_file, # TODO: add autoedges #Depend => Pkg[$pkgs], @@ -433,7 +433,7 @@ class base:repo($config) { #Depend => Pkg[$pkgs], } - http:file "/${uid}/initrd.img" { # when using ipxe + http:server:file "/${uid}/initrd.img" { # when using ipxe path => $initrd_file, # TODO: add autoedges #Depend => Pkg[$pkgs], @@ -441,15 +441,15 @@ class base:repo($config) { # this file resource serves the entire rsync directory over http if $mirror == "" { # and $rsync != "" - http:file "/fedora/releases/${version}/Everything/${arch}/os/" { + http:server:file "/fedora/releases/${version}/Everything/${arch}/os/" { path => $distroarch_release_http_prefix, } - http:file "/fedora/updates/${version}/Everything/${arch}/" { + http:server:file "/fedora/updates/${version}/Everything/${arch}/" { path => $distroarch_updates_http_prefix, } } else { - # same as the above http:file path would have been - http:proxy "/fedora/releases/${version}/Everything/${arch}/os/" { + # same as the above http:server:file path would have been + http:server:proxy "/fedora/releases/${version}/Everything/${arch}/os/" { sub => "/fedora/", # we remove this from the name! head => $mirror, @@ -457,7 +457,7 @@ class base:repo($config) { } # XXX: if we had both of these in the same http_prefix, we could overlap them with an rsync :/ hmm... - http:proxy "/fedora/updates/${version}/Everything/${arch}/" { # no os/ dir at the end + http:server:proxy "/fedora/updates/${version}/Everything/${arch}/" { # no os/ dir at the end sub => "/fedora/", # we remove this from the name! head => $mirror, @@ -486,10 +486,10 @@ class base:repo($config) { # # baseurl => "http://${router_ip}:${http_port_str}/fedora/updates/${version}/Everything/${arch}/", #} - #http:file "/fedora/${uid}/fedora.repo" { + #http:server:file "/fedora/${uid}/fedora.repo" { # data => golang.template(deploy.readfile("/files/repo.tmpl"), $fedora_repo_template), #} - #http:file "/fedora/${uid}/updates.repo" { + #http:server:file "/fedora/${uid}/updates.repo" { # data => golang.template(deploy.readfile("/files/repo.tmpl"), $updates_repo_template), #} @@ -699,7 +699,7 @@ class base:host($name, $config) { } } - http:file "/${ipxe_menu}" { # for ipxe + http:server:file "/${ipxe_menu}" { # for ipxe data => golang.template(deploy.readfile("/files/ipxe-menu.tmpl"), $menu_template), } @@ -740,7 +740,7 @@ class base:host($name, $config) { gzip "${abs_gz}" { input => "${abs_tar}", } - http:file "/mgmt/deploy-${provision_key}.tar.gz" { + http:server:file "/mgmt/deploy-${provision_key}.tar.gz" { path => "${abs_gz}", } } @@ -797,7 +797,7 @@ class base:host($name, $config) { "echo '#!/usr/bin/env bash' > ${firstboot_scripts_dir}mgmt-deploy.sh && echo '${handoff_binary_path} deploy lang --seeds=http://127.0.0.1:2379 --no-git --module-path=${deploy_dir_modules} ${deploy_dir}${handoff_code_chunk}' >> ${firstboot_scripts_dir}mgmt-deploy.sh && chmod u+x ${firstboot_scripts_dir}mgmt-deploy.sh" } - # TODO: Do we want to signal an http:flag if we're a "default" host? + # TODO: Do we want to signal an http:server:flag if we're a "default" host? $provisioning_done = if $provision_key == "default" { "" } else { @@ -851,7 +851,7 @@ class base:host($name, $config) { content => golang.template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template), } - http:file "/fedora/kickstart/${hkey}.ks" { # usually $mac or `default` + http:server:file "/fedora/kickstart/${hkey}.ks" { # usually $mac or `default` #data => golang.template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template), path => $kickstart_file, @@ -871,7 +871,7 @@ class base:host($name, $config) { ##$str_true = convert.format_bool(true) ##$str_false = convert.format_bool(false) - #http:flag "${name}" { + #http:server:flag "${name}" { # key => "done", # path => "/action/done/mac=${provision_key}", # #mapped => {$str_true => $str_true, $str_false => $str_false,}, diff --git a/lang/interpret_test/TestAstFunc2/send-recv-3.txtar b/lang/interpret_test/TestAstFunc2/send-recv-3.txtar index e98bbcfe..c3e52c80 100644 --- a/lang/interpret_test/TestAstFunc2/send-recv-3.txtar +++ b/lang/interpret_test/TestAstFunc2/send-recv-3.txtar @@ -1,11 +1,11 @@ -- main.mcl -- -http:ui:input "text1" {} +http:server:ui:input "text1" {} -Http:Ui:Input["text1"].value -> Kv["kv1"].value +Http:Server:Ui:Input["text1"].value -> Kv["kv1"].value kv "kv1" {} -- OUTPUT -- -Edge: http:ui:input[text1] -> kv[kv1] # http:ui:input[text1] -> kv[kv1] -Vertex: http:ui:input[text1] +Edge: http:server:ui:input[text1] -> kv[kv1] # http:server:ui:input[text1] -> kv[kv1] +Vertex: http:server:ui:input[text1] Vertex: kv[kv1] diff --git a/misc/vim/mcl.vim b/misc/vim/mcl.vim index ce642f5e..8e98071e 100644 --- a/misc/vim/mcl.vim +++ b/misc/vim/mcl.vim @@ -23,7 +23,7 @@ hi def link mclComment Comment syn keyword mclResources augeas aws:ec2 bmc:power config:etcd consul:kv cron deploy:tar dhcp:host syn keyword mclResources dhcp:range dhcp:server docker:container docker:image exec file -syn keyword mclResources firewalld group gzip hetzner:vm hostname http:file http:flag http:proxy +syn keyword mclResources firewalld group gzip hetzner:vm hostname http:server:file http:server:flag http:server:proxy syn keyword mclResources http:server kv mount msg net noop nspawn password pippet pkg print svc syn keyword mclResources sysctl tar test tftp:file tftp:server timer user value virt virt:builder diff --git a/test/test-gotest.sh b/test/test-gotest.sh index daa896f1..2237e020 100755 --- a/test/test-gotest.sh +++ b/test/test-gotest.sh @@ -50,7 +50,7 @@ else for pkg in `go list -e ./... | grep -v "^${base}/vendor/" | grep -v "^${base}/examples/" | grep -v "^${base}/test/" | grep -v "^${base}/old" | grep -v "^${base}/old/" | grep -v "^${base}/tmp" | grep -v "^${base}/tmp/" | grep -v "^${base}/integration"`; do echo -e "\ttesting: $pkg" - if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_ui" ]; then + if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_server_ui" ]; then continue # skip this special main package fi diff --git a/test/test-govet.sh b/test/test-govet.sh index 5693cccb..d7d810bb 100755 --- a/test/test-govet.sh +++ b/test/test-govet.sh @@ -130,7 +130,7 @@ function reflowed-comments() { base=$(go list .) for pkg in `go list -e ./... | grep -v "^${base}/vendor/" | grep -v "^${base}/examples/" | grep -v "^${base}/test/" | grep -v "^${base}/old" | grep -v "^${base}/old/" | grep -v "^${base}/tmp" | grep -v "^${base}/tmp/"`; do - if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_ui" ]; then + if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_server_ui" ]; then continue # skip this special main package fi