blob: 1ff75017b0316ddf93c812ccef8ec87a905b6c74 [file] [log] [blame]
// Copyright 2018 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 "crosdns/hosts_modifier.h"
#include <arpa/inet.h>
#include <base/files/file.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
namespace {
// To avoid reading something too big, this should never happen.
constexpr size_t kMaxFileSize = 1048576; // 1MB.
// This is the delimiter we write out before our changes so that if we crash and
// are restarted we know what the base file was before we modified it.
constexpr char kFileModificationDelimeter[] =
"\n#####DYNAMIC-CROSDNS-ENTRIES#####\n";
constexpr char kAllowedHostnameSuffix[] = ".linux.test";
constexpr uint32_t kAllowedIpSubnet = 0x64735c00; // 100.115.92.0
constexpr uint32_t kAllowedIpMask = 0xFFFFFF00;
// Only allow IPs in the 100.115.92.0/24 subnet.
bool IsAllowedIpv4Address(uint32_t ipv4, std::string* err_out) {
if ((ipv4 & kAllowedIpMask) != kAllowedIpSubnet) {
*err_out = "IP address disallowed in mapping";
return false;
}
return true;
}
// Hostname must be *.test and use legal chars.
bool IsAllowedHostname(const std::string& hostname, std::string* err_out) {
// Make sure this is a legal hostname. It must be comprised of alphanumeric
// characters, dashes or dots. The dot or dash may not be the first character.
// It may not have consecutive dots. It must also have the '.test' suffix.
if (!base::EndsWith(hostname, kAllowedHostnameSuffix,
base::CompareCase::SENSITIVE)) {
*err_out = "Attempt to add invalid hostname to mapping of: " + hostname;
return false;
}
if (hostname[0] == '-' || hostname[0] == '.') {
*err_out = "First char in hostname may not be a dot or dash: " + hostname;
return false;
}
bool last_was_dot = false;
for (auto c : hostname) {
if (!base::IsAsciiAlpha(c) && !base::IsAsciiDigit(c) && c != '-') {
if (c == '.' && !last_was_dot) {
last_was_dot = true;
continue;
}
*err_out = "Invalid char in hostname: " + hostname;
return false;
}
last_was_dot = false;
}
return true;
}
} // namespace
namespace crosdns {
HostsModifier::HostsModifier() = default;
HostsModifier::~HostsModifier() = default;
bool HostsModifier::Init(const base::FilePath& hosts_filepath) {
filepath_ = hosts_filepath;
if (!base::ReadFileToStringWithMaxSize(filepath_, &base_hosts_contents_,
kMaxFileSize)) {
PLOG(ERROR) << "Failed reading in existing hostname file from "
<< filepath_.value();
return false;
}
// See if our delimiter was in there or not, if it is then we strip everything
// off after it (including the delimiter) and then rewrite out the file so
// that any stale entries are removed.
size_t delim_pos = base_hosts_contents_.find(kFileModificationDelimeter);
if (delim_pos == std::string::npos) {
return true;
}
// Delimeter was in there, strip it off and rewrite the file.
base_hosts_contents_.erase(delim_pos);
return WriteHostsFile();
}
bool HostsModifier::SetHostnameIpMapping(const std::string& hostname,
const std::string& ipv4,
const std::string& ipv6,
std::string* err_out) {
CHECK(err_out);
uint32_t int_ip;
if (inet_pton(AF_INET, ipv4.c_str(), &int_ip) != 1) {
*err_out = "Failed parsing IPv4 address: " + ipv4;
return false;
}
int_ip = htonl(int_ip);
if (!IsAllowedIpv4Address(int_ip, err_out)) {
return false;
}
// TODO(jkardatzke): Add IPv6 support when it is needed.
if (!IsAllowedHostname(hostname, err_out)) {
return false;
}
hostname_ipv4_map_[hostname] = ipv4;
if (!WriteHostsFile()) {
*err_out = "Failed writing the updated /etc/hosts file";
return false;
}
return true;
}
bool HostsModifier::RemoveHostnameIpMapping(const std::string& hostname,
std::string* err_out) {
CHECK(err_out);
if (!hostname_ipv4_map_.erase(hostname)) {
*err_out =
"Attempt to remove non-existent hostname mapping for: " + hostname;
return false;
}
if (!WriteHostsFile()) {
*err_out = "Failed writing the updated /etc/hosts file";
return false;
}
return true;
}
bool HostsModifier::WriteHostsFile() {
// We first write this to an adjacent temp file and then atomically rename
// that file to be our target aftewards.
base::FilePath temp_file_path;
if (!base::CreateTemporaryFileInDir(filepath_.DirName(), &temp_file_path)) {
PLOG(ERROR) << "Failed creating temp file in dir for hostname writing "
<< filepath_.DirName().value();
return false;
}
// Open our file for writing.
base::File temp_file(temp_file_path,
base::File::FLAG_OPEN | base::File::FLAG_WRITE);
if (!temp_file.IsValid()) {
PLOG(ERROR) << "Failed opening temp file for writing: "
<< temp_file_path.value();
return false;
}
// First write out the base contents for the file.
if (temp_file.WriteAtCurrentPos(base_hosts_contents_.c_str(),
base_hosts_contents_.size()) !=
base_hosts_contents_.size()) {
PLOG(ERROR) << "Failed writing base contents to temp file: "
<< temp_file_path.value();
return false;
}
// We only write out more if we have contents in our map.
if (!hostname_ipv4_map_.empty()) {
// Now write out our delimiter, which includes newlines at both ends so we
// are sure it is on its own line and we will be on a new line after this.
if (temp_file.WriteAtCurrentPos(kFileModificationDelimeter,
sizeof(kFileModificationDelimeter) - 1) !=
sizeof(kFileModificationDelimeter) - 1) {
PLOG(ERROR) << "Failed writing delimiter to temp file: "
<< temp_file_path.value();
return false;
}
// Now write out all of our entries, one per line.
for (const auto& entry : hostname_ipv4_map_) {
std::string curr_line = base::StringPrintf(
"%s %s\n", entry.second.c_str(), entry.first.c_str());
if (temp_file.WriteAtCurrentPos(curr_line.c_str(), curr_line.size()) !=
curr_line.size()) {
PLOG(ERROR) << "Failed writing hostname entry to temp file: "
<< temp_file_path.value();
return false;
}
}
}
// Done writing to the file, close it, ensure permissions are correct and
// rename it.
temp_file.Close();
if (!base::SetPosixFilePermissions(temp_file_path,
base::FILE_PERMISSION_READ_BY_USER |
base::FILE_PERMISSION_READ_BY_GROUP |
base::FILE_PERMISSION_READ_BY_OTHERS |
base::FILE_PERMISSION_WRITE_BY_USER)) {
PLOG(ERROR) << "Failed setting file permissions on our temp file: "
<< temp_file_path.value();
return false;
}
if (!base::ReplaceFile(temp_file_path, filepath_, nullptr)) {
PLOG(ERROR) << "Failed replacing existing hosts file with our temp file";
return false;
}
return true;
}
} // namespace crosdns