blob: 9aea6090f800226d9f965fcc92c4511f5ed5548e [file] [log] [blame] [edit]
// Copyright 2019 The Chromium OS 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 "debugd/src/helpers/cups_uri_helper_utils.h"
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_piece.h>
#include <base/strings/string_util.h>
#include <cstddef>
#include <string>
#include <vector>
namespace debugd {
namespace cups_helper {
namespace {
// Evaluates true when |c| is in the reserved set specified in RFC 3986.
bool CharIsReserved(char c) {
switch (c) {
case ':':
case '/':
case '?':
case '#':
case '[':
case ']':
case '@':
case '!':
case '$':
case '&':
case '\'':
case '(':
case ')':
case '*':
case '+':
case ',':
case ';':
case '=':
return true;
}
return false;
}
// Evaluates true when |c| is in the unreserved set specified in RFC 3986.
bool CharIsUnreserved(char c) {
switch (c) {
case '-':
case '.':
case '_':
case '~':
return true;
}
return base::IsAsciiAlpha(c) || base::IsAsciiDigit(c);
}
// Evaluates true when |c|
// * is in the reserved set specified by RFC 3986,
// * is in the unreserved set specified by RFC 3986, or
// * is a literal '%' (which is in neither preceding set).
bool CharIsAllowed(char c) {
return CharIsReserved(c) || CharIsUnreserved(c) || c == '%';
}
// Determines whether |uri| comprises mostly alphanumeric ASCII.
bool UriIsGoodAscii(const std::string& uri) {
int expected_hex_digits = 0;
for (const char c : uri) {
if (expected_hex_digits > 0) {
// We are currently processing a percent-encoded segment "%XY."
if (!base::IsHexDigit(c)) {
return false;
}
expected_hex_digits--;
} else if (c == '%') {
// We are not currently processing a percent-encoded segment and
// we see the start of a percent-encoded segment.
expected_hex_digits = 2;
} else if (!CharIsAllowed(c)) {
return false;
}
}
return expected_hex_digits == 0;
}
// Gets the starting index of the authority from |uri_view| - that is,
// returns the index following "://". If none is found, returns npos.
// Caller must ensure |uri_view| is already properly percent-encoded.
size_t UriAuthorityStartIndex(base::StringPiece uri_view) {
size_t scheme_ender = uri_view.find("://");
return scheme_ender == base::StringPiece::npos ? scheme_ender
: scheme_ender + 3;
}
// Evaluates true when |scheme_view| (including the trailing colon and 2
// slashes) equals a known printing URI scheme. Caller must ensure
// |scheme_view| is already properly percent-encoded.
bool SchemeIsForPrinting(base::StringPiece scheme_view) {
// Enumerate known printing URIs. Values are lifted from Chrome browser's
// Printer::GetProtocol().
const std::vector<base::StringPiece> known_schemes = {
"usb://", "ipp://", "ipps://", "http://",
"https://", "socket://", "lpd://", "ippusb://"};
for (const base::StringPiece scheme : known_schemes) {
if (base::EqualsCaseInsensitiveASCII(scheme_view, scheme)) {
return true;
}
}
return false;
}
// Evaluates true when the authority portion of a printing URI appears
// reasonable. Caller must ensure |authority_view| is already properly
// percent-encoded and does not contain the slash that begins the path.
bool AuthoritySeemsReasonable(base::StringPiece authority_view) {
if (authority_view.empty()) {
return false;
}
// My reading of RFC 3986 says to me that any non-reserved character
// in the host can be percent-encoded. I'm going to punt on decoding
// the variety of possible hosts and focus on the port number.
// TODO(kdlee): figure out why nobody else in platform2 uses libcurl.
size_t last_colon = authority_view.rfind(':');
if (last_colon == base::StringPiece::npos) {
// We don't see a port number - punt.
return true;
} else if (last_colon == authority_view.length() - 1) {
// We see a colon but no port number - this is unreasonable.
return false;
}
// There are several possibilities for other placements of the colon.
// 1. It could be inside the user info (before host and port).
// 2. It could be inside an IP literal (e.g. if the host is an IPv6
// address and no port is attached to this authority).
// 3. It could be near the end of the authority with actual numeric
// values following it.
base::StringPiece port_view = authority_view.substr(last_colon + 1);
if (port_view.find('@') != base::StringPiece::npos) {
// This colon is inside user info - punt.
return true;
} else if (base::StartsWith(authority_view, "[") &&
base::EndsWith(authority_view, "]")) {
// This colon is part of an IPv6 literal without a port number - punt.
return true;
}
// This must be intended to be a decimal port number.
for (const char c : port_view) {
if (!base::IsAsciiDigit(c)) {
return false;
}
}
size_t decimal_port;
if (!base::StringToSizeT(port_view, &decimal_port)) {
return false;
}
return (0ll <= decimal_port) && (decimal_port < 65536ll);
}
} // namespace
bool UriSeemsReasonable(const std::string& uri) {
if (!UriIsGoodAscii(uri)) {
return false;
}
base::StringPiece uri_view = uri;
size_t authority_starter = UriAuthorityStartIndex(uri_view);
if (authority_starter == base::StringPiece::npos ||
authority_starter == uri_view.length()) {
return false;
}
base::StringPiece scheme = uri_view.substr(0, authority_starter);
base::StringPiece after_scheme = uri_view.substr(authority_starter);
if (!SchemeIsForPrinting(scheme)) {
return false;
}
base::StringPiece authority = after_scheme.substr(0, after_scheme.find('/'));
if (!AuthoritySeemsReasonable(authority)) {
return false;
}
return true;
}
} // namespace cups_helper
} // namespace debugd