This patch makes it possible to serve static content from Nix store paths, by using the hash of the store path for the ETag header. diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c index 97a91aee2..2d07d71e6 100644 --- a/src/http/ngx_http_core_module.c +++ b/src/http/ngx_http_core_module.c @@ -1676,6 +1676,8 @@ ngx_http_set_etag(ngx_http_request_t *r) { ngx_table_elt_t *etag; ngx_http_core_loc_conf_t *clcf; + u_char *real, *ptr1, *ptr2; + ngx_err_t err; clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); @@ -1692,16 +1694,82 @@ ngx_http_set_etag(ngx_http_request_t *r) etag->next = NULL; ngx_str_set(&etag->key, "ETag"); - etag->value.data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN + NGX_TIME_T_LEN + 3); - if (etag->value.data == NULL) { - etag->hash = 0; - return NGX_ERROR; + // Upstream nginx uses file mod timestamp and content-length for Etag, but + // files in the Nix store have their timestamps reset, so that doesn't work. + // Instead, when serving from the Nix store, we use the hash from the store + // path and content-length. + // + // Every file in under the given store path will share the same store path + // hash. It is fine to serve different resources with the same Etag, but + // different representations of the same resource (eg the same file, but + // gzip-compressed) should have different Etags. Thus, we also append + // content-length, which should be different when the response is compressed + + err = ngx_errno; + real = ngx_realpath(clcf->root.data, NULL); + ngx_set_errno(err); + + #define NIX_STORE_DIR "@nixStoreDir@" + #define NIX_STORE_LEN @nixStoreDirLen@ + + if (r->headers_out.last_modified_time == 1 + && real != NULL + && !ngx_strncmp(real, NIX_STORE_DIR, NIX_STORE_LEN) + && real[NIX_STORE_LEN] == '/' + && real[NIX_STORE_LEN + 1] != '\0') + { + // extract the hash from a path formatted like + // /nix/store/hashhere1234-pname-1.0.0 + // +1 to skip the leading / + ptr1 = real + NIX_STORE_LEN + 1; + + ptr2 = (u_char *) ngx_strchr(ptr1, '-'); + + if (ptr2 == NULL) { + ngx_free(real); + etag->hash = 0; + return NGX_ERROR; + } + + *ptr2 = '\0'; + + // hash + content-length + quotes and hyphen. Note that the + // content-length part of the string can vary in length. + etag->value.data = ngx_pnalloc(r->pool, ngx_strlen(ptr1) + NGX_OFF_T_LEN + 3); + + if (etag->value.data == NULL) { + ngx_free(real); + etag->hash = 0; + return NGX_ERROR; + } + + + // set value.data content to "{hash}-{content-length}" (including quote + // marks), and set value.len to the length of the resulting string + etag->value.len = ngx_sprintf(etag->value.data, "\"\%s-%xO\"", + ptr1, + r->headers_out.content_length_n) + - etag->value.data; + + ngx_http_clear_last_modified(r); + } else { + // outside of Nix store, use the upstream Nginx logic for etags + + etag->value.data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN + NGX_TIME_T_LEN + 3); + + if (etag->value.data == NULL) { + ngx_free(real); + etag->hash = 0; + return NGX_ERROR; + } + + etag->value.len = ngx_sprintf(etag->value.data, "\"%xT-%xO\"", + r->headers_out.last_modified_time, + r->headers_out.content_length_n) + - etag->value.data; } - etag->value.len = ngx_sprintf(etag->value.data, "\"%xT-%xO\"", - r->headers_out.last_modified_time, - r->headers_out.content_length_n) - - etag->value.data; + ngx_free(real); r->headers_out.etag = etag;