Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : //
4 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 : //
7 : // Official repository: https://github.com/cppalliance/http
8 : //
9 :
10 : #include <boost/http/server/accepts.hpp>
11 : #include <boost/http/server/mime_types.hpp>
12 : #include <boost/http/field.hpp>
13 : #include <algorithm>
14 :
15 : namespace boost {
16 : namespace http {
17 :
18 : namespace {
19 :
20 : //----------------------------------------------------------
21 : // Helpers
22 : //----------------------------------------------------------
23 :
24 : std::string_view
25 142 : trim_ows( std::string_view s ) noexcept
26 : {
27 338 : while( ! s.empty() &&
28 169 : ( s.front() == ' ' || s.front() == '\t' ) )
29 27 : s.remove_prefix( 1 );
30 284 : while( ! s.empty() &&
31 142 : ( s.back() == ' ' || s.back() == '\t' ) )
32 0 : s.remove_suffix( 1 );
33 142 : return s;
34 : }
35 :
36 : bool
37 75 : iequals(
38 : std::string_view a,
39 : std::string_view b ) noexcept
40 : {
41 75 : if( a.size() != b.size() )
42 26 : return false;
43 202 : for( std::size_t i = 0; i < a.size(); ++i )
44 : {
45 174 : unsigned char ca = a[i];
46 174 : unsigned char cb = b[i];
47 174 : if( ca >= 'A' && ca <= 'Z' )
48 0 : ca += 32;
49 174 : if( cb >= 'A' && cb <= 'Z' )
50 0 : cb += 32;
51 174 : if( ca != cb )
52 21 : return false;
53 : }
54 28 : return true;
55 : }
56 :
57 : // Returns quality as integer 0-1000
58 : int
59 16 : parse_q( std::string_view s ) noexcept
60 : {
61 16 : s = trim_ows( s );
62 16 : if( s.empty() )
63 0 : return 1000;
64 16 : if( s[0] == '1' )
65 0 : return 1000;
66 16 : if( s[0] != '0' )
67 1 : return 0;
68 15 : if( s.size() < 2 || s[1] != '.' )
69 1 : return 0;
70 14 : int result = 0;
71 14 : int mult = 100;
72 14 : for( std::size_t i = 2;
73 28 : i < s.size() && i < 5; ++i )
74 : {
75 14 : if( s[i] < '0' || s[i] > '9' )
76 0 : break;
77 14 : result += ( s[i] - '0' ) * mult;
78 14 : mult /= 10;
79 : }
80 14 : return result;
81 : }
82 :
83 : // Extract q-value from parameters after first semicolon
84 : int
85 16 : extract_q( std::string_view params ) noexcept
86 : {
87 16 : while( ! params.empty() )
88 : {
89 16 : auto semi = params.find( ';' );
90 16 : auto param = trim_ows(
91 : semi != std::string_view::npos
92 0 : ? params.substr( 0, semi )
93 : : params );
94 16 : if( param.size() >= 2 &&
95 32 : ( param[0] == 'q' || param[0] == 'Q' ) &&
96 16 : param[1] == '=' )
97 : {
98 16 : return parse_q( param.substr( 2 ) );
99 : }
100 0 : if( semi != std::string_view::npos )
101 0 : params.remove_prefix( semi + 1 );
102 : else
103 0 : break;
104 : }
105 0 : return 1000;
106 : }
107 :
108 : //----------------------------------------------------------
109 : // Negotiation priority
110 : //----------------------------------------------------------
111 :
112 : struct priority
113 : {
114 : int q;
115 : int specificity;
116 : int order;
117 : };
118 :
119 : bool
120 6 : is_better(
121 : priority const& a,
122 : priority const& b ) noexcept
123 : {
124 6 : if( a.q != b.q )
125 4 : return a.q > b.q;
126 2 : if( a.specificity != b.specificity )
127 1 : return a.specificity > b.specificity;
128 1 : return a.order < b.order;
129 : }
130 :
131 : //----------------------------------------------------------
132 : // Media type parsing (Accept header)
133 : //----------------------------------------------------------
134 :
135 : struct media_range
136 : {
137 : std::string_view type;
138 : std::string_view subtype;
139 : std::string_view full;
140 : int q;
141 : int order;
142 : };
143 :
144 : std::vector<media_range>
145 13 : parse_accept( std::string_view header )
146 : {
147 13 : std::vector<media_range> result;
148 13 : int order = 0;
149 :
150 35 : while( ! header.empty() )
151 : {
152 22 : auto comma = header.find( ',' );
153 : auto entry = ( comma != std::string_view::npos )
154 22 : ? header.substr( 0, comma )
155 13 : : header;
156 22 : if( comma != std::string_view::npos )
157 9 : header.remove_prefix( comma + 1 );
158 : else
159 13 : header = {};
160 :
161 22 : entry = trim_ows( entry );
162 22 : if( entry.empty() )
163 0 : continue;
164 :
165 22 : auto semi = entry.find( ';' );
166 26 : auto mime_part = trim_ows(
167 : semi != std::string_view::npos
168 4 : ? entry.substr( 0, semi )
169 : : entry );
170 :
171 22 : auto slash = mime_part.find( '/' );
172 22 : if( slash == std::string_view::npos )
173 0 : continue;
174 :
175 22 : media_range mr;
176 22 : mr.type = mime_part.substr( 0, slash );
177 22 : mr.subtype = mime_part.substr( slash + 1 );
178 22 : mr.full = mime_part;
179 22 : mr.q = ( semi != std::string_view::npos )
180 22 : ? extract_q( entry.substr( semi + 1 ) )
181 : : 1000;
182 22 : mr.order = order++;
183 22 : result.push_back( mr );
184 : }
185 :
186 13 : return result;
187 0 : }
188 :
189 : // Returns specificity (0-6) or -1 for no match
190 : int
191 16 : match_media(
192 : media_range const& range,
193 : std::string_view type,
194 : std::string_view subtype ) noexcept
195 : {
196 16 : int s = 0;
197 :
198 16 : if( range.type == "*" )
199 : {
200 : // wildcard type
201 : }
202 15 : else if( iequals( range.type, type ) )
203 : {
204 7 : s |= 4;
205 : }
206 : else
207 : {
208 8 : return -1;
209 : }
210 :
211 8 : if( range.subtype == "*" )
212 : {
213 : // wildcard subtype
214 : }
215 5 : else if( iequals( range.subtype, subtype ) )
216 : {
217 5 : s |= 2;
218 : }
219 : else
220 : {
221 0 : return -1;
222 : }
223 :
224 8 : return s;
225 : }
226 :
227 : //----------------------------------------------------------
228 : // Simple token parsing (Accept-Encoding/Charset/Language)
229 : //----------------------------------------------------------
230 :
231 : struct simple_entry
232 : {
233 : std::string_view value;
234 : int q;
235 : int order;
236 : };
237 :
238 : std::vector<simple_entry>
239 15 : parse_simple( std::string_view header )
240 : {
241 15 : std::vector<simple_entry> result;
242 15 : int order = 0;
243 :
244 48 : while( ! header.empty() )
245 : {
246 33 : auto comma = header.find( ',' );
247 : auto entry = ( comma != std::string_view::npos )
248 33 : ? header.substr( 0, comma )
249 15 : : header;
250 33 : if( comma != std::string_view::npos )
251 18 : header.remove_prefix( comma + 1 );
252 : else
253 15 : header = {};
254 :
255 33 : entry = trim_ows( entry );
256 33 : if( entry.empty() )
257 0 : continue;
258 :
259 33 : auto semi = entry.find( ';' );
260 45 : auto value = trim_ows(
261 : semi != std::string_view::npos
262 12 : ? entry.substr( 0, semi )
263 : : entry );
264 33 : if( value.empty() )
265 0 : continue;
266 :
267 33 : simple_entry se;
268 33 : se.value = value;
269 33 : se.q = ( semi != std::string_view::npos )
270 33 : ? extract_q( entry.substr( semi + 1 ) )
271 : : 1000;
272 33 : se.order = order++;
273 33 : result.push_back( se );
274 : }
275 :
276 15 : return result;
277 0 : }
278 :
279 : //----------------------------------------------------------
280 : // Matching helpers
281 : //----------------------------------------------------------
282 :
283 : // Exact or wildcard match (encoding, charset)
284 : int
285 25 : match_exact(
286 : std::string_view spec,
287 : std::string_view offered ) noexcept
288 : {
289 25 : if( iequals( spec, offered ) )
290 9 : return 1;
291 16 : if( spec == "*" )
292 1 : return 0;
293 15 : return -1;
294 : }
295 :
296 : // Language prefix: "en-US" -> "en"
297 : std::string_view
298 17 : lang_prefix( std::string_view tag ) noexcept
299 : {
300 17 : auto dash = tag.find( '-' );
301 17 : if( dash != std::string_view::npos )
302 3 : return tag.substr( 0, dash );
303 14 : return tag;
304 : }
305 :
306 : // Language match with prefix support
307 : int
308 13 : match_language(
309 : std::string_view spec,
310 : std::string_view offered ) noexcept
311 : {
312 13 : if( iequals( spec, offered ) )
313 4 : return 4;
314 9 : if( iequals( lang_prefix( spec ), offered ) )
315 1 : return 2;
316 8 : if( iequals( spec, lang_prefix( offered ) ) )
317 2 : return 1;
318 6 : if( spec == "*" )
319 0 : return 0;
320 6 : return -1;
321 : }
322 :
323 : //----------------------------------------------------------
324 : // Generic negotiation for simple headers
325 : //----------------------------------------------------------
326 :
327 : template< class MatchFn >
328 : std::string_view
329 12 : negotiate(
330 : std::vector<simple_entry> const& entries,
331 : std::initializer_list<std::string_view> offered,
332 : MatchFn match )
333 : {
334 12 : std::string_view best_val;
335 12 : priority best_pri{ -1, -1, 0 };
336 12 : bool found = false;
337 :
338 30 : for( auto const& o : offered )
339 : {
340 18 : priority pri{ -1, -1, 0 };
341 18 : bool matched = false;
342 :
343 56 : for( auto const& e : entries )
344 : {
345 38 : if( e.q <= 0 )
346 21 : continue;
347 38 : auto s = match( e.value, o );
348 38 : if( s < 0 )
349 21 : continue;
350 17 : priority p{ e.q, s, e.order };
351 17 : if( ! matched ||
352 0 : p.specificity > pri.specificity ||
353 0 : ( p.specificity == pri.specificity &&
354 0 : p.q > pri.q ) ||
355 0 : ( p.specificity == pri.specificity &&
356 0 : p.q == pri.q &&
357 0 : p.order < pri.order ) )
358 : {
359 17 : pri = p;
360 17 : matched = true;
361 : }
362 : }
363 :
364 18 : if( ! matched || pri.q <= 0 )
365 1 : continue;
366 :
367 17 : if( ! found || is_better( pri, best_pri ) )
368 : {
369 13 : best_val = o;
370 13 : best_pri = pri;
371 13 : found = true;
372 : }
373 : }
374 :
375 12 : return found ? best_val : std::string_view{};
376 : }
377 :
378 : // Return sorted values from simple entries
379 : std::vector<std::string_view>
380 3 : sorted_values(
381 : std::vector<simple_entry>& entries )
382 : {
383 3 : std::sort( entries.begin(), entries.end(),
384 13 : []( simple_entry const& a,
385 : simple_entry const& b )
386 : {
387 13 : if( a.q != b.q )
388 11 : return a.q > b.q;
389 2 : return a.order < b.order;
390 : });
391 :
392 3 : std::vector<std::string_view> result;
393 3 : result.reserve( entries.size() );
394 12 : for( auto const& e : entries )
395 : {
396 9 : if( e.q <= 0 )
397 0 : continue;
398 9 : result.push_back( e.value );
399 : }
400 3 : return result;
401 0 : }
402 :
403 : } // (anon)
404 :
405 : //----------------------------------------------------------
406 :
407 26 : accepts::accepts(
408 26 : fields_base const& fields ) noexcept
409 26 : : fields_( fields )
410 : {
411 26 : }
412 :
413 : std::string_view
414 12 : accepts::type(
415 : std::initializer_list<
416 : std::string_view> offered ) const
417 : {
418 12 : if( offered.size() == 0 )
419 1 : return {};
420 :
421 11 : auto accept = fields_.value_or(
422 : field::accept, "" );
423 :
424 11 : if( accept.empty() )
425 1 : return *offered.begin();
426 :
427 10 : auto ranges = parse_accept( accept );
428 10 : if( ranges.empty() )
429 0 : return *offered.begin();
430 :
431 10 : std::string_view best_val;
432 10 : priority best_pri{ -1, -1, 0 };
433 10 : bool found = false;
434 :
435 22 : for( auto const& o : offered )
436 : {
437 : // Convert extension to MIME if needed
438 12 : std::string_view mime_str = o;
439 12 : if( o.find( '/' ) == std::string_view::npos )
440 : {
441 9 : auto looked = mime_types::lookup( o );
442 9 : if( ! looked.empty() )
443 8 : mime_str = looked;
444 : else
445 1 : continue;
446 : }
447 :
448 11 : auto slash = mime_str.find( '/' );
449 11 : if( slash == std::string_view::npos )
450 0 : continue;
451 :
452 11 : auto type = mime_str.substr( 0, slash );
453 11 : auto subtype = mime_str.substr( slash + 1 );
454 :
455 : // Find best matching range for this type
456 11 : priority pri{ -1, -1, 0 };
457 11 : bool matched = false;
458 :
459 29 : for( auto const& r : ranges )
460 : {
461 18 : if( r.q <= 0 )
462 10 : continue;
463 16 : auto s = match_media( r, type, subtype );
464 16 : if( s < 0 )
465 8 : continue;
466 8 : priority p{ r.q, s, r.order };
467 8 : if( ! matched ||
468 0 : p.specificity > pri.specificity ||
469 0 : ( p.specificity == pri.specificity &&
470 0 : p.q > pri.q ) ||
471 0 : ( p.specificity == pri.specificity &&
472 0 : p.q == pri.q &&
473 0 : p.order < pri.order ) )
474 : {
475 8 : pri = p;
476 8 : matched = true;
477 : }
478 : }
479 :
480 11 : if( ! matched || pri.q <= 0 )
481 3 : continue;
482 :
483 8 : if( ! found || is_better( pri, best_pri ) )
484 : {
485 8 : best_val = o;
486 8 : best_pri = pri;
487 8 : found = true;
488 : }
489 : }
490 :
491 10 : return found ? best_val : std::string_view{};
492 10 : }
493 :
494 : std::vector<std::string_view>
495 4 : accepts::types() const
496 : {
497 4 : auto accept = fields_.value_or(
498 : field::accept, "" );
499 4 : if( accept.empty() )
500 1 : return {};
501 :
502 3 : auto ranges = parse_accept( accept );
503 :
504 3 : std::sort( ranges.begin(), ranges.end(),
505 6 : []( media_range const& a,
506 : media_range const& b )
507 : {
508 6 : if( a.q != b.q )
509 6 : return a.q > b.q;
510 0 : return a.order < b.order;
511 : });
512 :
513 3 : std::vector<std::string_view> result;
514 3 : result.reserve( ranges.size() );
515 9 : for( auto const& r : ranges )
516 : {
517 6 : if( r.q <= 0 )
518 1 : continue;
519 5 : result.push_back( r.full );
520 : }
521 3 : return result;
522 3 : }
523 :
524 : std::string_view
525 6 : accepts::encoding(
526 : std::initializer_list<
527 : std::string_view> offered ) const
528 : {
529 6 : if( offered.size() == 0 )
530 0 : return {};
531 :
532 6 : auto header = fields_.value_or(
533 : field::accept_encoding, "" );
534 :
535 6 : if( header.empty() )
536 1 : return *offered.begin();
537 :
538 5 : auto entries = parse_simple( header );
539 5 : if( entries.empty() )
540 0 : return *offered.begin();
541 :
542 5 : return negotiate( entries, offered, match_exact );
543 5 : }
544 :
545 : std::vector<std::string_view>
546 2 : accepts::encodings() const
547 : {
548 2 : auto header = fields_.value_or(
549 : field::accept_encoding, "" );
550 2 : if( header.empty() )
551 1 : return {};
552 :
553 1 : auto entries = parse_simple( header );
554 1 : return sorted_values( entries );
555 1 : }
556 :
557 : std::string_view
558 3 : accepts::charset(
559 : std::initializer_list<
560 : std::string_view> offered ) const
561 : {
562 3 : if( offered.size() == 0 )
563 0 : return {};
564 :
565 3 : auto header = fields_.value_or(
566 : field::accept_charset, "" );
567 :
568 3 : if( header.empty() )
569 1 : return *offered.begin();
570 :
571 2 : auto entries = parse_simple( header );
572 2 : if( entries.empty() )
573 0 : return *offered.begin();
574 :
575 2 : return negotiate( entries, offered, match_exact );
576 2 : }
577 :
578 : std::vector<std::string_view>
579 1 : accepts::charsets() const
580 : {
581 1 : auto header = fields_.value_or(
582 : field::accept_charset, "" );
583 1 : if( header.empty() )
584 0 : return {};
585 :
586 1 : auto entries = parse_simple( header );
587 1 : return sorted_values( entries );
588 1 : }
589 :
590 : std::string_view
591 6 : accepts::language(
592 : std::initializer_list<
593 : std::string_view> offered ) const
594 : {
595 6 : if( offered.size() == 0 )
596 0 : return {};
597 :
598 6 : auto header = fields_.value_or(
599 : field::accept_language, "" );
600 :
601 6 : if( header.empty() )
602 1 : return *offered.begin();
603 :
604 5 : auto entries = parse_simple( header );
605 5 : if( entries.empty() )
606 0 : return *offered.begin();
607 :
608 5 : return negotiate( entries, offered, match_language );
609 5 : }
610 :
611 : std::vector<std::string_view>
612 1 : accepts::languages() const
613 : {
614 1 : auto header = fields_.value_or(
615 : field::accept_language, "" );
616 1 : if( header.empty() )
617 0 : return {};
618 :
619 1 : auto entries = parse_simple( header );
620 1 : return sorted_values( entries );
621 1 : }
622 :
623 : } // http
624 : } // boost
|