config_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. /*
  2. * Copyright (c) 2014, Psiphon Inc.
  3. * All rights reserved.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. package psiphon
  20. import (
  21. "encoding/json"
  22. "fmt"
  23. "io/ioutil"
  24. "os"
  25. "path/filepath"
  26. "testing"
  27. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  28. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  29. "github.com/stretchr/testify/suite"
  30. )
  31. type ConfigTestSuite struct {
  32. suite.Suite
  33. confStubBlob []byte
  34. requiredFields []string
  35. nonRequiredFields []string
  36. testDirectory string
  37. }
  38. func (suite *ConfigTestSuite) SetupSuite() {
  39. suite.confStubBlob = []byte(`
  40. {
  41. "PropagationChannelId" : "<placeholder>",
  42. "SponsorId" : "<placeholder>",
  43. "LocalHttpProxyPort" : 8080,
  44. "LocalSocksProxyPort" : 1080
  45. }
  46. `)
  47. var obj map[string]interface{}
  48. json.Unmarshal(suite.confStubBlob, &obj)
  49. // Use a temporary directory for the data root directory so any artifacts
  50. // created by config.Commit() can be cleaned up.
  51. testDirectory, err := ioutil.TempDir("", "psiphon-config-test")
  52. if err != nil {
  53. suite.T().Fatalf("TempDir failed: %s\n", err)
  54. }
  55. suite.testDirectory = testDirectory
  56. obj["DataRootDirectory"] = testDirectory
  57. suite.confStubBlob, err = json.Marshal(obj)
  58. if err != nil {
  59. suite.T().Fatalf("Marshal failed: %s\n", err)
  60. }
  61. for k, v := range obj {
  62. if k == "DataRootDirectory" {
  63. // skip
  64. } else if v == "<placeholder>" {
  65. suite.requiredFields = append(suite.requiredFields, k)
  66. } else {
  67. suite.nonRequiredFields = append(suite.nonRequiredFields, k)
  68. }
  69. }
  70. }
  71. func (suite *ConfigTestSuite) TearDownSuite() {
  72. if common.FileExists(suite.testDirectory) {
  73. err := os.RemoveAll(suite.testDirectory)
  74. if err != nil {
  75. suite.T().Fatalf("Failed to remove test directory %s: %s", suite.testDirectory, err.Error())
  76. }
  77. } else {
  78. suite.T().Fatalf("Test directory not found: %s", suite.testDirectory)
  79. }
  80. }
  81. func TestConfigTestSuite(t *testing.T) {
  82. suite.Run(t, new(ConfigTestSuite))
  83. }
  84. // Tests good config
  85. func (suite *ConfigTestSuite) Test_LoadConfig_BasicGood() {
  86. config, err := LoadConfig(suite.confStubBlob)
  87. if err == nil {
  88. err = config.Commit(false)
  89. }
  90. suite.Nil(err, "a basic config should succeed")
  91. }
  92. // Tests non-JSON file contents
  93. func (suite *ConfigTestSuite) Test_LoadConfig_BadFileContents() {
  94. _, err := LoadConfig([]byte(`this is not JSON`))
  95. suite.NotNil(err, "bytes that are not JSON at all should give an error")
  96. }
  97. // Tests config file with JSON contents that don't match our structure
  98. func (suite *ConfigTestSuite) Test_LoadConfig_BadJson() {
  99. var testObj map[string]interface{}
  100. var testObjJSON []byte
  101. // JSON with none of our fields
  102. //
  103. // DataRootDirectory must to be set to avoid a migration in the current
  104. // working directory.
  105. config, err := LoadConfig([]byte(
  106. fmt.Sprintf(
  107. `{"f1": 11, "f2": "two", "DataRootDirectory" : %s}`,
  108. suite.testDirectory)))
  109. if err == nil {
  110. err = config.Commit(false)
  111. }
  112. suite.NotNil(err, "JSON with none of our fields should fail")
  113. // Test all required fields
  114. for _, field := range suite.requiredFields {
  115. // Missing a required field
  116. json.Unmarshal(suite.confStubBlob, &testObj)
  117. delete(testObj, field)
  118. testObjJSON, _ = json.Marshal(testObj)
  119. config, err = LoadConfig(testObjJSON)
  120. if err == nil {
  121. err = config.Commit(false)
  122. }
  123. suite.NotNil(err, "JSON with one of our required fields missing should fail: %s", field)
  124. // Bad type for required field
  125. json.Unmarshal(suite.confStubBlob, &testObj)
  126. testObj[field] = false // basically guessing a wrong type
  127. testObjJSON, _ = json.Marshal(testObj)
  128. config, err = LoadConfig(testObjJSON)
  129. if err == nil {
  130. err = config.Commit(false)
  131. }
  132. suite.NotNil(err, "JSON with one of our required fields with the wrong type should fail: %s", field)
  133. // One of our required fields is null
  134. json.Unmarshal(suite.confStubBlob, &testObj)
  135. testObj[field] = nil
  136. testObjJSON, _ = json.Marshal(testObj)
  137. config, err = LoadConfig(testObjJSON)
  138. if err == nil {
  139. err = config.Commit(false)
  140. }
  141. suite.NotNil(err, "JSON with one of our required fields set to null should fail: %s", field)
  142. // One of our required fields is an empty string
  143. json.Unmarshal(suite.confStubBlob, &testObj)
  144. testObj[field] = ""
  145. testObjJSON, _ = json.Marshal(testObj)
  146. config, err = LoadConfig(testObjJSON)
  147. if err == nil {
  148. err = config.Commit(false)
  149. }
  150. suite.NotNil(err, "JSON with one of our required fields set to an empty string should fail: %s", field)
  151. }
  152. // Test optional fields
  153. for _, field := range suite.nonRequiredFields {
  154. // Has incorrect type for optional field
  155. json.Unmarshal(suite.confStubBlob, &testObj)
  156. testObj[field] = false // basically guessing a wrong type
  157. testObjJSON, _ = json.Marshal(testObj)
  158. config, err = LoadConfig(testObjJSON)
  159. if err == nil {
  160. err = config.Commit(false)
  161. }
  162. suite.NotNil(err, "JSON with one of our optional fields with the wrong type should fail: %s", field)
  163. }
  164. }
  165. // Tests config file with JSON contents that don't match our structure
  166. func (suite *ConfigTestSuite) Test_LoadConfig_GoodJson() {
  167. var testObj map[string]interface{}
  168. var testObjJSON []byte
  169. // TODO: Test that the config actually gets the values we expect?
  170. // Has all of our required fields, but no optional fields
  171. json.Unmarshal(suite.confStubBlob, &testObj)
  172. for i := range suite.nonRequiredFields {
  173. delete(testObj, suite.nonRequiredFields[i])
  174. }
  175. testObjJSON, _ = json.Marshal(testObj)
  176. config, err := LoadConfig(testObjJSON)
  177. if err == nil {
  178. err = config.Commit(false)
  179. }
  180. suite.Nil(err, "JSON with good values for our required fields but no optional fields should succeed")
  181. // Has all of our required fields, and all optional fields
  182. config, err = LoadConfig(suite.confStubBlob)
  183. if err == nil {
  184. err = config.Commit(false)
  185. }
  186. suite.Nil(err, "JSON with all good values for required and optional fields should succeed")
  187. // Has null for optional fields
  188. json.Unmarshal(suite.confStubBlob, &testObj)
  189. for i := range suite.nonRequiredFields {
  190. testObj[suite.nonRequiredFields[i]] = nil
  191. }
  192. testObjJSON, _ = json.Marshal(testObj)
  193. config, err = LoadConfig(testObjJSON)
  194. if err == nil {
  195. err = config.Commit(false)
  196. }
  197. suite.Nil(err, "JSON with null for optional values should succeed")
  198. }
  199. func (suite *ConfigTestSuite) Test_LoadConfig_Migrate() {
  200. oslFiles := []FileTree{
  201. {
  202. Name: "osl-registry",
  203. },
  204. {
  205. Name: "osl-registry.cached",
  206. },
  207. {
  208. Name: "osl-1",
  209. },
  210. {
  211. Name: "osl-1.part",
  212. }}
  213. nonOSLFile := FileTree{
  214. Name: "should_not_be_deleted",
  215. Children: []FileTree{
  216. {
  217. Name: "should_not_be_deleted",
  218. },
  219. },
  220. }
  221. // Test where OSL directory is not deleted after migration because
  222. // it contains non-OSL files.
  223. LoadConfigMigrateTest(append(oslFiles, nonOSLFile), &nonOSLFile, suite)
  224. // Test where OSL directory is deleted after migration because it only
  225. // contained OSL files.
  226. LoadConfigMigrateTest(oslFiles, nil, suite)
  227. }
  228. // Test when migrating from old config fields results in filesystem changes.
  229. func LoadConfigMigrateTest(oslDirChildrenPreMigration []FileTree, oslDirChildrenPostMigration *FileTree, suite *ConfigTestSuite) {
  230. // This test needs its own temporary directory because a previous test may
  231. // have paved the file which signals that migration has already been
  232. // completed.
  233. testDirectory, err := ioutil.TempDir("", "psiphon-config-migration-test")
  234. if err != nil {
  235. suite.T().Fatalf("TempDir failed: %s\n", err)
  236. }
  237. defer func() {
  238. if common.FileExists(testDirectory) {
  239. err := os.RemoveAll(testDirectory)
  240. if err != nil {
  241. suite.T().Fatalf("Failed to remove test directory %s: %s", testDirectory, err.Error())
  242. }
  243. }
  244. }()
  245. // Pre migration files and directories
  246. oldDataStoreDirectory := filepath.Join(testDirectory, "datastore_old")
  247. oldRemoteServerListname := "rsl"
  248. oldObfuscatedServerListDirectoryName := "obfuscated_server_list"
  249. oldObfuscatedServerListDirectory := filepath.Join(testDirectory, oldObfuscatedServerListDirectoryName)
  250. oldUpgradeDownloadFilename := "upgrade"
  251. oldRotatingNoticesFilename := "rotating_notices"
  252. oldHomepageNoticeFilename := "homepage"
  253. // Post migration data root directory
  254. testDataRootDirectory := filepath.Join(testDirectory, "data_root_directory")
  255. oldFileTree := FileTree{
  256. Name: testDirectory,
  257. Children: []FileTree{
  258. {
  259. Name: "datastore_old",
  260. Children: []FileTree{
  261. {
  262. Name: "psiphon.boltdb",
  263. },
  264. {
  265. Name: "psiphon.boltdb.lock",
  266. },
  267. {
  268. Name: "non_tunnel_core_file_should_not_be_migrated",
  269. },
  270. },
  271. },
  272. {
  273. Name: oldRemoteServerListname,
  274. },
  275. {
  276. Name: oldRemoteServerListname + ".part",
  277. },
  278. {
  279. Name: oldRemoteServerListname + ".part.etag",
  280. },
  281. {
  282. Name: oldObfuscatedServerListDirectoryName,
  283. Children: oslDirChildrenPreMigration,
  284. },
  285. {
  286. Name: oldRotatingNoticesFilename,
  287. },
  288. {
  289. Name: oldRotatingNoticesFilename + ".1",
  290. },
  291. {
  292. Name: oldHomepageNoticeFilename,
  293. },
  294. {
  295. Name: oldUpgradeDownloadFilename,
  296. },
  297. {
  298. Name: oldUpgradeDownloadFilename + ".1234",
  299. },
  300. {
  301. Name: oldUpgradeDownloadFilename + ".1234.part",
  302. },
  303. {
  304. Name: oldUpgradeDownloadFilename + ".1234.part.etag",
  305. },
  306. {
  307. Name: "data_root_directory",
  308. Children: []FileTree{
  309. {
  310. Name: "non_tunnel_core_file_should_not_be_clobbered",
  311. },
  312. },
  313. },
  314. },
  315. }
  316. // Write test files
  317. traverseFileTree(func(tree FileTree, path string) {
  318. if tree.Children == nil || len(tree.Children) == 0 {
  319. if !common.FileExists(path) {
  320. f, err := os.Create(path)
  321. if err != nil {
  322. suite.T().Fatalf("Failed to create test file %s with error: %s", path, err.Error())
  323. }
  324. f.Close()
  325. }
  326. } else {
  327. if !common.FileExists(path) {
  328. err := os.Mkdir(path, os.ModePerm)
  329. if err != nil {
  330. suite.T().Fatalf("Failed to create test directory %s with error: %s", path, err.Error())
  331. }
  332. }
  333. }
  334. }, "", oldFileTree)
  335. // Create config with legacy config values
  336. config := &Config{
  337. DataRootDirectory: testDataRootDirectory,
  338. MigrateRotatingNoticesFilename: filepath.Join(testDirectory, oldRotatingNoticesFilename),
  339. MigrateHomepageNoticesFilename: filepath.Join(testDirectory, oldHomepageNoticeFilename),
  340. MigrateDataStoreDirectory: oldDataStoreDirectory,
  341. PropagationChannelId: "ABCDEFGH",
  342. SponsorId: "12345678",
  343. LocalSocksProxyPort: 0,
  344. LocalHttpProxyPort: 0,
  345. MigrateRemoteServerListDownloadFilename: filepath.Join(testDirectory, oldRemoteServerListname),
  346. MigrateObfuscatedServerListDownloadDirectory: oldObfuscatedServerListDirectory,
  347. MigrateUpgradeDownloadFilename: filepath.Join(testDirectory, oldUpgradeDownloadFilename),
  348. }
  349. // Commit config, this is where file migration happens
  350. err = config.Commit(true)
  351. if err != nil {
  352. suite.T().Fatal("Error committing config:", err)
  353. return
  354. }
  355. expectedNewTree := FileTree{
  356. Name: testDirectory,
  357. Children: []FileTree{
  358. {
  359. Name: "data_root_directory",
  360. Children: []FileTree{
  361. {
  362. Name: "non_tunnel_core_file_should_not_be_clobbered",
  363. },
  364. {
  365. Name: "ca.psiphon.PsiphonTunnel.tunnel-core",
  366. Children: []FileTree{
  367. {
  368. Name: "migration_complete",
  369. },
  370. {
  371. Name: "remote_server_list",
  372. },
  373. {
  374. Name: "remote_server_list.part",
  375. },
  376. {
  377. Name: "remote_server_list.part.etag",
  378. },
  379. {
  380. Name: "datastore",
  381. Children: []FileTree{
  382. {
  383. Name: "psiphon.boltdb",
  384. },
  385. {
  386. Name: "psiphon.boltdb.lock",
  387. },
  388. },
  389. },
  390. {
  391. Name: "osl",
  392. Children: []FileTree{
  393. {
  394. Name: "osl-registry",
  395. },
  396. {
  397. Name: "osl-registry.cached",
  398. },
  399. {
  400. Name: "osl-1",
  401. },
  402. {
  403. Name: "osl-1.part",
  404. },
  405. },
  406. },
  407. {
  408. Name: "upgrade",
  409. },
  410. {
  411. Name: "upgrade.1234",
  412. },
  413. {
  414. Name: "upgrade.1234.part",
  415. },
  416. {
  417. Name: "upgrade.1234.part.etag",
  418. },
  419. {
  420. Name: "notices",
  421. },
  422. {
  423. Name: "notices.1",
  424. },
  425. {
  426. Name: "homepage",
  427. },
  428. },
  429. },
  430. },
  431. },
  432. {
  433. Name: "datastore_old",
  434. Children: []FileTree{
  435. {
  436. Name: "non_tunnel_core_file_should_not_be_migrated",
  437. },
  438. },
  439. },
  440. },
  441. }
  442. // The OSL directory will have been deleted if it has no children after
  443. // migration.
  444. if oslDirChildrenPostMigration != nil {
  445. oslDir := FileTree{
  446. Name: oldObfuscatedServerListDirectoryName,
  447. Children: []FileTree{*oslDirChildrenPostMigration},
  448. }
  449. expectedNewTree.Children = append(expectedNewTree.Children, oslDir)
  450. }
  451. // Read the test directory into a file tree
  452. testDirectoryTree, err := buildDirectoryTree("", testDirectory)
  453. if err != nil {
  454. suite.T().Fatal("Failed to build directory tree:", err)
  455. }
  456. // Enumerate the file paths, relative to the test directory,
  457. // of each file in the test directory after migration.
  458. testDirectoryFilePaths := make(map[string]int)
  459. traverseFileTree(func(tree FileTree, path string) {
  460. if val, ok := testDirectoryFilePaths[path]; ok {
  461. testDirectoryFilePaths[path] = val + 1
  462. } else {
  463. testDirectoryFilePaths[path] = 1
  464. }
  465. }, "", *testDirectoryTree)
  466. // Enumerate the file paths, relative to the test directory,
  467. // of each file we expect to exist in the test directory tree
  468. // after migration.
  469. expectedTestDirectoryFilePaths := make(map[string]int)
  470. traverseFileTree(func(tree FileTree, path string) {
  471. if val, ok := expectedTestDirectoryFilePaths[path]; ok {
  472. expectedTestDirectoryFilePaths[path] = val + 1
  473. } else {
  474. expectedTestDirectoryFilePaths[path] = 1
  475. }
  476. }, "", expectedNewTree)
  477. // The set of expected file paths and set of actual file paths should be
  478. // identical.
  479. for k, _ := range expectedTestDirectoryFilePaths {
  480. _, ok := testDirectoryFilePaths[k]
  481. if ok {
  482. // Prevent redundant checks
  483. delete(testDirectoryFilePaths, k)
  484. } else {
  485. suite.T().Errorf("Expected %s to exist in directory", k)
  486. }
  487. }
  488. for k, _ := range testDirectoryFilePaths {
  489. if _, ok := expectedTestDirectoryFilePaths[k]; !ok {
  490. suite.T().Errorf("%s in directory but not expected", k)
  491. }
  492. }
  493. }
  494. // FileTree represents a file or directory in a file tree.
  495. // There is no need to distinguish between the two in our tests.
  496. type FileTree struct {
  497. Name string
  498. Children []FileTree
  499. }
  500. // traverseFileTree traverses a file tree and emits the filepath of each node.
  501. //
  502. // For example:
  503. //
  504. // a
  505. // ├── b
  506. // │ ├── 1
  507. // │ └── 2
  508. // └── c
  509. // └── 3
  510. //
  511. // Will result in: ["a", "a/b", "a/b/1", "a/b/2", "a/c", "a/c/3"].
  512. func traverseFileTree(f func(node FileTree, nodePath string), basePath string, tree FileTree) {
  513. filePath := filepath.Join(basePath, tree.Name)
  514. f(tree, filePath)
  515. if tree.Children == nil || len(tree.Children) == 0 {
  516. return
  517. }
  518. for _, childTree := range tree.Children {
  519. traverseFileTree(f, filePath, childTree)
  520. }
  521. }
  522. // buildDirectoryTree creates a file tree, with the given directory as its root,
  523. // representing the directory structure that exists relative to the given directory.
  524. func buildDirectoryTree(basePath, directoryName string) (*FileTree, error) {
  525. tree := &FileTree{
  526. Name: directoryName,
  527. Children: nil,
  528. }
  529. dirPath := filepath.Join(basePath, directoryName)
  530. files, err := ioutil.ReadDir(dirPath)
  531. if err != nil {
  532. return nil, errors.Tracef("Failed to read directory %s with error: %s", dirPath, err.Error())
  533. }
  534. if len(files) > 0 {
  535. for _, file := range files {
  536. if file.IsDir() {
  537. filePath := filepath.Join(basePath, directoryName)
  538. childTree, err := buildDirectoryTree(filePath, file.Name())
  539. if err != nil {
  540. return nil, err
  541. }
  542. tree.Children = append(tree.Children, *childTree)
  543. } else {
  544. tree.Children = append(tree.Children, FileTree{
  545. Name: file.Name(),
  546. Children: nil,
  547. })
  548. }
  549. }
  550. }
  551. return tree, nil
  552. }