From 5946a537ffcd85543f994822d73b7933c586eb03 Mon Sep 17 00:00:00 2001 From: Cyrill Troxler Date: Mon, 18 Dec 2023 17:32:15 +0100 Subject: [PATCH] feat: set last-modified header with build time As the source files in the reulting buildpack always have the same timestamp, we work around this by setting the last-modified header in the nginx.conf to the current time at build time. This makes caching work at least somewhat. --- assets/default.conf | 227 ++++++++++++++++++++++++++++++++++++ build.go | 17 ++- default_config_generator.go | 88 ++++++++++++++ 3 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 assets/default.conf create mode 100644 default_config_generator.go diff --git a/assets/default.conf b/assets/default.conf new file mode 100644 index 0000000..94d3bda --- /dev/null +++ b/assets/default.conf @@ -0,0 +1,227 @@ +# Number of worker processes running in container +worker_processes 1; + +# Run NGINX in foreground (necessary for containerized NGINX) +daemon off; + +# Set the location of the server's error log +error_log stderr; + +events { + # Set number of simultaneous connections each worker process can serve + worker_connections 1024; +} + +http { + client_body_temp_path {{ tempDir }}/client_body_temp; + proxy_temp_path {{ tempDir }}/proxy_temp; + fastcgi_temp_path {{ tempDir }}/fastcgi_temp; + + charset utf-8; + + # Map media types to file extensions + types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + font/ttf ttf; + font/woff woff; + font/woff2 woff2; + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + text/cache-manifest manifest; + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + image/webp webp; + application/java-archive jar war ear; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.ms-excel xls; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/zip zip; + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream eot; + application/octet-stream iso img; + application/octet-stream msi msp msm; + application/json json; + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + video/3gpp 3gpp 3gp; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; + } + + access_log /dev/stdout; + + # Set the default MIME type of responses; 'application/octet-stream' + # represents an arbitrary byte stream + default_type application/octet-stream; + + # (Performance) When sending files, skip copying into buffer before sending. + sendfile on; + # (Only active with sendfile on) wait for packets to reach max size before + # sending. + tcp_nopush on; + + # (Performance) Enable compressing responses + gzip on; + # For all clients + gzip_static always; + # Including responses to proxied requests + gzip_proxied any; + # For responses above a certain length + gzip_min_length 1100; + # That are one of the following MIME types + gzip_types + text/plain + text/css + text/js + text/xml + text/javascript + application/javascript + application/x-javascript + application/json + application/xml + application/xml+rss + font/eot + font/otf + font/ttf + image/svg+xml; + # Compress responses to a medium degree + gzip_comp_level 6; + # Using 16 buffers of 8k bytes each + gzip_buffers 16 8k; + + # Add "Vary: Accept-Encoding” response header to compressed responses + gzip_vary on; + + # Decompress responses if client doesn't support compressed + gunzip on; + + # Don't compress responses if client is Internet Explorer 6 + gzip_disable "msie6"; + + # Set a timeout during which a keep-alive client connection will stay open on + # the server side + keepalive_timeout 30; + + # Ensure that redirects don't include the internal container PORT - <%= + # ENV["PORT"] %> + port_in_redirect off; + + # (Security) Disable emitting nginx version on error pages and in the + # “Server” response header field + server_tokens off; + + server { + listen {{port}} default_server; + server_name _; + + # Directory where static files are located + root $(( .WebServerRoot -)); +$(( if .WebServerForceHTTPS )) + # If HTTP request is made, redirect to HTTPS requests + set $updated_host $host; + if ($http_x_forwarded_host != "") { + set $updated_host $http_x_forwarded_host; + } + + if ($http_x_forwarded_proto != "https") { + return 301 https://$updated_host$request_uri; + } +$(( end )) +$((- if (ne .BasicAuthFile "") )) + # Require username + password authentication for access + auth_basic "Password Protected"; + auth_basic_user_file $(( .BasicAuthFile )); +$(( end )) + location $(( .WebServerLocationPath )) { +$((- if .WebServerEnablePushState )) + # Send the content at / in response to *any* requested endpoint + if (!-e $request_filename) { + rewrite ^(.*)$ / break; + } +$(( end )) + # Specify files sent to client if specific file not requested (e.g. + # GET www.example.com/). NGINX sends first existing file in the list. + index index.html index.htm Default.htm; + } + +$((- if .LastModifiedValue )) + add_header last-modified '$(( .LastModifiedValue ))'; +$(( end )) + +$((- if .ETag )) + etag 'on'; +$(( else )) + etag 'off'; +$(( end )) + + # (Security) Don't serve dotfiles, except .well-known/, which is needed by + # LetsEncrypt + location ~ /\.(?!well-known) { + deny all; + return 404; + } + } + +$(( if .NGINXStubStatusPort )) + # stub_status + server { + listen $(( .NGINXStubStatusPort -)); + listen [::]:$(( .NGINXStubStatusPort -)); + + location /stub_status { + stub_status; + } + } +$(( end )) +} diff --git a/build.go b/build.go index 79b65ce..17e00e9 100644 --- a/build.go +++ b/build.go @@ -3,8 +3,10 @@ package static import ( "errors" "fmt" + "net/http" "os" "path/filepath" + "time" "github.com/paketo-buildpacks/nginx" "github.com/paketo-buildpacks/packit/v2" @@ -29,10 +31,17 @@ func Build(logger scribe.Emitter) packit.BuildFunc { nginxConf := filepath.Join(context.WorkingDir, nginx.ConfFile) if _, err := os.Stat(nginxConf); err != nil { if errors.Is(err, os.ErrNotExist) { - confGen := nginx.NewDefaultConfigGenerator(logger) - if err := confGen.Generate(nginx.Configuration{ - NGINXConfLocation: nginxConf, - WebServerRoot: webRoot, + confGen := NewDefaultConfigGenerator(logger) + if err := confGen.Generate(Configuration{ + // we set the last-modified header to the current time + // during build. This works around the issue described in: + // https://github.com/paketo-buildpacks/nginx/issues/447 + LastModifiedValue: time.Now().UTC().Format(http.TimeFormat), + ETag: false, + Configuration: nginx.Configuration{ + NGINXConfLocation: nginxConf, + WebServerRoot: webRoot, + }, }); err != nil { return packit.BuildResult{}, packit.Fail.WithMessage("unable to create nginx.conf: %s", err) } diff --git a/default_config_generator.go b/default_config_generator.go new file mode 100644 index 0000000..442a33f --- /dev/null +++ b/default_config_generator.go @@ -0,0 +1,88 @@ +package static + +import ( + "bytes" + _ "embed" + "fmt" + "html/template" + "io" + "os" + "path/filepath" + + "github.com/paketo-buildpacks/nginx" + "github.com/paketo-buildpacks/packit/v2/scribe" +) + +//go:embed assets/default.conf +var DefaultConfigTemplate string + +// this has been adapted from https://github.com/paketo-buildpacks/nginx/blob/main/default_config_generator.go +// to add our own customizations to the config +type DefaultConfigGenerator struct { + logs scribe.Emitter +} + +type Configuration struct { + LastModifiedValue string + ETag bool + nginx.Configuration +} + +func NewDefaultConfigGenerator(logs scribe.Emitter) DefaultConfigGenerator { + return DefaultConfigGenerator{logs: logs} +} + +func (g DefaultConfigGenerator) Generate(config Configuration) error { + g.logs.Process("Generating %s", config.NGINXConfLocation) + t := template.Must(template.New("template.conf").Delims("$((", "))").Parse(DefaultConfigTemplate)) + + if !filepath.IsAbs(config.WebServerRoot) { + config.WebServerRoot = filepath.Join(`{{ env "APP_ROOT" }}`, config.WebServerRoot) + } + + g.logs.Subprocess("Setting server root directory to '%s'", config.WebServerRoot) + + if config.WebServerLocationPath == "" { + config.WebServerLocationPath = "/" + } + + g.logs.Subprocess("Setting server location path to '%s'", config.WebServerLocationPath) + + if config.WebServerEnablePushState { + g.logs.Subprocess("Enabling push state routing") + } + + if config.WebServerForceHTTPS { + g.logs.Subprocess("Setting server to redirect HTTP requests to HTTPS") + } + + if config.BasicAuthFile != "" { + g.logs.Subprocess("Enabling basic authentication with .htpasswd credentials") + } + + if config.NGINXStubStatusPort != "" { + g.logs.Subprocess("Enabling basic status information with stub_status module") + } + + g.logs.Break() + + var b bytes.Buffer + err := t.Execute(&b, config) + if err != nil { + // not tested + return err + } + + f, err := os.OpenFile(config.NGINXConfLocation, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) + if err != nil { + return fmt.Errorf("failed to create %s: %w", config.NGINXConfLocation, err) + } + defer f.Close() + + _, err = io.Copy(f, &b) + if err != nil { + // not tested + return err + } + return nil +}