/* * Copyright (C) 1996-2023 The Squid Software Foundation and contributors * * Squid software is distributed under GPLv2+ license and includes * contributions from numerous individuals and organizations. * Please see the COPYING and CONTRIBUTORS files for details. */ /* DEBUG: section 66 HTTP Header Tools */ #include "squid.h" #include "acl/FilledChecklist.h" #include "acl/Gadgets.h" #include "base/EnumIterator.h" #include "client_side.h" #include "client_side_request.h" #include "comm/Connection.h" #include "compat/strtoll.h" #include "ConfigParser.h" #include "fde.h" #include "globals.h" #include "http/RegisteredHeaders.h" #include "http/Stream.h" #include "HttpHdrContRange.h" #include "HttpHeader.h" #include "HttpHeaderTools.h" #include "HttpRequest.h" #include "MemBuf.h" #include "sbuf/Stream.h" #include "sbuf/StringConvert.h" #include "SquidConfig.h" #include "Store.h" #include "StrList.h" #if USE_OPENSSL #include "ssl/support.h" #endif #include #include #include static void httpHeaderPutStrvf(HttpHeader * hdr, Http::HdrType id, const char *fmt, va_list vargs); static void httpHdrAdd(HttpHeader *heads, HttpRequest *request, const AccessLogEntryPointer &al, HeaderWithAclList &headersAdd); void httpHeaderMaskInit(HttpHeaderMask * mask, int value) { memset(mask, value, sizeof(*mask)); } /* same as httpHeaderPutStr, but formats the string using snprintf first */ void httpHeaderPutStrf(HttpHeader * hdr, Http::HdrType id, const char *fmt,...) { va_list args; va_start(args, fmt); httpHeaderPutStrvf(hdr, id, fmt, args); va_end(args); } /* used by httpHeaderPutStrf */ static void httpHeaderPutStrvf(HttpHeader * hdr, Http::HdrType id, const char *fmt, va_list vargs) { MemBuf mb; mb.init(); mb.vappendf(fmt, vargs); hdr->putStr(id, mb.buf); mb.clean(); } /** wrapper arrounf PutContRange */ void httpHeaderAddContRange(HttpHeader * hdr, HttpHdrRangeSpec spec, int64_t ent_len) { HttpHdrContRange *cr = httpHdrContRangeCreate(); assert(hdr && ent_len >= 0); httpHdrContRangeSet(cr, spec, ent_len); hdr->putContRange(cr); delete cr; } /** * \return true if a given directive is found in the Connection header field-value. * * \note if no Connection header exists we may check the Proxy-Connection header */ bool httpHeaderHasConnDir(const HttpHeader * hdr, const SBuf &directive) { String list; /* what type of header do we have? */ if (hdr->getList(Http::HdrType::CONNECTION, &list)) return strListIsMember(&list, directive, ',') != 0; #if USE_HTTP_VIOLATIONS if (hdr->getList(Http::HdrType::PROXY_CONNECTION, &list)) return strListIsMember(&list, directive, ',') != 0; #endif // else, no connection header for it to exist in return false; } /** handy to printf prefixes of potentially very long buffers */ const char * getStringPrefix(const char *str, size_t sz) { #define SHORT_PREFIX_SIZE 512 LOCAL_ARRAY(char, buf, SHORT_PREFIX_SIZE); xstrncpy(buf, str, (sz+1 > SHORT_PREFIX_SIZE) ? SHORT_PREFIX_SIZE : sz); return buf; } /** * parses an int field, complains if something went wrong, returns true on * success */ int httpHeaderParseInt(const char *start, int *value) { assert(value); *value = atoi(start); if (!*value && !xisdigit(*start)) { debugs(66, 2, "failed to parse an int header field near '" << start << "'"); return 0; } return 1; } bool httpHeaderParseOffset(const char *start, int64_t *value, char **endPtr) { char *end = nullptr; errno = 0; const int64_t res = strtoll(start, &end, 10); if (errno && !res) { debugs(66, 7, "failed to parse malformed offset in " << start); return false; } if (errno == ERANGE && (res == LLONG_MIN || res == LLONG_MAX)) { // no overflow debugs(66, 7, "failed to parse huge offset in " << start); return false; } if (start == end) { debugs(66, 7, "failed to parse empty offset"); return false; } *value = res; if (endPtr) *endPtr = end; debugs(66, 7, "offset " << start << " parsed as " << res); return true; } /** * Parses a quoted-string field (RFC 2616 section 2.2), complains if * something went wrong, returns non-zero on success. * Un-escapes quoted-pair characters found within the string. * start should point at the first double-quote. */ int httpHeaderParseQuotedString(const char *start, const int len, String *val) { const char *end, *pos; val->clean(); if (*start != '"') { debugs(66, 2, "failed to parse a quoted-string header field near '" << start << "'"); return 0; } pos = start + 1; while (*pos != '"' && len > (pos-start)) { if (*pos =='\r') { ++pos; if ((pos-start) > len || *pos != '\n') { debugs(66, 2, "failed to parse a quoted-string header field with '\\r' octet " << (start-pos) << " bytes into '" << start << "'"); val->clean(); return 0; } } if (*pos == '\n') { ++pos; if ( (pos-start) > len || (*pos != ' ' && *pos != '\t')) { debugs(66, 2, "failed to parse multiline quoted-string header field '" << start << "'"); val->clean(); return 0; } // TODO: replace the entire LWS with a space val->append(" "); ++pos; debugs(66, 2, "len < pos-start => " << len << " < " << (pos-start)); continue; } bool quoted = (*pos == '\\'); if (quoted) { ++pos; if (!*pos || (pos-start) > len) { debugs(66, 2, "failed to parse a quoted-string header field near '" << start << "'"); val->clean(); return 0; } } end = pos; while (end < (start+len) && *end != '\\' && *end != '\"' && (unsigned char)*end > 0x1F && *end != 0x7F) ++end; if (((unsigned char)*end <= 0x1F && *end != '\r' && *end != '\n') || *end == 0x7F) { debugs(66, 2, "failed to parse a quoted-string header field with CTL octet " << (start-pos) << " bytes into '" << start << "'"); val->clean(); return 0; } val->append(pos, end-pos); pos = end; } if (*pos != '\"') { debugs(66, 2, "failed to parse a quoted-string header field which did not end with \" "); val->clean(); return 0; } /* Make sure it's defined even if empty "" */ if (!val->termedBuf()) val->assign("", 0); return 1; } SBuf Http::SlowlyParseQuotedString(const char * const description, const char * const start, const size_t length) { String s; if (!httpHeaderParseQuotedString(start, length, &s)) throw TextException(ToSBuf("Cannot parse ", description, " as a quoted string"), Here()); return StringToSBuf(s); } SBuf httpHeaderQuoteString(const char *raw) { assert(raw); // TODO: Optimize by appending a sequence of characters instead of a char. // This optimization may be easier with Tokenizer after raw becomes SBuf. // RFC 7230 says a "sender SHOULD NOT generate a quoted-pair in a // quoted-string except where necessary" (i.e., DQUOTE and backslash) bool needInnerQuote = false; for (const char *s = raw; !needInnerQuote && *s; ++s) needInnerQuote = *s == '"' || *s == '\\'; SBuf quotedStr; quotedStr.append('"'); if (needInnerQuote) { for (const char *s = raw; *s; ++s) { if (*s == '"' || *s == '\\') quotedStr.append('\\'); quotedStr.append(*s); } } else { quotedStr.append(raw); } quotedStr.append('"'); return quotedStr; } /** * Checks the anonymizer (header_access) configuration. * * \retval 0 Header is explicitly blocked for removal * \retval 1 Header is explicitly allowed * \retval 1 Header has been replaced, the current version can be used. * \retval 1 Header has no access controls to test */ static int httpHdrMangle(HttpHeaderEntry * e, HttpRequest * request, HeaderManglers *hms, const AccessLogEntryPointer &al) { int retval; assert(e); const headerMangler *hm = hms->find(*e); /* mangler or checklist went away. default allow */ if (!hm || !hm->access_list) { debugs(66, 7, "couldn't find mangler or access list. Allowing"); return 1; } ACLFilledChecklist checklist(hm->access_list, request); checklist.updateAle(al); // XXX: The two "It was denied" clauses below mishandle cases with no // matching rules, violating the "If no rules within the set have matching // ACLs, the header field is left as is" promise in squid.conf. // TODO: Use Acl::Answer::implicit. See HttpStateData::forwardUpgrade(). if (checklist.fastCheck().allowed()) { /* aclCheckFast returns true for allow. */ debugs(66, 7, "checklist for mangler is positive. Mangle"); retval = 1; } else if (nullptr == hm->replacement) { /* It was denied, and we don't have any replacement */ debugs(66, 7, "checklist denied, we have no replacement. Pass"); // XXX: We said "Pass", but the caller will delete on zero retval. retval = 0; } else { /* It was denied, but we have a replacement. Replace the * header on the fly, and return that the new header * is allowed. */ debugs(66, 7, "checklist denied but we have replacement. Replace"); e->value = hm->replacement; retval = 1; } return retval; } /** Mangles headers for a list of headers. */ void httpHdrMangleList(HttpHeader *l, HttpRequest *request, const AccessLogEntryPointer &al, req_or_rep_t req_or_rep) { HttpHeaderEntry *e; HttpHeaderPos p = HttpHeaderInitPos; /* check with anonymizer tables */ HeaderManglers *hms = nullptr; HeaderWithAclList *headersAdd = nullptr; switch (req_or_rep) { case ROR_REQUEST: hms = Config.request_header_access; headersAdd = Config.request_header_add; break; case ROR_REPLY: hms = Config.reply_header_access; headersAdd = Config.reply_header_add; break; } if (hms) { int headers_deleted = 0; while ((e = l->getEntry(&p))) { if (httpHdrMangle(e, request, hms, al) == 0) l->delAt(p, headers_deleted); } if (headers_deleted) l->refreshMask(); } if (headersAdd && !headersAdd->empty()) { httpHdrAdd(l, request, al, *headersAdd); } } static void header_mangler_clean(headerMangler &m) { aclDestroyAccessList(&m.access_list); safe_free(m.replacement); } static void header_mangler_dump_access(StoreEntry * entry, const char *option, const headerMangler &m, const char *name) { if (m.access_list != nullptr) { storeAppendPrintf(entry, "%s ", option); dump_acl_access(entry, name, m.access_list); } } static void header_mangler_dump_replacement(StoreEntry * entry, const char *option, const headerMangler &m, const char *name) { if (m.replacement) storeAppendPrintf(entry, "%s %s %s\n", option, name, m.replacement); } HeaderManglers::HeaderManglers() { memset(known, 0, sizeof(known)); memset(&all, 0, sizeof(all)); } HeaderManglers::~HeaderManglers() { for (auto i : WholeEnum()) header_mangler_clean(known[i]); for (auto i : custom) header_mangler_clean(i.second); header_mangler_clean(all); } void HeaderManglers::dumpAccess(StoreEntry * entry, const char *name) const { for (auto id : WholeEnum()) header_mangler_dump_access(entry, name, known[id], Http::HeaderLookupTable.lookup(id).name); for (auto i : custom) header_mangler_dump_access(entry, name, i.second, i.first.c_str()); header_mangler_dump_access(entry, name, all, "All"); } void HeaderManglers::dumpReplacement(StoreEntry * entry, const char *name) const { for (auto id : WholeEnum()) { header_mangler_dump_replacement(entry, name, known[id], Http::HeaderLookupTable.lookup(id).name); } for (auto i: custom) { header_mangler_dump_replacement(entry, name, i.second, i.first.c_str()); } header_mangler_dump_replacement(entry, name, all, "All"); } headerMangler * HeaderManglers::track(const char *name) { if (strcmp(name, "All") == 0) return &all; const Http::HdrType id = Http::HeaderLookupTable.lookup(SBuf(name)).id; if (id != Http::HdrType::BAD_HDR) return &known[id]; if (strcmp(name, "Other") == 0) return &known[Http::HdrType::OTHER]; return &custom[name]; } void HeaderManglers::setReplacement(const char *name, const char *value) { // for backword compatibility, we allow replacements to be configured // for headers w/o access rules, but such replacements are ignored headerMangler *m = track(name); safe_free(m->replacement); // overwrite old value if any m->replacement = xstrdup(value); } const headerMangler * HeaderManglers::find(const HttpHeaderEntry &e) const { // a known header with a configured ACL list if (e.id != Http::HdrType::OTHER && Http::any_HdrType_enum_value(e.id) && known[e.id].access_list) return &known[e.id]; // a custom header if (e.id == Http::HdrType::OTHER) { // does it have an ACL list configured? // Optimize: use a name type that we do not need to convert to here SBuf tmp(e.name); // XXX: performance regression. c_str() reallocates const ManglersByName::const_iterator i = custom.find(tmp.c_str()); if (i != custom.end()) return &i->second; } // Next-to-last resort: "Other" rules match any custom header if (e.id == Http::HdrType::OTHER && known[Http::HdrType::OTHER].access_list) return &known[Http::HdrType::OTHER]; // Last resort: "All" rules match any header if (all.access_list) return &all; return nullptr; } void httpHdrAdd(HttpHeader *heads, HttpRequest *request, const AccessLogEntryPointer &al, HeaderWithAclList &headersAdd) { ACLFilledChecklist checklist(nullptr, request); checklist.updateAle(al); for (HeaderWithAclList::const_iterator hwa = headersAdd.begin(); hwa != headersAdd.end(); ++hwa) { if (!hwa->aclList || checklist.fastCheck(hwa->aclList).allowed()) { const char *fieldValue = nullptr; MemBuf mb; if (hwa->quoted) { if (al != nullptr) { mb.init(); hwa->valueFormat->assemble(mb, al, 0); fieldValue = mb.content(); } } else { fieldValue = hwa->fieldValue.c_str(); } if (!fieldValue || fieldValue[0] == '\0') fieldValue = "-"; HttpHeaderEntry *e = new HttpHeaderEntry(hwa->fieldId, SBuf(hwa->fieldName), fieldValue); heads->addEntry(e); } } }