config_test.go 17 KB

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