tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

TestEtcHostsParsing.cpp (9726B)


      1 #include "gtest/gtest.h"
      2 
      3 #include "mozilla/net/rust_helper.h"
      4 #include "nsDirectoryServiceDefs.h"
      5 #include "nsDirectoryServiceUtils.h"
      6 #include "nsIFile.h"
      7 #include "nsIFileStreams.h"
      8 #include "nsNetUtil.h"
      9 #include "nsString.h"
     10 #include "nsTArray.h"
     11 #include "nsUnicharUtils.h"
     12 #include <fcntl.h>
     13 #include <sys/stat.h>
     14 
     15 #ifndef XP_WIN
     16 #  include <sys/file.h>
     17 #  include <unistd.h>
     18 #endif
     19 
     20 #ifdef XP_WIN
     21 #  include <io.h>
     22 #  include <share.h>
     23 #endif
     24 
     25 using namespace mozilla;
     26 using namespace mozilla::net;
     27 
     28 class TestEtcHostsParsing : public ::testing::Test {
     29 protected:
     30  void SetUp() override {
     31    // Get temp directory
     32    nsCOMPtr<nsIFile> tmpDir;
     33    nsresult rv =
     34        NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpDir));
     35    ASSERT_TRUE(NS_SUCCEEDED(rv));
     36 
     37    // Create unique test file names
     38    nsAutoString baseName(u"test_hosts_");
     39    baseName.AppendInt(PR_IntervalNow());
     40 
     41    // Normal test file
     42    rv = tmpDir->Clone(getter_AddRefs(mTestHostsFile));
     43    ASSERT_TRUE(NS_SUCCEEDED(rv));
     44    nsAutoString normalName = baseName + u"_normal"_ns;
     45    rv = mTestHostsFile->Append(normalName);
     46    ASSERT_TRUE(NS_SUCCEEDED(rv));
     47 
     48    // Locked test file
     49    rv = tmpDir->Clone(getter_AddRefs(mLockedHostsFile));
     50    ASSERT_TRUE(NS_SUCCEEDED(rv));
     51    nsAutoString lockedName = baseName + u"_locked"_ns;
     52    rv = mLockedHostsFile->Append(lockedName);
     53    ASSERT_TRUE(NS_SUCCEEDED(rv));
     54 
     55    // Permission denied test file
     56    rv = tmpDir->Clone(getter_AddRefs(mNoPermHostsFile));
     57    ASSERT_TRUE(NS_SUCCEEDED(rv));
     58    nsAutoString noPermName = baseName + u"_noperm"_ns;
     59    rv = mNoPermHostsFile->Append(noPermName);
     60    ASSERT_TRUE(NS_SUCCEEDED(rv));
     61 
     62    mCallbackInvoked = false;
     63    mCallbackResult = true;
     64    mParsedHosts.Clear();
     65  }
     66 
     67  void TearDown() override {
     68    // Clean up test files
     69    if (mTestHostsFile) {
     70      mTestHostsFile->Remove(false);
     71    }
     72    if (mLockedHostsFile) {
     73      mLockedHostsFile->Remove(false);
     74    }
     75    if (mNoPermHostsFile) {
     76      mNoPermHostsFile->Remove(false);
     77    }
     78 
     79    // Close any open file descriptors
     80    if (mLockFd != -1) {
     81 #ifdef XP_WIN
     82      _close(mLockFd);
     83 #else
     84      close(mLockFd);
     85 #endif
     86      mLockFd = -1;
     87    }
     88  }
     89 
     90  void CreateNormalHostsFile() {
     91    nsCOMPtr<nsIOutputStream> stream;
     92    nsresult rv =
     93        NS_NewLocalFileOutputStream(getter_AddRefs(stream), mTestHostsFile);
     94    ASSERT_TRUE(NS_SUCCEEDED(rv));
     95 
     96    const char* content =
     97        "127.0.0.1 localhost\n"
     98        "::1 localhost\n"
     99        "127.0.0.1 example.com www.example.com\n"
    100        "# This is a comment\n"
    101        "192.168.1.1 router.local\n";
    102 
    103    uint32_t written;
    104    rv = stream->Write(content, strlen(content), &written);
    105    ASSERT_TRUE(NS_SUCCEEDED(rv));
    106    ASSERT_EQ(written, strlen(content));
    107 
    108    rv = stream->Close();
    109    ASSERT_TRUE(NS_SUCCEEDED(rv));
    110  }
    111 
    112  void CreateLockedHostsFile() {
    113    // First create the file with content
    114    nsCOMPtr<nsIOutputStream> stream;
    115    nsresult rv =
    116        NS_NewLocalFileOutputStream(getter_AddRefs(stream), mLockedHostsFile);
    117    ASSERT_TRUE(NS_SUCCEEDED(rv));
    118 
    119    const char* content = "127.0.0.1 locked.test\n";
    120    uint32_t written;
    121    rv = stream->Write(content, strlen(content), &written);
    122    ASSERT_TRUE(NS_SUCCEEDED(rv));
    123    rv = stream->Close();
    124    ASSERT_TRUE(NS_SUCCEEDED(rv));
    125 
    126    // Now lock it by opening with exclusive access
    127    nsAutoCString nativePath;
    128    rv = GetCrossplatformNativePath(mLockedHostsFile, nativePath);
    129    ASSERT_TRUE(NS_SUCCEEDED(rv));
    130 
    131 #ifdef XP_WIN
    132    // On Windows, open with no sharing to simulate a locked file
    133    mLockFd = _sopen(nativePath.get(), _O_RDONLY, _SH_DENYRW, _S_IREAD);
    134 #else
    135    // On Unix, use flock to lock the file
    136    mLockFd = open(nativePath.get(), O_RDONLY);
    137    if (mLockFd != -1) {
    138      ::flock(mLockFd, LOCK_EX | LOCK_NB);  // Exclusive, non-blocking lock
    139    }
    140 #endif
    141    ASSERT_NE(mLockFd, -1);
    142  }
    143 
    144  void CreateNoPermissionFile() {
    145 #ifndef XP_WIN
    146    // Create the file first
    147    nsCOMPtr<nsIOutputStream> stream;
    148    nsresult rv =
    149        NS_NewLocalFileOutputStream(getter_AddRefs(stream), mNoPermHostsFile);
    150    ASSERT_TRUE(NS_SUCCEEDED(rv));
    151 
    152    const char* content = "127.0.0.1 noperm.test\n";
    153    uint32_t written;
    154    rv = stream->Write(content, strlen(content), &written);
    155    ASSERT_TRUE(NS_SUCCEEDED(rv));
    156    rv = stream->Close();
    157    ASSERT_TRUE(NS_SUCCEEDED(rv));
    158 
    159    // Remove read permissions
    160    nsAutoCString nativePath;
    161    rv = GetCrossplatformNativePath(mNoPermHostsFile, nativePath);
    162    ASSERT_TRUE(NS_SUCCEEDED(rv));
    163    chmod(nativePath.get(), 0000);  // No permissions
    164 #endif
    165  }
    166 
    167  nsresult GetCrossplatformNativePath(nsIFile* aFile, nsACString& aPath) {
    168 #ifdef XP_WIN
    169    nsAutoString widePath;
    170    nsresult rv = aFile->GetPath(widePath);
    171    if (NS_FAILED(rv)) return rv;
    172    aPath = NS_ConvertUTF16toUTF8(widePath);
    173    return NS_OK;
    174 #else
    175    return aFile->GetNativePath(aPath);
    176 #endif
    177  }
    178 
    179  static bool TestCallback(const nsTArray<nsCString>* aArray) {
    180    auto* self = static_cast<TestEtcHostsParsing*>(sCurrentTest);
    181    self->mCallbackInvoked = true;
    182 
    183    if (aArray) {
    184      self->mParsedHosts.AppendElements(*aArray);
    185    }
    186 
    187    return self->mCallbackResult;
    188  }
    189 
    190  nsCOMPtr<nsIFile> mTestHostsFile;
    191  nsCOMPtr<nsIFile> mLockedHostsFile;
    192  nsCOMPtr<nsIFile> mNoPermHostsFile;
    193  int mLockFd = -1;
    194 
    195  bool mCallbackInvoked;
    196  bool mCallbackResult;
    197  nsTArray<nsCString> mParsedHosts;
    198 
    199  static TestEtcHostsParsing* sCurrentTest;
    200 };
    201 
    202 TestEtcHostsParsing* TestEtcHostsParsing::sCurrentTest = nullptr;
    203 
    204 TEST_F(TestEtcHostsParsing, ParseNormalFile) {
    205  sCurrentTest = this;
    206  CreateNormalHostsFile();
    207 
    208  nsAutoCString path;
    209  nsresult rv = GetCrossplatformNativePath(mTestHostsFile, path);
    210  ASSERT_TRUE(NS_SUCCEEDED(rv));
    211 
    212  rust_parse_etc_hosts(&path, TestCallback);
    213 
    214  EXPECT_TRUE(mCallbackInvoked);
    215  EXPECT_GT(mParsedHosts.Length(), 0u);
    216 
    217  // Check that expected hostnames are parsed
    218  bool foundLocalhost = false;
    219  bool foundExampleCom = false;
    220  bool foundWwwExampleCom = false;
    221  bool foundRouterLocal = false;
    222 
    223  for (const auto& host : mParsedHosts) {
    224    if (host.EqualsLiteral("localhost")) foundLocalhost = true;
    225    if (host.EqualsLiteral("example.com")) foundExampleCom = true;
    226    if (host.EqualsLiteral("www.example.com")) foundWwwExampleCom = true;
    227    if (host.EqualsLiteral("router.local")) foundRouterLocal = true;
    228  }
    229 
    230  EXPECT_TRUE(foundLocalhost);
    231  EXPECT_TRUE(foundExampleCom);
    232  EXPECT_TRUE(foundWwwExampleCom);
    233  EXPECT_TRUE(foundRouterLocal);
    234 }
    235 
    236 TEST_F(TestEtcHostsParsing, ParseLockedFile) {
    237  sCurrentTest = this;
    238  CreateLockedHostsFile();
    239 
    240  nsAutoCString path;
    241  nsresult rv = GetCrossplatformNativePath(mLockedHostsFile, path);
    242  ASSERT_TRUE(NS_SUCCEEDED(rv));
    243 
    244  // The function should handle locked files gracefully and not crash or hang
    245  rust_parse_etc_hosts(&path, TestCallback);
    246 
    247  // On some systems, locked files might still be readable, on others not.
    248  // The important thing is that the function doesn't crash or hang.
    249  // We don't assert on mCallbackInvoked because it depends on platform
    250  // behavior.
    251 }
    252 
    253 TEST_F(TestEtcHostsParsing, ParseNoPermissionFile) {
    254 #ifndef XP_WIN
    255  sCurrentTest = this;
    256  CreateNoPermissionFile();
    257 
    258  nsAutoCString path;
    259  nsresult rv = GetCrossplatformNativePath(mNoPermHostsFile, path);
    260  ASSERT_TRUE(NS_SUCCEEDED(rv));
    261 
    262  // The function should handle permission denied gracefully
    263  rust_parse_etc_hosts(&path, TestCallback);
    264 
    265  // Callback should not be invoked for files we can't read
    266  EXPECT_FALSE(mCallbackInvoked);
    267 #endif
    268 }
    269 
    270 TEST_F(TestEtcHostsParsing, ParseNonExistentFile) {
    271  sCurrentTest = this;
    272 
    273  nsAutoCString fakePath("/nonexistent/file/hosts");
    274 
    275  // The function should handle non-existent files gracefully
    276  rust_parse_etc_hosts(&fakePath, TestCallback);
    277 
    278  // Callback should not be invoked for non-existent files
    279  EXPECT_FALSE(mCallbackInvoked);
    280 }
    281 
    282 TEST_F(TestEtcHostsParsing, CallbackReturnsFalse) {
    283  sCurrentTest = this;
    284  CreateNormalHostsFile();
    285  mCallbackResult = false;  // Tell callback to return false
    286 
    287  nsAutoCString path;
    288  nsresult rv = GetCrossplatformNativePath(mTestHostsFile, path);
    289  ASSERT_TRUE(NS_SUCCEEDED(rv));
    290 
    291  rust_parse_etc_hosts(&path, TestCallback);
    292 
    293  EXPECT_TRUE(mCallbackInvoked);
    294  // When callback returns false, parsing should stop early
    295  // So we shouldn't get all the hosts
    296 }
    297 
    298 TEST_F(TestEtcHostsParsing, EmptyFile) {
    299  sCurrentTest = this;
    300 
    301  // Create empty file
    302  nsCOMPtr<nsIOutputStream> stream;
    303  nsresult rv =
    304      NS_NewLocalFileOutputStream(getter_AddRefs(stream), mTestHostsFile);
    305  ASSERT_TRUE(NS_SUCCEEDED(rv));
    306  rv = stream->Close();
    307  ASSERT_TRUE(NS_SUCCEEDED(rv));
    308 
    309  nsAutoCString path;
    310  rv = GetCrossplatformNativePath(mTestHostsFile, path);
    311  ASSERT_TRUE(NS_SUCCEEDED(rv));
    312 
    313  rust_parse_etc_hosts(&path, TestCallback);
    314 
    315  // Callback might or might not be invoked for empty files, but shouldn't crash
    316  EXPECT_EQ(mParsedHosts.Length(), 0u);
    317 }
    318 
    319 TEST_F(TestEtcHostsParsing, FileWithOnlyComments) {
    320  sCurrentTest = this;
    321 
    322  nsCOMPtr<nsIOutputStream> stream;
    323  nsresult rv =
    324      NS_NewLocalFileOutputStream(getter_AddRefs(stream), mTestHostsFile);
    325  ASSERT_TRUE(NS_SUCCEEDED(rv));
    326 
    327  const char* content =
    328      "# This is a comment\n"
    329      "# Another comment\n"
    330      "   # Indented comment\n";
    331 
    332  uint32_t written;
    333  rv = stream->Write(content, strlen(content), &written);
    334  ASSERT_TRUE(NS_SUCCEEDED(rv));
    335  rv = stream->Close();
    336  ASSERT_TRUE(NS_SUCCEEDED(rv));
    337 
    338  nsAutoCString path;
    339  rv = GetCrossplatformNativePath(mTestHostsFile, path);
    340  ASSERT_TRUE(NS_SUCCEEDED(rv));
    341 
    342  rust_parse_etc_hosts(&path, TestCallback);
    343 
    344  // Should not parse any hosts from a file with only comments
    345  EXPECT_EQ(mParsedHosts.Length(), 0u);
    346 }