├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
--------------------------------------------------------------------------------