diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..39f9642 --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +IndentWidth: 4 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..20d4e93 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,35 @@ +name: ci + +on: + push: + branches: + - main + + pull_request: + types: + - opened + - synchronize + - reopened + +jobs: + test: + runs-on: ubuntu-latest + + timeout-minutes: 30 + + strategy: + matrix: + varnish_version: + - "7.5" + - "7.6" + + concurrency: + group: ${{ github.ref_name != 'main' && format('{0}-{1}-{2}', github.workflow, github.ref, matrix.varnish_version) || github.sha }} + cancel-in-progress: ${{ github.ref_name != 'main' }} + + steps: + - uses: actions/checkout@v4 + + - run: docker compose build --build-arg "VARNISH_VERSION=${{ matrix.varnish_version }}" + + - run: docker compose run test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c22f0c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# build system + +.deps/ +.libs/ +autom4te.cache/ +build-aux/ +m4/ + +*.la +*.lo +*.o +*.tar.gz + +Makefile +Makefile.in +aclocal.m4 +config.h +config.h.in +config.log +config.status +configure +configure~ +libtool +stamp-h1 + +# test suite + +*.log +*.trs + +# vmodtool + +vcc_*_if.[ch] +vmod_*.rst + +# man + +*.1 +*_options.rst +*_synopsis.rst +vmod_*.3 + +# rpm + +mockbuild/ +rpmbuild/ + +*.rpm +*.spec + +# clang + +.cache/ +compile_commands.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f36ffe7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +ARG VARNISH_VERSION=7.6 +FROM varnish:${VARNISH_VERSION} + +USER root + +RUN apt-get update && apt-get install -y \ + build-essential \ + libtool \ + automake \ + python3-docutils + +WORKDIR / +COPY . . + +RUN ./bootstrap \ + && make \ + && make install diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..97c8098 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 duffn + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..70a77ac --- /dev/null +++ b/Makefile.am @@ -0,0 +1,7 @@ +ACLOCAL_AMFLAGS = -I m4 -I @VARNISHAPI_DATAROOTDIR@/aclocal + +DISTCHECK_CONFIGURE_FLAGS = RST2MAN=: + +SUBDIRS = src + +EXTRA_DIST = libvmod-querymodifier.spec diff --git a/README.md b/README.md new file mode 100644 index 0000000..4597776 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# libvmod-querymodifier + +This is a simple Varnish VMOD that allows modification of a URL's query parameters by including or excluding specified parameters and their values. + +## Status + +ℹ️ This VMOD is currently exploratory and being actively developed and tested. It has not been run in any production environment yet, +so you should not yet use this in a production environment. + +If you need to manipulate querystrings in production, you should currently explore [`libvmod-queryfilter`](https://github.com/nytimes/libvmod-queryfilter/) or [`vmod-querystring`](https://git.sr.ht/~dridi/vmod-querystring). + +If instead you just want to contribute to a friendly VMOD repository, continue on! + +## Usage + +### Inclusion + +List the parameters that you would like to have _remain_ in the URL. All other query parameters and their values will be removed. + +``` +import querymodifier; +set req.url = querymodifier.modifyparams(url=req.url, params="search,id", exclude_params=false); + +# Original URL: example.com/?search=name&ts=123456789&id=987654321 +# Modified URL: example.com/?search=name&id=987654321 +``` + +### Exclusion + +List the parameters that you would like to have _removed_ from the URL. All other query parameters and their values will remain. + +``` +import querymodifier; +set req.url = querymodifier.modifyparams(url=req.url, params="ts,v", exclude_params=true); + +# Original URL: example.com/?search=name&ts=123456789&v=123456789&id=987654321 +# Modified URL: example.com/?search=name&id=987654321 +``` + +### Remove all + +Remove all query parameters by passing in an empty string. + +``` +import querymodifier; +set req.url = querymodifier.modifyparams(url=req.url, params="", exclude_params=true); + +# Original URL: example.com/?search=name&ts=123456789&v=123456789&id=987654321 +# Modified URL: example.com/ +``` + +### Additional + +See the tests for more parameter edge cases. + +## Building + +This module is primarily manually tested with Varnish 7.6, but also includes vtc tests for version 7.5. + +``` +./bootstrap +make +make check # optionally run tests, recommended. +sudo make install +``` + +## Contributing + +Fork, code, and PR! See build instructions above. + +I'm happy to review any PRs. Any bug reports are also welcome. + +## Acknowledgements + +- The NY Times [`libvmod-queryfilter` VMOD](https://github.com/nytimes/libvmod-queryfilter/) for insipiration. +- [`vcdk`](https://github.com/nigoroll/vcdk/) for the project structure. +- Guillaume Quintard for the [VMOD tutorial](https://info.varnish-software.com/blog/creating-a-vmod-vmod-str). + +## License + +[Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) diff --git a/bootstrap b/bootstrap new file mode 100755 index 0000000..41fa8a3 --- /dev/null +++ b/bootstrap @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e +set -u + +WORK_DIR=$(pwd) +ROOT_DIR=$(dirname "$0") + +cd "$ROOT_DIR" + +if ! command -v libtoolize >/dev/null +then + echo "libtoolize: command not found, falling back to glibtoolize" >&2 + alias libtoolize=glibtoolize +fi + +mkdir -p m4 +aclocal +libtoolize --copy --force +autoheader +automake --add-missing --copy --foreign +autoconf + +cd "$WORK_DIR" +"$ROOT_DIR"/configure "$@" diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..6eaeb1e --- /dev/null +++ b/configure.ac @@ -0,0 +1,47 @@ +AC_PREREQ([2.68]) +AC_INIT([libvmod-querymodifier], [0.1]) +AC_CONFIG_MACRO_DIR([m4]) +AC_CONFIG_AUX_DIR([build-aux]) +AC_CONFIG_HEADERS([config.h]) + +AM_INIT_AUTOMAKE([1.12 -Wall -Werror foreign parallel-tests]) +AM_SILENT_RULES([yes]) +AM_PROG_AR + +LT_PREREQ([2.2.6]) +LT_INIT([dlopen disable-static]) + +AC_ARG_WITH([rst2man], + AS_HELP_STRING( + [--with-rst2man=PATH], + [Location of rst2man (auto)]), + [RST2MAN="$withval"], + [AC_CHECK_PROGS(RST2MAN, [rst2man rst2man.py], [])]) + +VARNISH_PREREQ([7.0.0]) +VARNISH_VMODS([querymodifier]) + +VMOD_TESTS="$(cd $srcdir/src && echo vtc/*.vtc)" +AC_SUBST(VMOD_TESTS) + +AC_CONFIG_FILES([ + Makefile + src/Makefile + libvmod-querymodifier.spec +]) + +AC_OUTPUT + +AS_ECHO(" + ==== $PACKAGE_STRING ==== + + varnish: $VARNISH_VERSION + prefix: $prefix + vmoddir: $vmoddir + vcldir: $vcldir + pkgvcldir: $pkgvcldir + + compiler: $CC + cflags: $CFLAGS + ldflags: $LDFLAGS +") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..65b3bb8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + vmod: + build: &build + context: . + dockerfile: Dockerfile + + test: + build: *build + command: make check diff --git a/libvmod-querymodifier.spec.in b/libvmod-querymodifier.spec.in new file mode 100644 index 0000000..fe81f04 --- /dev/null +++ b/libvmod-querymodifier.spec.in @@ -0,0 +1,41 @@ +%global __debug_package 0 +%global __strip true + +%global vmoddir %{_libdir}/varnish/vmods +%global vcldir %{_datadir}/varnish/vcl + +Name: @PACKAGE@ +Version: @PACKAGE_VERSION@ +Release: 1%{?dist} +Summary: XXX: put your summary here + +License: XXX: put your license here +URL: XXX://put.your/url/here +Source: %{name}-%{version}.tar.gz + +BuildRequires: pkgconfig(varnishapi) >= 6.0.0 + +%description +XXX: put your long description here + +%prep +%setup -q + +%build +%configure CFLAGS="%{optflags}" RST2MAN=: +%make_build V=1 + +%install +%make_install +rm -f %{buildroot}%{vmoddir}/*.la + +%check +%make_build check + +%files +%{_mandir}/man*/* +%{vmoddir}/libvmod_querymodifier.so + +%changelog +* Tue Nov 26 2024 XXX: author - 0.1 +- Initial spec diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..ad4e225 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,40 @@ +AM_CFLAGS = $(VARNISHAPI_CFLAGS) + +# Modules + +vmod_LTLIBRARIES = \ + libvmod_querymodifier.la + +libvmod_querymodifier_la_LDFLAGS = $(VMOD_LDFLAGS) +libvmod_querymodifier_la_SOURCES = vmod_querymodifier.c +nodist_libvmod_querymodifier_la_SOURCES = \ + vcc_querymodifier_if.c \ + vcc_querymodifier_if.h + +@BUILD_VMOD_QUERYMODIFIER@ + +# Test suite + +AM_TESTS_ENVIRONMENT = \ + PATH="$(abs_builddir):$(VARNISH_TEST_PATH):$(PATH)" \ + LD_LIBRARY_PATH="$(VARNISH_LIBRARY_PATH)" +TEST_EXTENSIONS = .vtc +VTC_LOG_COMPILER = varnishtest -v +AM_VTC_LOG_FLAGS = \ + -p vcl_path="$(abs_top_srcdir)/vcl:$(VARNISHAPI_VCLDIR)" \ + -p vmod_path="$(abs_builddir)/.libs:$(vmoddir):$(VARNISHAPI_VMODDIR)" + +TESTS = $(VMOD_TESTS) + +# Documentation + +dist_doc_DATA = \ + vmod_querymodifier.vcc \ + $(TESTS) + +dist_man_MANS = \ + vmod_querymodifier.3 + + +.rst.1: + $(AM_V_GEN) $(RST2MAN) $< $@ diff --git a/src/vmod_querymodifier.c b/src/vmod_querymodifier.c new file mode 100644 index 0000000..346f7e0 --- /dev/null +++ b/src/vmod_querymodifier.c @@ -0,0 +1,224 @@ +#include "config.h" +#include +#include +#include +#include +#include + +#include "cache/cache.h" +#include "vcc_querymodifier_if.h" +#include "vcl.h" +#include "vsb.h" + +#define MAX_QUERY_PARAMS 100 +#define MAX_FILTER_PARAMS 100 + +typedef struct query_param { + char *name; + char *value; +} query_param_t; + +/** + * Tokenize the query string into an array of query parameters. + * @param ctx The Varnish context. + * @param result The array of query parameters. + * @param query_str The query string to tokenize. + * @return The number of query parameters. + */ +static int tokenize_querystring(VRT_CTX, query_param_t **result, + char *query_str) { + int no_param = 0; + char *save_ptr; + char *param_str; + + *result = NULL; + + if (query_str == NULL) { + VRT_fail(ctx, "query_str is NULL"); + *result = NULL; + return -1; + } + + query_param_t *params_array = + WS_Alloc(ctx->ws, MAX_QUERY_PARAMS * sizeof(query_param_t)); + if (params_array == NULL) { + VRT_fail(ctx, "WS_Alloc: params_array: out of workspace"); + *result = NULL; + return -1; + } + + // Tokenize the query parameters into an array. + for (param_str = strtok_r(query_str, "&", &save_ptr); param_str; + param_str = strtok_r(NULL, "&", &save_ptr)) { + + if (no_param >= MAX_QUERY_PARAMS) { + VRT_fail(ctx, "Exceeded maximum number of query parameters"); + *result = NULL; + return -1; + } + + params_array[no_param].name = param_str; + params_array[no_param].value = strchr(param_str, '='); + if (params_array[no_param].value) { + *(params_array[no_param].value++) = '\0'; + } + no_param++; + } + + *result = params_array; + return no_param; +} + +/** + * This function modifies the query string of a URL by including or excluding + * query parameters based on the input parameters. + * @param ctx The Varnish context. + * @param uri The URL to modify. + * @param params_in The query parameters to include or exclude. + * @param exclude_params If true, exclude the parameters in params_in. If false, + * include the parameters in params_in. + */ +VCL_STRING vmod_modifyparams(VRT_CTX, VCL_STRING uri, VCL_STRING params_in, + VCL_BOOL exclude_params) { + char *saveptr; + char *new_uri; + char *new_uri_end; + char *query_str; + char *params; + query_param_t *head; + query_param_t *current; + char *filter_params[MAX_FILTER_PARAMS]; + int num_filter_params = 0; + int i; + int no_param; + char sep = '?'; + + CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC); + + // Return if the URL is NULL. + if (uri == NULL) { + VRT_fail(ctx, "uri is NULL"); + return NULL; + } + + // Return if there's no query string. + query_str = strchr(uri, '?'); + if (query_str == NULL) { + return uri; + } + + // Copy the base URL up to '?' into the workspace. + size_t base_uri_len = query_str - uri; + size_t query_str_len = strlen(query_str + 1); // +1 to skip '?' + size_t new_uri_max_len = + base_uri_len + query_str_len + 2; // +2 for '?' and '\0' + + new_uri = WS_Alloc(ctx->ws, new_uri_max_len); + if (new_uri == NULL) { + VRT_fail(ctx, "WS_Alloc: new_uri: out of workspace"); + return NULL; + } + + memcpy(new_uri, uri, base_uri_len); + new_uri[base_uri_len] = '\0'; + new_uri_end = new_uri + base_uri_len; + + // Skip past the '?' to get the query string. + query_str = query_str + 1; + + // If there are no query params, return the URL. + if (*query_str == '\0') { + return new_uri; + } + + // Check if params_in is an empty string and if so, return only + // the URL which removes all query params. + if (params_in == NULL || *params_in == '\0') { + return new_uri; + } + + // Copy the query string to the workspace. + char *query_str_copy = WS_Copy(ctx->ws, query_str, strlen(query_str) + 1); + if (!query_str_copy) { + VRT_fail(ctx, "WS_Copy: query_str_copy: out of workspace"); + return NULL; + } + + // Copy the params_in to the workspace. + params = WS_Copy(ctx->ws, params_in, strlen(params_in) + 1); + if (!params) { + VRT_fail(ctx, "WS_Copy: params: out of workspace"); + return NULL; + } + + // Tokenize params_in into filter_params array. + num_filter_params = 0; + for (char *filter_name = strtok_r(params, ",", &saveptr); filter_name; + filter_name = strtok_r(NULL, ",", &saveptr)) { + if (num_filter_params >= MAX_FILTER_PARAMS) { + VRT_fail(ctx, "Exceeded maximum number of filter parameters"); + return NULL; + } + filter_params[num_filter_params++] = filter_name; + } + + // Tokenize the query string into parameters. + no_param = tokenize_querystring(ctx, &head, query_str_copy); + if (no_param < 0) { + VRT_fail(ctx, "tokensize_querystring: no_param: out of workspace"); + return NULL; + } + + if (no_param == 0) { + return new_uri; + } + + struct vsb *vsb = VSB_new_auto(); + if (vsb == NULL) { + VRT_fail(ctx, "VSB_new_auto failed"); + return NULL; + } + + VSB_bcat(vsb, uri, base_uri_len); + + // Iterate through the query parameters. + for (i = 0, current = head; i < no_param; ++i, ++current) { + int match = 0; + for (int j = 0; j < num_filter_params; ++j) { + if (strcmp(current->name, filter_params[j]) == 0) { + match = 1; + break; + } + } + + // Include or exclude parameters based upon the argument. + int include = exclude_params ? !match : match; + if (include) { + if (current->value && (*current->value) != '\0') { + VSB_printf(vsb, "%c%s=%s", sep, current->name, current->value); + } else { + VSB_printf(vsb, "%c%s", sep, current->name); + } + sep = '&'; + } + } + + if (VSB_finish(vsb) != 0) { + VRT_fail(ctx, "VSB_finish failed"); + VSB_destroy(&vsb); + return NULL; + } + + // Copy the final URI from the VSB into the workspace + const char *final_uri = VSB_data(vsb); + size_t final_len = VSB_len(vsb); + char *ws_uri = WS_Copy(ctx->ws, final_uri, final_len + 1); + VSB_destroy(&vsb); + + if (ws_uri == NULL) { + VRT_fail(ctx, "WS_Copy: out of workspace"); + return NULL; + } + + return ws_uri; +} diff --git a/src/vmod_querymodifier.vcc b/src/vmod_querymodifier.vcc new file mode 100644 index 0000000..b129e53 --- /dev/null +++ b/src/vmod_querymodifier.vcc @@ -0,0 +1,18 @@ +$Module querymodifier Varnish "Query Parameter Modifier Module" + +DESCRIPTION +=========== + +`querymodifier` provides simple query string modification. + +$Function STRING modifyparams(STRING url, STRING params, BOOL exclude_params) + +Description + The function accepts a comma separated list of parameter names and returns the request URL with + either the provided parameters and their values included or excluded based upon + the `exclude_params` argument. + +Example + :: + + set req.url = querymodifier.modifyparams(req.url, "ts,v", true); diff --git a/src/vtc/arrays.vtc b/src/vtc/arrays.vtc new file mode 100644 index 0000000..b7121fe --- /dev/null +++ b/src/vtc/arrays.vtc @@ -0,0 +1,33 @@ +varnishtest "Test querymodifier vmod for arrays" + +server s1 { + rxreq + txresp -body "OK1" + expect req.url == "/feed/?arr[]=arr1&arr[]=arr2" +} -start + +varnish v1 -vcl+backend { + import std; + import querymodifier; + + sub vcl_hash { + std.syslog(180, "querymodifier before: " + req.url); + set req.url = querymodifier.modifyparams(url=req.url, params="arr[]", exclude_params=false); + std.syslog(180, "querymodifier after: " + req.url); + } +} -start + +client c1 { + txreq -url "/feed/?arr[]=arr1&arr[]=arr2¬arr[]=1¬arr[]=2" + rxresp + expect resp.status == 200 + + # This request will be cached as only `arr[]` is included. + txreq -url "/feed/?arr[]=arr1&arr[]=arr2&other[]=1&something=2" + rxresp + expect resp.status == 200 +} -run + +varnish v1 -expect n_object == 1 +varnish v1 -expect cache_miss == 1 +varnish v1 -expect cache_hit == 1 diff --git a/src/vtc/empty_params.vtc b/src/vtc/empty_params.vtc new file mode 100644 index 0000000..be279af --- /dev/null +++ b/src/vtc/empty_params.vtc @@ -0,0 +1,33 @@ +varnishtest "Test querymodifier vmod for empty params" + +server s1 { + rxreq + txresp -body "OK1" + expect req.url == "/feed/" +} -start + +varnish v1 -vcl+backend { + import std; + import querymodifier; + + sub vcl_recv { + std.syslog(180, "querymodifier before: " + req.url); + set req.url = querymodifier.modifyparams(url=req.url, params="q,id", exclude_params=false); + std.syslog(180, "querymodifier after: " + req.url); + } +} -start + +client c1 { + txreq -url "/feed/?" + rxresp + expect resp.status == 200 + + # This one is cached. + txreq -url "/feed/" + rxresp + expect resp.status == 200 +} -run + +varnish v1 -expect n_object == 1 +varnish v1 -expect cache_miss == 1 +varnish v1 -expect cache_hit == 1 diff --git a/src/vtc/exclusion.vtc b/src/vtc/exclusion.vtc new file mode 100644 index 0000000..57ad0eb --- /dev/null +++ b/src/vtc/exclusion.vtc @@ -0,0 +1,42 @@ +varnishtest "Test querymodifier vmod for proper exclusion of matching parameters" + +server s1 { + rxreq + txresp -body "OK1" + expect req.url == "/feed/" + + rxreq + txresp -body "OK1" + expect req.url == "/blog?before_date=2024-11-23T00%3A00%3A00.000Z" +} -start + +varnish v1 -vcl+backend { + import std; + import querymodifier; + + sub vcl_hash { + std.syslog(180, "querymodifier before: " + req.url); + set req.url = querymodifier.modifyparams(url=req.url, params="ts,v,date", exclude_params=true); + std.syslog(180, "querymodifier after: " + req.url); + } +} -start + +client c1 { + txreq -url "/feed/?ts=1730210988319" + rxresp + expect resp.status == 200 + + # This one will be cached as all of the query params are excluded. + txreq -url "/feed/?ts=1730210988319&v=1730210988319&date=1730210988319" + rxresp + expect resp.status == 200 + + txreq -url "/blog?ts=1730210988319&v=1730210988319&date=1730210988319&before_date=2024-11-23T00%3A00%3A00.000Z" + rxresp + expect resp.status == 200 +} -run + +varnish v1 -expect n_object == 2 +varnish v1 -expect cache_miss == 2 +varnish v1 -expect cache_hit == 1 + diff --git a/src/vtc/inclusion.vtc b/src/vtc/inclusion.vtc new file mode 100644 index 0000000..835396c --- /dev/null +++ b/src/vtc/inclusion.vtc @@ -0,0 +1,41 @@ +varnishtest "Test querymodifier vmod for proper inclusion of matching parameters" + +server s1 { + rxreq + txresp -body "OK1" + expect req.url == "/feed/?q=search" + + rxreq + txresp -body "OK1" + expect req.url == "/blog?id=1234&q=search" +} -start + +varnish v1 -vcl+backend { + import std; + import querymodifier; + + sub vcl_recv { + std.syslog(180, "querymodifier before: " + req.url); + set req.url = querymodifier.modifyparams(url=req.url, params="q,id", exclude_params=false); + std.syslog(180, "querymodifier after: " + req.url); + } +} -start + +client c1 { + txreq -url "/feed/?q=search" + rxresp + expect resp.status == 200 + + # This one is cached as `ts` is excluded. + txreq -url "/feed/?q=search&ts=123456789" + rxresp + expect resp.status == 200 + + txreq -url "/blog?id=1234&ts=1730210988319&v=1730210988319&date=1730210988319&q=search" + rxresp + expect resp.status == 200 +} -run + +varnish v1 -expect n_object == 2 +varnish v1 -expect cache_miss == 2 +varnish v1 -expect cache_hit == 1 diff --git a/src/vtc/missing_params.vtc b/src/vtc/missing_params.vtc new file mode 100644 index 0000000..6366cf7 --- /dev/null +++ b/src/vtc/missing_params.vtc @@ -0,0 +1,33 @@ +varnishtest "Test querymodifier vmod for empty params" + +server s1 { + rxreq + txresp -body "OK1" + expect req.url == "/feed/?q" +} -start + +varnish v1 -vcl+backend { + import std; + import querymodifier; + + sub vcl_recv { + std.syslog(180, "querymodifier before: " + req.url); + set req.url = querymodifier.modifyparams(url=req.url, params="q,id", exclude_params=false); + std.syslog(180, "querymodifier after: " + req.url); + } +} -start + +client c1 { + txreq -url "/feed/?q=¬=1" + rxresp + expect resp.status == 200 + + # This one is cached. + txreq -url "/feed/?not=1&another=2&q=" + rxresp + expect resp.status == 200 +} -run + +varnish v1 -expect n_object == 1 +varnish v1 -expect cache_miss == 1 +varnish v1 -expect cache_hit == 1 diff --git a/src/vtc/missing_url.vtc b/src/vtc/missing_url.vtc new file mode 100644 index 0000000..6131a7f --- /dev/null +++ b/src/vtc/missing_url.vtc @@ -0,0 +1,23 @@ +varnishtest "Test querymodifier vmod for missing URL" + +server s1 { + rxreq + txresp -body "OK1" +} -start + +varnish v1 -vcl+backend { + import std; + import querymodifier; + + sub vcl_hash { + std.syslog(180, "querymodifier before: " + req.url); + set req.url = querymodifier.modifyparams(url="", params="id", exclude_params=false); + std.syslog(180, "querymodifier after: " + req.url); + } +} -start + +client c1 { + txreq -url "/feed/?id=1" + rxresp + expect resp.status == 503 +} -run diff --git a/src/vtc/null_params_exclusion.vtc b/src/vtc/null_params_exclusion.vtc new file mode 100644 index 0000000..f2ed001 --- /dev/null +++ b/src/vtc/null_params_exclusion.vtc @@ -0,0 +1,28 @@ +varnishtest "Test querymodifier vmod for partial non-matches" + +server s1 { + rxreq + txresp -body "OK1" + expect req.url == "/feed/" +} -start + +varnish v1 -vcl+backend { + import std; + import querymodifier; + + sub vcl_recv { + std.syslog(180, "querymodifier before: " + req.url); + set req.url = querymodifier.modifyparams(url=req.url, params="", exclude_params=true); + std.syslog(180, "querymodifier after: " + req.url); + } +} -start + +client c1 { + txreq -url "/feed/?id=1&d=2&another=3" + rxresp + expect resp.status == 200 +} -run + +varnish v1 -expect n_object == 1 +varnish v1 -expect cache_miss == 1 +varnish v1 -expect cache_hit == 0 diff --git a/src/vtc/null_params_inclusion.vtc b/src/vtc/null_params_inclusion.vtc new file mode 100644 index 0000000..0f6dc19 --- /dev/null +++ b/src/vtc/null_params_inclusion.vtc @@ -0,0 +1,32 @@ +varnishtest "Test querymodifier vmod for partial non-matches" + +server s1 { + rxreq + txresp -body "OK1" + expect req.url == "/feed/" +} -start + +varnish v1 -vcl+backend { + import std; + import querymodifier; + + sub vcl_recv { + std.syslog(180, "querymodifier before: " + req.url); + set req.url = querymodifier.modifyparams(url=req.url, params="", exclude_params=false); + std.syslog(180, "querymodifier after: " + req.url); + } +} -start + +client c1 { + txreq -url "/feed/?id=1&d=2&another=3" + rxresp + expect resp.status == 200 + + txreq -url "/feed/" + rxresp + expect resp.status == 200 +} -run + +varnish v1 -expect n_object == 1 +varnish v1 -expect cache_miss == 1 +varnish v1 -expect cache_hit == 1 diff --git a/src/vtc/partial_match.vtc b/src/vtc/partial_match.vtc new file mode 100644 index 0000000..684d236 --- /dev/null +++ b/src/vtc/partial_match.vtc @@ -0,0 +1,28 @@ +varnishtest "Test querymodifier vmod for partial non-matches" + +server s1 { + rxreq + txresp -body "OK1" + expect req.url == "/feed/?id=1" +} -start + +varnish v1 -vcl+backend { + import std; + import querymodifier; + + sub vcl_recv { + std.syslog(180, "querymodifier before: " + req.url); + set req.url = querymodifier.modifyparams(url=req.url, params="id", exclude_params=false); + std.syslog(180, "querymodifier after: " + req.url); + } +} -start + +client c1 { + txreq -url "/feed/?id=1&d=2" + rxresp + expect resp.status == 200 +} -run + +varnish v1 -expect n_object == 1 +varnish v1 -expect cache_miss == 1 +varnish v1 -expect cache_hit == 0