Browse Source

Merge remote-tracking branch 'origin/master' into obfuscated-server-lists

Rod Hynes 9 years ago
parent
commit
4b3bd6cd4f
59 changed files with 5796 additions and 895 deletions
  1. 192 0
      MobileLibrary/iOS/.gitignore
  2. 37 0
      MobileLibrary/iOS/BUILD.md
  3. 540 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/project.pbxproj
  4. 1 1
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  5. 0 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Info.plist
  6. 256 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h
  7. 500 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m
  8. 0 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/Reachability.h
  9. 0 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/Reachability.m
  10. 35 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4.h
  11. 248 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4Parser.h
  12. 287 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4Parser.m
  13. 131 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamParser.h
  14. 317 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamParser.m
  15. 82 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamParserState.h
  16. 355 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamParserState.m
  17. 40 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamTokeniser.h
  18. 395 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamTokeniser.m
  19. 210 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamWriter.h
  20. 358 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamWriter.m
  21. 69 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamWriterState.h
  22. 147 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamWriterState.m
  23. 103 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4Writer.h
  24. 102 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4Writer.m
  25. 22 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnelTests/Info.plist
  26. 39 0
      MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnelTests/PsiphonTunnelTests.m
  27. 0 3
      MobileLibrary/iOS/PsiphonTunnelController/.gitignore
  28. 0 349
      MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/project.pbxproj
  29. 0 114
      MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/project.xcworkspace/xcshareddata/PsiphonTunnelController.xcscmblueprint
  30. 0 22
      MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/project.xcworkspace/xcuserdata/eugene.xcuserdatad/WorkspaceSettings.xcsettings
  31. 0 80
      MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/xcuserdata/eugene.xcuserdatad/xcschemes/PsiphonTunnelController.xcscheme
  32. 0 22
      MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/xcuserdata/eugene.xcuserdatad/xcschemes/xcschememanagement.plist
  33. 0 2
      MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController/.gitignore
  34. 0 56
      MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController/PsiphonTunnelController.h
  35. 0 155
      MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController/PsiphonTunnelController.m
  36. 7 28
      MobileLibrary/iOS/README.md
  37. 579 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest.xcodeproj/project.pbxproj
  38. 7 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  39. 43 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/AppDelegate.swift
  40. 68 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/Contents.json
  41. 27 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Base.lproj/LaunchScreen.storyboard
  42. 26 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Base.lproj/Main.storyboard
  43. 45 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Info.plist
  44. 210 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/ViewController.swift
  45. 13 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/psiphon-config.json.stub
  46. 22 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequestTests/Info.plist
  47. 36 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequestTests/TunneledWebRequestTests.swift
  48. 22 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequestUITests/Info.plist
  49. 36 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequestUITests/TunneledWebRequestUITests.swift
  50. 33 0
      MobileLibrary/iOS/USAGE.md
  51. 61 8
      MobileLibrary/iOS/build-psiphon-framework.sh
  52. 2 2
      Server/README.md
  53. 1 0
      psiphon/common/protocol/protocol.go
  54. 29 25
      psiphon/controller_test.go
  55. 24 6
      psiphon/meekConn.go
  56. 4 2
      psiphon/server/api.go
  57. 1 1
      psiphon/server/meek.go
  58. 31 19
      psiphon/server/psinet/psinet.go
  59. 3 0
      psiphon/server/services.go

+ 192 - 0
MobileLibrary/iOS/.gitignore

@@ -1,3 +1,195 @@
+# Don't commit build artifacts
 go-ios-build
 build
+Psi.framework
+PsiphonTunnel.framework
+rootCAs.txt
+psiphon-config.json
 
+#
+# Xcode
+#
+# https://github.com/github/gitignore/blob/master/Global/Xcode.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+
+#
+# Objective-C
+#
+# https://github.com/github/gitignore/blob/master/Objective-C.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xcuserstate
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+
+#
+# Swift
+#
+# https://github.com/github/gitignore/blob/master/Swift.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xcuserstate
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+
+#
+# MacOS
+#
+# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk

+ 37 - 0
MobileLibrary/iOS/BUILD.md

@@ -0,0 +1,37 @@
+# Building the Psiphon iOS Library
+
+**Note:** If you want to use the Psiphon library, you must use the pre-built binary. See [USAGE.md](USAGE.md) for instructions. (This building doc is for Psiphon devs' reference.)
+
+## Manual Build
+
+### Prerequisites
+
+* xcode `xcode-select --install`
+
+* [git](https://git-scm.com/download/mac)
+
+* [homebrew](http://brew.sh/)
+
+* golang
+  - `brew install go`
+
+### Build Steps
+
+* Run `build-psiphon-framework.sh`.
+  - If this fails, especially in the `gomobile` step, try re-running it.
+
+* The result will be in `MobileLibrary/iOS/build`.
+
+
+## Automatic Build -- Jenkins
+
+Build artifacts can be found in Jenkins.
+
+
+## Deployment
+
+* Version numbers are arbitrary, but should be [semver](http://semver.org/)-compatible.
+
+* iOS and Android Library builds should be done at the same time, from the code, with the same version number. (There may be exceptions to this, where only one platform release makes sense.)
+
+* Use Github Releases to publish the Library binaries. Create a tag on the correct commit hash with the name of the version. Create a Release with a zip file containing the `.framework` directory and the `USAGE.md` file. Attach the Android Library binary

+ 540 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/project.pbxproj

@@ -0,0 +1,540 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		662659271DD270E900872F6C /* Reachability.h in Headers */ = {isa = PBXBuildFile; fileRef = 662659251DD270E900872F6C /* Reachability.h */; };
+		662659281DD270E900872F6C /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 662659261DD270E900872F6C /* Reachability.m */; };
+		66BDB02A1DA6BFCC0079384C /* PsiphonTunnel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66BDB0201DA6BFCC0079384C /* PsiphonTunnel.framework */; };
+		66BDB02F1DA6BFCC0079384C /* PsiphonTunnelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB02E1DA6BFCC0079384C /* PsiphonTunnelTests.m */; };
+		66BDB0311DA6BFCC0079384C /* PsiphonTunnel.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB0231DA6BFCC0079384C /* PsiphonTunnel.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		66BDB03B1DA6C4A70079384C /* Psi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66BDB03A1DA6C4A70079384C /* Psi.framework */; };
+		66BDB03E1DA6C79E0079384C /* rootCAs.txt in Resources */ = {isa = PBXBuildFile; fileRef = 66BDB03D1DA6C79E0079384C /* rootCAs.txt */; };
+		66BDB0441DA6C7DD0079384C /* PsiphonTunnel.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB0431DA6C7DD0079384C /* PsiphonTunnel.m */; };
+		66BDB0491DA6D7050079384C /* Psi.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 66BDB03A1DA6C4A70079384C /* Psi.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		66BDB05A1DC26CCC0079384C /* SBJson4.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB04B1DC26CCC0079384C /* SBJson4.h */; };
+		66BDB05B1DC26CCC0079384C /* SBJson4Parser.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB04C1DC26CCC0079384C /* SBJson4Parser.h */; };
+		66BDB05C1DC26CCC0079384C /* SBJson4Parser.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB04D1DC26CCC0079384C /* SBJson4Parser.m */; };
+		66BDB05D1DC26CCC0079384C /* SBJson4StreamParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB04E1DC26CCC0079384C /* SBJson4StreamParser.h */; };
+		66BDB05E1DC26CCC0079384C /* SBJson4StreamParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB04F1DC26CCC0079384C /* SBJson4StreamParser.m */; };
+		66BDB05F1DC26CCC0079384C /* SBJson4StreamParserState.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB0501DC26CCC0079384C /* SBJson4StreamParserState.h */; };
+		66BDB0601DC26CCC0079384C /* SBJson4StreamParserState.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB0511DC26CCC0079384C /* SBJson4StreamParserState.m */; };
+		66BDB0611DC26CCC0079384C /* SBJson4StreamTokeniser.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB0521DC26CCC0079384C /* SBJson4StreamTokeniser.h */; };
+		66BDB0621DC26CCC0079384C /* SBJson4StreamTokeniser.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB0531DC26CCC0079384C /* SBJson4StreamTokeniser.m */; };
+		66BDB0631DC26CCC0079384C /* SBJson4StreamWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB0541DC26CCC0079384C /* SBJson4StreamWriter.h */; };
+		66BDB0641DC26CCC0079384C /* SBJson4StreamWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB0551DC26CCC0079384C /* SBJson4StreamWriter.m */; };
+		66BDB0651DC26CCC0079384C /* SBJson4StreamWriterState.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB0561DC26CCC0079384C /* SBJson4StreamWriterState.h */; };
+		66BDB0661DC26CCC0079384C /* SBJson4StreamWriterState.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB0571DC26CCC0079384C /* SBJson4StreamWriterState.m */; };
+		66BDB0671DC26CCC0079384C /* SBJson4Writer.h in Headers */ = {isa = PBXBuildFile; fileRef = 66BDB0581DC26CCC0079384C /* SBJson4Writer.h */; };
+		66BDB0681DC26CCC0079384C /* SBJson4Writer.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB0591DC26CCC0079384C /* SBJson4Writer.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		66BDB02B1DA6BFCC0079384C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 66BDB0171DA6BFCC0079384C /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 66BDB01F1DA6BFCC0079384C;
+			remoteInfo = PsiphonTunnel;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		66BDB0481DA6D6FA0079384C /* CopyFiles */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+				66BDB0491DA6D7050079384C /* Psi.framework in CopyFiles */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		662659251DD270E900872F6C /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Reachability.h; sourceTree = "<group>"; };
+		662659261DD270E900872F6C /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Reachability.m; sourceTree = "<group>"; };
+		66BDB0201DA6BFCC0079384C /* PsiphonTunnel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PsiphonTunnel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		66BDB0231DA6BFCC0079384C /* PsiphonTunnel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PsiphonTunnel.h; sourceTree = "<group>"; };
+		66BDB0241DA6BFCC0079384C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		66BDB0291DA6BFCC0079384C /* PsiphonTunnelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PsiphonTunnelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		66BDB02E1DA6BFCC0079384C /* PsiphonTunnelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PsiphonTunnelTests.m; sourceTree = "<group>"; };
+		66BDB0301DA6BFCC0079384C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		66BDB03A1DA6C4A70079384C /* Psi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Psi.framework; sourceTree = "<group>"; };
+		66BDB03D1DA6C79E0079384C /* rootCAs.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = rootCAs.txt; path = PsiphonTunnel/rootCAs.txt; sourceTree = "<group>"; };
+		66BDB0431DA6C7DD0079384C /* PsiphonTunnel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PsiphonTunnel.m; sourceTree = "<group>"; };
+		66BDB04B1DC26CCC0079384C /* SBJson4.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4.h; sourceTree = "<group>"; };
+		66BDB04C1DC26CCC0079384C /* SBJson4Parser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4Parser.h; sourceTree = "<group>"; };
+		66BDB04D1DC26CCC0079384C /* SBJson4Parser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJson4Parser.m; sourceTree = "<group>"; };
+		66BDB04E1DC26CCC0079384C /* SBJson4StreamParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4StreamParser.h; sourceTree = "<group>"; };
+		66BDB04F1DC26CCC0079384C /* SBJson4StreamParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJson4StreamParser.m; sourceTree = "<group>"; };
+		66BDB0501DC26CCC0079384C /* SBJson4StreamParserState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4StreamParserState.h; sourceTree = "<group>"; };
+		66BDB0511DC26CCC0079384C /* SBJson4StreamParserState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJson4StreamParserState.m; sourceTree = "<group>"; };
+		66BDB0521DC26CCC0079384C /* SBJson4StreamTokeniser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4StreamTokeniser.h; sourceTree = "<group>"; };
+		66BDB0531DC26CCC0079384C /* SBJson4StreamTokeniser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJson4StreamTokeniser.m; sourceTree = "<group>"; };
+		66BDB0541DC26CCC0079384C /* SBJson4StreamWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4StreamWriter.h; sourceTree = "<group>"; };
+		66BDB0551DC26CCC0079384C /* SBJson4StreamWriter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJson4StreamWriter.m; sourceTree = "<group>"; };
+		66BDB0561DC26CCC0079384C /* SBJson4StreamWriterState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4StreamWriterState.h; sourceTree = "<group>"; };
+		66BDB0571DC26CCC0079384C /* SBJson4StreamWriterState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJson4StreamWriterState.m; sourceTree = "<group>"; };
+		66BDB0581DC26CCC0079384C /* SBJson4Writer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4Writer.h; sourceTree = "<group>"; };
+		66BDB0591DC26CCC0079384C /* SBJson4Writer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJson4Writer.m; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		66BDB01C1DA6BFCC0079384C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				66BDB03B1DA6C4A70079384C /* Psi.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		66BDB0261DA6BFCC0079384C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				66BDB02A1DA6BFCC0079384C /* PsiphonTunnel.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		662659241DD270E900872F6C /* Reachability */ = {
+			isa = PBXGroup;
+			children = (
+				662659251DD270E900872F6C /* Reachability.h */,
+				662659261DD270E900872F6C /* Reachability.m */,
+			);
+			path = Reachability;
+			sourceTree = "<group>";
+		};
+		66BDB0161DA6BFCC0079384C = {
+			isa = PBXGroup;
+			children = (
+				66BDB03C1DA6C7940079384C /* Resources */,
+				66BDB0221DA6BFCC0079384C /* PsiphonTunnel */,
+				66BDB02D1DA6BFCC0079384C /* PsiphonTunnelTests */,
+				66BDB0211DA6BFCC0079384C /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		66BDB0211DA6BFCC0079384C /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				66BDB0201DA6BFCC0079384C /* PsiphonTunnel.framework */,
+				66BDB0291DA6BFCC0079384C /* PsiphonTunnelTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		66BDB0221DA6BFCC0079384C /* PsiphonTunnel */ = {
+			isa = PBXGroup;
+			children = (
+				662659241DD270E900872F6C /* Reachability */,
+				66BDB04A1DC26CCC0079384C /* json-framework */,
+				66BDB0231DA6BFCC0079384C /* PsiphonTunnel.h */,
+				66BDB0431DA6C7DD0079384C /* PsiphonTunnel.m */,
+				66BDB0241DA6BFCC0079384C /* Info.plist */,
+				66BDB03A1DA6C4A70079384C /* Psi.framework */,
+			);
+			path = PsiphonTunnel;
+			sourceTree = "<group>";
+		};
+		66BDB02D1DA6BFCC0079384C /* PsiphonTunnelTests */ = {
+			isa = PBXGroup;
+			children = (
+				66BDB02E1DA6BFCC0079384C /* PsiphonTunnelTests.m */,
+				66BDB0301DA6BFCC0079384C /* Info.plist */,
+			);
+			path = PsiphonTunnelTests;
+			sourceTree = "<group>";
+		};
+		66BDB03C1DA6C7940079384C /* Resources */ = {
+			isa = PBXGroup;
+			children = (
+				66BDB03D1DA6C79E0079384C /* rootCAs.txt */,
+			);
+			name = Resources;
+			sourceTree = "<group>";
+		};
+		66BDB04A1DC26CCC0079384C /* json-framework */ = {
+			isa = PBXGroup;
+			children = (
+				66BDB04B1DC26CCC0079384C /* SBJson4.h */,
+				66BDB04C1DC26CCC0079384C /* SBJson4Parser.h */,
+				66BDB04D1DC26CCC0079384C /* SBJson4Parser.m */,
+				66BDB04E1DC26CCC0079384C /* SBJson4StreamParser.h */,
+				66BDB04F1DC26CCC0079384C /* SBJson4StreamParser.m */,
+				66BDB0501DC26CCC0079384C /* SBJson4StreamParserState.h */,
+				66BDB0511DC26CCC0079384C /* SBJson4StreamParserState.m */,
+				66BDB0521DC26CCC0079384C /* SBJson4StreamTokeniser.h */,
+				66BDB0531DC26CCC0079384C /* SBJson4StreamTokeniser.m */,
+				66BDB0541DC26CCC0079384C /* SBJson4StreamWriter.h */,
+				66BDB0551DC26CCC0079384C /* SBJson4StreamWriter.m */,
+				66BDB0561DC26CCC0079384C /* SBJson4StreamWriterState.h */,
+				66BDB0571DC26CCC0079384C /* SBJson4StreamWriterState.m */,
+				66BDB0581DC26CCC0079384C /* SBJson4Writer.h */,
+				66BDB0591DC26CCC0079384C /* SBJson4Writer.m */,
+			);
+			path = "json-framework";
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+		66BDB01D1DA6BFCC0079384C /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				66BDB05D1DC26CCC0079384C /* SBJson4StreamParser.h in Headers */,
+				66BDB05F1DC26CCC0079384C /* SBJson4StreamParserState.h in Headers */,
+				66BDB0311DA6BFCC0079384C /* PsiphonTunnel.h in Headers */,
+				66BDB0651DC26CCC0079384C /* SBJson4StreamWriterState.h in Headers */,
+				66BDB05B1DC26CCC0079384C /* SBJson4Parser.h in Headers */,
+				66BDB05A1DC26CCC0079384C /* SBJson4.h in Headers */,
+				66BDB0611DC26CCC0079384C /* SBJson4StreamTokeniser.h in Headers */,
+				66BDB0631DC26CCC0079384C /* SBJson4StreamWriter.h in Headers */,
+				66BDB0671DC26CCC0079384C /* SBJson4Writer.h in Headers */,
+				662659271DD270E900872F6C /* Reachability.h in Headers */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+		66BDB01F1DA6BFCC0079384C /* PsiphonTunnel */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 66BDB0341DA6BFCC0079384C /* Build configuration list for PBXNativeTarget "PsiphonTunnel" */;
+			buildPhases = (
+				66BDB01B1DA6BFCC0079384C /* Sources */,
+				66BDB01C1DA6BFCC0079384C /* Frameworks */,
+				66BDB01D1DA6BFCC0079384C /* Headers */,
+				66BDB01E1DA6BFCC0079384C /* Resources */,
+				66BDB0481DA6D6FA0079384C /* CopyFiles */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = PsiphonTunnel;
+			productName = PsiphonTunnel;
+			productReference = 66BDB0201DA6BFCC0079384C /* PsiphonTunnel.framework */;
+			productType = "com.apple.product-type.framework";
+		};
+		66BDB0281DA6BFCC0079384C /* PsiphonTunnelTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 66BDB0371DA6BFCC0079384C /* Build configuration list for PBXNativeTarget "PsiphonTunnelTests" */;
+			buildPhases = (
+				66BDB0251DA6BFCC0079384C /* Sources */,
+				66BDB0261DA6BFCC0079384C /* Frameworks */,
+				66BDB0271DA6BFCC0079384C /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				66BDB02C1DA6BFCC0079384C /* PBXTargetDependency */,
+			);
+			name = PsiphonTunnelTests;
+			productName = PsiphonTunnelTests;
+			productReference = 66BDB0291DA6BFCC0079384C /* PsiphonTunnelTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		66BDB0171DA6BFCC0079384C /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 0800;
+				ORGANIZATIONNAME = "Psiphon Inc.";
+				TargetAttributes = {
+					66BDB01F1DA6BFCC0079384C = {
+						CreatedOnToolsVersion = 8.0;
+						DevelopmentTeam = Q6HLNEX92A;
+						ProvisioningStyle = Automatic;
+					};
+					66BDB0281DA6BFCC0079384C = {
+						CreatedOnToolsVersion = 8.0;
+						DevelopmentTeam = Q6HLNEX92A;
+						ProvisioningStyle = Automatic;
+					};
+				};
+			};
+			buildConfigurationList = 66BDB01A1DA6BFCC0079384C /* Build configuration list for PBXProject "PsiphonTunnel" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+			);
+			mainGroup = 66BDB0161DA6BFCC0079384C;
+			productRefGroup = 66BDB0211DA6BFCC0079384C /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				66BDB01F1DA6BFCC0079384C /* PsiphonTunnel */,
+				66BDB0281DA6BFCC0079384C /* PsiphonTunnelTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		66BDB01E1DA6BFCC0079384C /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				66BDB03E1DA6C79E0079384C /* rootCAs.txt in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		66BDB0271DA6BFCC0079384C /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		66BDB01B1DA6BFCC0079384C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				66BDB05E1DC26CCC0079384C /* SBJson4StreamParser.m in Sources */,
+				66BDB0641DC26CCC0079384C /* SBJson4StreamWriter.m in Sources */,
+				66BDB0661DC26CCC0079384C /* SBJson4StreamWriterState.m in Sources */,
+				66BDB05C1DC26CCC0079384C /* SBJson4Parser.m in Sources */,
+				66BDB0681DC26CCC0079384C /* SBJson4Writer.m in Sources */,
+				66BDB0621DC26CCC0079384C /* SBJson4StreamTokeniser.m in Sources */,
+				66BDB0441DA6C7DD0079384C /* PsiphonTunnel.m in Sources */,
+				662659281DD270E900872F6C /* Reachability.m in Sources */,
+				66BDB0601DC26CCC0079384C /* SBJson4StreamParserState.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		66BDB0251DA6BFCC0079384C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				66BDB02F1DA6BFCC0079384C /* PsiphonTunnelTests.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		66BDB02C1DA6BFCC0079384C /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 66BDB01F1DA6BFCC0079384C /* PsiphonTunnel */;
+			targetProxy = 66BDB02B1DA6BFCC0079384C /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+		66BDB0321DA6BFCC0079384C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVES = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Debug;
+		};
+		66BDB0331DA6BFCC0079384C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVES = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Release;
+		};
+		66BDB0351DA6BFCC0079384C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_IDENTITY = "";
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/PsiphonTunnel",
+				);
+				INFOPLIST_FILE = PsiphonTunnel/Info.plist;
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.PsiphonTunnel;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+			};
+			name = Debug;
+		};
+		66BDB0361DA6BFCC0079384C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_IDENTITY = "";
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/PsiphonTunnel",
+				);
+				INFOPLIST_FILE = PsiphonTunnel/Info.plist;
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.PsiphonTunnel;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+			};
+			name = Release;
+		};
+		66BDB0381DA6BFCC0079384C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = PsiphonTunnelTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.PsiphonTunnelTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		66BDB0391DA6BFCC0079384C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = PsiphonTunnelTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.PsiphonTunnelTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		66BDB01A1DA6BFCC0079384C /* Build configuration list for PBXProject "PsiphonTunnel" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				66BDB0321DA6BFCC0079384C /* Debug */,
+				66BDB0331DA6BFCC0079384C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		66BDB0341DA6BFCC0079384C /* Build configuration list for PBXNativeTarget "PsiphonTunnel" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				66BDB0351DA6BFCC0079384C /* Debug */,
+				66BDB0361DA6BFCC0079384C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		66BDB0371DA6BFCC0079384C /* Build configuration list for PBXNativeTarget "PsiphonTunnelTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				66BDB0381DA6BFCC0079384C /* Debug */,
+				66BDB0391DA6BFCC0079384C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 66BDB0171DA6BFCC0079384C /* Project object */;
+}

+ 1 - 1
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/project.xcworkspace/contents.xcworkspacedata → MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -2,6 +2,6 @@
 <Workspace
    version = "1.0">
    <FileRef
-      location = "self:PsiphonTunnelController.xcodeproj">
+      location = "self:PsiphonTunnel.xcodeproj">
    </FileRef>
 </Workspace>

+ 0 - 0
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController/Info.plist → MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Info.plist


+ 256 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h

@@ -0,0 +1,256 @@
+//
+//  PsiphonTunnel.h
+//  PsiphonTunnel
+//
+
+/*
+ * Copyright (c) 2016, 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/>.
+ *
+ */
+
+#import <UIKit/UIKit.h>
+
+//! Project version number for PsiphonTunnel.
+FOUNDATION_EXPORT double PsiphonTunnelVersionNumber;
+
+//! Project version string for PsiphonTunnel.
+FOUNDATION_EXPORT const unsigned char PsiphonTunnelVersionString[];
+
+/*!
+ @protocol TunneledAppDelegate
+ Used to communicate with the application that is using the PsiphonTunnel framework,
+ and retrieve config info from it.
+ */
+@protocol TunneledAppDelegate
+
+/*!
+ Called when tunnel is started to get the library consumer's desired configuration.
+
+ @code
+ Required fields:
+ - `PropagationChannelId`
+ - `SponsorId`
+ - Remote server list functionality is not strictly required, but absence greatly undermines circumvention ability.
+   - `RemoteServerListUrl`
+   - `RemoteServerListSignaturePublicKey`
+
+ Optional fields (if you don't need them, don't set them):
+ - `DataStoreDirectory`: If not set, the library will use a sane location. Override if the client wants to restrict where operational data is kept.
+ - `RemoteServerListDownloadFilename`: See comment for `DataStoreDirectory`.
+ - `ClientPlatform`: Should not be set by most library consumers.
+ - `UpstreamProxyUrl`
+ - `EmitDiagnosticNotices`
+ - `LocalHttpProxyPort` // TODO: Should this be set-able for iOS?
+ - `LocalSocksProxyPort` // TODO: Should this be set-able for iOS?
+ - `EgressRegion`
+ - `EstablishTunnelTimeoutSeconds`
+ - `TunnelWholeDevice`: For stats purposes, but must be accurate. Defaults to 0 (false).
+ - Should only be set if the Psiphon library is handling upgrade downloading (which it usually is _not_):
+   - `UpgradeDownloadUrl`
+   - `UpgradeDownloadClientVersionHeader`
+   - `UpgradeDownloadFilename`
+ - Only set if disabling timeouts (for very slow network connections):
+   - `TunnelConnectTimeoutSeconds`
+   - `TunnelPortForwardDialTimeoutSeconds`
+   - `TunnelSshKeepAliveProbeTimeoutSeconds`
+   - `TunnelSshKeepAlivePeriodicTimeoutSeconds`
+   - `FetchRemoteServerListTimeoutSeconds`
+   - `PsiphonApiServerTimeoutSeconds`
+   - `FetchRoutesTimeoutSeconds`
+   - `HttpProxyOriginServerTimeoutSeconds`
+ @endcode
+
+ @note All other config fields must not be set.
+
+ See the tunnel-core config code for details about the fields.
+ https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/master/psiphon/config.go
+ 
+ @return NSString  JSON string with config that should used to run the Psiphon tunnel, or nil on error.
+ 
+ Swift: @code func getPsiphonConfig() -> String? @endcode
+ */
+- (NSString * _Nullable)getPsiphonConfig;
+
+/*!
+ Gets runtime errors info that may be useful for debugging.
+ @param message  The diagnostic message string.
+ Swift: @code func onDiagnosticMessage(_ message: String) @endcode
+ */
+- (void) onDiagnosticMessage: (NSString * _Nonnull)message;
+
+/*! 
+ Called when the tunnel is in the process of connecting.
+ Swift: @code func onConnecting() @endcode
+ */
+- (void) onConnecting;
+/*!
+ Called when the tunnel has successfully connected.
+ Swift: @code func onConnected() @endcode
+ */
+- (void) onConnected;
+
+/*!
+ Called to indicate that tunnel-core is exiting imminently (usually do to
+ a `stop()` call, but could be due to an unexpected error).
+ Swift: @code func onExiting() @endcode
+ */
+- (void) onExiting;
+
+/*!
+ Called when tunnel-core determines which server egress regions are available
+ for use. This can be used for updating the UI which provides the options to
+ the user.
+ @param regions  A string array containing the available egress region country codes.
+ Swift: @code func onAvailableEgressRegions(_ regions: [Any]) @endcode
+ */
+- (void) onAvailableEgressRegions: (NSArray * _Nonnull)regions;
+
+/*!
+ If the tunnel is started with a fixed SOCKS proxy port, and that port is
+ already in use, this will be called.
+ @param port  The port number.
+ Swift: @code func onSocksProxyPort(inUse port: Int) @endcode
+ */
+- (void) onSocksProxyPortInUse: (NSInteger)port;
+/*!
+ If the tunnel is started with a fixed HTTP proxy port, and that port is
+ already in use, this will be called.
+ @param port  The port number.
+ Swift: @code func onHttpProxyPort(inUse port: Int) @endcode
+ */
+- (void) onHttpProxyPortInUse: (NSInteger)port;
+
+/*!
+ Called when tunnel-core determines what port will be used for the local SOCKS proxy.
+ @param port  The port number.
+ Swift: @code func onListeningSocksProxyPort(_ port: Int) @endcode
+ */
+- (void) onListeningSocksProxyPort: (NSInteger)port;
+/*!
+ Called when tunnel-core determines what port will be used for the local HTTP proxy.
+ @param port  The port number.
+ Swift: @code func onListeningHttpProxyPort(_ port: Int) @endcode
+ */
+- (void) onListeningHttpProxyPort: (NSInteger)port;
+
+/*!
+ Called when a error occurs when trying to utilize a configured upstream proxy.
+ @param message  A message giving additional info about the error.
+ Swift: @code func onUpstreamProxyError(_ message: String) @endcode
+ */
+- (void) onUpstreamProxyError: (NSString * _Nonnull)message;
+
+/*!
+ Called after the handshake with the Psiphon server, with the client region as determined by the server.
+ @param region  The country code of the client, as determined by the server.
+ Swift: @code func onClientRegion(_ region: String) @endcode
+ */
+- (void) onClientRegion: (NSString * _Nonnull)region;
+
+/*!
+ Called to report that split tunnel is on for the given region.
+ @param region  The region split tunnel is on for.
+ Swift: @code func onSplitTunnelRegion(_ region: String) @endcode
+ */
+- (void) onSplitTunnelRegion: (NSString * _Nonnull)region;
+
+/*!
+ Called to indicate that an address has been classified as being within the
+ split tunnel region and therefore is being access directly rather than tunneled.
+ Note: `address` should remain private; this notice should be used for alerting
+ users, not for diagnotics logs.
+ @param address  The IP or hostname that is not being tunneled.
+ Swift: @code func onUntunneledAddress(_ address: String) @endcode
+ */
+- (void) onUntunneledAddress: (NSString * _Nonnull) address;
+
+/*!
+ Called to report how many bytes have been transferred since the last time
+ this function was called.
+ @param sent  The number of bytes sent.
+ @param received  The number of bytes received.
+ Swift: @code func onBytesTransferred(_ sent: Int64, _ received: Int64) @endcode
+ */
+- (void) onBytesTransferred: (int64_t)sent : (int64_t)received;
+
+// TODO: Only applicable to Psiphon proper?
+/*!
+ Called when tunnel-core discovers a home page associated with this client.
+ If there are no home pages, it will not be called. May be called more than
+ once, for multiple home pages.
+ @param url  The URL of the home page.
+ Swift: @code func onHomepage(_ url: String) @endcode
+ */
+- (void) onHomepage: (NSString * _Nonnull)url;
+
+// TODO: Only applicable to Psiphon proper?
+/*!
+ Called if the current version of the client is the latest (i.e., there is no upgrade available).
+ Swift: @code func onClientIsLatestVersion() @endcode
+ */
+- (void) onClientIsLatestVersion;
+
+// TODO: Only applicable to Psiphon proper?
+/*!
+ Called when a client upgrade has been downloaded.
+ @param filename  The name of the file containing the upgrade.
+ Swift: @code func onClientUpgradeDownloaded(_ filename: String) @endcode
+ */
+- (void) onClientUpgradeDownloaded: (NSString * _Nonnull)filename;
+
+// TODO: Applies to iOS?
+//func onClientVerificationRequired(nonce: String, ttlSeconds: Int, resetCache: Bool)
+
+@end
+
+/*!
+ The interface for managing the Psiphon tunnel -- set up, tear down, receive info about.
+ */
+@interface PsiphonTunnel : NSObject
+
+/*!
+ Returns an instance of PsiphonTunnel. This is either a new instance or the pre-existing singleton. If an instance already exists, it will be stopped when this function is called.
+ @param tunneledAppDelegate  The delegate implementation to use for callbacks.
+ @return  The PsiphonTunnel instance.
+ */
++(PsiphonTunnel * _Nonnull) newPsiphonTunnel:(id<TunneledAppDelegate> _Nonnull)tunneledAppDelegate;
+
+/*!
+ Start connecting the PsiphonTunnel. Returns before connection is complete -- delegate callbacks (such as `onConnected`) are used to indicate progress and state.
+ @param embeddedServerEntries  Pre-existing server entries to use when attempting to connect to a server. May be null if there are no embedded server entries.
+ @return TRUE if the connection start was successful, FALSE otherwise.
+ */
+-(BOOL) start:(NSString * _Nullable)embeddedServerEntries;
+
+/*!
+ Stop the tunnel (regardless of its current connection state). Returns before full stop is complete -- `TunneledAppDelegate::onExiting` is called when complete.
+ */
+-(void) stop;
+
+/*!
+ Upload a feedback package to Psiphon Inc. The app collects feedback and diagnostics information in a particular format, then calls this function to upload it for later investigation.
+ @note The key, server, path, and headers must be provided by Psiphon Inc.
+ @param connectionConfigJson  This function may create a tunnel to perform the upload, and this configuration is used to create that tunnel.
+ @param diagnosticsJson  The feedback and diagnostics data to upload.
+ @param b64EncodedPublicKey  The key that will be used to encrypt the payload before uploading.
+ @param uploadServer  The server to which the data will be uploaded.
+ @param uploadPath  The path on the server to which the data will be loaded.
+ @param uploadServerHeaders  The request headers that will be used when uploading.
+ */
++ (void)sendFeedback:(NSString * _Nonnull)connectionConfigJson diagnostics:(NSString * _Nonnull)diagnosticsJson publicKey:(NSString * _Nonnull)b64EncodedPublicKey uploadServer:(NSString * _Nonnull)uploadServer uploadPath:(NSString * _Nonnull)uploadPath uploadServerHeaders:(NSString * _Nonnull)uploadServerHeaders;
+
+@end

File diff suppressed because it is too large
+ 500 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m


+ 0 - 0
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController/Reachability.h → MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/Reachability.h


+ 0 - 0
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController/Reachability.m → MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Reachability/Reachability.m


+ 35 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4.h

@@ -0,0 +1,35 @@
+/*
+ Copyright (C) 2009-2011 Stig Brautaset. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+ * Neither the name of the author nor the names of its contributors may be used
+   to endorse or promote products derived from this software without specific
+   prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "SBJson4Writer.h"
+#import "SBJson4StreamParser.h"
+#import "SBJson4Parser.h"
+#import "SBJson4StreamWriter.h"
+#import "SBJson4StreamTokeniser.h"
+

+ 248 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4Parser.h

@@ -0,0 +1,248 @@
+/*
+ Copyright (c) 2010-2013, Stig Brautaset.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+   Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+   Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+   Neither the name of the the author nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <Foundation/Foundation.h>
+#import "SBJson4StreamParser.h"
+
+/**
+ Block called when the parser has parsed an item. This could be once
+ for each root document parsed, or once for each unwrapped root array element.
+
+ @param item contains the parsed item.
+ @param stop set to YES if you want the parser to stop
+ */
+typedef void (^SBJson4ValueBlock)(id item, BOOL* stop);
+
+/**
+ Block called if an error occurs.
+ @param error the error.
+ */
+typedef void (^SBJson4ErrorBlock)(NSError* error);
+
+/**
+ Block used to process parsed tokens as they are encountered. You can use this
+ to transform strings containing dates into NSDate, for example.
+ @param item the parsed token
+ @param path the JSON Path of the token
+ */
+typedef id (^SBJson4ProcessBlock)(id item, NSString* path);
+
+
+/**
+ Parse one or more chunks of JSON data.
+
+ Using this class directly you can reduce the apparent latency for each
+ download/parse cycle of documents over a slow connection. You can start
+ parsing *and return chunks of the parsed document* before the entire
+ document is downloaded.
+
+ Using this class is also useful to parse huge documents on disk
+ bit by bit so you don't have to keep them all in memory.
+
+ JSON is mapped to Objective-C types in the following way:
+
+ - null    -> NSNull
+ - string  -> NSString
+ - array   -> NSMutableArray
+ - object  -> NSMutableDictionary
+ - true    -> NSNumber's -numberWithBool:YES
+ - false   -> NSNumber's -numberWithBool:NO
+ - number -> NSNumber
+
+ Since Objective-C doesn't have a dedicated class for boolean values,
+ these turns into NSNumber instances. However, since these are
+ initialised with the -initWithBool: method they round-trip back to JSON
+ properly. In other words, they won't silently suddenly become 0 or 1;
+ they'll be represented as 'true' and 'false' again.
+
+ Integers are parsed into either a `long long` or `unsigned long long`
+ type if they fit, else a `double` is used. All real & exponential numbers
+ are represented using a `double`. Previous versions of this library used
+ an NSDecimalNumber in some cases, but this is no longer the case.
+
+ The default behaviour is that your passed-in block is only called once the
+ entire input is parsed. If you set supportManyDocuments to YES and your input
+ contains multiple (whitespace limited) JSON documents your block will be called
+ for each document:
+
+    SBJson4ValueBlock block = ^(id v, BOOL *stop) {
+        BOOL isArray = [v isKindOfClass:[NSArray class]];
+        NSLog(@"Found: %@", isArray ? @"Array" : @"Object");
+    };
+
+    SBJson4ErrorBlock eh = ^(NSError* err) {
+        NSLog(@"OOPS: %@", err);
+    };
+
+    id parser = [SBJson4Parser multiRootParserWithBlock:block
+                                           errorHandler:eh];
+
+    // Note that this input contains multiple top-level JSON documents
+    id data = [@"[]{}" dataWithEncoding:NSUTF8StringEncoding];
+    [parser parse:data];
+    [parser parse:data];
+
+ The above example will print:
+
+ - Found: Array
+ - Found: Object
+ - Found: Array
+ - Found: Object
+
+ Often you won't have control over the input you're parsing, so can't make use
+ of this feature. But, all is not lost: if you are parsing a long array you can
+ get the same effect by setting  rootArrayItems to YES:
+
+    id parser = [SBJson4Parser unwrapRootArrayParserWithBlock:block
+                                                 errorHandler:eh];
+
+    // Note that this input contains A SINGLE top-level document
+    id data = [@"[[],{},[],{}]" dataWithEncoding:NSUTF8StringEncoding];
+    [parser parse:data];
+
+ @note Stream based parsing does mean that you lose some of the correctness
+ verification you would have with a parser that considered the entire input
+ before returning an answer. It is technically possible to have some parts
+ of a document returned *as if they were correct* but then encounter an error
+ in a later part of the document. You should keep this in mind when
+ considering whether it would suit your application.
+
+
+*/
+@interface SBJson4Parser : NSObject
+
+/**
+ Create a JSON Parser.
+
+ This can be used to create a parser that accepts only one document, or one that parses
+ many documents any
+
+ @param block Called for each element. Set *stop to `YES` if you have seen
+ enough and would like to skip the rest of the elements.
+
+ @param allowMultiRoot Indicate that you are expecting multiple whitespace-separated
+ JSON documents, similar to what Twitter uses.
+
+ @param unwrapRootArray If set the parser will pretend an root array does not exist
+ and the enumerator block will be called once for each item in it. This option
+ does nothing if the the JSON has an object at its root.
+
+ @param eh Called if the parser encounters an error.
+
+ @see -unwrapRootArrayParserWithBlock:errorHandler:
+ @see -multiRootParserWithBlock:errorHandler:
+ @see -initWithBlock:processBlock:multiRoot:unwrapRootArray:maxDepth:errorHandler:
+
+ */
++ (id)parserWithBlock:(SBJson4ValueBlock)block
+       allowMultiRoot:(BOOL)allowMultiRoot
+      unwrapRootArray:(BOOL)unwrapRootArray
+         errorHandler:(SBJson4ErrorBlock)eh;
+
+
+/**
+ Create a JSON Parser that parses multiple whitespace separated documents.
+ This is useful for something like Twitter's feed, which gives you one JSON
+ document per line.
+
+ @param block Called for each element. Set *stop to `YES` if you have seen
+ enough and would like to skip the rest of the elements.
+
+ @param eh Called if the parser encounters an error.
+
+ @see +unwrapRootArrayParserWithBlock:errorHandler:
+ @see +parserWithBlock:allowMultiRoot:unwrapRootArray:errorHandler:
+ @see -initWithBlock:processBlock:multiRoot:unwrapRootArray:maxDepth:errorHandler:
+ */
++ (id)multiRootParserWithBlock:(SBJson4ValueBlock)block
+                  errorHandler:(SBJson4ErrorBlock)eh;
+
+/**
+ Create a JSON Parser that parses a huge array and calls for the value block for
+ each element in the outermost array.
+
+ @param block Called for each element. Set *stop to `YES` if you have seen
+ enough and would like to skip the rest of the elements.
+
+ @param eh Called if the parser encounters an error.
+
+ @see +multiRootParserWithBlock:errorHandler:
+ @see +parserWithBlock:allowMultiRoot:unwrapRootArray:errorHandler:
+ @see -initWithBlock:processBlock:multiRoot:unwrapRootArray:maxDepth:errorHandler:
+ */
++ (id)unwrapRootArrayParserWithBlock:(SBJson4ValueBlock)block
+                        errorHandler:(SBJson4ErrorBlock)eh;
+
+/**
+ Create a JSON Parser.
+
+ @param block Called for each element. Set *stop to `YES` if you have seen
+ enough and would like to skip the rest of the elements.
+
+ @param processBlock A block that allows you to process individual values before being
+ returned.
+
+ @param multiRoot Indicate that you are expecting multiple whitespace-separated
+ JSON documents, similar to what Twitter uses.
+
+ @param unwrapRootArray If set the parser will pretend an root array does not exist
+ and the enumerator block will be called once for each item in it. This option
+ does nothing if the the JSON has an object at its root.
+
+ @param maxDepth The max recursion depth of the parser. Defaults to 32.
+
+ @param eh Called if the parser encounters an error.
+
+ */
+- (id)initWithBlock:(SBJson4ValueBlock)block
+       processBlock:(SBJson4ProcessBlock)processBlock
+          multiRoot:(BOOL)multiRoot
+    unwrapRootArray:(BOOL)unwrapRootArray
+           maxDepth:(NSUInteger)maxDepth
+       errorHandler:(SBJson4ErrorBlock)eh;
+
+/**
+ Parse some JSON
+
+ The JSON is assumed to be UTF8 encoded. This can be a full JSON document, or a part of one.
+
+ @param data An NSData object containing the next chunk of JSON
+
+ @return
+ - SBJson4ParserComplete if a full document was found
+ - SBJson4ParserWaitingForData if a partial document was found and more data is required to complete it
+ - SBJson4ParserError if an error occurred.
+
+ */
+- (SBJson4ParserStatus)parse:(NSData*)data;
+
+@end

+ 287 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4Parser.m

@@ -0,0 +1,287 @@
+/*
+ Copyright (c) 2010-2013, Stig Brautaset.
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+ 
+   Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+  
+   Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+ 
+   Neither the name of the the author nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if !__has_feature(objc_arc)
+#error "This source file must be compiled with ARC enabled!"
+#endif
+
+#import "SBJson4Parser.h"
+
+@interface SBJson4Parser () <SBJson4StreamParserDelegate>
+
+- (void)pop;
+
+@end
+
+typedef enum {
+    SBJson4ChunkNone,
+    SBJson4ChunkArray,
+    SBJson4ChunkObject,
+} SBJson4ChunkType;
+
+@implementation SBJson4Parser {
+    SBJson4StreamParser *_parser;
+    NSUInteger depth;
+    NSMutableArray *array;
+    NSMutableDictionary *dict;
+    NSMutableArray *keyStack;
+    NSMutableArray *stack;
+    NSMutableArray *path;
+    SBJson4ProcessBlock processBlock;
+    SBJson4ErrorBlock errorHandler;
+    SBJson4ValueBlock valueBlock;
+    SBJson4ChunkType currentType;
+    BOOL supportManyDocuments;
+    BOOL supportPartialDocuments;
+    NSUInteger _maxDepth;
+}
+
+#pragma mark Housekeeping
+
+- (id)init {
+    @throw @"Not Implemented";
+}
+
++ (id)multiRootParserWithBlock:(SBJson4ValueBlock)block errorHandler:(SBJson4ErrorBlock)eh {
+    return [self parserWithBlock:block
+                  allowMultiRoot:YES
+                 unwrapRootArray:NO
+                    errorHandler:eh];
+}
+
++ (id)unwrapRootArrayParserWithBlock:(SBJson4ValueBlock)block errorHandler:(SBJson4ErrorBlock)eh {
+    return [self parserWithBlock:block
+                  allowMultiRoot:NO
+                 unwrapRootArray:YES
+                    errorHandler:eh];
+}
+
++ (id)parserWithBlock:(SBJson4ValueBlock)block
+       allowMultiRoot:(BOOL)allowMultiRoot
+      unwrapRootArray:(BOOL)unwrapRootArray
+         errorHandler:(SBJson4ErrorBlock)eh {
+
+    return [[self alloc] initWithBlock:block
+                          processBlock:nil
+                             multiRoot:allowMultiRoot
+                       unwrapRootArray:unwrapRootArray
+                              maxDepth:32
+                          errorHandler:eh];
+}
+
+- (id)initWithBlock:(SBJson4ValueBlock)block
+       processBlock:(SBJson4ProcessBlock)initialProcessBlock
+          multiRoot:(BOOL)multiRoot
+    unwrapRootArray:(BOOL)unwrapRootArray
+           maxDepth:(NSUInteger)maxDepth
+       errorHandler:(SBJson4ErrorBlock)eh {
+
+	self = [super init];
+	if (self) {
+        _parser = [[SBJson4StreamParser alloc] init];
+        _parser.delegate = self;
+
+        supportManyDocuments = multiRoot;
+        supportPartialDocuments = unwrapRootArray;
+
+        valueBlock = block;
+		keyStack = [[NSMutableArray alloc] initWithCapacity:32];
+		stack = [[NSMutableArray alloc] initWithCapacity:32];
+        if (initialProcessBlock)
+            path = [[NSMutableArray alloc] initWithCapacity:32];
+        processBlock = initialProcessBlock;
+        errorHandler = eh ? eh : ^(NSError*err) { NSLog(@"%@", err); };
+		currentType = SBJson4ChunkNone;
+        _maxDepth = maxDepth;
+	}
+	return self;
+}
+
+
+#pragma mark Private methods
+
+- (void)pop {
+	[stack removeLastObject];
+	array = nil;
+	dict = nil;
+	currentType = SBJson4ChunkNone;
+	
+	id value = [stack lastObject];
+	
+	if ([value isKindOfClass:[NSArray class]]) {
+		array = value;
+		currentType = SBJson4ChunkArray;
+	} else if ([value isKindOfClass:[NSDictionary class]]) {
+		dict = value;
+		currentType = SBJson4ChunkObject;
+	}
+}
+
+- (void)parserFound:(id)obj isValue:(BOOL)isValue {
+	NSParameterAssert(obj);
+	
+    if(processBlock&&path) {
+        if(isValue) {
+            obj = processBlock(obj,[NSString stringWithFormat:@"%@.%@",[self pathString],[keyStack lastObject]]);
+        }
+        else {
+            [path removeLastObject];
+        }
+    }
+
+	switch (currentType) {
+		case SBJson4ChunkArray:
+			[array addObject:obj];
+			break;
+
+		case SBJson4ChunkObject:
+			NSParameterAssert(keyStack.count);
+			[dict setObject:obj forKey:[keyStack lastObject]];
+			[keyStack removeLastObject];
+			break;
+
+		case SBJson4ChunkNone: {
+            __block BOOL stop = NO;
+            valueBlock(obj, &stop);
+            if (stop) [_parser stop];
+        }
+			break;
+
+		default:
+			break;
+	}
+}
+
+
+#pragma mark Delegate methods
+
+- (void)parserFoundObjectStart {
+    ++depth;
+    if (depth > _maxDepth)
+        [self maxDepthError];
+
+    if (path)
+        [self addToPath];
+    dict = [NSMutableDictionary new];
+	[stack addObject:dict];
+    currentType = SBJson4ChunkObject;
+}
+
+- (void)parserFoundObjectKey:(NSString *)key_ {
+    [keyStack addObject:key_];
+}
+
+- (void)parserFoundObjectEnd {
+    depth--;
+	id value = dict;
+	[self pop];
+    [self parserFound:value isValue:NO ];
+}
+
+- (void)parserFoundArrayStart {
+    depth++;
+    if (depth > _maxDepth)
+        [self maxDepthError];
+
+    if (depth > 1 || !supportPartialDocuments) {
+        if(path)
+            [self addToPath];
+		array = [NSMutableArray new];
+		[stack addObject:array];
+		currentType = SBJson4ChunkArray;
+    }
+}
+
+- (void)parserFoundArrayEnd {
+    depth--;
+    if (depth > 1 || !supportPartialDocuments) {
+		id value = array;
+		[self pop];
+        [self parserFound:value isValue:NO ];
+    }
+}
+
+- (void)maxDepthError {
+    id ui = @{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Input depth exceeds max depth of %lu", (unsigned long)_maxDepth]};
+    errorHandler([NSError errorWithDomain:@"org.sbjson.parser" code:3 userInfo:ui]);
+    [_parser stop];
+}
+
+- (void)parserFoundBoolean:(BOOL)x {
+	[self parserFound:[NSNumber numberWithBool:x] isValue:YES ];
+}
+
+- (void)parserFoundNull {
+    [self parserFound:[NSNull null] isValue:YES ];
+}
+
+- (void)parserFoundNumber:(NSNumber *)num {
+    [self parserFound:num isValue:YES ];
+}
+
+- (void)parserFoundString:(NSString *)string {
+    [self parserFound:string isValue:YES ];
+}
+
+- (void)parserFoundError:(NSError *)err {
+    errorHandler(err);
+}
+
+- (void)addToPath {
+    if([path count]==0)
+        [path addObject:@"$"];
+    else if([[stack lastObject] isKindOfClass:[NSArray class]])
+        [path addObject:@([[stack lastObject] count])];
+    else
+        [path addObject:[keyStack lastObject]];
+}
+
+- (NSString *)pathString {
+    NSMutableString *pathString = [NSMutableString stringWithString:@"$"];
+    for(NSUInteger i=1;i<[path count];i++) {
+        if([[path objectAtIndex:i] isKindOfClass:[NSNumber class]])
+            [pathString appendString:[NSString stringWithFormat:@"[%@]",[path objectAtIndex:i]]];
+        else
+            [pathString appendString:[NSString stringWithFormat:@".%@",[path objectAtIndex:i]]];
+    }
+    return pathString;
+}
+
+- (BOOL)parserShouldSupportManyDocuments {
+    return supportManyDocuments;
+}
+
+- (SBJson4ParserStatus)parse:(NSData *)data {
+    return [_parser parse:data];
+}
+
+@end

+ 131 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamParser.h

@@ -0,0 +1,131 @@
+/*
+ Copyright (c) 2010-2013, Stig Brautaset.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+   Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+   Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+   Neither the name of the the author nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class SBJson4StreamParser;
+@class SBJson4StreamParserState;
+
+typedef enum {
+    SBJson4ParserComplete,
+    SBJson4ParserStopped,
+    SBJson4ParserWaitingForData,
+    SBJson4ParserError,
+} SBJson4ParserStatus;
+
+
+/**
+ Delegate for interacting directly with the low-level parser
+
+ You will most likely find it much more convenient to use the SBJson4Parser instead.
+ */
+@protocol SBJson4StreamParserDelegate < NSObject >
+
+/// Called when object start is found
+- (void)parserFoundObjectStart;
+
+/// Called when object key is found
+- (void)parserFoundObjectKey:(NSString *)key;
+
+/// Called when object end is found
+- (void)parserFoundObjectEnd;
+
+/// Called when array start is found
+- (void)parserFoundArrayStart;
+
+/// Called when array end is found
+- (void)parserFoundArrayEnd;
+
+/// Called when a boolean value is found
+- (void)parserFoundBoolean:(BOOL)x;
+
+/// Called when a null value is found
+- (void)parserFoundNull;
+
+/// Called when a number is found
+- (void)parserFoundNumber:(NSNumber *)num;
+
+/// Called when a string is found
+- (void)parserFoundString:(NSString *)string;
+
+/// Called when an error occurs
+- (void)parserFoundError:(NSError *)err;
+
+@optional
+
+/// Called to determine whether to allow multiple whitespace-separated documents
+- (BOOL)parserShouldSupportManyDocuments;
+
+@end
+
+/**
+ Low-level Stream parser
+
+ You most likely want to use the SBJson4Parser instead, but if you
+ really need low-level access to tokens one-by-one you can use this class.
+ */
+@interface SBJson4StreamParser : NSObject
+
+@property (nonatomic, weak) SBJson4StreamParserState *state; // Private
+
+/**
+ Delegate to receive messages
+
+ The object set here receives a series of messages as the parser breaks down the JSON stream
+ into valid tokens.
+
+ Usually this should be an instance of SBJson4Parser, but you can
+ substitute your own implementation of the SBJson4StreamParserDelegate protocol if you need to.
+ */
+@property (nonatomic, weak) id<SBJson4StreamParserDelegate> delegate;
+
+/**
+ Parse some JSON
+
+ The JSON is assumed to be UTF8 encoded. This can be a full JSON document, or a part of one.
+
+ @param data An NSData object containing the next chunk of JSON
+
+ @return
+ - SBJson4ParserComplete if a full document was found
+ - SBJson4ParserWaitingForData if a partial document was found and more data is required to complete it
+ - SBJson4ParserError if an error occurred.
+
+ */
+- (SBJson4ParserStatus)parse:(NSData*)data;
+
+/**
+ Call this to cause parsing to stop.
+ */
+- (void)stop;
+
+@end

+ 317 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamParser.m

@@ -0,0 +1,317 @@
+/*
+ Copyright (c) 2010, Stig Brautaset.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ Neither the name of the the author nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if !__has_feature(objc_arc)
+#error "This source file must be compiled with ARC enabled!"
+#endif
+
+#import "SBJson4StreamParser.h"
+#import "SBJson4StreamTokeniser.h"
+#import "SBJson4StreamParserState.h"
+
+#define SBStringIsSurrogateHighCharacter(character) ((character >= 0xD800UL) && (character <= 0xDBFFUL))
+
+@implementation SBJson4StreamParser {
+@private
+  SBJson4StreamTokeniser *tokeniser;
+  BOOL stopped;
+  NSMutableArray *_stateStack;
+}
+
+#pragma mark Housekeeping
+
+- (id)init {
+	self = [super init];
+	if (self) {
+        _stateStack = [[NSMutableArray alloc] initWithCapacity:32];
+        _state = [SBJson4StreamParserStateStart sharedInstance];
+		tokeniser = [[SBJson4StreamTokeniser alloc] init];
+	}
+	return self;
+}
+
+
+#pragma mark Methods
+
+- (NSString*)tokenName:(sbjson4_token_t)token {
+	switch (token) {
+		case sbjson4_token_array_open:
+			return @"start of array";
+
+		case sbjson4_token_array_close:
+			return @"end of array";
+
+        case sbjson4_token_integer:
+        case sbjson4_token_real:
+			return @"number";
+
+        case sbjson4_token_string:
+        case sbjson4_token_encoded:
+			return @"string";
+
+        case sbjson4_token_bool:
+			return @"boolean";
+
+		case sbjson4_token_null:
+			return @"null";
+
+        case sbjson4_token_entry_sep:
+			return @"key-value separator";
+
+        case sbjson4_token_value_sep:
+			return @"value separator";
+
+		case sbjson4_token_object_open:
+			return @"start of object";
+
+		case sbjson4_token_object_close:
+			return @"end of object";
+
+		case sbjson4_token_eof:
+		case sbjson4_token_error:
+			break;
+	}
+	NSAssert(NO, @"Should not get here");
+	return @"<aaiiie!>";
+}
+
+- (void)handleObjectStart {
+    [_delegate parserFoundObjectStart];
+    [_stateStack addObject:_state];
+    _state = [SBJson4StreamParserStateObjectStart sharedInstance];
+}
+
+- (void)handleObjectEnd: (sbjson4_token_t) tok  {
+    _state = [_stateStack lastObject];
+    [_stateStack removeLastObject];
+    [_state parser:self shouldTransitionTo:tok];
+    [_delegate parserFoundObjectEnd];
+}
+
+- (void)handleArrayStart {
+	[_delegate parserFoundArrayStart];
+    [_stateStack addObject:_state];
+    _state = [SBJson4StreamParserStateArrayStart sharedInstance];
+}
+
+- (void)handleArrayEnd: (sbjson4_token_t) tok  {
+    _state = [_stateStack lastObject];
+    [_stateStack removeLastObject];
+    [_state parser:self shouldTransitionTo:tok];
+    [_delegate parserFoundArrayEnd];
+}
+
+- (void) handleTokenNotExpectedHere: (sbjson4_token_t) tok  {
+    NSString *tokenName = [self tokenName:tok];
+    NSString *stateName = [_state name];
+
+    _state = [SBJson4StreamParserStateError sharedInstance];
+    id ui = @{ NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Token '%@' not expected %@", tokenName, stateName]};
+    [_delegate parserFoundError:[NSError errorWithDomain:@"org.sbjson.parser" code:2 userInfo:ui]];
+}
+
+- (SBJson4ParserStatus)parse:(NSData *)data_ {
+    @autoreleasepool {
+        [tokeniser appendData:data_];
+        
+        for (;;) {
+
+            if (stopped)
+                return SBJson4ParserStopped;
+            
+            if ([_state isError])
+                return SBJson4ParserError;
+
+            char *token;
+            NSUInteger token_len;
+            sbjson4_token_t tok = [tokeniser getToken:&token length:&token_len];
+            
+            switch (tok) {
+                case sbjson4_token_eof:
+                    return [_state parserShouldReturn:self];
+
+                case sbjson4_token_error:
+                    _state = [SBJson4StreamParserStateError sharedInstance];
+                    [_delegate parserFoundError:[NSError errorWithDomain:@"org.sbjson.parser" code:3
+                                                                userInfo:@{NSLocalizedDescriptionKey : tokeniser.error}]];
+                    return SBJson4ParserError;
+
+                default:
+                    
+                    if (![_state parser:self shouldAcceptToken:tok]) {
+                        [self handleTokenNotExpectedHere: tok];
+                        return SBJson4ParserError;
+                    }
+                    
+                    switch (tok) {
+                        case sbjson4_token_object_open:
+                            [self handleObjectStart];
+                            break;
+                            
+                        case sbjson4_token_object_close:
+                            [self handleObjectEnd: tok];
+                            break;
+                            
+                        case sbjson4_token_array_open:
+                            [self handleArrayStart];
+                            break;
+                            
+                        case sbjson4_token_array_close:
+                            [self handleArrayEnd: tok];
+                            break;
+                            
+                        case sbjson4_token_value_sep:
+                        case sbjson4_token_entry_sep:
+                            [_state parser:self shouldTransitionTo:tok];
+                            break;
+                            
+                        case sbjson4_token_bool:
+                            [_delegate parserFoundBoolean:token[0] == 't'];
+                            [_state parser:self shouldTransitionTo:tok];
+                            break;
+                            
+
+                        case sbjson4_token_null:
+                            [_delegate parserFoundNull];
+                            [_state parser:self shouldTransitionTo:tok];
+                            break;
+
+                        case sbjson4_token_integer: {
+                            const int UNSIGNED_LONG_LONG_MAX_DIGITS = 20;
+                            if (token_len <= UNSIGNED_LONG_LONG_MAX_DIGITS) {
+                                if (*token == '-')
+                                    [_delegate parserFoundNumber:@(strtoll(token, NULL, 10))];
+                                else
+                                    [_delegate parserFoundNumber:@(strtoull(token, NULL, 10))];
+                                
+                                [_state parser:self shouldTransitionTo:tok];
+                                break;
+                            }
+                        }
+                            // FALL THROUGH
+
+                        case sbjson4_token_real: {
+                            [_delegate parserFoundNumber:@(strtod(token, NULL))];
+                            [_state parser:self shouldTransitionTo:tok];
+                            break;
+                        }
+
+                        case sbjson4_token_string: {
+                            NSString *string = [[NSString alloc] initWithBytes:token length:token_len encoding:NSUTF8StringEncoding];
+                            if ([_state needKey])
+                                [_delegate parserFoundObjectKey:string];
+                            else
+                                [_delegate parserFoundString:string];
+                            [_state parser:self shouldTransitionTo:tok];
+                            break;
+                        }
+
+                        case sbjson4_token_encoded: {
+                            NSString *string = [self decodeStringToken:token length:token_len];
+                            if ([_state needKey])
+                                [_delegate parserFoundObjectKey:string];
+                            else
+                                [_delegate parserFoundString:string];
+                            [_state parser:self shouldTransitionTo:tok];
+                            break;
+                        }
+
+                        default:
+                            break;
+                    }
+                    break;
+            }
+        }
+        return SBJson4ParserComplete;
+    }
+}
+
+- (unichar)decodeHexQuad:(char *)quad {
+    unichar ch = 0;
+    for (NSUInteger i = 0; i < 4; i++) {
+        int c = quad[i];
+        ch *= 16;
+        switch (c) {
+            case '0' ... '9': ch += c - '0'; break;
+            case 'a' ... 'f': ch += 10 + c - 'a'; break;
+            case 'A' ... 'F': ch += 10 + c - 'A'; break;
+            default: @throw @"FUT FUT FUT";
+        }
+    }
+    return ch;
+}
+
+- (NSString*)decodeStringToken:(char*)bytes length:(NSUInteger)len {
+    NSMutableData *buf = [NSMutableData dataWithCapacity:len];
+    for (NSUInteger i = 0; i < len;) {
+        switch ((unsigned char)bytes[i]) {
+            case '\\': {
+                switch ((unsigned char)bytes[++i]) {
+                    case '"': [buf appendBytes:"\"" length:1]; i++; break;
+                    case '/': [buf appendBytes:"/" length:1]; i++; break;
+                    case '\\': [buf appendBytes:"\\" length:1]; i++; break;
+                    case 'b': [buf appendBytes:"\b" length:1]; i++; break;
+                    case 'f': [buf appendBytes:"\f" length:1]; i++; break;
+                    case 'n': [buf appendBytes:"\n" length:1]; i++; break;
+                    case 'r': [buf appendBytes:"\r" length:1]; i++; break;
+                    case 't': [buf appendBytes:"\t" length:1]; i++; break;
+                    case 'u': {
+                        unichar hi = [self decodeHexQuad:bytes + i + 1];
+                        i += 5;
+                        if (SBStringIsSurrogateHighCharacter(hi)) {
+                            // Skip past \u that we know is there..
+                            unichar lo = [self decodeHexQuad:bytes + i + 2];
+                            i += 6;
+                            [buf appendData:[[NSString stringWithFormat:@"%C%C", hi, lo] dataUsingEncoding:NSUTF8StringEncoding]];
+                        } else {
+                            [buf appendData:[[NSString stringWithFormat:@"%C", hi] dataUsingEncoding:NSUTF8StringEncoding]];
+                        }
+                        break;
+                    }
+                    default: @throw @"FUT FUT FUT";
+                }
+                break;
+            }
+            default:
+                [buf appendBytes:bytes + i length:1];
+                i++;
+                break;
+        }
+    }
+    return [[NSString alloc] initWithData:buf encoding:NSUTF8StringEncoding];
+}
+
+- (void)stop {
+    stopped = YES;
+}
+
+@end

+ 82 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamParserState.h

@@ -0,0 +1,82 @@
+/*
+ Copyright (c) 2010, Stig Brautaset.
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+ 
+   Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+  
+   Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+ 
+   Neither the name of the the author nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "SBJson4StreamTokeniser.h"
+#import "SBJson4StreamParser.h"
+
+@interface SBJson4StreamParserState : NSObject
++ (id)sharedInstance;
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token;
+- (SBJson4ParserStatus)parserShouldReturn:(SBJson4StreamParser *)parser;
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok;
+- (BOOL)needKey;
+- (BOOL)isError;
+
+- (NSString*)name;
+
+@end
+
+@interface SBJson4StreamParserStateStart : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateComplete : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateError : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateObjectStart : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateObjectGotKey : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateObjectSeparator : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateObjectGotValue : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateObjectNeedKey : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateArrayStart : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateArrayGotValue : SBJson4StreamParserState
+@end
+
+@interface SBJson4StreamParserStateArrayNeedValue : SBJson4StreamParserState
+@end

+ 355 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamParserState.m

@@ -0,0 +1,355 @@
+/*
+ Copyright (c) 2010-2013, Stig Brautaset.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+   Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+   Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+   Neither the name of the the author nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if !__has_feature(objc_arc)
+#error "This source file must be compiled with ARC enabled!"
+#endif
+
+#import "SBJson4StreamParserState.h"
+
+#define SINGLETON \
++ (id)sharedInstance { \
+    static id state = nil; \
+    if (!state) { \
+        @synchronized(self) { \
+            if (!state) state = [[self alloc] init]; \
+        } \
+    } \
+    return state; \
+}
+
+@implementation SBJson4StreamParserState
+
++ (id)sharedInstance { return nil; }
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+	return NO;
+}
+
+- (SBJson4ParserStatus)parserShouldReturn:(SBJson4StreamParser *)parser {
+	return SBJson4ParserWaitingForData;
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {}
+
+- (BOOL)needKey {
+	return NO;
+}
+
+- (NSString*)name {
+	return @"<aaiie!>";
+}
+
+- (BOOL)isError {
+    return NO;
+}
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateStart
+
+SINGLETON
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+	return token == sbjson4_token_array_open || token == sbjson4_token_object_open;
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {
+
+	SBJson4StreamParserState *state = nil;
+	switch (tok) {
+		case sbjson4_token_array_open:
+			state = [SBJson4StreamParserStateArrayStart sharedInstance];
+			break;
+
+		case sbjson4_token_object_open:
+			state = [SBJson4StreamParserStateObjectStart sharedInstance];
+			break;
+
+		case sbjson4_token_array_close:
+		case sbjson4_token_object_close:
+			if ([parser.delegate respondsToSelector:@selector(parserShouldSupportManyDocuments)] && [parser.delegate parserShouldSupportManyDocuments])
+				state = parser.state;
+			else
+				state = [SBJson4StreamParserStateComplete sharedInstance];
+			break;
+
+		case sbjson4_token_eof:
+			return;
+
+		default:
+			state = [SBJson4StreamParserStateError sharedInstance];
+			break;
+	}
+
+
+	parser.state = state;
+}
+
+- (NSString*)name { return @"before outer-most array or object"; }
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateComplete
+
+SINGLETON
+
+- (NSString*)name { return @"after outer-most array or object"; }
+
+- (SBJson4ParserStatus)parserShouldReturn:(SBJson4StreamParser *)parser {
+	return SBJson4ParserComplete;
+}
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateError
+
+SINGLETON
+
+- (NSString*)name { return @"in error"; }
+
+- (SBJson4ParserStatus)parserShouldReturn:(SBJson4StreamParser *)parser {
+	return SBJson4ParserError;
+}
+
+- (BOOL)isError {
+    return YES;
+}
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateObjectStart
+
+SINGLETON
+
+- (NSString*)name { return @"at beginning of object"; }
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+	switch (token) {
+		case sbjson4_token_object_close:
+		case sbjson4_token_string:
+        case sbjson4_token_encoded:
+			return YES;
+		default:
+			return NO;
+	}
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {
+	parser.state = [SBJson4StreamParserStateObjectGotKey sharedInstance];
+}
+
+- (BOOL)needKey {
+	return YES;
+}
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateObjectGotKey
+
+SINGLETON
+
+- (NSString*)name { return @"after object key"; }
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+	return token == sbjson4_token_entry_sep;
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {
+	parser.state = [SBJson4StreamParserStateObjectSeparator sharedInstance];
+}
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateObjectSeparator
+
+SINGLETON
+
+- (NSString*)name { return @"as object value"; }
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+	switch (token) {
+		case sbjson4_token_object_open:
+		case sbjson4_token_array_open:
+		case sbjson4_token_bool:
+		case sbjson4_token_null:
+        case sbjson4_token_integer:
+        case sbjson4_token_real:
+        case sbjson4_token_string:
+        case sbjson4_token_encoded:
+			return YES;
+
+		default:
+			return NO;
+	}
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {
+	parser.state = [SBJson4StreamParserStateObjectGotValue sharedInstance];
+}
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateObjectGotValue
+
+SINGLETON
+
+- (NSString*)name { return @"after object value"; }
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+	switch (token) {
+		case sbjson4_token_object_close:
+        case sbjson4_token_value_sep:
+			return YES;
+
+		default:
+			return NO;
+	}
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {
+	parser.state = [SBJson4StreamParserStateObjectNeedKey sharedInstance];
+}
+
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateObjectNeedKey
+
+SINGLETON
+
+- (NSString*)name { return @"in place of object key"; }
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+    return sbjson4_token_string == token || sbjson4_token_encoded == token;
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {
+	parser.state = [SBJson4StreamParserStateObjectGotKey sharedInstance];
+}
+
+- (BOOL)needKey {
+	return YES;
+}
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateArrayStart
+
+SINGLETON
+
+- (NSString*)name { return @"at array start"; }
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+	switch (token) {
+		case sbjson4_token_object_close:
+        case sbjson4_token_entry_sep:
+        case sbjson4_token_value_sep:
+			return NO;
+
+		default:
+			return YES;
+	}
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {
+	parser.state = [SBJson4StreamParserStateArrayGotValue sharedInstance];
+}
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateArrayGotValue
+
+SINGLETON
+
+- (NSString*)name { return @"after array value"; }
+
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+	return token == sbjson4_token_array_close || token == sbjson4_token_value_sep;
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {
+	if (tok == sbjson4_token_value_sep)
+		parser.state = [SBJson4StreamParserStateArrayNeedValue sharedInstance];
+}
+
+@end
+
+#pragma mark -
+
+@implementation SBJson4StreamParserStateArrayNeedValue
+
+SINGLETON
+
+- (NSString*)name { return @"as array value"; }
+
+
+- (BOOL)parser:(SBJson4StreamParser *)parser shouldAcceptToken:(sbjson4_token_t)token {
+	switch (token) {
+		case sbjson4_token_array_close:
+        case sbjson4_token_entry_sep:
+		case sbjson4_token_object_close:
+		case sbjson4_token_value_sep:
+			return NO;
+
+		default:
+			return YES;
+	}
+}
+
+- (void)parser:(SBJson4StreamParser *)parser shouldTransitionTo:(sbjson4_token_t)tok {
+	parser.state = [SBJson4StreamParserStateArrayGotValue sharedInstance];
+}
+
+@end
+

+ 40 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamTokeniser.h

@@ -0,0 +1,40 @@
+//
+// Created by SuperPappi on 09/01/2013.
+//
+// To change the template use AppCode | Preferences | File Templates.
+//
+
+#import <Foundation/Foundation.h>
+
+typedef enum {
+    sbjson4_token_error = -1,
+    sbjson4_token_eof,
+
+    sbjson4_token_array_open,
+    sbjson4_token_array_close,
+    sbjson4_token_value_sep,
+
+    sbjson4_token_object_open,
+    sbjson4_token_object_close,
+    sbjson4_token_entry_sep,
+
+    sbjson4_token_bool,
+    sbjson4_token_null,
+
+    sbjson4_token_integer,
+    sbjson4_token_real,
+
+    sbjson4_token_string,
+    sbjson4_token_encoded,
+} sbjson4_token_t;
+
+
+@interface SBJson4StreamTokeniser : NSObject
+
+@property (nonatomic, readonly, copy) NSString *error;
+
+- (void)appendData:(NSData*)data_;
+- (sbjson4_token_t)getToken:(char**)tok length:(NSUInteger*)len;
+
+@end
+

+ 395 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamTokeniser.m

@@ -0,0 +1,395 @@
+//
+// Created by SuperPappi on 09/01/2013.
+//
+// To change the template use AppCode | Preferences | File Templates.
+//
+
+
+#import "SBJson4StreamTokeniser.h"
+
+#define SBStringIsIllegalSurrogateHighCharacter(character) (((character) >= 0xD800UL) && ((character) <= 0xDFFFUL))
+#define SBStringIsSurrogateLowCharacter(character) ((character >= 0xDC00UL) && (character <= 0xDFFFUL))
+#define SBStringIsSurrogateHighCharacter(character) ((character >= 0xD800UL) && (character <= 0xDBFFUL))
+
+@implementation SBJson4StreamTokeniser {
+    NSMutableData *data;
+    const char *bytes;
+    NSUInteger index;
+    NSUInteger offset;
+}
+
+- (void)setError:(NSString *)error {
+    _error = [NSString stringWithFormat:@"%@ at index %lu", error, (unsigned long)(offset + index)];
+}
+
+- (void)appendData:(NSData *)data_ {
+    if (!data) {
+        data = [data_ mutableCopy];
+
+    } else if (index) {
+        // Discard data we've already parsed
+        [data replaceBytesInRange:NSMakeRange(0, index) withBytes:"" length:0];
+        [data appendData:data_];
+
+        // Add to the offset for reporting
+        offset += index;
+
+        // Reset index to point to current position
+        index = 0u;
+
+    }
+    else {
+       [data appendData:data_];
+    }
+
+    bytes = [data bytes];
+}
+
+- (void)skipWhitespace {
+    while (index < data.length) {
+        switch (bytes[index]) {
+            case ' ':
+            case '\t':
+            case '\r':
+            case '\n':
+                index++;
+            break;
+            default:
+                return;
+        }
+    }
+}
+
+- (BOOL)getUnichar:(unichar *)ch {
+    if ([self haveRemainingCharacters:1]) {
+        *ch = (unichar) bytes[index];
+        return YES;
+    }
+    return NO;
+}
+
+- (BOOL)haveOneMoreCharacter {
+    return [self haveRemainingCharacters:1];
+}
+
+- (BOOL)haveRemainingCharacters:(NSUInteger)length {
+    return data.length - index >= length;
+}
+
+- (sbjson4_token_t)match:(char *)str retval:(sbjson4_token_t)tok token:(char **)token length:(NSUInteger *)length {
+    NSUInteger len = strlen(str);
+    if ([self haveRemainingCharacters:len]) {
+        if (!memcmp(bytes + index, str, len)) {
+            *token = str;
+            *length = len;
+            index += len;
+            return tok;
+        }
+        [self setError: [NSString stringWithFormat:@"Expected '%s' after initial '%.1s'", str, str]];
+        return sbjson4_token_error;
+    }
+
+    return sbjson4_token_eof;
+}
+
+- (BOOL)decodeHexQuad:(unichar*)quad {
+    unichar tmp = 0;
+
+    for (int i = 0; i < 4; i++, index++) {
+        unichar c = (unichar)bytes[index];
+        tmp *= 16;
+        switch (c) {
+            case '0' ... '9':
+                tmp += c - '0';
+                break;
+
+            case 'a' ... 'f':
+                tmp += 10 + c - 'a';
+                break;
+
+            case 'A' ... 'F':
+                tmp += 10 + c - 'A';
+                break;
+
+            default:
+                return NO;
+        }
+    }
+    *quad = tmp;
+    return YES;
+}
+
+- (sbjson4_token_t)getStringToken:(char **)token length:(NSUInteger *)length {
+
+    // Skip initial "
+    index++;
+
+    NSUInteger string_start = index;
+    sbjson4_token_t tok = sbjson4_token_string;
+
+    for (;;) {
+        if (![self haveOneMoreCharacter])
+            return sbjson4_token_eof;
+
+        switch (bytes[index]) {
+            case 0 ... 0x1F:
+                [self setError:[NSString stringWithFormat:@"Unescaped control character [0x%0.2X] in string", bytes[index]]];
+                return sbjson4_token_error;
+
+            case '"':
+                *token = (char *)(bytes + string_start);
+                *length = index - string_start;
+                index++;
+                return tok;
+
+            case '\\':
+                tok = sbjson4_token_encoded;
+                index++;
+                if (![self haveOneMoreCharacter])
+                    return sbjson4_token_eof;
+
+                if (bytes[index] == 'u') {
+                    index++;
+                    if (![self haveRemainingCharacters:4])
+                        return sbjson4_token_eof;
+
+                    unichar hi;
+                    if (![self decodeHexQuad:&hi]) {
+                        [self setError:@"Invalid hex quad"];
+                        return sbjson4_token_error;
+                    }
+
+                    if (SBStringIsSurrogateHighCharacter(hi)) {
+                        if (![self haveRemainingCharacters:6])
+                            return sbjson4_token_eof;
+
+                        unichar lo;
+                        if (bytes[index++] != '\\' || bytes[index++] != 'u' || ![self decodeHexQuad:&lo]) {
+                            [self setError:@"Missing low character in surrogate pair"];
+                            return sbjson4_token_error;
+                        }
+
+                        if (!SBStringIsSurrogateLowCharacter(lo)) {
+                            [self setError:@"Invalid low character in surrogate pair"];
+                            return sbjson4_token_error;
+                        }
+
+                    } else if (SBStringIsIllegalSurrogateHighCharacter(hi)) {
+                        [self setError:@"Invalid high character in surrogate pair"];
+                        return sbjson4_token_error;
+
+                    }
+
+
+                } else {
+                    switch (bytes[index]) {
+                        case '\\':
+                        case '/':
+                        case '"':
+                        case 'b':
+                        case 'n':
+                        case 'r':
+                        case 't':
+                        case 'f':
+                            index++;
+                            break;
+
+                        default:
+                            [self setError:[NSString stringWithFormat:@"Illegal escape character [%x]", bytes[index]]];
+                            return sbjson4_token_error;
+                    }
+                }
+
+                break;
+
+            default:
+                index++;
+                break;
+        }
+    }
+}
+
+- (sbjson4_token_t)getNumberToken:(char **)token length:(NSUInteger *)length {
+    NSUInteger num_start = index;
+    if (bytes[index] == '-') {
+        index++;
+
+        if (![self haveOneMoreCharacter])
+            return sbjson4_token_eof;
+    }
+
+    sbjson4_token_t tok = sbjson4_token_integer;
+    if (bytes[index] == '0') {
+        index++;
+
+        if (![self haveOneMoreCharacter])
+            return sbjson4_token_eof;
+
+        if (isdigit(bytes[index])) {
+            [self setError:@"Leading zero is illegal in number"];
+            return sbjson4_token_error;
+        }
+    }
+
+    while (isdigit(bytes[index])) {
+        index++;
+        if (![self haveOneMoreCharacter])
+            return sbjson4_token_eof;
+    }
+
+    if (![self haveOneMoreCharacter])
+        return sbjson4_token_eof;
+
+
+    if (bytes[index] == '.') {
+        index++;
+        tok = sbjson4_token_real;
+
+        if (![self haveOneMoreCharacter])
+            return sbjson4_token_eof;
+
+        NSUInteger fraction_start = index;
+        while (isdigit(bytes[index])) {
+            index++;
+            if (![self haveOneMoreCharacter])
+                return sbjson4_token_eof;
+        }
+
+        if (fraction_start == index) {
+            [self setError:@"No digits after decimal point"];
+            return sbjson4_token_error;
+        }
+    }
+
+    if (bytes[index] == 'e' || bytes[index] == 'E') {
+        index++;
+        tok = sbjson4_token_real;
+
+        if (![self haveOneMoreCharacter])
+            return sbjson4_token_eof;
+
+        if (bytes[index] == '-' || bytes[index] == '+') {
+            index++;
+            if (![self haveOneMoreCharacter])
+                return sbjson4_token_eof;
+        }
+
+        NSUInteger exp_start = index;
+        while (isdigit(bytes[index])) {
+            index++;
+            if (![self haveOneMoreCharacter])
+                return sbjson4_token_eof;
+        }
+
+        if (exp_start == index) {
+            [self setError:@"No digits in exponent"];
+            return sbjson4_token_error;
+        }
+
+    }
+
+    if (num_start + 1 == index && bytes[num_start] == '-') {
+        [self setError:@"No digits after initial minus"];
+        return sbjson4_token_error;
+    }
+
+    *token = (char *)(bytes + num_start);
+    *length = index - num_start;
+    return tok;
+}
+
+
+- (sbjson4_token_t)getToken:(char **)token length:(NSUInteger *)length {
+    [self skipWhitespace];
+    NSUInteger copyOfIndex = index;
+
+    unichar ch;
+    if (![self getUnichar:&ch])
+        return sbjson4_token_eof;
+
+    sbjson4_token_t tok;
+    switch (ch) {
+        case '{': {
+            index++;
+            tok = sbjson4_token_object_open;
+            break;
+        }
+        case '}': {
+            index++;
+            tok = sbjson4_token_object_close;
+            break;
+
+        }
+        case '[': {
+            index++;
+            tok = sbjson4_token_array_open;
+            break;
+
+        }
+        case ']': {
+            index++;
+            tok = sbjson4_token_array_close;
+            break;
+
+        }
+        case 't': {
+            tok = [self match:"true" retval:sbjson4_token_bool token:token length:length];
+            break;
+
+        }
+        case 'f': {
+            tok = [self match:"false" retval:sbjson4_token_bool token:token length:length];
+            break;
+
+        }
+        case 'n': {
+            tok = [self match:"null" retval:sbjson4_token_null token:token length:length];
+            break;
+
+        }
+        case ',': {
+            index++;
+            tok = sbjson4_token_value_sep;
+            break;
+
+        }
+        case ':': {
+            index++;
+            tok = sbjson4_token_entry_sep;
+            break;
+
+        }
+        case '"': {
+            tok = [self getStringToken:token length:length];
+            break;
+
+        }
+        case '-':
+        case '0' ... '9': {
+            tok = [self getNumberToken:token length:length];
+            break;
+
+        }
+        case '+': {
+            self.error = @"Leading + is illegal in number";
+            tok = sbjson4_token_error;
+            break;
+
+        }
+        default: {
+            self.error = [NSString stringWithFormat:@"Illegal start of token [%c]", ch];
+            tok = sbjson4_token_error;
+            break;
+        }
+    }
+
+    if (tok == sbjson4_token_eof) {
+        // We ran out of bytes before we could finish parsing the current token.
+        // Back up to the start & wait for more data.
+        index = copyOfIndex;
+    }
+
+    return tok;
+}
+
+@end

+ 210 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamWriter.h

@@ -0,0 +1,210 @@
+/*
+ Copyright (c) 2010, Stig Brautaset.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+   Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+   Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+   Neither the name of the the author nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <Foundation/Foundation.h>
+
+/// Enable JSON writing for non-native objects
+@interface NSObject (SBProxyForJson)
+
+/**
+ Allows generation of JSON for otherwise unsupported classes.
+
+ If you have a custom class that you want to create a JSON representation
+ for you can implement this method in your class. It should return a
+ representation of your object defined in terms of objects that can be
+ translated into JSON. For example, a Person object might implement it like this:
+
+     - (id)proxyForJson {
+        return [NSDictionary dictionaryWithObjectsAndKeys:
+        name, @"name",
+        phone, @"phone",
+        email, @"email",
+        nil];
+     }
+
+ */
+- (id)proxyForJson;
+
+@end
+
+@class SBJson4StreamWriter;
+
+@protocol SBJson4StreamWriterDelegate
+
+- (void)writer:(SBJson4StreamWriter *)writer appendBytes:(const void *)bytes length:(NSUInteger)length;
+
+@end
+
+@class SBJson4StreamWriterState;
+
+/**
+ The Stream Writer class.
+
+ Accepts a stream of messages and writes JSON of these to its delegate object.
+
+ This class provides a range of high-, mid- and low-level methods. You can mix
+ and match calls to these. For example, you may want to call -writeArrayOpen
+ to start an array and then repeatedly call -writeObject: with various objects
+ before finishing off with a -writeArrayClose call.
+
+ Objective-C types are mapped to JSON types in the following way:
+
+ - NSNull        -> null
+ - NSString      -> string
+ - NSArray       -> array
+ - NSDictionary  -> object
+ - NSNumber's -initWithBool:YES -> true
+ - NSNumber's -initWithBool:NO  -> false
+ - NSNumber      -> number
+
+ NSNumber instances created with the -numberWithBool: method are
+ converted into the JSON boolean "true" and "false" values, and vice
+ versa. Any other NSNumber instances are converted to a JSON number the
+ way you would expect.
+
+ @warning: In JSON the keys of an object must be strings. NSDictionary
+ keys need not be, but attempting to convert an NSDictionary with
+ non-string keys into JSON will throw an exception.*
+
+ */
+
+@interface SBJson4StreamWriter : NSObject {
+    NSMutableDictionary *cache;
+}
+
+@property (nonatomic, weak) SBJson4StreamWriterState *state; // Internal
+@property (nonatomic, readonly, strong) NSMutableArray *stateStack; // Internal
+
+/**
+ delegate to receive JSON output
+ Delegate that will receive messages with output.
+ */
+@property (nonatomic, weak) id<SBJson4StreamWriterDelegate> delegate;
+
+/**
+ The maximum depth.
+
+ Defaults to 512. If the input is nested deeper than this the input will be deemed to be
+ malicious and the parser returns nil, signalling an error. ("Nested too deep".) You can
+ turn off this security feature by setting the maxDepth value to 0.
+ */
+@property(nonatomic) NSUInteger maxDepth;
+
+/**
+ Whether we are generating human-readable (multi line) JSON.
+
+ Set whether or not to generate human-readable JSON. The default is NO, which produces
+ JSON without any whitespace between tokens. If set to YES, generates human-readable
+ JSON with line breaks after each array value and dictionary key/value pair, indented two
+ spaces per nesting level.
+ */
+@property(nonatomic) BOOL humanReadable;
+
+/**
+ Whether or not to sort the dictionary keys in the output.
+
+ If this is set to YES, the dictionary keys in the JSON output will be in sorted order.
+ (This is useful if you need to compare two structures, for example.) The default is NO.
+ */
+@property(nonatomic) BOOL sortKeys;
+
+/**
+ An optional comparator to be used if sortKeys is YES.
+
+ If this is nil, sorting will be done via @selector(compare:).
+ */
+@property (nonatomic, copy) NSComparator sortKeysComparator;
+
+/// Contains the error description after an error has occurred.
+@property (nonatomic, copy) NSString *error;
+
+/**
+ Write an NSDictionary to the JSON stream.
+ @return YES if successful, or NO on failure
+ */
+- (BOOL)writeObject:(NSDictionary*)dict;
+
+/**
+ Write an NSArray to the JSON stream.
+ @return YES if successful, or NO on failure
+ */
+- (BOOL)writeArray:(NSArray *)array;
+
+/**
+ Start writing an Object to the stream
+ @return YES if successful, or NO on failure
+*/
+- (BOOL)writeObjectOpen;
+
+/**
+ Close the current object being written
+ @return YES if successful, or NO on failure
+*/
+- (BOOL)writeObjectClose;
+
+/** Start writing an Array to the stream
+ @return YES if successful, or NO on failure
+*/
+- (BOOL)writeArrayOpen;
+
+/** Close the current Array being written
+ @return YES if successful, or NO on failure
+*/
+- (BOOL)writeArrayClose;
+
+/** Write a null to the stream
+ @return YES if successful, or NO on failure
+*/
+- (BOOL)writeNull;
+
+/** Write a boolean to the stream
+ @return YES if successful, or NO on failure
+*/
+- (BOOL)writeBool:(BOOL)x;
+
+/** Write a Number to the stream
+ @return YES if successful, or NO on failure
+*/
+- (BOOL)writeNumber:(NSNumber*)n;
+
+/** Write a String to the stream
+ @return YES if successful, or NO on failure
+*/
+- (BOOL)writeString:(NSString*)s;
+
+@end
+
+@interface SBJson4StreamWriter (Private)
+- (BOOL)writeValue:(id)v;
+- (void)appendBytes:(const void *)bytes length:(NSUInteger)length;
+@end
+

+ 358 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamWriter.m

@@ -0,0 +1,358 @@
+/*
+ Copyright (c) 2010, Stig Brautaset.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+   Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+   Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+   Neither the name of the the author nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if !__has_feature(objc_arc)
+#error "This source file must be compiled with ARC enabled!"
+#endif
+
+#import "SBJson4StreamWriter.h"
+#import "SBJson4StreamWriterState.h"
+
+static NSNumber *kTrue;
+static NSNumber *kFalse;
+static NSNumber *kPositiveInfinity;
+static NSNumber *kNegativeInfinity;
+
+
+@implementation SBJson4StreamWriter
+
++ (void)initialize {
+    kPositiveInfinity = [NSNumber numberWithDouble:+HUGE_VAL];
+    kNegativeInfinity = [NSNumber numberWithDouble:-HUGE_VAL];
+    kTrue = [NSNumber numberWithBool:YES];
+    kFalse = [NSNumber numberWithBool:NO];
+}
+
+#pragma mark Housekeeping
+
+- (id)init {
+	self = [super init];
+	if (self) {
+		_maxDepth = 32u;
+        _stateStack = [[NSMutableArray alloc] initWithCapacity:_maxDepth];
+        _state = [SBJson4StreamWriterStateStart sharedInstance];
+        cache = [[NSMutableDictionary alloc] initWithCapacity:32];
+    }
+	return self;
+}
+
+#pragma mark Methods
+
+- (void)appendBytes:(const void *)bytes length:(NSUInteger)length {
+    [_delegate writer:self appendBytes:bytes length:length];
+}
+
+- (BOOL)writeObject:(NSDictionary *)dict {
+	if (![self writeObjectOpen])
+		return NO;
+
+	NSArray *keys = [dict allKeys];
+
+	if (_sortKeys) {
+		if (_sortKeysComparator) {
+			keys = [keys sortedArrayWithOptions:NSSortStable usingComparator:_sortKeysComparator];
+		}
+		else{
+			keys = [keys sortedArrayUsingSelector:@selector(compare:)];
+		}
+	}
+
+	for (id k in keys) {
+		if (![k isKindOfClass:[NSString class]]) {
+			self.error = [NSString stringWithFormat:@"JSON object key must be string: %@", k];
+			return NO;
+		}
+
+		if (![self writeString:k])
+			return NO;
+		if (![self writeValue:[dict objectForKey:k]])
+			return NO;
+	}
+
+	return [self writeObjectClose];
+}
+
+- (BOOL)writeArray:(NSArray*)array {
+	if (![self writeArrayOpen])
+		return NO;
+	for (id v in array)
+		if (![self writeValue:v])
+			return NO;
+	return [self writeArrayClose];
+}
+
+
+- (BOOL)writeObjectOpen {
+	if ([_state isInvalidState:self]) return NO;
+	if ([_state expectingKey:self]) return NO;
+	[_state appendSeparator:self];
+	if (_humanReadable && _stateStack.count) [_state appendWhitespace:self];
+
+    [_stateStack addObject:_state];
+    self.state = [SBJson4StreamWriterStateObjectStart sharedInstance];
+
+	if (_maxDepth && _stateStack.count > _maxDepth) {
+		self.error = @"Nested too deep";
+		return NO;
+	}
+
+	[_delegate writer:self appendBytes:"{" length:1];
+	return YES;
+}
+
+- (BOOL)writeObjectClose {
+	if ([_state isInvalidState:self]) return NO;
+
+    SBJson4StreamWriterState *prev = _state;
+
+    self.state = [_stateStack lastObject];
+    [_stateStack removeLastObject];
+
+	if (_humanReadable) [prev appendWhitespace:self];
+	[_delegate writer:self appendBytes:"}" length:1];
+
+	[_state transitionState:self];
+	return YES;
+}
+
+- (BOOL)writeArrayOpen {
+	if ([_state isInvalidState:self]) return NO;
+	if ([_state expectingKey:self]) return NO;
+	[_state appendSeparator:self];
+	if (_humanReadable && _stateStack.count) [_state appendWhitespace:self];
+
+    [_stateStack addObject:_state];
+	self.state = [SBJson4StreamWriterStateArrayStart sharedInstance];
+
+	if (_maxDepth && _stateStack.count > _maxDepth) {
+		self.error = @"Nested too deep";
+		return NO;
+	}
+
+	[_delegate writer:self appendBytes:"[" length:1];
+	return YES;
+}
+
+- (BOOL)writeArrayClose {
+	if ([_state isInvalidState:self]) return NO;
+	if ([_state expectingKey:self]) return NO;
+
+    SBJson4StreamWriterState *prev = _state;
+
+    self.state = [_stateStack lastObject];
+    [_stateStack removeLastObject];
+
+	if (_humanReadable) [prev appendWhitespace:self];
+	[_delegate writer:self appendBytes:"]" length:1];
+
+	[_state transitionState:self];
+	return YES;
+}
+
+- (BOOL)writeNull {
+	if ([_state isInvalidState:self]) return NO;
+	if ([_state expectingKey:self]) return NO;
+	[_state appendSeparator:self];
+	if (_humanReadable) [_state appendWhitespace:self];
+
+	[_delegate writer:self appendBytes:"null" length:4];
+	[_state transitionState:self];
+	return YES;
+}
+
+- (BOOL)writeBool:(BOOL)x {
+	if ([_state isInvalidState:self]) return NO;
+	if ([_state expectingKey:self]) return NO;
+	[_state appendSeparator:self];
+	if (_humanReadable) [_state appendWhitespace:self];
+
+	if (x)
+		[_delegate writer:self appendBytes:"true" length:4];
+	else
+		[_delegate writer:self appendBytes:"false" length:5];
+	[_state transitionState:self];
+	return YES;
+}
+
+
+- (BOOL)writeValue:(id)o {
+	if ([o isKindOfClass:[NSDictionary class]]) {
+		return [self writeObject:o];
+
+	} else if ([o isKindOfClass:[NSArray class]]) {
+		return [self writeArray:o];
+
+	} else if ([o isKindOfClass:[NSString class]]) {
+		[self writeString:o];
+		return YES;
+
+	} else if ([o isKindOfClass:[NSNumber class]]) {
+		return [self writeNumber:o];
+
+	} else if ([o isKindOfClass:[NSNull class]]) {
+		return [self writeNull];
+
+	} else if ([o respondsToSelector:@selector(proxyForJson)]) {
+		return [self writeValue:[o proxyForJson]];
+
+	}
+
+	self.error = [NSString stringWithFormat:@"JSON serialisation not supported for %@", [o class]];
+	return NO;
+}
+
+static const char *strForChar(int c) {
+	switch (c) {
+		case 0: return "\\u0000";
+		case 1: return "\\u0001";
+		case 2: return "\\u0002";
+		case 3: return "\\u0003";
+		case 4: return "\\u0004";
+		case 5: return "\\u0005";
+		case 6: return "\\u0006";
+		case 7: return "\\u0007";
+		case 8: return "\\b";
+		case 9: return "\\t";
+		case 10: return "\\n";
+		case 11: return "\\u000b";
+		case 12: return "\\f";
+		case 13: return "\\r";
+		case 14: return "\\u000e";
+		case 15: return "\\u000f";
+		case 16: return "\\u0010";
+		case 17: return "\\u0011";
+		case 18: return "\\u0012";
+		case 19: return "\\u0013";
+		case 20: return "\\u0014";
+		case 21: return "\\u0015";
+		case 22: return "\\u0016";
+		case 23: return "\\u0017";
+		case 24: return "\\u0018";
+		case 25: return "\\u0019";
+		case 26: return "\\u001a";
+		case 27: return "\\u001b";
+		case 28: return "\\u001c";
+		case 29: return "\\u001d";
+		case 30: return "\\u001e";
+		case 31: return "\\u001f";
+		case 34: return "\\\"";
+		case 92: return "\\\\";
+		default:
+			[NSException raise:@"Illegal escape char" format:@"-->%c<-- is not a legal escape character", c];
+			return NULL;
+	}
+}
+
+- (BOOL)writeString:(NSString*)string {
+	if ([_state isInvalidState:self]) return NO;
+	[_state appendSeparator:self];
+	if (_humanReadable) [_state appendWhitespace:self];
+
+	NSMutableData *buf = [cache objectForKey:string];
+	if (!buf) {
+
+        NSUInteger len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+        const char *utf8 = [string UTF8String];
+        NSUInteger written = 0, i = 0;
+
+        buf = [NSMutableData dataWithCapacity:(NSUInteger)(len * 1.1f)];
+        [buf appendBytes:"\"" length:1];
+
+        for (i = 0; i < len; i++) {
+            int c = utf8[i];
+            BOOL isControlChar = c >= 0 && c < 32;
+            if (isControlChar || c == '"' || c == '\\') {
+                if (i - written)
+                    [buf appendBytes:utf8 + written length:i - written];
+                written = i + 1;
+
+                const char *t = strForChar(c);
+                [buf appendBytes:t length:strlen(t)];
+            }
+        }
+
+        if (i - written)
+            [buf appendBytes:utf8 + written length:i - written];
+
+        [buf appendBytes:"\"" length:1];
+        [cache setObject:buf forKey:string];
+    }
+
+	[_delegate writer:self appendBytes:[buf bytes] length:[buf length]];
+	[_state transitionState:self];
+	return YES;
+}
+
+- (BOOL)writeNumber:(NSNumber*)number {
+	if (number == kTrue || number == kFalse)
+		return [self writeBool:[number boolValue]];
+
+	if ([_state isInvalidState:self]) return NO;
+	if ([_state expectingKey:self]) return NO;
+	[_state appendSeparator:self];
+	if (_humanReadable) [_state appendWhitespace:self];
+
+	if ([kPositiveInfinity isEqualToNumber:number]) {
+		self.error = @"+Infinity is not a valid number in JSON";
+		return NO;
+
+	} else if ([kNegativeInfinity isEqualToNumber:number]) {
+		self.error = @"-Infinity is not a valid number in JSON";
+		return NO;
+
+	} else if (isnan([number doubleValue])) {
+		self.error = @"NaN is not a valid number in JSON";
+		return NO;
+	}
+
+	const char *objcType = [number objCType];
+	char num[128];
+	size_t len;
+
+	switch (objcType[0]) {
+		case 'c': case 'i': case 's': case 'l': case 'q':
+			len = snprintf(num, sizeof num, "%lld", [number longLongValue]);
+			break;
+		case 'C': case 'I': case 'S': case 'L': case 'Q':
+			len = snprintf(num, sizeof num, "%llu", [number unsignedLongLongValue]);
+			break;
+		case 'f': case 'd': default: {
+            len = snprintf(num, sizeof num, "%.17g", [number doubleValue]);
+			break;
+        }
+	}
+	[_delegate writer:self appendBytes:num length: len];
+	[_state transitionState:self];
+	return YES;
+}
+
+@end

+ 69 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamWriterState.h

@@ -0,0 +1,69 @@
+/*
+ Copyright (c) 2010, Stig Brautaset.
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+ 
+   Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+  
+   Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+ 
+   Neither the name of the the author nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class SBJson4StreamWriter;
+
+@interface SBJson4StreamWriterState : NSObject
++ (id)sharedInstance;
+- (BOOL)isInvalidState:(SBJson4StreamWriter *)writer;
+- (void)appendSeparator:(SBJson4StreamWriter *)writer;
+- (BOOL)expectingKey:(SBJson4StreamWriter *)writer;
+- (void)transitionState:(SBJson4StreamWriter *)writer;
+- (void)appendWhitespace:(SBJson4StreamWriter *)writer;
+@end
+
+@interface SBJson4StreamWriterStateObjectStart : SBJson4StreamWriterState
+@end
+
+@interface SBJson4StreamWriterStateObjectKey : SBJson4StreamWriterStateObjectStart
+@end
+
+@interface SBJson4StreamWriterStateObjectValue : SBJson4StreamWriterState
+@end
+
+@interface SBJson4StreamWriterStateArrayStart : SBJson4StreamWriterState
+@end
+
+@interface SBJson4StreamWriterStateArrayValue : SBJson4StreamWriterState
+@end
+
+@interface SBJson4StreamWriterStateStart : SBJson4StreamWriterState
+@end
+
+@interface SBJson4StreamWriterStateComplete : SBJson4StreamWriterState
+@end
+
+@interface SBJson4StreamWriterStateError : SBJson4StreamWriterState
+@end
+

+ 147 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4StreamWriterState.m

@@ -0,0 +1,147 @@
+/*
+ Copyright (c) 2010, Stig Brautaset.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+   Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+   Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+   Neither the name of the the author nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if !__has_feature(objc_arc)
+#error "This source file must be compiled with ARC enabled!"
+#endif
+
+#import "SBJson4StreamWriterState.h"
+#import "SBJson4StreamWriter.h"
+
+#define SINGLETON \
++ (id)sharedInstance { \
+    static id state = nil; \
+    if (!state) { \
+        @synchronized(self) { \
+            if (!state) state = [[self alloc] init]; \
+        } \
+    } \
+    return state; \
+}
+
+
+@implementation SBJson4StreamWriterState
++ (id)sharedInstance { return nil; }
+- (BOOL)isInvalidState:(SBJson4StreamWriter *)writer { return NO; }
+- (void)appendSeparator:(SBJson4StreamWriter *)writer {}
+- (BOOL)expectingKey:(SBJson4StreamWriter *)writer { return NO; }
+- (void)transitionState:(SBJson4StreamWriter *)writer {}
+- (void)appendWhitespace:(SBJson4StreamWriter *)writer {
+	[writer appendBytes:"\n" length:1];
+	for (NSUInteger i = 0; i < writer.stateStack.count; i++)
+	    [writer appendBytes:"  " length:2];
+}
+@end
+
+@implementation SBJson4StreamWriterStateObjectStart
+
+SINGLETON
+
+- (void)transitionState:(SBJson4StreamWriter *)writer {
+	writer.state = [SBJson4StreamWriterStateObjectValue sharedInstance];
+}
+- (BOOL)expectingKey:(SBJson4StreamWriter *)writer {
+	writer.error = @"JSON object key must be string";
+	return YES;
+}
+@end
+
+@implementation SBJson4StreamWriterStateObjectKey
+
+SINGLETON
+
+- (void)appendSeparator:(SBJson4StreamWriter *)writer {
+	[writer appendBytes:"," length:1];
+}
+@end
+
+@implementation SBJson4StreamWriterStateObjectValue
+
+SINGLETON
+
+- (void)appendSeparator:(SBJson4StreamWriter *)writer {
+	[writer appendBytes:":" length:1];
+}
+- (void)transitionState:(SBJson4StreamWriter *)writer {
+    writer.state = [SBJson4StreamWriterStateObjectKey sharedInstance];
+}
+- (void)appendWhitespace:(SBJson4StreamWriter *)writer {
+	[writer appendBytes:" " length:1];
+}
+@end
+
+@implementation SBJson4StreamWriterStateArrayStart
+
+SINGLETON
+
+- (void)transitionState:(SBJson4StreamWriter *)writer {
+    writer.state = [SBJson4StreamWriterStateArrayValue sharedInstance];
+}
+@end
+
+@implementation SBJson4StreamWriterStateArrayValue
+
+SINGLETON
+
+- (void)appendSeparator:(SBJson4StreamWriter *)writer {
+	[writer appendBytes:"," length:1];
+}
+@end
+
+@implementation SBJson4StreamWriterStateStart
+
+SINGLETON
+
+
+- (void)transitionState:(SBJson4StreamWriter *)writer {
+    writer.state = [SBJson4StreamWriterStateComplete sharedInstance];
+}
+- (void)appendSeparator:(SBJson4StreamWriter *)writer {
+}
+@end
+
+@implementation SBJson4StreamWriterStateComplete
+
+SINGLETON
+
+- (BOOL)isInvalidState:(SBJson4StreamWriter *)writer {
+	writer.error = @"Stream is closed";
+	return YES;
+}
+@end
+
+@implementation SBJson4StreamWriterStateError
+
+SINGLETON
+
+@end
+

+ 103 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4Writer.h

@@ -0,0 +1,103 @@
+/*
+ Copyright (C) 2009 Stig Brautaset. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+ * Neither the name of the author nor the names of its contributors may be used
+   to endorse or promote products derived from this software without specific
+   prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ The JSON writer class.
+
+ This uses SBJson4StreamWriter internally.
+
+ */
+
+@interface SBJson4Writer : NSObject
+
+/**
+ The maximum depth.
+
+ Defaults to 32. If the input is nested deeper than this the input will be deemed to be
+ malicious and the parser returns nil, signalling an error. ("Nested too deep".) You can
+ turn off this security feature by setting the maxDepth value to 0.
+ */
+@property(nonatomic) NSUInteger maxDepth;
+
+/**
+ Return an error trace, or nil if there was no errors.
+
+ Note that this method returns the trace of the last method that failed.
+ You need to check the return value of the call you're making to figure out
+ if the call actually failed, before you know call this method.
+ */
+@property (nonatomic, readonly, copy) NSString *error;
+
+/**
+ Whether we are generating human-readable (multi line) JSON.
+
+ Set whether or not to generate human-readable JSON. The default is NO, which produces
+ JSON without any whitespace. (Except inside strings.) If set to YES, generates human-readable
+ JSON with line breaks after each array value and dictionary key/value pair, indented two
+ spaces per nesting level.
+ */
+@property(nonatomic) BOOL humanReadable;
+
+/**
+ Whether or not to sort the dictionary keys in the output.
+
+ If this is set to YES, the dictionary keys in the JSON output will be in sorted order.
+ (This is useful if you need to compare two structures, for example.) The default is NO.
+ */
+@property(nonatomic) BOOL sortKeys;
+
+/**
+ An optional comparator to be used if sortKeys is YES.
+
+ If this is nil, sorting will be done via @selector(compare:).
+ */
+@property (nonatomic, copy) NSComparator sortKeysComparator;
+
+/**
+ Generates string with JSON representation for the given object.
+
+ Returns a string containing JSON representation of the passed in value, or nil on error.
+ If nil is returned and error is not NULL, *error can be interrogated to find the cause of the error.
+
+ @param value any instance that can be represented as JSON text.
+ */
+- (NSString*)stringWithObject:(id)value;
+
+/**
+ Generates JSON representation for the given object.
+
+ Returns an NSData object containing JSON represented as UTF8 text, or nil on error.
+
+ @param value any instance that can be represented as JSON text.
+ */
+- (NSData*)dataWithObject:(id)value;
+
+@end

+ 102 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/json-framework/SBJson4Writer.m

@@ -0,0 +1,102 @@
+/*
+ Copyright (C) 2009 Stig Brautaset. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+ * Neither the name of the author nor the names of its contributors may be used
+   to endorse or promote products derived from this software without specific
+   prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if !__has_feature(objc_arc)
+#error "This source file must be compiled with ARC enabled!"
+#endif
+
+#import "SBJson4Writer.h"
+#import "SBJson4StreamWriter.h"
+
+
+@interface SBJson4Writer () < SBJson4StreamWriterDelegate >
+@property (nonatomic, copy) NSString *error;
+@property (nonatomic, strong) NSMutableData *acc;
+@end
+
+@implementation SBJson4Writer
+
+- (id)init {
+    self = [super init];
+    if (self) {
+        self.maxDepth = 32u;
+    }
+    return self;
+}
+
+
+- (NSString*)stringWithObject:(id)value {
+	NSData *data = [self dataWithObject:value];
+	if (data)
+		return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+	return nil;
+}
+
+- (NSData*)dataWithObject:(id)object {
+    self.error = nil;
+
+    self.acc = [[NSMutableData alloc] initWithCapacity:8096u];
+
+    SBJson4StreamWriter *streamWriter = [[SBJson4StreamWriter alloc] init];
+	streamWriter.sortKeys = self.sortKeys;
+	streamWriter.maxDepth = self.maxDepth;
+	streamWriter.sortKeysComparator = self.sortKeysComparator;
+	streamWriter.humanReadable = self.humanReadable;
+    streamWriter.delegate = self;
+
+	BOOL ok = NO;
+	if ([object isKindOfClass:[NSDictionary class]])
+		ok = [streamWriter writeObject:object];
+
+	else if ([object isKindOfClass:[NSArray class]])
+		ok = [streamWriter writeArray:object];
+
+	else if ([object respondsToSelector:@selector(proxyForJson)])
+		return [self dataWithObject:[object proxyForJson]];
+	else {
+		self.error = @"Not valid type for JSON";
+		return nil;
+	}
+
+	if (ok)
+		return self.acc;
+
+	self.error = streamWriter.error;
+	return nil;
+}
+
+#pragma mark SBJson4StreamWriterDelegate
+
+- (void)writer:(SBJson4StreamWriter *)writer appendBytes:(const void *)bytes length:(NSUInteger)length {
+    [self.acc appendBytes:bytes length:length];
+}
+
+
+
+@end

+ 22 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnelTests/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 39 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnelTests/PsiphonTunnelTests.m

@@ -0,0 +1,39 @@
+//
+//  PsiphonTunnelTests.m
+//  PsiphonTunnelTests
+//
+//  Created by Adam Pritchard on 2016-10-06.
+//  Copyright © 2016 Psiphon Inc. All rights reserved.
+//
+
+#import <XCTest/XCTest.h>
+
+@interface PsiphonTunnelTests : XCTestCase
+
+@end
+
+@implementation PsiphonTunnelTests
+
+- (void)setUp {
+    [super setUp];
+    // Put setup code here. This method is called before the invocation of each test method in the class.
+}
+
+- (void)tearDown {
+    // Put teardown code here. This method is called after the invocation of each test method in the class.
+    [super tearDown];
+}
+
+- (void)testExample {
+    // This is an example of a functional test case.
+    // Use XCTAssert and related functions to verify your tests produce the correct results.
+}
+
+- (void)testPerformanceExample {
+    // This is an example of a performance test case.
+    [self measureBlock:^{
+        // Put the code you want to measure the time of here.
+    }];
+}
+
+@end

+ 0 - 3
MobileLibrary/iOS/PsiphonTunnelController/.gitignore

@@ -1,3 +0,0 @@
-project.xcworkspace
-xcuserdata
-build

+ 0 - 349
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/project.pbxproj

@@ -1,349 +0,0 @@
-// !$*UTF8*$!
-{
-	archiveVersion = 1;
-	classes = {
-	};
-	objectVersion = 46;
-	objects = {
-
-/* Begin PBXBuildFile section */
-		4445243B1D8B356E00AF7B5B /* Psi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4445243A1D8B356E00AF7B5B /* Psi.framework */; };
-		4445243C1D8B357500AF7B5B /* Psi.framework in Resources */ = {isa = PBXBuildFile; fileRef = 4445243A1D8B356E00AF7B5B /* Psi.framework */; };
-		44980AC21D63A3B300B78274 /* PsiphonTunnelController.h in Headers */ = {isa = PBXBuildFile; fileRef = 44980AC01D63A3B300B78274 /* PsiphonTunnelController.h */; settings = {ATTRIBUTES = (Public, ); }; };
-		44980ACB1D63A3EA00B78274 /* PsiphonTunnelController.m in Sources */ = {isa = PBXBuildFile; fileRef = 44980AC81D63A3EA00B78274 /* PsiphonTunnelController.m */; };
-		44980ACC1D63A3EA00B78274 /* Reachability.h in Headers */ = {isa = PBXBuildFile; fileRef = 44980AC91D63A3EA00B78274 /* Reachability.h */; settings = {ATTRIBUTES = (Public, ); }; };
-		44980ACD1D63A3EA00B78274 /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 44980ACA1D63A3EA00B78274 /* Reachability.m */; };
-		44CC93D61D6CBD740082F743 /* rootCAs.txt in Resources */ = {isa = PBXBuildFile; fileRef = 44CC93D51D6CBD740082F743 /* rootCAs.txt */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXFileReference section */
-		4445243A1D8B356E00AF7B5B /* Psi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Psi.framework; path = PsiphonTunnelController/Psi.framework; sourceTree = "<group>"; };
-		44980ABD1D63A3B300B78274 /* PsiphonTunnelController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PsiphonTunnelController.framework; sourceTree = BUILT_PRODUCTS_DIR; };
-		44980AC01D63A3B300B78274 /* PsiphonTunnelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PsiphonTunnelController.h; sourceTree = "<group>"; };
-		44980AC11D63A3B300B78274 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-		44980AC81D63A3EA00B78274 /* PsiphonTunnelController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PsiphonTunnelController.m; sourceTree = "<group>"; };
-		44980AC91D63A3EA00B78274 /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Reachability.h; sourceTree = "<group>"; };
-		44980ACA1D63A3EA00B78274 /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Reachability.m; sourceTree = "<group>"; };
-		44CC93D51D6CBD740082F743 /* rootCAs.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = rootCAs.txt; path = PsiphonTunnelController/rootCAs.txt; sourceTree = "<group>"; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
-		44980AB91D63A3B300B78274 /* Frameworks */ = {
-			isa = PBXFrameworksBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				4445243B1D8B356E00AF7B5B /* Psi.framework in Frameworks */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
-		44980AB31D63A3B300B78274 = {
-			isa = PBXGroup;
-			children = (
-				4445243A1D8B356E00AF7B5B /* Psi.framework */,
-				44CC93D41D6CBD260082F743 /* Resources */,
-				44980ABF1D63A3B300B78274 /* PsiphonTunnelController */,
-				44980ABE1D63A3B300B78274 /* Products */,
-			);
-			sourceTree = "<group>";
-		};
-		44980ABE1D63A3B300B78274 /* Products */ = {
-			isa = PBXGroup;
-			children = (
-				44980ABD1D63A3B300B78274 /* PsiphonTunnelController.framework */,
-			);
-			name = Products;
-			sourceTree = "<group>";
-		};
-		44980ABF1D63A3B300B78274 /* PsiphonTunnelController */ = {
-			isa = PBXGroup;
-			children = (
-				44980AC81D63A3EA00B78274 /* PsiphonTunnelController.m */,
-				44980AC91D63A3EA00B78274 /* Reachability.h */,
-				44980ACA1D63A3EA00B78274 /* Reachability.m */,
-				44980AC01D63A3B300B78274 /* PsiphonTunnelController.h */,
-				44980AC11D63A3B300B78274 /* Info.plist */,
-			);
-			path = PsiphonTunnelController;
-			sourceTree = "<group>";
-		};
-		44CC93D41D6CBD260082F743 /* Resources */ = {
-			isa = PBXGroup;
-			children = (
-				44CC93D51D6CBD740082F743 /* rootCAs.txt */,
-			);
-			name = Resources;
-			sourceTree = "<group>";
-		};
-/* End PBXGroup section */
-
-/* Begin PBXHeadersBuildPhase section */
-		44980ABA1D63A3B300B78274 /* Headers */ = {
-			isa = PBXHeadersBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				44980AC21D63A3B300B78274 /* PsiphonTunnelController.h in Headers */,
-				44980ACC1D63A3EA00B78274 /* Reachability.h in Headers */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXHeadersBuildPhase section */
-
-/* Begin PBXNativeTarget section */
-		44980ABC1D63A3B300B78274 /* PsiphonTunnelController */ = {
-			isa = PBXNativeTarget;
-			buildConfigurationList = 44980AC51D63A3B300B78274 /* Build configuration list for PBXNativeTarget "PsiphonTunnelController" */;
-			buildPhases = (
-				44980AB81D63A3B300B78274 /* Sources */,
-				44980AB91D63A3B300B78274 /* Frameworks */,
-				44980ABA1D63A3B300B78274 /* Headers */,
-				44980ABB1D63A3B300B78274 /* Resources */,
-			);
-			buildRules = (
-			);
-			dependencies = (
-			);
-			name = PsiphonTunnelController;
-			productName = PsiphonTunnelController;
-			productReference = 44980ABD1D63A3B300B78274 /* PsiphonTunnelController.framework */;
-			productType = "com.apple.product-type.framework";
-		};
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
-		44980AB41D63A3B300B78274 /* Project object */ = {
-			isa = PBXProject;
-			attributes = {
-				LastUpgradeCheck = 0800;
-				ORGANIZATIONNAME = "Psiphon Inc.";
-				TargetAttributes = {
-					44980ABC1D63A3B300B78274 = {
-						CreatedOnToolsVersion = 8.0;
-						ProvisioningStyle = Manual;
-					};
-				};
-			};
-			buildConfigurationList = 44980AB71D63A3B300B78274 /* Build configuration list for PBXProject "PsiphonTunnelController" */;
-			compatibilityVersion = "Xcode 3.2";
-			developmentRegion = English;
-			hasScannedForEncodings = 0;
-			knownRegions = (
-				en,
-			);
-			mainGroup = 44980AB31D63A3B300B78274;
-			productRefGroup = 44980ABE1D63A3B300B78274 /* Products */;
-			projectDirPath = "";
-			projectRoot = "";
-			targets = (
-				44980ABC1D63A3B300B78274 /* PsiphonTunnelController */,
-			);
-		};
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
-		44980ABB1D63A3B300B78274 /* Resources */ = {
-			isa = PBXResourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				4445243C1D8B357500AF7B5B /* Psi.framework in Resources */,
-				44CC93D61D6CBD740082F743 /* rootCAs.txt in Resources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
-		44980AB81D63A3B300B78274 /* Sources */ = {
-			isa = PBXSourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				44980ACD1D63A3EA00B78274 /* Reachability.m in Sources */,
-				44980ACB1D63A3EA00B78274 /* PsiphonTunnelController.m in Sources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXSourcesBuildPhase section */
-
-/* Begin XCBuildConfiguration section */
-		44980AC31D63A3B300B78274 /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_SUSPICIOUS_MOVES = YES;
-				CLANG_WARN_UNREACHABLE_CODE = YES;
-				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
-				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 1;
-				DEBUG_INFORMATION_FORMAT = dwarf;
-				ENABLE_BITCODE = NO;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				ENABLE_TESTABILITY = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu99;
-				GCC_DYNAMIC_NO_PIC = NO;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_OPTIMIZATION_LEVEL = 0;
-				GCC_PREPROCESSOR_DEFINITIONS = (
-					"DEBUG=1",
-					"$(inherited)",
-				);
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNDECLARED_SELECTOR = YES;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
-				MTL_ENABLE_DEBUG_INFO = YES;
-				ONLY_ACTIVE_ARCH = YES;
-				OTHER_LDFLAGS = (
-					"-read_only_relocs",
-					suppress,
-				);
-				SDKROOT = iphoneos;
-				TARGETED_DEVICE_FAMILY = "1,2";
-				VERSIONING_SYSTEM = "apple-generic";
-				VERSION_INFO_PREFIX = "";
-			};
-			name = Debug;
-		};
-		44980AC41D63A3B300B78274 /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_SUSPICIOUS_MOVES = YES;
-				CLANG_WARN_UNREACHABLE_CODE = YES;
-				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
-				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 1;
-				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
-				ENABLE_BITCODE = NO;
-				ENABLE_NS_ASSERTIONS = NO;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu99;
-				GCC_DYNAMIC_NO_PIC = NO;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNDECLARED_SELECTOR = YES;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
-				MTL_ENABLE_DEBUG_INFO = NO;
-				OTHER_LDFLAGS = (
-					"-read_only_relocs",
-					suppress,
-				);
-				SDKROOT = iphoneos;
-				TARGETED_DEVICE_FAMILY = "1,2";
-				VALIDATE_PRODUCT = YES;
-				VERSIONING_SYSTEM = "apple-generic";
-				VERSION_INFO_PREFIX = "";
-			};
-			name = Release;
-		};
-		44980AC61D63A3B300B78274 /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ARCHS = "$(ARCHS_STANDARD)";
-				CODE_SIGN_IDENTITY = "";
-				DEFINES_MODULE = NO;
-				DEVELOPMENT_TEAM = "";
-				DYLIB_COMPATIBILITY_VERSION = 1;
-				DYLIB_CURRENT_VERSION = 1;
-				DYLIB_INSTALL_NAME_BASE = "@rpath";
-				ENABLE_BITCODE = NO;
-				FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/PsiphonTunnelController";
-				INFOPLIST_FILE = PsiphonTunnelController/Info.plist;
-				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
-				ONLY_ACTIVE_ARCH = NO;
-				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon.PsiphonTunnelController;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-				SKIP_INSTALL = YES;
-				TARGETED_DEVICE_FAMILY = "1,2";
-				VALID_ARCHS = "arm64 armv7 armv7s";
-			};
-			name = Debug;
-		};
-		44980AC71D63A3B300B78274 /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ARCHS = "$(ARCHS_STANDARD)";
-				CODE_SIGN_IDENTITY = "";
-				DEFINES_MODULE = NO;
-				DEVELOPMENT_TEAM = "";
-				DYLIB_COMPATIBILITY_VERSION = 1;
-				DYLIB_CURRENT_VERSION = 1;
-				DYLIB_INSTALL_NAME_BASE = "@rpath";
-				ENABLE_BITCODE = NO;
-				FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/PsiphonTunnelController";
-				INFOPLIST_FILE = PsiphonTunnelController/Info.plist;
-				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
-				ONLY_ACTIVE_ARCH = NO;
-				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon.PsiphonTunnelController;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-				SKIP_INSTALL = YES;
-				TARGETED_DEVICE_FAMILY = "1,2";
-				VALID_ARCHS = "arm64 armv7 armv7s";
-			};
-			name = Release;
-		};
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
-		44980AB71D63A3B300B78274 /* Build configuration list for PBXProject "PsiphonTunnelController" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				44980AC31D63A3B300B78274 /* Debug */,
-				44980AC41D63A3B300B78274 /* Release */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-		44980AC51D63A3B300B78274 /* Build configuration list for PBXNativeTarget "PsiphonTunnelController" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				44980AC61D63A3B300B78274 /* Debug */,
-				44980AC71D63A3B300B78274 /* Release */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-/* End XCConfigurationList section */
-	};
-	rootObject = 44980AB41D63A3B300B78274 /* Project object */;
-}

+ 0 - 114
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/project.xcworkspace/xcshareddata/PsiphonTunnelController.xcscmblueprint

@@ -1,114 +0,0 @@
-{
-  "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "654BDFA72D0BD5C4AAA375C3F6D7EA32AC58A304+++F92F6D3",
-  "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : {
-
-  },
-  "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : {
-    "8B61C8B48DB4B96AD94F67018EFB8A9684F3EFD6+++DCC608E" : 9223372036854775807,
-    "654BDFA72D0BD5C4AAA375C3F6D7EA32AC58A304+++38A1A30" : 9223372036854775807,
-    "A5AD174D8B9671567C38590D166034173C866E1D+++C7AE692" : 9223372036854775807,
-    "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++ED25EBA" : 9223372036854775807,
-    "3CDC0364FAC963F881E15357C59A43097543A382+++74D281F" : 9223372036854775807,
-    "654BDFA72D0BD5C4AAA375C3F6D7EA32AC58A304+++F92F6D3" : 9223372036854775807,
-    "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++D9DC38C" : 9223372036854775807,
-    "D059B056F0F8D707428CFA53B26B54CB05124896+++41B0777" : 9223372036854775807,
-    "C811301B9BA5BED994AD79D58835794EFE2C4BAD+++5A1EDEB" : 9223372036854775807,
-    "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++739CA66" : 9223372036854775807,
-    "A5AD174D8B9671567C38590D166034173C866E1D+++795FDF9" : 9223372036854775807,
-    "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++0719F03" : 9223372036854775807,
-    "0EDA2F64A27382CC2BF1F85AB9A4AEECDF2AEE03+++14C946E" : 9223372036854775807,
-    "0559510868EA25C63F4143C7E150BF185B7A091F+++F4FA2CA" : 9223372036854775807
-  },
-  "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "7D3F11EA-BDE9-4C77-841B-858842838E65",
-  "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : {
-    "8B61C8B48DB4B96AD94F67018EFB8A9684F3EFD6+++DCC608E" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/github.com\/Psiphon-Inc\/bolt\/",
-    "654BDFA72D0BD5C4AAA375C3F6D7EA32AC58A304+++38A1A30" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/github.com\/Psiphon-Labs\/psiphon-tunnel-core\/",
-    "A5AD174D8B9671567C38590D166034173C866E1D+++C7AE692" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/github.com\/spacemonkeygo\/openssl\/",
-    "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++ED25EBA" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/golang.org\/x\/net\/",
-    "3CDC0364FAC963F881E15357C59A43097543A382+++74D281F" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/github.com\/Psiphon-Inc\/dns\/",
-    "654BDFA72D0BD5C4AAA375C3F6D7EA32AC58A304+++F92F6D3" : "psiphon-tunnel-core\/",
-    "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++D9DC38C" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/golang.org\/x\/net\/",
-    "D059B056F0F8D707428CFA53B26B54CB05124896+++41B0777" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/github.com\/Psiphon-Inc\/goptlib\/",
-    "C811301B9BA5BED994AD79D58835794EFE2C4BAD+++5A1EDEB" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/github.com\/Psiphon-Inc\/goregen\/",
-    "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++739CA66" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/golang.org\/x\/net\/",
-    "A5AD174D8B9671567C38590D166034173C866E1D+++795FDF9" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/github.com\/spacemonkeygo\/openssl\/",
-    "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++0719F03" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/golang.org\/x\/net\/",
-    "0EDA2F64A27382CC2BF1F85AB9A4AEECDF2AEE03+++14C946E" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/github.com\/Psiphon-Inc\/ratelimit\/",
-    "0559510868EA25C63F4143C7E150BF185B7A091F+++F4FA2CA" : "psiphon-tunnel-core\/MobileLibrary\/iOS\/go-ios-build\/src\/github.com\/spacemonkeygo\/spacelog\/"
-  },
-  "DVTSourceControlWorkspaceBlueprintNameKey" : "PsiphonTunnelController",
-  "DVTSourceControlWorkspaceBlueprintVersion" : 204,
-  "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "MobileLibrary\/iOS\/PsiphonTunnelController\/PsiphonTunnelController.xcodeproj",
-  "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/spacemonkeygo\/spacelog",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0559510868EA25C63F4143C7E150BF185B7A091F+++F4FA2CA"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Psiphon-Inc\/ratelimit",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0EDA2F64A27382CC2BF1F85AB9A4AEECDF2AEE03+++14C946E"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/go.googlesource.com\/net",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++0719F03"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/go.googlesource.com\/crypto",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++739CA66"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/go.googlesource.com\/mobile",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++D9DC38C"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Psiphon-Inc\/crypto",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "18F8E8F32D4612669B2AC73CE391ED54473F75DA+++ED25EBA"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Psiphon-Inc\/dns",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "3CDC0364FAC963F881E15357C59A43097543A382+++74D281F"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:efryntov\/psiphon-tunnel-core.git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "654BDFA72D0BD5C4AAA375C3F6D7EA32AC58A304+++38A1A30"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:efryntov\/psiphon-tunnel-core.git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "654BDFA72D0BD5C4AAA375C3F6D7EA32AC58A304+++F92F6D3"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Psiphon-Inc\/bolt",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "8B61C8B48DB4B96AD94F67018EFB8A9684F3EFD6+++DCC608E"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/spacemonkeygo\/openssl",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "A5AD174D8B9671567C38590D166034173C866E1D+++795FDF9"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Psiphon-Inc\/openssl",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "A5AD174D8B9671567C38590D166034173C866E1D+++C7AE692"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Psiphon-Inc\/goregen",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "C811301B9BA5BED994AD79D58835794EFE2C4BAD+++5A1EDEB"
-    },
-    {
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Psiphon-Inc\/goptlib",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
-      "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "D059B056F0F8D707428CFA53B26B54CB05124896+++41B0777"
-    }
-  ]
-}

+ 0 - 22
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/project.xcworkspace/xcuserdata/eugene.xcuserdatad/WorkspaceSettings.xcsettings

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>BuildLocationStyle</key>
-	<string>CustomLocation</string>
-	<key>CustomBuildIntermediatesPath</key>
-	<string>Build/Intermediates</string>
-	<key>CustomBuildLocationType</key>
-	<string>RelativeToWorkspace</string>
-	<key>CustomBuildProductsPath</key>
-	<string>Build/Products</string>
-	<key>DerivedDataCustomLocation</key>
-	<string>/Users/eugene/Library/Developer/Xcode/DerivedData</string>
-	<key>DerivedDataLocationStyle</key>
-	<string>AbsolutePath</string>
-	<key>IssueFilterStyle</key>
-	<string>ShowActiveSchemeOnly</string>
-	<key>LiveSourceIssuesEnabled</key>
-	<true/>
-</dict>
-</plist>

+ 0 - 80
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/xcuserdata/eugene.xcuserdatad/xcschemes/PsiphonTunnelController.xcscheme

@@ -1,80 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Scheme
-   LastUpgradeVersion = "0800"
-   version = "1.3">
-   <BuildAction
-      parallelizeBuildables = "YES"
-      buildImplicitDependencies = "YES">
-      <BuildActionEntries>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "44980ABC1D63A3B300B78274"
-               BuildableName = "PsiphonTunnelController.framework"
-               BlueprintName = "PsiphonTunnelController"
-               ReferencedContainer = "container:PsiphonTunnelController.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-      </BuildActionEntries>
-   </BuildAction>
-   <TestAction
-      buildConfiguration = "Debug"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      shouldUseLaunchSchemeArgsEnv = "YES">
-      <Testables>
-      </Testables>
-      <AdditionalOptions>
-      </AdditionalOptions>
-   </TestAction>
-   <LaunchAction
-      buildConfiguration = "Release"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      launchStyle = "0"
-      useCustomWorkingDirectory = "NO"
-      ignoresPersistentStateOnLaunch = "NO"
-      debugDocumentVersioning = "YES"
-      debugServiceExtension = "internal"
-      allowLocationSimulation = "YES">
-      <MacroExpansion>
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "44980ABC1D63A3B300B78274"
-            BuildableName = "PsiphonTunnelController.framework"
-            BlueprintName = "PsiphonTunnelController"
-            ReferencedContainer = "container:PsiphonTunnelController.xcodeproj">
-         </BuildableReference>
-      </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
-   </LaunchAction>
-   <ProfileAction
-      buildConfiguration = "Release"
-      shouldUseLaunchSchemeArgsEnv = "YES"
-      savedToolIdentifier = ""
-      useCustomWorkingDirectory = "NO"
-      debugDocumentVersioning = "YES">
-      <MacroExpansion>
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "44980ABC1D63A3B300B78274"
-            BuildableName = "PsiphonTunnelController.framework"
-            BlueprintName = "PsiphonTunnelController"
-            ReferencedContainer = "container:PsiphonTunnelController.xcodeproj">
-         </BuildableReference>
-      </MacroExpansion>
-   </ProfileAction>
-   <AnalyzeAction
-      buildConfiguration = "Debug">
-   </AnalyzeAction>
-   <ArchiveAction
-      buildConfiguration = "Release"
-      revealArchiveInOrganizer = "YES">
-   </ArchiveAction>
-</Scheme>

+ 0 - 22
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/xcuserdata/eugene.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>SchemeUserState</key>
-	<dict>
-		<key>PsiphonTunnelController.xcscheme</key>
-		<dict>
-			<key>orderHint</key>
-			<integer>0</integer>
-		</dict>
-	</dict>
-	<key>SuppressBuildableAutocreation</key>
-	<dict>
-		<key>44980ABC1D63A3B300B78274</key>
-		<dict>
-			<key>primary</key>
-			<true/>
-		</dict>
-	</dict>
-</dict>
-</plist>

+ 0 - 2
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController/.gitignore

@@ -1,2 +0,0 @@
-Psi.framework
-rootCAs.txt

+ 0 - 56
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController/PsiphonTunnelController.h

@@ -1,56 +0,0 @@
-//
-//  PsiphonMobile.h
-//  PsiphonMobile
-//
-//  Created by eugene-imac on 2016-08-16.
-//  Copyright © 2016 Psiphon Inc. All rights reserved.
-//
-
-
-//! Project version number for PsiphonMobile.
-FOUNDATION_EXPORT double PsiphonMobileVersionNumber;
-
-//! Project version string for PsiphonMobile.
-FOUNDATION_EXPORT const unsigned char PsiphonMobileVersionString[];
-
-// In this header, you should import all the public headers of your framework using statements like #import <PsiphonMobile/PublicHeader.h>
-
-
-@protocol TunneledAppProtocol
-- (NSString *) getPsiphonConfig;
-- (void) onDiagnosticMessage: (NSString *) message;
-- (void) onAvailableEgressRegions: (NSArray *) regions;
-- (void) onSocksProxyPortInUse: (NSInteger) port;
-- (void) onHttpProxyPortInUse: (NSInteger) port;
-- (void) onListeningSocksProxyPort: (NSInteger) port;
-- (void) onListeningHttpProxyPort: (NSInteger) port;
-- (void) onUpstreamProxyError: (NSString *) message;
-- (void) onConnecting;
-- (void) onConnected;
-- (void) onHomepage: (NSString *) url;
-- (void) onClientRegion: (NSString *) region;
-- (void) onClientUpgradeDownloaded: (NSString *) filename;
-- (void) onSplitTunnelRegion: (NSString *) region;
-- (void) onUntunneledAddress: (NSString *) address;
-- (void) onBytesTransferred: (long) sent : (long) received;
-- (void) onStartedWaitingForNetworkConnectivity;
-@end
-
-
-@interface PsiphonTunnelController : NSObject
-
-@property (weak) id <TunneledAppProtocol> tunneledAppProtocolDelegate;
-@property (nonatomic) NSInteger listeningSocksProxyPort;
-@property (nonatomic) NSArray *homepages;
-
-
-+ (id) sharedInstance;
-
--(void) startTunnel;
--(void) stopTunnel;
-
-@end
-
-@interface Psi : NSObject
-+ (void)sendFeedback:(NSString*)configJson diagnostics: (NSString*)diagnosticsJson b64EncodedPublicKey: (NSString*) b64EncodedPublicKey uploadServer: (NSString*)uploadServer uploadPath: (NSString*) uploadPath uploadServerHeaders: (NSString*)uploadServerHeaders;
-@end

+ 0 - 155
MobileLibrary/iOS/PsiphonTunnelController/PsiphonTunnelController/PsiphonTunnelController.m

@@ -1,155 +0,0 @@
-//
-//  PsiphonMobile.m
-//  PsiphonMobile
-//
-//  Created by eugene-imac on 2016-08-16.
-//  Copyright © 2016 Psiphon Inc. All rights reserved.
-//
-
-#import <Psi/Psi.h>
-#import "PsiphonTunnelController.h"
-#import "Reachability.h"
-
-@interface PsiphonTunnelController () <GoPsiPsiphonProvider>
-@end
-
-@implementation PsiphonTunnelController
-
-
-+(PsiphonTunnelController *) sharedInstance {
-    static PsiphonTunnelController *sharedInstance = nil;
-    static dispatch_once_t onceToken = 0;
-    dispatch_once(&onceToken, ^{
-        sharedInstance = [[self alloc] init];
-        // Do any other initialisation stuff here
-    });
-    return sharedInstance;
-}
-
--(void) startTunnel {
-    [self stopTunnel];
-    [[self tunneledAppProtocolDelegate] onDiagnosticMessage:@"starting Psiphon library"];
-    
-    @try {
-        NSString *configStr = [[self tunneledAppProtocolDelegate] getPsiphonConfig];
-        NSError *e = nil;
-        
-        GoPsiStart(
-                   configStr,
-                   @"",
-                   self,
-                   false, // useDeviceBinder
-                   &e);
-    }
-    @catch(NSException *exception) {
-        [[self tunneledAppProtocolDelegate] onDiagnosticMessage:[NSString stringWithFormat: @"failed to start Psiphon library: %@", exception.reason]];
-    }
-    [[self tunneledAppProtocolDelegate] onDiagnosticMessage:@"Psiphon library started"];
-}
-
--(void) stopTunnel {
-    [[self tunneledAppProtocolDelegate] onDiagnosticMessage: @"stopping Psiphon library"];
-    GoPsiStop();
-    [[self tunneledAppProtocolDelegate] onDiagnosticMessage: @"Psiphon library stop"];
-    
-}
-
-#pragma mark - GoPsiphonProvider protocol implementation
-
-- (NSString*)getPrimaryDnsServer {
-    return @"8.8.8.8";
-}
-
-- (NSString*)getSecondaryDnsServer {
-    return @"8.8.4.4";    
-}
-
-
-
-
-- (BOOL)bindToDevice:(long)fileDescriptor error:(NSError**)error {
-    return TRUE;
-}
-
-- (NSString*)getDnsServer {
-    //This method is used only in VPN mode
-    return @"";
-}
-
-- (long)hasNetworkConnectivity {
-    Reachability *reachability = [Reachability reachabilityForInternetConnection];
-    NetworkStatus netstat = [reachability currentReachabilityStatus];
-    return (int) netstat != NotReachable;
-}
-
-- (void)notice:(NSString*)noticeJSON {
-    NSData *noticeData = [noticeJSON dataUsingEncoding:NSUTF8StringEncoding];
-    NSError *error = nil;
-    BOOL diagnostic = TRUE;
-    
-    NSDictionary *notice = [NSJSONSerialization JSONObjectWithData:noticeData options:kNilOptions error:&error];
-    
-    if(error) {
-        
-        // TODO: handle JSON error
-        
-    }
-    else {
-        NSString *noticeType = [notice valueForKey:@"noticeType"];
-        if ([noticeType isEqualToString:@"Tunnels"]) {
-            NSInteger count = [[[notice valueForKey: @"data"] valueForKey:@"count"] integerValue];
-            if (count > 0) {
-                [self.tunneledAppProtocolDelegate onConnected];
-            } else {
-                [self.tunneledAppProtocolDelegate onConnecting];
-            }
-            
-        } else if ([noticeType isEqualToString:@"AvailableEgressRegions"]) {
-            NSArray *regions = [[notice valueForKey: @"data"] valueForKey:@"regions"];
-            [self.tunneledAppProtocolDelegate onAvailableEgressRegions:regions];
-        } else if ([noticeType isEqualToString:@"SocksProxyPortInUse"]) {
-            NSInteger port = [(NSNumber*)[[notice valueForKey: @"data"] valueForKey:@"port"] integerValue];
-            [self.tunneledAppProtocolDelegate onSocksProxyPortInUse:port];
-        } else if ([noticeType isEqualToString:@"HttpProxyPortInUse"]) {
-            NSInteger port = [(NSNumber*)[[notice valueForKey: @"data"] valueForKey:@"port"] integerValue];
-            [self.tunneledAppProtocolDelegate onHttpProxyPortInUse:port];
-        } else if ([noticeType isEqualToString:@"ListeningSocksProxyPort"]) {
-            NSInteger port = [(NSNumber*)[[notice valueForKey: @"data"] valueForKey:@"port"] integerValue];
-            [self.tunneledAppProtocolDelegate onListeningSocksProxyPort:port];
-        } else if ([noticeType isEqualToString:@"ListeningHttpProxyPort"]) {
-            NSInteger port = [(NSNumber*)[[notice valueForKey: @"data"] valueForKey:@"port"] integerValue];
-            [self.tunneledAppProtocolDelegate onListeningHttpProxyPort:port];
-        } else if ([noticeType isEqualToString:@"UpstreamProxyError"]) {
-            [self.tunneledAppProtocolDelegate onUpstreamProxyError:[[notice valueForKey: @"data"] valueForKey:@"message"]];
-        } else if ([noticeType isEqualToString:@"ClientUpgradeDownloaded"]) {
-            [self.tunneledAppProtocolDelegate onClientUpgradeDownloaded:[[notice valueForKey: @"data"] valueForKey:@"filename"]];
-        } else if ([noticeType isEqualToString:@"Homepage"]) {
-            [self.tunneledAppProtocolDelegate onHomepage:[[notice valueForKey: @"data"] valueForKey:@"url"]];
-        } else if ([noticeType isEqualToString:@"ClientRegion"]) {
-            [self.tunneledAppProtocolDelegate onClientRegion:[[notice valueForKey: @"data"] valueForKey:@"region"]];
-        } else if ([noticeType isEqualToString:@"UntunneledAddress"]) {
-            [self.tunneledAppProtocolDelegate onUntunneledAddress :[[notice valueForKey: @"data"] valueForKey:@"address"]];
-        } else if ([noticeType isEqualToString:@"BytesTransferred"]) {
-            diagnostic = FALSE;
-            NSDictionary *bytes = [notice valueForKey: @"data"];
-            [self.tunneledAppProtocolDelegate onBytesTransferred:[bytes[@"received"] longValue]:[bytes[@"sent"] longValue]];
-        }
-        
-        if (diagnostic) {
-            NSData *diagnosticData = [NSJSONSerialization dataWithJSONObject:[notice valueForKey: @"data"] options:kNilOptions error:&error];
-            if (error == nil){
-                NSString *diagnosticStr = [[NSString alloc] initWithData:diagnosticData encoding:NSUTF8StringEncoding];
-                NSString *diagnosticMessage = [NSString stringWithFormat:@"%@: %@", noticeType, diagnosticStr];
-                [self. tunneledAppProtocolDelegate onDiagnosticMessage : diagnosticMessage];
-            }
-        }
-    }
-}
-
-@end
-
-@implementation Psi
-+ (void)sendFeedback:(NSString*)configJson diagnostics: (NSString*)diagnosticsJson b64EncodedPublicKey: (NSString*) b64EncodedPublicKey uploadServer: (NSString*)uploadServer uploadPath: (NSString*) uploadPath uploadServerHeaders: (NSString*)uploadServerHeaders {
-    GoPsiSendFeedback(configJson, diagnosticsJson, b64EncodedPublicKey, uploadServer, uploadPath, uploadServerHeaders);
-}
-@end

+ 7 - 28
MobileLibrary/iOS/README.md

@@ -1,42 +1,21 @@
-##Psiphon iOS Library README
+# Psiphon iOS Library Meta-README
 
-###Overview
+## Usage
 
-Psiphon Library for iOS enables you to easily embed Psiphon in your iOS
-app. The Psiphon Library for iOS is implemented in Go and follows the standard
-conventions for using a Go library in an iOS app.
+If you are using the Library in your app, please read the [USAGE.md](USAGE.md) instructions.
 
-###Building
-
-####Prerequisites
-
-* xcode `xcode-select --install`
-* [git](https://git-scm.com/download/mac)
-* homebrew
-  * Install from terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
-* golang 
-  * Install from terminal: `brew install go`
-
-####Build Steps
-
-* run `build-psiphon-framework.sh`
-
-###Using the Library and Sample Apps
-
-Coming soon
-
-###Acknowledgements
+## Acknowledgements
 
 Psiphon iOS Library uses:
 * [OpenSSL-for-iPhone](https://github.com/x2on/OpenSSL-for-iPhone)
 
-####OpenSSL-for-iPhone Changes
+### OpenSSL-for-iPhone Changes
 
-`build-libssl.sh` rebuilds openssl on every run.  Modifications were made to 
+`build-libssl.sh` rebuilds openssl on every run.  Modifications were made to
 not run unless required, they are:
 
 * Check if `libssl.a` and `libcrypto.a` are built and compare the version strings
 found in files to the `VERSION` variable in `build-libssl.sh`.
 
-* A new variable `FORCE_BUILD` is set to force a build.  Set this to *true* as 
+* A new variable `FORCE_BUILD` is set to force a build.  Set this to *true* as
 necessary.

+ 579 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest.xcodeproj/project.pbxproj

@@ -0,0 +1,579 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		662658EE1DCB8CF300872F6C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662658ED1DCB8CF300872F6C /* AppDelegate.swift */; };
+		662658F01DCB8CF300872F6C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662658EF1DCB8CF300872F6C /* ViewController.swift */; };
+		662658F31DCB8CF300872F6C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 662658F11DCB8CF300872F6C /* Main.storyboard */; };
+		662658F51DCB8CF300872F6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 662658F41DCB8CF300872F6C /* Assets.xcassets */; };
+		662658F81DCB8CF300872F6C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 662658F61DCB8CF300872F6C /* LaunchScreen.storyboard */; };
+		662659031DCB8CF400872F6C /* TunneledWebRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662659021DCB8CF400872F6C /* TunneledWebRequestTests.swift */; };
+		6626590E1DCB8CF400872F6C /* TunneledWebRequestUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6626590D1DCB8CF400872F6C /* TunneledWebRequestUITests.swift */; };
+		662659211DCBC7C300872F6C /* PsiphonTunnel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 662659201DCBC7C300872F6C /* PsiphonTunnel.framework */; };
+		662659231DCBC8D800872F6C /* PsiphonTunnel.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 662659201DCBC7C300872F6C /* PsiphonTunnel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		6688DBB61DCD684B00721A9E /* psiphon-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 6688DBB51DCD684B00721A9E /* psiphon-config.json */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		662658FF1DCB8CF400872F6C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 662658E21DCB8CF300872F6C /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 662658E91DCB8CF300872F6C;
+			remoteInfo = TunneledWebRequest;
+		};
+		6626590A1DCB8CF400872F6C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 662658E21DCB8CF300872F6C /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 662658E91DCB8CF300872F6C;
+			remoteInfo = TunneledWebRequest;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		662659221DCBC8CB00872F6C /* CopyFiles */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+				662659231DCBC8D800872F6C /* PsiphonTunnel.framework in CopyFiles */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		662658EA1DCB8CF300872F6C /* TunneledWebRequest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TunneledWebRequest.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		662658ED1DCB8CF300872F6C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		662658EF1DCB8CF300872F6C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
+		662658F21DCB8CF300872F6C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		662658F41DCB8CF300872F6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		662658F71DCB8CF300872F6C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		662658F91DCB8CF300872F6C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		662658FE1DCB8CF400872F6C /* TunneledWebRequestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TunneledWebRequestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		662659021DCB8CF400872F6C /* TunneledWebRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunneledWebRequestTests.swift; sourceTree = "<group>"; };
+		662659041DCB8CF400872F6C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		662659091DCB8CF400872F6C /* TunneledWebRequestUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TunneledWebRequestUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		6626590D1DCB8CF400872F6C /* TunneledWebRequestUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunneledWebRequestUITests.swift; sourceTree = "<group>"; };
+		6626590F1DCB8CF400872F6C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		662659201DCBC7C300872F6C /* PsiphonTunnel.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = PsiphonTunnel.framework; sourceTree = "<group>"; };
+		6688DBB51DCD684B00721A9E /* psiphon-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "psiphon-config.json"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		662658E71DCB8CF300872F6C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				662659211DCBC7C300872F6C /* PsiphonTunnel.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662658FB1DCB8CF400872F6C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662659061DCB8CF400872F6C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		662658E11DCB8CF300872F6C = {
+			isa = PBXGroup;
+			children = (
+				662658EC1DCB8CF300872F6C /* TunneledWebRequest */,
+				662659011DCB8CF400872F6C /* TunneledWebRequestTests */,
+				6626590C1DCB8CF400872F6C /* TunneledWebRequestUITests */,
+				662658EB1DCB8CF300872F6C /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		662658EB1DCB8CF300872F6C /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				662658EA1DCB8CF300872F6C /* TunneledWebRequest.app */,
+				662658FE1DCB8CF400872F6C /* TunneledWebRequestTests.xctest */,
+				662659091DCB8CF400872F6C /* TunneledWebRequestUITests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		662658EC1DCB8CF300872F6C /* TunneledWebRequest */ = {
+			isa = PBXGroup;
+			children = (
+				662658ED1DCB8CF300872F6C /* AppDelegate.swift */,
+				662658EF1DCB8CF300872F6C /* ViewController.swift */,
+				662658F11DCB8CF300872F6C /* Main.storyboard */,
+				662658F41DCB8CF300872F6C /* Assets.xcassets */,
+				662658F61DCB8CF300872F6C /* LaunchScreen.storyboard */,
+				662658F91DCB8CF300872F6C /* Info.plist */,
+				6688DBB51DCD684B00721A9E /* psiphon-config.json */,
+				662659201DCBC7C300872F6C /* PsiphonTunnel.framework */,
+			);
+			path = TunneledWebRequest;
+			sourceTree = "<group>";
+		};
+		662659011DCB8CF400872F6C /* TunneledWebRequestTests */ = {
+			isa = PBXGroup;
+			children = (
+				662659021DCB8CF400872F6C /* TunneledWebRequestTests.swift */,
+				662659041DCB8CF400872F6C /* Info.plist */,
+			);
+			path = TunneledWebRequestTests;
+			sourceTree = "<group>";
+		};
+		6626590C1DCB8CF400872F6C /* TunneledWebRequestUITests */ = {
+			isa = PBXGroup;
+			children = (
+				6626590D1DCB8CF400872F6C /* TunneledWebRequestUITests.swift */,
+				6626590F1DCB8CF400872F6C /* Info.plist */,
+			);
+			path = TunneledWebRequestUITests;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		662658E91DCB8CF300872F6C /* TunneledWebRequest */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 662659121DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebRequest" */;
+			buildPhases = (
+				662658E61DCB8CF300872F6C /* Sources */,
+				662658E71DCB8CF300872F6C /* Frameworks */,
+				662658E81DCB8CF300872F6C /* Resources */,
+				662659221DCBC8CB00872F6C /* CopyFiles */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = TunneledWebRequest;
+			productName = TunneledWebRequest;
+			productReference = 662658EA1DCB8CF300872F6C /* TunneledWebRequest.app */;
+			productType = "com.apple.product-type.application";
+		};
+		662658FD1DCB8CF400872F6C /* TunneledWebRequestTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 662659151DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebRequestTests" */;
+			buildPhases = (
+				662658FA1DCB8CF400872F6C /* Sources */,
+				662658FB1DCB8CF400872F6C /* Frameworks */,
+				662658FC1DCB8CF400872F6C /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				662659001DCB8CF400872F6C /* PBXTargetDependency */,
+			);
+			name = TunneledWebRequestTests;
+			productName = TunneledWebRequestTests;
+			productReference = 662658FE1DCB8CF400872F6C /* TunneledWebRequestTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		662659081DCB8CF400872F6C /* TunneledWebRequestUITests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 662659181DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebRequestUITests" */;
+			buildPhases = (
+				662659051DCB8CF400872F6C /* Sources */,
+				662659061DCB8CF400872F6C /* Frameworks */,
+				662659071DCB8CF400872F6C /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				6626590B1DCB8CF400872F6C /* PBXTargetDependency */,
+			);
+			name = TunneledWebRequestUITests;
+			productName = TunneledWebRequestUITests;
+			productReference = 662659091DCB8CF400872F6C /* TunneledWebRequestUITests.xctest */;
+			productType = "com.apple.product-type.bundle.ui-testing";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		662658E21DCB8CF300872F6C /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastSwiftUpdateCheck = 0800;
+				LastUpgradeCheck = 0810;
+				ORGANIZATIONNAME = "Psiphon Inc.";
+				TargetAttributes = {
+					662658E91DCB8CF300872F6C = {
+						CreatedOnToolsVersion = 8.0;
+						DevelopmentTeam = Q6HLNEX92A;
+						ProvisioningStyle = Automatic;
+					};
+					662658FD1DCB8CF400872F6C = {
+						CreatedOnToolsVersion = 8.0;
+						DevelopmentTeam = Q6HLNEX92A;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 662658E91DCB8CF300872F6C;
+					};
+					662659081DCB8CF400872F6C = {
+						CreatedOnToolsVersion = 8.0;
+						DevelopmentTeam = Q6HLNEX92A;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 662658E91DCB8CF300872F6C;
+					};
+				};
+			};
+			buildConfigurationList = 662658E51DCB8CF300872F6C /* Build configuration list for PBXProject "TunneledWebRequest" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 662658E11DCB8CF300872F6C;
+			productRefGroup = 662658EB1DCB8CF300872F6C /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				662658E91DCB8CF300872F6C /* TunneledWebRequest */,
+				662658FD1DCB8CF400872F6C /* TunneledWebRequestTests */,
+				662659081DCB8CF400872F6C /* TunneledWebRequestUITests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		662658E81DCB8CF300872F6C /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				662658F81DCB8CF300872F6C /* LaunchScreen.storyboard in Resources */,
+				662658F51DCB8CF300872F6C /* Assets.xcassets in Resources */,
+				662658F31DCB8CF300872F6C /* Main.storyboard in Resources */,
+				6688DBB61DCD684B00721A9E /* psiphon-config.json in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662658FC1DCB8CF400872F6C /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662659071DCB8CF400872F6C /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		662658E61DCB8CF300872F6C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				662658F01DCB8CF300872F6C /* ViewController.swift in Sources */,
+				662658EE1DCB8CF300872F6C /* AppDelegate.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662658FA1DCB8CF400872F6C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				662659031DCB8CF400872F6C /* TunneledWebRequestTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662659051DCB8CF400872F6C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6626590E1DCB8CF400872F6C /* TunneledWebRequestUITests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		662659001DCB8CF400872F6C /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 662658E91DCB8CF300872F6C /* TunneledWebRequest */;
+			targetProxy = 662658FF1DCB8CF400872F6C /* PBXContainerItemProxy */;
+		};
+		6626590B1DCB8CF400872F6C /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 662658E91DCB8CF300872F6C /* TunneledWebRequest */;
+			targetProxy = 6626590A1DCB8CF400872F6C /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		662658F11DCB8CF300872F6C /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				662658F21DCB8CF300872F6C /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		662658F61DCB8CF300872F6C /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				662658F71DCB8CF300872F6C /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		662659101DCB8CF400872F6C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_SUSPICIOUS_MOVES = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_BITCODE = YES;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		662659111DCB8CF400872F6C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_SUSPICIOUS_MOVES = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_BITCODE = YES;
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		662659131DCB8CF400872F6C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/TunneledWebRequest",
+				);
+				INFOPLIST_FILE = TunneledWebRequest/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequest;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRIP_BITCODE_FROM_COPIED_FILES = NO;
+				SWIFT_VERSION = 3.0;
+			};
+			name = Debug;
+		};
+		662659141DCB8CF400872F6C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/TunneledWebRequest",
+				);
+				INFOPLIST_FILE = TunneledWebRequest/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequest;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRIP_BITCODE_FROM_COPIED_FILES = NO;
+				SWIFT_VERSION = 3.0;
+			};
+			name = Release;
+		};
+		662659161DCB8CF400872F6C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = TunneledWebRequestTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequestTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 3.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TunneledWebRequest.app/TunneledWebRequest";
+			};
+			name = Debug;
+		};
+		662659171DCB8CF400872F6C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = TunneledWebRequestTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequestTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 3.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TunneledWebRequest.app/TunneledWebRequest";
+			};
+			name = Release;
+		};
+		662659191DCB8CF400872F6C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = TunneledWebRequestUITests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequestUITests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 3.0;
+				TEST_TARGET_NAME = TunneledWebRequest;
+			};
+			name = Debug;
+		};
+		6626591A1DCB8CF400872F6C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = TunneledWebRequestUITests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequestUITests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 3.0;
+				TEST_TARGET_NAME = TunneledWebRequest;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		662658E51DCB8CF300872F6C /* Build configuration list for PBXProject "TunneledWebRequest" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				662659101DCB8CF400872F6C /* Debug */,
+				662659111DCB8CF400872F6C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		662659121DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebRequest" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				662659131DCB8CF400872F6C /* Debug */,
+				662659141DCB8CF400872F6C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		662659151DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebRequestTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				662659161DCB8CF400872F6C /* Debug */,
+				662659171DCB8CF400872F6C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		662659181DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebRequestUITests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				662659191DCB8CF400872F6C /* Debug */,
+				6626591A1DCB8CF400872F6C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 662658E21DCB8CF300872F6C /* Project object */;
+}

+ 7 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:TunneledWebRequest.xcodeproj">
+   </FileRef>
+</Workspace>

+ 43 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/AppDelegate.swift

@@ -0,0 +1,43 @@
+//
+//  AppDelegate.swift
+//  TunneledWebRequest
+//
+
+import UIKit
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+    var window: UIWindow?
+
+
+    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+        // Override point for customization after application launch.
+        return true
+    }
+
+    func applicationWillResignActive(_ application: UIApplication) {
+        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
+    }
+
+    func applicationDidEnterBackground(_ application: UIApplication) {
+        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+    }
+
+    func applicationWillEnterForeground(_ application: UIApplication) {
+        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
+    }
+
+    func applicationDidBecomeActive(_ application: UIApplication) {
+        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+    }
+
+    func applicationWillTerminate(_ application: UIApplication) {
+        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+    }
+
+
+}
+

+ 68 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,68 @@
+{
+  "images" : [
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "60x60",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "60x60",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "29x29",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "29x29",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "40x40",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "40x40",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "76x76",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "76x76",
+      "scale" : "2x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 27 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+                        <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>

+ 26 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Base.lproj/Main.storyboard

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+        </scene>
+    </scenes>
+</document>

+ 45 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Info.plist

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>armv7</string>
+	</array>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+</dict>
+</plist>

+ 210 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/ViewController.swift

@@ -0,0 +1,210 @@
+//
+//  ViewController.swift
+//  TunneledWebView
+//
+
+import UIKit
+
+import PsiphonTunnel
+
+class ViewController: UIViewController {
+    
+    var webView: UIWebView!
+    
+    // The instance of PsiphonTunnel we'll use for connecting.
+    var psiphonTunnel: PsiphonTunnel?
+    
+    // This are the ports that we can proxy through.
+    var socksProxyPort = -1
+    var httpProxyPort = -1
+    
+    override func loadView() {
+        // Make our whole view the webview.
+        webView = UIWebView()
+        view = webView
+        
+        self.psiphonTunnel = PsiphonTunnel.newPsiphonTunnel(self)
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        // Do any additional setup after loading the view, typically from a nib.
+        
+        // Start up the tunnel and begin connecting.
+        // This could be started elsewhere or earlier.
+        NSLog("Starting tunnel")
+        
+        let embeddedServerEntries = ""
+        guard let success = self.psiphonTunnel?.start(embeddedServerEntries), success else {
+            NSLog("psiphonTunnel.start returned false")
+            return
+        }
+    }
+    
+    override func didReceiveMemoryWarning() {
+        super.didReceiveMemoryWarning()
+        // Dispose of any resources that can be recreated.
+    }
+}
+
+// MARK: TunneledAppDelegate implementation
+// See the protocol definition for details about the functions.
+extension ViewController: TunneledAppDelegate {
+    func getPsiphonConfig() -> String? {
+        // In this example, we're going to retrieve our Psiphon config from a file in the app bundle.
+        // Alternatively, it could be a string literal in the code, or whatever makes sense.
+        
+        guard let psiphonConfigUrl = Bundle.main.url(forResource: "psiphon-config", withExtension: "json") else {
+            NSLog("Error getting Psiphon config resource file URL!")
+            return nil
+        }
+        
+        do {
+            return try String.init(contentsOf: psiphonConfigUrl)
+        } catch {
+            NSLog("Error getting Psiphon config resource file URL!")
+            return nil
+        }
+    }
+    
+    func onDiagnosticMessage(_ message: String) {
+        NSLog("onDiagnosticMessage: %@", message)
+    }
+    
+    func onConnecting() {
+        NSLog("onConnecting")
+    }
+    
+    func onConnected() {
+        NSLog("onConnected")
+        
+        // After we're connected, make a tunneled request and populate the webview.
+        
+        DispatchQueue.main.async {
+            assert(self.httpProxyPort > 0)
+            
+            // We'll check out IP to make sure we're tunneled.
+            let urlPath: String = "https://freegeoip.net/csv/"
+            let url = URL(string: urlPath)!
+            let request = URLRequest(url: url)
+            
+            let config = URLSessionConfiguration.ephemeral
+            config.requestCachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+            config.connectionProxyDictionary = [AnyHashable: Any]()
+            
+            // Enable and set the SOCKS proxy values.
+            config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxy as String] = 1
+            config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyHost as String] = "127.0.0.1"
+            config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyPort as String] = self.socksProxyPort
+            
+            // Alternatively, the HTTP proxy can be used. Below are the settings for that.
+            // The HTTPS key constants are mismatched and Xcode gives deprecation warnings, but they seem to be necessary to proxy HTTPS requests. This is probably a bug on Apple's side; see: https://forums.developer.apple.com/thread/19356#131446
+            // config.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] = 1
+            // config.connectionProxyDictionary?[kCFNetworkProxiesHTTPProxy as String] = "127.0.0.1"
+            // config.connectionProxyDictionary?[kCFNetworkProxiesHTTPPort as String] = self.httpProxyPort
+            // config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyHost as String] = "127.0.0.1"
+            // config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyPort as String] = self.httpProxyPort
+            
+            let session = URLSession.init(configuration: config, delegate: nil, delegateQueue: OperationQueue.current)
+            
+            // Create the URLSession task that will make the request via the tunnel proxy.
+            let task = session.dataTask(with: request) {
+                (data: Data?, response: URLResponse?, error: Error?) in
+                if error != nil {
+                    NSLog("Client-side error in request to \(urlPath): \(error)")
+                    return
+                }
+                
+                let httpResponse = response as? HTTPURLResponse
+                if httpResponse?.statusCode != 200 {
+                    NSLog("Server-side error in request to \(urlPath): \(httpResponse)")
+                    return
+                }
+                
+                let encodingName = response?.textEncodingName != nil ? response?.textEncodingName : "utf-8"
+                let encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName as CFString!))
+                
+                var stringData = String(data: data!, encoding: String.Encoding(rawValue: UInt(encoding)))
+                stringData = stringData?.replacingOccurrences(of: ",", with: "\n")
+                
+                // Load the IP info result into the web view.
+                self.webView.loadHTMLString("<br><pre>\(stringData!)</pre>", baseURL: url)
+                
+                // Make sure the session is cleaned up.
+                session.invalidateAndCancel()
+                
+                // We're done with the Psiphon tunnel, so stop it.
+                // In a real app, we would keep this alive for as long as we need it.
+                self.psiphonTunnel?.stop()
+            }
+            
+            // Start the request task.
+            task.resume()
+        }
+    }
+    
+    func onExiting() {
+        // TODO: After updating tunnel-core, make sure this is getting hit.
+        NSLog("onExiting")
+    }
+    
+    func onAvailableEgressRegions(_ regions: [Any]) {
+        NSLog("onAvailableEgressRegions: %@", regions)
+    }
+    
+    func onSocksProxyPort(inUse port: Int) {
+        NSLog("onSocksProxyPort: %d", port)
+    }
+    
+    func onHttpProxyPort(inUse port: Int) {
+        NSLog("onHttpProxyPort: %d", port)
+    }
+    
+    func onListeningSocksProxyPort(_ port: Int) {
+        NSLog("onListeningSocksProxyPort: %d", port)
+        // Record the port being used so that we can proxy through it later.
+        DispatchQueue.main.async {
+            self.socksProxyPort = port
+        }
+    }
+    
+    func onListeningHttpProxyPort(_ port: Int) {
+        NSLog("onListeningHttpProxyPort: %d", port)
+        // Record the port being used so that we can proxy through it later.
+        DispatchQueue.main.async {
+            self.httpProxyPort = port
+        }
+    }
+    
+    func onUpstreamProxyError(_ message: String) {
+        NSLog("onUpstreamProxyError: %@", message)
+    }
+    
+    func onClientRegion(_ region: String) {
+        NSLog("onClientRegion: %@", region)
+    }
+    
+    func onSplitTunnelRegion(_ region: String) {
+        NSLog("onSplitTunnelRegion: %@", region)
+    }
+    
+    func onUntunneledAddress(_ address: String) {
+        NSLog("onUntunneledAddress: %@", address)
+    }
+    
+    func onBytesTransferred(_ sent: Int64, _ received: Int64) {
+        NSLog("onBytesTransferred: sent:%d, received:%d", sent, received)
+    }
+    
+    func onHomepage(_ url: String) {
+        NSLog("onHomepage: %@", url)
+    }
+    
+    func onClientIsLatestVersion() {
+        NSLog("onClientIsLatestVersion")
+    }
+    
+    func onClientUpgradeDownloaded(_ filename: String) {
+        NSLog("onClientUpgradeDownloaded: %@", filename)
+    }
+}

+ 13 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/psiphon-config.json.stub

@@ -0,0 +1,13 @@
+/*
+These are the minimum values required to use the Psiphon Tunnel.
+ClientVersion is the version number of your software, which will be tracked in Psiphon stats.
+All other values will be provided to you by Psiphon Inc.
+*/
+
+{
+  "ClientVersion": "123", /* Must be a number in a string. */
+  "PropagationChannelId": "...",
+  "SponsorId": "...",
+  "RemoteServerListSignaturePublicKey": "...",
+  "RemoteServerListUrl": "..."
+}

+ 22 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequestTests/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 36 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequestTests/TunneledWebRequestTests.swift

@@ -0,0 +1,36 @@
+//
+//  TunneledWebRequestTests.swift
+//  TunneledWebRequestTests
+//
+//  Created by Adam Pritchard on 2016-11-03.
+//  Copyright © 2016 Psiphon Inc. All rights reserved.
+//
+
+import XCTest
+@testable import TunneledWebRequest
+
+class TunneledWebRequestTests: XCTestCase {
+    
+    override func setUp() {
+        super.setUp()
+        // Put setup code here. This method is called before the invocation of each test method in the class.
+    }
+    
+    override func tearDown() {
+        // Put teardown code here. This method is called after the invocation of each test method in the class.
+        super.tearDown()
+    }
+    
+    func testExample() {
+        // This is an example of a functional test case.
+        // Use XCTAssert and related functions to verify your tests produce the correct results.
+    }
+    
+    func testPerformanceExample() {
+        // This is an example of a performance test case.
+        self.measure {
+            // Put the code you want to measure the time of here.
+        }
+    }
+    
+}

+ 22 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequestUITests/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 36 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequestUITests/TunneledWebRequestUITests.swift

@@ -0,0 +1,36 @@
+//
+//  TunneledWebRequestUITests.swift
+//  TunneledWebRequestUITests
+//
+//  Created by Adam Pritchard on 2016-11-03.
+//  Copyright © 2016 Psiphon Inc. All rights reserved.
+//
+
+import XCTest
+
+class TunneledWebRequestUITests: XCTestCase {
+        
+    override func setUp() {
+        super.setUp()
+        
+        // Put setup code here. This method is called before the invocation of each test method in the class.
+        
+        // In UI tests it is usually best to stop immediately when a failure occurs.
+        continueAfterFailure = false
+        // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
+        XCUIApplication().launch()
+
+        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
+    }
+    
+    override func tearDown() {
+        // Put teardown code here. This method is called after the invocation of each test method in the class.
+        super.tearDown()
+    }
+    
+    func testExample() {
+        // Use recording to get started writing UI tests.
+        // Use XCTAssert and related functions to verify your tests produce the correct results.
+    }
+    
+}

+ 33 - 0
MobileLibrary/iOS/USAGE.md

@@ -0,0 +1,33 @@
+# Using the Psiphon iOS Library
+
+## Overview
+
+Psiphon Library for iOS enables you to easily embed Psiphon in your iOS app.
+You can then tunnel requests through Psiphon, ensuring that your app can't be
+blocked by censors.
+
+The Psiphon Library is available as a `.framework` that can be easily included
+in your project using these instructions.
+
+## Using the Library in your App
+
+**First step:** Review the sample app, located under `SampleApps`.
+This code is a canonical guide for integrating the Library.
+
+**Second step:** Review the comments in [`PsiphonTunnel.h`](PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h). They describe the interface and delegate requirements.
+
+### Setting up your project
+
+1. Add `PsiphonTunnel.framework` to project (drag into project tree).
+
+2. In the "General" settings for the target, set "Deployment Target" to 9.3.
+
+3. In the "Build Settings" for the target, under "Build Options", set "Enable Bitcode" to "No".
+
+4. In the "Build Settings" for the target, click the `+` at the top, then "Add User-Defined Setting". Name the new setting `STRIP_BITCODE_FROM_COPIED_FILES` and set it to `NO`.
+
+5. In target Build Phases, add a "Copy Files" phase. Set "Destination" to "Frameworks". Add `PsiphonTunnel.framework` to the list. Ensure "Code Sign on Copy" is checked.
+
+## Compiling and testing
+
+Only phone targets are compiled into the Library, so you must compile for and test on an actual device. If you don't do this, you'll get a linker error that says "missing required architecture x86_64 in file".

+ 61 - 8
MobileLibrary/iOS/build-psiphon-framework.sh

@@ -1,7 +1,13 @@
 #!/usr/bin/env bash
+
+set -e
+
 BASE_DIR=$(cd "$(dirname "$0")" ; pwd -P)
 cd ${BASE_DIR}
 
+# The location of the final framework build
+BUILD_DIR="${BASE_DIR}/build"
+
 # Ensure go is installed
 which go 2>&1 > /dev/null
 if [ $? -ne 0 ]; then
@@ -11,19 +17,24 @@ fi
 
 VALID_ARCHS="arm64 armv7 armv7s"
 FRAMEWORK="Psi"
-INTERMEDIATE_OUPUT_DIR="${BASE_DIR}/PsiphonTunnelController/PsiphonTunnelController"
+INTERMEDIATE_OUPUT_DIR="${BASE_DIR}/PsiphonTunnel/PsiphonTunnel"
 INTERMEDIATE_OUPUT_FILE="${FRAMEWORK}.framework"
 FRAMEWORK_BINARY="${INTERMEDIATE_OUPUT_DIR}/${INTERMEDIATE_OUPUT_FILE}/Versions/A/${FRAMEWORK}"
 
 LIBSSL=${BASE_DIR}/OpenSSL-for-iPhone/lib/libssl.a
 LIBCRYPTO=${BASE_DIR}/OpenSSL-for-iPhone/lib/libcrypto.a
 OPENSSL_INCLUDE=${BASE_DIR}/OpenSSL-for-iPhone/include/
-UMBRELLA_FRAMEWORK_XCODE_PROJECT=${BASE_DIR}/PsiphonTunnelController/PsiphonTunnelController.xcodeproj/
-TRUSTED_ROOT_CA_FILE=${BASE_DIR}/PsiphonTunnelController/PsiphonTunnelController/rootCAs.txt
+UMBRELLA_FRAMEWORK_XCODE_PROJECT=${BASE_DIR}/PsiphonTunnel/PsiphonTunnel.xcodeproj/
+TRUSTED_ROOT_CA_FILE=${BASE_DIR}/PsiphonTunnel/PsiphonTunnel/rootCAs.txt
 
-#Download trustedroot CAs off curl website, see https://curl.haxx.se/docs/caextract.html for details
+# Download trustedroot CAs off curl website, see https://curl.haxx.se/docs/caextract.html for details
 curl -o $TRUSTED_ROOT_CA_FILE https://curl.haxx.se/ca/cacert.pem
 
+rc=$?; if [[ $rc != 0 ]]; then
+  echo "FAILURE: curl -o $TRUSTED_ROOT_CA_FILE https://curl.haxx.se/ca/cacert.pem"
+  exit $rc
+fi
+
 # Not exporting this breaks go commands later if run via jenkins
 export GOPATH=${PWD}/go-ios-build
 
@@ -37,7 +48,16 @@ OPENSSL_SRC_DIR=${GOPATH}/src/github.com/Psiphon-Inc/openssl
 PATH=${PATH}:${GOPATH}/bin
 
 mkdir -p ${GOPATH}
+rc=$?; if [[ $rc != 0 ]]; then
+  echo "FAILURE: mkdir -p ${GOPATH}"
+  exit $rc
+fi
+
 mkdir -p ${INTERMEDIATE_OUPUT_DIR}
+rc=$?; if [[ $rc != 0 ]]; then
+  echo "FAILURE: mkdir -p ${INTERMEDIATE_OUPUT_DIR}"
+  exit $rc
+fi
 
 if [ ! -e ${IOS_SRC_DIR} ]; then
   echo "iOS source directory (${IOS_SRC_DIR}) not found, creating link"
@@ -52,7 +72,16 @@ fi
 cd OpenSSL-for-iPhone && ./build-libssl.sh; cd -
 
 go get -d  -u -v github.com/Psiphon-Inc/openssl
+rc=$?; if [[ $rc != 0 ]]; then
+  echo "FAILURE: go get -d  -u -v github.com/Psiphon-Inc/openssl"
+  exit $rc
+fi
+
 go get -d -u -v github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
+rc=$?; if [[ $rc != 0 ]]; then
+  echo "FAILURE: go get -d -u -v github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi"
+  exit $rc
+fi
 
 function check_pinned_version() {
   echo "Checking for gomobile revision: '${GOMOBILE_PINNED_REV}'"
@@ -77,7 +106,7 @@ check_pinned_version
 if [ $? -ne 0 ]; then
     go get -u golang.org/x/mobile/cmd/gomobile
     cd ${GOPATH}/src/golang.org/x/mobile/cmd/gomobile
-    git checkout master 
+    git checkout master
     git branch -d pinned
     git checkout -b pinned ${GOMOBILE_PINNED_REV}
     go install
@@ -122,14 +151,38 @@ IOS_CGO_BUILD_FLAGS='// #cgo darwin CFLAGS: -I'"${OPENSSL_INCLUDE}"'\
 
 LC_ALL=C sed -i -- "s|// #cgo pkg-config: libssl|${IOS_CGO_BUILD_FLAGS}|" "${OPENSSL_SRC_DIR}/build.go"
 
-gomobile bind  -target ios -ldflags="${LDFLAGS}" -o ${INTERMEDIATE_OUPUT_DIR}/${INTERMEDIATE_OUPUT_FILE} github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
+gomobile init
+
+gomobile bind -target ios -ldflags="${LDFLAGS}" -o ${INTERMEDIATE_OUPUT_DIR}/${INTERMEDIATE_OUPUT_FILE} github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/psi
+rc=$?; if [[ $rc != 0 ]]; then
+  echo "FAILURE: gomobile bind"
+  exit $rc
+fi
+
 ARCHS="$(lipo -info "${FRAMEWORK_BINARY}" | rev | cut -d ':' -f1 | rev)"
 for ARCH in $ARCHS; do
   if ! [[ "${VALID_ARCHS}" == *"$ARCH"* ]]; then
     echo "Stripping ARCH ${ARCH} from ${FRAMEWORK_BINARY}"
-    lipo -remove "$ARCH" -output "$FRAMEWORK_BINARY" "$FRAMEWORK_BINARY" || exit 1
+    lipo -remove "$ARCH" -output "$FRAMEWORK_BINARY" "$FRAMEWORK_BINARY"
+    rc=$?; if [[ $rc != 0 ]]; then
+      echo "FAILURE: lipo"
+      exit $rc
+    fi
   fi
 done
 
-xcodebuild clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -configuration Release -sdk iphoneos -project ${UMBRELLA_FRAMEWORK_XCODE_PROJECT} CONFIGURATION_BUILD_DIR="${BASE_DIR}/build"
+#
+# Do the outer framework build using Xcode
+#
+
+# Clean previous output
+rm -rf "${BUILD_DIR}"
+
+# Build the outer framework
+xcodebuild clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -configuration Release -sdk iphoneos ONLY_ACTIVE_ARCH=NO -project ${UMBRELLA_FRAMEWORK_XCODE_PROJECT} CONFIGURATION_BUILD_DIR="${BUILD_DIR}"
+rc=$?; if [[ $rc != 0 ]]; then
+  echo "FAILURE: xcodebuild"
+  exit $rc
+fi
+
 echo "Done."

+ 2 - 2
Server/README.md

@@ -1,9 +1,9 @@
 ## Psiphon Tunnel Core Server README
 
 ### Overview
-The `Server`/`psiphond` program and the `github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server` package contain an experimental Psiphon server stack.
+The `Server`/`psiphond` program and the `github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server` package contain the Psiphon server software.
 
-Functionality is based on the [production server stack](https://bitbucket.org/psiphon/psiphon-circumvention-system/src/tip/Server/) but only a small subset is implemented. Currently, this stack supports the `SSH` and `OSSH` protocols and has a minimal web server to support the API calls the tunnel-core client requires.
+Functionality is based on the [legacy server stack](https://bitbucket.org/psiphon/psiphon-circumvention-system/src/tip/Server/). `psiphond` has entered production.
 
 ### Build
 Prerequisites:

+ 1 - 0
psiphon/common/protocol/protocol.go

@@ -88,6 +88,7 @@ func TunnelProtocolUsesMeekHTTPS(protocol string) bool {
 }
 
 type HandshakeResponse struct {
+	SSHSessionID         string              `json:"ssh_session_id"`
 	Homepages            []string            `json:"homepages"`
 	UpgradeClientVersion string              `json:"upgrade_client_version"`
 	PageViewRegexes      []map[string]string `json:"page_view_regexes"`

+ 29 - 25
psiphon/controller_test.go

@@ -683,40 +683,44 @@ func controllerRun(t *testing.T, runConfig *controllerRunConfig) {
 			}
 		}
 
-		// Test: fetch website through tunnel
+		// Cannot establish port forwards in DisableApi mode
+		if !runConfig.disableApi {
 
-		// Allow for known race condition described in NewHttpProxy():
-		time.Sleep(1 * time.Second)
+			// Test: fetch website through tunnel
 
-		fetchAndVerifyWebsite(t, httpProxyPort)
+			// Allow for known race condition described in NewHttpProxy():
+			time.Sleep(1 * time.Second)
 
-		// Test: run for duration, periodically using the tunnel to
-		// ensure failed tunnel detection, and ultimately hitting
-		// impaired protocol checks.
+			fetchAndVerifyWebsite(t, httpProxyPort)
 
-		startTime := monotime.Now()
+			// Test: run for duration, periodically using the tunnel to
+			// ensure failed tunnel detection, and ultimately hitting
+			// impaired protocol checks.
 
-		for {
+			startTime := monotime.Now()
 
-			time.Sleep(1 * time.Second)
-			useTunnel(t, httpProxyPort)
+			for {
 
-			if startTime.Add(runConfig.runDuration).Before(monotime.Now()) {
-				break
-			}
-		}
+				time.Sleep(1 * time.Second)
+				useTunnel(t, httpProxyPort)
 
-		// Test: with disruptNetwork, impaired protocols should be exercised
+				if startTime.Add(runConfig.runDuration).Before(monotime.Now()) {
+					break
+				}
+			}
 
-		if runConfig.runDuration > 0 && runConfig.disruptNetwork {
-			count := atomic.LoadInt32(&impairedProtocolCount)
-			if count <= 0 {
-				t.Fatalf("unexpected impaired protocol count: %d", count)
-			} else {
-				impairedProtocolClassification.RLock()
-				t.Logf("impaired protocol classification: %+v",
-					impairedProtocolClassification.classification)
-				impairedProtocolClassification.RUnlock()
+			// Test: with disruptNetwork, impaired protocols should be exercised
+
+			if runConfig.runDuration > 0 && runConfig.disruptNetwork {
+				count := atomic.LoadInt32(&impairedProtocolCount)
+				if count <= 0 {
+					t.Fatalf("unexpected impaired protocol count: %d", count)
+				} else {
+					impairedProtocolClassification.RLock()
+					t.Logf("impaired protocol classification: %+v",
+						impairedProtocolClassification.classification)
+					impairedProtocolClassification.RUnlock()
+				}
 			}
 		}
 	}

+ 24 - 6
psiphon/meekConn.go

@@ -105,7 +105,7 @@ type MeekConfig struct {
 // through a CDN.
 type MeekConn struct {
 	url                  *url.URL
-	additionalHeaders    map[string]string
+	additionalHeaders    http.Header
 	cookie               *http.Cookie
 	pendingConns         *common.Conns
 	transport            transporter
@@ -153,6 +153,8 @@ func DialMeek(
 	meekDialConfig.PendingConns = pendingConns
 
 	var transport transporter
+	var additionalHeaders http.Header
+	var proxyUrl func(*http.Request) (*url.URL, error)
 
 	if meekConfig.UseHTTPS {
 		// Custom TLS dialer:
@@ -216,7 +218,6 @@ func DialMeek(
 		// http.Transport will put the the HTTP server address in the HTTP
 		// request line. In this one case, we can use an HTTP proxy that does
 		// not offer CONNECT support.
-		var proxyUrl func(*http.Request) (*url.URL, error)
 		if strings.HasPrefix(meekDialConfig.UpstreamProxyUrl, "http://") &&
 			(meekConfig.DialAddress == meekConfig.HostHeader ||
 				meekConfig.DialAddress == meekConfig.HostHeader+":80") {
@@ -257,14 +258,17 @@ func DialMeek(
 		Path:   "/",
 	}
 
-	var additionalHeaders map[string]string
 	if meekConfig.UseHTTPS {
 		host, _, err := net.SplitHostPort(meekConfig.DialAddress)
 		if err != nil {
 			return nil, common.ContextError(err)
 		}
-		additionalHeaders = map[string]string{
-			"X-Psiphon-Fronting-Address": host,
+		additionalHeaders = map[string][]string{
+			"X-Psiphon-Fronting-Address": {host},
+		}
+	} else {
+		if proxyUrl == nil {
+			additionalHeaders = meekDialConfig.UpstreamProxyCustomHeaders
 		}
 	}
 
@@ -574,8 +578,22 @@ func (meek *MeekConn) roundTrip(sendPayload []byte) (io.ReadCloser, error) {
 
 		request.Header.Set("Content-Type", "application/octet-stream")
 
+		// Set additional headers to the HTTP request using the same method we use for adding 
+		// custom headers to HTTP proxy requests
 		for name, value := range meek.additionalHeaders {
-			request.Header.Set(name, value)
+			// hack around special case of "Host" header
+			// https://golang.org/src/net/http/request.go#L474
+			// using URL.Opaque, see URL.RequestURI() https://golang.org/src/net/url/url.go#L915
+			if name == "Host" {
+				if len(value) > 0 {
+					if request.URL.Opaque == "" {
+						request.URL.Opaque = request.URL.Scheme + "://" + request.Host + request.URL.RequestURI()
+					}
+					request.Host = value[0]
+				}
+			} else {
+				request.Header[name] = value
+			}
 		}
 
 		request.AddCookie(meek.cookie)

+ 4 - 2
psiphon/server/api.go

@@ -169,9 +169,11 @@ func handshakeAPIRequestHandler(
 
 	// Note: no guarantee that PsinetDatabase won't reload between database calls
 	db := support.PsinetDatabase
-	handshakeResponse := protocol.HandshakeResponse{
-		Homepages:            db.GetHomepages(sponsorID, geoIPData.Country, isMobile),
+	handshakeResponse := common.HandshakeResponse{
+		SSHSessionID:         sessionID,
+		Homepages:            db.GetRandomHomepage(sponsorID, geoIPData.Country, isMobile),
 		UpgradeClientVersion: db.GetUpgradeClientVersion(clientVersion, normalizedPlatform),
+		PageViewRegexes:      make([]map[string]string, 0),
 		HttpsRequestRegexes:  db.GetHttpsRequestRegexes(sponsorID),
 		EncodedServerList:    db.DiscoverServers(geoIPData.DiscoveryValue),
 		ClientRegion:         geoIPData.Country,

+ 1 - 1
psiphon/server/meek.go

@@ -328,7 +328,7 @@ func (server *MeekServer) getSession(
 				// Some headers, such as X-Forwarded-For, are a comma-separated
 				// list of IPs (each proxy in a chain). The first IP should be
 				// the client IP.
-				proxyClientIP := strings.Split(header, ",")[0]
+				proxyClientIP := strings.Split(value, ",")[0]
 				if net.ParseIP(proxyClientIP) != nil {
 					clientIP = proxyClientIP
 					break

+ 31 - 19
psiphon/server/psinet/psinet.go

@@ -28,6 +28,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"math"
+	"math/rand"
 	"strconv"
 	"strings"
 	"time"
@@ -144,7 +145,18 @@ func NewDatabase(filename string) (*Database, error) {
 	return database, nil
 }
 
-// GetHomepages returns a list of  home pages for the specified sponsor,
+// GetRandomHomepage returns a random home page from a set of home pages
+// for the specified sponsor, region, and platform.
+func (db *Database) GetRandomHomepage(sponsorID, clientRegion string, isMobilePlatform bool) []string {
+	homepages := db.GetHomepages(sponsorID, clientRegion, isMobilePlatform)
+	if len(homepages) > 0 {
+		index := rand.Intn(len(homepages))
+		return homepages[index : index+1]
+	}
+	return homepages
+}
+
+// GetHomepages returns a list of home pages for the specified sponsor,
 // region, and platform.
 func (db *Database) GetHomepages(sponsorID, clientRegion string, isMobilePlatform bool) []string {
 	db.ReloadableFile.RLock()
@@ -155,7 +167,7 @@ func (db *Database) GetHomepages(sponsorID, clientRegion string, isMobilePlatfor
 	// Sponsor id does not exist: fail gracefully
 	sponsor, ok := db.Sponsors[sponsorID]
 	if !ok {
-		return nil
+		return sponsorHomePages
 	}
 
 	homePages := sponsor.HomePages
@@ -175,7 +187,7 @@ func (db *Database) GetHomepages(sponsorID, clientRegion string, isMobilePlatfor
 	}
 
 	// Case: lookup failed or no corresponding homepages found for region --> use default
-	if sponsorHomePages == nil {
+	if len(sponsorHomePages) == 0 {
 		defaultHomePages, ok := homePages["None"]
 		if ok {
 			for _, homePage := range defaultHomePages {
@@ -390,21 +402,21 @@ func (db *Database) getEncodedServerEntry(server Server) string {
 
 	// Extended (new) entry fields are in a JSON string
 	var extendedConfig struct {
-		IpAddress                     string
-		WebServerPort                 string
-		WebServerSecret               string
-		WebServerCertificate          string
-		SshPort                       int
-		SshUsername                   string
-		SshPassword                   string
-		SshHostKey                    string
-		SshObfuscatedPort             int
-		SshObfuscatedKey              string
-		Region                        string
-		MeekCookieEncryptionPublicKey string
-		MeekObfuscatedKey             string
-		MeekServerPort                int
-		capabilities                  []string
+		IpAddress                     string   `json:"ipAddress"`
+		WebServerPort                 string   `json:"webServerPort"` // not an int
+		WebServerSecret               string   `json:"webServerSecret"`
+		WebServerCertificate          string   `json:"webServerCertificate"`
+		SshPort                       int      `json:"sshPort"`
+		SshUsername                   string   `json:"sshUsername"`
+		SshPassword                   string   `json:"sshPassword"`
+		SshHostKey                    string   `json:"sshHostKey"`
+		SshObfuscatedPort             int      `json:"sshObfuscatedPort"`
+		SshObfuscatedKey              string   `json:"sshObfuscatedKey"`
+		Capabilities                  []string `json:"capabilities"`
+		Region                        string   `json:"region"`
+		MeekServerPort                int      `json:"meekServerPort"`
+		MeekCookieEncryptionPublicKey string   `json:"meekCookieEncryptionPublicKey"`
+		MeekObfuscatedKey             string   `json:"meekObfuscatedKey"`
 	}
 
 	// NOTE: also putting original values in extended config for easier parsing by new clients
@@ -458,7 +470,7 @@ func (db *Database) getEncodedServerEntry(server Server) string {
 
 	for capability, enabled := range serverCapabilities {
 		if enabled == true {
-			extendedConfig.capabilities = append(extendedConfig.capabilities, capability)
+			extendedConfig.Capabilities = append(extendedConfig.Capabilities, capability)
 		}
 	}
 

+ 3 - 0
psiphon/server/services.go

@@ -24,6 +24,7 @@
 package server
 
 import (
+	"math/rand"
 	"os"
 	"os/signal"
 	"path/filepath"
@@ -43,6 +44,8 @@ import (
 // os.Kill signals are received. The config determines which components are run.
 func RunServices(configJSON []byte) error {
 
+	rand.Seed(int64(time.Now().Nanosecond()))
+
 	config, err := LoadConfig(configJSON)
 	if err != nil {
 		log.WithContextFields(LogFields{"error": err}).Error("load config failed")

Some files were not shown because too many files changed in this diff