├── .gitignore ├── examples ├── sample.vcl └── accept-language.vcl ├── t ├── basic.t ├── q.t ├── composite.t ├── crash-me.t └── test.pm ├── Makefile ├── gen_vcl.pl ├── README └── accept-language.c /.gitignore: -------------------------------------------------------------------------------- 1 | accept-language 2 | accept-language.vcl 3 | -------------------------------------------------------------------------------- /examples/sample.vcl: -------------------------------------------------------------------------------- 1 | # Sample VCL file 2 | # ... 3 | 4 | include "/etc/varnish/accept-language.vcl"; 5 | 6 | # Everything proceeds as normal 7 | sub vcl_recv { 8 | 9 | # ... 10 | C{ 11 | vcl_rewrite_accept_language(sp); 12 | }C 13 | 14 | # ... 15 | # return(lookup); 16 | 17 | return(pass); 18 | } 19 | 20 | sub vcl_fetch { 21 | 22 | # ... 23 | 24 | # Store different versions of the resource by the 25 | # content of the new X-Varnish-Accept-Language header 26 | set beresp.http.Vary = "X-Varnish-Accept-Language"; 27 | 28 | # ... 29 | # return(deliver); 30 | 31 | return(pass); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /t/basic.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | # 4 | # Basic Accept-Language tests 5 | # 6 | 7 | use strict; 8 | require test; 9 | 10 | my @langs = qw(bg cs da en fi fy hu it ja no pl ru tr vn); 11 | 12 | Test::More::plan(tests => 5 + (@langs * 3)); 13 | 14 | test::update_binary(); 15 | test::is_lang('en', 'en'); 16 | test::is_lang('', 'en'); 17 | 18 | # Basically test for supported languages 19 | for (@langs) { 20 | my $lang = $_; 21 | test::is_lang($lang, $lang); 22 | test::is_lang("$lang,xy-zx;q=0.01", $_); 23 | test::is_lang("$lang,en;q=0.99", $_); 24 | } 25 | 26 | test::is_lang('xy', 'en'); 27 | test::is_lang('xy-XX', 'en'); 28 | 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Set your language preferences here 3 | # 4 | DEFAULT_LANGUAGE ?= en 5 | SUPPORTED_LANGUAGES ?= bg cs da de en es fi fy hu it ja no pl ru sq sk tr uk vn xx-lol zh-cn 6 | 7 | CC=cc 8 | CPP=cpp -C -P -E 9 | DEBUG= 10 | #DEBUG=-g3 11 | 12 | all: accept-language accept-language.vcl 13 | 14 | accept-language: accept-language.c 15 | $(CC) -Wall -pedantic $(DEBUG) -o accept-language accept-language.c 16 | 17 | accept-language.vcl: Makefile accept-language.c gen_vcl.pl 18 | ./gen_vcl.pl $(DEFAULT_LANGUAGE) $(SUPPORTED_LANGUAGES) < accept-language.c > accept-language.vcl 19 | 20 | test: 21 | prove -I./t -v ./t 22 | 23 | clean: 24 | $(RM) $(shell cat .gitignore) 25 | -------------------------------------------------------------------------------- /t/q.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | # 4 | # Accept-Language "q"-related tests 5 | # 6 | 7 | use strict; 8 | require test; 9 | 10 | my @tests = ( 11 | [ 'xy,en;q=0.2,ru;q=0.9', 'ru' ], 12 | [ 'xy,en;q=0.97,ru;q=0.98,it;q=0.99', 'it' ], 13 | [ 'ru-RU,ru;q=0.9,en;q=0.8', 'ru' ], 14 | [ '', 'en' ], 15 | [ 'en-us,en;q=0.9', 'en' ], 16 | [ 'en-US,en;q=0.9', 'en' ], 17 | [ 'ru,en;q=0.9', 'ru' ], 18 | [ '*,uk;q=0.2,fr;q=0.1', 'uk', 'Wildcard * should be last one' ], 19 | [ 'de;q=0.8,pl,fr;q=0.2', 'pl', 'Unspecified q means q=1, regardless of position' ], 20 | ); 21 | 22 | Test::More::plan(tests => @tests + 1); 23 | test::update_binary(); 24 | 25 | for (@tests) { 26 | my ($header, $lang, $message) = @{ $_ }; 27 | test::is_lang($header, $lang, $message); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /t/composite.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | # 4 | # Accept-Language composite language tests 5 | # 6 | 7 | use strict; 8 | require test; 9 | 10 | my @tests = ( 11 | [ 'en-US,en-GB;q=0.9,de-AT;q=0.8,de;q=0.7' => 'en', 'Composite language en-US should be treated as "en"' ], 12 | [ 'de-at' => 'de', 'Composite language de-at that is not supported should fallback to "de"' ], 13 | [ 'de-AT' => 'de', 'Composite language de-AT that is not supported should fallback to "de"' ], 14 | [ 'zh-cn' => 'zh-cn', 'Composite language that is supported should be detected correctly' ], 15 | [ 'zh-CN' => 'zh-cn', 'Composite language that is supported should be detected correctly' ], 16 | ); 17 | 18 | Test::More::plan(tests => @tests + 1); 19 | test::update_binary(); 20 | 21 | for (@tests) { 22 | my ($header, $lang, $message) = @{ $_ }; 23 | test::is_lang($header, $lang, $message); 24 | } 25 | 26 | -------------------------------------------------------------------------------- /t/crash-me.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | # 4 | # Accept-Language really long strings 5 | # 6 | 7 | use strict; 8 | require test; 9 | 10 | # 11 | # These are all test cases from the actual web access logs 12 | # 13 | 14 | my @tests = ( 15 | 16 | # Try to overflow the max static buffer allocated by accept-language.c 17 | [ 'fr,en;q=0.9,ja;q=0.8,de;q=0.7,es;q=0.6,it;q=0.5,pt;q=0.4,pt-PT;q=0.3,nl;q=0.2,sv;q=0.1,nb;q=0.1,da;q=0.1,fi;q=0.1,ru;q=0.1,pl;q=0.1,zh-CN;q=0.1,zh-TW;q=0.1,ko;q=0.1' => 'en' ], 18 | 19 | [ 'fr,en;q=0.9,ja;q=0.8,de;q=0.7,es;q=0.6,it;q=0.5,pt;q=0.4,pt-PT;q=0.3,nl;q=0.2,sv;q=0.1,nb;q=0.1,da;q=0.1,fi;q=0.1,ru;q=0.1,pl;q=0.1,zh-CN;q=0.1,zh-TW;q=0.1,ko;q=0.1,fr,en;q=0.9,ja;q=0.8,de;q=0.7,es;q=0.6,it;q=0.5,pt;q=0.4,pt-PT;q=0.3,nl;q=0.2,sv;q=0.1,nb;q=0.1,da;q=0.1,fi;q=0.1,ru;q=0.1,pl;q=0.1,zh-CN;q=0.1,zh-TW;q=0.1,ko;q=0.1' => 'en' ], 20 | 21 | # Malformed string 22 | [ 'en-us,en;q=0.5,x-ns1w9Ea$X$dNhK,x-ns2Ef70Nnym7b6' => 'en' ], 23 | 24 | # Strange case of the crashing avatars 25 | [ 'en-US,en;q=0.9' => 'en' ], 26 | 27 | ); 28 | 29 | Test::More::plan(tests => @tests + 1); 30 | test::update_binary(); 31 | 32 | for (@tests) { 33 | my ($header, $lang) = @{ $_ }; 34 | test::is_lang($header, $lang); 35 | } 36 | 37 | -------------------------------------------------------------------------------- /t/test.pm: -------------------------------------------------------------------------------- 1 | # 2 | # Accept-Language VCL binary tests 3 | # Test helper functions 4 | # 5 | # $Id: test.pm 16778 2010-01-21 16:33:09Z cosimo $ 6 | 7 | package test; 8 | 9 | use strict; 10 | use Test::More; 11 | 12 | sub update_binary { 13 | my $status = system('make'); 14 | $status >>= 8; 15 | return ok(0 == $status, 'C binary updated correctly'); 16 | } 17 | 18 | sub run_binary { 19 | my @args = @_; 20 | my $exec = './accept-language'; 21 | my $cmd = $exec; 22 | $cmd .= ' '; 23 | $cmd .= join(' ', map { q(') . $_ . q(') } @args); 24 | my $output = `$cmd`; 25 | return $output; 26 | } 27 | 28 | sub is_language { 29 | my ($header_value, $expected_language, $message) = @_; 30 | my $selected_language = run_binary($header_value); 31 | chomp $selected_language; 32 | 33 | $message ||= qq(parsing header '$header_value' should hold language '$expected_language'); 34 | 35 | return is($selected_language, $expected_language, $message); 36 | } 37 | 38 | *is_lang = *is_language; 39 | 40 | 1; 41 | 42 | =pod 43 | 44 | =head1 NAME 45 | 46 | Accept-Language test functions 47 | 48 | =head1 DESCRIPTION 49 | 50 | Helper test functions for the Accept-Language varnish C code extension. 51 | 52 | =head1 FUNCTIONS 53 | 54 | =head2 C 55 | 56 | Runs the C stage and updates the C binary for testing. 57 | 58 | =head2 C 59 | 60 | Runs the C binary with the contents of the accept language 61 | header passed as argument. Returns the output of the binary, which should 62 | be a supported language code. 63 | 64 | =head2 C 65 | 66 | =head2 C 67 | 68 | Test assertion. 69 | 70 | Parses the C<$accept_lang_header> as coming in from a client, and asserts that 71 | the resulting language as output by the C binary is equal 72 | to C<$lang>. 73 | 74 | Optionally with a test message (C<$message>). 75 | 76 | =head1 AUTHOR 77 | 78 | Cosimo Streppone, Ecosimo@opera.comE 79 | 80 | =head1 LICENSE AND COPYRIGHT 81 | 82 | Copyright (c), 2010 Opera Software ASA. 83 | All rights reserved. 84 | 85 | =end 86 | 87 | -------------------------------------------------------------------------------- /gen_vcl.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # 3 | # Generates the VCL file from accept-language.c 4 | # depending on your language preferences 5 | # 6 | 7 | use strict; 8 | use warnings; 9 | 10 | my @languages = @ARGV; 11 | 12 | if (! @languages) { 13 | die "Usage; $0 \n" . 14 | "example: $0 en de en es fr it no ru zh-cn ...\n"; 15 | } 16 | 17 | my $delim = ":"; 18 | my $default_lang = shift @languages; 19 | 20 | # Add default language to the list if there isn't already 21 | if (! grep { $_ eq $default_lang } @languages ) { 22 | unshift @languages, $default_lang; 23 | } 24 | 25 | # Build up list for easy matching 26 | my $lang_list = join($delim, @languages); 27 | $lang_list = ":$lang_list:"; 28 | 29 | my @c_code = ; 30 | my @vcl_code; 31 | 32 | READ_C: while (my $line = shift @c_code) { 33 | 34 | # Parse VCL ifdefs 35 | if ($line =~ m{^ \s* \#(ifn?def) \s+ __VCL__}mx) { 36 | #warn "\$1 = $1 \$line = $line\n"; 37 | my $include = $1 eq 'ifdef' ? 1 : 0; 38 | IFDEF: while ($line = shift @c_code) { 39 | #warn "\tread_line $line\n"; 40 | if ($line =~ m{^ \s* \#endif}mx) { 41 | last IFDEF; 42 | } 43 | elsif ($line =~ m{^ \s* \#else}mx) { 44 | $include = 1 - $include; 45 | } 46 | push @vcl_code, $line if $include; 47 | } 48 | next; 49 | } 50 | 51 | # Change the supported languages 52 | if ($line =~ m{^ \s* \#define \s+ SUPPORTED_LANGUAGES}mx) { 53 | push @vcl_code, qq{#define SUPPORTED_LANGUAGES "$lang_list"\n}; 54 | next; 55 | } 56 | 57 | if ($line =~ m{^ \s* \#define \s+ DEFAULT_LANGUAGE}mx) { 58 | push @vcl_code, qq{#define DEFAULT_LANGUAGE "$default_lang"\n}; 59 | next; 60 | } 61 | 62 | push @vcl_code, $line; 63 | } 64 | 65 | @vcl_code = ( 66 | "C{\n\n", 67 | "/* ------------------------------------------------------" . ("-" x length($0)) . " */\n", 68 | "/* THIS FILE IS AUTOMATICALLY GENERATED BY $0. DO NOT EDIT. */\n\n", 69 | @vcl_code, 70 | "\n/* THIS FILE IS AUTOMATICALLY GENERATED BY $0. DO NOT EDIT. */\n", 71 | "/* ------------------------------------------------------" . ("-" x length($0)) . " */\n", 72 | "}C\n", 73 | ); 74 | 75 | print @vcl_code; 76 | 77 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Varnish Accept-Language VCL 2 | =========================== 3 | 4 | Last updated: 03/11/2011 5 | Cosimo Streppone 6 | Opera Software ASA 7 | 8 | Here you will find a VCL config file for Varnish (http://varnish-cache.org) 9 | This VCL allows you to normalize and filter all the incoming requests 10 | Accept-Language headers and reduce them to just the languages your site supports. 11 | 12 | *** WARNING *** 13 | This VCL consists of C code. Your Varnish might explode. YMMV. 14 | Don't use it in production if you don't know what you're doing. 15 | We are using it in production, but we _don't_ know what we're doing :). 16 | 17 | 18 | Why would you want this? 19 | ------------------------ 20 | 21 | Your site supports English and Japanese languages. 22 | Your client browsers will send every possible Accept-Language header on Earth. 23 | If you enable "Vary: Accept-Language" on Varnish or on your backends, 24 | the cache hit ratio will rapidly drop, because of the huge variations 25 | in Accept-Language contents. 26 | 27 | With this VCL, the Accept-Language header will be "rewritten" to just 28 | "en" or "ja", depending on your client settings. If no match occurs, 29 | you can select a default language. 30 | 31 | The rewritten header is "X-Varnish-Accept-Language". 32 | You can choose to put this header back in "Accept-Language" if you wish. 33 | In this way, the normalization will be completely transparent. 34 | 35 | 36 | Requirements 37 | ------------ 38 | 39 | - gcc, make 40 | - a recent perl, with `prove' 41 | 42 | 43 | Instructions 44 | ------------- 45 | 46 | 1) Run 'make' informing the list of languages your site supports 47 | and the default fallback in the command line, e.g. 48 | 49 | make DEFAULT_LANGUAGE=en SUPPORTED_LANGUAGES="en ja pt pt-br" 50 | 51 | 2) Run 'make test' 52 | You should see "All tests successful" at the end of the execution 53 | 54 | 3) Install the generated accept-language.vcl in /etc/varnish/ 55 | 56 | 4) At the top of your main VCL file, add the following line: 57 | 58 | include "/etc/varnish/accept-language.vcl"; 59 | 60 | and then in your vcl_recv() add: 61 | 62 | C{ 63 | vcl_rewrite_accept_language(sp); 64 | }C 65 | 66 | This will parse Accept-Language and insert the final language into the 67 | "X-Varnish-Accept-Language" header (req.http.X-Varnish-Accept-Language). 68 | 69 | 6) Restart Varnish 70 | 71 | 7) Cross your fingers 72 | 73 | 8) Profit !! 74 | 75 | -------------------------------------------------------------------------------- /examples/accept-language.vcl: -------------------------------------------------------------------------------- 1 | C{ 2 | 3 | /* ------------------------------------------------------------------ */ 4 | /* THIS FILE IS AUTOMATICALLY GENERATED BY ./gen_vcl.pl. DO NOT EDIT. */ 5 | 6 | /* 7 | * Accept-language header normalization 8 | * 9 | * Cosimo, 21/01/2010 10 | * 11 | */ 12 | 13 | #include /* isupper */ 14 | #include 15 | #include /* qsort */ 16 | #include 17 | 18 | #define DEFAULT_LANGUAGE "en" 19 | #define SUPPORTED_LANGUAGES ":bg:cs:da:de:en:es:fi:fy:hu:it:ja:no:pl:ru:sq:sk:tr:uk:vn:xx-lol:zh-cn:" 20 | 21 | #define vcl_string char 22 | #define LANG_LIST_SIZE 16 23 | #define LANG_MAXLEN 16 24 | #define RETURN_LANG(x) { \ 25 | strncpy(lang, x, LANG_MAXLEN); \ 26 | return; \ 27 | } 28 | #define RETURN_DEFAULT_LANG RETURN_LANG(DEFAULT_LANGUAGE) 29 | #define PUSH_LANG(x,y) { \ 30 | /* fprintf(stderr, "Pushing lang [%d] %s %.4f\n", curr_lang, x, y); */ \ 31 | /* We have to copy, otherwise root_lang will be the same every time */ \ 32 | strncpy(pl[curr_lang].lang, x, LANG_MAXLEN); \ 33 | pl[curr_lang].q = y; \ 34 | curr_lang++; \ 35 | } 36 | 37 | struct lang_list { 38 | vcl_string lang[LANG_MAXLEN]; 39 | float q; 40 | }; 41 | 42 | /* In-place lowercase of a string */ 43 | static void strtolower(char *s) { 44 | register char *c; 45 | for (c=s; *c; c++) { 46 | if (isupper(*c)) { 47 | *c = tolower(*c); 48 | } 49 | } 50 | return; 51 | } 52 | 53 | /* Checks if a given language is in the static list of the ones we support */ 54 | int is_supported(vcl_string *lang) { 55 | vcl_string *supported_languages = SUPPORTED_LANGUAGES; 56 | vcl_string match_str[LANG_MAXLEN + 3] = ""; /* :, :, \0 = 3 */ 57 | int is_supported = 0; 58 | 59 | /* We want to match 'zh-cn' and 'zh-CN' too */ 60 | strtolower(lang); 61 | 62 | /* Search "::" in supported languages string */ 63 | strncpy(match_str, ":", 1); 64 | strncat(match_str, lang, LANG_MAXLEN); 65 | strncat(match_str, ":\0", 2); 66 | 67 | if (strstr(supported_languages, match_str)) { 68 | is_supported = 1; 69 | } 70 | 71 | return is_supported; 72 | } 73 | 74 | /* Used by qsort() below */ 75 | int sort_by_q(const void *x, const void *y) { 76 | struct lang_list *a = (struct lang_list *)x; 77 | struct lang_list *b = (struct lang_list *)y; 78 | if (a->q > b->q) return -1; 79 | if (a->q < b->q) return 1; 80 | return 0; 81 | } 82 | 83 | /* Reads Accept-Language, parses it, and finds the first match 84 | among the supported languages. In case of no match, 85 | returns the default language. 86 | */ 87 | void select_language(const vcl_string *incoming_header, char *lang) { 88 | 89 | struct lang_list pl[LANG_LIST_SIZE]; 90 | vcl_string *lang_tok = NULL; 91 | vcl_string root_lang[3]; 92 | vcl_string *header; 93 | vcl_string *pos = NULL; 94 | vcl_string *q_spec = NULL; 95 | unsigned int curr_lang = 0, i = 0; 96 | float q; 97 | 98 | /* Empty or default string, return default language immediately */ 99 | if ( 100 | !incoming_header 101 | || (0 == strcmp(incoming_header, "en-US")) 102 | || (0 == strcmp(incoming_header, "en-GB")) 103 | || (0 == strcmp(incoming_header, DEFAULT_LANGUAGE)) 104 | || (0 == strcmp(incoming_header, "")) 105 | ) 106 | RETURN_DEFAULT_LANG; 107 | 108 | /* Tokenize Accept-Language */ 109 | header = (vcl_string *) incoming_header; 110 | 111 | while ((lang_tok = strtok_r(header, " ,", &pos))) { 112 | 113 | q = 1.0; 114 | 115 | if ((q_spec = strstr(lang_tok, ";q="))) { 116 | /* Truncate language name before ';' */ 117 | *q_spec = '\0'; 118 | /* Get q value */ 119 | sscanf(q_spec + 3, "%f", &q); 120 | } 121 | 122 | /* Wildcard language '*' should be last in list */ 123 | if ((*lang_tok) == '*') q = 0.0; 124 | 125 | /* Push in the prioritized list */ 126 | PUSH_LANG(lang_tok, q); 127 | 128 | /* For cases like 'en-GB', we also want the root language in the final list */ 129 | if ('-' == lang_tok[2]) { 130 | root_lang[0] = lang_tok[0]; 131 | root_lang[1] = lang_tok[1]; 132 | root_lang[2] = '\0'; 133 | PUSH_LANG(root_lang, q - 0.001); 134 | } 135 | 136 | /* For strtok_r() to proceed from where it left off */ 137 | header = NULL; 138 | 139 | /* Break out if stored max no. of languages */ 140 | if (curr_lang >= LANG_MAXLEN) break; 141 | } 142 | 143 | /* Sort by priority */ 144 | qsort(pl, curr_lang, sizeof(struct lang_list), &sort_by_q); 145 | 146 | /* Match with supported languages */ 147 | for (i = 0; i < curr_lang; i++) { 148 | if (is_supported(pl[i].lang)) 149 | RETURN_LANG(pl[i].lang); 150 | } 151 | 152 | RETURN_DEFAULT_LANG; 153 | } 154 | 155 | /* Reads req.http.Accept-Language and writes X-Varnish-Accept-Language */ 156 | void vcl_rewrite_accept_language(const struct sess *sp) { 157 | vcl_string *in_hdr; 158 | vcl_string lang[LANG_MAXLEN]; 159 | 160 | memset(lang, 0, LANG_MAXLEN); 161 | 162 | /* Get Accept-Language header from client */ 163 | in_hdr = VRT_GetHdr(sp, HDR_REQ, "\020Accept-Language:"); 164 | 165 | /* Normalize and filter out by list of supported languages */ 166 | select_language(in_hdr, lang); 167 | 168 | /* By default, use a different header name: don't mess with backend logic */ 169 | VRT_SetHdr(sp, HDR_REQ, "\032X-Varnish-Accept-Language:", lang, vrt_magic_string_end); 170 | 171 | return; 172 | } 173 | 174 | /* vim: syn=c ts=4 et sts=4 sw=4 tw=0 175 | */ 176 | 177 | /* THIS FILE IS AUTOMATICALLY GENERATED BY ./gen_vcl.pl. DO NOT EDIT. */ 178 | /* ------------------------------------------------------------------ */ 179 | }C 180 | -------------------------------------------------------------------------------- /accept-language.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Accept-language header normalization 3 | * 4 | * - Parses client Accept-Language HTTP header 5 | * - Tries to find the best match with the supported languages 6 | * - Writes the best match as req.http.X-Varnish-Accept-Language 7 | * 8 | * First version: Cosimo, 21/Jan/2010 9 | * Last update: Cosimo, 03/Nov/2011 10 | * 11 | * http://github.com/cosimo/varnish-accept-language 12 | * 13 | */ 14 | 15 | #include /* isupper */ 16 | #include 17 | #include /* qsort */ 18 | #include 19 | 20 | #define DEFAULT_LANGUAGE "en" 21 | #define SUPPORTED_LANGUAGES ":bg:cs:da:de:en:fi:fy:hu:it:ja:no:pl:ru:sq:sk:tr:uk:xx-lol:vn:zh-cn:" 22 | 23 | #define vcl_string char 24 | #define LANG_LIST_SIZE 16 25 | #define HDR_MAXLEN 256 26 | #define LANG_MAXLEN 8 27 | #define RETURN_LANG(x) { \ 28 | strncpy(lang, x, LANG_MAXLEN); \ 29 | return; \ 30 | } 31 | #define RETURN_DEFAULT_LANG RETURN_LANG(DEFAULT_LANGUAGE) 32 | #define PUSH_LANG(x,y) { \ 33 | /* fprintf(stderr, "Pushing lang [%d] %s %.4f\n", curr_lang, x, y); */ \ 34 | /* We have to copy, otherwise root_lang will be the same every time */ \ 35 | strncpy(pl[curr_lang].lang, x, LANG_MAXLEN); \ 36 | pl[curr_lang].q = y; \ 37 | curr_lang++; \ 38 | } 39 | 40 | struct lang_list { 41 | vcl_string lang[LANG_MAXLEN]; 42 | float q; 43 | }; 44 | 45 | /* In-place lowercase of a string */ 46 | static void strtolower(char *s) { 47 | register char *c; 48 | for (c=s; *c; c++) { 49 | if (isupper(*c)) { 50 | *c = tolower(*c); 51 | } 52 | } 53 | return; 54 | } 55 | 56 | /* Checks if a given language is in the static list of the ones we support */ 57 | int is_supported(vcl_string *lang) { 58 | vcl_string *supported_languages = SUPPORTED_LANGUAGES; 59 | vcl_string match_str[LANG_MAXLEN + 3] = ""; /* :, :, \0 = 3 */ 60 | int is_supported = 0; 61 | 62 | /* We want to match 'zh-cn' and 'zh-CN' too */ 63 | strtolower(lang); 64 | 65 | /* Search "::" in supported languages string */ 66 | strncpy(match_str, ":", 1); 67 | strncat(match_str, lang, LANG_MAXLEN); 68 | strncat(match_str, ":\0", 2); 69 | 70 | if (strstr(supported_languages, match_str)) 71 | is_supported = 1; 72 | 73 | return is_supported; 74 | } 75 | 76 | /* Used by qsort() below */ 77 | int sort_by_q(const void *x, const void *y) { 78 | struct lang_list *a = (struct lang_list *)x; 79 | struct lang_list *b = (struct lang_list *)y; 80 | if (a->q > b->q) return -1; 81 | if (a->q < b->q) return 1; 82 | return 0; 83 | } 84 | 85 | /* Reads Accept-Language, parses it, and finds the first match 86 | among the supported languages. In case of no match, 87 | returns the default language. 88 | */ 89 | void select_language(const vcl_string *incoming_header, char *lang) { 90 | 91 | struct lang_list pl[LANG_LIST_SIZE]; 92 | vcl_string *lang_tok = NULL; 93 | vcl_string root_lang[3]; 94 | vcl_string *header; 95 | vcl_string header_copy[HDR_MAXLEN]; 96 | vcl_string *pos = NULL; 97 | vcl_string *q_spec = NULL; 98 | unsigned int curr_lang = 0, i = 0; 99 | float q; 100 | 101 | /* Empty or default string, return default language immediately */ 102 | if ( 103 | !incoming_header 104 | || (0 == strcmp(incoming_header, "en-US")) 105 | || (0 == strcmp(incoming_header, "en-GB")) 106 | || (0 == strcmp(incoming_header, DEFAULT_LANGUAGE)) 107 | || (0 == strcmp(incoming_header, "")) 108 | ) 109 | RETURN_DEFAULT_LANG; 110 | 111 | /* Tokenize Accept-Language */ 112 | header = strncpy(header_copy, incoming_header, sizeof(header_copy)); 113 | 114 | while ((lang_tok = strtok_r(header, " ,", &pos))) { 115 | 116 | q = 1.0; 117 | 118 | if ((q_spec = strstr(lang_tok, ";q="))) { 119 | /* Truncate language name before ';' */ 120 | *q_spec = '\0'; 121 | /* Get q value */ 122 | sscanf(q_spec + 3, "%f", &q); 123 | } 124 | 125 | /* Wildcard language '*' should be last in list */ 126 | if ((*lang_tok) == '*') q = 0.0; 127 | 128 | /* Push in the prioritized list */ 129 | PUSH_LANG(lang_tok, q); 130 | 131 | /* For cases like 'en-GB', we also want the root language in the final list */ 132 | if ('-' == lang_tok[2]) { 133 | root_lang[0] = lang_tok[0]; 134 | root_lang[1] = lang_tok[1]; 135 | root_lang[2] = '\0'; 136 | PUSH_LANG(root_lang, q - 0.001); 137 | } 138 | 139 | /* For strtok_r() to proceed from where it left off */ 140 | header = NULL; 141 | 142 | /* Break out if stored max no. of languages */ 143 | if (curr_lang >= LANG_LIST_SIZE) 144 | break; 145 | } 146 | 147 | /* Sort by priority */ 148 | qsort(pl, curr_lang, sizeof(struct lang_list), &sort_by_q); 149 | 150 | /* Match with supported languages */ 151 | for (i = 0; i < curr_lang; i++) { 152 | if (is_supported(pl[i].lang)) 153 | RETURN_LANG(pl[i].lang); 154 | } 155 | 156 | RETURN_DEFAULT_LANG; 157 | } 158 | 159 | #ifdef __VCL__ 160 | /* Reads req.http.Accept-Language and writes X-Varnish-Accept-Language */ 161 | void vcl_rewrite_accept_language(const struct sess *sp) { 162 | vcl_string *in_hdr; 163 | vcl_string lang[LANG_MAXLEN]; 164 | 165 | /* Get Accept-Language header from client */ 166 | in_hdr = VRT_GetHdr(sp, HDR_REQ, "\020Accept-Language:"); 167 | 168 | /* Normalize and filter out by list of supported languages */ 169 | memset(lang, 0, sizeof(lang)); 170 | select_language(in_hdr, lang); 171 | 172 | /* By default, use a different header name: don't mess with backend logic */ 173 | VRT_SetHdr(sp, HDR_REQ, "\032X-Varnish-Accept-Language:", lang, vrt_magic_string_end); 174 | 175 | return; 176 | } 177 | #else 178 | int main(int argc, char **argv) { 179 | vcl_string lang[LANG_MAXLEN]; 180 | 181 | /* We need to check that we don't modify our arguments */ 182 | vcl_string argv_copy[HDR_MAXLEN]; 183 | strncpy(argv_copy, argv[1], sizeof(argv_copy)); 184 | 185 | if (argc != 2 || ! argv[1]) 186 | strncpy(lang, "??", 2); 187 | else 188 | select_language(argv[1], lang); 189 | 190 | /* If original header value is longer than our internal copy buffer, 191 | then just output a diagnostic message, don't compare them. See below. */ 192 | if (strlen(argv[1]) > strlen(argv_copy)) { 193 | fprintf(stderr, "# overflowed the max header copy buffer\n"); 194 | } 195 | 196 | /* Detect "corruption" of original arg string */ 197 | else if (strcmp(argv_copy, argv[1])) { 198 | fprintf(stderr, "# argument '%s' was modified! (now '%s')\n", 199 | argv_copy, argv[1] 200 | ); 201 | return 1; 202 | } 203 | 204 | printf("%s\n", lang); 205 | 206 | return 0; 207 | } 208 | #endif /* __VCL__ */ 209 | 210 | /* vim: syn=c ts=4 et sts=4 sw=4 tw=0 211 | */ 212 | --------------------------------------------------------------------------------