diff --git a/Gemini.xcodeproj/project.pbxproj b/Gemini.xcodeproj/project.pbxproj index dbb11d9..a959d9a 100644 --- a/Gemini.xcodeproj/project.pbxproj +++ b/Gemini.xcodeproj/project.pbxproj @@ -12,6 +12,18 @@ D626646324BBF1C300DF9B88 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D626646224BBF1C300DF9B88 /* Assets.xcassets */; }; D626646624BBF1C300DF9B88 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D626646524BBF1C300DF9B88 /* Preview Assets.xcassets */; }; D626646924BBF1C300DF9B88 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D626646724BBF1C300DF9B88 /* Main.storyboard */; }; + D626648024BBF22E00DF9B88 /* GeminiProtocol.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D626647724BBF22E00DF9B88 /* GeminiProtocol.framework */; }; + D626648724BBF22E00DF9B88 /* GeminiProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626648624BBF22E00DF9B88 /* GeminiProtocolTests.swift */; }; + D626648924BBF22E00DF9B88 /* GeminiProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D626647924BBF22E00DF9B88 /* GeminiProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D626648C24BBF22E00DF9B88 /* GeminiProtocol.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D626647724BBF22E00DF9B88 /* GeminiProtocol.framework */; }; + D626648D24BBF22E00DF9B88 /* GeminiProtocol.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D626647724BBF22E00DF9B88 /* GeminiProtocol.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D626649C24BBF24100DF9B88 /* GeminiResponseHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */; }; + D626649D24BBF24100DF9B88 /* Message+Gemini.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649624BBF24100DF9B88 /* Message+Gemini.swift */; }; + D626649E24BBF24100DF9B88 /* GeminiConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649724BBF24100DF9B88 /* GeminiConnection.swift */; }; + D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */; }; + D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649924BBF24100DF9B88 /* GeminiProtocol.swift */; }; + D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649A24BBF24100DF9B88 /* GeminiRequest.swift */; }; + D62664A224BBF24100DF9B88 /* GeminiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649B24BBF24100DF9B88 /* GeminiResponse.swift */; }; D62664B124BBF26A00DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; }; D62664B824BBF26A00DF9B88 /* GeminiFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664B724BBF26A00DF9B88 /* GeminiFormatTests.swift */; }; D62664BA24BBF26A00DF9B88 /* GeminiFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = D62664AA24BBF26A00DF9B88 /* GeminiFormat.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -21,6 +33,14 @@ D62664C824BBF2C600DF9B88 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664C724BBF2C600DF9B88 /* Document.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + D626648124BBF22E00DF9B88 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D626645324BBF1C200DF9B88 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D626647624BBF22E00DF9B88; + remoteInfo = GeminiProtocol; + }; D626648324BBF22E00DF9B88 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D626645324BBF1C200DF9B88 /* Project object */; @@ -28,6 +48,13 @@ remoteGlobalIDString = D626645A24BBF1C200DF9B88; remoteInfo = Gemini; }; + D626648A24BBF22E00DF9B88 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D626645324BBF1C200DF9B88 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D626647624BBF22E00DF9B88; + remoteInfo = GeminiProtocol; + }; D62664B224BBF26A00DF9B88 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D626645324BBF1C200DF9B88 /* Project object */; @@ -59,6 +86,7 @@ dstSubfolderSpec = 10; files = ( D62664BE24BBF26A00DF9B88 /* GeminiFormat.framework in Embed Frameworks */, + D626648D24BBF22E00DF9B88 /* GeminiProtocol.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -74,6 +102,19 @@ D626646824BBF1C300DF9B88 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; D626646A24BBF1C300DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D626646B24BBF1C300DF9B88 /* Gemini.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Gemini.entitlements; sourceTree = ""; }; + D626647724BBF22E00DF9B88 /* GeminiProtocol.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GeminiProtocol.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D626647924BBF22E00DF9B88 /* GeminiProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeminiProtocol.h; sourceTree = ""; }; + D626647A24BBF22E00DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D626647F24BBF22E00DF9B88 /* GeminiProtocolTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GeminiProtocolTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D626648624BBF22E00DF9B88 /* GeminiProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiProtocolTests.swift; sourceTree = ""; }; + D626648824BBF22E00DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiResponseHeader.swift; sourceTree = ""; }; + D626649624BBF24100DF9B88 /* Message+Gemini.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Message+Gemini.swift"; sourceTree = ""; }; + D626649724BBF24100DF9B88 /* GeminiConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiConnection.swift; sourceTree = ""; }; + D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NWParameters+Gemini.swift"; sourceTree = ""; }; + D626649924BBF24100DF9B88 /* GeminiProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiProtocol.swift; sourceTree = ""; }; + D626649A24BBF24100DF9B88 /* GeminiRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiRequest.swift; sourceTree = ""; }; + D626649B24BBF24100DF9B88 /* GeminiResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiResponse.swift; sourceTree = ""; }; D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GeminiFormat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D62664AA24BBF26A00DF9B88 /* GeminiFormat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeminiFormat.h; sourceTree = ""; }; D62664AB24BBF26A00DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -90,6 +131,25 @@ buildActionMask = 2147483647; files = ( D62664BD24BBF26A00DF9B88 /* GeminiFormat.framework in Frameworks */, + D626648C24BBF22E00DF9B88 /* GeminiProtocol.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D626647424BBF22E00DF9B88 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D626647C24BBF22E00DF9B88 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D626648024BBF22E00DF9B88 /* GeminiProtocol.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D62664A524BBF26A00DF9B88 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -112,6 +172,8 @@ isa = PBXGroup; children = ( D626645D24BBF1C200DF9B88 /* Gemini */, + D626647824BBF22E00DF9B88 /* GeminiProtocol */, + D626648524BBF22E00DF9B88 /* GeminiProtocolTests */, D62664A924BBF26A00DF9B88 /* GeminiFormat */, D62664B624BBF26A00DF9B88 /* GeminiFormatTests */, D626645C24BBF1C200DF9B88 /* Products */, @@ -122,6 +184,8 @@ isa = PBXGroup; children = ( D626645B24BBF1C200DF9B88 /* Gemini.app */, + D626647724BBF22E00DF9B88 /* GeminiProtocol.framework */, + D626647F24BBF22E00DF9B88 /* GeminiProtocolTests.xctest */, D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */, D62664B024BBF26A00DF9B88 /* GeminiFormatTests.xctest */, ); @@ -150,6 +214,31 @@ path = "Preview Content"; sourceTree = ""; }; + D626647824BBF22E00DF9B88 /* GeminiProtocol */ = { + isa = PBXGroup; + children = ( + D626647924BBF22E00DF9B88 /* GeminiProtocol.h */, + D626647A24BBF22E00DF9B88 /* Info.plist */, + D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */, + D626649624BBF24100DF9B88 /* Message+Gemini.swift */, + D626649924BBF24100DF9B88 /* GeminiProtocol.swift */, + D626649A24BBF24100DF9B88 /* GeminiRequest.swift */, + D626649B24BBF24100DF9B88 /* GeminiResponse.swift */, + D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */, + D626649724BBF24100DF9B88 /* GeminiConnection.swift */, + ); + path = GeminiProtocol; + sourceTree = ""; + }; + D626648524BBF22E00DF9B88 /* GeminiProtocolTests */ = { + isa = PBXGroup; + children = ( + D626648624BBF22E00DF9B88 /* GeminiProtocolTests.swift */, + D626648824BBF22E00DF9B88 /* Info.plist */, + ); + path = GeminiProtocolTests; + sourceTree = ""; + }; D62664A924BBF26A00DF9B88 /* GeminiFormat */ = { isa = PBXGroup; children = ( @@ -173,6 +262,14 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + D626647224BBF22E00DF9B88 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D626648924BBF22E00DF9B88 /* GeminiProtocol.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D62664A324BBF26A00DF9B88 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -191,10 +288,12 @@ D626645724BBF1C200DF9B88 /* Sources */, D626645824BBF1C200DF9B88 /* Frameworks */, D626645924BBF1C200DF9B88 /* Resources */, + D626649124BBF22E00DF9B88 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + D626648B24BBF22E00DF9B88 /* PBXTargetDependency */, D62664BC24BBF26A00DF9B88 /* PBXTargetDependency */, ); name = Gemini; @@ -202,6 +301,43 @@ productReference = D626645B24BBF1C200DF9B88 /* Gemini.app */; productType = "com.apple.product-type.application"; }; + D626647624BBF22E00DF9B88 /* GeminiProtocol */ = { + isa = PBXNativeTarget; + buildConfigurationList = D626648E24BBF22E00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiProtocol" */; + buildPhases = ( + D626647224BBF22E00DF9B88 /* Headers */, + D626647324BBF22E00DF9B88 /* Sources */, + D626647424BBF22E00DF9B88 /* Frameworks */, + D626647524BBF22E00DF9B88 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GeminiProtocol; + productName = GeminiProtocol; + productReference = D626647724BBF22E00DF9B88 /* GeminiProtocol.framework */; + productType = "com.apple.product-type.framework"; + }; + D626647E24BBF22E00DF9B88 /* GeminiProtocolTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D626649224BBF22E00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiProtocolTests" */; + buildPhases = ( + D626647B24BBF22E00DF9B88 /* Sources */, + D626647C24BBF22E00DF9B88 /* Frameworks */, + D626647D24BBF22E00DF9B88 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D626648224BBF22E00DF9B88 /* PBXTargetDependency */, + D626648424BBF22E00DF9B88 /* PBXTargetDependency */, + ); + name = GeminiProtocolTests; + productName = GeminiProtocolTests; + productReference = D626647F24BBF22E00DF9B88 /* GeminiProtocolTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; D62664A724BBF26A00DF9B88 /* GeminiFormat */ = { isa = PBXNativeTarget; buildConfigurationList = D62664BF24BBF26A00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiFormat" */; @@ -251,6 +387,14 @@ D626645A24BBF1C200DF9B88 = { CreatedOnToolsVersion = 12.0; }; + D626647624BBF22E00DF9B88 = { + CreatedOnToolsVersion = 12.0; + LastSwiftMigration = 1200; + }; + D626647E24BBF22E00DF9B88 = { + CreatedOnToolsVersion = 12.0; + TestTargetID = D626645A24BBF1C200DF9B88; + }; D62664A724BBF26A00DF9B88 = { CreatedOnToolsVersion = 12.0; LastSwiftMigration = 1200; @@ -275,6 +419,8 @@ projectRoot = ""; targets = ( D626645A24BBF1C200DF9B88 /* Gemini */, + D626647624BBF22E00DF9B88 /* GeminiProtocol */, + D626647E24BBF22E00DF9B88 /* GeminiProtocolTests */, D62664A724BBF26A00DF9B88 /* GeminiFormat */, D62664AF24BBF26A00DF9B88 /* GeminiFormatTests */, ); @@ -292,6 +438,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D626647524BBF22E00DF9B88 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D626647D24BBF22E00DF9B88 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D62664A624BBF26A00DF9B88 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -318,6 +478,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D626647324BBF22E00DF9B88 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */, + D62664A224BBF24100DF9B88 /* GeminiResponse.swift in Sources */, + D626649E24BBF24100DF9B88 /* GeminiConnection.swift in Sources */, + D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */, + D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */, + D626649D24BBF24100DF9B88 /* Message+Gemini.swift in Sources */, + D626649C24BBF24100DF9B88 /* GeminiResponseHeader.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D626647B24BBF22E00DF9B88 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D626648724BBF22E00DF9B88 /* GeminiProtocolTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D62664A424BBF26A00DF9B88 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -338,6 +520,21 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + D626648224BBF22E00DF9B88 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D626647624BBF22E00DF9B88 /* GeminiProtocol */; + targetProxy = D626648124BBF22E00DF9B88 /* PBXContainerItemProxy */; + }; + D626648424BBF22E00DF9B88 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D626645A24BBF1C200DF9B88 /* Gemini */; + targetProxy = D626648324BBF22E00DF9B88 /* PBXContainerItemProxy */; + }; + D626648B24BBF22E00DF9B88 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D626647624BBF22E00DF9B88 /* GeminiProtocol */; + targetProxy = D626648A24BBF22E00DF9B88 /* PBXContainerItemProxy */; + }; D62664B324BBF26A00DF9B88 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D62664A724BBF26A00DF9B88 /* GeminiFormat */; @@ -532,6 +729,105 @@ }; name = Release; }; + D626648F24BBF22E00DF9B88 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = GeminiProtocol/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GeminiProtocol; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + D626649024BBF22E00DF9B88 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = GeminiProtocol/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GeminiProtocol; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + D626649324BBF22E00DF9B88 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + INFOPLIST_FILE = GeminiProtocolTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GeminiProtocolTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gemini.app/Contents/MacOS/Gemini"; + }; + name = Debug; + }; + D626649424BBF22E00DF9B88 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + INFOPLIST_FILE = GeminiProtocolTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GeminiProtocolTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gemini.app/Contents/MacOS/Gemini"; + }; + name = Release; + }; D62664C024BBF26A00DF9B88 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -652,6 +948,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D626648E24BBF22E00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiProtocol" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D626648F24BBF22E00DF9B88 /* Debug */, + D626649024BBF22E00DF9B88 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D626649224BBF22E00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiProtocolTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D626649324BBF22E00DF9B88 /* Debug */, + D626649424BBF22E00DF9B88 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D62664BF24BBF26A00DF9B88 /* Build configuration list for PBXNativeTarget "GeminiFormat" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Gemini.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist b/Gemini.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist index 59c38fb..b8c4466 100644 --- a/Gemini.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Gemini.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist @@ -14,6 +14,11 @@ orderHint 2 + GeminiProtocol.xcscheme_^#shared#^_ + + orderHint + 1 + diff --git a/GeminiProtocol/GeminiConnection.swift b/GeminiProtocol/GeminiConnection.swift new file mode 100644 index 0000000..4e91cda --- /dev/null +++ b/GeminiProtocol/GeminiConnection.swift @@ -0,0 +1,102 @@ +// +// GeminiConnection.swift +// Gemini +// +// Created by Shadowfacts on 7/12/20. +// + +import Foundation +import Network + +protocol GeminiConnectionDelegate: class { + func connectionReady(_ connection: GeminiConnection) + func connection(_ connection: GeminiConnection, receivedData data: Data?, header: GeminiResponseHeader) + func connectionCompleted(_ connection: GeminiConnection) +} + +class GeminiConnection { + + weak var delegate: GeminiConnectionDelegate? + + private var connection: NWConnection? + + init(endpoint: NWEndpoint, delegate: GeminiConnectionDelegate? = nil) { + self.connection = NWConnection(to: endpoint, using: .gemini) + self.delegate = delegate + + startConnection() + } + + private func startConnection() { + guard let connection = connection else { return } + + connection.stateUpdateHandler = { (newState) in + switch newState { + case .ready: + print("\(connection) established") + self.delegate?.connectionReady(self) + case let .failed(error): + print("\(connection) failed: \(error)") + connection.cancel() + default: + break + } + } + + // todo: should this be on a background queue? + connection.start(queue: .main) + } + + var report: NWConnection.PendingDataTransferReport! + + func sendRequest(_ request: GeminiRequest) { + guard let connection = connection else { return } + + let message = NWProtocolFramer.Message(geminiRequest: request) + let context = NWConnection.ContentContext(identifier: "GeminiRequest", metadata: [message]) + // todo: should this really always be idempotent? + connection.send(content: nil, contentContext: context, isComplete: true, completion: .contentProcessed({ (_) in })) + receiveNextMessage() + report = connection.startDataTransferReport() + } + + private func receiveNextMessage() { + guard let connection = connection else { return } + + // todo: should this really be .max + connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { (data, context, isComplete, error) in + // todo: handle error + if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message, + let header = message.geminiResponseHeader, + let delegate = self.delegate { + delegate.connection(self, receivedData: data, header: header) + self.receiveNextMessage() + } + if isComplete { + self.delegate?.connectionCompleted(self) + self.report.collect(queue: .main) { (report) in + print(report.debugDescription) + } +// connection.cancel() + } + } +// connection.receiveMessage { (data, context, isComplete, error) in +// if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message, +// let header = message.geminiResponseHeader, +// let delegate = self.delegate { +//// let response = GeminiResponse(status: header.status, meta: header.meta, body: data) +// delegate.connection(self, receivedData: data, header: header) +// } +// // todo: error handling +// if let error = error { +// print(error) +// } else if isComplete { +// self.delegate?.connectionCompleted(self) +// connection.cancel() +// } else { +// self.receiveNextMessage() +// } +// } + } + +} diff --git a/GeminiProtocol/GeminiProtocol.h b/GeminiProtocol/GeminiProtocol.h new file mode 100644 index 0000000..c28628c --- /dev/null +++ b/GeminiProtocol/GeminiProtocol.h @@ -0,0 +1,18 @@ +// +// GeminiProtocol.h +// GeminiProtocol +// +// Created by Shadowfacts on 7/12/20. +// + +#import + +//! Project version number for GeminiProtocol. +FOUNDATION_EXPORT double GeminiProtocolVersionNumber; + +//! Project version string for GeminiProtocol. +FOUNDATION_EXPORT const unsigned char GeminiProtocolVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/GeminiProtocol/GeminiProtocol.swift b/GeminiProtocol/GeminiProtocol.swift new file mode 100644 index 0000000..ad89553 --- /dev/null +++ b/GeminiProtocol/GeminiProtocol.swift @@ -0,0 +1,96 @@ +// +// GeminiProtocol.swift +// Gemini +// +// Created by Shadowfacts on 7/12/20. +// + +import Network + +class GeminiProtocol: NWProtocolFramerImplementation { + static let definition = NWProtocolFramer.Definition(implementation: GeminiProtocol.self) + + static let label = "Gemini" + + required init(framer: NWProtocolFramer.Instance) { + } + + func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { + return .ready + } + + func wakeup(framer: NWProtocolFramer.Instance) { + } + + func stop(framer: NWProtocolFramer.Instance) -> Bool { + return true + } + + func cleanup(framer: NWProtocolFramer.Instance) { + } + + func handleInput(framer: NWProtocolFramer.Instance) -> Int { + while true { + var tempStatusCode: GeminiResponse.StatusCode? + let parsedStatusCodeAndSpace = framer.parseInput(minimumIncompleteLength: 3, maximumLength: 3) { (buffer, isComplete) -> Int in + guard let buffer = buffer, + buffer.count == 3 else { return 0 } + tempStatusCode = GeminiResponse.StatusCode(buffer) + return 3 + } + guard parsedStatusCodeAndSpace, let statusCode = tempStatusCode else { + return 3 + } + + var tempMeta: String? + // Minimum length is 2 bytes, spec does not say meta string is required + let parsedMeta = framer.parseInput(minimumIncompleteLength: 2, maximumLength: 1024 + 2) { (buffer, isComplete) -> Int in + guard let buffer = buffer, + buffer.count >= 2 else { return 0 } + + let lastPossibleCRIndex = buffer.index(before: buffer.index(before: buffer.endIndex)) + var index = buffer.startIndex + var found = false + while index <= lastPossibleCRIndex { + // + if buffer[index] == 13 && buffer[buffer.index(after: index)] == 10 { + found = true + break + } + index = buffer.index(after: index) + } + + guard found else { fatalError("Didn't find in buffer. Meta string was longer than 1024 bytes") } + + tempMeta = String(bytes: buffer[.. 1024 { + throw Error.urlTooLong + } + self.url = url + } + + var data: Data { + var data = url.absoluteString.data(using: .utf8)! + data.append(contentsOf: [13, 10]) // + return data + } +} + +extension GeminiRequest { + enum Error: Swift.Error { + case urlTooLong + } +} diff --git a/GeminiProtocol/GeminiResponse.swift b/GeminiProtocol/GeminiResponse.swift new file mode 100644 index 0000000..3ac54c6 --- /dev/null +++ b/GeminiProtocol/GeminiResponse.swift @@ -0,0 +1,132 @@ +// +// GeminiResponse.swift +// Gemini +// +// Created by Shadowfacts on 7/12/20. +// + +import Foundation +import UniformTypeIdentifiers + +struct GeminiResponse { + let status: StatusCode + let meta: String + let body: Data? + + // Helpers + var rawMimeType: String? { + guard status.isSuccess else { return nil } + return meta.trimmingCharacters(in: .whitespacesAndNewlines) + } + var mimeType: String? { + guard let rawMimeType = rawMimeType else { return nil } + return rawMimeType.split(separator: ";").first?.trimmingCharacters(in: .whitespaces) + } + var mimeTypeParameters: [String: String]? { + guard let rawMimeType = rawMimeType else { return nil } + return rawMimeType.split(separator: ";").dropFirst().reduce(into: [String: String]()) { (parameters, parameter) in + let parts = parameter.split(separator: "=").map { $0.trimmingCharacters(in: .whitespaces) } + precondition(parts.count == 2) + parameters[parts[0].lowercased()] = parts[1] + } + } + @available(macOS 10.16, *) + var utiType: UTType? { + guard let mimeType = mimeType else { return nil } + return UTType.types(tag: mimeType, tagClass: .mimeType, conformingTo: nil).first + } + + var bodyText: String? { + guard let body = body, let parameters = mimeTypeParameters else { return nil } + let encoding: String.Encoding + switch parameters["charset"]?.lowercased() { + case nil, "utf-8": + // The Gemini spec defines UTF-8 to be the default charset. + encoding = .utf8 + case "us-ascii": + encoding = .ascii + default: + // todo: log warning + encoding = .utf8 + } + return String(data: body, encoding: encoding) + } +} + +extension GeminiResponse { + enum StatusCode: Int { + // All statuses and subtypes + case input = 10 + case sensitiveInput = 11 + case success = 20 + case temporaryRedirect = 30 + case permanentRedirect = 31 + case temporaryFailure = 40 + case serverUnavailable = 41 + case cgiError = 42 + case proxyError = 43 + case slowDown = 44 + case permanentFailure = 50 + case notFound = 51 + case gone = 52 + case proxyRequestRefused = 53 + case badRequest = 59 + case clientCertificateRequested = 60 + case certificateNotAuthorised = 61 + case certificateNotValid = 62 + + // Status type helpers + var isInput: Bool { rawValue / 10 == 1 } + var isSuccess: Bool { rawValue / 10 == 2 } + var isRedirect: Bool { rawValue / 10 == 3 } + var isTemporaryFailure: Bool { rawValue / 10 == 4 } + var isPermanentFailure: Bool { rawValue / 10 == 5 } + var isClientCertificateRequired: Bool { rawValue / 10 == 6 } + + // Other helpers + var isFailure: Bool { isTemporaryFailure || isPermanentFailure } + } +} + +extension GeminiResponse.StatusCode: CustomStringConvertible { + var description: String { + switch self { + case .input: + return "input" + case .sensitiveInput: + return "sensitiveInput" + case .success: + return "success" + case .temporaryRedirect: + return "temporaryRedirect" + case .permanentRedirect: + return "permanentRedirect" + case .temporaryFailure: + return "temporaryFailure" + case .serverUnavailable: + return "serverUnavailable" + case .cgiError: + return "cgiError" + case .proxyError: + return "proxyError" + case .slowDown: + return "slowDown" + case .permanentFailure: + return "permanentFailure" + case .notFound: + return "notFound" + case .gone: + return "gone" + case .proxyRequestRefused: + return "proxyRequestRefused" + case .badRequest: + return "badRequest" + case .clientCertificateRequested: + return "clientCertificateRequested" + case .certificateNotAuthorised: + return "certificateNotAuthorised" + case .certificateNotValid: + return "certificateNotValid" + } + } +} diff --git a/GeminiProtocol/GeminiResponseHeader.swift b/GeminiProtocol/GeminiResponseHeader.swift new file mode 100644 index 0000000..909244a --- /dev/null +++ b/GeminiProtocol/GeminiResponseHeader.swift @@ -0,0 +1,13 @@ +// +// GeminiResponseHeader.swift +// Gemini +// +// Created by Shadowfacts on 7/12/20. +// + +import Foundation + +struct GeminiResponseHeader { + let status: GeminiResponse.StatusCode + let meta: String +} diff --git a/GeminiProtocol/Info.plist b/GeminiProtocol/Info.plist new file mode 100644 index 0000000..9bcb244 --- /dev/null +++ b/GeminiProtocol/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/GeminiProtocol/Message+Gemini.swift b/GeminiProtocol/Message+Gemini.swift new file mode 100644 index 0000000..7943abd --- /dev/null +++ b/GeminiProtocol/Message+Gemini.swift @@ -0,0 +1,33 @@ +// +// Message+Gemini.swift +// Gemini +// +// Created by Shadowfacts on 7/12/20. +// + +import Network + +private let requestKey = "gemini_request" +private let responseHeaderKey = "gemini_response_header" + +extension NWProtocolFramer.Message { + + convenience init(geminiRequest request: GeminiRequest) { + self.init(definition: GeminiProtocol.definition) + self[requestKey] = request + } + + convenience init(geminiResponseHeader header: GeminiResponseHeader) { + self.init(definition: GeminiProtocol.definition) + self[responseHeaderKey] = header + } + + var geminiRequest: GeminiRequest? { + self[requestKey] as? GeminiRequest + } + + var geminiResponseHeader: GeminiResponseHeader? { + self[responseHeaderKey] as? GeminiResponseHeader + } + +} diff --git a/GeminiProtocol/NWParameters+Gemini.swift b/GeminiProtocol/NWParameters+Gemini.swift new file mode 100644 index 0000000..7a98a7b --- /dev/null +++ b/GeminiProtocol/NWParameters+Gemini.swift @@ -0,0 +1,42 @@ +// +// NWParameters+Gemini.swift +// Gemini +// +// Created by Shadowfacts on 7/12/20. +// + +import Network + +extension NWParameters { + static var gemini: NWParameters { + let tlsOptions = geminiTLSOptions + let tcpOptions = NWProtocolTCP.Options() + let parameters = NWParameters(tls: tlsOptions, tcp: tcpOptions) + + let geminiOptions = NWProtocolFramer.Options(definition: GeminiProtocol.definition) + parameters.defaultProtocolStack.applicationProtocols.insert(geminiOptions, at: 0) + + return parameters + } + + private static var geminiTLSOptions: NWProtocolTLS.Options { + let options = NWProtocolTLS.Options() + + sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, .TLSv12) + // based on https://developer.apple.com/forums/thread/104018 + sec_protocol_options_set_verify_block(options.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in + + let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue() + + var error: CFError? + if SecTrustEvaluateWithError(trust, &error) { + sec_protocol_verify_complete(true) + } else { + // todo: prompt user to trust cert on first use + sec_protocol_verify_complete(true) + } + }, .main) + + return options + } +} diff --git a/GeminiProtocolTests/GeminiProtocolTests.swift b/GeminiProtocolTests/GeminiProtocolTests.swift new file mode 100644 index 0000000..2d8eca5 --- /dev/null +++ b/GeminiProtocolTests/GeminiProtocolTests.swift @@ -0,0 +1,33 @@ +// +// GeminiProtocolTests.swift +// GeminiProtocolTests +// +// Created by Shadowfacts on 7/12/20. +// + +import XCTest +@testable import GeminiProtocol + +class GeminiProtocolTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/GeminiProtocolTests/Info.plist b/GeminiProtocolTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/GeminiProtocolTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + +