| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- /*
- * Copyright (c) 2014, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- package psiphon
- import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
- "testing"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
- "github.com/stretchr/testify/suite"
- )
- type ConfigTestSuite struct {
- suite.Suite
- confStubBlob []byte
- requiredFields []string
- nonRequiredFields []string
- testDirectory string
- }
- func (suite *ConfigTestSuite) SetupSuite() {
- suite.confStubBlob = []byte(`
- {
- "PropagationChannelId" : "<placeholder>",
- "SponsorId" : "<placeholder>",
- "LocalHttpProxyPort" : 8080,
- "LocalSocksProxyPort" : 1080
- }
- `)
- var obj map[string]interface{}
- json.Unmarshal(suite.confStubBlob, &obj)
- // Use a temporary directory for the data root directory so any artifacts
- // created by config.Commit() can be cleaned up.
- testDirectory, err := ioutil.TempDir("", "psiphon-config-test")
- if err != nil {
- suite.T().Fatalf("TempDir failed: %s\n", err)
- }
- suite.testDirectory = testDirectory
- obj["DataRootDirectory"] = testDirectory
- suite.confStubBlob, err = json.Marshal(obj)
- if err != nil {
- suite.T().Fatalf("Marshal failed: %s\n", err)
- }
- for k, v := range obj {
- if k == "DataRootDirectory" {
- // skip
- } else if v == "<placeholder>" {
- suite.requiredFields = append(suite.requiredFields, k)
- } else {
- suite.nonRequiredFields = append(suite.nonRequiredFields, k)
- }
- }
- }
- func (suite *ConfigTestSuite) TearDownSuite() {
- if common.FileExists(suite.testDirectory) {
- err := os.RemoveAll(suite.testDirectory)
- if err != nil {
- suite.T().Fatalf("Failed to remove test directory %s: %s", suite.testDirectory, err.Error())
- }
- } else {
- suite.T().Fatalf("Test directory not found: %s", suite.testDirectory)
- }
- }
- func TestConfigTestSuite(t *testing.T) {
- suite.Run(t, new(ConfigTestSuite))
- }
- // Tests good config
- func (suite *ConfigTestSuite) Test_LoadConfig_BasicGood() {
- config, err := LoadConfig(suite.confStubBlob)
- if err == nil {
- err = config.Commit(false)
- }
- suite.Nil(err, "a basic config should succeed")
- }
- // Tests non-JSON file contents
- func (suite *ConfigTestSuite) Test_LoadConfig_BadFileContents() {
- _, err := LoadConfig([]byte(`this is not JSON`))
- suite.NotNil(err, "bytes that are not JSON at all should give an error")
- }
- // Tests config file with JSON contents that don't match our structure
- func (suite *ConfigTestSuite) Test_LoadConfig_BadJson() {
- var testObj map[string]interface{}
- var testObjJSON []byte
- // JSON with none of our fields
- //
- // DataRootDirectory must to be set to avoid a migration in the current
- // working directory.
- config, err := LoadConfig([]byte(
- fmt.Sprintf(
- `{"f1": 11, "f2": "two", "DataRootDirectory" : %s}`,
- suite.testDirectory)))
- if err == nil {
- err = config.Commit(false)
- }
- suite.NotNil(err, "JSON with none of our fields should fail")
- // Test all required fields
- for _, field := range suite.requiredFields {
- // Missing a required field
- json.Unmarshal(suite.confStubBlob, &testObj)
- delete(testObj, field)
- testObjJSON, _ = json.Marshal(testObj)
- config, err = LoadConfig(testObjJSON)
- if err == nil {
- err = config.Commit(false)
- }
- suite.NotNil(err, "JSON with one of our required fields missing should fail: %s", field)
- // Bad type for required field
- json.Unmarshal(suite.confStubBlob, &testObj)
- testObj[field] = false // basically guessing a wrong type
- testObjJSON, _ = json.Marshal(testObj)
- config, err = LoadConfig(testObjJSON)
- if err == nil {
- err = config.Commit(false)
- }
- suite.NotNil(err, "JSON with one of our required fields with the wrong type should fail: %s", field)
- // One of our required fields is null
- json.Unmarshal(suite.confStubBlob, &testObj)
- testObj[field] = nil
- testObjJSON, _ = json.Marshal(testObj)
- config, err = LoadConfig(testObjJSON)
- if err == nil {
- err = config.Commit(false)
- }
- suite.NotNil(err, "JSON with one of our required fields set to null should fail: %s", field)
- // One of our required fields is an empty string
- json.Unmarshal(suite.confStubBlob, &testObj)
- testObj[field] = ""
- testObjJSON, _ = json.Marshal(testObj)
- config, err = LoadConfig(testObjJSON)
- if err == nil {
- err = config.Commit(false)
- }
- suite.NotNil(err, "JSON with one of our required fields set to an empty string should fail: %s", field)
- }
- // Test optional fields
- for _, field := range suite.nonRequiredFields {
- // Has incorrect type for optional field
- json.Unmarshal(suite.confStubBlob, &testObj)
- testObj[field] = false // basically guessing a wrong type
- testObjJSON, _ = json.Marshal(testObj)
- config, err = LoadConfig(testObjJSON)
- if err == nil {
- err = config.Commit(false)
- }
- suite.NotNil(err, "JSON with one of our optional fields with the wrong type should fail: %s", field)
- }
- }
- // Tests config file with JSON contents that don't match our structure
- func (suite *ConfigTestSuite) Test_LoadConfig_GoodJson() {
- var testObj map[string]interface{}
- var testObjJSON []byte
- // TODO: Test that the config actually gets the values we expect?
- // Has all of our required fields, but no optional fields
- json.Unmarshal(suite.confStubBlob, &testObj)
- for i := range suite.nonRequiredFields {
- delete(testObj, suite.nonRequiredFields[i])
- }
- testObjJSON, _ = json.Marshal(testObj)
- config, err := LoadConfig(testObjJSON)
- if err == nil {
- err = config.Commit(false)
- }
- suite.Nil(err, "JSON with good values for our required fields but no optional fields should succeed")
- // Has all of our required fields, and all optional fields
- config, err = LoadConfig(suite.confStubBlob)
- if err == nil {
- err = config.Commit(false)
- }
- suite.Nil(err, "JSON with all good values for required and optional fields should succeed")
- // Has null for optional fields
- json.Unmarshal(suite.confStubBlob, &testObj)
- for i := range suite.nonRequiredFields {
- testObj[suite.nonRequiredFields[i]] = nil
- }
- testObjJSON, _ = json.Marshal(testObj)
- config, err = LoadConfig(testObjJSON)
- if err == nil {
- err = config.Commit(false)
- }
- suite.Nil(err, "JSON with null for optional values should succeed")
- }
- func (suite *ConfigTestSuite) Test_LoadConfig_Migrate() {
- oslFiles := []FileTree{
- {
- Name: "osl-registry",
- },
- {
- Name: "osl-registry.cached",
- },
- {
- Name: "osl-1",
- },
- {
- Name: "osl-1.part",
- }}
- nonOSLFile := FileTree{
- Name: "should_not_be_deleted",
- Children: []FileTree{
- {
- Name: "should_not_be_deleted",
- },
- },
- }
- // Test where OSL directory is not deleted after migration because
- // it contains non-OSL files.
- LoadConfigMigrateTest(append(oslFiles, nonOSLFile), &nonOSLFile, suite)
- // Test where OSL directory is deleted after migration because it only
- // contained OSL files.
- LoadConfigMigrateTest(oslFiles, nil, suite)
- }
- // Test when migrating from old config fields results in filesystem changes.
- func LoadConfigMigrateTest(oslDirChildrenPreMigration []FileTree, oslDirChildrenPostMigration *FileTree, suite *ConfigTestSuite) {
- // This test needs its own temporary directory because a previous test may
- // have paved the file which signals that migration has already been
- // completed.
- testDirectory, err := ioutil.TempDir("", "psiphon-config-migration-test")
- if err != nil {
- suite.T().Fatalf("TempDir failed: %s\n", err)
- }
- defer func() {
- if common.FileExists(testDirectory) {
- err := os.RemoveAll(testDirectory)
- if err != nil {
- suite.T().Fatalf("Failed to remove test directory %s: %s", testDirectory, err.Error())
- }
- }
- }()
- // Pre migration files and directories
- oldDataStoreDirectory := filepath.Join(testDirectory, "datastore_old")
- oldRemoteServerListname := "rsl"
- oldObfuscatedServerListDirectoryName := "obfuscated_server_list"
- oldObfuscatedServerListDirectory := filepath.Join(testDirectory, oldObfuscatedServerListDirectoryName)
- oldUpgradeDownloadFilename := "upgrade"
- oldRotatingNoticesFilename := "rotating_notices"
- oldHomepageNoticeFilename := "homepage"
- // Post migration data root directory
- testDataRootDirectory := filepath.Join(testDirectory, "data_root_directory")
- oldFileTree := FileTree{
- Name: testDirectory,
- Children: []FileTree{
- {
- Name: "datastore_old",
- Children: []FileTree{
- {
- Name: "psiphon.boltdb",
- },
- {
- Name: "psiphon.boltdb.lock",
- },
- {
- Name: "non_tunnel_core_file_should_not_be_migrated",
- },
- },
- },
- {
- Name: oldRemoteServerListname,
- },
- {
- Name: oldRemoteServerListname + ".part",
- },
- {
- Name: oldRemoteServerListname + ".part.etag",
- },
- {
- Name: oldObfuscatedServerListDirectoryName,
- Children: oslDirChildrenPreMigration,
- },
- {
- Name: oldRotatingNoticesFilename,
- },
- {
- Name: oldRotatingNoticesFilename + ".1",
- },
- {
- Name: oldHomepageNoticeFilename,
- },
- {
- Name: oldUpgradeDownloadFilename,
- },
- {
- Name: oldUpgradeDownloadFilename + ".1234",
- },
- {
- Name: oldUpgradeDownloadFilename + ".1234.part",
- },
- {
- Name: oldUpgradeDownloadFilename + ".1234.part.etag",
- },
- {
- Name: "data_root_directory",
- Children: []FileTree{
- {
- Name: "non_tunnel_core_file_should_not_be_clobbered",
- },
- },
- },
- },
- }
- // Write test files
- traverseFileTree(func(tree FileTree, path string) {
- if tree.Children == nil || len(tree.Children) == 0 {
- if !common.FileExists(path) {
- f, err := os.Create(path)
- if err != nil {
- suite.T().Fatalf("Failed to create test file %s with error: %s", path, err.Error())
- }
- f.Close()
- }
- } else {
- if !common.FileExists(path) {
- err := os.Mkdir(path, os.ModePerm)
- if err != nil {
- suite.T().Fatalf("Failed to create test directory %s with error: %s", path, err.Error())
- }
- }
- }
- }, "", oldFileTree)
- // Create config with legacy config values
- config := &Config{
- DataRootDirectory: testDataRootDirectory,
- MigrateRotatingNoticesFilename: filepath.Join(testDirectory, oldRotatingNoticesFilename),
- MigrateHomepageNoticesFilename: filepath.Join(testDirectory, oldHomepageNoticeFilename),
- MigrateDataStoreDirectory: oldDataStoreDirectory,
- PropagationChannelId: "ABCDEFGH",
- SponsorId: "12345678",
- LocalSocksProxyPort: 0,
- LocalHttpProxyPort: 0,
- MigrateRemoteServerListDownloadFilename: filepath.Join(testDirectory, oldRemoteServerListname),
- MigrateObfuscatedServerListDownloadDirectory: oldObfuscatedServerListDirectory,
- MigrateUpgradeDownloadFilename: filepath.Join(testDirectory, oldUpgradeDownloadFilename),
- }
- // Commit config, this is where file migration happens
- err = config.Commit(true)
- if err != nil {
- suite.T().Fatal("Error committing config:", err)
- return
- }
- expectedNewTree := FileTree{
- Name: testDirectory,
- Children: []FileTree{
- {
- Name: "data_root_directory",
- Children: []FileTree{
- {
- Name: "non_tunnel_core_file_should_not_be_clobbered",
- },
- {
- Name: "ca.psiphon.PsiphonTunnel.tunnel-core",
- Children: []FileTree{
- {
- Name: "migration_complete",
- },
- {
- Name: "remote_server_list",
- },
- {
- Name: "remote_server_list.part",
- },
- {
- Name: "remote_server_list.part.etag",
- },
- {
- Name: "datastore",
- Children: []FileTree{
- {
- Name: "psiphon.boltdb",
- },
- {
- Name: "psiphon.boltdb.lock",
- },
- },
- },
- {
- Name: "osl",
- Children: []FileTree{
- {
- Name: "osl-registry",
- },
- {
- Name: "osl-registry.cached",
- },
- {
- Name: "osl-1",
- },
- {
- Name: "osl-1.part",
- },
- },
- },
- {
- Name: "upgrade",
- },
- {
- Name: "upgrade.1234",
- },
- {
- Name: "upgrade.1234.part",
- },
- {
- Name: "upgrade.1234.part.etag",
- },
- {
- Name: "notices",
- },
- {
- Name: "notices.1",
- },
- {
- Name: "homepage",
- },
- },
- },
- },
- },
- {
- Name: "datastore_old",
- Children: []FileTree{
- {
- Name: "non_tunnel_core_file_should_not_be_migrated",
- },
- },
- },
- },
- }
- // The OSL directory will have been deleted if it has no children after
- // migration.
- if oslDirChildrenPostMigration != nil {
- oslDir := FileTree{
- Name: oldObfuscatedServerListDirectoryName,
- Children: []FileTree{*oslDirChildrenPostMigration},
- }
- expectedNewTree.Children = append(expectedNewTree.Children, oslDir)
- }
- // Read the test directory into a file tree
- testDirectoryTree, err := buildDirectoryTree("", testDirectory)
- if err != nil {
- suite.T().Fatal("Failed to build directory tree:", err)
- }
- // Enumerate the file paths, relative to the test directory,
- // of each file in the test directory after migration.
- testDirectoryFilePaths := make(map[string]int)
- traverseFileTree(func(tree FileTree, path string) {
- if val, ok := testDirectoryFilePaths[path]; ok {
- testDirectoryFilePaths[path] = val + 1
- } else {
- testDirectoryFilePaths[path] = 1
- }
- }, "", *testDirectoryTree)
- // Enumerate the file paths, relative to the test directory,
- // of each file we expect to exist in the test directory tree
- // after migration.
- expectedTestDirectoryFilePaths := make(map[string]int)
- traverseFileTree(func(tree FileTree, path string) {
- if val, ok := expectedTestDirectoryFilePaths[path]; ok {
- expectedTestDirectoryFilePaths[path] = val + 1
- } else {
- expectedTestDirectoryFilePaths[path] = 1
- }
- }, "", expectedNewTree)
- // The set of expected file paths and set of actual file paths should be
- // identical.
- for k, _ := range expectedTestDirectoryFilePaths {
- _, ok := testDirectoryFilePaths[k]
- if ok {
- // Prevent redundant checks
- delete(testDirectoryFilePaths, k)
- } else {
- suite.T().Errorf("Expected %s to exist in directory", k)
- }
- }
- for k, _ := range testDirectoryFilePaths {
- if _, ok := expectedTestDirectoryFilePaths[k]; !ok {
- suite.T().Errorf("%s in directory but not expected", k)
- }
- }
- }
- // FileTree represents a file or directory in a file tree.
- // There is no need to distinguish between the two in our tests.
- type FileTree struct {
- Name string
- Children []FileTree
- }
- // traverseFileTree traverses a file tree and emits the filepath of each node.
- //
- // For example:
- //
- // a
- // ├── b
- // │ ├── 1
- // │ └── 2
- // └── c
- // └── 3
- //
- // Will result in: ["a", "a/b", "a/b/1", "a/b/2", "a/c", "a/c/3"].
- func traverseFileTree(f func(node FileTree, nodePath string), basePath string, tree FileTree) {
- filePath := filepath.Join(basePath, tree.Name)
- f(tree, filePath)
- if tree.Children == nil || len(tree.Children) == 0 {
- return
- }
- for _, childTree := range tree.Children {
- traverseFileTree(f, filePath, childTree)
- }
- }
- // buildDirectoryTree creates a file tree, with the given directory as its root,
- // representing the directory structure that exists relative to the given directory.
- func buildDirectoryTree(basePath, directoryName string) (*FileTree, error) {
- tree := &FileTree{
- Name: directoryName,
- Children: nil,
- }
- dirPath := filepath.Join(basePath, directoryName)
- files, err := ioutil.ReadDir(dirPath)
- if err != nil {
- return nil, errors.Tracef("Failed to read directory %s with error: %s", dirPath, err.Error())
- }
- if len(files) > 0 {
- for _, file := range files {
- if file.IsDir() {
- filePath := filepath.Join(basePath, directoryName)
- childTree, err := buildDirectoryTree(filePath, file.Name())
- if err != nil {
- return nil, err
- }
- tree.Children = append(tree.Children, *childTree)
- } else {
- tree.Children = append(tree.Children, FileTree{
- Name: file.Name(),
- Children: nil,
- })
- }
- }
- }
- return tree, nil
- }
|