├── .github └── workflows │ ├── monthly-copyright-update.yml │ ├── nightly-documentation-update.yml │ ├── nightly-package-update.yml │ ├── nightly-pr-to-main.yml │ ├── nightly-publish-main.yml │ └── nightly-submodule-update.yml ├── .gitignore ├── .gitmodules ├── 51Degrees_module └── ngx_http_51D_module.c ├── Makefile ├── README.md ├── ci ├── README.md ├── build-package-requirements.ps1 ├── build-package.ps1 ├── build-project.ps1 ├── fetch-assets.ps1 ├── get-next-package-version.ps1 ├── install-package.ps1 ├── options.json ├── publish-package.ps1 ├── run-integration-tests.ps1 ├── run-performance-tests.ps1 ├── run-unit-tests.ps1 ├── setup-environment.ps1 └── update-packages.ps1 ├── docs ├── Doxyfile └── DoxygenLayout.xml ├── examples ├── curl │ ├── Dockerfile │ ├── README.md │ ├── curl.php │ ├── curl.sh │ ├── nginx.conf │ └── run.sh ├── example.php └── hash │ ├── config.conf │ ├── gettingStarted.conf │ ├── jsExample │ ├── index.html │ └── javascript.conf │ ├── matchMetrics.conf │ ├── matchQuery.conf │ └── responseHeaders │ ├── response.html │ └── responseHeader.conf ├── module_conf └── hash_config ├── suppressions.txt ├── test.sh └── tests ├── 51degrees.t ├── examples ├── config.t ├── gettingStarted.t ├── jsExample │ ├── cdn.test.js │ ├── package-lock.json │ └── package.json ├── matchMetrics.t ├── matchQuery.t └── responseHeader.t └── performance ├── CMakeLists.txt ├── nginx.conf.template └── runPerf.sh /.github/workflows/monthly-copyright-update.yml: -------------------------------------------------------------------------------- 1 | name: Monthly Copyright Update 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # * is a special character in YAML so quote this string 7 | - cron: '0 0 1 * *' 8 | 9 | jobs: 10 | # Run the common workflow on each pull request 11 | Monthly_Copyright_Update: 12 | uses: 51Degrees/common-ci/.github/workflows/monthly-copyright-update.yml@main 13 | with: 14 | repo-name: ${{ github.event.repository.name }} 15 | secrets: 16 | token: ${{ secrets.ACCESS_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/nightly-documentation-update.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Documentation Update 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # * is a special character in YAML so quote this string 7 | - cron: '30 3 * * *' 8 | 9 | jobs: 10 | # Run the common workflow on each pull request 11 | Nightly_Documentation_Update: 12 | uses: 51Degrees/common-ci/.github/workflows/nightly-documentation-update.yml@main 13 | with: 14 | repo-name: ${{ github.event.repository.name }} 15 | secrets: 16 | token: ${{ secrets.ACCESS_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/nightly-package-update.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Package Update 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # * is a special character in YAML so quote this string 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | Nightly_Package_Update: 11 | uses: 51Degrees/common-ci/.github/workflows/nightly-package-update.yml@main 12 | with: 13 | repo-name: ${{ github.event.repository.name }} 14 | org-name: ${{ github.event.repository.owner.login }} 15 | secrets: 16 | token: ${{ secrets.ACCESS_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/nightly-pr-to-main.yml: -------------------------------------------------------------------------------- 1 | name: Nightly PRs to Main 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | dryrun: 7 | default: false 8 | type: boolean 9 | description: Dry Run 10 | workflow_run: 11 | workflows: [Nightly Submodule Update] 12 | types: [completed] 13 | 14 | jobs: 15 | # Run the common workflow on each pull request 16 | Nightly_PR_to_Main: 17 | uses: 51Degrees/common-ci/.github/workflows/nightly-prs-to-main.yml@main 18 | with: 19 | repo-name: ${{ github.event.repository.name }} 20 | org-name: ${{ github.event.repository.owner.login }} 21 | dryrun: ${{ inputs.dryrun || false }} 22 | secrets: 23 | token: ${{ secrets.ACCESS_TOKEN }} 24 | DeviceDetection: ${{ secrets.DEVICE_DETECTION_KEY }} 25 | DeviceDetectionUrl: ${{ secrets.DEVICE_DETECTION_URL }} 26 | NginxKey: ${{ secrets.NGINX_PLUS_KEY }} 27 | NginxCert: ${{ secrets.NGINX_PLUS_CERT }} 28 | NginxJwtToken: ${{ secrets.NGINX_PLUS_JWT_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/nightly-publish-main.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Publish Main 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_run: 6 | workflows: [Nightly PRs to Main] 7 | types: [completed] 8 | 9 | jobs: 10 | # Run the common workflow to test and publish the main branch 11 | Nightly_Publish_Main: 12 | uses: 51Degrees/common-ci/.github/workflows/nightly-publish-main.yml@main 13 | with: 14 | repo-name: ${{ github.event.repository.name }} 15 | build-platform: ubuntu-latest 16 | secrets: 17 | token: ${{ secrets.ACCESS_TOKEN }} 18 | DeviceDetection: ${{ secrets.DEVICE_DETECTION_KEY }} 19 | DeviceDetectionUrl: ${{ secrets.DEVICE_DETECTION_URL }} 20 | NginxKey: ${{ secrets.NGINX_PLUS_KEY }} 21 | NginxCert: ${{ secrets.NGINX_PLUS_CERT }} 22 | NginxJwtToken: ${{ secrets.NGINX_PLUS_JWT_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/nightly-submodule-update.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Submodule Update 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # * is a special character in YAML so quote this string 7 | - cron: '0 2 * * *' 8 | 9 | jobs: 10 | # Run the common workflow on each pull request 11 | Nightly_Submodule_Update: 12 | uses: 51Degrees/common-ci/.github/workflows/nightly-submodule-update.yml@main 13 | with: 14 | repo-name: ${{ github.event.repository.name }} 15 | secrets: 16 | token: ${{ secrets.ACCESS_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | vendor/ 3 | nginx -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "device-detection-cxx"] 2 | path = device-detection-cxx 3 | url=../device-detection-cxx 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifndef FIFTYONEDEGREES_NGINX_VERSION 2 | VERSION := 1.20.0 3 | else 4 | VERSION := $(FIFTYONEDEGREES_NGINX_VERSION) 5 | endif 6 | 7 | ifndef DONT_CLEAN_TESTS 8 | CLEANTESTS := 1 9 | else 10 | CLEANTESTS := 0 11 | endif 12 | 13 | TESTS := tests/51degrees.t 14 | 15 | 16 | $(eval $(API):;@:) 17 | 18 | ifneq (test,$(firstword $(MAKECMDGOALS))) 19 | API := hash 20 | ARGS := -std=gnu11 -Wall -Wno-unused-variable -Wno-missing-braces 21 | endif 22 | 23 | 24 | FULLPATH := $(shell pwd) 25 | ifndef FIFTYONEDEGREES_DATAFILE 26 | DATAFILE := 51Degrees-LiteV4.1.hash 27 | else 28 | DATAFILE := $(FIFTYONEDEGREES_DATAFILE) 29 | endif 30 | 31 | MODULEPATH := $(FULLPATH)/build/modules/ngx_http_51D_module.so 32 | FILEPATH := $(FULLPATH)/device-detection-cxx/device-detection-data/$(DATAFILE) 33 | 34 | 35 | ifndef STATIC_BUILD 36 | MODULE_ARG := --add-dynamic-module 37 | else 38 | MODULE_ARG := --add-module 39 | endif 40 | 41 | .PHONY hash: 42 | 43 | clean: 44 | if [ -d "51Degrees_module/src" ]; then \ 45 | rm -rf 51Degrees_module/src; \ 46 | fi 47 | if [ -d "build" ]; then \ 48 | rm -rf build; \ 49 | fi 50 | if [ -f "nginx" ]; then \ 51 | rm -f nginx; \ 52 | fi 53 | if [ -f "vendor/nginx-$(VERSION)/Makefile" ]; then \ 54 | cd $(CURDIR)/vendor/nginx-$(VERSION) && make clean; \ 55 | fi 56 | if [ "$(CLEANTESTS)" -eq "1" ]; then \ 57 | if [ -d "tests/nginx-tests" ]; then \ 58 | rm -r tests/nginx-tests; \ 59 | fi; \ 60 | fi; 61 | 62 | build: clean 63 | mkdir -p 51Degrees_module/src/hash 64 | mkdir -p 51Degrees_module/src/common-cxx 65 | exit 66 | cp module_conf/$(API)_config 51Degrees_module/config 67 | cp device-detection-cxx/src/*.c 51Degrees_module/src/ 68 | cp device-detection-cxx/src/*.h 51Degrees_module/src/ 69 | cp device-detection-cxx/src/hash/*.c 51Degrees_module/src/hash/ 70 | cp device-detection-cxx/src/hash/*.h 51Degrees_module/src/hash/ 71 | cp device-detection-cxx/src/common-cxx/*.c 51Degrees_module/src/common-cxx/ 72 | cp device-detection-cxx/src/common-cxx/*.h 51Degrees_module/src/common-cxx/ 73 | 74 | 75 | get-source: 76 | if [ ! -d "vendor" ]; then mkdir vendor; fi 77 | cd vendor && curl -L -O "http://nginx.org/download/nginx-$(VERSION).tar.gz" 78 | cd vendor && tar xzf "nginx-$(VERSION).tar.gz" 79 | 80 | configure: build 81 | if [ ! -d "vendor/nginx-$(VERSION)" ]; then $(MAKE) get-source; fi 82 | cd $(CURDIR)/vendor/nginx-$(VERSION) && \ 83 | ./configure \ 84 | --prefix=$(CURDIR)/build \ 85 | --with-ld-opt="-lm -latomic $(MEM_LD_FLAGS)" \ 86 | $(MODULE_ARG)=$(CURDIR)/51Degrees_module \ 87 | --with-compat \ 88 | --with-cc-opt="$(ARGS) $(MEM_CC_FLAGS)" \ 89 | --with-debug \ 90 | --sbin-path=$(CURDIR) \ 91 | --conf-path="nginx.conf" \ 92 | --with-http_sub_module 93 | 94 | configure-no-module: clean 95 | if [ ! -d "vendor/nginx-$(VERSION)" ]; then $(MAKE) get-source; fi 96 | cd $(CURDIR)/vendor/nginx-$(VERSION) && \ 97 | ./configure \ 98 | --prefix=$(CURDIR)/build \ 99 | --with-compat \ 100 | --with-debug \ 101 | --sbin-path=$(CURDIR) \ 102 | --conf-path="nginx.conf" \ 103 | --with-http_sub_module 104 | 105 | install: configure 106 | cd $(CURDIR)/vendor/nginx-$(VERSION) && make install 107 | sed "/\/\*\*/,/\*\//d" examples/$(API)/gettingStarted.conf > build/nginx.conf 108 | sed -i "s!%%DAEMON_MODE%%!on!g" build/nginx.conf 109 | sed -i "s!%%MODULE_PATH%%!!g" build/nginx.conf 110 | sed -i "s!%%FILE_PATH%%!$(FILEPATH)!g" build/nginx.conf 111 | sed -i "s!%%TEST_GLOBALS%%!!g" build/nginx.conf 112 | sed -i "s!%%TEST_GLOBALS_HTTP%%!!g" build/nginx.conf 113 | echo > build/html/$(API) 114 | 115 | install-no-module: configure-no-module 116 | cd $(CURDIR)/vendor/nginx-$(VERSION) && make install 117 | 118 | module: configure 119 | cd $(CURDIR)/vendor/nginx-$(VERSION) && make modules 120 | if [ ! -d "build/modules" ]; then mkdir -p build/modules; fi 121 | cp $(CURDIR)/vendor/nginx-$(VERSION)/objs/*.so build/modules 122 | 123 | all-versions: 124 | mkdir modules 125 | $(foreach version, \ 126 | 1.19.0 1.19.5 1.19.8 1.20.0, \ 127 | $(MAKE) module VERSION=$(version); \ 128 | mv build/modules/ngx_http_51D_module.so modules/ngx_http_51D_hash_module-$(version).so;) 129 | 130 | set-mem: 131 | $(eval MEM_CC_FLAGS := -O0 -g -fsanitize=address) 132 | $(eval MEM_LD_FLAGS := -g -fsanitize=address) 133 | 134 | mem-check: set-mem install 135 | 136 | test-prep: 137 | if [ ! -f "tests/nginx-tests/lib/Test/Nginx.pm" ]; then \ 138 | rm -rf tests/nginx-tests; mkdir -p tests/nginx-tests; \ 139 | curl -fL "https://github.com/nginx/nginx-tests/archive/refs/heads/master.tar.gz" \ 140 | | tar -xzC tests/nginx-tests --strip-components 1; \ 141 | fi; 142 | 143 | test-full: 144 | $(MAKE) test TESTS="$(TESTS) tests/nginx-tests" 145 | 146 | test-examples: test-prep 147 | ifeq (,$(wildcard $(FULLPATH)/nginx)) 148 | $(error Local binary must be built first (use "make install")) 149 | else 150 | $(eval CMD := TEST_NGINX_BINARY="$(FULLPATH)/nginx" TEST_MODULE_PATH="$(FULLPATH)/build/" TEST_FILE_PATH="$(FILEPATH)" ASAN_OPTIONS=detect_odr_violation=0 LSAN_OPTIONS=suppressions=suppressions.txt prove $(FIFTYONEDEGREES_FORMATTER) -v tests/examples :: $(DATAFILE)) 151 | ifdef FIFTYONEDEGREES_TEST_OUTPUT 152 | $(CMD) > $(FIFTYONEDEGREES_TEST_OUTPUT) 153 | else 154 | $(CMD) 155 | endif 156 | endif 157 | 158 | test: test-prep 159 | ifeq (,$(wildcard $(FULLPATH)/nginx)) 160 | $(error Local binary must be built first (use "make install")) 161 | else 162 | $(eval CMD := TEST_NGINX_BINARY="$(FULLPATH)/nginx" TEST_NGINX_GLOBALS="load_module $(MODULEPATH);" TEST_NGINX_GLOBALS_HTTP="51D_file_path $(FILEPATH);" ASAN_OPTIONS=detect_odr_violation=0 LSAN_OPTIONS=suppressions=suppressions.txt prove $(FIFTYONEDEGREES_FORMATTER) -v $(TESTS) :: $(DATAFILE)) 163 | ifdef FIFTYONEDEGREES_TEST_OUTPUT 164 | $(CMD) > $(FIFTYONEDEGREES_TEST_OUTPUT) 165 | else 166 | $(CMD) 167 | endif 168 | endif 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 51Degrees Device Detection Nginx Module 2 | 3 | ![51Degrees](https://51degrees.com/img/logo.png?utm_source=github&utm_medium=repository&utm_campaign=nginx_open_source&utm_content=readme_main "Data rewards the curious") **Device Detection in C** Nginx module 4 | 5 | [Developer documentation](https://51degrees.com/documentation/_other_integrations__nginx.html) 6 | 7 | # Introduction 8 | This project integrates 51Degrees Device Detection V4 engine as a module to Nginx, allowing users to configure Nginx to perform device detection by setting corresponding 51Degrees directives in the Nginx configuration file. Currently only Linux platform is supported. 9 | 10 | # Getting Started 11 | Before following the quick start, make sure to have Nginx installed and have a 51Degrees data file ready to use. The Nginx executable can be installed and built as instructed in the [Build and Test](#build-and-test) section. 12 | 13 | ## Data File 14 | 15 | In order to perform device detection, you will need to use a 51Degrees data file. 16 | This repository includes a free, 'lite' file in the 'device-detection-data' 17 | sub-module that has a significantly reduced set of properties. To obtain a 18 | file with a more complete set of device properties see the 19 | [51Degrees website](https://51degrees.com/pricing). 20 | If you want to use the lite file, you will need to install [GitLFS](https://git-lfs.github.com/). 21 | 22 | For Linux: 23 | ``` 24 | sudo apt-get install git-lfs 25 | git lfs install 26 | ``` 27 | 28 | Then, navigate to the `device-detection-cxx/device-detection-data` directory and execute: 29 | 30 | ``` 31 | git lfs pull 32 | ``` 33 | 34 | ## Quick Start 35 | 36 | To quickly start using 51Degrees module with Nginx, please follow the below steps: 37 | - Install an Nginx version 1.19.0 or above. 38 | - Obtain a 51Degrees data file. A Lite version is available at [Github](https://github.com/51Degrees/device-detection-data). 39 | - Download the pre-built binaries at our [Github release page](https://github.com/51Degrees/device-detection-nginx/releases). 40 | - Choose the binary for the version of Nginx that you want to use. 41 | - Create a folder which we will use for this quick start. 42 | - In the folder: 43 | - Create a Nginx configuration file and paste the following content, replacing the path in `load_module` and `51D_file_path` with the actual path to the downloaded module and the downloaded data file. 44 | ``` 45 | # Specify the module to load. 46 | load_module [path to 51Degrees module]; 47 | 48 | events { 49 | } 50 | 51 | http { 52 | # Specify the data file to use 53 | 51D_file_path [path to 51Degrees data file]; 54 | server { 55 | listen 8888; 56 | 57 | location /test { 58 | # Perform a detection using a single User-Agent header. 59 | # Set the request header 'x-mobile' to returned value for property 'IsMobile'. 60 | 51D_match_ua x-mobile IsMobile; 61 | 62 | # Set the response header 'x-mobile' using the request header 'x-mobile'. 63 | add_header x-mobile $http_x_mobile; 64 | } 65 | } 66 | } 67 | ``` 68 | - Create a folder named `html` and an empty file `test` in that folder. This is because we are serving static content in the above config file and the root directory to serve requests is defaulted at `html`. 69 | - Create a folder named `logs` for nginx to write its log messages to. 70 | - In the quick start folder, start Nginx using the following command: 71 | ``` 72 | nginx -p . -c nginx.conf 73 | ``` 74 | - The `-p` flag tells Nginx to set the prefix path which Nginx will use. 75 | - The `-c` flag tells Nginx to use the config file that we just created. 76 | - Install curl if you haven't. 77 | - Fire a request and see the result being sent back in the response using the following command: 78 | ``` 79 | curl -H 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53' http://localhost:8888/test -I 80 | ``` 81 | - You should see the `x-mobile: True` in the response header, indicating that Nginx has successfully used 51Degrees Device Detection engine to detect that client user agent is a mobile device using the input 'User-Agent' header. 82 | - At this point you can terminate the Nginx server using: 83 | ``` 84 | nginx -p . -s quit 85 | ``` 86 | 87 | The above gave you a quick overview of how to use Nginx 51Degrees Device Detection V4 module. However, that used a pre-built binaries of 51Degrees module, and in some cases, it might not work on your target machine due to the differences in the build environment that was used. In those cases, you will have options to rebuild the module yourself. This is detailed in the [Build and Test](#build-and-test) section. 88 | 89 | ## Latest releases 90 | The latest releases of 51Degrees Nginx module can be found on [Github](https://github.com/51Degrees/device-detection-nginx/releases). 91 | 92 | For the supported platforms and Nginx version, please check the [tested versions page](https://51degrees.com/documentation/_info__tested_versions.html) for latest update. At the time of writing, the followings are supported: 93 | - Platform Ubuntu-18.04 and 20.04, 64bit. 94 | - Nginx version: 1.19.0, 1.19.5, 1.19.10 and 1.20.0 95 | 96 | ## API references 97 | Below is the list of directives that can be used with 51Degrees Device Detection V4 module. 98 | 99 | |Directives| 100 | |---------| 101 | |Syntax: `51D_file_path` *filename*;
Default: ---
Context: main
Specify the data file to used for 51Degrees Device Detection V4 engine| 102 | |Syntax: `51D_drift` *drift*;
Default: 51D_drift 0;
Context: main
Specify the drift value that a detection can allow.| 103 | |Syntax: `51D_difference` *difference*;
Default: 51D_difference 0;
Context: main
Specify the difference value that a detection can allow.| 104 | |Syntax: `51D_allow_unmatched` *on \| off*;
Default: 51D_allow_unmatched off;
Context: main
Specify if unmatched should be allowed.| 105 | |**DEPRECATED** Syntax: `51D_use_performance_graph` *on \| off*;
Default: 51D_use_performance_graph off;
Context: main
Specify if performance graph should be used in detection. **DEPRECATED**: Has no effect on configuration, the data file has a single (predictive) graph that is always used.| 106 | |**DEPRECATED** Syntax: `51D_use_predictive_graph` *on \| off*;
Default: 51D_use_predictive_graph on;
Context: main
Specify if predictive graph should be used in detection. **DEPRECATED**: Has no effect on configuration, the data file has a single graph that is always used.| 107 | |Syntax: `51D_value_separator` *separator*;
Default: 51D_value_separator ',';
Context: main
Specify the separator to be used in the value string returned from a detection. Each value in the returned result string is correspond to a requested property.| 108 | |Syntax: `51D_match_ua` *header* *properties* \[*argument*\];
Default: ---
Context: main, server, `location` (**NOTE**: This directive can be used in main, server and location blocks. Specified properties are aggregated and eventually queried in the location. *header* value is set after the query is performed and is only available within `location` block)
Perform a detection using a single request header `User-Agent`. *header* specifies which request header the returned *properties* values should be stored at. *properties* is a comma separated list string. *argument* specifies if a `User-Agent` is supplied as a query argument. This will override the value in the `User-Agent` header. The *argument* is optional.
If a property is not available for any reason, the value being returned for that property will be `NA`
This directive was previously known as `51D_match_single` (name deprecated)| 109 | |Syntax: `51D_match_ua_client_hints` *header* *properties* \[*argument*\];
Default: ---
Context: main, server, `location` (**NOTE**: This directive can be used in main, server and location blocks. Specified properties are aggregated and eventually queried in the location. *header* value is set after the query is performed and is only available within `location` block)
Perform a detection using request headers `User-Agent` and `Sec-CH-UA-*`. *header* specifies which request header the returned *properties* values should be stored at. *properties* is a comma separated list string. *argument* specifies if a `User-Agent` is supplied as a query argument. This will override the value in the `User-Agent` header. The *argument* is optional.
If a property is not available for any reason, the value being returned for that property will be `NA`| 110 | |Syntax: `51D_match_all` *header* *properties*;
Default: ---
Context: main, server, `location` (**NOTE**: This directive can be used in main, server and location blocks. Specified properties are aggregated and eventually queried in the location. *header* value is set after the query is performed and is only available within `location` block)
Perform a detection using all headers, query argument and cookie from a http request. *header* specifies which request header the returned *properties* values should be stored at. *properties* is a comma separated list string.
If a property is not available for any reason, the value being returned for that property will be `NA`| 111 | |Syntax: `51D_get_javascript_single` *javascript_property* \[*argument*\];
Default: ---
Context: location
Perform a detection using a single request header `User-Agent`. The returned value of *javascript_property* is set in the response body. This works in a similar way as CDN to serve static content. *argument* specifies if a `User-Agent` is supplied as a query argument. This will override the value in the `User-Agent` header. The *argument* is optional.
If the Javascript property is not available for any reason, a Javascript block comment will be returned so that it will not cause syntax error when the client executes it.
The whole response body is used for the returned content so only one of these directives can be used in a single location block. Also, since the static content does not actually exist as a static file, the nginx http core module will log an error, so it is recommended to use this directive with [log_not_found](http://nginx.org/en/docs/http/ngx_http_core_module.html#log_not_found) set to off.| 112 | |Syntax: `51D_get_javascript_all` *javascript_property*;
Default: ---
Context: location
Perform a detection using all headers, cookie and query arguments from a http request. The returned value of the *javascript_property* is set in the response body. This works in a similar way as CDN to serve static content.
If the Javascript property is not available for any reason, a Javascript block comment will be returned so that it will not cause syntax error when the client executes it.
The whole response body is used for the returned content so only one of these directives can be used in a single location block. Also, since the static content does not actually exist as a static file, the nginx http core module will log an error, so it is recommended to use this directive with [log_not_found](http://nginx.org/en/docs/http/ngx_http_core_module.html#log_not_found) set to off.| 113 | |Syntax: `51D_set_resp_headers` *on \| off*;
Default: 51D_set_resp_headers off
Context: main, server, location
Allow Client Hints to be set in response headers where it is applicable to the user agent (e.g. Chrome 89 or above) so that more evidence can be returned in subsequent requests, allowing more accurate detection. Value set in a block overwrites values set in precedent blocks (e.g. value set in `location` block will overwrite value set in `server` and `main` blocks). This will only be available from the 4.3.0 version onwards.| 114 | 115 | ## Proxy Passing 116 | When using the `proxy_pass` directive in a location block where a match directive is used, the properties selected are passed as additional HTTP headers with the name specified in the first argument of `51D_match_ua`/`51D_match_ua_client_hints`/`51D_match_all`. 117 | 118 | ## Fast-CGI 119 | Using `include fastcgi_params;` makes these additional headers available via the `$_SERVER` variable. 120 | 121 | ## Match Metrics 122 | Other than detection properties, users can request further metrics of a match in the same way. The available metrics are Drift, Difference, Method, MatchedNodes, User-Agents and DeviceId. 123 | e.g. 124 | ``` 125 | 51D_match_all x-metrics Drift,Difference,Method,MatchedNodes,DeviceId,UserAgents 126 | ``` 127 | 128 | ## Output Format 129 | The value of the header is set to a comma separated list of values (comma delimited is the default behaviour, but the delimiter can be set explicitly with `51D_value_separator`), these are in the same order the properties are listed in the config file. So setting a header with the line: 130 | ``` 131 | 51D_match_all x-device HardwareName,BrowserName,PlatformName; 132 | ``` 133 | will give a header named `x-device` with a value like `Desktop,Firefox,Ubuntu`. Alternatively, headers can be set individually like: 134 | ``` 135 | 51D_match_all x-hardware HardwareName; 136 | 51D_match_all x-browser BrowserName; 137 | 51D_match_all x-platform PlatformName; 138 | ``` 139 | giving three separate headers. 140 | 141 | ## Examples 142 | All examples are located in the `examples` folder: 143 | |Example|Description| 144 | |-------|-----------| 145 | |gettingStarted.conf|Shows a simple instance of how to use 51D_match_ua, 51D_match_ua_client_hints and 51D_match_all in a configuration file.| 146 | |config.conf|Shows how to configure 51Degrees detection using directives such as 51D_drift, 51D_difference, etc...| 147 | |matchQuery.conf|Shows how to perform detection using input from http request query argument| 148 | |matchMetrics.conf|Shows how to obtain other match metrics of the detection such as drift, difference, method and etc...| 149 | |responseHeaders|Shows how to enable Client Hints support to request further evidence from user agent to provide more accurate detection. This will only be available from the 4.3.0 version onwards.| 150 | |jsExample|Shows how to use 51D_get_javascript* directive to get the Javascript to obtain further evidences from the client. To run this example, start Nginx with the included `javascript.conf` set as the configuration file. Once Nginx fully start, open `index.html` in a browser to see that the screen width is now set correctly.| 151 |
152 | 153 | To run these examples, follow the instructions detailed in each example. 154 | 155 | # Build and Test (Linux) 156 | Before build and test the 51Degrees Device Detection V4 module, make sure to check out the project and all of its sub-modules. 157 | 158 | ## Fetching sub-modules 159 | 160 | This repository has sub-modules that must be fetched. 161 | If cloning for the first time, use: 162 | 163 | ``` 164 | git clone --recurse-submodules https://github.com/51Degrees/device-detection-nginx.git 165 | ``` 166 | 167 | If you have already cloned the repository and want to fetch the sub modules, use: 168 | 169 | ``` 170 | git submodule update --init --recursive 171 | ``` 172 | 173 | If you have downloaded this repository as a zip file then these sub modules need to be downloaded separately as GitHub does not include them in the archive. In particular, note that the zip download will contain links to LFS content, rather than the files themselves. As such, these need to be downloaded individually. 174 | 175 | ## Build 176 | 177 | To build 51Degrees Device Detection V4 module, the following libraries are required. 178 | - C compiler that support C11 or above. 179 | - make 180 | - zlib1g-dev 181 | - libpcre3 182 | - libpcre3-dev 183 | - libatomic 184 | 185 | To build the module only, run the following command. This will output to `ngx_http_51D_module.so` in `build/modules` directory. 186 | ``` 187 | make module 188 | ``` 189 |
190 | 191 | To build the module and Nginx, run the following command. 192 | ``` 193 | make install 194 | ``` 195 | By default this will use the latest version of Nginx specified in the Makefile. To use a specific version of Nginx, run the above command with `FIFTYONEDEGREES_NGINX_VERSION` variable. e.g. 196 | ``` 197 | make install FIFTYONEDEGREES_NGINX_VERSION=[Version] 198 | ``` 199 |
200 | 201 | To build and link the module statically with Nginx, use `STATIC_BUILD` variable. e.g. 202 | ``` 203 | make install STATIC_BUILD=1 204 | ``` 205 |
206 | 207 | The Nginx executable will be output to the project root directory. The prefix path of the Nginx will be set to `build` directory. A `nginx.conf` is also included with a simple example of `51D_match_ua`, `51D_match_ua_client_hints` and `51D_match_all` directives, set in a static content location. The build process has created the static file in the `build/html` folder so you can start Nginx and start sending requests to that static content endpoint. 208 | 209 | ## Test 210 | All tests are allocated in the `tests` folder. 211 | 212 | To be able to test the 51Degrees module the following libraries are required. 213 | - Perl 5 and prove Test::Harness 214 | - CMake 10 or above 215 | - node and jest 216 | - curl 217 | - grep 218 | 219 | Before running any test, make sure to build the project with `make install` command. The tests are designed to test the dynamic build only so `STATIC_BUILD` will not work. 220 | 221 | To run the 51Degrees tests with the default 51Degrees Lite data file included in the `device-detection-cxx\device-detection-data` directory, run the following command: 222 | ``` 223 | make test 224 | ``` 225 | 226 | To run the 51Degrees together with the Nginx test suite, run the following command: 227 | ``` 228 | make test-full 229 | ``` 230 | 231 | These do not run all the required tests as some tests requires properties that are not supported with the Lite version of the data file. To run all tests, obtain a data file with support properties JavascriptHardwareProfile,ScreenPixelsWidthJavascript and ScreenPixelsWidth. Then, use the `FIFTYONEDEGREES_DATAFILE` variable to specify the file name to run the tests with. The new data file should be placed in the `device-detection-cxx\device-detection-data` folder and should have different name to `51Degrees-LiteV4.1.hash`. If tests still fail, obtain data file with any other properties used in the tests. Below is an example of running tests with different data file: 232 | ``` 233 | make [test|test-full] FIFTYONEDEGREES_DATAFILE=51Degrees-EnterpriseV4.1.hash 234 | ``` 235 | 236 | ## Performance Test 237 | 238 | The project also include a performance test suite. This required `CMake 10` or above to be installed. 239 | 240 | To run the performance test, please follow the below steps: 241 | - Make sure to build the project with `make install`. 242 | - Navigate to the `tests/performance` directory. 243 | - Create a `build` folder and navigate to it. 244 | - Run: 245 | ``` 246 | cmake .. 247 | cmake --build . 248 | ./runPerf.sh 249 | ``` 250 | - This step will build our internal version of `Apache Benchmark` and use it for performance testing. 251 | 252 | # For Developer 253 | 254 | ## Build Options 255 | Run the following to build all current supported versions in `modules` folder: 256 | ``` 257 | make all-versions 258 | ``` 259 | 260 | ## Test Options 261 | There are number of test options that are recommended for development only. 262 | |Build Option|Description| 263 | |------------|-----------| 264 | |FIFTYONEDEGREES_FORMATTER|This is only available when running with `test` or `test-full` target. This allow to specify the formatter that can be use to format the test report. e.g. `make test FIFTYONEDEGREES_FORMATTER=--formatter TAP::Formatter::Junit`. This example will output a JUnit report and requires the module TAP::Formatter::JUnit to be installed. `cpan` and `cpanm` are recommended to install this module.| 265 | |FIFTYONEDEGREES_TEST_OUTPUT| This specifies the file that the test report should be written to. e.g. `make test FIFTYONEDEGREES_TEST_OUTPUT=test.xml`| 266 | 267 | ## Memory Check 268 | The project can be built with address sanitizer enabled. To do so, run: 269 | ``` 270 | make mem-check 271 | ``` 272 | The build options mentioned in the [Build](#build) section are also applicable to this `mem-check` rule. However, it has been observed that, in some version of Nginx such 1.19.0 or 1.20.0, the address sanitizer will report memory leak in Nginx without 51Degrees module loaded. Thus, when develop project, make sure that the error is confirmed to not be generated from the 51Degrees module. 273 | 274 | ## Stress Test 275 | The project also include a stress test. This test is inherited from V3 version of 51Degrees project. This test can be run to compared the performance against V3 version and to see how the 51Degrees module perform under heavy load. Note that, in V4 we have constructed a slightly different approach to performance testing so the output from this test is only used as a reference to compare with V3 version. For actually performance test, please follow the instructions in [Performance Test](#performance-test) section. 276 | 277 | To run the stress test, Make sure that the project is built with `make install` and the Apache Benchmark is built as described in the [Performance Test](#performance-test) section, without running `runPerf.sh` script. Then, run the `test.sh` script. 278 | ``` 279 | ./test.sh 280 | ``` 281 | 282 | ## Examples Test 283 | Before running the tests for the example, make sure to obtain a data file that contains all required properties in the examples. 284 | 285 | Run the example tests with the obtained data file: 286 | ``` 287 | make test-examples FIFTYONEDEGREES_DATAFILE=[Obtained data file] 288 | ``` 289 | 290 | ### Javascript and overrides example test 291 | The Javascript and overrides example test is designed to be run separately with Selenium using NodeJs. The test is located in `tests/examples/jsExample` directory. To run the test, follow the below steps: 292 | - Make sure to have Chrome, Firefox and Edge installed. 293 | - Download and install their drivers. 294 | - Ensure the browser versions and the drivers versions are aligned. 295 | - Navigate to the directory `tests/examples/jsExample` and run: 296 | ``` 297 | npm install 298 | ``` 299 | - This will installed all required package including the Selenium web driver. 300 | - Start Nginx with the `javascript.conf` as described in the file itself. 301 | - Run the test 302 | ``` 303 | npm test 304 | ``` 305 | - Result is output to a file called `cdn_test_results.xml`. 306 | - Stop the Nginx. 307 | 308 | ## Re-certify 51Degrees Device Detection module 309 | When a new release is made to 51Degrees Device Detection module, module maintainer will need to re-certify it the Nginx Plus for target releases. 310 | 311 | To re-certify, follow the "Partner Build, Functional Testing, and Self Certification Process" in the documentation [here](https://www.nginx.com/partners/certified-module-program-documentation/) then submit your results to Nginx to be reviewed by their engineering team. 312 | 313 | Make sure to use the latest versions of [NGINX OSS & Plus](https://docs.nginx.com/nginx/releases/) for re-certification. 314 | 315 | A NGINX Plus dev license might be required for testing. 316 | -------------------------------------------------------------------------------- /ci/README.md: -------------------------------------------------------------------------------- 1 | # API Specific CI Approach 2 | 3 | For the general CI Approach, see [common-ci](https://github.com/51degrees/common-ci). 4 | 5 | The following secrets are required: 6 | * `ACCESS_TOKEN` - GitHub [access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#about-personal-access-tokens) for cloning repos, creating PRs, etc. 7 | * Example: `github_pat_l0ng_r4nd0m_s7r1ng` 8 | 9 | The following secrets are required to run tests: 10 | * `DEVICE_DETECTION_KEY` - [license key](https://51degrees.com/pricing) for downloading assets (TAC hashes file and TAC CSV data file) 11 | * Example: `V3RYL0NGR4ND0M57R1NG` 12 | 13 | The following secrets are required to run the full set of tests agains NGINX Plus: 14 | * `NGINX_PLUS_KEY` - Signing key used for installing NGINX Plus 15 | * `NGINX_PLUS_CERT` - NGINX Plus certificate 16 | 17 | ### Differences 18 | - There are no packages produced by this repository, so the only output from the `Nightly Publish Main` workflow is a new tag and release. 19 | - The package update step does not update dependencies from a package manager in the same way as other repos. Instead it checks the supported versions on the [NGINX Plus Releases](https://docs.nginx.com/nginx/releases/) page, and uses that to update `options.json` to ensure they are tested. 20 | - As Windows is not supported, the scripts here can be a little less strict with implementation. e.g. using `cp` rather than `Copy-Item` as we know we're in a Linux environment. 21 | 22 | ### Build Options 23 | 24 | The additional build options for this repo are: 25 | | Option | Type | Default | Purpose | 26 | | ------ | --------- | ---- | ------- | 27 | | `NginxVersion` | string | `1.21.3` | The version of the opensource NGINX code to use for building and testing. | 28 | | `MemCheck` | bool | `false` | If true the binaries are built in Debug, with memory checks enabled. | 29 | | `BuildMethod` | string | `dynamic` | Can be either `static` or `dynamic` to determine how the nginx module is built | 30 | | `FullTests` | bool | `false` | If true, then the full NGINX test suite is run. This should be set to true for the latest NGINX Plus version. | 31 | 32 | ## Prerequisites 33 | 34 | In addition to the [common prerequisites](https://github.com/51Degrees/common-ci#prerequisites), the following environment variables are required: 35 | - DEVICE_DETECTION_KEY - License key to download a data file for testing 36 | - DEVICE_DETECTION_URL - Url to download the data file from (optional) 37 | - NGINX_PLUS_KEY - Signing key used for installing NGINX Plus 38 | - NGINX_PLUT_CERT - NGINX Plus certificate 39 | -------------------------------------------------------------------------------- /ci/build-package-requirements.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory=$true)] 3 | [string]$RepoName, 4 | [string]$Name, 5 | [string]$NginxVersion 6 | ) 7 | 8 | $ModuleName = "ngx_http_51D_module.so" 9 | $RepoPath = [IO.Path]::Combine($pwd, $RepoName) 10 | $OutputDir = [IO.Path]::Combine($pwd, "package-files") 11 | $OutputFile = [IO.Path]::Combine($OutputDir, $ModuleName) 12 | 13 | Write-Output "Entering '$RepoPath'" 14 | Push-Location $RepoPath 15 | 16 | try { 17 | 18 | make install FIFTYONEDEGREES_NGINX_VERSION=$NginxVersion 19 | 20 | # Create a directory for binary files from which they will be uploaded 21 | # as artifacts. 22 | New-Item -Path $OutputDir -ItemType Directory -Force 23 | 24 | $InputFile = [IO.Path]::Combine($RepoPath, "build", "modules", $ModuleName) 25 | 26 | # Copy module 27 | Copy-Item -Path $InputFile -Destination $OutputFile 28 | 29 | } 30 | finally { 31 | 32 | Write-Output "Leaving '$RepoPath'" 33 | Pop-Location 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ci/build-package.ps1: -------------------------------------------------------------------------------- 1 | 2 | param( 3 | [Parameter(Mandatory=$true)] 4 | [string]$RepoName 5 | ) 6 | 7 | 8 | 9 | # Create a directory for binary files from which they will be uploaded 10 | # as artifacts. 11 | New-Item -path "package" -ItemType Directory -Force 12 | 13 | # Copy all modules 14 | Copy-Item -r "package-files/*" "package/" 15 | -------------------------------------------------------------------------------- /ci/build-project.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory=$true)] 3 | [string]$RepoName, 4 | [string]$Name = "1.21.3_dynamic", 5 | [string]$NginxVersion = "1.21.3", 6 | [string]$BuildMethod = "dynamic", 7 | [bool]$MemCheck = $False 8 | ) 9 | 10 | $RepoPath = [IO.Path]::Combine($pwd, $RepoName) 11 | 12 | Write-Output "Entering '$RepoPath'" 13 | Push-Location $RepoPath 14 | 15 | try { 16 | 17 | # The default command is install 18 | $InstallCommand = "install" 19 | # Set the base build arguments to be added to 20 | $BuildArgs = @("FIFTYONEDEGREES_NGINX_VERSION=$NginxVersion", "FIFTYONEDEGREES_DATAFILE=TAC-HashV41.hash") 21 | 22 | if ($BuildType -eq "static") { 23 | # Add static to the build flags if this configuration is for a static build 24 | Write-Output "Configuring build for a static module" 25 | $BuildArgs += "STATIC_BUILD=1" 26 | } 27 | 28 | if ($MemCheck -eq $True) { 29 | # If this configuration requires a memory check, use mem-check instead of install 30 | Write-Output "Configuring build memomry checks" 31 | $InstallCommand = "mem-check" 32 | } 33 | 34 | Write-Output "Building module" 35 | & make $InstallCommand $BuildArgs 36 | 37 | } 38 | finally { 39 | 40 | Write-Output "Leaving '$RepoPath'" 41 | Pop-Location 42 | 43 | } -------------------------------------------------------------------------------- /ci/fetch-assets.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory=$true)] 3 | [string]$RepoName, 4 | [Parameter(Mandatory=$true)] 5 | [string]$DeviceDetection, 6 | [string]$DeviceDetectionUrl, 7 | [Parameter(Mandatory=$true)] 8 | [string]$NginxKey, 9 | [Parameter(Mandatory=$true)] 10 | [string]$NginxCert, 11 | [Parameter(Mandatory=$true)] 12 | [string]$NginxJwtToken 13 | ) 14 | 15 | $RepoPath = [IO.Path]::Combine($pwd, $RepoName) 16 | 17 | # Get the TAC data file for testing 18 | Write-Output "Downloading Hash data file" 19 | ./steps/fetch-hash-assets.ps1 -RepoName $RepoName -LicenseKey $DeviceDetection -Url $DeviceDetectionUrl 20 | 21 | # And move it to the correct directory 22 | Write-Output "Moving Hash data file" 23 | Move-Item $RepoPath/TAC-HashV41.hash $RepoPath/device-detection-cxx/device-detection-data/TAC-HashV41.hash 24 | 25 | # Write the key and certificate files for NGINX Plus so it can be installed. 26 | Write-Output "Writing NGINX Plus repo key" 27 | Write-Output $NginxKey >> $RepoPath/nginx-repo.key 28 | Write-Output "Writing NGINX Plus repo certificate" 29 | Write-Output $NginxCert >> $RepoPath/nginx-repo.crt 30 | Write-Output "Writing NGINX Plus Jwt token" 31 | Write-Output $NginxJwtToken >> $RepoPath/license.jwt 32 | 33 | # Pull the evidence files for testing as they are not by default. 34 | $DataFileDir = [IO.Path]::Combine($RepoPath, "device-detection-cxx", "device-detection-data") 35 | Push-Location $DataFileDir 36 | try { 37 | Write-Output "Pulling evidence files" 38 | git lfs pull -I "*.csv" 39 | git lfs pull -I "*.yml" 40 | } 41 | finally { 42 | Pop-Location 43 | } 44 | -------------------------------------------------------------------------------- /ci/get-next-package-version.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory)][string]$RepoName, 3 | [string]$VariableName = "Version" 4 | ) 5 | $ErrorActionPreference = "Stop" 6 | 7 | Set-Variable -Scope Global -Name $VariableName -Value (./steps/get-next-package-version.ps1 -RepoName $RepoName) 8 | -------------------------------------------------------------------------------- /ci/install-package.ps1: -------------------------------------------------------------------------------- 1 | 2 | param( 3 | [Parameter(Mandatory=$true)] 4 | [string]$RepoName, 5 | [string]$Name = "", 6 | [string]$NginxVersion = "" 7 | ) 8 | 9 | # Combine the current working directory with the repository name 10 | $RepoPath = [IO.Path]::Combine($pwd, $RepoName) 11 | 12 | $ModuleName = "ngx_http_51D_module.so" 13 | 14 | # Define the path for downloaded artifacts 15 | $PackagePath = [IO.Path]::Combine($pwd, "package", "package_$Name", $ModuleName) 16 | 17 | # Get the local install path 18 | $ModulesPath = [IO.Path]::Combine($RepoPath, "build", "modules") 19 | $InstallPath = [IO.Path]::Combine($ModulesPath, $ModuleName) 20 | 21 | # Display the repository path and enter it 22 | Write-Output "Entering '$RepoPath'" 23 | Push-Location $RepoPath 24 | 25 | try { 26 | 27 | if ($Name -ne "") { 28 | 29 | # Install NGINX without building the module. 30 | Write-Output "Install NGINX '$NginxVersion' without 51Degrees module" 31 | make install-no-module FIFTYONEDEGREES_NGINX_VERSION=$NginxVersion 32 | 33 | # Now copy the module to the installs module directory 34 | Write-Output "Copying the 51Degrees module from '$PackagePath' to '$InstallPath'" 35 | mkdir $ModulesPath 36 | Copy-Item -Path $PackagePath -Destination $InstallPath 37 | 38 | } 39 | else { 40 | 41 | Write-Output "Not installing locally, as no NGINX version is specified." 42 | 43 | } 44 | } 45 | finally { 46 | 47 | # Leave the repository path and display it 48 | Write-Output "Leaving '$RepoPath'" 49 | Pop-Location 50 | 51 | } 52 | 53 | # Exit the script with the last exit code 54 | exit $LASTEXITCODE 55 | -------------------------------------------------------------------------------- /ci/options.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Image": "ubuntu-latest", 4 | "Name": "ubuntu-latest_Nginx1.27.4", 5 | "NginxVersion": "1.27.4", 6 | "NginxPlusVersion": "34", 7 | "RunPerformance": true, 8 | "FullTests": true, 9 | "PackageRequirement": true 10 | }, 11 | { 12 | "Image": "ubuntu-latest", 13 | "Name": "ubuntu-latest_Nginx1.27.4_MemCheck", 14 | "NginxVersion": "1.27.4", 15 | "MemCheck": true 16 | }, 17 | { 18 | "Image": "ubuntu-latest", 19 | "Name": "ubuntu-latest_Nginx1.27.4_MemCheck_Static", 20 | "NginxVersion": "1.27.4", 21 | "BuildMethod": "static", 22 | "MemCheck": true 23 | }, 24 | { 25 | "Image": "ubuntu-latest", 26 | "Name": "ubuntu-latest_Nginx1.27.2", 27 | "NginxVersion": "1.27.2", 28 | "NginxPlusVersion": "33", 29 | "RunPerformance": false, 30 | "FullTests": false, 31 | "PackageRequirement": true 32 | }, 33 | { 34 | "Image": "ubuntu-latest", 35 | "Name": "ubuntu-latest_Nginx1.25.5", 36 | "NginxVersion": "1.25.5", 37 | "NginxPlusVersion": "32", 38 | "RunPerformance": false, 39 | "FullTests": false, 40 | "PackageRequirement": true 41 | }, 42 | { 43 | "Image": "ubuntu-latest", 44 | "Name": "ubuntu-latest_Nginx1.25.3", 45 | "NginxVersion": "1.25.3", 46 | "NginxPlusVersion": "31", 47 | "RunPerformance": false, 48 | "FullTests": false, 49 | "PackageRequirement": true 50 | }, 51 | { 52 | "Image": "ubuntu-latest", 53 | "Name": "ubuntu-latest_Nginx1.25.1", 54 | "NginxVersion": "1.25.1", 55 | "NginxPlusVersion": "30", 56 | "RunPerformance": false, 57 | "FullTests": false, 58 | "PackageRequirement": true 59 | }, 60 | { 61 | "Image": "ubuntu-latest", 62 | "Name": "ubuntu-latest_Nginx1.23.4", 63 | "NginxVersion": "1.23.4", 64 | "NginxPlusVersion": "29", 65 | "RunPerformance": false, 66 | "FullTests": false, 67 | "PackageRequirement": true 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /ci/publish-package.ps1: -------------------------------------------------------------------------------- 1 | Write-Output "No packages to publish, just update the tag" 2 | -------------------------------------------------------------------------------- /ci/run-integration-tests.ps1: -------------------------------------------------------------------------------- 1 | 2 | param( 3 | [Parameter(Mandatory=$true)] 4 | [string]$RepoName, 5 | [string]$Name = "1.21.3_dynamic", 6 | [string]$NginxVersion = "1.21.3", 7 | [string]$BuildMethod = "dynamic" 8 | ) 9 | 10 | $RepoPath = [IO.Path]::Combine($pwd, $RepoName) 11 | 12 | Write-Output "Entering '$RepoPath'" 13 | Push-Location $RepoPath 14 | 15 | try { 16 | 17 | # Create the output directories if they don't already exist. 18 | if ($(Test-Path -Path "test-results") -eq $False) { 19 | mkdir test-results 20 | } 21 | if ($(Test-Path -Path "test-results/integration") -eq $False) { 22 | mkdir test-results/integration 23 | } 24 | 25 | # Disable leak checks here, as they are for the unit tests. 26 | $env:ASAN_OPTIONS="detect_odr_violation=0:detect_leaks=0" 27 | 28 | Write-Output "Setting up the CDN example" 29 | # Copy the example config 30 | Copy-Item examples/hash/jsExample/javascript.conf build/nginx.conf 31 | # Set the data file 32 | sed -i "s/51Degrees-LiteV4\.1\.hash/TAC-HashV41\.hash/g" build/nginx.conf 33 | # Remove the documentation block 34 | sed -i "/\/\*\*/,/\*\//d" build/nginx.conf 35 | # Start NGINX 36 | ./nginx 37 | 38 | # Run the selenium tests 39 | $JsExamplePath = [IO.Path]::Combine($RepoPath, "tests", "examples", "jsExample") 40 | Write-Output "Entering '$JsExamplePath'" 41 | Push-Location $JsExamplePath 42 | 43 | try { 44 | Write-Output "Running CDN tests" 45 | $env:PATH="$($env:PATH):$pwd/driver" 46 | npm install 47 | npx browserslist@latest --update-db 48 | npm test 49 | } 50 | finally { 51 | 52 | Write-Output "Leaving '$JsExamplePath'" 53 | Pop-Location 54 | Write-Output "Stopping NGINX" 55 | ./nginx -s quit 56 | if ($LASTEXITCODE -ne 0 ) { 57 | Write-Error "Failed to exit Nginx" 58 | exit 1 59 | } 60 | 61 | } 62 | 63 | # Test the examples 64 | Write-Output "Testing examples" 65 | make test-examples FIFTYONEDEGREES_DATAFILE=TAC-HashV41.hash FIFTYONEDEGREES_FORMATTER='--formatter TAP::Formatter::JUnit' FIFTYONEDEGREES_TEST_OUTPUT=$RepoPath/test-results/integration/$($Name)_Examples.xml 66 | 67 | } 68 | finally { 69 | 70 | Write-Output "Leaving '$RepoPath'" 71 | Pop-Location 72 | 73 | } 74 | -------------------------------------------------------------------------------- /ci/run-performance-tests.ps1: -------------------------------------------------------------------------------- 1 | 2 | param( 3 | [Parameter(Mandatory=$true)] 4 | [string]$RepoName, 5 | [string]$Name = "1.21.3_dynamic", 6 | [string]$NginxVersion = "1.21.3", 7 | [string]$BuildMethod = "dynamic" 8 | ) 9 | 10 | $RepoPath = [IO.Path]::Combine($pwd, $RepoName) 11 | $PerfPath = [IO.Path]::Combine($RepoPath, "tests", "performance") 12 | $PerfResultsFile = [IO.Path]::Combine($RepoPath, "test-results", "performance-summary", "results_$Name.json") 13 | 14 | Write-Output "Entering '$RepoPath'" 15 | Push-Location $RepoPath 16 | 17 | try { 18 | 19 | # Create the output directories if they don't already exist. 20 | if ($(Test-Path -Path "test-results") -eq $False) { 21 | mkdir test-results 22 | } 23 | if ($(Test-Path -Path "test-results/performance") -eq $False) { 24 | mkdir test-results/performance 25 | } 26 | if ($(Test-Path -Path "test-results/performance-summary") -eq $False) { 27 | mkdir test-results/performance-summary 28 | } 29 | 30 | } 31 | finally { 32 | 33 | Write-Output "Leaving '$RepoPath'" 34 | Pop-Location 35 | 36 | } 37 | 38 | Write-Output "Entering '$PerfPath'" 39 | Push-Location $PerfPath 40 | 41 | try { 42 | mkdir build 43 | Push-Location build 44 | try { 45 | 46 | # Build the performance tests 47 | Write-Output "Building performance test" 48 | cmake .. 49 | cmake --build . 50 | 51 | # When running the performance tests, set the data file name manually, 52 | # then unset once we're done 53 | Write-Output "Running performance test" 54 | $env:DATA_FILE_NAME="TAC-HashV41.hash" 55 | ./runPerf.sh 56 | $env:DATA_FILE_NAME=$Null 57 | 58 | # Write out the results for comparison 59 | Write-Output "Writing performance test results" 60 | $Results = Get-Content ./summary.json | ConvertFrom-Json 61 | Write-Output "{ 62 | 'HigherIsBetter': { 63 | 'DetectionsPerSecond': $(1/($Results.overhead_ms / 1000)) 64 | }, 65 | 'LowerIsBetter': { 66 | 'MsPerDetection': $($Results.overhead_ms) 67 | } 68 | }" > $PerfResultsFile 69 | 70 | } 71 | finally { 72 | 73 | Write-Output "Leaving build" 74 | Pop-Location 75 | 76 | } 77 | } 78 | finally { 79 | 80 | Write-Output "Leaving '$PerfPath'" 81 | Pop-Location 82 | 83 | } 84 | 85 | 86 | Write-Output "Entering '$RepoPath'" 87 | 88 | Push-Location $RepoPath 89 | 90 | try { 91 | 92 | # Run the stress test script. ApacheBench will already be built from the previous test 93 | Write-Output "Run stress tests using ApacheBench" 94 | if (Test-Path -Path "tests/performance/build/ApacheBench-prefix/src/ApacheBench-build/bin/ab") { 95 | $env:DATA_FILE_NAME="TAC-HashV41.hash" 96 | ./test.sh 97 | $env:DATA_FILE_NAME=$Null 98 | } 99 | else { 100 | Write-Error 'The performance test need to be build first.' 101 | exit 1 102 | } 103 | 104 | } 105 | finally { 106 | 107 | Write-Output "Leaving '$RepoPath'" 108 | Pop-Location 109 | 110 | } 111 | -------------------------------------------------------------------------------- /ci/run-unit-tests.ps1: -------------------------------------------------------------------------------- 1 | 2 | param( 3 | [Parameter(Mandatory=$true)] 4 | [string]$RepoName, 5 | [string]$Name = "1.21.3_dynamic", 6 | [string]$NginxVersion = "1.21.3", 7 | [string]$BuildMethod = "dynamic", 8 | [bool]$FullTests = $False, 9 | [bool]$MemCheck = $False 10 | ) 11 | 12 | $RepoPath = [IO.Path]::Combine($pwd, $RepoName) 13 | 14 | Write-Output "Entering '$RepoPath'" 15 | Push-Location $RepoPath 16 | 17 | try { 18 | 19 | # Create the output directories if they don't exist already. 20 | if ($(Test-Path -Path "test-results") -eq $False) { 21 | mkdir test-results 22 | } 23 | if ($(Test-Path -Path "test-results/unit") -eq $False) { 24 | mkdir test-results/unit 25 | } 26 | 27 | Write-Output "Running 51Degrees unit tests" 28 | make test FIFTYONEDEGREES_DATAFILE=TAC-HashV41.hash FIFTYONEDEGREES_FORMATTER='--formatter TAP::Formatter::JUnit' FIFTYONEDEGREES_TEST_OUTPUT=$RepoPath/test-results/unit/$Name.xml 29 | # Keep track of failed tests 30 | $Passed = $($LASTEXITCODE -eq 0) 31 | # Output the full results file, as some bits are not reported in GitHub 32 | cat $RepoPath/test-results/unit/$Name.xml 33 | 34 | if ($FullTests -eq $True) { 35 | 36 | # If full tests are requested, then run the full test suite (NGINX core tests, with the module enabled) 37 | Write-Output "Running full NGINX unit tests" 38 | make test-full FIFTYONEDEGREES_DATAFILE=TAC-HashV41.hash FIFTYONEDEGREES_FORMATTER='--formatter TAP::Formatter::JUnit' FIFTYONEDEGREES_TEST_OUTPUT=$RepoPath/test-results/unit/$($Name)_Full.xml 39 | $Passed = $($Passed -and $LASTEXITCODE -eq 0) 40 | 41 | # Now do the same with the NGINX Plus executable 42 | Write-Output "Running full NGINX unit tests against NGINX Plus" 43 | 44 | # For mgmt module we need to set permissions to nginx-mgmt-state 45 | sudo chmod -R a+xrw /var/lib 46 | 47 | # As we're not using the makefile, we need to set the environment variables ourselves, and add common path to license_token 48 | $env:TEST_NGINX_BINARY="/usr/sbin/nginx" 49 | $env:TEST_NGINX_GLOBALS="mgmt {license_token /etc/nginx/license.jwt;} load_module $RepoPath/build/modules/ngx_http_51D_module.so;" 50 | $env:TEST_NGINX_GLOBALS_HTTP="51D_file_path $RepoPath/device-detection-cxx/device-detection-data/TAC-HashV41.hash;" 51 | prove --formatter TAP::Formatter::JUnit -v tests/51degrees.t tests/nginx-tests :: TAC-HashV41.hash > $RepoPath/test-results/unit/$($Name)_Full_Plus.xml 52 | $Passed = $($Passed -and $LASTEXITCODE -eq 0) 53 | # Unset the environment variables for future tests. 54 | $env:TEST_NGINX_BINARY="" 55 | $env:TEST_NGINX_GLOBALS="" 56 | $env:TEST_NGINX_GLOBALS_HTTP="" 57 | 58 | } 59 | 60 | } 61 | finally { 62 | 63 | Write-Output "Leaving '$RepoPath'" 64 | Pop-Location 65 | 66 | } 67 | 68 | if ($Passed) { 69 | exit 0 70 | } 71 | else { 72 | exit 1 73 | } 74 | -------------------------------------------------------------------------------- /ci/setup-environment.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory=$true)] 3 | [string]$RepoName, 4 | [string]$Name = "1.21.3 dynamic", 5 | [string]$NginxVersion = "1.21.3", 6 | [string]$NginxPlusVersion = "25", 7 | [bool]$FullTests = $False, 8 | [string]$BuildMethod = "dynamic" 9 | ) 10 | 11 | $RepoPath = [IO.Path]::Combine($pwd, $RepoName) 12 | $TestsPath = [IO.Path]::Combine($RepoPath, "tests") 13 | $JsExamplePath = [IO.Path]::Combine($RepoPath, "tests", "examples", "jsExample") 14 | 15 | # Install perl libraries 16 | Write-Output 'Install perl libraries for test output formatters' 17 | # cpan will ask for auto configuration first time it is run so answer 'yes'. 18 | Write-Output y | sudo cpan 19 | sudo cpan App::cpanminus --notest 20 | sudo cpanm --force TAP::Formatter::JUnit --notest 21 | 22 | # Get the version of nginx-test to use. Where new tests have been introduced, previous versions 23 | # of NGINX do not always pass them 24 | $ParsedVersion = [System.Version]::Parse($NginxVersion) 25 | if ($ParsedVersion -le [System.Version]::Parse("1.19.5")) { 26 | $TestCommit = "6bf30e564c06b404876f0bd44ace8431b3541f24" 27 | } 28 | elseif ($ParsedVersion -le [System.Version]::Parse("1.23.2")) { 29 | $TestCommit = "3356f91a3fdae372d0946e4c89a9413f558c8017" 30 | } 31 | else { 32 | $TestCommit = $Null 33 | } 34 | 35 | 36 | # Download the nginx-tests repository 37 | Write-Output "Moving into $TestsPath" 38 | Push-Location $TestsPath 39 | try { 40 | Write-Output "Clean any existing nginx-tests folder." 41 | if (Test-Path -Path nginx-tests) { 42 | Remove-Item nginx-tests -Recurse -Force 43 | } 44 | 45 | Write-Output "Clone the nginx-tests source." 46 | git clone https://github.com/nginx/nginx-tests.git 47 | 48 | Write-Output "Moving into nginx-tests." 49 | Push-Location nginx-tests 50 | 51 | try { 52 | if ($Null -ne $TestCommit) { 53 | Write-Output "Checkout to before the breaking changes for this version." 54 | git reset --hard $TestCommit 55 | } 56 | } 57 | finally { 58 | Pop-Location 59 | } 60 | } 61 | finally { 62 | Pop-Location 63 | } 64 | 65 | # If full tests are requested, then we'll need to install NGINX Plus 66 | if ($FullTests -eq $True) { 67 | Write-Output "Uninstall existing Nginx" 68 | sudo apt-get purge nginx -y 69 | 70 | Write-Output "Create ssl directory for Nginx Plus" 71 | sudo mkdir -p /etc/ssl/nginx 72 | sudo mkdir -p /etc/nginx 73 | 74 | Write-Output "Copy the nginx-repo.* file to the created directory" 75 | sudo cp $([IO.Path]::Combine($RepoPath, "nginx-repo.key")) /etc/ssl/nginx 76 | sudo cp $([IO.Path]::Combine($RepoPath, "nginx-repo.crt")) /etc/ssl/nginx 77 | sudo cp $([IO.Path]::Combine($RepoPath, "license.jwt")) /etc/nginx 78 | 79 | Write-Output "Setting permissions for the certificates" 80 | sudo chown -R root:root /etc/ssl/nginx 81 | sudo chmod -R a+r /etc/ssl/nginx 82 | sudo chmod -R a+r /etc/nginx 83 | 84 | ls -l /etc/ssl 85 | ls -l /etc/ssl/nginx 86 | ls -l /etc/nginx 87 | 88 | Write-Output "Certificate expiration date:" 89 | openssl x509 -enddate -noout -in /etc/ssl/nginx/nginx-repo.crt 90 | 91 | Write-Output "Download and add NGINX signing key and App-protect security updates signing key:" 92 | curl -O https://nginx.org/keys/nginx_signing.key && sudo apt-key add ./nginx_signing.key 93 | 94 | Write-Output "Install apt utils" 95 | sudo apt-get install apt-transport-https lsb-release ca-certificates wget gnupg2 ubuntu-keyring 96 | 97 | Write-Output "Add Nginx Plus repository" 98 | # Add allow-insecure because there is an issue with the signing of the NGINX Plus repository. 99 | printf "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg allow-insecure=yes] https://pkgs.nginx.com/plus/ubuntu $(lsb_release -cs) nginx-plus\n" | sudo tee /etc/apt/sources.list.d/nginx-plus.list 100 | 101 | Write-Output "Download nginx-plus apt configuration files to /etc/apt/apt.conf.d" 102 | sudo wget -P /etc/apt/apt.conf.d https://cs.nginx.com/static/files/90pkgs-nginx 103 | 104 | Write-Output "Update the repository and install Nginx Plus" 105 | sudo apt-get update 106 | $packageVersion = ((apt-cache show nginx-plus | Select-String -CaseSensitive '^Version: ') -replace 'Version: ' -clike "$NginxPlusVersion-*")[0] 107 | sudo apt-get install nginx-plus=$packageVersion -y --allow-unauthenticated 108 | 109 | Write-Output "Check if installation was successful" 110 | /usr/sbin/nginx -v 2>&1 | grep -F $NginxVersion || $(throw "Failed to install Nginx Plus $NginxVersion") 111 | } 112 | 113 | Write-Output "Install Apache dev for performance tests" 114 | sudo apt-get update -y 115 | sudo apt-get install cmake apache2-dev libapr1-dev libaprutil1-dev -y 116 | 117 | # Install any extra requirements for the module 118 | Write-Output "Installing NGINX dependencies" 119 | sudo apt-get install make zlib1g-dev libpcre3 libpcre3-dev 120 | 121 | -------------------------------------------------------------------------------- /ci/update-packages.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory)][string]$RepoName, 3 | [string[]]$Images = "ubuntu-latest" 4 | ) 5 | $ErrorActionPreference = "Stop" 6 | 7 | Write-Host "Fetching releases document..." 8 | $req = Invoke-WebRequest "https://raw.githubusercontent.com/nginx/documentation/refs/heads/main/content/nginx/releases.md" 9 | 10 | Write-Host "Parsing the table of supported releases..." 11 | $tableStart = $req.Content.IndexOf("| NGINX Plus Release |") 12 | $tableEnd = $req.Content.IndexOf("`n`n", $tableStart) 13 | $supportedReleases = ([regex]'(?m)^\| R(\d+)').Matches($req.Content.Substring($tableStart, $tableEnd-$tableStart)) | ForEach-Object { $_.Groups[1].Value } 14 | Write-Host "Supported NGINX Plus releases: $supportedReleases" 15 | 16 | Write-Host "Searching for corresponding Open Source versions of each release..." 17 | $releaseRegex = [regex]'(?mx) # enable multiline mode and allow comments/spaces 18 | ^\#\#\sNGINX\sPlus\sRelease\s(\d+)\s\(R\d+\) \n # \s is required to match non-breaking spaces that the document uses 19 | .* \n # discard release date since we know supported versions from the table 20 | _?Based\son\sNGINX\sOpen\sSource\s([0-9.]+) 21 | ' 22 | $openSourceOf = @{} 23 | $releaseRegex.Matches($req.Content) | ForEach-Object { $openSourceOf[$_.Groups[1].Value] = $_.Groups[2].Value } 24 | 25 | Write-Host "Building options array..." 26 | [Collections.ArrayList]$options = @() 27 | foreach ($release in $supportedReleases) { 28 | $isLatest = $options.Count -lt 1 29 | foreach ($image in $Images) { 30 | [void]$options.Add([ordered]@{ 31 | Image = $image 32 | Name = "$($image)_Nginx$($openSourceOf[$release])" 33 | NginxVersion = $openSourceOf[$release] 34 | NginxPlusVersion = $release 35 | RunPerformance = $IsLatest # Only run performance if this is the latest NGINX Plus version 36 | FullTests = $IsLatest # Only run the full NGINX test suite if this is the latest NGINX Plus version 37 | PackageRequirement = $True 38 | }) 39 | if ($isLatest) { 40 | # Add memory check configurations if this is the latest NGINX Plus version 41 | [void]$options.Add([ordered]@{ 42 | Image = $image 43 | Name = "$($image)_Nginx$($openSourceOf[$release])_MemCheck" 44 | NginxVersion = $openSourceOf[$release] 45 | MemCheck = $True 46 | }) 47 | [void]$options.Add([ordered]@{ 48 | Image = $image 49 | Name = "$($image)_Nginx$($openSourceOf[$release])_MemCheck_Static" 50 | NginxVersion = $openSourceOf[$release] 51 | BuildMethod = "static" 52 | MemCheck = $True 53 | }) 54 | } 55 | } 56 | } 57 | 58 | Write-Host "Writing options file to '$RepoName/ci/options.json'" 59 | $Options | ConvertTo-Json > "$RepoName/ci/options.json" 60 | Get-Content "$RepoName/ci/options.json" 61 | -------------------------------------------------------------------------------- /docs/DoxygenLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /examples/curl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:12-slim 2 | WORKDIR /app 3 | 4 | #build dependencies 5 | RUN apt-get update && apt-get install --no-install-recommends -y git curl ca-certificates build-essential libpcre3-dev libz-dev nano && rm -rf /var/lib/apt/lists/* 6 | 7 | #cloning the current repo 8 | RUN git clone https://github.com/51Degrees/device-detection-nginx . && git submodule update --init --recursive 9 | 10 | #dev dependencies 11 | RUN curl -Lo device-detection-cxx/device-detection-data/51Degrees-LiteV4.1.hash https://github.com/51Degrees/device-detection-data/raw/main/51Degrees-LiteV4.1.hash 12 | 13 | RUN make install 14 | 15 | COPY nginx.conf /app/build/nginx.conf 16 | 17 | EXPOSE 8080 18 | 19 | CMD ["/app/nginx", "-c", "/app/build/nginx.conf"] 20 | -------------------------------------------------------------------------------- /examples/curl/README.md: -------------------------------------------------------------------------------- 1 | # cURL Example 2 | 3 | This example demonstrates how to feed arbitrary evidence in the form of headers to an nginx module using cURL. 4 | 5 | ## Build 6 | 7 | There is a docker image with nginx and 51d module - convenient for testing on a mac. 8 | 9 | You can just run `./run.sh` from this directory or `docker` commands similar to: 10 | 11 | ```sh 12 | docker build -t 51d-nginx-curl . 13 | docker run -d -p 8080:8080 51d-nginx-curl 14 | ``` 15 | 16 | ## Run 17 | 18 | Use the `curl.sh` script or just plain curl command (the evidence provided may be arbitrary): 19 | ``` 20 | $ curl --head \ 21 | -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.41' \ 22 | -H 'Sec-CH-UA: "Not.A/Brand";v="8","Chromium";v="114","Microsoft Edge";v="114"' \ 23 | -H 'Sec-CH-UA-Arch: x86' \ 24 | -H 'Sec-CH-UA-Bitness: 64' \ 25 | -H 'Sec-CH-UA-Full-Version: 114.0.5735.110' \ 26 | -H 'Sec-CH-UA-Full-Version-List: "Not.A/Brand";v="8.0.0.0","Chromium";v="114.0.5735.110","Microsoft Edge";v="114.0.1823.41"' \ 27 | -H 'Sec-CH-UA-Mobile: ?0' \ 28 | -H 'Sec-CH-UA-Model: ' \ 29 | -H 'Sec-CH-UA-Platform: Windows' \ 30 | -H 'Sec-CH-UA-Platform-Version: 15.0.0' \ 31 | http://127.0.0.1:8080/hash 32 | 33 | HTTP/1.1 200 OK 34 | Server: nginx/1.20.0 35 | Date: Wed, 16 Aug 2023 16:03:00 GMT 36 | Content-Type: text/plain 37 | Content-Length: 1 38 | Last-Modified: Wed, 16 Aug 2023 13:41:12 GMT 39 | Connection: keep-alive 40 | ETag: "64dcd1f8-1" 41 | x-device-type: NoMatch 42 | x-platform-name: Windows 43 | x-platform-version: 11.0 44 | x-browser-name: Edge (Chromium) for Windows 45 | x-browser-version: 114.0 46 | x-is-mobile: False 47 | Accept-Ranges: bytes 48 | ``` 49 | 50 | To test feeding the evidence via the PHP libcurl - use `curl.php` script: 51 | 52 | ``` 53 | $ php examples/curl/curl.php 54 | HTTP/1.1 200 OK 55 | Server: nginx/1.20.0 56 | Date: Wed, 16 Aug 2023 16:01:19 GMT 57 | Content-Type: text/plain 58 | Content-Length: 1 59 | Last-Modified: Wed, 16 Aug 2023 13:41:12 GMT 60 | Connection: keep-alive 61 | ETag: "64dcd1f8-1" 62 | x-device-type: NoMatch 63 | x-platform-name: Windows 64 | x-platform-version: 11.0 65 | x-browser-name: Edge (Chromium) for Windows 66 | x-browser-version: 114.0 67 | x-is-mobile: False 68 | Accept-Ranges: bytes 69 | ``` -------------------------------------------------------------------------------- /examples/curl/curl.php: -------------------------------------------------------------------------------- 1 | $value) { 5 | echo "$header: $value
\n"; 6 | } 7 | ?> 8 | -------------------------------------------------------------------------------- /examples/hash/config.conf: -------------------------------------------------------------------------------- 1 | /** 2 | @example hash/config.conf 3 | 4 | This example shows how to configure the available settings for 51Degrees' 5 | on-premise device detection in Nginx. This example is available in full 6 | on [GitHub](https://github.com/51Degrees/device-detection-nginx/blob/master/examples/hash/config.conf). 7 | 8 | @include{doc} example-require-datafile.txt 9 | 10 | Make sure to include at least IsMobile, BrowserName and PlatformName properties 11 | for this to work. 12 | 13 | Before using the example, update the followings: 14 | - Remove this 'how to' guide block. 15 | - Update the %%%DAEMON_MODE%% to 'on' or 'off'. 16 | - Remove the %%%TEST_GLOBALS%%. 17 | - Update the %%%MODULE_PATH%% with the actual path. 18 | - Remove the %%%TEST_GLOBALS_HTTP%%. 19 | - Update the %%%FILE_PATH%% with the actual file path. 20 | - Replace the nginx.conf with this file or run Nginx with `-c` 21 | option pointing to this file. 22 | - Create a static file `config` in the Nginx `document_root`. 23 | 24 | In a Linux environment, once Nginx has started, run the following command: 25 | ``` 26 | $ curl localhost:8080/config -I -A "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53" 27 | ``` 28 | Expected output: 29 | ``` 30 | HTTP/1.1 200 OK 31 | ... 32 | x-device: Mobile Safari^sep^iOS^sep^PERFORMANCE 33 | x-mobile: True 34 | ... 35 | ``` 36 | 37 | `NOTE`: All the lines above, this line and the end of comment block line after 38 | this line should be removed before using this example. 39 | */ 40 | 41 | ## Replace the DAEMON_MODE with 'on' or 'off'. ## 42 | daemon %%DAEMON_MODE%%; 43 | worker_processes 4; 44 | 45 | ## The following line is required for testing. Remove before ## 46 | ## running with Nginx. ## 47 | %%TEST_GLOBALS%% 48 | ## Update the MODULE_PATH before running with Nginx ## 49 | load_module %%MODULE_PATH%%modules/ngx_http_51D_module.so; 50 | 51 | events { 52 | worker_connections 1024; 53 | } 54 | 55 | # // Snippet Start 56 | http { 57 | ## The following line is required for testing. Remove before ## 58 | ## running with Nginx. ## 59 | %%TEST_GLOBALS_HTTP%% 60 | ## Set the data file for the 51Degrees module to use. ## 61 | ## Update the FILE_PATH before running with Nginx. ## 62 | 51D_file_path %%FILE_PATH%%; 63 | 51D_value_separator ^sep^; 64 | 51D_drift 3; 65 | 51D_difference 5; 66 | 51D_allow_unmatched on; 67 | 68 | server { 69 | listen 127.0.0.1:8080; 70 | server_name localhost; 71 | 72 | location /config { 73 | ## Do a single User-Agent match for device information ## 74 | 51D_match_ua x-device BrowserName,PlatformName,Method; 75 | 76 | ## Do a multiple HTTP header match for IsMobile ## 77 | 51D_match_all x-mobile IsMobile; 78 | 79 | ## Add to response headers for easy viewing. ## 80 | add_header x-device $http_x_device; 81 | add_header x-mobile $http_x_mobile; 82 | } 83 | } 84 | } 85 | # // Snippet End 86 | -------------------------------------------------------------------------------- /examples/hash/gettingStarted.conf: -------------------------------------------------------------------------------- 1 | /** 2 | @example hash/gettingStarted.conf 3 | 4 | This example shows how to quickly start using 51Degrees' on-premise device 5 | detection in Nginx. This example is available in full on [GitHub]( 6 | https://github.com/51Degrees/device-detection-nginx/blob/master/examples/hash/gettingStarted.conf). 7 | 8 | @include{doc} example-require-datafile.txt 9 | 10 | Make sure to include at least IsMobile, BrowserName and PlatformName properties 11 | for this to work. 12 | 13 | Before using the example, update the followings: 14 | - Remove this 'how to' guide block. 15 | - Update the %%%DAEMON_MODE%% to 'on' or 'off'. 16 | - Remove the %%%TEST_GLOBALS%%. 17 | - Update the %%%MODULE_PATH%% with the actual path. 18 | - Remove the %%%TEST_GLOBALS_HTTP%%. 19 | - Update the %%%FILE_PATH%% with the actual file path. 20 | - Replace the nginx.conf with this file or run Nginx with `-c` 21 | option pointing to this file. 22 | - Create a static file `hash` in the Nginx `document_root`. 23 | 24 | In a Linux environment, once Nginx has started, run the following command: 25 | ``` 26 | $ curl localhost:8080/hash -I -A "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53" 27 | ``` 28 | Expected output: 29 | ``` 30 | HTTP/1.1 200 OK 31 | ... 32 | x-device: Mobile Safari,iOS 33 | x-mobile: True 34 | ... 35 | ``` 36 | 37 | `NOTE`: All the lines above, this line and the end of comment block line after 38 | this line should be removed before using this example. 39 | */ 40 | 41 | ## Replace DAEMON_MODE with 'on' or 'off' before running ## 42 | ## with Nginx. ## 43 | daemon %%DAEMON_MODE%%; 44 | worker_processes 4; 45 | 46 | ## The following line is required for testing. Remove before ## 47 | ## running with Nginx. ## 48 | %%TEST_GLOBALS%% 49 | ## Update MODULE_PATH before running with Nginx ## 50 | load_module %%MODULE_PATH%%modules/ngx_http_51D_module.so; 51 | 52 | events { 53 | worker_connections 1024; 54 | } 55 | 56 | # // Snippet Start 57 | http { 58 | ## The following line is required for testing. Remove before ## 59 | ## running with Nginx. ## 60 | %%TEST_GLOBALS_HTTP%% 61 | ## Set the data file for the 51Degrees module to use. ## 62 | ## Update the FILE_PATH before running with Nginx. ## 63 | 51D_file_path %%FILE_PATH%%; 64 | 65 | server { 66 | listen 127.0.0.1:8080; 67 | server_name localhost; 68 | 69 | location /hash { 70 | ## Do a single User-Agent match for device information ## 71 | 51D_match_ua x-device BrowserName,PlatformName; 72 | 73 | ## Do a User-Agent + Client-Hints match for device information ## 74 | 51D_match_ua_client_hints x-device-hinted BrowserName,PlatformName; 75 | 76 | ## Do a multiple HTTP header match for IsMobile ## 77 | 51D_match_all x-mobile IsMobile; 78 | 79 | ## Add to response headers for easy viewing. ## 80 | add_header x-device $http_x_device; 81 | add_header x-device-hinted $http_x_device_hinted; 82 | add_header x-mobile $http_x_mobile; 83 | } 84 | } 85 | } 86 | # // Snippet End 87 | -------------------------------------------------------------------------------- /examples/hash/jsExample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 164 | 171 | 172 |

173 | 174 | -------------------------------------------------------------------------------- /examples/hash/jsExample/javascript.conf: -------------------------------------------------------------------------------- 1 | /** 2 | @example hash/jsExample/javascript.conf 3 | 4 | This example shows how to return Javascript to obtain further evidence for 5 | 51Degrees' on-premise device detection in Nginx. This example is available 6 | in full on [GitHub](https://github.com/51Degrees/device-detection-nginx/blob/master/examples/hash/jsExample/javascript.conf). 7 | 8 | @include{doc} example-require-datafile.txt 9 | 10 | Properties required for this example are not included in the free Lite file, 11 | so a data file that includes ScreenPixelsWidth and ScreenPixelsWidthJavascript 12 | need to be obtained from [configurator](https://configure.51degrees.com/). 13 | 14 | Before using the example, update the followings: 15 | - Remove this 'how to' guide block. 16 | - Update the file path specifed in `51D_file_path` with the actual file path. 17 | - Replace the nginx.conf with this file or run Nginx with `-c` 18 | option pointing to this file. 19 | - Create a static file `overrides` in the Nginx `document_root`. 20 | 21 | In a Linux environment, once Nginx has started, open the [index.html]( 22 | https://github.com/51Degrees/device-detection-nginx/blob/master/examples/hash/jsExample/index.html) 23 | in a browser. 24 | 25 | The `index.html` send a request to the `/51D.js` endpoint to obtain the javascript 26 | and execute it as below: 27 | ``` 28 | 29 | ``` 30 | Then use the obtained screen pixels width to send back to the `/overrides` endpoint. 31 | The full details can be seen in the `index.html`. 32 | 33 | The Javascript can be seen by running the below command. 34 | ``` 35 | $ curl localhost:8080/51D.js -A "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53" 36 | ``` 37 | 38 | Expected display after open the `index.html` in the browser is: 39 | ``` 40 | Screen Pixels Width: 1920 41 | ``` 42 | The number should be the screen pixels width of your machine. 43 | 44 | `NOTE`: All the lines above, this line and the end of comment block line after 45 | this line should be removed before using this example. 46 | */ 47 | 48 | worker_processes 4; 49 | 50 | load_module modules/ngx_http_51D_module.so; 51 | 52 | events { 53 | worker_connections 1024; 54 | } 55 | # // Snippet Start 56 | http { 57 | ## Set the data file for the 51Degrees module to use ## 58 | 51D_file_path ./device-detection-cxx/device-detection-data/51Degrees-LiteV4.1.hash; 59 | 60 | server { 61 | listen 8888; 62 | 63 | location /overrides { 64 | ## Get screen pixels width. Use matched all so override evidence can be processed ## 65 | 51D_match_all x-screenpixelswidth ScreenPixelsWidth; 66 | 67 | ## Add to response headers for easy viewing. ## 68 | add_header Access-Control-Allow-Origin *; 69 | add_header Access-Control-Allow-Heaaders *; 70 | add_header Access-Control-Expose-Headers *; 71 | add_header x-screenpixelswidth $http_x_screenpixelswidth; 72 | } 73 | 74 | location /51D.js { 75 | ## Get the javascript from ScreenPixelsWidthJavascript property ## 76 | 51D_get_javascript_single ScreenPixelsWidthJavascript; 77 | 78 | ## Since the URL location does not actually exist, an error will be logged, ## 79 | ## disable file not found error logging ## 80 | log_not_found off; 81 | } 82 | } 83 | } 84 | # // Snippet End 85 | -------------------------------------------------------------------------------- /examples/hash/matchMetrics.conf: -------------------------------------------------------------------------------- 1 | /** 2 | @example hash/matchMetrics.conf 3 | 4 | This example shows how to obtain further metrics for 51Degrees' on-premise 5 | device detection in Nginx. The available metris are Drift, Difference, Method, 6 | UserAgents, DeviceId and MatchedNodes. This example is available in full on 7 | [GitHub]( 8 | https://github.com/51Degrees/device-detection-nginx/blob/master/examples/hash/matchMetrics.conf). 9 | 10 | @include{doc} example-require-datafile.txt 11 | 12 | Make sure to include at least IsMobile property for this to work. 13 | 14 | Before using the example, update the followings: 15 | - Remove this 'how to' guide block. 16 | - Update the %%%DAEMON_MODE%% to 'on' or 'off'. 17 | - Remove the %%%TEST_GLOBALS%%. 18 | - Update the %%%MODULE_PATH%% with the actual path. 19 | - Remove the %%%TEST_GLOBALS_HTTP%%. 20 | - Update the %%%FILE_PATH%% with the actual file path. 21 | - Replace the nginx.conf with this file or run Nginx with `-c` 22 | option pointing to this file. 23 | - Create a static file `metrics` in the Nginx `document_root`. 24 | 25 | In a Linux environment, once Nginx has started, run the following command: 26 | ``` 27 | $ curl localhost:8080/metrics -I -A "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53" 28 | ``` 29 | Expected output: 30 | ``` 31 | HTTP/1.1 200 OK 32 | ... 33 | x-metrics: 0,0,PERFORMANCE,18 34 | x-user-agents: _________.0 (iPhone_______Phone ___7_1 like ____OS X_______WebK__________2 (KHTML,______Gecko_ Ver_________Mobile______7 Safari________ 35 | x-device-id: 12280-24384-24305-0 36 | ... 37 | ``` 38 | 39 | `NOTE`: All the lines above, this line and the end of comment block line after 40 | this line should be removed before using this example. 41 | */ 42 | 43 | ## Update DAEMON_MODE to 'on' or 'off' before running with Nginx ## 44 | daemon %%DAEMON_MODE%%; 45 | worker_processes 4; 46 | 47 | ## The following line is only for testing. Remove before running ## 48 | ## with Nginx ## 49 | %%TEST_GLOBALS%% 50 | ## Update MODULE_PATH before running with Nginx ## 51 | load_module %%MODULE_PATH%%modules/ngx_http_51D_module.so; 52 | 53 | events { 54 | worker_connections 1024; 55 | } 56 | 57 | # // Snippet Start 58 | http { 59 | ## The following line is only for testing. Remove before ## 60 | ## running with Nginx ## 61 | %%TEST_GLOBALS_HTTP%% 62 | ## Set the data file for the 51Degrees module to use ## 63 | ## Update the FILE_PATH before running with Nginx ## 64 | 51D_file_path %%FILE_PATH%%; 65 | 51D_drift 1; 66 | 51D_difference 1; 67 | 68 | server { 69 | listen 127.0.0.1:8080; 70 | server_name localhost; 71 | 72 | location /metrics { 73 | ## Get the metrics for the match using single User-Agent ## 74 | 51D_match_ua x-metrics Drift,Difference,Method,MatchedNodes; 75 | 76 | ## Get the matched user agent string ## 77 | 51D_match_ua x-user-agents UserAgents; 78 | 79 | ## Get the matched device ID ## 80 | 51D_match_ua x-device-id DeviceId; 81 | 82 | ## Add to response headers for easy viewing. ## 83 | add_header x-metrics $http_x_metrics; 84 | add_header x-user-agents $http_x_user_agents; 85 | add_header x-device-id $http_x_device_id; 86 | } 87 | } 88 | } 89 | # // Snippet End 90 | -------------------------------------------------------------------------------- /examples/hash/matchQuery.conf: -------------------------------------------------------------------------------- 1 | /** 2 | @example hash/matchQuery.conf 3 | 4 | This example shows how query argument take precendence over header value 5 | for 51Degrees' on-premise device detection in Nginx. This example is available 6 | in full on [GitHub]( 7 | https://github.com/51Degrees/device-detection-nginx/blob/master/examples/hash/matchQuery.conf). 8 | 9 | @include{doc} example-require-datafile.txt 10 | 11 | Make sure to include at least IsMobile property for this to work. 12 | 13 | Before using the example, update the followings: 14 | - Remove this 'how to' guide block. 15 | - Update the %%%DAEMON_MODE%% to 'on' or 'off'. 16 | - Remove the %%%TEST_GLOBALS%%. 17 | - Update the %%%MODULE_PATH%% with the actual path. 18 | - Remove the %%%TEST_GLOBALS_HTTP%%. 19 | - Update the %%%FILE_PATH%% with the actual file path. 20 | - Replace the nginx.conf with this file or run Nginx with `-c` 21 | option pointing to this file. 22 | - Create a static file `query` in the Nginx `document_root`. 23 | 24 | In a Linux environment, once Nginx has started, run the following command: 25 | ``` 26 | curl -I "http://localhost:8080/query?User-Agent=Mozilla/5.0%20(iPhone;%20CPU%20iPhone%20OS%207_1%20like%20Mac%20OS%20X)%20AppleWebKit/537.51.2%20(KHTML,%20like%20Gecko)%20Version/7.0%20Mobile/11D167%20Safari/9537.53" -A "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0" 27 | ``` 28 | The header is a desktop User-Agent while the argument is a mobile User-Agent. 29 | Expected output: 30 | ``` 31 | HTTP/1.1 200 OK 32 | ... 33 | x-mobile-single: True 34 | x-mobile-all: True 35 | ... 36 | ``` 37 | 38 | `NOTE`: All the lines above, this line and the end of comment block line after 39 | this line should be removed before using this example. 40 | */ 41 | 42 | ## Replace DAEMON_NODE with 'on' or 'off' before running with Nginx ## 43 | daemon %%DAEMON_MODE%%; 44 | worker_processes 4; 45 | 46 | ## The following line is only for testing. Remove before ## 47 | ## running with Nginx ## 48 | %%TEST_GLOBALS%% 49 | ## Update the MODULE_PATH before running with Nginx. ## 50 | load_module %%MODULE_PATH%%modules/ngx_http_51D_module.so; 51 | 52 | events { 53 | worker_connections 1024; 54 | } 55 | 56 | # // Snippet Start 57 | http { 58 | ## The following line is only for testing. Remove before ## 59 | ## running with Nginx ## 60 | %%TEST_GLOBALS_HTTP%% 61 | ## Set the data file for the 51Degrees module to use ## 62 | ## Update the FILE_PATH before running with Nginx. ## 63 | 51D_file_path %%FILE_PATH%%; 64 | 65 | server { 66 | listen 127.0.0.1:8080; 67 | server_name localhost; 68 | 69 | location /query { 70 | ## Do a single User-Agent match for device information ## 71 | 51D_match_ua x-mobile-single IsMobile $arg_user_agent; 72 | 73 | ## Do a multiple HTTP header match for IsMobile ## 74 | 51D_match_all x-mobile-all IsMobile; 75 | 76 | ## Add to response headers for easy viewing. ## 77 | add_header x-mobile-single $http_x_mobile_single; 78 | add_header x-mobile-all $http_x_mobile_all; 79 | } 80 | } 81 | } 82 | # // Snippet End 83 | -------------------------------------------------------------------------------- /examples/hash/responseHeaders/response.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | User-Agent Client Hints Example 4 | 5 | 6 | 7 |

User Agent Client Hints Example

8 |

Select the Use User Agent Client Hints button below, to use User Agent Client Hint headers in evidence for device detections.

9 | 10 |
11 | 12 | 43 | 44 |
45 |

46 | Hardware Vendor: HardwareVendor 47 |
48 | Hardware Name: HardwareName 49 |
50 | Device Type: DeviceType 51 |
52 | Platform Vendor: PlatformVendor 53 |
54 | Platform Name: PlatformName 55 |
56 | Platform Version: PlatformVersion 57 |
58 | Browser Vendor: BrowserVendor 59 |
60 | Browser Name: BrowserName 61 |
62 | Browser Version: BrowserVersion 63 |

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/hash/responseHeaders/responseHeader.conf: -------------------------------------------------------------------------------- 1 | /** 2 | @example hash/responseHeaders/responseHeader.conf 3 | 4 | This example shows how to obtain response header for 51Degrees' on-premise 5 | device detection in Nginx when Client Hints is enabled. This example is 6 | available in full on [GitHub]( 7 | https://github.com/51Degrees/device-detection-nginx/blob/master/examples/hash/responseHeader.conf). 8 | 9 | @include{doc} example-require-datafile.txt 10 | 11 | Make sure to include the 'BrowserName','BrowserVendor', 12 | 'BrowserVersion','HardwareName','HardwareVendor', 13 | 'PlatformName','PlatformVendor','PlatformVersion' 14 | properties. 15 | 16 | Before using the example, update the followings: 17 | - Remove this 'how to' guide block. 18 | - Update the %%%DAEMON_MODE%% to 'on' or 'off'. 19 | - Remove the %%%TEST_GLOBALS%%. 20 | - Update the %%%MODULE_PATH%% with the actual path. 21 | - Remove the %%%TEST_GLOBALS_HTTP%%. 22 | - Update the %%%FILE_PATH%% with the actual file path. 23 | - Replace the nginx.conf with this file or run Nginx with `-c` 24 | option pointing to this file. 25 | - Added the included response.html to the Nginx `document_root`. 26 | 27 | Once Nginx has started, run the following command: 28 | ``` 29 | curl -H "sec-ch-ua: \" Not A;Brand\";v=\"99\", \"Chromium\";v=\"90\", \"Google Chrome\";v=\"90\"" http://localhost:8080/response.html 30 | ``` 31 | Expected output: 32 | ``` 33 | ... 34 | Hardware Vendor: Unknown 35 |
36 | Hardware Name: Desktop|Emulator 37 |
38 | Device Type: Desktop 39 |
40 | Platform Vendor: Unknown 41 |
42 | Platform Name: Unknown 43 |
44 | Platform Version: Unknown 45 |
46 | Browser Vendor: Google 47 |
48 | Browser Name: Chrome 49 |
50 | Browser Version: 90 51 | ... 52 | ``` 53 | This shows that 51Degrees module can perform detection on Client Hints without User-Agent. 54 |
55 | 56 | Run the command again with `-I` flag: 57 | ``` 58 | curl -H "sec-ch-ua: \" Not A;Brand\";v=\"99\", \"Chromium\";v=\"90\", \"Google Chrome\";v=\"90\"" http://localhost:8080/response.html -I 59 | ``` 60 | Expected output: 61 | ``` 62 | Accept-CH: SEC-CH-UA,SEC-CH-UA-Full-Version,SEC-CH-UA-Model,SEC-CH-UA-Mobile,SEC-CH-UA-Arch,SEC-CH-UA-Platform,SEC-CH-UA-Platform-Version 63 | ``` 64 | This shows that after a detection, 51Degrees module will construct response headers, indicating the Client Hints that it needs in order to perform more detection where is applicable.. 65 | 66 | User agents that support Client Hints will process the response headers and send further Client Hints in the subsequent requests. With further Client Hints, 51Degrees module will be able to detect more properties of the sending device. 67 |
68 | 69 | User can also open the following url in a browser that supports Client Hints and follows the displayed instructions. 70 | 71 | ``` 72 | http://localhost:8080/response.html 73 | ``` 74 | 75 | `NOTE`: All the lines above, this line and the end of comment block line after 76 | this line should be removed before using this example. 77 | */ 78 | 79 | ## Update DAEMON_NODE to 'on' or 'off' before running with Nginx ## 80 | daemon %%DAEMON_MODE%%; 81 | worker_processes 4; 82 | 83 | ## The following line is only for testing. Remove before ## 84 | ## running with Nginx ## 85 | %%TEST_GLOBALS%% 86 | ## Update MODULE_PATH before running with Nginx ## 87 | load_module %%MODULE_PATH%%modules/ngx_http_51D_module.so; 88 | 89 | events { 90 | worker_connections 1024; 91 | } 92 | 93 | # // Snippet Start 94 | http { 95 | ## The following line is only for testing. Remove before ## 96 | ## running with Nginx ## 97 | %%TEST_GLOBALS_HTTP%% 98 | ## Set the data file for the 51Degrees module to use ## 99 | ## Update the FILE_PATH before running with Nginx ## 100 | 51D_file_path %%FILE_PATH%%; 101 | 102 | server { 103 | listen 127.0.0.1:8080; 104 | server_name localhost; 105 | 106 | location /response.html { 107 | # Enable the response headers to be set for Client-Hints support. 108 | 51D_set_resp_headers on; 109 | 110 | # Request browserVersion and IsMobile 111 | 51D_match_all x-hardwarevendor HardwareVendor; 112 | 51D_match_all x-hardwarename HardwareName; 113 | 51D_match_all x-devicetype DeviceType; 114 | 51D_match_all x-platformvendor PlatformVendor; 115 | 51D_match_all x-platformname PlatformName; 116 | 51D_match_all x-platformversion PlatformVersion; 117 | 51D_match_all x-browservendor BrowserVendor; 118 | 51D_match_all x-browsername BrowserName; 119 | 51D_match_all x-browserversion BrowserVersion; 120 | 121 | # Replace the content in returned html with found one. 122 | sub_filter 'HardwareVendor' $http_x_hardwarevendor; 123 | sub_filter 'HardwareName' $http_x_hardwarename; 124 | sub_filter 'DeviceType' $http_x_devicetype; 125 | sub_filter 'PlatformVendor' $http_x_platformvendor; 126 | sub_filter 'PlatformName' $http_x_platformname; 127 | sub_filter 'PlatformVersion' $http_x_platformversion; 128 | sub_filter 'BrowserVendor' $http_x_browservendor; 129 | sub_filter 'BrowserName' $http_x_browsername; 130 | sub_filter 'BrowserVersion' $http_x_browserversion; 131 | } 132 | } 133 | } 134 | # // Snippet End 135 | -------------------------------------------------------------------------------- /module_conf/hash_config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_51D_module 2 | 3 | if test -n "$ngx_module_link"; then 4 | ngx_module_type=HTTP 5 | ngx_module_name=ngx_http_51D_module 6 | ngx_module_srcs="$ngx_addon_dir/ngx_http_51D_module.c $ngx_addon_dir/src/*.c $ngx_addon_dir/src/hash/*.c $ngx_addon_dir/src/common-cxx/*.c" 7 | 8 | . auto/module 9 | else 10 | HTTP_MODULES="$HTTP_MODULES ngx_http_51D_module" 11 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_51D_module.c $ngx_addon_dir/src/*.c $ngx_addon_dir/src/hash/*.c $ngx_addon_dir/src/common-cxx/*.c" 12 | fi 13 | -------------------------------------------------------------------------------- /suppressions.txt: -------------------------------------------------------------------------------- 1 | leak:libpcre 2 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Set test constants. ## 4 | if [ "$REQUESTS" == "" ]; then 5 | REQUESTS=100000 6 | fi 7 | LIMIT=0.500 8 | RELOADLIMIT=1000 9 | if [ "$RELOADSTODO" == "" ]; then 10 | RELOADSTODO=5 11 | fi 12 | FAILED=0 13 | if [ "$DATA_FILE_NAME" == "" ]; then 14 | DATA_FILE_NAME="51Degrees-LiteV4.1.hash" 15 | fi 16 | 17 | ## Set text macros. ## 18 | RED='\033[0;31m' 19 | GREEN='\033[0;32m' 20 | NC='\033[0m' 21 | PASS="${GREEN}[Pass]${NC}\t" 22 | FAIL="${RED}[Fail]${NC}\t" 23 | 24 | HASH=`2>&1 ./nginx -V | grep -c "FIFTYONEDEGREES_HASH"` 25 | 26 | if [ ! -e "./nginx" ]; then 27 | printf "nginx is not installed in this directory. Install with \"make install\"\n" 28 | exit 29 | fi 30 | 31 | ## Write the config file to use for testing. ## 32 | printf "\n---------------------\n| Writing config file.\n--------------------\n" 33 | 34 | mv build/nginx.conf build/nginx.conf.bkp 35 | echo "worker_processes 4;" >> build/nginx.conf 36 | echo "worker_rlimit_core 500M;" >> build/nginx.conf 37 | echo "working_directory coredumps/;" >> build/nginx.conf 38 | echo "" >> build/nginx.conf 39 | echo "load_module modules/ngx_http_51D_module.so;" >> build/nginx.conf 40 | echo "" >> build/nginx.conf 41 | echo "events {" >> build/nginx.conf 42 | echo " worker_connections 1024;" >> build/nginx.conf 43 | echo "}" >> build/nginx.conf 44 | echo "" >> build/nginx.conf 45 | echo "http {" >> build/nginx.conf 46 | echo " ## set the datafile ##" >> build/nginx.conf 47 | echo " 51D_file_path ./device-detection-cxx/device-detection-data/${DATA_FILE_NAME};" >> build/nginx.conf 48 | echo "" >> build/nginx.conf 49 | echo " server {" >> build/nginx.conf 50 | echo " listen 8888;" >> build/nginx.conf 51 | echo "" >> build/nginx.conf 52 | echo " location / {" >> build/nginx.conf 53 | echo " 51D_match_all x-mobile IsMobile;" >> build/nginx.conf 54 | echo " add_header test \$http_x_mobile;" >> build/nginx.conf 55 | echo " }" >> build/nginx.conf 56 | echo " }" >> build/nginx.conf 57 | echo "}" >> build/nginx.conf 58 | 59 | printf "Written test config to \"build/nginx.conf\" and stored current one in \"build/nginx.conf.bkp\".\n" 60 | 61 | ## Make a directory for core dumps if one does not already exist. ## 62 | if [ ! -e "coredumps" ]; then 63 | mkdir coredumps 64 | fi 65 | 66 | ## Kill any current Nginx processes and start a new one. ## 67 | printf "\n---------------------\n| Starting new nginx process.\n---------------------\n" 68 | 69 | SLEEPCOUNT='0' 70 | while [ "$(pidof nginx)" ]; do 71 | killall nginx 72 | SLEEPCOUNT=$(($SLEEPCOUNT + 1)) 73 | if [ $SLEEPCOUNT -gt 5 ]; then 74 | printf "\n--------------------\n| Restoring previous config file.\n--------------------\n" 75 | rm build/nginx.conf 76 | mv build/nginx.conf.bkp build/nginx.conf 77 | printf "Removed test config \"build/nginx.conf\" and restored \"build/nginx.conf.bkp\".\n" 78 | printf "Failed to stop currently running nginx process\n" 79 | exit 80 | fi 81 | sleep 2 82 | done 83 | ./nginx 84 | printf "Nginx process started.\n" 85 | 86 | ## Test the server gives a valid response. ## 87 | printf "\n---------------------\n| Testing Server response.\n--------------------\n" 88 | 89 | RESPONSE=`curl -I 127.0.0.1:8888 | grep -c "200 OK"` 90 | if [ $RESPONSE -gt '0' ]; then 91 | printf "${PASS}Server Response: Server response 200 recieved.\n" 92 | else 93 | printf "${FAIL}Server Response: Server response 200 not recieved.\n" 94 | FAILED=$(($FAILED + 1)) 95 | fi 96 | 97 | ## Test the server matches desktop and mobile User-Agents correctly. ## 98 | printf "\n---------------------\n| Testing mobile/non-mobile User-Agents.\n--------------------\n" 99 | 100 | DESKTOPUA="Mozilla/5.0 (Windows NT 6.3; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0" 101 | MOBILEUA="Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) 'Version/7.0 Mobile/11D167 Safari/9537.53" 102 | TRUE='test: True' 103 | FALSE='test: False' 104 | 105 | CORRECTHEADER=`curl -A "$DESKTOPUA" -I 127.0.0.1:8888 | grep -c "$FALSE"` 106 | if [ $CORRECTHEADER -gt '0' ]; then 107 | printf "${PASS}Desktop User-Agent: matched correctly\n" 108 | else 109 | printf "${FAIL}Desktop User-Agent: matched incorrectly\n" 110 | FAILED=$(($FAILED + 1)) 111 | fi 112 | 113 | CORRECTHEADER=`curl -A "$MOBILEUA" -I 127.0.0.1:8888/ | grep -c "$TRUE"` 114 | if [ $CORRECTHEADER -gt '0' ]; then 115 | printf "${PASS}Mobile User-Agent: matched correctly\n" 116 | else 117 | printf "${FAIL}Mobile User-Agent: matched incorrectly\n" 118 | FAILED=$(($FAILED + 1)) 119 | fi 120 | 121 | ## Test the time per response when none of the User-Agents are stored in the cache. ## 122 | printf "\n---------------------\n| Testing ApacheBench stress with no cache.\n--------------------\n" 123 | 124 | TIME=`./tests/performance/build/ApacheBench-prefix/src/ApacheBench-build/bin/ab -c 10 -i -n $REQUESTS -U ./tests/performance/build/uas.csv 127.0.0.1:8888/ | grep "(mean, across all concurrent requests)" | egrep -o '\<[0-9]{1,2}\.[0-9]{2,5}\>'` 125 | if [ $TIME ]; then 126 | if [ $(echo "$TIME < $LIMIT" | bc -l) -eq 1 ]; then 127 | printf "${PASS}Stress test: $TIME ms per detection is within limit of $LIMIT.\n" 128 | else 129 | printf "${FAIL}Stress test: $TIME ms per detection exceedes limit of $LIMIT.\n" 130 | FAILED=$(($FAILED + 1)) 131 | fi 132 | BASETIME=$TIME 133 | else 134 | printf "${FAIL}Stress test reload: ApacheBench could not complete tests.\n" 135 | FAILED=$(($FAILED + 1)) 136 | fi 137 | 138 | ## Test the time per response when periodically reloading Nginx, and the time ## 139 | ## penalty involved with reloading (this should be around 0 unless there is a ## 140 | ## problem). ## 141 | printf "\n---------------------\n| Testing ApacheBench stress with reload.\n--------------------\n" 142 | 143 | RELOADCOUNT=0 144 | RELOADPASS=1 145 | TOTALTIME=0 146 | while [ $RELOADCOUNT -lt $RELOADSTODO ]; do 147 | printf "Doing nginx reload test $((RELOADCOUNT + 1)) of ${RELOADSTODO}\n" 148 | ./nginx -s reload 149 | RELOADCOUNT=$(($RELOADCOUNT + 1)) 150 | TIME=`./tests/performance/build/ApacheBench-prefix/src/ApacheBench-build/bin/ab -c 10 -i -n $(echo "$REQUESTS / $RELOADSTODO" | bc -l) -q -U ./tests/performance/build/uas.csv 127.0.0.1:8888/ | grep "mean, across all concurrent requests" | egrep -o '\<[0-9]{1,2}\.[0-9]{2,5}\>'` 151 | if [ ! "$TIME" ]; then 152 | break 153 | fi 154 | TOTALTIME=$(echo "$TOTALTIME + $TIME" | bc -l) 155 | done 156 | 157 | if [ "$TIME" ]; then 158 | TOTALTIME=0$(echo "scale=3; $TOTALTIME / $RELOADSTODO" | bc -l) 159 | RELOADTIME=$(echo "scale=0; ( $TOTALTIME - $BASETIME ) * $REQUESTS" | bc -l) 160 | if [ $(echo "$RELOADTIME < $RELOADLIMIT" | bc -l) -eq 1 ]; then 161 | printf "${PASS}Reload penalty: $RELOADTIME ms penalty on reloading is within limit of $RELOADLIMIT.\n" 162 | else 163 | printf "${FAIL}Reload penalty: $RELOADTIME ms penalty on reloading exceedes limit of $RELOADLIMIT.\n" 164 | FAILED=$(($FAILED + 1)) 165 | fi 166 | if [ $(echo "$TOTALTIME < $LIMIT" | bc -l) -eq 1 ]; then 167 | printf "${PASS}Stress test reload: $TOTALTIME ms per detection is within limit of $LIMIT.\n" 168 | else 169 | printf "${FAIL}Stress test reload: $TOTALTIME ms per detection exceedes limit of $LIMIT.\n" 170 | FAILED=$(($FAILED + 1)) 171 | fi 172 | else 173 | printf "${FAIL}Stress test reload: ApacheBench could not complete tests.\n" 174 | FAILED=$(($FAILED + 1)) 175 | fi 176 | 177 | ## Revert to the config file which was backed up earlier. ## 178 | printf "\n--------------------\n| Restoring previous config file.\n--------------------\n" 179 | 180 | rm build/nginx.conf 181 | mv build/nginx.conf.bkp build/nginx.conf 182 | printf "Removed test config \"build/nginx.conf\" and restored \"build/nginx.conf.bkp\".\n" 183 | ./nginx -s quit 184 | printf "Killed running nginx processes.\n" 185 | if [ $FAILED -gt '0' ]; then 186 | TESTCOLOUR=${RED} 187 | else 188 | TESTCOLOUR=${GREEN} 189 | fi 190 | 191 | ## Print the outcome of the tests. ## 192 | printf "\n${TESTCOLOUR}Finished tests with ${FAILED} failure(s).${NC}\n" 193 | 194 | ## If there were no core dumps, remove the directory. ## 195 | if [ ! "$(find coredumps -type f)" ]; then 196 | rmdir coredumps 197 | fi 198 | -------------------------------------------------------------------------------- /tests/51degrees.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # (C) Sergey Kandaurov 4 | # (C) Maxim Dounin 5 | # (C) Nginx, Inc. 6 | 7 | # Tests for 51Degrees Hash Trie module. 8 | 9 | ############################################################################### 10 | 11 | use warnings; 12 | use strict; 13 | use File::Temp qw/ tempdir /; 14 | use Test::More; 15 | 16 | BEGIN { use FindBin; chdir($FindBin::Bin); } 17 | 18 | use lib 'nginx-tests/lib'; 19 | use Test::Nginx; 20 | use URI::Escape; 21 | 22 | ############################################################################### 23 | 24 | select STDERR; $| = 1; 25 | select STDOUT; $| = 1; 26 | 27 | my $n = 30; 28 | my $t_lite = 1; 29 | 30 | # The Lite data file version does not contains properties that can be used 31 | # to test the overrides feature. Please consider to use one that have at least 32 | # ScreenPixelsWidth and ScreenPixelsWidthJavascript properties 33 | if (scalar(@ARGV) > 0 && index($ARGV[0], "51Degrees-Lite") == -1) { 34 | $n += 20; 35 | $t_lite = 0; 36 | } 37 | 38 | my $t = Test::Nginx->new()->has(qw/http/)->plan($n) 39 | ->write_file_expand('nginx.conf', <<'EOF'); 40 | 41 | daemon off; 42 | 43 | %%TEST_GLOBALS%% 44 | events { 45 | } 46 | 47 | http { 48 | %%TEST_GLOBALS_HTTP%% 49 | 50 | 51D_value_separator ^sep^; 51 | 51D_performance_profile IN_MEMORY; 52 | 51D_drift 1; 53 | 51D_difference 1; 54 | 51D_allow_unmatched on; 55 | 56 | 51D_match_ua x-main-ismobile-single IsMobile; 57 | 51D_match_all x-main-ismobile-all IsMobile; 58 | 59 | 51D_set_resp_headers on; 60 | 61 | server { 62 | listen 127.0.0.1:8081; 63 | server_name localhost1; 64 | 65 | location /clienthints { 66 | # Do nothings 67 | } 68 | 69 | location /addchheaders { 70 | 51D_set_resp_headers off; 71 | add_header Accept-CH Test1,Test2,Test3; 72 | } 73 | } 74 | 75 | server { 76 | listen 127.0.0.1:8080; 77 | server_name localhost; 78 | 79 | 51D_match_ua x-server-ismobile-single IsMobile; 80 | 51D_match_all x-server-ismobile-all IsMobile; 81 | 51D_set_resp_headers off; 82 | 83 | location /single { 84 | 51D_match_single x-ismobile IsMobile; 85 | 51D_match_single x-hardware-model HardwareModel; 86 | 51D_match_single x-hardware-vendor HardwareVendor; 87 | 51D_match_single x-platform-version PlatformVersion; 88 | add_header x-ismobile $http_x_ismobile; 89 | add_header x-hardware-model $http_x_hardware_model; 90 | add_header x-hardware-vendor $http_x_hardware_vendor; 91 | add_header x-platform-version $http_x_platform_version; 92 | } 93 | 94 | location /ua { 95 | 51D_match_ua x-ismobile IsMobile; 96 | 51D_match_ua x-hardware-model HardwareModel; 97 | 51D_match_ua x-hardware-vendor HardwareVendor; 98 | 51D_match_ua x-platform-version PlatformVersion; 99 | add_header x-ismobile $http_x_ismobile; 100 | add_header x-hardware-model $http_x_hardware_model; 101 | add_header x-hardware-vendor $http_x_hardware_vendor; 102 | add_header x-platform-version $http_x_platform_version; 103 | } 104 | 105 | location /ua_uach { 106 | 51D_match_ua_client_hints x-ismobile IsMobile; 107 | 51D_match_ua_client_hints x-browsername BrowserName; 108 | 51D_match_ua_client_hints x-hardware-model HardwareModel; 109 | 51D_match_ua_client_hints x-hardware-vendor HardwareVendor; 110 | 51D_match_ua_client_hints x-platform-version PlatformVersion; 111 | add_header x-ismobile $http_x_ismobile; 112 | add_header x-browsername $http_x_browsername; 113 | add_header x-hardware-model $http_x_hardware_model; 114 | add_header x-hardware-vendor $http_x_hardware_vendor; 115 | add_header x-platform-version $http_x_platform_version; 116 | } 117 | 118 | location /all { 119 | 51D_match_all x-ismobile IsMobile; 120 | 51D_match_all x-browsername BrowserName; 121 | 51D_match_all x-hardware-model HardwareModel; 122 | 51D_match_all x-hardware-vendor HardwareVendor; 123 | 51D_match_all x-platform-version PlatformVersion; 124 | add_header x-ismobile $http_x_ismobile; 125 | add_header x-browsername $http_x_browsername; 126 | add_header x-hardware-model $http_x_hardware_model; 127 | add_header x-hardware-vendor $http_x_hardware_vendor; 128 | add_header x-platform-version $http_x_platform_version; 129 | } 130 | 131 | location /metrics { 132 | 51D_match_ua x-iterations Iterations; 133 | 51D_match_ua x-drift Drift; 134 | 51D_match_ua x-difference Difference; 135 | 51D_match_ua x-method Method; 136 | 51D_match_ua x-useragents UserAgents; 137 | 51D_match_ua x-matchednodes MatchedNodes; 138 | 51D_match_ua x-deviceid DeviceId; 139 | add_header x-metrics $http_x_Iterations,$http_x_drift,$http_x_difference,$http_x_method,$http_x_matchedNodes,$http_x_deviceid; 140 | add_header x-metrics-ua $http_x_useragents; 141 | } 142 | 143 | location /more-properties { 144 | 51D_match_ua x-more-properties Ismobile,BrowserName; 145 | add_header x-more-properties $http_x_more_properties; 146 | } 147 | 148 | location /non-property { 149 | 51D_match_ua x-non-property thisisnotarealpropertyname; 150 | add_header x-non-property $http_x_non_property; 151 | } 152 | 153 | location /redirect { 154 | 51D_match_ua x-ismobile IsMobile; 155 | if ($http_x_ismobile ~ "True") { 156 | return 301 https://mobilesite; 157 | } 158 | } 159 | 160 | location /locations { 161 | 51D_match_ua x-ismobile-single IsMobile; 162 | 51D_match_all x-ismobile-all IsMobile; 163 | add_header x-location-ismobile $http_x_ismobile_single$http_x_ismobile_all; 164 | add_header x-server-ismobile $http_x_server_ismobile_single$http_x_server_ismobile_all; 165 | add_header x-main-ismobile $http_x_main_ismobile_single$http_x_main_ismobile_all; 166 | } 167 | 168 | location /variable { 169 | 51D_match_ua x-ismobile-from-var IsMobile $arg_ua; 170 | 51D_match_ua x-ismobile-from-wrong-var IsMobile $ua; 171 | add_header x-ismobile-from-var $http_x_ismobile_from_var; 172 | add_header x-ismobile-from-wrong-var $http_x_ismobile_from_wrong_var; 173 | } 174 | 175 | location /overrides { 176 | 51D_match_all x-javascript ScreenPixelsWidthJavascript; 177 | 51D_match_all x-screenpixelswidth ScreenPixelsWidth; 178 | add_header x-screenpixelswidth $http_x_screenpixelswidth; 179 | } 180 | 181 | location /clienthints { 182 | 51D_set_resp_headers on; 183 | } 184 | 185 | location /clienthintsoff { 186 | 51D_set_resp_headers off; 187 | } 188 | 189 | location /clienthintsnone { 190 | # Do nothing 191 | } 192 | 193 | location /51D-single.js { 194 | 51D_get_javascript_single JavascriptHardwareProfile; 195 | } 196 | 197 | location /51D-single-query.js { 198 | 51D_get_javascript_single JavascriptHardwareProfile $arg_ua; 199 | } 200 | 201 | location /51D-all.js { 202 | 51D_set_resp_headers on; 203 | 51D_get_javascript_all JavascriptHardwareProfile; 204 | } 205 | 206 | location /51D-non-property.js { 207 | 51D_set_resp_headers on; 208 | 51D_get_javascript_single thisisnotarealpropertyname; 209 | } 210 | 211 | location /51D-chua.js { 212 | 51D_set_resp_headers on; 213 | 51D_get_javascript_all ScreenPixelsWidthJavascript; 214 | } 215 | 216 | location /addchheaders { 217 | 51D_set_resp_headers on; 218 | proxy_pass http://127.0.0.1:8081/addchheaders; 219 | } 220 | } 221 | } 222 | 223 | EOF 224 | 225 | $t->write_file('single', ''); 226 | $t->write_file('ua', ''); 227 | $t->write_file('ua_uach', ''); 228 | $t->write_file('all', ''); 229 | $t->write_file('metrics', ''); 230 | $t->write_file('more-properties', ''); 231 | $t->write_file('non-property', ''); 232 | $t->write_file('redirect', ''); 233 | $t->write_file('locations', ''); 234 | $t->write_file('variable', ''); 235 | $t->write_file('overrides', ''); 236 | $t->write_file('clienthints', ''); 237 | $t->write_file('clienthintsoff', ''); 238 | $t->write_file('clienthintsnone', ''); 239 | $t->write_file('addchheaders', ''); 240 | $t->write_file('51D-chua.js', ''); 241 | 242 | $t->run(); 243 | 244 | sub get_content_with_ua { 245 | my ($uri, $ua) = @_; 246 | return http(<reload(); 406 | 407 | ############################################################################### 408 | # Test config blocks. 409 | ############################################################################### 410 | 411 | # Match desktop in server and location blocks. 412 | $r = get_with_ua('/locations', $desktopUserAgent); 413 | like($r, qr/x-server-ismobile: FalseFalse/, 'Desktop match in server block'); 414 | like($r, qr/x-location-ismobile: FalseFalse/, 'Desktop match in location block'); 415 | like($r, qr/x-main-ismobile: FalseFalse/, 'Desktop match in main block'); 416 | 417 | # Match mobile in server and location blocks. 418 | $r = get_with_ua('/locations', $mobileUserAgent); 419 | like($r, qr/x-server-ismobile: TrueTrue/, 'Mobile match in server block'); 420 | like($r, qr/x-location-ismobile: TrueTrue/, 'Mobile match in location block'); 421 | like($r, qr/x-main-ismobile: TrueTrue/, 'Mobile match in main block'); 422 | 423 | ############################################################################### 424 | # Test multiple HTTP header functionality. 425 | ############################################################################### 426 | 427 | # Match mobile User-Agent in Opera header. 428 | $r = get_with_opera_header('/all', $mobileUserAgent); 429 | like($r, qr/x-ismobile: True/, 'Opera mobile header'); 430 | 431 | # Match desktop User-Agent in Opera header. 432 | $r = get_with_opera_header('/all', $desktopUserAgent); 433 | like($r, qr/x-ismobile: False/, 'Opera desktop header'); 434 | 435 | ############################################################################### 436 | # Test multiple HTTP header functionality (should fail for `ua_uach` mode) 437 | ############################################################################### 438 | 439 | # Match mobile User-Agent in Opera header. 440 | $r = get_with_opera_header('/ua_uach', $mobileUserAgent); 441 | like($r, qr/x-ismobile: NoMatch/, 'Opera mobile header (unrecognized by ua_uach)'); 442 | 443 | # Match desktop User-Agent in Opera header. 444 | $r = get_with_opera_header('/ua_uach', $desktopUserAgent); 445 | like($r, qr/x-ismobile: NoMatch/, 'Opera desktop header (unrecognized by ua_uach)'); 446 | 447 | ############################################################################### 448 | # Test extended matching from query String. 449 | ############################################################################### 450 | 451 | # Match with a parameter passed from the config. 452 | $r = get_with_ua('/variable?ua='.uri_escape($mobileUserAgent), $desktopUserAgent); 453 | like($r, qr/x-ismobile-from-var: True/, 'Match single from parameter'); 454 | like($r, qr/x-ismobile-from-wrong-var: False/, 'Match single from parameter with wrong name'); 455 | 456 | # Match all with a parameter passed from the config. 457 | $r = get_with_ua('/all?User-Agent='.uri_escape($mobileUserAgent), $desktopUserAgent); 458 | like($r, qr/x-ismobile: True/, 'Match all from parameter'); 459 | 460 | ############################################################################### 461 | # Test overrides 462 | ############################################################################### 463 | 464 | # In order to test the overrides feature, a data file that contains at least 465 | # ScreenPixelsWidth and ScreenPixelsWidthJavascript properties are required 466 | if (!$t_lite) { 467 | # Match with override value from cookie. 468 | $r = get_with_ua_cookie('/overrides', $desktopUserAgent, '51D_ScreenPixelsWidth=20'); 469 | like($r, qr/x-screenpixelswidth: 20/, 'Screen Pixels Width value was not overriden by value in cookie.'); 470 | 471 | # Match with override value from query string. 472 | $r = get_with_ua('/overrides?51D_ScreenPixelsWidth=20', $desktopUserAgent); 473 | like($r, qr/x-screenpixelswidth: 20/, 'Screen Pixels Width value was not overriden by value in query string.'); 474 | } 475 | 476 | ############################################################################### 477 | # Test response headers 478 | ############################################################################### 479 | 480 | # TODO: Enable and UACH is supported 481 | if (!$t_lite && 0) { 482 | # Set Client Hints response headers locally 483 | $r = get_with_ua('/clienthints', $chrome89UserAgent); 484 | like($r, qr/Accept-CH: SEC-CH-[a-zA-Z-]+(,SEC-CH-[a-zA-Z-]+)*/, 'Set Client Hints response header locally.'); 485 | 486 | # Set Client Hints response headers locally 487 | $r = get_with_ua_common('localhost1', '/clienthints', $chrome89UserAgent); 488 | like($r, qr/Accept-CH: SEC-CH-[a-zA-Z-]+(,SEC-CH-[a-zA-Z-]+)*/, 'Set Client Hints response header locally.'); 489 | 490 | # Set No Client Hints response headers 491 | $r = get_with_ua('/clienthintsoff', $chrome89UserAgent); 492 | unlike($r, qr/Accept-CH: SEC-CH-[a-zA-Z-]+(,SEC-CH-[a-zA-Z-]+)*/, 'Set No Client Hints response header locally.'); 493 | 494 | # No Client Hints response headers set. Relies on Main and Server settings. 495 | $r = get_with_ua('/clienthintsnone', $chrome89UserAgent); 496 | unlike($r, qr/Accept-CH: SEC-CH-[a-zA-Z-]+(,SEC-CH-[a-zA-Z-]+)*/, 'Set Client Hints response header locally.'); 497 | 498 | # Append Client Hints to already create headers. 499 | $r = get_with_ua_common('localhost', '/addchheaders', $chrome89UserAgent); 500 | like($r, qr/Accept-CH: Test1,Test2,Test3,SEC-CH-[a-zA-Z-]+(,SEC-CH-[a-zA-Z-]+)*/, 'Append Client Hints to existing headers.'); 501 | } 502 | 503 | ############################################################################### 504 | # Test CDN javascript 505 | ############################################################################### 506 | 507 | # In order to test the Javascript feature, JavascriptHardwareProfile is required. 508 | if (!$t_lite) { 509 | # Javascript using single User-Agent header. 510 | $r = get_content_with_ua('/51D-single.js', $mobileUserAgent); 511 | like($r, qr/getProfileId/, 'Javascript response content using single User-Agent header.'); 512 | 513 | # Javascript using single User-Agent from query string. 514 | $r = get_content_with_ua('/51D-single-query.js?ua='.uri_escape($mobileUserAgent), $desktopUserAgent); 515 | like($r, qr/getProfileId/, 'Javascript response content using variable.'); 516 | 517 | # Javascript using all evidence. 518 | $r = get_content_with_ua('/51D-all.js', $mobileUserAgent); 519 | like($r, qr/getProfileId/, 'Javascript response content using all evidence.'); 520 | 521 | # Javascript using query. 522 | $r = get_content_with_ua('/51D-all.js?User-Agent='.uri_escape($mobileUserAgent), $desktopUserAgent); 523 | like($r, qr/getProfileId/, 'Javascript response content using all evidence including query string.'); 524 | 525 | # Javascript using property that is not supported. 526 | $r = get_content_with_ua('/51D-single.js', $desktopUserAgent); 527 | like($r, qr/51Degrees Javascript not available/, 'Javascript response content body from non supported user agent'); 528 | 529 | # Javascript using property with Client Hints response headers on. 530 | $r = get_content_with_ua('/51D-chua.js', $chrome89UserAgent); 531 | like($r, qr/document\.cookie.*51D_ScreenPixelsWidth.*/, 'Javascript response content to get ScreenPixelsWidth.'); 532 | # TODO: Enable once UACH is supported 533 | if (0) { 534 | like($r, qr/Accept-CH: SEC-CH-[a-zA-Z-]+(,SEC-CH-[a-zA-Z-]+)*/, 'Set Client Hints response header locally.'); 535 | } 536 | } 537 | 538 | # Javascript using non property. 539 | $r = get_content_with_ua('/51D-non-property.js', $mobileUserAgent); 540 | like($r, qr/51Degrees Javascript not available/, 'Javascript response content body from non supported property'); 541 | 542 | # Javascript missing User-Agent. 543 | $r = http_get('/51D-single.js'); 544 | like($r, qr/51Degrees Javascript not available/, 'Javascript response content body with missing User-Agent'); 545 | 546 | ############################################################################### 547 | # Test use cases. 548 | ############################################################################### 549 | 550 | # Redirect mobile using the IsMobile property. 551 | $r = get_with_ua('/redirect', $mobileUserAgent); 552 | like($r, qr/301 Moved Permanently/, 'Redirected to mobile'); 553 | 554 | # Redirect desktop using the IsMobile property. 555 | $r = get_with_ua('/redirect', $desktopUserAgent); 556 | unlike($r, qr/301 Moved Permanently/, 'Didn\'t redirect for desktop'); 557 | 558 | ############################################################################### 559 | 560 | # Print out warnings at the end for user attention 561 | if ($t_lite) { 562 | print "WARNING: Overrides and Javascript feature test will not be run \n"; 563 | print " as the Lite version of data file does not contains the \n"; 564 | print " required properties. Please consider to obtain a data \n"; 565 | print " file that have at least the ScreenPixelsWidth, \n"; 566 | print " ScreenPixelsWidthJavascript and \n"; 567 | print " JavascriptHardwareProfile supported.\n"; 568 | } 569 | -------------------------------------------------------------------------------- /tests/examples/config.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # (C) Sergey Kandaurov 4 | # (C) Maxim Dounin 5 | # (C) Nginx, Inc. 6 | 7 | # Tests for 51Degrees Hash Trie module. 8 | 9 | ############################################################################### 10 | 11 | use warnings; 12 | use strict; 13 | use File::Temp qw/ tempdir /; 14 | use Test::More; 15 | use File::Copy; 16 | 17 | BEGIN { use FindBin; chdir($FindBin::Bin); } 18 | 19 | use lib '../nginx-tests/lib'; 20 | use Test::Nginx; 21 | use URI::Escape; 22 | 23 | ############################################################################### 24 | 25 | select STDERR; $| = 1; 26 | select STDOUT; $| = 1; 27 | 28 | sub read_example($) { 29 | my ($name) = @_; 30 | open my $fh, '<', '../../examples/hash/' . $name or die "Can't open file $name: $!"; 31 | read $fh, my $content, -s $fh; 32 | close $fh; 33 | 34 | return $content; 35 | } 36 | 37 | my $t = Test::Nginx->new()->has(qw/http/)->plan(2); 38 | 39 | my $t_file = read_example('config.conf'); 40 | # Remove the comment block 41 | $t_file =~ s/\/\*\*.+\*\//''/gmse; 42 | # Updated all variable place holders 43 | $t_file =~ s/%%DAEMON_MODE%%/'off'/gmse; 44 | $t_file =~ s/%%MODULE_PATH%%/$ENV{TEST_MODULE_PATH}/gmse; 45 | $t_file =~ s/%%FILE_PATH%%/$ENV{TEST_FILE_PATH}/gmse; 46 | $t->write_file_expand('nginx.conf', $t_file); 47 | 48 | $t->write_file('config', ''); 49 | 50 | $t->run(); 51 | 52 | sub get_with_ua { 53 | my ($uri, $ua) = @_; 54 | return http(<new()->has(qw/http/)->plan(3); 38 | 39 | my $t_file = read_example('gettingStarted.conf'); 40 | # Remove the documentation block 41 | $t_file =~ s/\/\*\*.+\*\//''/gmse; 42 | # Replace all variable place holders. 43 | $t_file =~ s/%%DAEMON_MODE%%/'off'/gmse; 44 | $t_file =~ s/%%MODULE_PATH%%/$ENV{TEST_MODULE_PATH}/gmse; 45 | $t_file =~ s/%%FILE_PATH%%/$ENV{TEST_FILE_PATH}/gmse; 46 | $t->write_file_expand('nginx.conf', $t_file); 47 | 48 | $t->write_file('hash', ''); 49 | 50 | $t->run(); 51 | 52 | sub get_with_ua { 53 | my ($uri, $ua) = @_; 54 | return http(< { 51 | test('Javascript in Chrome', async () => { 52 | let driver = await new Builder() 53 | .forBrowser('chrome') 54 | .setChromeOptions(cOpts.headless()) 55 | .build(); 56 | await cdnTest(driver); 57 | }, 20000); 58 | 59 | test('Javascript in Firefox', async () => { 60 | let driver = await new Builder() 61 | .forBrowser('firefox') 62 | .setFirefoxOptions(ffOpts.headless()) 63 | .build(); 64 | await cdnTest(driver); 65 | }, 20000); 66 | 67 | // Edge 108 is currently has issues starting in headless mode. 68 | // This test will be reinstated once the problem is solved. 69 | // test('Javascript in Microsoft Edge', async () => { 70 | // let driver = await new Builder() 71 | // .forBrowser('MicrosoftEdge') 72 | // .setEdgeOptions(eOpts.headless()) 73 | // .build(); 74 | // await cdnTest(driver); 75 | // }, 20000); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/examples/jsExample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fiftyone.test.cdn", 3 | "version": "1.0.0", 4 | "description": "Test for device-detection-nginx CDN support", 5 | "main": "cdn.test.js", 6 | "directories": { 7 | "test": "./" 8 | }, 9 | "scripts": { 10 | "test": "jest --ci --reportes=default --reporters=jest-junit" 11 | }, 12 | "jest-junit": { 13 | "outputName": "cdn_test_results.xml" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/51Degrees/device-detection-nginx/tests/example/jsExample" 18 | }, 19 | "author": "51Degrees", 20 | "contributors": [ 21 | "Filip Hnízdo (https://octophindigital.com/)", 22 | "Steve Ballantine (https://51degrees.com)", 23 | "Ben Shilito (https://51degrees.com)", 24 | "Joseph Dix (https://51degrees.com)", 25 | "Tung Pham (https://51degrees.com)" 26 | ], 27 | "dependencies": { 28 | "jest-junit": "latest", 29 | "selenium-webdriver": "^4.0.0-beta.2" 30 | }, 31 | "devDependencies": { 32 | "eslint": "^6.8.0", 33 | "eslint-config-standard": "^14.1.1", 34 | "eslint-plugin-import": "^2.20.2", 35 | "eslint-plugin-jest": "^23.13.2", 36 | "eslint-plugin-jsdoc": "^25.4.0", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-standard": "^4.0.1", 40 | "jest": "^24.9.0", 41 | "jest-junit": "^9.0.0" 42 | }, 43 | "license": "EUPL-1.2", 44 | "files": [ 45 | "build", 46 | "*.js" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /tests/examples/matchMetrics.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # (C) Sergey Kandaurov 4 | # (C) Maxim Dounin 5 | # (C) Nginx, Inc. 6 | 7 | # Tests for 51Degrees Hash Trie module. 8 | 9 | ############################################################################### 10 | 11 | use warnings; 12 | use strict; 13 | use File::Temp qw/ tempdir /; 14 | use Test::More; 15 | use File::Copy; 16 | 17 | BEGIN { use FindBin; chdir($FindBin::Bin); } 18 | 19 | use lib '../nginx-tests/lib'; 20 | use Test::Nginx; 21 | use URI::Escape; 22 | 23 | ############################################################################### 24 | 25 | select STDERR; $| = 1; 26 | select STDOUT; $| = 1; 27 | 28 | sub read_example($) { 29 | my ($name) = @_; 30 | open my $fh, '<', '../../examples/hash/' . $name or die "Can't open file $name: $!"; 31 | read $fh, my $content, -s $fh; 32 | close $fh; 33 | 34 | return $content; 35 | } 36 | 37 | my $t = Test::Nginx->new()->has(qw/http/)->plan(3); 38 | 39 | my $t_file = read_example('matchMetrics.conf'); 40 | # Remove documentation block 41 | $t_file =~ s/\/\*\*.+\*\//''/gmse; 42 | # Replace variable place holders 43 | $t_file =~ s/%%DAEMON_MODE%%/'off'/gmse; 44 | $t_file =~ s/%%MODULE_PATH%%/$ENV{TEST_MODULE_PATH}/gmse; 45 | $t_file =~ s/%%FILE_PATH%%/$ENV{TEST_FILE_PATH}/gmse; 46 | $t->write_file_expand('nginx.conf', $t_file); 47 | 48 | $t->write_file('metrics', ''); 49 | 50 | $t->run(); 51 | 52 | sub get_with_ua { 53 | my ($uri, $ua) = @_; 54 | return http(<new()->has(qw/http/)->plan(2); 38 | 39 | my $t_file = read_example('matchQuery.conf'); 40 | # Remove documentation block 41 | $t_file =~ s/\/\*\*.+\*\//''/gmse; 42 | # Remove all variable place holders 43 | $t_file =~ s/%%DAEMON_MODE%%/'off'/gmse; 44 | $t_file =~ s/%%MODULE_PATH%%/$ENV{TEST_MODULE_PATH}/gmse; 45 | $t_file =~ s/%%FILE_PATH%%/$ENV{TEST_FILE_PATH}/gmse; 46 | $t->write_file_expand('nginx.conf', $t_file); 47 | 48 | $t->write_file('query', ''); 49 | 50 | $t->run(); 51 | 52 | sub get_with_ua { 53 | my ($uri, $ua) = @_; 54 | return http(< 0 && index($ARGV[0], "51Degrees-Lite") == -1) { 44 | $n += 0; 45 | $t_lite = 0; 46 | } 47 | my $t = Test::Nginx->new()->has(qw/http/)->plan($n); 48 | 49 | my $t_file = read_example('responseHeader.conf'); 50 | # Remove documentation block 51 | $t_file =~ s/\/\*\*.+\*\//''/gmse; 52 | # Replace all variable place holder 53 | $t_file =~ s/%%DAEMON_MODE%%/'off'/gmse; 54 | $t_file =~ s/%%MODULE_PATH%%/$ENV{TEST_MODULE_PATH}/gmse; 55 | $t_file =~ s/%%FILE_PATH%%/$ENV{TEST_FILE_PATH}/gmse; 56 | $t->write_file_expand('nginx.conf', $t_file); 57 | 58 | my $t_html_file = read_example('response.html'); 59 | $t->write_file('response.html', $t_html_file); 60 | 61 | $t->run(); 62 | 63 | sub get_with_ua { 64 | my ($uri, $ua) = @_; 65 | return http(</dev/null && pwd)" 5 | PASSES=20000 6 | HOST=127.0.0.1:3000 7 | CAL=calibrate 8 | PRO=process 9 | PERF=./ApacheBench-prefix/src/ApacheBench-build/bin/runPerf.sh 10 | 11 | # Get the repo directory as an absolute path 12 | ORIGINPATH="$(pwd)" 13 | cd $FULLPATH/../../../ 14 | REPO_DIR="$(pwd)" 15 | cd $ORIGINPATH 16 | 17 | echo $REPO_DIR 18 | 19 | # Create the nginx.conf 20 | MODULES_DIR=$REPO_DIR/build/modules 21 | DATA_FILE_DIR=$REPO_DIR/device-detection-cxx/device-detection-data 22 | sed "s/\${MODULES_DIR}/${MODULES_DIR//\//\\/}/g" ./nginx.conf.template > ./nginx.conf 23 | sed -i "s/\${DATA_FILE_DIR}/${DATA_FILE_DIR//\//\\/}/g" ./nginx.conf 24 | if [ "$DATA_FILE_NAME" ]; then 25 | sed -i "s/51Degrees-LiteV4\.1\.hash/${DATA_FILE_NAME}/g" nginx.conf 26 | fi 27 | 28 | # Create required directories and target files for service 29 | echo > ../../../build/html/calibrate 30 | echo > ../../../build/html/process 31 | 32 | # Create coredumps folder 33 | mkdir -p coredumps 34 | 35 | # Backup the current config and replace with the test config 36 | mv $REPO_DIR/build/nginx.conf $REPO_DIR/build/nginx.conf.bkp 37 | cp $FULLPATH/nginx.conf $REPO_DIR/build/nginx.conf 38 | 39 | # Run the performrance 40 | $PERF -n $PASSES -s "$REPO_DIR/nginx" -t "$REPO_DIR/nginx -s stop" -c $CAL -p $PRO -h $HOST 41 | 42 | # Replace the original config 43 | mv $REPO_DIR/build/nginx.conf.bkp $REPO_DIR/build/nginx.conf 44 | 45 | # Remove coredumps folder 46 | if [ ! "$(find coredumps -type f)" ]; then 47 | rmdir coredumps 48 | fi 49 | --------------------------------------------------------------------------------