// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/http/http_content_disposition.h" #include "base/base64.h" #include "base/logging.h" #include "base/strings/string_piece.h" #include "base/strings/string_tokenizer.h" #include "base/strings/string_util.h" #include "base/strings/sys_string_conversions.h" #include "base/strings/utf_string_conversions.h" #include "net/base/escape.h" #include "net/base/net_string_util.h" #include "net/http/http_util.h" namespace net { namespace { enum RFC2047EncodingType { Q_ENCODING, B_ENCODING }; // Decodes a "Q" encoded string as described in RFC 2047 section 4.2. Similar to // decoding a quoted-printable string. Returns true if the input was valid. bool DecodeQEncoding(const std::string& input, std::string* output) { std::string temp; temp.reserve(input.size()); for (std::string::const_iterator it = input.begin(); it != input.end(); ++it) { if (*it == '_') { temp.push_back(' '); } else if (*it == '=') { if ((input.end() - it < 3) || !base::IsHexDigit(static_cast(*(it + 1))) || !base::IsHexDigit(static_cast(*(it + 2)))) return false; unsigned char ch = base::HexDigitToInt(*(it + 1)) * 16 + base::HexDigitToInt(*(it + 2)); temp.push_back(static_cast(ch)); ++it; ++it; } else if (0x20 < *it && *it < 0x7F && *it != '?') { // In a Q-encoded word, only printable ASCII characters // represent themselves. Besides, space, '=', '_' and '?' are // not allowed, but they're already filtered out. DCHECK_NE('=', *it); DCHECK_NE('?', *it); DCHECK_NE('_', *it); temp.push_back(*it); } else { return false; } } output->swap(temp); return true; } // Decodes a "Q" or "B" encoded string as per RFC 2047 section 4. The encoding // type is specified in |enc_type|. bool DecodeBQEncoding(const std::string& part, RFC2047EncodingType enc_type, const std::string& charset, std::string* output) { std::string decoded; if (!((enc_type == B_ENCODING) ? base::Base64Decode(part, &decoded) : DecodeQEncoding(part, &decoded))) { return false; } if (decoded.empty()) { output->clear(); return true; } return ConvertToUtf8(decoded, charset.c_str(), output); } bool DecodeWord(const std::string& encoded_word, const std::string& referrer_charset, bool* is_rfc2047, std::string* output, int* parse_result_flags) { *is_rfc2047 = false; output->clear(); if (encoded_word.empty()) return true; if (!base::IsStringASCII(encoded_word)) { // Try UTF-8, referrer_charset and the native OS default charset in turn. if (base::IsStringUTF8(encoded_word)) { *output = encoded_word; } else { base::string16 utf16_output; if (!referrer_charset.empty() && ConvertToUTF16(encoded_word, referrer_charset.c_str(), &utf16_output)) { *output = base::UTF16ToUTF8(utf16_output); } else { *output = base::WideToUTF8(base::SysNativeMBToWide(encoded_word)); } } *parse_result_flags |= HttpContentDisposition::HAS_NON_ASCII_STRINGS; return true; } // RFC 2047 : one of encoding methods supported by Firefox and relatively // widely used by web servers. // =?charset???= where '' is either 'B' or 'Q'. // We don't care about the length restriction (72 bytes) because // many web servers generate encoded words longer than the limit. std::string decoded_word; *is_rfc2047 = true; int part_index = 0; std::string charset; base::StringTokenizer t(encoded_word, "?"); RFC2047EncodingType enc_type = Q_ENCODING; while (*is_rfc2047 && t.GetNext()) { std::string part = t.token(); switch (part_index) { case 0: if (part != "=") { *is_rfc2047 = false; break; } ++part_index; break; case 1: // Do we need charset validity check here? charset = part; ++part_index; break; case 2: if (part.size() > 1 || part.find_first_of("bBqQ") == std::string::npos) { *is_rfc2047 = false; break; } if (part[0] == 'b' || part[0] == 'B') { enc_type = B_ENCODING; } ++part_index; break; case 3: *is_rfc2047 = DecodeBQEncoding(part, enc_type, charset, &decoded_word); if (!*is_rfc2047) { // Last minute failure. Invalid B/Q encoding. Rather than // passing it through, return now. return false; } ++part_index; break; case 4: if (part != "=") { // Another last minute failure ! // Likely to be a case of two encoded-words in a row or // an encoded word followed by a non-encoded word. We can be // generous, but it does not help much in terms of compatibility, // I believe. Return immediately. *is_rfc2047 = false; return false; } ++part_index; break; default: *is_rfc2047 = false; return false; } } if (*is_rfc2047) { if (*(encoded_word.end() - 1) == '=') { output->swap(decoded_word); *parse_result_flags |= HttpContentDisposition::HAS_RFC2047_ENCODED_STRINGS; return true; } // encoded_word ending prematurelly with '?' or extra '?' *is_rfc2047 = false; return false; } // We're not handling 'especial' characters quoted with '\', but // it should be Ok because we're not an email client but a // web browser. // What IE6/7 does: %-escaped UTF-8. decoded_word = UnescapeURLComponent(encoded_word, UnescapeRule::SPACES); if (decoded_word != encoded_word) *parse_result_flags |= HttpContentDisposition::HAS_PERCENT_ENCODED_STRINGS; if (base::IsStringUTF8(decoded_word)) { output->swap(decoded_word); return true; // We can try either the OS default charset or 'origin charset' here, // As far as I can tell, IE does not support it. However, I've seen // web servers emit %-escaped string in a legacy encoding (usually // origin charset). // TODO(jungshik) : Test IE further and consider adding a fallback here. } return false; } // Decodes the value of a 'filename' or 'name' parameter given as |input|. The // value is supposed to be of the form: // // value = token | quoted-string // // However we currently also allow RFC 2047 encoding and non-ASCII // strings. Non-ASCII strings are interpreted based on |referrer_charset|. bool DecodeFilenameValue(const std::string& input, const std::string& referrer_charset, std::string* output, int* parse_result_flags) { int current_parse_result_flags = 0; std::string decoded_value; bool is_previous_token_rfc2047 = true; // Tokenize with whitespace characters. base::StringTokenizer t(input, " \t\n\r"); t.set_options(base::StringTokenizer::RETURN_DELIMS); while (t.GetNext()) { if (t.token_is_delim()) { // If the previous non-delimeter token is not RFC2047-encoded, // put in a space in its place. Otheriwse, skip over it. if (!is_previous_token_rfc2047) decoded_value.push_back(' '); continue; } // We don't support a single multibyte character split into // adjacent encoded words. Some broken mail clients emit headers // with that problem, but most web servers usually encode a filename // in a single encoded-word. Firefox/Thunderbird do not support // it, either. std::string decoded; if (!DecodeWord(t.token(), referrer_charset, &is_previous_token_rfc2047, &decoded, ¤t_parse_result_flags)) return false; decoded_value.append(decoded); } output->swap(decoded_value); if (parse_result_flags && !output->empty()) *parse_result_flags |= current_parse_result_flags; return true; } // Parses the charset and value-chars out of an ext-value string. // // ext-value = charset "'" [ language ] "'" value-chars bool ParseExtValueComponents(const std::string& input, std::string* charset, std::string* value_chars) { base::StringTokenizer t(input, "'"); t.set_options(base::StringTokenizer::RETURN_DELIMS); std::string temp_charset; std::string temp_value; int numDelimsSeen = 0; while (t.GetNext()) { if (t.token_is_delim()) { ++numDelimsSeen; continue; } else { switch (numDelimsSeen) { case 0: temp_charset = t.token(); break; case 1: // Language is ignored. break; case 2: temp_value = t.token(); break; default: return false; } } } if (numDelimsSeen != 2) return false; if (temp_charset.empty() || temp_value.empty()) return false; charset->swap(temp_charset); value_chars->swap(temp_value); return true; } // http://tools.ietf.org/html/rfc5987#section-3.2 // // ext-value = charset "'" [ language ] "'" value-chars // // charset = "UTF-8" / "ISO-8859-1" / mime-charset // // mime-charset = 1*mime-charsetc // mime-charsetc = ALPHA / DIGIT // / "!" / "#" / "$" / "%" / "&" // / "+" / "-" / "^" / "_" / "`" // / "{" / "}" / "~" // // language = // // value-chars = *( pct-encoded / attr-char ) // // pct-encoded = "%" HEXDIG HEXDIG // // attr-char = ALPHA / DIGIT // / "!" / "#" / "$" / "&" / "+" / "-" / "." // / "^" / "_" / "`" / "|" / "~" bool DecodeExtValue(const std::string& param_value, std::string* decoded) { if (param_value.find('"') != std::string::npos) return false; std::string charset; std::string value; if (!ParseExtValueComponents(param_value, &charset, &value)) return false; // RFC 5987 value should be ASCII-only. if (!base::IsStringASCII(value)) { decoded->clear(); return true; } std::string unescaped = UnescapeURLComponent( value, UnescapeRule::SPACES | UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS); return ConvertToUtf8AndNormalize(unescaped, charset.c_str(), decoded); } } // namespace HttpContentDisposition::HttpContentDisposition( const std::string& header, const std::string& referrer_charset) : type_(INLINE), parse_result_flags_(INVALID) { Parse(header, referrer_charset); } HttpContentDisposition::~HttpContentDisposition() = default; std::string::const_iterator HttpContentDisposition::ConsumeDispositionType( std::string::const_iterator begin, std::string::const_iterator end) { DCHECK(type_ == INLINE); base::StringPiece header(begin, end); size_t delimiter = header.find(';'); base::StringPiece type = header.substr(0, delimiter); type = HttpUtil::TrimLWS(type); // If the disposition-type isn't a valid token the then the // Content-Disposition header is malformed, and we treat the first bytes as // a parameter rather than a disposition-type. if (type.empty() || !HttpUtil::IsToken(type)) return begin; parse_result_flags_ |= HAS_DISPOSITION_TYPE; DCHECK(type.find('=') == base::StringPiece::npos); if (base::LowerCaseEqualsASCII(type, "inline")) { type_ = INLINE; } else if (base::LowerCaseEqualsASCII(type, "attachment")) { type_ = ATTACHMENT; } else { parse_result_flags_ |= HAS_UNKNOWN_DISPOSITION_TYPE; type_ = ATTACHMENT; } return begin + (type.data() + type.size() - header.data()); } // http://tools.ietf.org/html/rfc6266 // // content-disposition = "Content-Disposition" ":" // disposition-type *( ";" disposition-parm ) // // disposition-type = "inline" | "attachment" | disp-ext-type // ; case-insensitive // disp-ext-type = token // // disposition-parm = filename-parm | disp-ext-parm // // filename-parm = "filename" "=" value // | "filename*" "=" ext-value // // disp-ext-parm = token "=" value // | ext-token "=" ext-value // ext-token = // void HttpContentDisposition::Parse(const std::string& header, const std::string& referrer_charset) { DCHECK(type_ == INLINE); DCHECK(filename_.empty()); std::string::const_iterator pos = header.begin(); std::string::const_iterator end = header.end(); pos = ConsumeDispositionType(pos, end); std::string filename; std::string ext_filename; HttpUtil::NameValuePairsIterator iter(pos, end, ';'); while (iter.GetNext()) { if (filename.empty() && base::LowerCaseEqualsASCII( base::StringPiece(iter.name_begin(), iter.name_end()), "filename")) { DecodeFilenameValue(iter.value(), referrer_charset, &filename, &parse_result_flags_); if (!filename.empty()) parse_result_flags_ |= HAS_FILENAME; } else if (ext_filename.empty() && base::LowerCaseEqualsASCII( base::StringPiece(iter.name_begin(), iter.name_end()), "filename*")) { DecodeExtValue(iter.raw_value(), &ext_filename); if (!ext_filename.empty()) parse_result_flags_ |= HAS_EXT_FILENAME; } } if (!ext_filename.empty()) filename_ = ext_filename; else filename_ = filename; } } // namespace net