package main import ( "errors" "fmt" "log" "net" "net/http" "strings" "text/template" cfsslconfig "github.com/cloudflare/cfssl/config" "github.com/emicklei/go-restful" "m.cluseau.fr/go/httperr" "novit.tech/direktil/pkg/localconfig" "novit.tech/direktil/local-server/pkg/mime" ) func registerWS(rest *restful.Container) { // public-level APIs { ws := &restful.WebService{} ws. Path("/public"). Produces(mime.JSON). Consumes(mime.JSON). Route(ws.POST("/unlock-store").To(wsUnlockStore). Reads(NamedPassphrase{}). Writes(""). Doc("Try to unlock the store")). Route(ws.GET("/store.tar").To(wsStoreDownload). Produces(mime.TAR). Param(ws.QueryParameter("token", "the download token")). Doc("Fetch the encrypted store")). Route(ws.POST("/store.tar").To(wsStoreUpload). Consumes(mime.TAR). Doc("Upload an existing store")). Route(ws.GET("/downloads/{token}/{asset}").To(wsDownload). Param(ws.PathParameter("token", "the download token")). Param(ws.PathParameter("asset", "the requested asset")). Doc("Fetch an asset via a download token")) rest.Add(ws) } // Admin-level APIs ws := (&restful.WebService{}). Filter(requireSecStore). Filter(adminAuth). Param(restful.HeaderParameter("Authorization", "Admin bearer token").Required(true)). Produces(mime.JSON) // - store management ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey). Consumes(mime.JSON).Reads(NamedPassphrase{}). Doc("Add an unlock key to the store")) ws.Route(ws.POST("/store/delete-key").To(wsStoreDelKey). Consumes(mime.JSON).Reads(""). Doc("Remove an unlock key to the store (by its name)")) // - downloads ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload). Consumes(mime.JSON).Reads(DownloadSpec{}). Produces(mime.JSON). Doc("Create a download token for the given download")) // - configs API ws.Route(ws.POST("/configs").To(wsUploadConfig). Consumes(mime.YAML).Param(ws.BodyParameter("config", "The new full configuration")). Produces(mime.JSON).Writes(true). Doc("Upload a new current configuration, archiving the previous one")) // - clusters API ws.Route(ws.GET("/clusters").To(wsListClusters). Doc("List clusters")) ws.Route(ws.GET("/hosts-from-template").To(wsHostsFromTemplateList). Doc("List host template instances")) ws.Route(ws.POST("/hosts-from-template/{name}").To(wsHostsFromTemplateSet). Reads(HostFromTemplate{}). Doc("Create or update a host template instance")) ws.Route(ws.DELETE("/hosts-from-template/{name}").To(wsHostsFromTemplateDelete). Reads(HostFromTemplate{}). Doc("Delete a host template instance")) const ( GET = http.MethodGet PUT = http.MethodPut ) cluster := func(method, subPath string) *restful.RouteBuilder { return ws.Method(method).Path("/clusters/{cluster-name}" + subPath). Param(ws.PathParameter("cluster-name", "name of the cluster")) } for _, builder := range []*restful.RouteBuilder{ cluster(GET, "").To(wsCluster). Doc("Get cluster details"), cluster(GET, "/addons").To(wsClusterAddons). Produces(mime.YAML). Doc("Get cluster addons"). Returns(http.StatusOK, "OK", nil). Returns(http.StatusNotFound, "The cluster does not exists or does not have addons defined", nil), cluster(GET, "/tokens").To(wsClusterTokens). Doc("List cluster's tokens"), cluster(GET, "/tokens/{token-name}").To(wsClusterToken). Doc("Get cluster's token"), cluster(GET, "/passwords").To(wsClusterPasswords). Doc("List cluster's passwords"), cluster(GET, "/passwords/{password-name}").To(wsClusterPassword). Doc("Get cluster's password"), cluster(PUT, "/passwords/{password-name}").To(wsClusterSetPassword). Doc("Set cluster's password"), cluster(GET, "/CAs").To(wsClusterCAs). Doc("Get cluster CAs"), cluster(GET, "/CAs/{ca-name}/certificate").To(wsClusterCACert). Produces(mime.CACERT). Doc("Get cluster CA's certificate"), cluster(GET, "/CAs/{ca-name}/signed").To(wsClusterSignedCert). Produces(mime.CERT). Param(ws.QueryParameter("name", "signed reference name").Required(true)). Doc("Get cluster's certificate signed by the CA"), } { ws.Route(builder) } ws.Route(ws.GET("/hosts").To(wsListHosts). Doc("List hosts")) ws.Route(ws.GET("/ssh-acls").To(wsSSH_ACL_List)) ws.Route(ws.GET("/ssh-acls/{acl-name}").To(wsSSH_ACL_Get)) ws.Route(ws.PUT("/ssh-acls/{acl-name}").To(wsSSH_ACL_Set)) rest.Add(ws) // Hosts API ws = (&restful.WebService{}). Filter(requireSecStore). Filter(adminAuth). Path("/hosts/{host-name}"). Param(ws.HeaderParameter("Authorization", "Host or admin bearer token")) (&wsHost{ hostDoc: "given host", getHost: func(req *restful.Request) (string, error) { return req.PathParameter("host-name"), nil }, }).register(ws, func(rb *restful.RouteBuilder) { rb.Param(ws.PathParameter("host-name", "host's name")) }) rest.Add(ws) // Detected host API ws = (&restful.WebService{}). Filter(requireSecStore). Path("/me"). Param(ws.HeaderParameter("Authorization", "Host or admin bearer token")) (&wsHost{ hostDoc: "detected host", getHost: detectHost, }).register(ws, func(rb *restful.RouteBuilder) { rb.Notes("In this case, the host is detected from the remote IP") }) // Hosts by token API ws = (&restful.WebService{}). Filter(requireSecStore). Path("/hosts-by-token/{host-token}"). Param(ws.PathParameter("host-token", "host's download token")) (&wsHost{ hostDoc: "token's host", getHost: func(req *restful.Request) (host string, err error) { reqToken := req.PathParameter("host-token") data, err := hostDownloadTokens.Data() if err != nil { return } for h, token := range data { if token == reqToken { host = h return } } return }, }).register(ws, func(rb *restful.RouteBuilder) { rb.Notes("In this case, the host is detected from the token") }) rest.Add(ws) } func requireSecStore(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { if !secStore.Unlocked() { wsError(resp, ErrStoreLocked) return } chain.ProcessFilter(req, resp) } func detectHost(req *restful.Request) (hostName string, err error) { if !*allowDetectedHost { return } r := req.Request remoteAddr := r.RemoteAddr if *trustXFF { if xff := r.Header.Get("X-Forwarded-For"); xff != "" { remoteAddr = strings.Split(xff, ",")[0] } } hostIP, _, err := net.SplitHostPort(remoteAddr) if err != nil { hostIP = remoteAddr } cfg, err := readConfig() if err != nil { return } host := cfg.HostByIP(hostIP) if host == nil { log.Print("no host found for IP ", hostIP) return } return host.Name, nil } func wsReadConfig(resp *restful.Response) *localconfig.Config { cfg, err := readConfig() if err != nil { log.Print("failed to read config: ", err) resp.WriteErrorString(http.StatusServiceUnavailable, "failed to read config") return nil } return cfg } func wsNotFound(resp *restful.Response) { wsError(resp, ErrNotFound) } func wsBadRequest(resp *restful.Response, err string) { httperr.New(http.StatusBadRequest, errors.New(err)).WriteJSON(resp.ResponseWriter) } func wsError(resp *restful.Response, err error) { log.Output(2, fmt.Sprint("request failed: ", err)) switch err := err.(type) { case httperr.Error: err.WriteJSON(resp.ResponseWriter) default: httperr.Internal(err).WriteJSON(resp.ResponseWriter) } } func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value interface{}) { tmpl, err := template.New("wsRender").Funcs(templateFuncs(sslCfg)).Parse(tmplStr) if err != nil { wsError(resp, err) return } err = tmpl.Execute(resp, value) if err != nil { wsError(resp, err) return } }