Skip to content

Commit

Permalink
feat: set last-modified header with build time
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ctrox committed Dec 18, 2023
1 parent 3028d4f commit 5946a53
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 4 deletions.
227 changes: 227 additions & 0 deletions assets/default.conf
Original file line number Diff line number Diff line change
@@ -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 ))
}
17 changes: 13 additions & 4 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
}
Expand Down
88 changes: 88 additions & 0 deletions default_config_generator.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 5946a53

Please sign in to comment.