@@ -453,6 +453,8 @@ class subop
453453};
454454
455455using SubOpQueue = std::vector<std::unique_ptr<subop>>;
456+ using HeaderPair = std::pair<std::string, std::string>;
457+ using HeaderList = std::vector<HeaderPair>;
456458
457459// ----------------------------------------------------------------------------
458460class op
@@ -514,6 +516,18 @@ class op
514516 return disable_pristine_host_hdr;
515517 }
516518
519+ void
520+ addSendtoHeader (std::string_view const name, std::string_view const value)
521+ {
522+ sendto_headers.emplace_back (name, value);
523+ }
524+
525+ HeaderList const &
526+ getSendtoHeaders () const
527+ {
528+ return sendto_headers;
529+ }
530+
517531 void
518532 printOp () const
519533 {
@@ -530,11 +544,17 @@ class op
530544 if (disable_pristine_host_hdr) {
531545 Dbg (dbg_ctl, " disable_pristine_host_hdr: true" );
532546 }
547+ if (!sendto_headers.empty ()) {
548+ Dbg (dbg_ctl, " set_sendto_headers:" );
549+ for (auto const &header : sendto_headers) {
550+ Dbg (dbg_ctl, " %s: %s" , header.first .c_str (), header.second .c_str ());
551+ }
552+ }
533553 }
534554
535555 bool
536556 process (CookieJar &jar, std::string &dest, TSHttpStatus &retstat, TSRemapRequestInfo *rri, UrlComponents &req_url,
537- bool &used_sendto) const
557+ bool &used_sendto, std::vector<std::string> ®ex_match_strings, int ®ex_ccount ) const
538558 {
539559 if (sendto == " " ) {
540560 return false ; // guessing every operation must have a
@@ -686,10 +706,19 @@ class op
686706
687707 // OPERATION::regex matching
688708 if (subop_type == REGEXP) {
689- RegexMatches matches ;
690- int ret = subop->regexMatch (string_to_match.c_str (), string_to_match.length (), matches );
709+ RegexMatches regex_matches ;
710+ int ret = subop->regexMatch (string_to_match.c_str (), string_to_match.length (), regex_matches );
691711
692712 if (ret >= 0 ) {
713+ regex_ccount = subop->getRegexCcount (); // Store for later use in header substitution
714+
715+ regex_match_strings.clear ();
716+ regex_match_strings.reserve (regex_ccount + 1 );
717+ for (int i = 0 ; i <= regex_ccount; i++) {
718+ auto const &match = regex_matches[i];
719+ regex_match_strings.emplace_back (match.data (), match.size ());
720+ }
721+
693722 std::string::size_type pos = sendto.find (' $' );
694723 std::string::size_type ppos = 0 ;
695724
@@ -717,9 +746,9 @@ class op
717746 if (isdigit (sendto[pos + 1 ])) {
718747 int ix = sendto[pos + 1 ] - ' 0' ;
719748
720- if (ix <= subop-> getRegexCcount () ) { // Just skip an illegal regex group
749+ if (ix <= regex_ccount ) { // Just skip an illegal regex group
721750 dest += sendto.substr (ppos, pos - ppos);
722- auto regex_match = matches [ix];
751+ auto regex_match = regex_matches [ix];
723752 dest.append (regex_match.data (), regex_match.size ());
724753 ppos = pos + 2 ;
725754 } else {
@@ -812,6 +841,7 @@ class op
812841 TSHttpStatus status = TS_HTTP_STATUS_NONE;
813842 TSHttpStatus else_status = TS_HTTP_STATUS_NONE;
814843 bool disable_pristine_host_hdr = false ;
844+ HeaderList sendto_headers{};
815845};
816846
817847using StringPair = std::pair<std::string, std::string>;
@@ -854,6 +884,19 @@ build_op(op &o, OpMap const &q)
854884 o.setDisablePristineHostHdr (val == " true" || val == " 1" || val == " yes" );
855885 }
856886
887+ if (key == " __set_sendto_header__" ) {
888+ // Parse "header_name: header_value" format. We set this below in the TSRemapNewInstance function.
889+ size_t const colon_pos = val.find (" : " );
890+ if (colon_pos != std::string::npos) {
891+ std::string_view const header_name = std::string_view (val).substr (0 , colon_pos);
892+ std::string_view const header_value = std::string_view (val).substr (colon_pos + 2 );
893+ o.addSendtoHeader (header_name, header_value);
894+ } else {
895+ Dbg (dbg_ctl, " ERROR: invalid set_sendto_header format: %s" , val.c_str ());
896+ goto error;
897+ }
898+ }
899+
857900 if (key == " operation" ) {
858901 sub->setOperation (val);
859902 }
@@ -933,16 +976,47 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE
933976 for (YAML::const_iterator it2 = it->second .begin (); it2 != it->second .end (); ++it2) {
934977 const YAML::Node first = it2->first ;
935978 const YAML::Node second = it2->second ;
979+ const string &key = first.as <std::string>();
980+
981+ // Special handling for set_sendto_headers which is a sequence of maps
982+ if (key == " set_sendto_headers" ) {
983+ if (!second.IsSequence ()) {
984+ const string reason = " set_sendto_headers must be a sequence" ;
985+ TSError (" Invalid YAML Configuration format for cookie_remap: %s, reason: %s" , filename.c_str (), reason.c_str ());
986+ return TS_ERROR;
987+ }
936988
937- if (second.IsScalar () == false ) {
938- const string reason = " All op nodes must be of type scalar" ;
939- TSError (" Invalid YAML Configuration format for cookie_remap: %s, reason: %s" , filename.c_str (), reason.c_str ());
940- return TS_ERROR;
941- }
989+ for (const auto &header_node : second) {
990+ if (!header_node.IsMap ()) {
991+ const string reason = " Each set_sendto_headers item must be a map" ;
992+ TSError (" Invalid YAML Configuration format for cookie_remap: %s, reason: %s" , filename.c_str (), reason.c_str ());
993+ return TS_ERROR;
994+ }
995+
996+ // Each header should be a single-key map
997+ if (header_node.size () != 1 ) {
998+ const string reason = " Each set_sendto_headers item must be a single key-value pair" ;
999+ TSError (" Invalid YAML Configuration format for cookie_remap: %s, reason: %s" , filename.c_str (), reason.c_str ());
1000+ return TS_ERROR;
1001+ }
9421002
943- const string &key = first.as <std::string>();
944- const string &value = second.as <std::string>();
945- op_data.emplace_back (key, value);
1003+ for (const auto &kv : header_node) {
1004+ const string &header_name = kv.first .as <std::string>();
1005+ const string &header_value = kv.second .as <std::string>();
1006+ // Store with special prefix to identify in build_op
1007+ op_data.emplace_back (" __set_sendto_header__" , header_name + " : " + header_value);
1008+ }
1009+ }
1010+ } else {
1011+ if (second.IsScalar () == false ) {
1012+ TSError (" Invalid YAML Configuration format for cookie_remap: %s, non-scalar value for key: %s (type=%d)" ,
1013+ filename.c_str (), key.c_str (), second.Type ());
1014+ return TS_ERROR;
1015+ }
1016+
1017+ const string &value = second.as <std::string>();
1018+ op_data.emplace_back (key, value);
1019+ }
9461020 }
9471021
9481022 if (op_data.size ()) {
@@ -1206,8 +1280,10 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
12061280
12071281 for (auto &op : *ops) {
12081282 Dbg (dbg_ctl, " >>> processing new operation" );
1209- bool used_sendto = false ;
1210- if (op->process (jar, rewrite_to, status, rri, req_url, used_sendto)) {
1283+ bool used_sendto = false ;
1284+ std::vector<std::string> regex_match_strings;
1285+ int regex_ccount = 0 ;
1286+ if (op->process (jar, rewrite_to, status, rri, req_url, used_sendto, regex_match_strings, regex_ccount)) {
12111287 cr_substitutions (rewrite_to, req_url);
12121288
12131289 size_t pos = 7 ; // 7 because we want to ignore the // in
@@ -1268,12 +1344,101 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
12681344 TSError (" can't parse substituted URL string" );
12691345 goto error;
12701346 } else {
1347+ bool host_header_was_set = false ;
1348+
1349+ // Set custom headers if configured and we took the sendto path.
1350+ if (!op->getSendtoHeaders ().empty () && used_sendto) {
1351+ for (auto const &header_pair : op->getSendtoHeaders ()) {
1352+ std::string header_name = header_pair.first ;
1353+ std::string header_value = header_pair.second ;
1354+
1355+ // Apply regex substitution to header value if we have regex matches ($1, $2, etc.)
1356+ if (regex_ccount > 0 && !regex_match_strings.empty () && header_value.find (' $' ) != std::string::npos) {
1357+ std::string::size_type pos = 0 ;
1358+ std::string::size_type ppos = 0 ;
1359+ std::string substituted_value;
1360+ substituted_value.reserve (header_value.size () * 2 );
1361+
1362+ while (pos < header_value.length ()) {
1363+ pos = header_value.find (' $' , ppos);
1364+ if (pos == std::string::npos) {
1365+ break ;
1366+ }
1367+ // Check if there's a digit after the $
1368+ if (pos + 1 < header_value.length () && isdigit (header_value[pos + 1 ])) {
1369+ int const ix = header_value[pos + 1 ] - ' 0' ;
1370+ if (ix <= regex_ccount && ix < static_cast <int >(regex_match_strings.size ())) {
1371+ // Append everything before the $
1372+ substituted_value += header_value.substr (ppos, pos - ppos);
1373+ // Append the regex match string
1374+ substituted_value += regex_match_strings[ix];
1375+ // Move past the $N
1376+ ppos = pos + 2 ;
1377+ }
1378+ }
1379+ pos++;
1380+ }
1381+ // Append any remaining text
1382+ if (ppos < header_value.length ()) {
1383+ substituted_value += header_value.substr (ppos);
1384+ }
1385+ header_value = substituted_value;
1386+ }
1387+
1388+ // Apply cr_substitutions for variables like $path, $cr_req_url, etc.
1389+ cr_substitutions (header_value, req_url);
1390+
1391+ Dbg (dbg_ctl, " Setting header: %s to value: %s" , header_name.c_str (), header_value.c_str ());
1392+
1393+ // Find or create the header
1394+ TSMLoc field_loc = TSMimeHdrFieldFind (rri->requestBufp , rri->requestHdrp , header_name.c_str (), header_name.length ());
1395+
1396+ if (field_loc == TS_NULL_MLOC) {
1397+ // Header doesn't exist, create it
1398+ if (TS_SUCCESS == TSMimeHdrFieldCreateNamed (rri->requestBufp , rri->requestHdrp , header_name.c_str (),
1399+ header_name.length (), &field_loc)) {
1400+ if (TS_SUCCESS == TSMimeHdrFieldValueStringSet (rri->requestBufp , rri->requestHdrp , field_loc, -1 ,
1401+ header_value.c_str (), header_value.length ())) {
1402+ TSMimeHdrFieldAppend (rri->requestBufp , rri->requestHdrp , field_loc);
1403+ Dbg (dbg_ctl, " Created and set header: %s" , header_name.c_str ());
1404+ }
1405+ TSHandleMLocRelease (rri->requestBufp , rri->requestHdrp , field_loc);
1406+ }
1407+ } else {
1408+ // Header exists, update it
1409+ TSMLoc tmp = TS_NULL_MLOC;
1410+ bool first = true ;
1411+
1412+ while (field_loc != TS_NULL_MLOC) {
1413+ tmp = TSMimeHdrFieldNextDup (rri->requestBufp , rri->requestHdrp , field_loc);
1414+ if (first) {
1415+ first = false ;
1416+ if (TS_SUCCESS == TSMimeHdrFieldValueStringSet (rri->requestBufp , rri->requestHdrp , field_loc, -1 ,
1417+ header_value.c_str (), header_value.length ())) {
1418+ Dbg (dbg_ctl, " Updated header: %s" , header_name.c_str ());
1419+ }
1420+ } else {
1421+ // Remove duplicate headers
1422+ TSMimeHdrFieldDestroy (rri->requestBufp , rri->requestHdrp , field_loc);
1423+ }
1424+ TSHandleMLocRelease (rri->requestBufp , rri->requestHdrp , field_loc);
1425+ field_loc = tmp;
1426+ }
1427+ }
1428+
1429+ // Check if we're setting the Host header (case-insensitive)
1430+ if (strcasecmp (header_name.c_str (), " Host" ) == 0 ) {
1431+ host_header_was_set = true ;
1432+ }
1433+ }
1434+ }
1435+
12711436 // Disable pristine host header if configured to do so and we took the
12721437 // sendto path. This allows the Host header to be updated to match the
12731438 // remapped destination. The else path (i.e., the non-sendto one)
12741439 // always preserves the pristine host header configuration, whether
12751440 // enabled or disabled.
1276- if (op->getDisablePristineHostHdr () && used_sendto ) {
1441+ if (used_sendto && ( op->getDisablePristineHostHdr () || host_header_was_set) ) {
12771442 Dbg (dbg_ctl, " Disabling pristine_host_hdr for this transaction (sendto path)" );
12781443 TSHttpTxnConfigIntSet (txnp, TS_CONFIG_URL_REMAP_PRISTINE_HOST_HDR, 0 );
12791444 }
0 commit comments