diff --git a/pkg/assets/apiserver/asset_apiserver.go b/pkg/assets/apiserver/asset_apiserver.go index 8eb62697a8..b62140a8f0 100644 --- a/pkg/assets/apiserver/asset_apiserver.go +++ b/pkg/assets/apiserver/asset_apiserver.go @@ -129,13 +129,6 @@ func (c completedAssetServerConfig) New(delegationTarget genericapiserver.Delega if err := c.addAssets(s.GenericAPIServer.Handler.NonGoRestfulMux); err != nil { return nil, err } - if err := c.addExtensionScripts(s.GenericAPIServer.Handler.NonGoRestfulMux); err != nil { - return nil, err - } - if err := c.addExtensionStyleSheets(s.GenericAPIServer.Handler.NonGoRestfulMux); err != nil { - return nil, err - } - c.addExtensionFiles(s.GenericAPIServer.Handler.NonGoRestfulMux) if err := c.addWebConsoleConfig(s.GenericAPIServer.Handler.NonGoRestfulMux); err != nil { return nil, err } @@ -173,41 +166,6 @@ func (c completedAssetServerConfig) addAssets(serverMux *genericmux.PathRecorder return nil } -func (c completedAssetServerConfig) addExtensionScripts(serverMux *genericmux.PathRecorderMux) error { - // Extension scripts - extScriptsPath := path.Join(c.PublicURL.Path, "scripts/extensions.js") - extScriptsHandler, err := assets.ExtensionScriptsHandler(c.Options.ExtensionScripts, c.Options.ExtensionDevelopment) - if err != nil { - return err - } - extScriptsHandler = assets.SecurityHeadersHandler(extScriptsHandler) - serverMux.UnlistedHandle(extScriptsPath, assets.GzipHandler(extScriptsHandler)) - return nil -} - -func (c completedAssetServerConfig) addExtensionStyleSheets(serverMux *genericmux.PathRecorderMux) error { - // Extension stylesheets - extStylesheetsPath := path.Join(c.PublicURL.Path, "styles/extensions.css") - extStylesheetsHandler, err := assets.ExtensionStylesheetsHandler(c.Options.ExtensionStylesheets, c.Options.ExtensionDevelopment) - if err != nil { - return err - } - extStylesheetsHandler = assets.SecurityHeadersHandler(extStylesheetsHandler) - serverMux.UnlistedHandle(extStylesheetsPath, assets.GzipHandler(extStylesheetsHandler)) - return nil -} - -func (c completedAssetServerConfig) addExtensionFiles(serverMux *genericmux.PathRecorderMux) { - // Extension files - for _, extConfig := range c.Options.Extensions { - extBasePath := path.Join(c.PublicURL.Path, "extensions", extConfig.Name) - extPath := extBasePath + "/" - extHandler := assets.AssetExtensionHandler(extConfig.SourceDirectory, extPath, extConfig.HTML5Mode) - serverMux.UnlistedHandlePrefix(extPath, http.StripPrefix(extBasePath, extHandler)) - serverMux.UnlistedHandle(extBasePath, http.RedirectHandler(extPath, http.StatusMovedPermanently)) - } -} - func (c *completedAssetServerConfig) addWebConsoleConfig(serverMux *genericmux.PathRecorderMux) error { masterURL, err := url.Parse(c.Options.MasterPublicURL) if err != nil { @@ -266,7 +224,7 @@ func (c completedAssetServerConfig) buildAssetHandler() (http.Handler, error) { } var err error - handler, err = assets.HTML5ModeHandler(c.PublicURL.Path, subcontextMap, handler, assetFunc) + handler, err = assets.HTML5ModeHandler(c.PublicURL.Path, subcontextMap, c.Options.ExtensionScripts, c.Options.ExtensionStylesheets, handler, assetFunc) if err != nil { return nil, err } diff --git a/pkg/assets/extensions.go b/pkg/assets/extensions.go deleted file mode 100644 index 372bd27f41..0000000000 --- a/pkg/assets/extensions.go +++ /dev/null @@ -1,186 +0,0 @@ -package assets - -import ( - "bytes" - "crypto/md5" - "encoding/hex" - "fmt" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "strings" - - utilruntime "k8s.io/apimachinery/pkg/util/runtime" -) - -// ExtensionScriptsHandler concatenates and serves extension JavaScript files as one HTTP response. -func ExtensionScriptsHandler(files []string, developmentMode bool) (http.Handler, error) { - return concatHandler(files, developmentMode, "application/javascript; charset=utf-8", ";\n") -} - -// ExtensionStylesheetsHandler concatenates and serves extension stylesheets as one HTTP response. -func ExtensionStylesheetsHandler(files []string, developmentMode bool) (http.Handler, error) { - return concatHandler(files, developmentMode, "text/css; charset=utf-8", "\n") -} - -func concatHandler(files []string, developmentMode bool, mediaType, separator string) (http.Handler, error) { - // Read the files for each request if development mode is enabled. - if developmentMode { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - bytes, err := concatAll(files, separator) - if err != nil { - utilruntime.HandleError(fmt.Errorf("Error serving extension content: %v", err)) - http.Error(w, "Internal server error", http.StatusInternalServerError) - } - serve(w, r, bytes, mediaType, "") - }), nil - } - - // Otherwise, read the files once on server startup. - bytes, err := concatAll(files, separator) - if err != nil { - return nil, err - } - hash := calculateMD5(bytes) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - serve(w, r, bytes, mediaType, hash) - }), nil -} - -func concatAll(files []string, separator string) ([]byte, error) { - var buffer bytes.Buffer - for _, file := range files { - f, err := os.Open(file) - if err != nil { - return nil, err - } - defer f.Close() - - _, err = buffer.ReadFrom(f) - if err != nil { - return nil, err - } - - _, err = buffer.WriteString(separator) - if err != nil { - return nil, err - } - } - - return buffer.Bytes(), nil -} - -func calculateMD5(bytes []byte) string { - hasher := md5.New() - hasher.Write(bytes) - sum := hasher.Sum(nil) - - return hex.EncodeToString(sum) -} - -func generateETag(w http.ResponseWriter, r *http.Request, hash string) string { - vary := w.Header().Get("Vary") - varyHeaders := []string{} - if vary != "" { - varyHeaders = varyHeaderRegexp.Split(vary, -1) - } - - varyHeaderValues := "" - for _, varyHeader := range varyHeaders { - varyHeaderValues += r.Header.Get(varyHeader) - } - - return fmt.Sprintf("W/\"%s_%s\"", hash, hex.EncodeToString([]byte(varyHeaderValues))) -} - -func serve(w http.ResponseWriter, r *http.Request, bytes []byte, mediaType, hash string) { - if len(hash) > 0 { - etag := generateETag(w, r, hash) - if r.Header.Get("If-None-Match") == etag { - w.WriteHeader(http.StatusNotModified) - return - } - - w.Header().Set("ETag", etag) - w.Header().Set("Cache-Control", "public, max-age=0, must-revalidate") - } else { - w.Header().Add("Cache-Control", "no-cache, no-store") - } - - w.Header().Set("Content-Type", mediaType) - _, err := w.Write(bytes) - if err != nil { - utilruntime.HandleError(fmt.Errorf("Error serving extension content: %v", err)) - } -} - -// AssetExtensionHandler serves extension files from sourceDir. context is the URL context for this -// extension. -func AssetExtensionHandler(sourceDir, context string, html5Mode bool) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - serveExtensionFile(w, r, sourceDir, context, html5Mode) - }) -} - -// Injects the HTML into the file and serves it. -func serveIndex(w http.ResponseWriter, r *http.Request, path, base string) { - content, err := ioutil.ReadFile(path) - if err != nil { - http.NotFound(w, r) - return - } - - // Make sure the base always ends in a trailing slash. - if !strings.HasSuffix(base, "/") { - base += "/" - } - content = bytes.Replace(content, []byte(``), []byte(fmt.Sprintf(``, base)), 1) - - w.Header().Add("Cache-Control", "no-cache, no-store") - w.Header().Set("Content-Type", "text/html; charset=UTF-8") - - w.Write(content) -} - -func serveFile(w http.ResponseWriter, r *http.Request, path, base string, html5Mode bool) { - _, name := filepath.Split(path) - if html5Mode && name == "index.html" { - // Inject the correct base for Angular apps if the file is index.html. - serveIndex(w, r, path, base) - } else { - // Otherwise just serve the file. - http.ServeFile(w, r, path) - } -} - -// Serve the extension file under dir matching the path from the request URI. -func serveExtensionFile(w http.ResponseWriter, r *http.Request, sourceDir, context string, html5Mode bool) { - // The path to the requested file on the filesystem. - file := filepath.Join(sourceDir, r.URL.Path) - - if html5Mode { - // Check if the file exists. - fileInfo, err := os.Stat(file) - if err != nil { - if os.IsNotExist(err) { - index := filepath.Join(sourceDir, "index.html") - serveFile(w, r, index, context, html5Mode) - return - } - - utilruntime.HandleError(fmt.Errorf("Error serving extension file: %v", err)) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - if fileInfo.IsDir() { - index := filepath.Join(sourceDir, "index.html") - serveFile(w, r, index, context, html5Mode) - return - } - } - - serveFile(w, r, file, context, html5Mode) -} diff --git a/pkg/assets/handlers.go b/pkg/assets/handlers.go index 8afce24220..e15f16c5a1 100644 --- a/pkg/assets/handlers.go +++ b/pkg/assets/handlers.go @@ -5,6 +5,7 @@ import ( "compress/gzip" "encoding/hex" "fmt" + "html" "io" "net/http" "path" @@ -112,7 +113,7 @@ func (s LongestToShortest) Less(i, j int) bool { // // subcontextMap is a map of keys (subcontexts, no leading or trailing slashes) to the asset path (no // leading slash) to serve for that subcontext if a resource that does not exist is requested -func HTML5ModeHandler(contextRoot string, subcontextMap map[string]string, h http.Handler, getAsset AssetFunc) (http.Handler, error) { +func HTML5ModeHandler(contextRoot string, subcontextMap map[string]string, extensionScripts []string, extensionStylesheets []string, h http.Handler, getAsset AssetFunc) (http.Handler, error) { subcontextData := map[string][]byte{} subcontexts := []string{} @@ -127,6 +128,17 @@ func HTML5ModeHandler(contextRoot string, subcontextMap map[string]string, h htt base += "/" } b = bytes.Replace(b, []byte(``), []byte(fmt.Sprintf(``, base)), 1) + + // Inject extension scripts and stylesheets, but only for the console itself, which has an empty subcontext + if len(subcontext) == 0 { + if len(extensionScripts) > 0 { + b = addExtensionScripts(b, extensionScripts) + } + if len(extensionStylesheets) > 0 { + b = addExtensionStylesheets(b, extensionStylesheets) + } + } + subcontextData[subcontext] = b subcontexts = append(subcontexts, subcontext) } @@ -153,6 +165,30 @@ func HTML5ModeHandler(contextRoot string, subcontextMap map[string]string, h htt }), nil } +// Add the extension scripts as the last scripts, just before the body closing tag. +func addExtensionScripts(content []byte, extensionScripts []string) []byte { + var scriptTags bytes.Buffer + for _, scriptURL := range extensionScripts { + scriptTags.WriteString(fmt.Sprintf("\n", html.EscapeString(scriptURL))) + } + + replaceBefore := []byte("") + scriptTags.Write(replaceBefore) + return bytes.Replace(content, replaceBefore, scriptTags.Bytes(), 1) +} + +// Add the extension stylesheets as the last stylesheets, just before the head closing tag. +func addExtensionStylesheets(content []byte, extensionStylesheets []string) []byte { + var styleTags bytes.Buffer + for _, stylesheetURL := range extensionStylesheets { + styleTags.WriteString(fmt.Sprintf("\n", html.EscapeString(stylesheetURL))) + } + + replaceBefore := []byte("") + styleTags.Write(replaceBefore) + return bytes.Replace(content, replaceBefore, styleTags.Bytes(), 1) +} + var versionTemplate = template.Must(template.New("webConsoleVersion").Parse(` window.OPENSHIFT_VERSION = { openshift: "{{ .OpenShiftVersion | js}}", diff --git a/vendor/github.com/openshift/origin/pkg/cmd/server/api/validation/master.go b/vendor/github.com/openshift/origin/pkg/cmd/server/api/validation/master.go index f283a97e55..4522dedaaf 100644 --- a/vendor/github.com/openshift/origin/pkg/cmd/server/api/validation/master.go +++ b/vendor/github.com/openshift/origin/pkg/cmd/server/api/validation/master.go @@ -502,13 +502,15 @@ func ValidateAssetConfig(config *api.AssetConfig, fldPath *field.Path) Validatio validationResults.AddWarnings(field.Invalid(fldPath.Child("metricsPublicURL"), "", "required to view cluster metrics in the console")) } - for i, scriptFile := range config.ExtensionScripts { - validationResults.AddErrors(ValidateFile(scriptFile, fldPath.Child("extensionScripts").Index(i))...) - } - - for i, stylesheetFile := range config.ExtensionStylesheets { - validationResults.AddErrors(ValidateFile(stylesheetFile, fldPath.Child("extensionStylesheets").Index(i))...) - } + // FIXME: Temporarily turn off validation since these are now treated as URLs. + // We will fix when we update the AssetConfig for the new extension script URL / style URL property names. + // for i, scriptFile := range config.ExtensionScripts { + // validationResults.AddErrors(ValidateFile(scriptFile, fldPath.Child("extensionScripts").Index(i))...) + // } + + // for i, stylesheetFile := range config.ExtensionStylesheets { + // validationResults.AddErrors(ValidateFile(stylesheetFile, fldPath.Child("extensionStylesheets").Index(i))...) + // } nameTaken := map[string]bool{} for i, extConfig := range config.Extensions {