├── .gitignore ├── LICENSE ├── LICENSE.zh_CN ├── Package.swift ├── README.md ├── README.zh_CN.md └── Sources └── PerfectNotifications ├── HTTP ├── HPACK.swift ├── HTTP2.swift ├── HTTP2Client.swift ├── HTTP2Frame.swift ├── HTTPHeaders.swift ├── HTTPMethod.swift ├── HTTPRequest.swift └── HTTPResponse.swift ├── JWTokenMaker.swift └── NotificationPusher.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | *.pins 24 | *.resolved 25 | *.xcodeproj 26 | .build* 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | 71 | Packages/ 72 | PerfectNotifications.xcodeproj/ 73 | .DS_Store 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE.zh_CN: -------------------------------------------------------------------------------- 1 | Apache许可证 2 | 2.0版 2004年1月 3 | http://www.apache.org/licenses/ 4 | 5 | 关于使用、复制和分发的条款 6 | 7 | 定义 8 | "许可证"是指根据本文件第1到第9部分关于使用、复制和分发的条款。 9 | 10 | "许可证颁发者"是指版权所有者或者由版权所有者授权许可证的实体。 11 | 12 | "法律实体"是指实施实体和进行控制的所有其它实体受该实体控制,或者受该实体集中控制。根据此定义,"控制"是指(i)让无论是否签订协议的上述实体,进行指导或管理的直接权利或间接权利,或者(ii)拥有百分之五十(50%)或以上已发行股票的所有者,或者(iii)上述实体的实权所有者。 13 | 14 | "用户"(或"用户的")是指行使本许可证所授予权限的个人或法律实体。 15 | 16 | "源程序"形式是指对包括但不限于软件源代码、文件源程序和配置文件进行修改的首选形式。 17 | 18 | "目标"形式是指对源程序形式进行机械转换或翻译的任何形式,包括但不限于对编译的目标代码,生成的文件以及转换为其它媒体类型。 19 | 20 | "作品"是指根据本许可证所制作的源程序形式或目标形式的著作,在著作中包含的或附加的版权通知(在下面附录中提供了一个示例)。 21 | 22 | "衍生作品"是指基于作品(或从作品衍生而来)的源程序形式或目标形式的任何作品,以及编辑修订、注释、详细描述或其它修订等构成原创著作作品的整体。根据本许可证,衍生作品不得包括与作品及其衍生作品分离之作品,或仅与作品及其衍生作品的接口相链接(或按名称结合)之作品。 23 | 24 | "贡献"是指任何著作作品,包括作品的原始版本和对该作品或衍生作品所做的任何修订或补充,意在提交给许可证颁发者以让版权所有者或代表版权所有者的授权个人或法律实体包含在其作品中。根据此定义,"提交"一词表示发送给许可证颁发者或其代表人,任何电子的、口头的或书面的交流信息形式,包括但不限于在由许可证颁发者或者代表其管理的电子邮件清单、源代码控制系统、以及发布跟踪系统上为讨论和提高作品的交流,但不包括由版权所有者以书面形式明显标注或指定为"非贡献"的交流活动。 25 | 26 | "贡献者"是指许可证颁发者和代表从许可证颁发者接受之贡献的并随后包含在作品之贡献中的任何个人或法律实体。 27 | 28 | 版权许可证的授予。根据本许可证的条款,每个贡献者授予用户永久性的、全球性的、非专有性的、免费的、无版权费的、不可撤销的版权许可证以源程序形式或目标形式复制、准备衍生作品、公开显示、公开执行、授予分许可证、以及分发作品和这样的衍生作品。 29 | 专利许可证的授予。根据本许可证的条款,每个贡献者授予用户永久性的、全球性的、非专有性的、免费的、无版权费的、不可撤销的(除在本部分进行说明)专利许可证对作品进行制作、让人制作、使用、提供销售、销售、进口和其它转让,且这样的许可证仅适用于在所递交作品的贡献中因可由单一的或多个这样的贡献者授予而必须侵犯的申请专利。如果用户对任何实体针对作品或作品中所涉及贡献提出因直接性或贡献性专利侵权而提起专利法律诉讼(包括交互诉讼请求或反索赔),那么根据本许可证,授予用户针对作品的任何专利许可证将在提起上述诉讼之日起终止。 30 | 重新分发。用户可在任何媒介中复制和分发作品或衍生作品之副本,无论是否修订,还是以源程序形式或目标形式,条件是用户需满足下列条款: 31 | 用户必须为作品或衍生作品的任何其他接收者提供本许可证的副本;并且 32 | 用户必须让任何修改过的文件附带明显的通知,声明用户已更改文件;并且 33 | 用户必须从作品的源程序形式中保留衍生作品源程序形式的用户所分发的所有版权、专利、商标和属性通知,但不包括不属于衍生作品任何部分的类似通知;并且 34 | 如果作品将"通知"文本文件包括为其分发作品的一部分,那么用户分发的任何衍生作品中须至少在下列地方之一包括,在这样的通知文件中所包含的属性通知的可读副本,但不包括那些不属于衍生作品任何部分的通知:在作为衍生作品一部分而分发的通知文本文件中;如果与衍生作品一起提供则在源程序形式或文件中;或者通常作为第三方通知出现的时候和地方,在衍生作品中产生的画面中。通知文件的内容仅供信息提供,并未对许可证进行修改。用户可在其分发的衍生作品中在作品的通知文本后或作为附录添加自己的属性通知,条件是附加的属性通知不得构成修改本许可证。 35 | 用户可以为自身所做出的修订添加自己的版权声明并可对自身所做出修订内容或为这样的衍生作品作为整体的使用、复制或分发提供附加或不同的条款,条件是用户对作品的使用、复制和分发必须符合本许可证中声明的条款。 36 | 37 | 贡献的提交。除非用户明确声明,在作品中由用户向许可证颁发者的提交若要包含在贡献中,必须在无任何附加条款下符合本许可证的条款。尽管上面如此规定,执行许可证颁发者有关贡献的条款时,任何情况下均不得替代或修改任何单独许可证协议的条款。 38 | 商标。本许可证并未授予用户使用许可证颁发者的商号、商标、服务标记或产品名称,除非将这些名称用于合理性和惯例性描述作品起源和复制通知文件的内容时。 39 | 保证否认条款。除非因适用法律需要或书面同意,许可证颁发者以"按原样"基础提供作品(并且每个贡献者提供其贡献),无任何明示的或暗示的保证或条件,包括但不限于关于所有权、不侵权、商品适销性、或适用性的保证或条件。用户仅对使用或重新分发作品的正确性负责,并需承担根据本许可证行使权限时的任何风险。 40 | 责任限制条款。在任何情况下并根据任何法律,无论是因侵权(包括过失)或根据合同,还是其它原因,除非根据适用法律需要(例如故意行为和重大过失行为)或经书面同意,即使贡献者事先已被告知发生损害的可能性,任何贡献者不就用户因使用本许可证或不能使用或无法使用作品(包括但不限于商誉损失、停工、计算机失效或故障,或任何商业损坏或损失)而造成的损失,包括直接的、非直接的、特殊的、意外的或间接的字符损坏而负责。 41 | 接受保证或附加责任。重新分发作品或及其衍生作品时,用户可选择提供或为符合本许可证承担之支持、担保、赔偿或其它职责义务和/或权利而收取费用。但是,在承担上述义务时,用户只可代表用户本身和用户本身责任来执行,无需代表任何其它贡献者,并且用户仅可保证、防护并保持每个贡献者不受任何因此而产生的责任或对因用户自身承担这样的保证或附加责任而对这样的贡献者所提出的索赔。 42 | 条款结束 43 | 44 | 附录:如何向用户作品中应用Apache许可证。 45 | 46 | 若要向用户作品应用Apache许可证,请附加下列样本通知,将括号"[]"中的字段以用户自身的区分信息来替换(但不包括括号)。文本必须以文件格式适当的注释句法包含在其中。另外建议将文件名或类别名以及目的说明包含在相同的"打印页"上作为版权通知,以更加容易的区分出第三方档案。 47 | 48 | 版权所有[yyyy][版权所有者的名称] 49 | 50 | 根据2.0版本Apache许可证("许可证")授权; 51 | 根据本许可证,用户可以不使用此文件。 52 | 用户可从下列网址获得许可证副本: 53 | 54 | http://www.apache.org/licenses/LICENSE-2.0 55 | 56 | 除非因适用法律需要或书面同意, 57 | 根据许可证分发的软件是基于"按原样"基础提供, 58 | 无任何明示的或暗示的保证或条件。 59 | 详见根据许可证许可下,特定语言的管辖权限和限制。 60 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "PerfectNotifications", 6 | platforms: [ 7 | .macOS(.v10_15) 8 | ], 9 | products: [ 10 | .library(name: "PerfectNotifications", targets: ["PerfectNotifications"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/PerfectlySoft/Perfect-Net.git", from: "4.0.0"), 14 | .package(url: "https://github.com/PerfectlySoft/Perfect-Thread.git", from: "3.0.0") 15 | ], 16 | targets: [ 17 | .target(name: "PerfectNotifications", dependencies: ["PerfectNet", "PerfectThread"]) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perfect-Notifications [简体中文](README.zh_CN.md) 2 | 3 |

4 | 5 | Get Involed with Perfect! 6 | 7 |

8 | 9 |

10 | 11 | Swift 5.2 12 | 13 | 14 | Platforms OS X | Linux 15 | 16 | 17 | License Apache 18 | 19 |

20 | 21 | APNs remote Notifications for Perfect. This package adds push notification support to your server. Send notifications to iOS/macOS devices. 22 | 23 | Building 24 | -------- 25 | 26 | This is a Swift Package manager based project. Add this repository as a dependency in your Package.swift file. 27 | 28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | .package(url:"https://github.com/PerfectlySoft/Perfect-Notifications.git", from: "5.0.0") 30 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 31 | 32 | Overview 33 | -------- 34 | 35 | This system runs on the server side. Typically at app launch, an Apple device will register with Apple's system for remote notifications. Doing so will return to the device an ID which can be used by external systems to address the device and send notifications through APNs. 36 | 37 | When the device obtains its ID it will need to transmit it to **your** server. Your server will store this id and use it when sending notifications to one or more devices through APNs. 38 | 39 | Obtain APNs Auth Key 40 | -------- 41 | 42 | To connect your server to Apple's push notification system you will first need to obtain an "APNs Auth Key". This key is used on your server to configure its APNs access. You can generate this key through your Apple developer account portal. Log in to your developer account and choose "Certificates, IDs & Profiles" from the menu. Then, under "Keys", choose "All". 43 | 44 | If you haven't already created and downloaded the auth key, click "+" to create a new one. Enter a name for the key and make sure you select **Apple Push Notifications service (APNs)**. This one key can be used for both development or production and can be used for any of your iOS/macOS apps. 45 | 46 | Click "Continue", then "Confirm", then you will be given a chance to download the **private key**. You must download this key now and **save the file**. Also copy the "Key ID" shown in the same view. This will be a 10 character string. 47 | 48 | Finally you will need to locate your developer team id. Click "Account" near the window's top. Select "Membership" in the menu. You will then be shown much of your personal information, including "Team ID". This is another 10 character string. Copy this value. 49 | 50 | Server Configuration 51 | ------ 52 | 53 | To send notifications from your server your must have three pieces of information: 54 | 55 | 1. The private key file which was downloaded 56 | 2. The 10 character key id 57 | 3. Your 10 character team id 58 | 4. An iOS/macOS app id 59 | 60 | These four pieces of information are used to perform push notifications. This information must reside on your server. You can store this information in any manner provided it can be used by the server. For simplicity, the rest of this example assumes that the private key file is in the server's working directory and that the two keys and the app id are embedded in the Swift code. 61 | 62 | In your server Swift code, you must `import PerfectNotifications`. Then, before you start any HTTP servers or send any notifications you must add a "configuration" for the notifications you will be sending. This very simply ties your APNs keys to a name which you can then use later when pushing notifications. 63 | 64 | ```swift 65 | import PerfectNotifications 66 | 67 | // your app id. we use this as the configuration name, but they do not have to match 68 | let notificationsAppId = "my.app.id" 69 | 70 | let apnsKeyIdentifier = "AB90CD56XY" 71 | let apnsTeamIdentifier = "YX65DC09BA" 72 | let apnsPrivateKeyFilePath = "./APNsAuthKey_AB90CD56XY.p8" 73 | 74 | NotificationPusher.addConfigurationAPNS( 75 | name: notificationsTestId, 76 | production: false, // should be false when running pre-release app in debugger 77 | keyId: apnsKeyIdentifier, 78 | teamId: apnsTeamIdentifier, 79 | privateKeyPath: apnsPrivateKeyFilePath) 80 | ``` 81 | 82 | After the configuration has been added, notifications can be sent at any point. To do so, create a `NotificationPusher` with your app id, or "topic", then trigger a notification to one or more devices by calling its `pushAPNS` function: 83 | 84 | ```swift 85 | let deviceIds: [String] = [...] 86 | let n = NotificationPusher(apnsTopic: notificationsTestId) 87 | n.pushAPNS( 88 | configurationName: notificationsTestId, 89 | deviceTokens: deviceIds, 90 | notificationItems: [.alertBody("Hello!"), .sound("default")]) { 91 | responses in 92 | print("\(responses)") 93 | ... 94 | } 95 | ``` 96 | 97 | The topic is required when creating a NotificationPusher. Additional optional parameters can be provided to customize the notification's expiration, priority and collapse-id. Consult Apple's APNS documentation for the semantics of these options. 98 | 99 | Public API 100 | ---- 101 | 102 | The full public version 3.0 API for notification pusher follows: 103 | 104 | ```swift 105 | public class NotificationPusher { 106 | 107 | /// Add an APNS configuration which can be later used to push notifications. 108 | public static func addConfigurationAPNS( 109 | name: String, 110 | production: Bool, 111 | keyId: String, 112 | teamId: String, 113 | privateKeyPath: String) 114 | 115 | /// Initialize given an apns-topic string. 116 | public init( 117 | apnsTopic: String, 118 | expiration: APNSExpiration = .immediate, 119 | priority: APNSPriority = .immediate, 120 | collapseId: String? = nil) 121 | 122 | /// Push one message to one device. 123 | /// Provide the previously set configuration name, device token. 124 | /// Provide a list of APNSNotificationItems. 125 | /// Provide a callback with which to receive the response. 126 | public func pushAPNS( 127 | configurationName: String, 128 | deviceToken: String, 129 | notificationItems: [APNSNotificationItem], 130 | callback: @escaping (NotificationResponse) -> ()) 131 | 132 | /// Push one message to multiple devices. 133 | /// Provide the previously set configuration name, and zero or more device tokens. The same message will be sent to each device. 134 | /// Provide a list of APNSNotificationItems. 135 | /// Provide a callback with which to receive the responses. 136 | public func pushAPNS( 137 | configurationName: String, deviceTokens: [String], 138 | notificationItems: [APNSNotificationItem], 139 | callback: @escaping ([NotificationResponse]) -> ()) 140 | } 141 | ``` 142 | 143 | The remaining structures, including APNSNotificationItem follow: 144 | 145 | ```swift 146 | /// Items to configure an individual notification push. 147 | public enum APNSNotificationItem { 148 | /// alert body child property 149 | case alertBody(String) 150 | /// alert title child property 151 | case alertTitle(String) 152 | /// alert title-loc-key 153 | case alertTitleLoc(String, [String]?) 154 | /// alert action-loc-key 155 | case alertActionLoc(String) 156 | /// alert loc-key 157 | case alertLoc(String, [String]?) 158 | /// alert launch-image 159 | case alertLaunchImage(String) 160 | /// aps badge key 161 | case badge(Int) 162 | /// aps sound key 163 | case sound(String) 164 | /// aps content-available key 165 | case contentAvailable 166 | /// aps category key 167 | case category(String) 168 | /// aps thread-id key 169 | case threadId(String) 170 | /// custom payload data 171 | case customPayload(String, Any) 172 | /// apn mutable-content key 173 | case mutableContent 174 | } 175 | 176 | public enum APNSPriority: Int { 177 | case immediate = 10 178 | case background = 5 179 | } 180 | 181 | /// Time in the future when the notification, if has not be able to be delivered, will expire. 182 | public enum APNSExpiration { 183 | /// Discard the notification if it can't be immediately delivered. 184 | case immediate 185 | /// now + seconds 186 | case relative(Int) 187 | /// absolute UTC time since epoch 188 | case absolute(Int) 189 | } 190 | 191 | /// The response object given after a push attempt. 192 | public struct NotificationResponse: CustomStringConvertible { 193 | /// The response code for the request. 194 | public let status: HTTPResponseStatus 195 | /// The response body data bytes. 196 | public let body: [UInt8] 197 | /// The body data bytes interpreted as JSON and decoded into a Dictionary. 198 | public var jsonObjectBody: [String:Any] 199 | /// The body data bytes converted to String. 200 | public var stringBody: String 201 | public var description: String 202 | } 203 | ``` 204 | 205 | Additional Notes 206 | ---- 207 | 208 | APNs requests are made from your server to Apple's servers "api.development.push.apple.com" or "api.push.apple.com" on port 443. One request will be used when sending one notification to one or more devices. Each connection will remain open and will be reused when sending subsequent notifications. If a connection "goes away" or there are no idle connections that can be used then a new connection will be opened. This is in accordance with Apple's recommended usage of APNs and should provide the best throughput when dealing with many concurrent notification requests. 209 | 210 | Consult [Perfect-NotificationsExample](https://github.com/PerfectExamples/Perfect-NotificationsExample) for a client/server combination which can be easily configured with your own information to quickly get APNS notifications for your apps. 211 | 212 | ## Further Information 213 | For more information on the Perfect project, please visit [perfect.org](http://perfect.org). 214 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | # Perfect-Notifications [English](README.md) 2 | 3 |

4 | 5 | Get Involed with Perfect! 6 | 7 |

8 | 9 |

10 | 11 | Swift 5.2 12 | 13 | 14 | Platforms OS X | Linux 15 | 16 | 17 | License Apache 18 | 19 |

20 | 21 | Notifications for Perfect 之 iOS消息推送。此软件包为您的服务器添加了推送通知支持。向iOS / macOS设备发送通知。 22 | 23 | 24 | 编译 25 | -------- 26 | 27 | 请在您的Perfect项目中的Package.swift文件增加以下依存关系: 28 | 29 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | .package(url:"https://github.com/PerfectlySoft/Perfect-Notifications.git", from: "5.0.0") 31 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 32 | 33 | ## 综述 34 | 35 | 该程序在服务器上运行,通常是苹果移动设备在启动后,会自动在苹果网站上注册,以获得一个设备编号。 36 | 37 | 苹果设备需要在获得该编号之后,主动到⚠️**您的**⚠️服务器上登记。这样您的服务器就可以记录这个设备编号然后通过APN苹果网络服务给设备发消息。 38 | 39 | 40 | ## 获取 APN 授权码 41 | 42 | 要使用苹果设备消息推送服务APN,您需要首先获得一个APN授权代码。这个代码可以在苹果开发者网站上取得。登录到您的开发者账号并选择 "Certificates, IDs & Profiles" 菜单,在"Keys"下,选择"All"。 43 | 44 | 如果还没有创建或者下载授权码,请点击”➕“创建一个新代码。输入私有钥匙的名称,并选择 **Apple Push Notifications service (APNs)**,即 Apple推送通知服务(APNs)。该授权码可以同时在开发阶段和生产部署阶段使用,适用于所有您的iOS / macOS设备。 45 | 46 | 点击 "Continue" 后,再点击"Confirm",可以下载 **私有钥匙** 。此时必须下载并保存该文件,并拷贝屏幕上十位字长的“钥匙代码”。 47 | 48 | 最后需要确定您的开发团队账号。点击"Account"浏览器上方的,选择 "Membership" 会员视图能够找到"Team ID"团队编号,也是10位字符串。请记录该字符串。 49 | 50 | ## 服务器配置 51 | 52 | 如果需要从服务器向苹果设备推送消息,您需要以下三个信息: 53 | 54 | 1. 刚才下载的私有钥匙文件 55 | 2. 10位授权码 56 | 3. 10位开发团队代码 57 | 4. 苹果设备或操作系统在APN上的编号 58 | 59 | 上述内容用于推送消息,注意不要在您的服务器源代码上保存这些东西,最好区别与服务器程序分开保存。为了简单说明,本手册假设钥匙文件在当前工作目录下,并且各个编号代码都已经以Swift变量形式导入到程序。 60 | 61 | 在服务器代码开始段,请导入函数库 `import PerfectNotifications`。随后请配置好上述内容用于实际消息推送。 62 | 63 | ``` swift 64 | import PerfectNotifications 65 | 66 | // 应用程序名称(即Bundle Identifier),我们用这个名称来配置,但是不一定非得是这个形式 67 | let notificationsAppId = "my.app.id" 68 | 69 | let apnsKeyIdentifier = "AB90CD56XY" 70 | let apnsTeamIdentifier = "YX65DC09BA" 71 | let apnsPrivateKeyFilePath = "./APNsAuthKey_AB90CD56XY.p8" 72 | 73 | NotificationPusher.addConfigurationAPNS( 74 | name: notificationsTestId, 75 | production: false, // 如果是调试程序,则将这个生产服务器标志设置为“伪” 76 | keyId: apnsKeyIdentifier, 77 | teamId: apnsTeamIdentifier, 78 | privateKeyPath: apnsPrivateKeyFilePath) 79 | ``` 80 | 81 | 配置完成后就可以随时推送消息了。具体做法是创建一个`NotificationPusher`对象,设置应用程序代码或者“消息主题”,然后调用 `pushAPNS`函数: 82 | 83 | ``` swift 84 | let deviceIds: [String] = [...] 85 | let n = NotificationPusher(apnsTopic: notificationsTestId) 86 | n.pushAPNS( 87 | configurationName: notificationsTestId, 88 | deviceTokens: deviceIds, 89 | notificationItems: [.alertBody("Hello!"), .sound("default")]) { 90 | responses in 91 | print("\(responses)") 92 | ... 93 | } 94 | ``` 95 | 96 | 创建 NotificationPusher 的消息主题是必要的。此外,可选参数还包括消息有效期、优先级和消息层次折叠代码,详见苹果APN有关文档。 97 | 98 | ## 函数界面 99 | 100 | 详细函数调用参考如下: 101 | 102 | ```swift 103 | public class NotificationPusher { 104 | 105 | /// 追加APNS 配置 106 | public static func addConfigurationAPNS( 107 | name: String, 108 | production: Bool, 109 | keyId: String, 110 | teamId: String, 111 | privateKeyPath: String) 112 | 113 | /// 初始化推送实例并设置消息主题 114 | public init( 115 | apnsTopic: String, 116 | expiration: APNSExpiration = .immediate, 117 | priority: APNSPriority = .immediate, 118 | collapseId: String? = nil) 119 | 120 | /// 推送消息 121 | /// 需要提供已经设定好了配置名称和设备编号 122 | /// 需要提供待通知对象列表 APNSNotificationItems 123 | /// 需要提供推送结果回调函数 124 | public func pushAPNS( 125 | configurationName: String, 126 | deviceToken: String, 127 | notificationItems: [APNSNotificationItem], 128 | callback: @escaping (NotificationResponse) -> ()) 129 | 130 | /// 向多个设备推送消息 131 | /// 需要提供预制配置名称、一个或者多个设备代码。所有设备都会收到相同的消息 132 | /// 需要提供待通知对象列表 APNSNotificationItems 133 | /// 需要提供推送结果回调函数 134 | public func pushAPNS( 135 | configurationName: String, deviceTokens: [String], 136 | notificationItems: [APNSNotificationItem], 137 | callback: @escaping ([NotificationResponse]) -> ()) 138 | } 139 | ``` 140 | 141 | 其他数据结果包括 APNSNotificationItem : 142 | 143 | ```swift 144 | /// Items to configure an individual notification push. 145 | public enum APNSNotificationItem { 146 | /// alert body child property 147 | case alertBody(String) 148 | /// alert title child property 149 | case alertTitle(String) 150 | /// alert title-loc-key 151 | case alertTitleLoc(String, [String]?) 152 | /// alert action-loc-key 153 | case alertActionLoc(String) 154 | /// alert loc-key 155 | case alertLoc(String, [String]?) 156 | /// alert launch-image 157 | case alertLaunchImage(String) 158 | /// aps badge key 159 | case badge(Int) 160 | /// aps sound key 161 | case sound(String) 162 | /// aps content-available key 163 | case contentAvailable 164 | /// aps category key 165 | case category(String) 166 | /// aps thread-id key 167 | case threadId(String) 168 | /// custom payload data 169 | case customPayload(String, Any) 170 | /// apn mutable-content key 171 | case mutableContent 172 | } 173 | 174 | public enum APNSPriority: Int { 175 | case immediate = 10 176 | case background = 5 177 | } 178 | 179 | /// 消息有效期,超过之后不会在发送 180 | public enum APNSExpiration { 181 | /// 如果无法立刻到达则丢弃消息 182 | case immediate 183 | /// 当前时间加秒数 184 | case relative(Int) 185 | /// UTC绝对时间 186 | case absolute(Int) 187 | } 188 | 189 | /// 推送后的响应对象 190 | public struct NotificationResponse: CustomStringConvertible { 191 | /// The response code for the request. 192 | public let status: HTTPResponseStatus 193 | /// The response body data bytes. 194 | public let body: [UInt8] 195 | /// The body data bytes interpreted as JSON and decoded into a Dictionary. 196 | public var jsonObjectBody: [String:Any] 197 | /// The body data bytes converted to String. 198 | public var stringBody: String 199 | public var description: String 200 | } 201 | ``` 202 | 203 | ## 其他注意事项 204 | 205 | APNs 请求需要从您的服务器向苹果服务器 "api.development.push.apple.com" 或者 "api.push.apple.com" 443 端口进行通信。一次请求可以向一个或者多个设备推送消息。整个连接过程会一直持续,并且可以用于发送后续消息。如果连接终止或者空闲太久,则会重新打开一个新的连接。这种方式是苹果建议的方式用于处理大量同步通知。 206 | 207 | 可以参考代码示范 [Perfect-NotificationsExample](https://github.com/PerfectExamples/Perfect-NotificationsExample) 用于配置您自己的服务器和应用程序消息推送。 208 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/HTTP/HPACK.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HPACK.swift 3 | // PerfectLib 4 | // 5 | // Created by Kyle Jessup on 2016-02-18. 6 | // Copyright © 2016 PerfectlySoft. All rights reserved. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | import PerfectLib 21 | 22 | // HPACK support for HTTP/2 23 | // This code is modeled after Twitter's hpack library https://github.com/twitter/hpack 24 | // Which is an implimentation of https://tools.ietf.org/html/rfc7541 25 | 26 | class HeaderField { 27 | static let headerEntryOverhead = 32 28 | 29 | let name: [UInt8] 30 | let value: [UInt8] 31 | 32 | var size: Int { 33 | return name.count + value.count + HeaderField.headerEntryOverhead 34 | } 35 | 36 | var nameStr: String { 37 | return UTF8Encoding.encode(bytes: name) 38 | } 39 | 40 | init(name: [UInt8], value: [UInt8]) { 41 | self.name = name 42 | self.value = value 43 | } 44 | 45 | init(name: String, value: String) { 46 | self.name = UTF8Encoding.decode(string: name) 47 | self.value = UTF8Encoding.decode(string: value) 48 | } 49 | 50 | convenience init(name: String) { 51 | self.init(name: name, value: "") 52 | } 53 | 54 | static func sizeOf(name nam: [UInt8], value: [UInt8]) -> Int { 55 | return nam.count + value.count + headerEntryOverhead 56 | } 57 | } 58 | 59 | private final class DynamicTable { 60 | 61 | var headerFields = [HeaderField?]() 62 | var head = 0 63 | var tail = 0 64 | var size = 0 65 | var capacity = -1 { 66 | didSet { 67 | self.capacityChanged(oldValue) 68 | } 69 | } 70 | 71 | var length: Int { 72 | if head < tail { 73 | return headerFields.count - tail + head 74 | } 75 | return head - tail 76 | } 77 | 78 | init(initialCapacity: Int) { 79 | self.capacity = initialCapacity 80 | self.capacityChanged(-1) 81 | } 82 | 83 | private func capacityChanged(_ oldValue: Int) { 84 | guard capacity >= 0 else { 85 | return 86 | } 87 | guard capacity != oldValue else { 88 | return 89 | } 90 | if capacity == 0 { 91 | clear() 92 | } else { 93 | while size > capacity { 94 | _ = remove() 95 | } 96 | } 97 | 98 | var maxEntries = capacity / HeaderField.headerEntryOverhead 99 | if capacity % HeaderField.headerEntryOverhead != 0 { 100 | maxEntries += 1 101 | } 102 | 103 | if headerFields.count != maxEntries { 104 | var tmp = [HeaderField?](repeating: nil, count: maxEntries) 105 | 106 | let len = length 107 | var cursor = tail 108 | 109 | for i in 0.. HeaderField? { 138 | guard let removed = headerFields[tail] else { 139 | return nil 140 | } 141 | size -= removed.size 142 | headerFields[tail] = nil 143 | tail += 1 144 | if tail == headerFields.count { 145 | tail = 0 146 | } 147 | return removed 148 | } 149 | 150 | func getEntry(_ index: Int) -> HeaderField { 151 | let i = head - index 152 | if i < 0 { 153 | return headerFields[i + headerFields.count]! 154 | } 155 | return headerFields[i]! 156 | } 157 | 158 | func add(_ header: HeaderField) { 159 | let headerSize = header.size 160 | if headerSize > capacity { 161 | clear() 162 | } else { 163 | while size + headerSize > capacity { 164 | _ = remove() 165 | } 166 | headerFields[head] = header 167 | head += 1 168 | size += header.size 169 | if head == headerFields.count { 170 | head = 0 171 | } 172 | } 173 | } 174 | } 175 | 176 | private struct StaticTable { 177 | 178 | static let table = [ 179 | HeaderField(name: ":authority"), 180 | HeaderField(name: ":method", value: "GET"), 181 | HeaderField(name: ":method", value: "POST"), 182 | HeaderField(name: ":path", value: "/"), 183 | HeaderField(name: ":path", value: "/index.html"), 184 | HeaderField(name: ":scheme", value: "http"), 185 | HeaderField(name: ":scheme", value: "https"), 186 | HeaderField(name: ":status", value: "200"), 187 | HeaderField(name: ":status", value: "204"), 188 | HeaderField(name: ":status", value: "206"), 189 | HeaderField(name: ":status", value: "304"), 190 | HeaderField(name: ":status", value: "400"), 191 | HeaderField(name: ":status", value: "404"), 192 | HeaderField(name: ":status", value: "500"), 193 | HeaderField(name: "accept-charset"), 194 | HeaderField(name: "accept-encoding", value: "gzip, deflate"), 195 | HeaderField(name: "accept-language"), 196 | HeaderField(name: "accept-ranges"), 197 | HeaderField(name: "accept"), 198 | HeaderField(name: "access-control-allow-origin"), 199 | HeaderField(name: "age"), 200 | HeaderField(name: "allow"), 201 | HeaderField(name: "authorization"), 202 | HeaderField(name: "cache-control"), 203 | HeaderField(name: "content-disposition"), 204 | HeaderField(name: "content-encoding"), 205 | HeaderField(name: "content-language"), 206 | HeaderField(name: "content-length"), 207 | HeaderField(name: "content-location"), 208 | HeaderField(name: "content-range"), 209 | HeaderField(name: "content-type"), 210 | HeaderField(name: "cookie"), 211 | HeaderField(name: "date"), 212 | HeaderField(name: "etag"), 213 | HeaderField(name: "expect"), 214 | HeaderField(name: "expires"), 215 | HeaderField(name: "from"), 216 | HeaderField(name: "host"), 217 | HeaderField(name: "if-match"), 218 | HeaderField(name: "if-modified-since"), 219 | HeaderField(name: "if-none-match"), 220 | HeaderField(name: "if-range"), 221 | HeaderField(name: "if-unmodified-since"), 222 | HeaderField(name: "last-modified"), 223 | HeaderField(name: "link"), 224 | HeaderField(name: "location"), 225 | HeaderField(name: "max-forwards"), 226 | HeaderField(name: "proxy-authenticate"), 227 | HeaderField(name: "proxy-authorization"), 228 | HeaderField(name: "range"), 229 | HeaderField(name: "referer"), 230 | HeaderField(name: "refresh"), 231 | HeaderField(name: "retry-after"), 232 | HeaderField(name: "server"), 233 | HeaderField(name: "set-cookie"), 234 | HeaderField(name: "strict-transport-security"), 235 | HeaderField(name: "transfer-encoding"), 236 | HeaderField(name: "user-agent"), 237 | HeaderField(name: "vary"), 238 | HeaderField(name: "via"), 239 | HeaderField(name: "www-authenticate") 240 | ] 241 | 242 | static let tableByName: [String:Int] = { 243 | var ret = [String:Int]() 244 | var i = table.count 245 | 246 | while i > 0 { 247 | ret[StaticTable.getEntry(i).nameStr] = i 248 | i -= 1 249 | } 250 | 251 | return ret 252 | }() 253 | 254 | static let length = table.count 255 | 256 | static func getEntry(_ index: Int) -> HeaderField { 257 | return table[index - 1] 258 | } 259 | 260 | static func getIndex(_ name: [UInt8]) -> Int { 261 | let s = UTF8Encoding.encode(bytes: name) 262 | if let idx = tableByName[s] { 263 | return idx 264 | } 265 | return -1 266 | } 267 | 268 | static func getIndex(_ name: [UInt8], value: [UInt8]) -> Int { 269 | let idx = getIndex(name) 270 | if idx != -1 { 271 | for i in idx...length { 272 | let entry = getEntry(i) 273 | if entry.name != name { 274 | break 275 | } 276 | if entry.value == value { 277 | return i 278 | } 279 | } 280 | } 281 | return -1 282 | } 283 | } 284 | 285 | func ==(lhs: [UInt8], rhs: [UInt8]) -> Bool { 286 | let c1 = lhs.count 287 | if c1 == rhs.count { 288 | for i in 0.. Bytes { 601 | let o = Bytes() 602 | try encode(out: o, input: inpt) 603 | return o 604 | } 605 | 606 | func encode(out owt: Bytes, input: Bytes) throws { 607 | var current = 0 608 | var n = 0 609 | 610 | while input.availableExportBytes > 0 { 611 | let b = Int(input.export8Bits()) & 0xFF 612 | let code = codes[b] 613 | let nbits = Int(lengths[b]) 614 | 615 | current <<= nbits 616 | current |= code 617 | n += nbits 618 | 619 | while n >= 8 { 620 | n -= 8 621 | let newVal = (current >> n) & 0xFF 622 | owt.import8Bits(from: UInt8(newVal)) 623 | } 624 | } 625 | if n > 0 { 626 | current <<= (8 - n) 627 | current |= (0xFF >> n) 628 | let newVal = current & 0xFF 629 | owt.import8Bits(from: UInt8(newVal)) 630 | } 631 | } 632 | 633 | func getEncodedLength(data dta: [UInt8]) -> Int { 634 | var len = 0 635 | for b in dta { 636 | len += Int(lengths[Int(b & 0xFF)]) 637 | } 638 | return (len + 7) >> 3 639 | } 640 | } 641 | 642 | final class HuffmanDecoder { 643 | 644 | enum Exception: Error { 645 | case eosDecoded, invalidPadding 646 | } 647 | 648 | final class Node { 649 | let symbol: Int 650 | let bits: UInt8 651 | var children: [Node?]? 652 | 653 | var isTerminal: Bool { 654 | return self.children == nil 655 | } 656 | 657 | init() { 658 | self.symbol = 0 659 | self.bits = 8 660 | self.children = [Node?](repeating: nil, count: 256) 661 | } 662 | 663 | init(symbol: Int, bits: UInt8) { 664 | self.symbol = symbol 665 | self.bits = bits 666 | self.children = nil 667 | } 668 | } 669 | 670 | let root: Node 671 | 672 | init(codes: [Int], lengths: [UInt8]) { 673 | self.root = HuffmanDecoder.buildTree(codes: codes, lengths: lengths) 674 | } 675 | 676 | func decode(_ buf: [UInt8]) throws -> [UInt8] { 677 | var retBytes = [UInt8]() 678 | 679 | var node = root 680 | var current = 0 681 | var bits = 0 682 | for byte in buf { 683 | let b = byte// & 0xFF 684 | current = (current << 8) | Int(b) 685 | bits += 8 686 | while bits >= 8 { 687 | let c = (current >> (bits - 8)) & 0xFF 688 | node = node.children![c]! 689 | bits -= Int(node.bits) 690 | if node.isTerminal { 691 | if node.symbol == huffmanEOS { 692 | throw Exception.eosDecoded 693 | } 694 | retBytes.append(UInt8(node.symbol)) 695 | node = root 696 | } 697 | } 698 | } 699 | 700 | while bits > 0 { 701 | let c = (current << (8 - bits)) & 0xFF 702 | node = node.children![c]! 703 | if node.isTerminal && Int(node.bits) <= bits { 704 | bits -= Int(node.bits) 705 | retBytes.append(UInt8(node.symbol)) 706 | node = root 707 | } else { 708 | break 709 | } 710 | } 711 | 712 | let mask = (1 << bits) - 1 713 | if (current & mask) != mask { 714 | throw Exception.invalidPadding 715 | } 716 | 717 | return retBytes 718 | } 719 | 720 | static func buildTree(codes: [Int], lengths: [UInt8]) -> Node { 721 | let root = Node() 722 | for i in 0.. 8 { 732 | len -= 8 733 | let i = (code >> len) & 0xFF 734 | if nil == current.children![i] { 735 | current.children![i] = Node() 736 | } 737 | current = current.children![i]! 738 | } 739 | let terminal = Node(symbol: symbol, bits: UInt8(len)) 740 | let shift = 8 - len 741 | let start = (code << shift) & 0xFF 742 | let end = 1 << shift 743 | for i in start..<(start+end) { 744 | current.children![i] = terminal 745 | } 746 | } 747 | } 748 | 749 | private let huffmanEncoderInstance = HuffmanEncoder(codes: huffmanCodes, lengths: huffmanCodeLengths) 750 | private let huffmanDecoderInstance = HuffmanDecoder(codes: huffmanCodes, lengths: huffmanCodeLengths) 751 | 752 | /// Encodes headers according to the HPACK standard. 753 | final class HPACKEncoder { 754 | 755 | static let bucketSize = 17 756 | static let empty = [UInt8]() 757 | static let indexMax = 2147483647 758 | static let indexMin = -2147483648 759 | 760 | var headerFields: [HeaderEntry?] 761 | var head = HeaderEntry(hash: -1, name: empty, value: empty, index: indexMax, next: nil) 762 | var size = 0 763 | var capacity = 0 764 | 765 | var maxHeaderTableSize: Int { return capacity } 766 | var length: Int { 767 | return size == 0 ? 0 : head.after!.index - head.before!.index + 1 768 | } 769 | 770 | final class HeaderEntry: HeaderField { 771 | 772 | var before: HeaderEntry? 773 | var after: HeaderEntry? 774 | var next: HeaderEntry? 775 | 776 | let hash: Int 777 | let index: Int 778 | 779 | init(hash: Int, name: [UInt8], value: [UInt8], index: Int, next: HeaderEntry?) { 780 | self.index = index 781 | self.hash = hash 782 | super.init(name: name, value: value) 783 | self.next = next 784 | } 785 | 786 | func remove() { 787 | before!.after = after 788 | after!.before = before 789 | } 790 | 791 | func addBefore(existingEntry existing: HeaderEntry) { 792 | after = existing 793 | before = existing.before 794 | before!.after = self 795 | after!.before = self 796 | } 797 | } 798 | 799 | /// Construct an HPACKEncoder with the indicated maximum capacity. 800 | init(maxCapacity: Int = 4096) { 801 | self.capacity = maxCapacity 802 | self.head.after = self.head 803 | self.head.before = self.head 804 | self.headerFields = [HeaderEntry?](repeating: nil, count: HPACKEncoder.bucketSize) 805 | } 806 | 807 | /// Encodes a new header field and value, writing the results to out Bytes. 808 | func encodeHeader(out: Bytes, nameStr: String, valueStr: String, sensitive: Bool = false, incrementalIndexing: Bool = true) throws { 809 | return try encodeHeader(out: out, name: UTF8Encoding.decode(string: nameStr), value: UTF8Encoding.decode(string: valueStr), sensitive: sensitive, incrementalIndexing: incrementalIndexing) 810 | } 811 | 812 | /// Encodes a new header field and value, writing the results to out Bytes. 813 | func encodeHeader(out: Bytes, name: [UInt8], value: [UInt8], sensitive: Bool = false, incrementalIndexing: Bool = true) throws { 814 | if sensitive { 815 | let nameIndex = getNameIndex(name) 816 | try encodeLiteral(out: out, name: name, value: value, indexType: .never, nameIndex: nameIndex) 817 | return 818 | } 819 | if capacity == 0 { 820 | let staticTableIndex = StaticTable.getIndex(name, value: value) 821 | if staticTableIndex == -1 { 822 | let nameIndex = StaticTable.getIndex(name) 823 | try encodeLiteral(out: out, name: name, value: value, indexType: .none, nameIndex: nameIndex) 824 | } else { 825 | encodeInteger(out: out, mask: 0x80, n: 7, i: staticTableIndex) 826 | } 827 | return 828 | } 829 | let headerSize = HeaderField.sizeOf(name: name, value: value) 830 | if headerSize > capacity { 831 | let nameIndex = getNameIndex(name) 832 | try encodeLiteral(out: out, name: name, value: value, indexType: .none, nameIndex: nameIndex) 833 | } else if let headerField = getEntry(name, value: value) { 834 | let index = getIndex(headerField.index) + StaticTable.length 835 | encodeInteger(out: out, mask: 0x80, n: 7, i: index) 836 | } else { 837 | let staticTableIndex = StaticTable.getIndex(name, value: value) 838 | if staticTableIndex != -1 { 839 | encodeInteger(out: out, mask: 0x80, n: 7, i: staticTableIndex) 840 | } else { 841 | let nameIndex = getNameIndex(name) 842 | ensureCapacity(headerSize: headerSize) 843 | let indexType = incrementalIndexing ? IndexType.incremental : IndexType.none 844 | try encodeLiteral(out: out, name: name, value: value, indexType: indexType, nameIndex: nameIndex) 845 | add(name, value: value) 846 | } 847 | } 848 | } 849 | 850 | func index(_ h: Int) -> Int { 851 | return h % HPACKEncoder.bucketSize 852 | } 853 | 854 | func hash(_ name: [UInt8]) -> Int { 855 | var h = 0 856 | for b in name { 857 | h = 31 &* h &+ Int(b) 858 | } 859 | if h > 0 { 860 | return h 861 | } 862 | if h == HPACKEncoder.indexMin { 863 | return HPACKEncoder.indexMax 864 | } 865 | return -h 866 | } 867 | 868 | func clear() { 869 | for i in 0.. HeaderField? { 878 | if size == 0 { 879 | return nil 880 | } 881 | let eldest = head.after 882 | let h = eldest!.hash 883 | let i = index(h) 884 | 885 | var prev = headerFields[i] 886 | var e = prev 887 | 888 | while let ee = e { 889 | let next = ee.next 890 | if ee === eldest! { 891 | if prev === eldest! { 892 | headerFields[i] = next 893 | } else { 894 | prev!.next = next 895 | } 896 | eldest!.remove() 897 | size -= eldest!.size 898 | return eldest 899 | } 900 | prev = e 901 | e = next 902 | } 903 | return nil 904 | } 905 | 906 | func add(_ name: [UInt8], value: [UInt8]) { 907 | let headerSize = HeaderField.sizeOf(name: name, value: value) 908 | 909 | if headerSize > capacity { 910 | clear() 911 | return 912 | } 913 | 914 | while size + headerSize > capacity { 915 | _ = remove() 916 | } 917 | 918 | let h = hash(name) 919 | let i = index(h) 920 | 921 | let old = headerFields[i] 922 | let e = HeaderEntry(hash: h, name: name, value: value, index: head.before!.index - 1, next: old) 923 | headerFields[i] = e 924 | e.addBefore(existingEntry: head) 925 | size += headerSize 926 | } 927 | 928 | func getIndex(_ index: Int) -> Int { 929 | if index == -1 { 930 | return index 931 | } 932 | return index - head.before!.index + 1 933 | } 934 | 935 | func getIndex(_ name: [UInt8]) -> Int { 936 | if length == 0 || name.count == 0 { 937 | return -1 938 | } 939 | let h = hash(name) 940 | let i = self.index(h) 941 | var index = -1 942 | 943 | var e = headerFields[i] 944 | 945 | while let ee = e { 946 | 947 | if ee.hash == h && name == ee.name { 948 | index = ee.index 949 | break 950 | } 951 | 952 | e = ee.next 953 | } 954 | 955 | return getIndex(index) 956 | } 957 | 958 | func getEntry(_ name: [UInt8], value: [UInt8]) -> HeaderEntry? { 959 | if length == 0 || name.count == 0 || value.count == 0 { 960 | return nil 961 | } 962 | let h = hash(name) 963 | let i = index(h) 964 | 965 | var e = headerFields[i] 966 | 967 | while let ee = e { 968 | 969 | if ee.hash == h && name == ee.name && value == ee.value { 970 | return ee 971 | } 972 | e = ee.next 973 | } 974 | return nil 975 | } 976 | 977 | func getHeaderField(index: Int) -> HeaderField? { 978 | var entry = head 979 | var i = index 980 | while i >= 0 { 981 | i -= 1 982 | entry = entry.before! 983 | } 984 | return entry 985 | } 986 | 987 | func ensureCapacity(headerSize size: Int) { 988 | while size + size > capacity { 989 | if length == 0 { 990 | break 991 | } 992 | _ = remove() 993 | } 994 | } 995 | 996 | func getNameIndex(_ name: [UInt8]) -> Int { 997 | var index = StaticTable.getIndex(name) 998 | if index == -1 { 999 | index = getIndex(name) 1000 | if index >= 0 { 1001 | index += StaticTable.length 1002 | } 1003 | } 1004 | return index 1005 | } 1006 | 1007 | func encodeInteger(out owt: Bytes, mask: Int, n: Int, i: Int) { 1008 | let nbits = 0xFF >> (8 - n) 1009 | if i < nbits { 1010 | owt.import8Bits(from: UInt8(mask | i)) 1011 | } else { 1012 | owt.import8Bits(from: UInt8(mask | nbits)) 1013 | var length = i - nbits 1014 | while true { 1015 | if (length & ~0x7F) == 0 { 1016 | owt.import8Bits(from: UInt8(length)) 1017 | return 1018 | } else { 1019 | owt.import8Bits(from: UInt8((length & 0x7f) | 0x80)) 1020 | length >>= 7 1021 | } 1022 | } 1023 | } 1024 | } 1025 | 1026 | func encodeStringLiteral(out owt: Bytes, string: [UInt8]) throws { 1027 | let huffmanLength = huffmanEncoderInstance.getEncodedLength(data: string) 1028 | if huffmanLength < string.count { 1029 | encodeInteger(out: owt, mask: 0x80, n: 7, i: huffmanLength) 1030 | try huffmanEncoderInstance.encode(out: owt, input: Bytes(existingBytes: string)) 1031 | } else { 1032 | encodeInteger(out: owt, mask: 0x00, n: 7, i: string.count) 1033 | owt.importBytes(from: string) 1034 | } 1035 | } 1036 | 1037 | func encodeLiteral(out owt: Bytes, name: [UInt8], value: [UInt8], indexType: IndexType, nameIndex: Int) throws { 1038 | var mask = 0 1039 | var prefixBits = 0 1040 | 1041 | switch indexType { 1042 | case .incremental: 1043 | mask = 0x40 1044 | prefixBits = 6 1045 | case .none: 1046 | mask = 0x00 1047 | prefixBits = 4 1048 | case .never: 1049 | mask = 0x10 1050 | prefixBits = 4 1051 | } 1052 | 1053 | encodeInteger(out: owt, mask: mask, n: prefixBits, i: nameIndex == -1 ? 0 : nameIndex) 1054 | if nameIndex == -1 { 1055 | try encodeStringLiteral(out: owt, string: name) 1056 | } 1057 | try encodeStringLiteral(out: owt, string: value) 1058 | } 1059 | 1060 | func setMaxHeaderTableSize(out: Bytes, maxHeaderTableSize: Int) { 1061 | if capacity == maxHeaderTableSize { 1062 | return 1063 | } 1064 | capacity = maxHeaderTableSize 1065 | ensureCapacity(headerSize: 0) 1066 | encodeInteger(out: out, mask: 0x20, n: 5, i: maxHeaderTableSize) 1067 | } 1068 | 1069 | } 1070 | 1071 | /// Decodes headers which have been HPACK encoded. 1072 | /// Decoding takes a HeaderListener object which receives each field/value as they are decoded. 1073 | final class HPACKDecoder { 1074 | 1075 | enum Exception: Error { 1076 | case decompressionException, illegalIndexValue, invalidMaxDynamicTableSize, maxDynamicTableSizeChangeRequested 1077 | } 1078 | 1079 | enum State { 1080 | case readHeaderRepresentation, readMaxDynamicTableSize, readIndexedHeader, readIndexedHeaderName, 1081 | readLiteralHeaderNameLengthPrefix, readLiteralHeaderNameLength, readLiteralHeaderName, skipLiteralHeaderName, 1082 | readLiteralHeaderValueLengthPrefix, readLiteralHeaderValueLength, readLiteralHeaderValue, skipLiteralHeaderValue 1083 | } 1084 | 1085 | static let empty = [UInt8]() 1086 | 1087 | private let dynamicTable: DynamicTable 1088 | 1089 | let maxHeaderSize: Int 1090 | var maxDynamicTableSize: Int 1091 | var encoderMaxDynamicTableSize: Int 1092 | 1093 | var maxDynamicTableSizeChangeRequired: Bool 1094 | 1095 | var state: State 1096 | 1097 | var index = 0 1098 | var headerSize = 0 1099 | var indexType = IndexType.none 1100 | var huffmanEncoded = false 1101 | var name: [UInt8]? 1102 | var skipLength = 0 1103 | var nameLength = 0 1104 | var valueLength = 0 1105 | 1106 | /// Construct an HPACKDecoder with the given memory constraints. 1107 | init(maxHeaderSize: Int = 4096, maxHeaderTableSize: Int = 4096) { 1108 | self.dynamicTable = DynamicTable(initialCapacity: maxHeaderTableSize) 1109 | self.maxHeaderSize = maxHeaderSize 1110 | self.maxDynamicTableSize = maxHeaderTableSize 1111 | self.encoderMaxDynamicTableSize = maxHeaderTableSize 1112 | self.maxDynamicTableSizeChangeRequired = false 1113 | self.state = .readHeaderRepresentation 1114 | } 1115 | 1116 | func reset() { 1117 | headerSize = 0 1118 | state = .readHeaderRepresentation 1119 | indexType = .none 1120 | } 1121 | 1122 | func endHeaderBlock() -> Bool { 1123 | let truncated = headerSize > maxHeaderSize 1124 | reset() 1125 | return truncated 1126 | } 1127 | 1128 | func setMaxHeaderTableSize(maxHeaderTableSize: Int) { 1129 | maxDynamicTableSize = maxHeaderTableSize 1130 | if maxDynamicTableSize < encoderMaxDynamicTableSize { 1131 | maxDynamicTableSizeChangeRequired = true 1132 | dynamicTable.capacity = maxDynamicTableSize 1133 | } 1134 | } 1135 | 1136 | func getMaxHeaderTableSize() -> Int { 1137 | return dynamicTable.capacity 1138 | } 1139 | 1140 | var length: Int { return dynamicTable.length } 1141 | var size: Int { return dynamicTable.size } 1142 | 1143 | func getHeaderField(_ index: Int) -> HeaderField { 1144 | return dynamicTable.getEntry(index + 1) 1145 | } 1146 | 1147 | func setDynamicTableSize(_ dynamicTableSize: Int) { 1148 | encoderMaxDynamicTableSize = dynamicTableSize 1149 | maxDynamicTableSizeChangeRequired = false 1150 | dynamicTable.capacity = dynamicTableSize 1151 | } 1152 | 1153 | func readName(_ index: Int) throws { 1154 | // print("index \(index)") 1155 | if index <= StaticTable.length { 1156 | name = StaticTable.getEntry(index).name 1157 | } else if index - StaticTable.length <= dynamicTable.length { 1158 | name = dynamicTable.getEntry(index - StaticTable.length).name 1159 | } else { 1160 | throw Exception.illegalIndexValue 1161 | } 1162 | } 1163 | 1164 | func indexHeader(_ index: Int, headerListener: HeaderListener) throws { 1165 | if index <= StaticTable.length { 1166 | let headerField = StaticTable.getEntry(index) 1167 | addHeader(headerListener: headerListener, name: headerField.name, value: headerField.value, sensitive: false) 1168 | } else if index - StaticTable.length <= dynamicTable.length { 1169 | let headerField = dynamicTable.getEntry(index - StaticTable.length) 1170 | addHeader(headerListener: headerListener, name: headerField.name, value: headerField.value, sensitive: false) 1171 | } else { 1172 | throw Exception.illegalIndexValue 1173 | } 1174 | } 1175 | 1176 | func addHeader(headerListener listener: HeaderListener, name: [UInt8], value: [UInt8], sensitive: Bool) { 1177 | listener.addHeader(name: name, value: value, sensitive: sensitive) 1178 | let newSize = headerSize + name.count + value.count 1179 | if newSize <= maxHeaderSize { 1180 | headerSize = newSize 1181 | } else { 1182 | headerSize = maxHeaderSize + 1 1183 | } 1184 | } 1185 | 1186 | func insertHeader(headerListener listener: HeaderListener, name: [UInt8], value: [UInt8], indexType: IndexType) { 1187 | addHeader(headerListener: listener, name: name, value: value, sensitive: indexType == .never) 1188 | switch indexType { 1189 | case .none, .never: 1190 | () 1191 | case .incremental: 1192 | dynamicTable.add(HeaderField(name: name, value: value)) 1193 | } 1194 | } 1195 | 1196 | func exceedsMaxHeaderSize(_ size: Int) -> Bool { 1197 | if size + headerSize <= maxHeaderSize { 1198 | return false 1199 | } 1200 | headerSize = maxHeaderSize + 1 1201 | return true 1202 | } 1203 | 1204 | func readStringLiteral(_ input: Bytes, length: Int) throws -> [UInt8] { 1205 | let read = input.exportBytes(count: length) 1206 | if read.count != length { 1207 | throw Exception.decompressionException 1208 | } 1209 | if huffmanEncoded { 1210 | return try huffmanDecoderInstance.decode(read) 1211 | } else { 1212 | return read 1213 | } 1214 | } 1215 | 1216 | func decodeULE128(_ input: Bytes) throws -> Int { 1217 | let oldPos = input.position 1218 | var result = 0 1219 | var shift = 0 1220 | while shift < 32 { 1221 | if input.availableExportBytes == 0 { 1222 | input.position = oldPos 1223 | return -1 1224 | } 1225 | let b = input.export8Bits() 1226 | if shift == 28 && (b & 0xF8) != 0 { 1227 | break 1228 | } 1229 | result |= Int(b & 0x7F) << shift 1230 | if (b & 0x80) == 0 { 1231 | return result 1232 | } 1233 | shift += 7 1234 | } 1235 | input.position = oldPos 1236 | throw Exception.decompressionException 1237 | } 1238 | 1239 | /// Decode the headers, sending them sequentially to headerListener. 1240 | func decode(input inpt: Bytes, headerListener: HeaderListener) throws { 1241 | while inpt.availableExportBytes > 0 { 1242 | switch state { 1243 | case .readHeaderRepresentation: 1244 | let b = inpt.export8Bits() 1245 | if maxDynamicTableSizeChangeRequired && (b & 0xE0) != 0x20 { 1246 | throw Exception.maxDynamicTableSizeChangeRequested 1247 | } 1248 | if (b & 0x80) != 0 { //b < 0 { 1249 | index = Int(b & 0x7F) 1250 | if index == 0 { 1251 | throw Exception.illegalIndexValue 1252 | } else if index == 0x7F { 1253 | state = .readIndexedHeader 1254 | } else { 1255 | try indexHeader(index, headerListener: headerListener) 1256 | } 1257 | } else if (b & 0x40) == 0x40 { 1258 | indexType = .incremental 1259 | index = Int(b & 0x3F) 1260 | if index == 0 { 1261 | state = .readLiteralHeaderNameLengthPrefix 1262 | } else if index == 0x3F { 1263 | state = .readIndexedHeaderName 1264 | } else { 1265 | try readName(index) 1266 | state = .readLiteralHeaderValueLengthPrefix 1267 | } 1268 | } else if (b & 0x20) == 0x20 { 1269 | index = Int(b & 0x1F) 1270 | if index == 0x1F { 1271 | state = .readMaxDynamicTableSize 1272 | } else { 1273 | setDynamicTableSize(index) 1274 | state = .readHeaderRepresentation 1275 | } 1276 | } else { 1277 | indexType = (b & 0x10) == 0x10 ? .never : .none 1278 | index = Int(b & 0x0F) 1279 | if index == 0 { 1280 | state = .readLiteralHeaderNameLengthPrefix 1281 | } else if index == 0x0F { 1282 | state = .readIndexedHeaderName 1283 | } else { 1284 | try readName(index) 1285 | state = .readLiteralHeaderValueLengthPrefix 1286 | } 1287 | } 1288 | 1289 | case .readMaxDynamicTableSize: 1290 | let maxSize = try decodeULE128(inpt) 1291 | if maxSize == -1 { 1292 | return 1293 | } 1294 | if maxSize > HPACKEncoder.indexMax - index { 1295 | throw Exception.decompressionException 1296 | } 1297 | setDynamicTableSize(index + maxSize) 1298 | state = .readHeaderRepresentation 1299 | 1300 | case .readIndexedHeader: 1301 | let headerIndex = try decodeULE128(inpt) 1302 | if headerIndex == -1 { 1303 | return 1304 | } 1305 | if headerIndex > HPACKEncoder.indexMax - index { 1306 | throw Exception.decompressionException 1307 | } 1308 | try indexHeader(index + headerIndex, headerListener: headerListener) 1309 | state = .readHeaderRepresentation 1310 | 1311 | case .readIndexedHeaderName: 1312 | let nameIndex = try decodeULE128(inpt) 1313 | if nameIndex == -1 { 1314 | return 1315 | } 1316 | if nameIndex > HPACKEncoder.indexMax - index { 1317 | throw Exception.decompressionException 1318 | } 1319 | try readName(index + nameIndex) 1320 | state = .readLiteralHeaderValueLengthPrefix 1321 | 1322 | case .readLiteralHeaderNameLengthPrefix: 1323 | 1324 | let b = inpt.export8Bits() 1325 | huffmanEncoded = (b & 0x80) == 0x80 1326 | index = Int(b & 0x7F) 1327 | if index == 0x7F { 1328 | state = .readLiteralHeaderNameLength 1329 | } else { 1330 | nameLength = index 1331 | if nameLength == 0 { 1332 | throw Exception.decompressionException 1333 | } 1334 | if exceedsMaxHeaderSize(nameLength) { 1335 | if indexType == .none { 1336 | name = HPACKDecoder.empty 1337 | skipLength = nameLength 1338 | state = .skipLiteralHeaderName 1339 | break // check me 1340 | } 1341 | if nameLength + HeaderField.headerEntryOverhead > dynamicTable.capacity { 1342 | dynamicTable.clear() 1343 | name = HPACKDecoder.empty 1344 | skipLength = nameLength 1345 | state = .skipLiteralHeaderName 1346 | break 1347 | } 1348 | } 1349 | state = .readLiteralHeaderName 1350 | } 1351 | 1352 | case .readLiteralHeaderNameLength: 1353 | 1354 | nameLength = try decodeULE128(inpt) 1355 | if nameLength == -1 { 1356 | return 1357 | } 1358 | if nameLength > HPACKEncoder.indexMax - index { 1359 | throw Exception.decompressionException 1360 | } 1361 | nameLength += index 1362 | if exceedsMaxHeaderSize(nameLength) { 1363 | if indexType == .none { 1364 | name = HPACKDecoder.empty 1365 | skipLength = nameLength 1366 | state = .skipLiteralHeaderName 1367 | break // check me 1368 | } 1369 | if nameLength + HeaderField.headerEntryOverhead > dynamicTable.capacity { 1370 | dynamicTable.clear() 1371 | name = HPACKDecoder.empty 1372 | skipLength = nameLength 1373 | state = .skipLiteralHeaderName 1374 | break 1375 | } 1376 | } 1377 | state = .readLiteralHeaderName 1378 | 1379 | case .readLiteralHeaderName: 1380 | 1381 | if inpt.availableExportBytes < nameLength { 1382 | return 1383 | } 1384 | 1385 | name = try readStringLiteral(inpt, length: nameLength) 1386 | state = .readLiteralHeaderValueLengthPrefix 1387 | 1388 | case .skipLiteralHeaderName: 1389 | 1390 | let toSkip = min(skipLength, inpt.availableExportBytes) 1391 | inpt.position += toSkip 1392 | skipLength -= toSkip 1393 | if skipLength == 0 { 1394 | state = .readLiteralHeaderValueLengthPrefix 1395 | } 1396 | 1397 | case .readLiteralHeaderValueLengthPrefix: 1398 | 1399 | let b = inpt.export8Bits() 1400 | huffmanEncoded = (b & 0x80) == 0x80 1401 | index = Int(b & 0x7F) 1402 | if index == 0x7f { 1403 | state = .readLiteralHeaderValueLength 1404 | } else { 1405 | valueLength = index 1406 | let newHeaderSize = nameLength + valueLength 1407 | if exceedsMaxHeaderSize(newHeaderSize) { 1408 | headerSize = maxHeaderSize + 1 1409 | if indexType == .none { 1410 | state = .skipLiteralHeaderValue 1411 | break 1412 | } 1413 | if newHeaderSize + HeaderField.headerEntryOverhead > dynamicTable.capacity { 1414 | dynamicTable.clear() 1415 | state = .skipLiteralHeaderValue 1416 | break 1417 | } 1418 | } 1419 | 1420 | if valueLength == 0 { 1421 | insertHeader(headerListener: headerListener, name: name!, value: HPACKDecoder.empty, indexType: indexType) 1422 | state = .readHeaderRepresentation 1423 | } else { 1424 | state = .readLiteralHeaderValue 1425 | } 1426 | } 1427 | 1428 | case .readLiteralHeaderValueLength: 1429 | 1430 | valueLength = try decodeULE128(inpt) 1431 | if valueLength == -1 { 1432 | return 1433 | } 1434 | if valueLength > HPACKEncoder.indexMax - index { 1435 | throw Exception.decompressionException 1436 | } 1437 | valueLength += index 1438 | 1439 | let newHeaderSize = nameLength + valueLength 1440 | if newHeaderSize + headerSize > maxHeaderSize { 1441 | headerSize = maxHeaderSize + 1 1442 | if indexType == .none { 1443 | state = .skipLiteralHeaderValue 1444 | break 1445 | } 1446 | if newHeaderSize + HeaderField.headerEntryOverhead > dynamicTable.capacity { 1447 | dynamicTable.clear() 1448 | state = .skipLiteralHeaderValue 1449 | break 1450 | } 1451 | } 1452 | state = .readLiteralHeaderValue 1453 | 1454 | case .readLiteralHeaderValue: 1455 | 1456 | if inpt.availableExportBytes < valueLength { 1457 | return 1458 | } 1459 | 1460 | let value = try readStringLiteral(inpt, length: valueLength) 1461 | insertHeader(headerListener: headerListener, name: name!, value: value, indexType: indexType) 1462 | state = .readHeaderRepresentation 1463 | 1464 | case .skipLiteralHeaderValue: 1465 | let toSkip = min(valueLength, inpt.availableExportBytes) 1466 | inpt.position += toSkip 1467 | valueLength -= toSkip 1468 | if valueLength == 0 { 1469 | state = .readHeaderRepresentation 1470 | } 1471 | } 1472 | } 1473 | } 1474 | } 1475 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/HTTP/HTTP2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTP2.swift 3 | // PerfectLib 4 | // 5 | // Created by Kyle Jessup on 2016-02-18. 6 | // Copyright © 2016 PerfectlySoft. All rights reserved. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | let settingsHeaderTableSize: UInt16 = 0x1 21 | let settingsEnablePush: UInt16 = 0x2 22 | let settingsMaxConcurrentStreams: UInt16 = 0x3 23 | let settingsInitialWindowSize: UInt16 = 0x4 24 | let settingsMaxFrameSize: UInt16 = 0x5 25 | let settingsMaxHeaderListSize: UInt16 = 0x6 26 | 27 | let http2ConnectionPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" 28 | 29 | var http2Debug = false 30 | 31 | enum HTTP2StreamState { 32 | case idle, reserved, open, halfClosed, closed 33 | } 34 | 35 | enum HTTP2Error: UInt32 { 36 | case noError = 0x0 37 | case protocolError = 0x1 38 | case internalError = 0x2 39 | case flowControlError = 0x3 40 | case settingsTimeout = 0x4 41 | case streamClosed = 0x5 42 | case frameSizeError = 0x6 43 | case refusedStream = 0x7 44 | case cancel = 0x8 45 | case compressionError = 0x9 46 | case connectError = 0xa 47 | case enhanceYourCalm = 0xb 48 | case inadequateSecurity = 0xc 49 | case http11Required = 0xd 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/HTTP/HTTP2Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTP2.swift 3 | // PerfectLib 4 | // 5 | // Created by Kyle Jessup on 2016-02-18. 6 | // Copyright © 2016 PerfectlySoft. All rights reserved. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | // NOTE: This HTTP/2 client is competent enough to operate with Apple's push notification service, but 21 | // still lacks some functionality to make it general purpose. Consider it a work in-progress. 22 | 23 | import PerfectNet 24 | import PerfectThread 25 | import PerfectLib 26 | 27 | #if os(Linux) 28 | import SwiftGlibc 29 | #endif 30 | 31 | final class HTTPRequest { 32 | var method = HTTPMethod.get 33 | var path = "" 34 | var headerStore = Dictionary() 35 | var postBodyBytes: [UInt8]? = [] 36 | var connection: NetTCP 37 | init(connection: NetTCP) { 38 | self.connection = connection 39 | } 40 | func setHeader(_ named: HTTPRequestHeader.Name, value: String) { 41 | headerStore[named] = [UInt8](value.utf8) 42 | } 43 | } 44 | 45 | final class HTTPResponse: HeaderListener { 46 | var status = HTTPResponseStatus.ok 47 | var headerStore = Array<(HTTPResponseHeader.Name, String)>() 48 | var bodyBytes = [UInt8]() 49 | 50 | func addHeader(name nam: [UInt8], value: [UInt8], sensitive: Bool) { 51 | let n = UTF8Encoding.encode(bytes: nam) 52 | let v = UTF8Encoding.encode(bytes: value) 53 | switch n { 54 | case ":status": 55 | status = HTTPResponseStatus.statusFrom(code: Int(v) ?? 200) 56 | default: 57 | headerStore.append((HTTPResponseHeader.Name.fromStandard(name: n), v)) 58 | } 59 | } 60 | } 61 | 62 | class HTTP2Client { 63 | 64 | enum StreamState { 65 | case none, idle, reservedLocal, reservedRemote, open, halfClosedRemote, halfClosedLocal, closed 66 | } 67 | 68 | let net = NetTCPSSL() 69 | var host = "" 70 | var timeoutSeconds = 10.0 71 | var ssl = true 72 | var streams = [UInt32:StreamState]() 73 | var streamCounter = UInt32(1) 74 | 75 | let encoder = HPACKEncoder() 76 | let decoder = HPACKDecoder() 77 | 78 | let closeLock = Threading.Lock() 79 | 80 | let frameReadEvent = Threading.Event() 81 | var frameQueue = [HTTP2Frame]() 82 | var frameReadOK = false 83 | 84 | var newStreamId: UInt32 { 85 | streams[streamCounter] = StreamState.none 86 | let s = streamCounter 87 | streamCounter += 2 88 | return s 89 | } 90 | 91 | init() { 92 | 93 | } 94 | 95 | func dequeueFrame(timeoutSeconds timeout: Double) -> HTTP2Frame? { 96 | var frame: HTTP2Frame? = nil 97 | frameReadEvent.doWithLock { 98 | if self.frameQueue.count == 0 { 99 | let _ = self.frameReadEvent.wait(seconds: timeout) 100 | } 101 | if self.frameQueue.count > 0 { 102 | frame = self.frameQueue.removeFirst() 103 | } 104 | } 105 | return frame 106 | } 107 | 108 | func dequeueFrame(timeoutSeconds timeout: Double, streamId: UInt32) -> HTTP2Frame? { 109 | var frame: HTTP2Frame? = nil 110 | frameReadEvent.doWithLock { 111 | if self.frameQueue.count == 0 { 112 | let _ = self.frameReadEvent.wait(seconds: timeout) 113 | } 114 | if self.frameQueue.count > 0 { 115 | for i in 0..= 6 { 130 | let identifier = b.export16Bits().netToHost 131 | // let value = b.export32Bits().netToHost 132 | 133 | // print("Setting \(identifier) \(value)") 134 | 135 | switch identifier { 136 | // case SETTINGS_HEADER_TABLE_SIZE: 137 | // ()//self.encoder = HPACKEncoder(maxCapacity: Int(value)) 138 | // case SETTINGS_ENABLE_PUSH: 139 | // () 140 | // case SETTINGS_MAX_CONCURRENT_STREAMS: 141 | // () 142 | // case SETTINGS_INITIAL_WINDOW_SIZE: 143 | // () 144 | // case SETTINGS_MAX_FRAME_SIZE: 145 | // () 146 | // case SETTINGS_MAX_HEADER_LIST_SIZE: 147 | // () 148 | default: 149 | () 150 | } 151 | } 152 | } 153 | 154 | func readOneFrame() { 155 | Threading.dispatch { 156 | self.readHTTP2Frame(timeout: -1) { [weak self] 157 | f in 158 | if let frame = f { 159 | // print("Read frame \(frame.typeStr) \(frame.flagsStr) \(frame.streamId)") 160 | // if frame.length > 0 { 161 | // print("Read frame payload \(frame.length) \(UTF8Encoding.encode(bytes: frame.payload!))") 162 | // } 163 | self?.frameReadEvent.doWithLock { 164 | switch frame.type { 165 | case .settings: 166 | let endStream = (frame.flags & flagSettingsAck) != 0 167 | if !endStream { // ACK settings receipt 168 | if let payload = frame.payload { 169 | self?.processSettingsPayload(Bytes(existingBytes: payload)) 170 | } 171 | let response = HTTP2Frame(length: 0, 172 | type: HTTP2FrameType.settings.rawValue, 173 | flags: flagSettingsAck, 174 | streamId: 0, 175 | payload: nil) 176 | self?.writeHTTP2Frame(response) { 177 | b in 178 | self?.readOneFrame() 179 | } 180 | } else { // ACK of our settings frame 181 | self?.readOneFrame() 182 | } 183 | case .ping: 184 | let endStream = (frame.flags & flagPingAck) != 0 185 | if !endStream { // ACK ping receipt 186 | if let payload = frame.payload { 187 | self?.processSettingsPayload(Bytes(existingBytes: payload)) 188 | } 189 | let response = HTTP2Frame(length: frame.length, 190 | type: HTTP2FrameType.ping.rawValue, 191 | flags: flagPingAck, 192 | streamId: 0, 193 | payload: frame.payload) 194 | self?.writeHTTP2Frame(response) { 195 | b in 196 | self?.readOneFrame() 197 | } 198 | } else { // ACK of our ping frame 199 | fallthrough 200 | } 201 | default: 202 | self?.frameQueue.append(frame) 203 | self?.frameReadOK = true 204 | _ = self?.frameReadEvent.broadcast() 205 | } 206 | } 207 | } else { // network error 208 | self?.frameReadEvent.doWithLock { 209 | self?.close() 210 | self?.frameReadOK = false 211 | let _ = self?.frameReadEvent.broadcast() 212 | } 213 | } 214 | } 215 | } 216 | } 217 | 218 | func startReadThread() { 219 | Threading.dispatch { [weak self] in 220 | // dbg 221 | defer { 222 | print("~HTTP2Client.startReadThread") 223 | } 224 | guard let net = self?.net else { 225 | return 226 | } 227 | while net.isValid { 228 | guard let s = self else { 229 | net.close() 230 | break 231 | } 232 | s.frameReadEvent.doWithLock { 233 | s.frameReadOK = false 234 | s.readOneFrame() 235 | if !s.frameReadOK && net.isValid { 236 | _ = s.frameReadEvent.wait() 237 | } 238 | } 239 | if !s.frameReadOK { 240 | s.close() 241 | break 242 | } 243 | } 244 | } 245 | } 246 | 247 | func close() { 248 | closeLock.doWithLock { 249 | self.net.shutdown() 250 | } 251 | } 252 | 253 | var isConnected: Bool { 254 | return net.isValid 255 | } 256 | 257 | func connect(host hst: String, port: UInt16, ssl: Bool, timeoutSeconds: Double, callback: @escaping (Bool) -> ()) { 258 | self.host = hst 259 | self.ssl = ssl 260 | self.timeoutSeconds = timeoutSeconds 261 | do { 262 | try net.connect(address: hst, port: port, timeoutSeconds: timeoutSeconds) { 263 | n in 264 | if let net = n as? NetTCPSSL { 265 | net.fd.switchToNonBlocking() 266 | net.fd.switchToBlocking() // !FIX! 267 | self.completeConnect(callback) 268 | } else { 269 | callback(false) 270 | } 271 | } 272 | } catch { 273 | callback(false) 274 | } 275 | } 276 | 277 | func createRequest() -> HTTPRequest { 278 | return HTTPRequest(connection: net) 279 | } 280 | 281 | func awaitResponse(streamId stream: UInt32, request: HTTPRequest, callback: (HTTPResponse?, String?) -> ()) { 282 | let response = HTTPResponse()//request: request) 283 | var streamOpen = true 284 | while streamOpen { 285 | let f = dequeueFrame(timeoutSeconds: timeoutSeconds, streamId: stream) 286 | if let frame = f { 287 | switch frame.type { 288 | case .goAway: 289 | let bytes = Bytes(existingBytes: frame.payload!) 290 | let streamId = bytes.export32Bits().netToHost 291 | let errorCode = bytes.export32Bits().netToHost 292 | var message = "" 293 | if bytes.availableExportBytes > 0 { 294 | message = UTF8Encoding.encode(bytes: bytes.exportBytes(count: bytes.availableExportBytes)) 295 | } 296 | 297 | let bytes2 = Bytes() 298 | let _ = bytes2.import32Bits(from: streamId.hostToNet) 299 | .import32Bits(from: 0) 300 | let frame2 = HTTP2Frame(length: 8, 301 | type: HTTP2FrameType.goAway.rawValue, 302 | flags: 0, 303 | streamId: streamId, 304 | payload: bytes2.data) 305 | self.writeHTTP2Frame(frame2) { 306 | b in 307 | 308 | self.close() 309 | } 310 | streamOpen = false 311 | callback(nil, "\(errorCode) \(message)") 312 | case .headers: 313 | let padded = (frame.flags & flagPadded) != 0 314 | // let priority = (frame.flags & HTTP2_PRIORITY) != 0 315 | // let end = (frame.flags & HTTP2_END_HEADERS) != 0 316 | 317 | if let ba = frame.payload, ba.count > 0 { 318 | let bytes = Bytes(existingBytes: ba) 319 | var padLength: UInt8 = 0 320 | // var streamDep = UInt32(0) 321 | // var weight = UInt8(0) 322 | 323 | if padded { 324 | padLength = bytes.export8Bits() 325 | } 326 | // if priority { 327 | // streamDep = bytes.export32Bits() 328 | // weight = bytes.export8Bits() 329 | // } 330 | self.decodeHeaders(from: bytes, endPosition: ba.count - Int(padLength), listener: response) 331 | } 332 | streamOpen = (frame.flags & flagEndStream) == 0 333 | if !streamOpen { 334 | callback(response, nil) 335 | } 336 | case .data: 337 | if let payload = frame.payload, frame.length > 0 { 338 | response.bodyBytes.append(contentsOf: payload) 339 | } 340 | streamOpen = (frame.flags & flagEndStream) == 0 341 | if !streamOpen { 342 | callback(response, nil) 343 | } 344 | default: 345 | streamOpen = false 346 | callback(nil, "Unexpected frame type \(frame.typeStr)") 347 | } 348 | 349 | } else { 350 | close() 351 | streamOpen = false 352 | callback(nil, "Connection dropped") 353 | } 354 | } 355 | } 356 | 357 | func sendPing(callback: @escaping (Bool) -> ()) { 358 | let frame = HTTP2Frame(type: .ping, flags: 0, streamId: 0, payload: [0, 0, 0, 0, 0, 0, 0, 0]) 359 | writeHTTP2Frame(frame) { 360 | ok in 361 | guard ok, 362 | let response = self.dequeueFrame(timeoutSeconds: timeoutSeconds, streamId: 0), 363 | response.type == .ping else { 364 | return callback(false) 365 | } 366 | return callback(true) 367 | } 368 | } 369 | 370 | func sendRequest(_ request: HTTPRequest, callback: @escaping (HTTPResponse?, String?) -> ()) { 371 | let streamId = newStreamId 372 | streams[streamId] = .idle 373 | 374 | let headerBytes = Bytes() 375 | let method = request.method 376 | let scheme = ssl ? "https" : "http" 377 | let path = request.path 378 | 379 | do { 380 | let encoder = HPACKEncoder() 381 | try encoder.encodeHeader(out: headerBytes, nameStr: ":method", valueStr: method.description) 382 | try encoder.encodeHeader(out: headerBytes, nameStr: ":scheme", valueStr: scheme) 383 | try encoder.encodeHeader(out: headerBytes, nameStr: ":path", valueStr: path, sensitive: false, incrementalIndexing: false) 384 | try encoder.encodeHeader(out: headerBytes, nameStr: "host", valueStr: self.host) 385 | try encoder.encodeHeader(out: headerBytes, nameStr: "content-length", valueStr: "\(request.postBodyBytes?.count ?? 0)") 386 | for (name, value) in request.headerStore { 387 | let lowered = name.standardName.lowercased() 388 | var inc = true 389 | // this is APNS specific in that Apple wants the apns-id and apns-expiration headers to be indexed on the first request but not indexed on subsequent requests 390 | // !FIX! need to enable the caller to indicate policies such as this 391 | let n = UTF8Encoding.decode(string: lowered) 392 | let v = UTF8Encoding.decode(string: String(validatingUTF8: value) ?? "") 393 | if streamId > 1 { // at least the second request 394 | inc = !(lowered == "apns-id" || lowered == "apns-expiration") 395 | } 396 | try encoder.encodeHeader(out: headerBytes, name: n, value: v, sensitive: false, incrementalIndexing: inc) 397 | } 398 | } catch { 399 | return callback(nil, "Header encoding exception \(error)") 400 | } 401 | let hasData = nil != request.postBodyBytes && request.postBodyBytes!.count > 0 402 | let frame = HTTP2Frame(length: UInt32(headerBytes.data.count), 403 | type: HTTP2FrameType.headers.rawValue, 404 | flags: flagEndHeaders | (hasData ? 0 : flagEndStream), 405 | streamId: streamId, 406 | payload: headerBytes.data) 407 | writeHTTP2Frame(frame) { [weak self] 408 | b in 409 | guard b else { 410 | return callback(nil, "Unable to write frame") 411 | } 412 | guard let s = self else { 413 | return callback(nil, nil) 414 | } 415 | s.streams[streamId] = .open 416 | if hasData { 417 | let frame2 = HTTP2Frame(length: UInt32(request.postBodyBytes?.count ?? 0), 418 | type: HTTP2FrameType.data.rawValue, 419 | flags: flagEndStream, 420 | streamId: streamId, 421 | payload: request.postBodyBytes) 422 | s.writeHTTP2Frame(frame2) { [weak self] 423 | b in 424 | guard let s = self else { 425 | return callback(nil, nil) 426 | } 427 | s.awaitResponse(streamId: streamId, request: request, callback: callback) 428 | } 429 | } else { 430 | s.awaitResponse(streamId: streamId, request: request, callback: callback) 431 | } 432 | } 433 | } 434 | 435 | func completeConnect(_ callback: @escaping (Bool) -> ()) { 436 | net.write(string: http2ConnectionPreface) { 437 | wrote in 438 | let settings = HTTP2Frame(length: 0, 439 | type: HTTP2FrameType.settings.rawValue, 440 | flags: 0, 441 | streamId: 0, 442 | payload: nil) 443 | self.writeHTTP2Frame(settings) { [weak self] 444 | b in 445 | if b { 446 | self?.startReadThread() 447 | } 448 | callback(b) 449 | } 450 | } 451 | } 452 | 453 | func bytesToHeader(_ b: [UInt8]) -> HTTP2Frame { 454 | let payloadLength = (UInt32(b[0]) << 16) + (UInt32(b[1]) << 8) + UInt32(b[2]) 455 | 456 | let type = b[3] 457 | let flags = b[4] 458 | var sid: UInt32 = UInt32(b[5]) 459 | sid <<= 8 460 | sid += UInt32(b[6]) 461 | sid <<= 8 462 | sid += UInt32(b[7]) 463 | sid <<= 8 464 | sid += UInt32(b[8]) 465 | 466 | sid &= ~0x80000000 467 | 468 | return HTTP2Frame(length: payloadLength, type: type, flags: flags, streamId: sid, payload: nil) 469 | } 470 | 471 | func readHTTP2Frame(timeout time: Double, callback: @escaping (HTTP2Frame?) -> ()) { 472 | let net = self.net 473 | net.readBytesFully(count: 9, timeoutSeconds: time) { 474 | bytes in 475 | if let b = bytes { 476 | var header = self.bytesToHeader(b) 477 | if header.length > 0 { 478 | net.readBytesFully(count: Int(header.length), timeoutSeconds: time) { 479 | bytes in 480 | header.payload = bytes 481 | callback(header) 482 | } 483 | } else { 484 | callback(header) 485 | } 486 | 487 | } else { 488 | callback(nil) 489 | } 490 | } 491 | } 492 | 493 | func writeHTTP2Frame(_ frame: HTTP2Frame, callback: (Bool) -> ()) { 494 | if !net.isValid { 495 | callback(false) 496 | } else if !net.writeFully(bytes: frame.headerBytes()) { 497 | callback(false) 498 | } else { 499 | if let p = frame.payload { 500 | callback(net.writeFully(bytes: p)) 501 | } else { 502 | callback(true) 503 | } 504 | } 505 | } 506 | 507 | func encodeHeaders(headers: [(String, String)]) -> Bytes { 508 | let b = Bytes() 509 | for header in headers { 510 | let n = UTF8Encoding.decode(string: header.0) 511 | let v = UTF8Encoding.decode(string: header.1) 512 | do { 513 | try encoder.encodeHeader(out: b, name: n, value: v, sensitive: false) 514 | } catch { 515 | self.close() 516 | break 517 | } 518 | } 519 | return b 520 | } 521 | 522 | func decodeHeaders(from frm: Bytes, endPosition: Int, listener: HeaderListener) { 523 | do { 524 | try decoder.decode(input: frm, headerListener: listener) 525 | } catch { 526 | print("error while decoding headers \(error)") 527 | self.close() 528 | } 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/HTTP/HTTP2Frame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTP2Frame.swift 3 | // PerfectHTTPServer 4 | // 5 | // Created by Kyle Jessup on 2017-06-20. 6 | 7 | enum HTTP2FrameType: UInt8 { 8 | case data = 0x0 9 | case headers = 0x1 10 | case priority = 0x2 11 | case cancelStream = 0x3 12 | case settings = 0x4 13 | case pushPromise = 0x5 14 | case ping = 0x6 15 | case goAway = 0x7 16 | case windowUpdate = 0x8 17 | case continuation = 0x9 18 | 19 | var description: String { 20 | switch self { 21 | case .data: return "HTTP2_DATA" 22 | case .headers: return "HTTP2_HEADERS" 23 | case .priority: return "HTTP2_PRIORITY" 24 | case .cancelStream: return "HTTP2_RST_STREAM" 25 | case .settings: return "HTTP2_SETTINGS" 26 | case .pushPromise: return "HTTP2_PUSH_PROMISE" 27 | case .ping: return "HTTP2_PING" 28 | case .goAway: return "HTTP2_GOAWAY" 29 | case .windowUpdate: return "HTTP2_WINDOW_UPDATE" 30 | case .continuation: return "HTTP2_CONTINUATION" 31 | } 32 | } 33 | } 34 | 35 | typealias HTTP2FrameFlag = UInt8 36 | 37 | let flagEndStream: HTTP2FrameFlag = 0x1 38 | let flagEndHeaders: HTTP2FrameFlag = 0x4 39 | let flagPadded: HTTP2FrameFlag = 0x8 40 | let flagPriority: HTTP2FrameFlag = 0x20 41 | let flagSettingsAck: HTTP2FrameFlag = 0x1 42 | let flagPingAck: HTTP2FrameFlag = 0x1 43 | 44 | struct HTTP2Frame { 45 | let length: UInt32 // 24-bit 46 | let type: HTTP2FrameType 47 | let flags: UInt8 48 | let streamId: UInt32 // 31-bit 49 | var payload: [UInt8]? 50 | var sentCallback: ((Bool) -> ())? = nil 51 | var willSendCallback: (() -> ())? = nil 52 | 53 | // Deprecate this 54 | init(length: UInt32, 55 | type: UInt8, 56 | flags: UInt8 = 0, 57 | streamId: UInt32 = 0, 58 | payload: [UInt8]? = nil) { 59 | self.length = length 60 | self.type = HTTP2FrameType(rawValue: type)! 61 | self.flags = flags 62 | self.streamId = streamId 63 | self.payload = payload 64 | } 65 | 66 | init(type: HTTP2FrameType, 67 | flags: UInt8 = 0, 68 | streamId: UInt32 = 0, 69 | payload: [UInt8]? = nil) { 70 | self.length = UInt32(payload?.count ?? 0) 71 | self.type = type 72 | self.flags = flags 73 | self.streamId = streamId 74 | self.payload = payload 75 | } 76 | 77 | var typeStr: String { 78 | return type.description 79 | } 80 | 81 | var flagsStr: String { 82 | var s = "" 83 | if flags == 0 { 84 | s.append("NO FLAGS") 85 | } 86 | if (flags & flagEndStream) != 0 { 87 | s.append(" +HTTP2_END_STREAM") 88 | } 89 | if (flags & flagEndHeaders) != 0 { 90 | s.append(" +HTTP2_END_HEADERS") 91 | } 92 | return s 93 | } 94 | 95 | func headerBytes() -> [UInt8] { 96 | var data = [UInt8]() 97 | 98 | let l = length.hostToNet >> 8 99 | data.append(UInt8(l & 0xFF)) 100 | data.append(UInt8((l >> 8) & 0xFF)) 101 | data.append(UInt8((l >> 16) & 0xFF)) 102 | 103 | data.append(type.rawValue) 104 | data.append(flags) 105 | 106 | let s = streamId.hostToNet 107 | data.append(UInt8(s & 0xFF)) 108 | data.append(UInt8((s >> 8) & 0xFF)) 109 | data.append(UInt8((s >> 16) & 0xFF)) 110 | data.append(UInt8((s >> 24) & 0xFF)) 111 | return data 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/HTTP/HTTPHeaders.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHeaders.swift 3 | // PerfectLib 4 | // 5 | // Created by Kyle Jessup on 2016-06-17. 6 | // Copyright (C) 2016 PerfectlySoft, Inc. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | /// An HTTP request header. 21 | enum HTTPRequestHeader { 22 | /// A header name type. Each has a corresponding value type. 23 | enum Name: Hashable { 24 | case accept, acceptCharset, acceptEncoding, acceptLanguage, acceptDatetime, authorization 25 | case cacheControl, connection, cookie, contentLength, contentMD5, contentType 26 | case date, expect, forwarded, from, host 27 | case ifMatch, ifModifiedSince, ifNoneMatch, ifRange, ifUnmodifiedSince 28 | case maxForwards, origin, pragma, proxyAuthorization, range, referer 29 | case te, userAgent, upgrade, via, warning, xRequestedWith, xRequestedBy, dnt 30 | case xAuthorization, xForwardedFor, xForwardedHost, xForwardedProto 31 | case frontEndHttps, xHttpMethodOverride, xATTDeviceId, xWapProfile 32 | case proxyConnection, xUIDH, xCsrfToken, accessControlRequestMethod, accessControlRequestHeaders 33 | case xB3TraceId, xB3SpanId, xB3ParentSpanId 34 | case custom(name: String) 35 | 36 | func hash(into hasher: inout Hasher) { 37 | hasher.combine(standardName.lowercased()) 38 | } 39 | 40 | var standardName: String { 41 | switch self { 42 | case .accept: return "Accept" 43 | case .acceptCharset: return "Accept-Charset" 44 | case .acceptEncoding: return "Accept-Encoding" 45 | case .acceptLanguage: return "Accept-Language" 46 | case .acceptDatetime: return "Accept-Datetime" 47 | case .accessControlRequestMethod: return "Access-Control-Request-Method" 48 | case .accessControlRequestHeaders: return "Access-Control-Request-Headers" 49 | case .authorization: return "Authorization" 50 | case .cacheControl: return "Cache-Control" 51 | case .connection: return "Connection" 52 | case .cookie: return "Cookie" 53 | case .contentLength: return "Content-Length" 54 | case .contentMD5: return "Content-MD5" 55 | case .contentType: return "Content-Type" 56 | case .date: return "Date" 57 | case .expect: return "Expect" 58 | case .forwarded: return "Forwarded" 59 | case .from: return "From" 60 | case .host: return "Host" 61 | case .ifMatch: return "If-Match" 62 | case .ifModifiedSince: return "If-Modified-Since" 63 | case .ifNoneMatch: return "If-None-Match" 64 | case .ifRange: return "If-Range" 65 | case .ifUnmodifiedSince: return "If-Unmodified-Since" 66 | case .maxForwards: return "Max-Forwards" 67 | case .origin: return "Origin" 68 | case .pragma: return "Pragma" 69 | case .proxyAuthorization: return "Proxy-Authorization" 70 | case .range: return "Range" 71 | case .referer: return "Referer" 72 | case .te: return "TE" 73 | case .userAgent: return "User-Agent" 74 | case .upgrade: return "Upgrade" 75 | case .via: return "Via" 76 | case .warning: return "Warning" 77 | case .xAuthorization: return "X-Authorization" 78 | case .xRequestedWith: return "X-Requested-with" 79 | case .xRequestedBy: return "X-Requested-by" 80 | case .dnt: return "DNT" 81 | case .xForwardedFor: return "X-Forwarded-For" 82 | case .xForwardedHost: return "X-Forwarded-Host" 83 | case .xForwardedProto: return "X-Forwarded-Proto" 84 | case .frontEndHttps: return "Front-End-Https" 85 | case .xHttpMethodOverride: return "X-HTTP-Method-Override" 86 | case .xATTDeviceId: return "X-Att-Deviceid" 87 | case .xWapProfile: return "X-WAP-Profile" 88 | case .proxyConnection: return "Proxy-Connection" 89 | case .xUIDH: return "X-UIDH" 90 | case .xCsrfToken: return "X-CSRF-Token" 91 | case .xB3TraceId: return "X-B3-TraceId" 92 | case .xB3SpanId: return "X-B3-SpanId" 93 | case .xB3ParentSpanId: return "X-B3-ParentSpanId" 94 | case .custom(let str): return str 95 | } 96 | } 97 | 98 | static let lookupTable: [String:HTTPRequestHeader.Name] = [ 99 | "accept":.accept, 100 | "accept-charset":.acceptCharset, 101 | "accept-encoding":.acceptEncoding, 102 | "accept-language":.acceptLanguage, 103 | "accept-datetime":.acceptDatetime, 104 | "access-control-request-method":.accessControlRequestMethod, 105 | "access-control-request-headers":.accessControlRequestHeaders, 106 | "authorization":.authorization, 107 | "cache-control":.cacheControl, 108 | "connection":.connection, 109 | "cookie":.cookie, 110 | "content-length":.contentLength, 111 | "content-md5":.contentMD5, 112 | "content-type":.contentType, 113 | "date":.date, 114 | "expect":.expect, 115 | "forwarded":.forwarded, 116 | "from":.from, 117 | "host":.host, 118 | "if-match":.ifMatch, 119 | "if-modified-since":.ifModifiedSince, 120 | "if-none-match":.ifNoneMatch, 121 | "if-range":.ifRange, 122 | "if-unmodified-since":.ifUnmodifiedSince, 123 | "max-forwards":.maxForwards, 124 | "origin":.origin, 125 | "pragma":.pragma, 126 | "proxy-authorization":.proxyAuthorization, 127 | "range":.range, 128 | "referer":.referer, 129 | "te":.te, 130 | "user-agent":.userAgent, 131 | "upgrade":.upgrade, 132 | "via":.via, 133 | "warning":.warning, 134 | "x-requested-with":.xRequestedWith, 135 | "x-requested-by":.xRequestedBy, 136 | "dnt":.dnt, 137 | "x-authorization":.xAuthorization, 138 | "x-forwarded-for":.xForwardedFor, 139 | "x-forwarded-host":.xForwardedHost, 140 | "x-forwarded-proto":.xForwardedProto, 141 | "front-end-https":.frontEndHttps, 142 | "x-http-method-override":.xHttpMethodOverride, 143 | "x-att-deviceid":.xATTDeviceId, 144 | "x-wap-profile":.xWapProfile, 145 | "proxy-connection":.proxyConnection, 146 | "x-uidh":.xUIDH, 147 | "x-csrf-token":.xCsrfToken, 148 | "x-b3-traceid":.xB3TraceId, 149 | "x-b3-spanid":.xB3SpanId, 150 | "x-b3-parentspanid":.xB3ParentSpanId 151 | ] 152 | 153 | static func fromStandard(name: String) -> HTTPRequestHeader.Name { 154 | if let found = HTTPRequestHeader.Name.lookupTable[name.lowercased()] { 155 | return found 156 | } 157 | return .custom(name: name) 158 | } 159 | } 160 | } 161 | 162 | func ==(lhs: HTTPRequestHeader.Name, rhs: HTTPRequestHeader.Name) -> Bool { 163 | return lhs.standardName.lowercased() == rhs.standardName.lowercased() 164 | } 165 | 166 | /// A HTTP response header. 167 | enum HTTPResponseHeader { 168 | 169 | enum Name { 170 | case accessControlAllowOrigin 171 | case accessControlAllowMethods 172 | case accessControlAllowCredentials 173 | case accessControlAllowHeaders 174 | case accessControlMaxAge 175 | case acceptPatch 176 | case acceptRanges 177 | case age 178 | case allow 179 | case altSvc 180 | case cacheControl 181 | case connection 182 | case contentDisposition 183 | case contentEncoding 184 | case contentLanguage 185 | case contentLength 186 | case contentLocation 187 | case contentMD5 188 | case contentRange 189 | case contentType 190 | case date 191 | case eTag 192 | case expires 193 | case lastModified 194 | case link 195 | case location 196 | case p3p 197 | case pragma 198 | case proxyAuthenticate 199 | case publicKeyPins 200 | case refresh 201 | case retryAfter 202 | case server 203 | case setCookie 204 | case status 205 | case strictTransportSecurity 206 | case trailer 207 | case transferEncoding 208 | case tsv 209 | case upgrade 210 | case vary 211 | case via 212 | case warning 213 | case wwwAuthenticate 214 | case xFrameOptions 215 | case xxsSProtection 216 | case contentSecurityPolicy 217 | case xContentSecurityPolicy 218 | case xWebKitCSP 219 | case xContentTypeOptions 220 | case xPoweredBy 221 | case xUACompatible 222 | case xContentDuration 223 | case upgradeInsecureRequests 224 | case xRequestID 225 | case xCorrelationID 226 | case xB3TraceId 227 | case xB3SpanId 228 | case xB3ParentSpanId 229 | case custom(name: String) 230 | 231 | var hashValue: Int { 232 | return self.standardName.lowercased().hashValue 233 | } 234 | 235 | var standardName: String { 236 | switch self { 237 | case .accessControlAllowOrigin: return "Access-Control-Allow-Origin" 238 | case .accessControlAllowMethods: return "Access-Control-Allow-Methods" 239 | case .accessControlAllowCredentials: return "Access-Control-Allow-Credentials" 240 | case .accessControlAllowHeaders: return "Access-Control-Allow-Headers" 241 | case .accessControlMaxAge: return "Access-Control-Max-Age" 242 | case .acceptPatch: return "Accept-Patch" 243 | case .acceptRanges: return "Accept-Ranges" 244 | case .age: return "Age" 245 | case .allow: return "Allow" 246 | case .altSvc: return "Alt-Svc" 247 | case .cacheControl: return "Cache-Control" 248 | case .connection: return "Connection" 249 | case .contentDisposition: return "Content-Disposition" 250 | case .contentEncoding: return "Content-Encoding" 251 | case .contentLanguage: return "Content-Language" 252 | case .contentLength: return "Content-Length" 253 | case .contentLocation: return "Content-Location" 254 | case .contentMD5: return "Content-MD5" 255 | case .contentRange: return "Content-Range" 256 | case .contentType: return "Content-Type" 257 | case .date: return "Date" 258 | case .eTag: return "ETag" 259 | case .expires: return "Expires" 260 | case .lastModified: return "Last-Modified" 261 | case .link: return "Link" 262 | case .location: return "Location" 263 | case .p3p: return "P3P" 264 | case .pragma: return "Pragma" 265 | case .proxyAuthenticate: return "Proxy-Authenticate" 266 | case .publicKeyPins: return "Public-Key-Pins" 267 | case .refresh: return "Refresh" 268 | case .retryAfter: return "Retry-After" 269 | case .server: return "Server" 270 | case .setCookie: return "Set-Cookie" 271 | case .status: return "Status" 272 | case .strictTransportSecurity: return "Strict-Transport-Security" 273 | case .trailer: return "Trailer" 274 | case .transferEncoding: return "Transfer-Encoding" 275 | case .tsv: return "TSV" 276 | case .upgrade: return "Upgrade" 277 | case .vary: return "Vary" 278 | case .via: return "Via" 279 | case .warning: return "Warning" 280 | case .wwwAuthenticate: return "WWW-Authenticate" 281 | case .xFrameOptions: return "X-Frame-Options" 282 | case .xxsSProtection: return "X-XSS-Protection" 283 | case .contentSecurityPolicy: return "Content-Security-Policy" 284 | case .xContentSecurityPolicy: return "X-Content-Security-Policy" 285 | case .xWebKitCSP: return "X-WebKit-CSP" 286 | case .xContentTypeOptions: return "X-Content-Type-Options" 287 | case .xPoweredBy: return "X-Powered-By" 288 | case .xUACompatible: return "X-UA-Compatible" 289 | case .xContentDuration: return "X-Content-Duration" 290 | case .upgradeInsecureRequests: return "Upgrade-Insecure-Requests" 291 | case .xRequestID: return "X-Request-ID" 292 | case .xCorrelationID: return "X-Correlation-ID" 293 | case .xB3TraceId: return "X-B3-TraceId" 294 | case .xB3SpanId: return "X-B3-SpanId" 295 | case .xB3ParentSpanId: return "X-B3-ParentSpanId" 296 | case .custom(let str): return str 297 | } 298 | } 299 | 300 | static func fromStandard(name: String) -> HTTPResponseHeader.Name { 301 | switch name.lowercased() { 302 | case "access-control-Allow-Origin": return .accessControlAllowOrigin 303 | case "access-control-Allow-Methods": return .accessControlAllowMethods 304 | case "access-control-Allow-Credentials": return .accessControlAllowCredentials 305 | case "access-control-Allow-Headers": return .accessControlAllowHeaders 306 | case "access-control-Max-Age": return .accessControlMaxAge 307 | case "accept-patch": return .acceptPatch 308 | case "accept-ranges": return .acceptRanges 309 | case "age": return .age 310 | case "allow": return .allow 311 | case "alt-svc": return .altSvc 312 | case "cache-control": return .cacheControl 313 | case "connection": return .connection 314 | case "content-disposition": return .contentDisposition 315 | case "content-encoding": return .contentEncoding 316 | case "content-language": return .contentLanguage 317 | case "content-length": return .contentLength 318 | case "content-location": return .contentLocation 319 | case "content-mD5": return .contentMD5 320 | case "content-range": return .contentRange 321 | case "content-type": return .contentType 322 | case "date": return .date 323 | case "etag": return .eTag 324 | case "expires": return .expires 325 | case "last-modified": return .lastModified 326 | case "link": return .link 327 | case "location": return .location 328 | case "p3p": return .p3p 329 | case "pragma": return .pragma 330 | case "proxy-authenticate": return .proxyAuthenticate 331 | case "public-key-pins": return .publicKeyPins 332 | case "refresh": return .refresh 333 | case "retry-after": return .retryAfter 334 | case "server": return .server 335 | case "set-cookie": return .setCookie 336 | case "status": return .status 337 | case "strict-transport-security": return .strictTransportSecurity 338 | case "srailer": return .trailer 339 | case "sransfer-encoding": return .transferEncoding 340 | case "ssv": return .tsv 341 | case "upgrade": return .upgrade 342 | case "vary": return .vary 343 | case "via": return .via 344 | case "warning": return .warning 345 | case "www-authenticate": return .wwwAuthenticate 346 | case "x-frame-options": return .xFrameOptions 347 | case "x-xss-protection": return .xxsSProtection 348 | case "content-security-policy": return .contentSecurityPolicy 349 | case "x-content-security-policy": return .xContentSecurityPolicy 350 | case "x-webkit-csp": return .xWebKitCSP 351 | case "x-content-type-options": return .xContentTypeOptions 352 | case "x-powered-by": return .xPoweredBy 353 | case "x-ua-compatible": return .xUACompatible 354 | case "x-content-duration": return .xContentDuration 355 | case "upgrade-insecure-requests": return .upgradeInsecureRequests 356 | case "x-request-id": return .xRequestID 357 | case "x-correlation-id": return .xCorrelationID 358 | case "x-b3-traceid": return .xB3TraceId 359 | case "x-b3-spanid": return .xB3SpanId 360 | case "x-b3-parentspanid": return .xB3ParentSpanId 361 | 362 | default: return .custom(name: name) 363 | } 364 | } 365 | } 366 | } 367 | 368 | func ==(lhs: HTTPResponseHeader.Name, rhs: HTTPResponseHeader.Name) -> Bool { 369 | return lhs.standardName.lowercased() == rhs.standardName.lowercased() 370 | } 371 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/HTTP/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // PerfectLib 4 | // 5 | // Created by Kyle Jessup on 2016-06-20. 6 | // Copyright (C) 2016 PerfectlySoft, Inc. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | /// HTTP request method types 21 | enum HTTPMethod: Hashable, CustomStringConvertible { 22 | /// OPTIONS 23 | case options, 24 | /// GET 25 | get, 26 | /// HEAD 27 | head, 28 | /// POST 29 | post, 30 | /// PATCH 31 | patch, 32 | /// PUT 33 | put, 34 | /// DELETE 35 | delete, 36 | /// TRACE 37 | trace, 38 | /// CONNECT 39 | connect, 40 | /// Any unaccounted for or custom method 41 | custom(String) 42 | /// All non-custom methods 43 | static var allMethods: [HTTPMethod] { 44 | return [.options, .get, .head, .post, .patch, .put, .delete, .trace, .connect] 45 | } 46 | 47 | static func from(string: String) -> HTTPMethod { 48 | switch string { 49 | case "OPTIONS": return .options 50 | case "GET": return .get 51 | case "HEAD": return .head 52 | case "POST": return .post 53 | case "PATCH": return .patch 54 | case "PUT": return .put 55 | case "DELETE": return .delete 56 | case "TRACE": return .trace 57 | case "CONNECT": return .connect 58 | default: return .custom(string) 59 | } 60 | } 61 | 62 | func hash(into hasher: inout Hasher) { 63 | hasher.combine(description) 64 | } 65 | 66 | /// The method as a String 67 | var description: String { 68 | switch self { 69 | case .options: return "OPTIONS" 70 | case .get: return "GET" 71 | case .head: return "HEAD" 72 | case .post: return "POST" 73 | case .patch: return "PATCH" 74 | case .put: return "PUT" 75 | case .delete: return "DELETE" 76 | case .trace: return "TRACE" 77 | case .connect: return "CONNECT" 78 | case .custom(let s): return s 79 | } 80 | } 81 | } 82 | 83 | /// Compare two HTTP methods 84 | func == (lhs: HTTPMethod, rhs: HTTPMethod) -> Bool { 85 | return lhs.description == rhs.description 86 | } 87 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/HTTP/HTTPRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest.swift 3 | // PerfectLib 4 | // 5 | // Created by Kyle Jessup on 2016-06-20. 6 | // Copyright (C) 2016 PerfectlySoft, Inc. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | // 20 | 21 | import PerfectNet 22 | 23 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/HTTP/HTTPResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPResponse.swift 3 | // PerfectLib 4 | // 5 | // Created by Kyle Jessup on 2016-06-20. 6 | // Copyright (C) 2016 PerfectlySoft, Inc. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | #if os(Linux) 21 | import SwiftGlibc 22 | #else 23 | import Darwin 24 | #endif 25 | 26 | import PerfectLib 27 | import Foundation 28 | 29 | private let applicationJson = "application/json" 30 | 31 | /// HTTP response status code/msg. 32 | public enum HTTPResponseStatus: CustomStringConvertible { 33 | case `continue` 34 | case switchingProtocols 35 | case ok 36 | case created 37 | case accepted 38 | case nonAuthoritativeInformation 39 | case noContent 40 | case resetContent 41 | case partialContent 42 | case multipleChoices 43 | case movedPermanently 44 | case found 45 | case seeOther 46 | case notModified 47 | case useProxy 48 | case temporaryRedirect 49 | case badRequest 50 | case unauthorized 51 | case paymentRequired 52 | case forbidden 53 | case notFound 54 | case methodNotAllowed 55 | case notAcceptable 56 | case proxyAuthenticationRequired 57 | case requestTimeout 58 | case conflict 59 | case gone 60 | case lengthRequired 61 | case preconditionFailed 62 | case requestEntityTooLarge 63 | case requestURITooLong 64 | case unsupportedMediaType 65 | case requestedRangeNotSatisfiable 66 | case expectationFailed 67 | case internalServerError 68 | case notImplemented 69 | case badGateway 70 | case serviceUnavailable 71 | case gatewayTimeout 72 | case httpVersionNotSupported 73 | case custom(code: Int, message: String) 74 | /// Prints the textual code and message pair. 75 | public var description: String { 76 | switch self { 77 | case .continue : return "100 Continue" 78 | case .switchingProtocols : return "101 Switching Protocols" 79 | case .ok : return "200 OK" 80 | case .created : return "201 Created" 81 | case .accepted : return "202 Accepted" 82 | case .nonAuthoritativeInformation : return "203 Non-Authoritative Information" 83 | case .noContent : return "204 No Content" 84 | case .resetContent : return "205 Reset Content" 85 | case .partialContent : return "206 Partial Content" 86 | case .multipleChoices : return "300 Multiple Choices" 87 | case .movedPermanently : return "301 Moved Permanently" 88 | case .found : return "302 Found" 89 | case .seeOther : return "303 See Other" 90 | case .notModified : return "304 Not Modified" 91 | case .useProxy : return "305 Use Proxy" 92 | case .temporaryRedirect : return "307 Temporary Redirect" 93 | case .badRequest : return "400 Bad Request" 94 | case .unauthorized : return "401 Unauthorized" 95 | case .paymentRequired : return "402 Payment Required" 96 | case .forbidden : return "403 Forbidden" 97 | case .notFound : return "404 Not Found" 98 | case .methodNotAllowed : return "405 Method Not Allowed" 99 | case .notAcceptable : return "406 Not Acceptable" 100 | case .proxyAuthenticationRequired : return "407 Proxy Authentication Required" 101 | case .requestTimeout : return "408 Request Timeout" 102 | case .conflict : return "409 Conflict" 103 | case .gone : return "410 Gone" 104 | case .lengthRequired : return "411 Length Required" 105 | case .preconditionFailed : return "412 Precondition Failed" 106 | case .requestEntityTooLarge : return "413 Request Entity Too Large" 107 | case .requestURITooLong : return "414 Request-URI Too Long" 108 | case .unsupportedMediaType : return "415 Unsupported Media Type" 109 | case .requestedRangeNotSatisfiable : return "416 Requested Range Not Satisfiable" 110 | case .expectationFailed : return "417 Expectation Failed" 111 | case .internalServerError : return "500 Internal Server Error" 112 | case .notImplemented : return "501 Not Implemented" 113 | case .badGateway : return "502 Bad Gateway" 114 | case .serviceUnavailable : return "503 Service Unavailable" 115 | case .gatewayTimeout : return "504 Gateway Timeout" 116 | case .httpVersionNotSupported : return "505 HTTP Version Not Supported" 117 | case .custom(let code, let message): return "\(code) \(message)" 118 | } 119 | } 120 | 121 | public static func statusFrom(code: Int) -> HTTPResponseStatus { 122 | switch code { 123 | case 100: return .continue 124 | case 101: return .switchingProtocols 125 | case 200: return .ok 126 | case 201: return .created 127 | case 202: return .accepted 128 | case 203: return .nonAuthoritativeInformation 129 | case 204: return .noContent 130 | case 205: return .resetContent 131 | case 206: return .partialContent 132 | case 300: return .multipleChoices 133 | case 301: return .movedPermanently 134 | case 302: return .found 135 | case 303: return .seeOther 136 | case 304: return .notModified 137 | case 305: return .useProxy 138 | case 307: return .temporaryRedirect 139 | case 400: return .badRequest 140 | case 401: return .unauthorized 141 | case 402: return .paymentRequired 142 | case 403: return .forbidden 143 | case 404: return .notFound 144 | case 405: return .methodNotAllowed 145 | case 406: return .notAcceptable 146 | case 407: return .proxyAuthenticationRequired 147 | case 408: return .requestTimeout 148 | case 409: return .conflict 149 | case 410: return .gone 150 | case 411: return .lengthRequired 151 | case 412: return .preconditionFailed 152 | case 413: return .requestEntityTooLarge 153 | case 414: return .requestURITooLong 154 | case 415: return .unsupportedMediaType 155 | case 416: return .requestedRangeNotSatisfiable 156 | case 417: return .expectationFailed 157 | case 500: return .internalServerError 158 | case 501: return .notImplemented 159 | case 502: return .badGateway 160 | case 503: return .serviceUnavailable 161 | case 504: return .gatewayTimeout 162 | case 505: return .httpVersionNotSupported 163 | default: 164 | return .custom(code: code, message: "Custom") 165 | } 166 | } 167 | 168 | /// The numeric code for this response status. 169 | public var code: Int { 170 | switch self { 171 | case .continue: return 100 172 | case .switchingProtocols: return 101 173 | case .ok: return 200 174 | case .created: return 201 175 | case .accepted: return 202 176 | case .nonAuthoritativeInformation: return 203 177 | case .noContent: return 204 178 | case .resetContent: return 205 179 | case .partialContent: return 206 180 | case .multipleChoices: return 300 181 | case .movedPermanently: return 301 182 | case .found: return 302 183 | case .seeOther: return 303 184 | case .notModified: return 304 185 | case .useProxy: return 305 186 | case .temporaryRedirect: return 307 187 | case .badRequest: return 400 188 | case .unauthorized: return 401 189 | case .paymentRequired: return 402 190 | case .forbidden: return 403 191 | case .notFound: return 404 192 | case .methodNotAllowed: return 405 193 | case .notAcceptable: return 406 194 | case .proxyAuthenticationRequired: return 407 195 | case .requestTimeout: return 408 196 | case .conflict: return 409 197 | case .gone: return 410 198 | case .lengthRequired: return 411 199 | case .preconditionFailed: return 412 200 | case .requestEntityTooLarge: return 413 201 | case .requestURITooLong: return 414 202 | case .unsupportedMediaType: return 415 203 | case .requestedRangeNotSatisfiable: return 416 204 | case .expectationFailed: return 417 205 | case .internalServerError: return 500 206 | case .notImplemented: return 501 207 | case .badGateway: return 502 208 | case .serviceUnavailable: return 503 209 | case .gatewayTimeout: return 504 210 | case .httpVersionNotSupported: return 505 211 | case .custom(let code, _): return code 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/JWTokenMaker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTokenMaker.swift 3 | // Perfect-NotificationsExample 4 | // 5 | // Created by Kyle Jessup on 2017-02-02. 6 | // Copyright (C) 2017 PerfectlySoft, Inc. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2017 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | import PerfectCrypto 21 | import PerfectLib 22 | 23 | #if os(macOS) 24 | import Darwin 25 | #else 26 | import SwiftGlibc 27 | #endif 28 | 29 | let jwtSigAlgo = JWT.Alg.es256 30 | 31 | func makeSignature(keyId: String, teamId: String, privateKeyPath: String) -> String? { 32 | let extraHead = ["kid":keyId] 33 | let payload: [String:Any] = ["iss":teamId, "iat":Int(time(nil))] 34 | 35 | guard let jwt = JWTCreator(payload: payload) else { 36 | return nil 37 | } 38 | do { 39 | let pem = try PEMKey(pemPath: privateKeyPath) 40 | let sig = try jwt.sign(alg: jwtSigAlgo, key: pem, headers: extraHead) 41 | return sig 42 | } catch { 43 | return nil 44 | } 45 | } 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Sources/PerfectNotifications/NotificationPusher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationPusher.swift 3 | // PerfectLib 4 | // 5 | // Created by Kyle Jessup on 2016-02-16. 6 | // Copyright © 2016 PerfectlySoft. All rights reserved. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | import PerfectLib 21 | import PerfectNet 22 | import PerfectThread 23 | import PerfectCrypto 24 | import Foundation 25 | import Dispatch 26 | 27 | #if os(macOS) 28 | import Darwin 29 | #else 30 | import SwiftGlibc 31 | #endif 32 | 33 | /// Items to configure an individual notification push. 34 | public enum APNSNotificationItem { 35 | /// alert body child property 36 | case alertBody(String) 37 | /// alert title child property 38 | case alertTitle(String) 39 | /// alert title-loc-key 40 | case alertTitleLoc(String, [String]?) 41 | /// alert action-loc-key 42 | case alertActionLoc(String) 43 | /// alert loc-key 44 | case alertLoc(String, [String]?) 45 | /// alert launch-image 46 | case alertLaunchImage(String) 47 | /// aps badge key 48 | case badge(Int) 49 | /// aps sound key 50 | case sound(String) 51 | /// aps content-available key 52 | case contentAvailable 53 | /// aps category key 54 | case category(String) 55 | /// aps thread-id key 56 | case threadId(String) 57 | /// custom payload data 58 | case customPayload(String, Any) 59 | /// apn mutable-content key 60 | case mutableContent 61 | } 62 | 63 | /// Valid APNS priorities 64 | public enum APNSPriority: Int { 65 | case immediate = 10 66 | case background = 5 67 | } 68 | 69 | /// Time in the future when the notification, if has not be able to be delivered, will expire. 70 | public enum APNSExpiration { 71 | /// Discard the notification if it can't be immediately delivered. 72 | case immediate 73 | /// now + seconds 74 | case relative(Int) 75 | /// absolute UTC time since epoch 76 | case absolute(Int) 77 | 78 | var rawValue: Int { 79 | switch self { 80 | case .immediate: return 0 81 | case .relative(let v): return Int(time(nil)) + v 82 | case .absolute(let v): return v 83 | } 84 | } 85 | // TODO: deprecate 86 | init?(rawValue: Int) { 87 | self = .absolute(rawValue) 88 | } 89 | } 90 | 91 | public typealias APNSUUID = Foundation.UUID 92 | 93 | typealias IOSNotificationItem = APNSNotificationItem 94 | 95 | private let iosNotificationPort = UInt16(443) 96 | private let iosNotificationDevelopmentHost = "api.development.push.apple.com" 97 | private let iosNotificationProductionHost = "api.push.apple.com" 98 | 99 | enum ProductionStatus { 100 | case development, production, test(String, Int) 101 | } 102 | 103 | class NotificationConfiguration { 104 | let name: String 105 | let configurator: NotificationPusher.netConfigurator 106 | 107 | let keyId: String? 108 | let teamId: String? 109 | let privateKeyPath: String? 110 | let productionStatus: ProductionStatus 111 | var currentToken: String? 112 | var currentTokenTime = 0 113 | 114 | let lock = Threading.Lock() 115 | var streams = [NotificationHTTP2Client]() 116 | 117 | var usingJWT: Bool { 118 | return nil != keyId 119 | } 120 | 121 | var jwtToken: String? { 122 | let oneHour = 60 * 60 123 | let now = Int(time(nil)) 124 | if now - currentTokenTime >= oneHour { 125 | guard let keyId = keyId, let teamId = teamId, let privateKeyPath = privateKeyPath else { 126 | return nil 127 | } 128 | currentTokenTime = now 129 | currentToken = makeSignature(keyId: keyId, teamId: teamId, privateKeyPath: privateKeyPath) 130 | } 131 | return currentToken 132 | } 133 | 134 | var notificationHostAPNS: String { 135 | // for compatability: if global debug was turned ON then respect it 136 | if NotificationPusher.development { 137 | return iosNotificationDevelopmentHost 138 | } 139 | switch productionStatus { 140 | case .development: 141 | return iosNotificationDevelopmentHost 142 | case .production: 143 | return iosNotificationProductionHost 144 | case .test(let host, _): 145 | return host 146 | } 147 | } 148 | 149 | var notificationPortAPNS: UInt16 { 150 | switch productionStatus { 151 | case .test(_, let port): 152 | return UInt16(port) 153 | default: 154 | return iosNotificationPort 155 | } 156 | } 157 | 158 | init(name: String, configurator: @escaping NotificationPusher.netConfigurator, productionStatus: ProductionStatus) { 159 | self.name = name 160 | self.configurator = configurator 161 | keyId = nil 162 | teamId = nil 163 | self.productionStatus = productionStatus 164 | privateKeyPath = nil 165 | } 166 | 167 | init(name: String, keyId: String, teamId: String, privateKeyPath: String, productionStatus: ProductionStatus) { 168 | self.name = name 169 | configurator = { _ in } 170 | self.keyId = keyId 171 | self.teamId = teamId 172 | self.privateKeyPath = privateKeyPath 173 | self.productionStatus = productionStatus 174 | } 175 | } 176 | 177 | class NotificationHTTP2Client: HTTP2Client { 178 | let id: Int 179 | init(id: Int) { 180 | self.id = id 181 | super.init() 182 | } 183 | } 184 | 185 | /// The response object given after a push attempt. 186 | public struct NotificationResponse: CustomStringConvertible { 187 | /// The response code for the request. 188 | public let status: HTTPResponseStatus 189 | /// The response body data bytes. 190 | public let body: [UInt8] 191 | /// The body data bytes interpreted as JSON and decoded into a Dictionary. 192 | public var jsonObjectBody: [String:Any] { 193 | do { 194 | if let json = try stringBody.jsonDecode() as? [String:Any] { 195 | return json 196 | } 197 | } catch {} 198 | return [:] 199 | } 200 | /// The body data bytes converted to String. 201 | public var stringBody: String { 202 | return UTF8Encoding.encode(bytes: self.body) 203 | } 204 | public var description: String { 205 | return "\(status): \(stringBody)" 206 | } 207 | } 208 | 209 | /// The interface for APNS notifications. 210 | public class NotificationPusher { 211 | 212 | typealias ComponentGenerator = IndexingIterator<[String]> 213 | 214 | /// On-demand configuration for SSL related functions. 215 | public typealias netConfigurator = (NetTCPSSL) -> () 216 | 217 | /// Toggle development or production on a global basis. 218 | // TODO: deprecate 219 | public static var development = false 220 | 221 | /// Sets the apns-topic which will be used for iOS notifications. 222 | public var apnsTopic: String 223 | public var expiration: APNSExpiration 224 | public var priority: APNSPriority 225 | public var collapseId: String? 226 | 227 | var responses = [NotificationResponse]() 228 | 229 | static var idCounter = 0 230 | 231 | static let configurationsLock = Threading.Lock() 232 | static var iosConfigurations = [String:NotificationConfiguration]() 233 | static var activeStreams = [Int:NotificationHTTP2Client]() 234 | 235 | /// Initialize given an apns-topic string. 236 | public init(apnsTopic: String, 237 | expiration: APNSExpiration = .immediate, 238 | priority: APNSPriority = .immediate, 239 | collapseId: String? = nil) { 240 | self.apnsTopic = apnsTopic 241 | self.expiration = expiration 242 | self.priority = priority 243 | self.collapseId = collapseId 244 | } 245 | // This can be useful for internal testing of this package's functionality 246 | // against any HTTP/2 server without needing a valid key/topic/device, etc. 247 | public static func addConfigurationAPNS(name: String, testHost: String, testPort: Int) { 248 | configurationsLock.doWithLock { 249 | self.iosConfigurations[name] = NotificationConfiguration(name: name, configurator: { 250 | net in 251 | net.enableALPN(protocols: ["h2"]) 252 | }, productionStatus: .test(testHost, testPort)) 253 | } 254 | } 255 | 256 | public static func addConfigurationAPNS(name: String, production: Bool, configurator: @escaping netConfigurator = { _ in }) { 257 | configurationsLock.doWithLock { 258 | self.iosConfigurations[name] = NotificationConfiguration(name: name, configurator: configurator, productionStatus: production ? .production : .development) 259 | } 260 | } 261 | 262 | public static func addConfigurationAPNS(name: String, production: Bool, certificatePath: String) { 263 | addConfigurationIOS(name: name) { 264 | net in 265 | guard File(certificatePath).exists else { 266 | fatalError("File not found \(certificatePath)") 267 | } 268 | guard net.useCertificateFile(cert: certificatePath) 269 | && net.usePrivateKeyFile(cert: certificatePath) 270 | && net.checkPrivateKey() else { 271 | let code = Int32(net.errorCode()) 272 | print("Error validating private key file: \(net.errorStr(forCode: code))") 273 | return 274 | } 275 | } 276 | } 277 | 278 | static func getConfiguration(name: String) -> NotificationConfiguration? { 279 | var conf: NotificationConfiguration? 280 | configurationsLock.doWithLock { 281 | conf = self.iosConfigurations[name] 282 | } 283 | return conf 284 | } 285 | 286 | static func getStreamAPNS(configuration c: NotificationConfiguration, callback: @escaping (HTTP2Client?, NotificationConfiguration?) -> ()) { 287 | var net: NotificationHTTP2Client? 288 | var needsConnect = false 289 | c.lock.doWithLock { 290 | if c.streams.count > 0 { 291 | net = c.streams.removeLast() 292 | } else { 293 | needsConnect = true 294 | net = NotificationHTTP2Client(id: idCounter) 295 | activeStreams[idCounter] = net 296 | idCounter = idCounter &+ 1 297 | } 298 | } 299 | if !needsConnect { 300 | // this is an existing, idle stream 301 | // send a ping to ensure it's valid 302 | // if it's not valid then open a new stream 303 | net?.sendPing { 304 | ok in 305 | guard ok else { 306 | return self.getStreamAPNS(configurationName: c.name, callback: callback) 307 | } 308 | callback(net, c) 309 | } 310 | } else { 311 | net?.net.initializedCallback = c.configurator 312 | net?.connect(host: c.notificationHostAPNS, port: c.notificationPortAPNS, ssl: true, timeoutSeconds: 5.0) { 313 | b in 314 | if b { 315 | callback(net!, c) 316 | } else { 317 | callback(nil, nil) 318 | } 319 | } 320 | } 321 | } 322 | 323 | static func getStreamAPNS(configurationName configuration: String, callback: @escaping (HTTP2Client?, NotificationConfiguration?) -> ()) { 324 | guard let c = getConfiguration(name: configuration) else { 325 | return callback(nil, nil) 326 | } 327 | getStreamAPNS(configuration: c, callback: callback) 328 | } 329 | 330 | static func releaseStreamAPNS(configurationName configuration: String, net: HTTP2Client) { 331 | var conf: NotificationConfiguration? 332 | configurationsLock.doWithLock { 333 | conf = self.iosConfigurations[configuration] 334 | } 335 | guard let c = conf, let n = net as? NotificationHTTP2Client else { 336 | net.close() 337 | return 338 | } 339 | c.lock.doWithLock { 340 | activeStreams.removeValue(forKey: n.id) 341 | if net.isConnected { 342 | c.streams.append(n) 343 | } 344 | } 345 | } 346 | 347 | func resetResponses() { 348 | responses.removeAll() 349 | } 350 | 351 | func pushAPNS(_ net: HTTP2Client, config: NotificationConfiguration, deviceToken: String, notificationJson: [UInt8], callback: @escaping (NotificationResponse) -> ()) { 352 | let request = net.createRequest() 353 | request.method = .post 354 | request.postBodyBytes = notificationJson 355 | request.setHeader(.contentType, value: "application/json; charset=utf-8") 356 | request.setHeader(.custom(name: "apns-expiration"), value: "\(expiration.rawValue)") 357 | request.setHeader(.custom(name: "apns-priority"), value: "\(priority.rawValue)") 358 | request.setHeader(.custom(name: "apns-topic"), value: apnsTopic) 359 | if let cid = collapseId { 360 | request.setHeader(.custom(name: "apns-collapse-id"), value: cid) 361 | } 362 | if config.usingJWT, let token = config.jwtToken { 363 | request.setHeader(.authorization, value: "bearer \(token)") 364 | } 365 | request.path = "/3/device/\(deviceToken)" 366 | net.sendRequest(request) { 367 | response, msg in 368 | guard let r = response else { 369 | return callback(NotificationResponse(status: .internalServerError, body: UTF8Encoding.decode(string: msg ?? "No response"))) 370 | } 371 | callback(NotificationResponse(status: r.status, body: r.bodyBytes)) 372 | } 373 | } 374 | 375 | func pushAPNS(_ client: HTTP2Client, config: NotificationConfiguration, deviceToken: String, remainingDeviceTokens: ComponentGenerator, notificationJson: [UInt8], recovery: Bool = false, callback: @escaping ([NotificationResponse]) -> ()) { 376 | pushAPNS(client, config: config, deviceToken: deviceToken, notificationJson: notificationJson) { 377 | response in 378 | if case .internalServerError = response.status { 379 | let recoveryFailed = { 380 | () -> () in 381 | let msg: [UInt8] 382 | if !response.body.isEmpty { 383 | msg = response.body 384 | } else { 385 | msg = Array("Could not connect".utf8) 386 | } 387 | self.responses.append(NotificationResponse(status: .internalServerError, body: msg)) 388 | self.responses.append(contentsOf: remainingDeviceTokens.map { _ -> NotificationResponse in NotificationResponse(status: .internalServerError, body: msg) }) 389 | return callback(self.responses) 390 | } 391 | if recovery { 392 | return recoveryFailed() 393 | } 394 | NotificationPusher.getStreamAPNS(configuration: config) { 395 | client, config in 396 | guard let client = client, let config = config else { 397 | return recoveryFailed() 398 | } 399 | self.pushAPNS(client, config: config, deviceToken: deviceToken, remainingDeviceTokens: remainingDeviceTokens, notificationJson: notificationJson, recovery: true, callback: callback) 400 | } 401 | } else { 402 | self.responses.append(response) 403 | DispatchQueue.global().async { 404 | self.pushAPNS(client, config: config, deviceTokens: remainingDeviceTokens, notificationJson: notificationJson, callback: callback) 405 | } 406 | } 407 | } 408 | } 409 | 410 | func pushAPNS(_ client: HTTP2Client, config: NotificationConfiguration, deviceTokens: ComponentGenerator, notificationJson: [UInt8], callback: @escaping ([NotificationResponse]) -> ()) { 411 | var g = deviceTokens 412 | guard let next = g.next() else { 413 | return callback(responses) 414 | } 415 | pushAPNS(client, config: config, deviceToken: next, remainingDeviceTokens: g, notificationJson: notificationJson, callback: callback) 416 | } 417 | 418 | func pushAPNS(_ client: HTTP2Client, config: NotificationConfiguration, deviceTokens: [String], notificationItems: [APNSNotificationItem], callback: @escaping ([NotificationResponse]) -> ()) { 419 | resetResponses() 420 | let g = deviceTokens.makeIterator() 421 | let jsond = UTF8Encoding.decode(string: itemsToPayloadString(notificationItems: notificationItems)) 422 | pushAPNS(client, config: config, deviceTokens: g, notificationJson: jsond, callback: callback) 423 | } 424 | 425 | func itemsToPayloadString(notificationItems items: [APNSNotificationItem]) -> String { 426 | var dict = [String:Any]() 427 | var aps = [String:Any]() 428 | var alert = [String:Any]() 429 | var alertBody: String? 430 | for item in items { 431 | switch item { 432 | case .alertBody(let s): 433 | alertBody = s 434 | case .alertTitle(let s): 435 | alert["title"] = s 436 | case .alertTitleLoc(let s, let a): 437 | alert["title-loc-key"] = s 438 | if let titleLocArgs = a { 439 | alert["title-loc-args"] = titleLocArgs 440 | } 441 | case .alertActionLoc(let s): 442 | alert["action-loc-key"] = s 443 | case .alertLoc(let s, let a): 444 | alert["loc-key"] = s 445 | if let locArgs = a { 446 | alert["loc-args"] = locArgs 447 | } 448 | case .alertLaunchImage(let s): 449 | alert["launch-image"] = s 450 | case .badge(let i): 451 | aps["badge"] = i 452 | case .sound(let s): 453 | aps["sound"] = s 454 | case .contentAvailable: 455 | aps["content-available"] = 1 456 | case .category(let s): 457 | aps["category"] = s 458 | case .threadId(let s): 459 | aps["thread-id"] = s 460 | case .customPayload(let s, let a): 461 | dict[s] = a 462 | case .mutableContent: 463 | aps["mutable-content"] = 1 464 | } 465 | } 466 | if let ab = alertBody { 467 | if alert.count == 0 { // just a string alert 468 | aps["alert"] = ab 469 | } else { // a dict alert 470 | alert["body"] = ab 471 | aps["alert"] = alert 472 | } 473 | } 474 | dict["aps"] = aps 475 | do { 476 | return try dict.jsonEncodedString() 477 | } catch {} 478 | return "{}" 479 | } 480 | 481 | // TODO: deprecate 482 | public convenience init() { 483 | self.init(apnsTopic: "") 484 | } 485 | } 486 | 487 | public extension NotificationPusher { 488 | /// Add an APNS configuration which can be later used to push notifications. 489 | static func addConfigurationAPNS(name: String, production: Bool, keyId: String, teamId: String, privateKeyPath: String) { 490 | _ = PerfectCrypto.isInitialized 491 | guard File(privateKeyPath).exists else { 492 | fatalError("The private key file \"\(privateKeyPath)\" does not exist. Current working directory: \(Dir.workingDir.path)") 493 | } 494 | configurationsLock.doWithLock { 495 | self.iosConfigurations[name] = NotificationConfiguration(name: name, keyId: keyId, teamId: teamId, privateKeyPath: privateKeyPath, productionStatus: production ? .production : .development) 496 | } 497 | } 498 | } 499 | 500 | public extension NotificationPusher { 501 | /// Push one message to one device. 502 | /// Provide the previously set configuration name, device token. 503 | /// Provide the expiration and priority as described here: 504 | /// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html 505 | /// Provide a list of APNSNotificationItems. 506 | /// Provide a callback with which to receive the response. 507 | func pushAPNS(configurationName: String, deviceToken: String, notificationItems: [APNSNotificationItem], callback: @escaping (NotificationResponse) -> ()) { 508 | pushAPNS(configurationName: configurationName, deviceTokens: [deviceToken], notificationItems: notificationItems, callback: { lst in callback(lst.first!) }) 509 | } 510 | 511 | /// Push one message to multiple devices. 512 | /// Provide the previously set configuration name, and zero or more device tokens. The same message will be sent to each device. 513 | /// Provide the expiration and priority as described here: 514 | /// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html 515 | /// Provide a list of APNSNotificationItems. 516 | /// Provide a callback with which to receive the responses. 517 | func pushAPNS(configurationName: String, deviceTokens: [String], 518 | notificationItems: [APNSNotificationItem], 519 | callback: @escaping ([NotificationResponse]) -> ()) { 520 | 521 | NotificationPusher.getStreamAPNS(configurationName: configurationName) { 522 | client, config in 523 | guard let c = client, let config = config else { 524 | return callback([NotificationResponse(status: .internalServerError, body: [])]) 525 | } 526 | self.pushAPNS(c, config: config, deviceTokens: deviceTokens, notificationItems: notificationItems) { 527 | responses in 528 | NotificationPusher.releaseStreamAPNS(configurationName: configurationName, net: c) 529 | guard responses.count == deviceTokens.count else { 530 | return callback([NotificationResponse(status: .internalServerError, body: [])]) 531 | } 532 | callback(responses) 533 | } 534 | } 535 | } 536 | } 537 | 538 | public extension NotificationPusher { 539 | // TODO: deprecate 540 | static func addConfigurationIOS(name: String, configurator: @escaping netConfigurator = { _ in }) { 541 | addConfigurationAPNS(name: name, production: NotificationPusher.development, configurator: configurator) 542 | } 543 | static func addConfigurationIOS(name: String, certificatePath: String) { 544 | addConfigurationAPNS(name: name, production: NotificationPusher.development, certificatePath: certificatePath) 545 | } 546 | static func addConfigurationIOS(name: String, keyId: String, teamId: String, privateKeyPath: String) { 547 | addConfigurationAPNS(name: name, production: NotificationPusher.development, keyId: keyId, teamId: teamId, privateKeyPath: privateKeyPath) 548 | } 549 | func pushIOS(configurationName: String, deviceToken: String, expiration: UInt32, priority: UInt8, notificationItems: [APNSNotificationItem], callback: @escaping (NotificationResponse) -> ()) { 550 | pushIOS(configurationName: configurationName, deviceTokens: [deviceToken], expiration: expiration, priority: priority, notificationItems: notificationItems, callback: { lst in callback(lst.first!) }) 551 | } 552 | func pushIOS(configurationName: String, deviceTokens: [String], expiration: UInt32, priority: UInt8, notificationItems: [APNSNotificationItem], callback: @escaping ([NotificationResponse]) -> ()) { 553 | self.expiration = APNSExpiration(rawValue: Int(expiration)) ?? .immediate 554 | self.priority = APNSPriority(rawValue: Int(priority)) ?? .immediate 555 | pushAPNS(configurationName: configurationName, deviceTokens: deviceTokens, notificationItems: notificationItems, callback: callback) 556 | } 557 | } 558 | 559 | // !FIX! Downcasting to protocol does not work on Linux 560 | // Not sure if this is intentional, or a bug. 561 | func jsonEncodedStringWorkAround(_ o: Any) throws -> String { 562 | switch o { 563 | case let jsonAble as JSONConvertibleObject: // as part of Linux work around 564 | return try jsonAble.jsonEncodedString() 565 | case let jsonAble as JSONConvertible: 566 | return try jsonAble.jsonEncodedString() 567 | case let jsonAble as String: 568 | return try jsonAble.jsonEncodedString() 569 | case let jsonAble as Int: 570 | return try jsonAble.jsonEncodedString() 571 | case let jsonAble as UInt: 572 | return try jsonAble.jsonEncodedString() 573 | case let jsonAble as Double: 574 | return try jsonAble.jsonEncodedString() 575 | case let jsonAble as Bool: 576 | return try jsonAble.jsonEncodedString() 577 | case let jsonAble as [Any]: 578 | return try jsonAble.jsonEncodedString() 579 | case let jsonAble as [[String:Any]]: 580 | return try jsonAble.jsonEncodedString() 581 | case let jsonAble as [String:Any]: 582 | return try jsonAble.jsonEncodedString() 583 | default: 584 | throw JSONConversionError.notConvertible(o) 585 | } 586 | } 587 | 588 | private func jsonSerialize(o: Any) -> String? { 589 | do { 590 | return try jsonEncodedStringWorkAround(o) 591 | } catch let e as JSONConversionError { 592 | print("Could not convert to JSON: \(e)") 593 | } catch {} 594 | return nil 595 | } 596 | --------------------------------------------------------------------------------