config_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  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: "tapdance",
  269. Children: []FileTree{
  270. {
  271. Name: "file1",
  272. },
  273. {
  274. Name: "file2",
  275. },
  276. },
  277. },
  278. {
  279. Name: "non_tunnel_core_file_should_not_be_migrated",
  280. },
  281. },
  282. },
  283. {
  284. Name: oldRemoteServerListname,
  285. },
  286. {
  287. Name: oldRemoteServerListname + ".part",
  288. },
  289. {
  290. Name: oldRemoteServerListname + ".part.etag",
  291. },
  292. {
  293. Name: oldObfuscatedServerListDirectoryName,
  294. Children: oslDirChildrenPreMigration,
  295. },
  296. {
  297. Name: oldRotatingNoticesFilename,
  298. },
  299. {
  300. Name: oldRotatingNoticesFilename + ".1",
  301. },
  302. {
  303. Name: oldHomepageNoticeFilename,
  304. },
  305. {
  306. Name: oldUpgradeDownloadFilename,
  307. },
  308. {
  309. Name: oldUpgradeDownloadFilename + ".1234",
  310. },
  311. {
  312. Name: oldUpgradeDownloadFilename + ".1234.part",
  313. },
  314. {
  315. Name: oldUpgradeDownloadFilename + ".1234.part.etag",
  316. },
  317. {
  318. Name: "data_root_directory",
  319. Children: []FileTree{
  320. {
  321. Name: "non_tunnel_core_file_should_not_be_clobbered",
  322. },
  323. },
  324. },
  325. },
  326. }
  327. // Write test files
  328. traverseFileTree(func(tree FileTree, path string) {
  329. if tree.Children == nil || len(tree.Children) == 0 {
  330. if !common.FileExists(path) {
  331. f, err := os.Create(path)
  332. if err != nil {
  333. suite.T().Fatalf("Failed to create test file %s with error: %s", path, err.Error())
  334. }
  335. f.Close()
  336. }
  337. } else {
  338. if !common.FileExists(path) {
  339. err := os.Mkdir(path, os.ModePerm)
  340. if err != nil {
  341. suite.T().Fatalf("Failed to create test directory %s with error: %s", path, err.Error())
  342. }
  343. }
  344. }
  345. }, "", oldFileTree)
  346. // Create config with legacy config values
  347. config := &Config{
  348. DataRootDirectory: testDataRootDirectory,
  349. MigrateRotatingNoticesFilename: filepath.Join(testDirectory, oldRotatingNoticesFilename),
  350. MigrateHomepageNoticesFilename: filepath.Join(testDirectory, oldHomepageNoticeFilename),
  351. MigrateDataStoreDirectory: oldDataStoreDirectory,
  352. PropagationChannelId: "ABCDEFGH",
  353. SponsorId: "12345678",
  354. LocalSocksProxyPort: 0,
  355. LocalHttpProxyPort: 0,
  356. MigrateRemoteServerListDownloadFilename: filepath.Join(testDirectory, oldRemoteServerListname),
  357. MigrateObfuscatedServerListDownloadDirectory: oldObfuscatedServerListDirectory,
  358. MigrateUpgradeDownloadFilename: filepath.Join(testDirectory, oldUpgradeDownloadFilename),
  359. }
  360. // Commit config, this is where file migration happens
  361. err = config.Commit(true)
  362. if err != nil {
  363. suite.T().Fatal("Error committing config:", err)
  364. return
  365. }
  366. expectedNewTree := FileTree{
  367. Name: testDirectory,
  368. Children: []FileTree{
  369. {
  370. Name: "data_root_directory",
  371. Children: []FileTree{
  372. {
  373. Name: "non_tunnel_core_file_should_not_be_clobbered",
  374. },
  375. {
  376. Name: "ca.psiphon.PsiphonTunnel.tunnel-core",
  377. Children: []FileTree{
  378. {
  379. Name: "migration_complete",
  380. },
  381. {
  382. Name: "remote_server_list",
  383. },
  384. {
  385. Name: "remote_server_list.part",
  386. },
  387. {
  388. Name: "remote_server_list.part.etag",
  389. },
  390. {
  391. Name: "datastore",
  392. Children: []FileTree{
  393. {
  394. Name: "psiphon.boltdb",
  395. },
  396. {
  397. Name: "psiphon.boltdb.lock",
  398. },
  399. },
  400. },
  401. {
  402. Name: "osl",
  403. Children: []FileTree{
  404. {
  405. Name: "osl-registry",
  406. },
  407. {
  408. Name: "osl-registry.cached",
  409. },
  410. {
  411. Name: "osl-1",
  412. },
  413. {
  414. Name: "osl-1.part",
  415. },
  416. },
  417. },
  418. {
  419. Name: "tapdance",
  420. Children: []FileTree{
  421. {
  422. Name: "tapdance",
  423. Children: []FileTree{
  424. {
  425. Name: "file1",
  426. },
  427. {
  428. Name: "file2",
  429. },
  430. },
  431. },
  432. },
  433. },
  434. {
  435. Name: "upgrade",
  436. },
  437. {
  438. Name: "upgrade.1234",
  439. },
  440. {
  441. Name: "upgrade.1234.part",
  442. },
  443. {
  444. Name: "upgrade.1234.part.etag",
  445. },
  446. {
  447. Name: "notices",
  448. },
  449. {
  450. Name: "notices.1",
  451. },
  452. {
  453. Name: "homepage",
  454. },
  455. },
  456. },
  457. },
  458. },
  459. {
  460. Name: "datastore_old",
  461. Children: []FileTree{
  462. {
  463. Name: "non_tunnel_core_file_should_not_be_migrated",
  464. },
  465. },
  466. },
  467. },
  468. }
  469. // The OSL directory will have been deleted if it has no children after
  470. // migration.
  471. if oslDirChildrenPostMigration != nil {
  472. oslDir := FileTree{
  473. Name: oldObfuscatedServerListDirectoryName,
  474. Children: []FileTree{*oslDirChildrenPostMigration},
  475. }
  476. expectedNewTree.Children = append(expectedNewTree.Children, oslDir)
  477. }
  478. // Read the test directory into a file tree
  479. testDirectoryTree, err := buildDirectoryTree("", testDirectory)
  480. if err != nil {
  481. suite.T().Fatal("Failed to build directory tree:", err)
  482. }
  483. // Enumerate the file paths, relative to the test directory,
  484. // of each file in the test directory after migration.
  485. testDirectoryFilePaths := make(map[string]int)
  486. traverseFileTree(func(tree FileTree, path string) {
  487. if val, ok := testDirectoryFilePaths[path]; ok {
  488. testDirectoryFilePaths[path] = val + 1
  489. } else {
  490. testDirectoryFilePaths[path] = 1
  491. }
  492. }, "", *testDirectoryTree)
  493. // Enumerate the file paths, relative to the test directory,
  494. // of each file we expect to exist in the test directory tree
  495. // after migration.
  496. expectedTestDirectoryFilePaths := make(map[string]int)
  497. traverseFileTree(func(tree FileTree, path string) {
  498. if val, ok := expectedTestDirectoryFilePaths[path]; ok {
  499. expectedTestDirectoryFilePaths[path] = val + 1
  500. } else {
  501. expectedTestDirectoryFilePaths[path] = 1
  502. }
  503. }, "", expectedNewTree)
  504. // The set of expected file paths and set of actual file paths should be
  505. // identical.
  506. for k, _ := range expectedTestDirectoryFilePaths {
  507. _, ok := testDirectoryFilePaths[k]
  508. if ok {
  509. // Prevent redundant checks
  510. delete(testDirectoryFilePaths, k)
  511. } else {
  512. suite.T().Errorf("Expected %s to exist in directory", k)
  513. }
  514. }
  515. for k, _ := range testDirectoryFilePaths {
  516. if _, ok := expectedTestDirectoryFilePaths[k]; !ok {
  517. suite.T().Errorf("%s in directory but not expected", k)
  518. }
  519. }
  520. }
  521. // FileTree represents a file or directory in a file tree.
  522. // There is no need to distinguish between the two in our tests.
  523. type FileTree struct {
  524. Name string
  525. Children []FileTree
  526. }
  527. // traverseFileTree traverses a file tree and emits the filepath of each node.
  528. //
  529. // For example:
  530. //
  531. // a
  532. // ├── b
  533. // │ ├── 1
  534. // │ └── 2
  535. // └── c
  536. // └── 3
  537. //
  538. // Will result in: ["a", "a/b", "a/b/1", "a/b/2", "a/c", "a/c/3"].
  539. func traverseFileTree(f func(node FileTree, nodePath string), basePath string, tree FileTree) {
  540. filePath := filepath.Join(basePath, tree.Name)
  541. f(tree, filePath)
  542. if tree.Children == nil || len(tree.Children) == 0 {
  543. return
  544. }
  545. for _, childTree := range tree.Children {
  546. traverseFileTree(f, filePath, childTree)
  547. }
  548. }
  549. // buildDirectoryTree creates a file tree, with the given directory as its root,
  550. // representing the directory structure that exists relative to the given directory.
  551. func buildDirectoryTree(basePath, directoryName string) (*FileTree, error) {
  552. tree := &FileTree{
  553. Name: directoryName,
  554. Children: nil,
  555. }
  556. dirPath := filepath.Join(basePath, directoryName)
  557. files, err := ioutil.ReadDir(dirPath)
  558. if err != nil {
  559. return nil, errors.Tracef("Failed to read directory %s with error: %s", dirPath, err.Error())
  560. }
  561. if len(files) > 0 {
  562. for _, file := range files {
  563. if file.IsDir() {
  564. filePath := filepath.Join(basePath, directoryName)
  565. childTree, err := buildDirectoryTree(filePath, file.Name())
  566. if err != nil {
  567. return nil, err
  568. }
  569. tree.Children = append(tree.Children, *childTree)
  570. } else {
  571. tree.Children = append(tree.Children, FileTree{
  572. Name: file.Name(),
  573. Children: nil,
  574. })
  575. }
  576. }
  577. }
  578. return tree, nil
  579. }