├── .gitignore
├── README.md
├── README_zh.md
├── docs
├── QuickStart.md
└── QuickStart.zh-CN.md
├── dub.sdl
├── examples
├── httpserver
│ ├── attachments
│ │ ├── abc.txt
│ │ └── avatar.jpg
│ ├── dub.sdl
│ └── source
│ │ └── main.d
└── parser
│ ├── dub.sdl
│ ├── reqeustdata
│ └── source
│ └── main.d
├── source
└── archttp
│ ├── Archttp.d
│ ├── Cookie.d
│ ├── HttpContext.d
│ ├── HttpHeader.d
│ ├── HttpMessageParser.d
│ ├── HttpMessageParserTest.d
│ ├── HttpMethod.d
│ ├── HttpRequest.d
│ ├── HttpRequestHandler.d
│ ├── HttpRequestParser.d
│ ├── HttpRequestParserHandler.d
│ ├── HttpResponse.d
│ ├── HttpStatusCode.d
│ ├── MiddlewareExecutor.d
│ ├── MultiPart.d
│ ├── Route.d
│ ├── Router.d
│ ├── Url.d
│ ├── codec
│ ├── HttpCodec.d
│ ├── HttpDecoder.d
│ └── HttpEncoder.d
│ └── package.d
└── workspace.code-workspace
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | libarchttp.a
4 | .dub
5 | dub.selections.json
6 | /examples/parser/parser
7 | /examples/parser/parser.exe
8 | /examples/httpserver/httpserver
9 | /examples/httpserver/httpserver.exe
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Archttp
2 | A highly performant web framework written in D.
3 |
4 | ## Documents
5 | 1. [Quick Start](docs/QuickStart.md)
6 | 2. [快速开始](docs/QuickStart.zh-CN.md)
7 |
8 | ## Example for web server
9 | ```D
10 |
11 | import archttp;
12 |
13 | void main()
14 | {
15 | auto app = new Archttp;
16 |
17 | app.get("/", (request, response) {
18 | response.send("Hello, World!");
19 | });
20 |
21 | app.get("/user/{id:\\d+}", (request, response) {
22 | response.send("User id: " ~ request.params["id"]);
23 | });
24 |
25 | app.get("/blog/{name}", (request, response) {
26 | response.send("Username: " ~ request.params["name"]);
27 | });
28 |
29 | app.post("/upload", (request, response) {
30 | response.send("Using post method!");
31 | });
32 |
33 | app.get("/download", (request, response) {
34 | response.sendFile("./attachments/avatar.jpg");
35 | });
36 |
37 | app.get("/json", (request, response) {
38 | import std.json;
39 | response.send( JSONValue( ["message" : "Hello, World!"] ) );
40 | });
41 |
42 | app.get("/cookie", (request, response) {
43 |
44 | import std.stdio : writeln;
45 |
46 | writeln(request.cookie("token"));
47 | writeln(request.cookies());
48 |
49 | response.cookie("username", "myuser");
50 | response.cookie(new Cookie("token", "0123456789"));
51 | response.send("Set cookies ..");
52 | });
53 |
54 | app.listen(8080);
55 | }
56 | ```
57 |
58 | ## Dependencies
59 | * [Geario](https://github.com/kerisy/geario)
60 | * [Nbuff](https://github.com/ikod/nbuff)
61 | * [httparsed](https://github.com/tchaloupka/httparsed)
62 | * [urld](https://github.com/dhasenan/urld)
63 |
64 | ## Thanks contributors
65 | * zoujiaqing
66 | * Heromyth
67 | * ikod
68 | * tchaloupka
69 | * dhasenan
70 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | # Archttp
2 | Archttp 是一个简单好用,拥有真正高性能的 Web 框架。
3 |
4 | ## 文档
5 | 1. [快速开始](docs/QuickStart.zh-CN.md)
6 | 2. [Quick Start](docs/QuickStart.md)
7 |
8 | ## 漂亮的示例代码
9 | ```D
10 |
11 | import archttp;
12 |
13 | void main()
14 | {
15 | auto app = new Archttp;
16 |
17 | app.get("/", (request, response) {
18 | response.send("Hello, World!");
19 | });
20 |
21 | app.get("/user/{id:\\d+}", (request, response) {
22 | response.send("User id: " ~ request.params["id"]);
23 | });
24 |
25 | app.get("/blog/{name}", (request, response) {
26 | response.send("Username: " ~ request.params["name"]);
27 | });
28 |
29 | app.post("/upload", (request, response) {
30 | response.send("Using post method!");
31 | });
32 |
33 | app.get("/download", (request, response) {
34 | response.sendFile("./attachments/avatar.jpg");
35 | });
36 |
37 | app.get("/json", (request, response) {
38 | import std.json;
39 | response.send( JSONValue( ["message" : "Hello, World!"] ) );
40 | });
41 |
42 | app.get("/cookie", (request, response) {
43 |
44 | import std.stdio : writeln;
45 |
46 | writeln(request.cookie("token"));
47 | writeln(request.cookies());
48 |
49 | response.cookie("username", "myuser");
50 | response.cookie(new Cookie("token", "0123456789"));
51 | response.send("Set cookies ..");
52 | });
53 |
54 | app.listen(8080);
55 | }
56 | ```
57 |
58 | ## 项目依赖
59 | * [Geario](https://github.com/kerisy/geario)
60 | * [Nbuff](https://github.com/ikod/nbuff)
61 | * [httparsed](https://github.com/tchaloupka/httparsed)
62 | * [urld](https://github.com/dhasenan/urld)
63 |
64 | ## 感谢贡献者
65 | * zoujiaqing
66 | * Heromyth
67 | * ikod
68 | * tchaloupka
69 | * dhasenan
70 |
--------------------------------------------------------------------------------
/docs/QuickStart.md:
--------------------------------------------------------------------------------
1 | ## Archttp
2 | Archttp is a Web server framework written in D language with Golang concurrency capability. Archttp has an ExpressJS-like API design, which makes it extremely easy to use.
3 |
4 | ### Archttp's core focus is on three metrics:
5 | 1. Simple
6 | 2. Flexible
7 | 3. Performance
8 |
9 | ## Document directory
10 | - [Quick Start](#QUICK_START)
11 | - [Routing](#ROUTING)
12 | - [Routing group](#ROUTING_MOUNT)
13 | - [Middleware](#MIDDLEWARE)
14 | - [Cookie](#COOKIE)
15 | - [Send files](#SEND_FILES)
16 | - [Upload files](#UPLOAD_FILES)
17 |
18 |
19 | ### Archttp base sample compiled and run
20 | First, use the dub command to create the example code. On the command line, type 'dub init example' and select 'sdl'. Next, press Enter and type 'archttp' under Adding Dependency. The duB package manager will add a version dependency for 'archttp' as follows:
21 |
22 | ```bash
23 | % dub init example
24 | Package recipe format (sdl/json) [json]: sdl
25 | Name [example]:
26 | Description [A minimal D application.]:
27 | Author name [zoujiaqing]:
28 | License [proprietary]:
29 | Copyright string [Copyright © 2022, zoujiaqing]:
30 | Add dependency (leave empty to skip) []: archttp
31 | Adding dependency archttp ~>1.0.0
32 | Add dependency (leave empty to skip) []:
33 | Successfully created an empty project in '/Users/zoujiaqing/projects/example'.
34 | Package successfully created in example
35 | ```
36 |
37 | Add this sample code to `example/source/app.d`
38 |
39 | ```D
40 | import archttp;
41 |
42 | void main()
43 | {
44 | auto app = new Archttp;
45 |
46 | app.get("/", (req, res) {
47 | res.send("Hello, World!");
48 | });
49 |
50 | app.listen(8080);
51 | }
52 | ```
53 |
54 | After saving the code, run the dub build command in the 'example/' directory and wait for the result:
55 |
56 | ```bash
57 | dub build
58 | Fetching archttp 1.0.0 (getting selected version)...
59 | ...
60 | Linking...
61 | To force a rebuild of up-to-date targets, run again with --force.
62 | ```
63 |
64 | 'dub' downloaded 'archttp 1.0.0' and compiled the project. 'No error, we can run the project.
65 | ```text
66 |
67 | # Archttp service has been started!
68 | - IO threads: 8
69 | - Listening: 0.0.0.0:8080
70 |
71 | ```
72 |
73 | The project is up and running, with eight IO threads enabled, listening on port 8080 in the code, and the browser accessing 'http://localhost:8080/' outputs' Hello, World! 'string.
74 |
75 |
76 | ### Routing example code
77 | ```D
78 | import archttp;
79 |
80 | void main()
81 | {
82 | auto app = new Archttp;
83 |
84 | app.get("/", (req, res) {
85 | res.send("Hello, World!");
86 | });
87 |
88 | app.get("/user/{id:\\d+}", (req, res) {
89 | res.send("User id: " ~ req.params["id"]);
90 | });
91 |
92 | app.get("/blog/{name}", (req, res) {
93 | res.send("Username: " ~ req.params["name"]);
94 | });
95 |
96 | app.listen(8080);
97 | }
98 | ```
99 |
100 | It can be seen that the routing function of Archttp is very simple and clear. It also supports regular matching and value selection.
101 |
102 |
103 | ### Route bind group mount
104 | ```D
105 | import archttp;
106 |
107 | void main()
108 | {
109 | auto app = new Archttp;
110 |
111 | app.get("/", (req, res) {
112 | res.send("Front page!");
113 | });
114 |
115 | auto adminRouter = Archttp.newRouter();
116 |
117 | adminRouter.get("/", (req, res) {
118 | res.send("Hello, Admin!");
119 | });
120 |
121 | adminRouter.get("/login", (req, res) {
122 | res.send("Login page!");
123 | });
124 |
125 | app.use("/admin", adminRouter);
126 |
127 | app.listen(8080);
128 | }
129 | ```
130 |
131 | AdminRouter acts as a routing group (a concept derived from the Hunt Framework), which can use its own middleware rules, i.e. it acts as a separate subapplication with independent control over permissions, etc.
132 |
133 |
134 |
135 | ### Middleware uses sample code
136 | ```D
137 | import archttp;
138 |
139 | import std.stdio : writeln;
140 |
141 | void main()
142 | {
143 | auto app = new Archttp;
144 |
145 | app.use((req, res, next) {
146 | writeln("middleware 1 ..");
147 | next();
148 | });
149 |
150 | app.use((req, res, next) {
151 | writeln("middleware 2 ..");
152 | next();
153 | });
154 |
155 | app.use((req, res, next) {
156 | writeln("middleware 3 ..");
157 | next();
158 | });
159 |
160 | app.use((req, es, next) {
161 | writeln("middleware 4 ..");
162 | });
163 |
164 | app.use((req, res, next) {
165 | writeln("middleware 5 ..");
166 | });
167 |
168 | app.get("/", (req, res) {
169 | res.send("Hello, World!");
170 | });
171 |
172 | app.listen(8080);
173 | }
174 | ```
175 |
176 | After running this code, you can see that middleware 5 is not executed, and Archttp is now executed according to onion rules.
177 |
178 |
179 | ### Cookie use sample code
180 | ```D
181 | import archttp;
182 |
183 | import std.stdio : writeln;
184 |
185 | void main()
186 | {
187 | auto app = new Archttp;
188 |
189 | app.get("/", (request, response) {
190 |
191 | writeln(request.cookie("token"));
192 | writeln(request.cookies());
193 |
194 | response.cookie("username", "myuser");
195 | response.cookie("token", "0123456789");
196 |
197 | response.send("Set cookies ..");
198 | });
199 |
200 | app.listen(8080);
201 | }
202 | ```
203 |
204 |
205 | Send sample file code to the client
206 |
207 | ```D
208 | import archttp;
209 |
210 | void main()
211 | {
212 | auto app = new Archttp;
213 |
214 | app.get("/download", (req, res) {
215 | res.sendFile("./attachments/avatar.jpg");
216 | });
217 |
218 | app.listen(8080);
219 | }
220 | ```
221 |
222 |
223 | ## Community and Communication groups
224 | Github discussion: https://github.com/kerisy/archttp
225 | D Language Chinese community: https://dlangchina.com
226 | QQ group: 184183224
227 |
--------------------------------------------------------------------------------
/docs/QuickStart.zh-CN.md:
--------------------------------------------------------------------------------
1 | ## Archttp 简介
2 | Archttp 是使用 D语言编写的 web 服务端框架,拥有 Golang 的并发能力。Archttp 拥有类似 ExpressJS 的 API 设计,所以易用性极强。
3 |
4 | ### Archttp 核心关注的指标有三个:
5 | 1. 简单
6 | 2. 灵活
7 | 3. 性能
8 |
9 | ## 文档目录
10 | - [快速入门](#QUICK_START)
11 | - [路由](#ROUTING)
12 | - [路由组](#ROUTER_GROUP)
13 | - [中间件](#MIDDLEWARE)
14 | - [Cookie](#COOKIE)
15 | - [向客户端发送文件](#SEND_FILES)
16 | - [上传文件](#UPLOAD_FILES)
17 |
18 |
19 | ### Archttp 快速开始示例
20 | 首先我们使用 dub 命令创建 example 示例代码,在命令行输入 `dub init example` 然后选择 `sdl` 格式配置,接下来一路回车,在 Adding dependency 的时候输入 `archttp`,这时候 dub 包管理器会为你添加好 `archttp` 的版本依赖,具体如下:
21 | ```bash
22 | % dub init example
23 | Package recipe format (sdl/json) [json]: sdl
24 | Name [example]:
25 | Description [A minimal D application.]:
26 | Author name [zoujiaqing]:
27 | License [proprietary]:
28 | Copyright string [Copyright © 2022, zoujiaqing]:
29 | Add dependency (leave empty to skip) []: archttp
30 | Adding dependency archttp ~>1.0.0
31 | Add dependency (leave empty to skip) []:
32 | Successfully created an empty project in '/Users/zoujiaqing/projects/example'.
33 | Package successfully created in example
34 | ```
35 | 这时候我们可以看到生成了一个目录为 `example` 的 D语言项目,编辑项目目录打开 `source/app.d` 编辑代码为下面的实例代码:
36 | ```D
37 | import archttp;
38 |
39 | void main()
40 | {
41 | auto app = new Archttp;
42 |
43 | app.get("/", (req, res) {
44 | res.send("Hello, World!");
45 | });
46 |
47 | app.listen(8080);
48 | }
49 | ```
50 | 保存好代码后我们进入 `example/` 目录下执行编译命令 `dub build` 等待编译结果:
51 | ```bash
52 | dub build
53 | Fetching archttp 1.0.0 (getting selected version)...
54 | ...
55 | Linking...
56 | To force a rebuild of up-to-date targets, run again with --force.
57 | ```
58 | 从上面的命令输出可以看出来 `dub` 帮我们下载了 `archttp 1.0.0` 并且进行了项目编译,最后的 `Linking...` 也没有出错,我们可以运行项目了。
59 | ```text
60 |
61 | # Archttp service has been started!
62 | - IO threads: 8
63 | - Listening: 0.0.0.0:8080
64 |
65 | ```
66 | 项目已经跑起来了,启用了 8 个 io 线程,代码中监听了 8080 端口,浏览器访问 `http://localhost:8080/` 会输出 `Hello, World!` 字符串。
67 |
68 |
69 | ### 路由功能示例代码
70 | ```D
71 | import archttp;
72 |
73 | void main()
74 | {
75 | auto app = new Archttp;
76 |
77 | app.get("/", (req, res) {
78 | res.send("Hello, World!");
79 | });
80 |
81 | app.get("/user/{id:\\d+}", (req, res) {
82 | res.send("User id: " ~ req.params["id"]);
83 | });
84 |
85 | app.get("/blog/{name}", (req, res) {
86 | res.send("Username: " ~ req.params["name"]);
87 | });
88 |
89 | app.listen(8080);
90 | }
91 | ```
92 |
93 | 可以看出 Archttp 的路由功能非常简单清晰,也支持正则匹配和取值。
94 |
95 |
96 | ### 路由组挂载绑定
97 | ```D
98 | import archttp;
99 |
100 | void main()
101 | {
102 | auto app = new Archttp;
103 |
104 | app.get("/", (req, res) {
105 | res.send("Front page!");
106 | });
107 |
108 | auto adminRouter = Archttp.newRouter();
109 |
110 | adminRouter.get("/", (req, res) {
111 | res.send("Hello, Admin!");
112 | });
113 |
114 | adminRouter.get("/login", (req, res) {
115 | res.send("Login page!");
116 | });
117 |
118 | app.use("/admin", adminRouter);
119 |
120 | app.listen(8080);
121 | }
122 | ```
123 |
124 | 可以看出 adminRouter 相当于一个路由组(路由组的概念来自于 Hunt Framework),路由组可以使用自己的中间件规则,也就是他相当于一个独立的子应用,可以独立控制权限等。
125 |
126 |
127 | ### 中间件使用示例代码
128 | ```java
129 | import archttp;
130 |
131 | import std.stdio : writeln;
132 |
133 | void main()
134 | {
135 | auto app = new Archttp;
136 |
137 | app.use((req, res, next) {
138 | writeln("middleware 1 ..");
139 | next();
140 | });
141 |
142 | app.use((req, res, next) {
143 | writeln("middleware 2 ..");
144 | next();
145 | });
146 |
147 | app.use((req, res, next) {
148 | writeln("middleware 3 ..");
149 | next();
150 | });
151 |
152 | app.use((req, es, next) {
153 | writeln("middleware 4 ..");
154 | });
155 |
156 | app.use((req, res, next) {
157 | writeln("middleware 5 ..");
158 | });
159 |
160 | app.get("/", (req, res) {
161 | res.send("Hello, World!");
162 | });
163 |
164 | app.listen(8080);
165 | }
166 | ```
167 |
168 | 这段代码运行之后可以发现没有执行到 middleware 5,现在 Archttp 的执行遵循洋葱规则。
169 |
170 |
171 | ### Cookie 使用示例代码
172 | ```java
173 | import archttp;
174 |
175 | import std.stdio : writeln;
176 |
177 | void main()
178 | {
179 | auto app = new Archttp;
180 |
181 | app.get("/", (request, response) {
182 |
183 | writeln(request.cookie("token"));
184 | writeln(request.cookies());
185 |
186 | response.cookie("username", "myuser");
187 | response.cookie("token", "0123456789");
188 |
189 | response.send("Set cookies ..");
190 | });
191 |
192 | app.listen(8080);
193 | }
194 | ```
195 |
196 |
197 | ### 向客户端发送文件示例代码
198 |
199 | ```D
200 | import archttp;
201 |
202 | void main()
203 | {
204 | auto app = new Archttp;
205 |
206 | app.get("/download", (req, res) {
207 | res.sendFile("./attachments/avatar.jpg");
208 | });
209 |
210 | app.listen(8080);
211 | }
212 | ```
213 |
214 |
215 | ## 社区与交流群
216 | Github讨论区:https://github.com/kerisy/archttp
217 | D语言中文社区:https://dlangchina.com
218 | QQ群:184183224
219 |
--------------------------------------------------------------------------------
/dub.sdl:
--------------------------------------------------------------------------------
1 | name "archttp"
2 | description "A highly performant web framework written in D."
3 | homepage "https://www.kerisy.com"
4 | targetType "library"
5 | license "Apache-2.0"
6 | dflags "-mcpu=native" platform="ldc"
7 | dependency "geario" version="~>0.1.0"
8 |
--------------------------------------------------------------------------------
/examples/httpserver/attachments/abc.txt:
--------------------------------------------------------------------------------
1 | abc123
--------------------------------------------------------------------------------
/examples/httpserver/attachments/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kerisy/archttp/b0557292a70aa51c2efea1868d022c58c853d3e9/examples/httpserver/attachments/avatar.jpg
--------------------------------------------------------------------------------
/examples/httpserver/dub.sdl:
--------------------------------------------------------------------------------
1 | name "httpserver"
2 | description "A minimal D application."
3 | authors "zoujiaqing"
4 | copyright "Copyright © 2022, zoujiaqing"
5 | dependency "archttp" path="../../"
6 |
--------------------------------------------------------------------------------
/examples/httpserver/source/main.d:
--------------------------------------------------------------------------------
1 |
2 | import archttp;
3 | import std.stdio;
4 |
5 | void main()
6 | {
7 | auto app = new Archttp;
8 |
9 | app.use((request, response, next) {
10 | writeln("middleware 1 ..");
11 | next();
12 | });
13 |
14 | app.use((request, response, next) {
15 | writeln("middleware 2 ..");
16 | next();
17 | });
18 |
19 | app.use((request, response, next) {
20 | writeln("middleware 3 ..");
21 | next();
22 | });
23 |
24 | app.use((request, response, next) {
25 | writeln("middleware 4 ..");
26 | });
27 |
28 | app.use((request, response, next) {
29 | writeln("middleware 5 ..");
30 | });
31 |
32 | app.get("/", (request, response) {
33 | response.send("Hello, World!");
34 | });
35 |
36 | app.post("/", (request, response) {
37 | import std.json;
38 | response.send( JSONValue( ["message" : "Hello, World!"] ) );
39 | });
40 |
41 | auto adminRouter = Archttp.newRouter();
42 |
43 | adminRouter.add("/", HttpMethod.GET, (request, response) {
44 | response.send("Hello, Admin!");
45 | });
46 |
47 | adminRouter.add("/login", HttpMethod.GET, (request, response) {
48 | response.send("Login page!");
49 | });
50 |
51 | app.use("/admin", adminRouter);
52 |
53 | app.get("/json", (request, response) {
54 | import std.json;
55 | response.send( JSONValue( ["message" : "Hello, World!"] ) );
56 | });
57 |
58 | app.get("/download", (request, response) {
59 | response.sendFile("./attachments/avatar.jpg");
60 | });
61 |
62 | app.get("/cookie", (request, response) {
63 |
64 | writeln(request.cookie("token"));
65 | writeln(request.cookies());
66 |
67 | response.cookie("username", "myuser");
68 | response.cookie(new Cookie("token", "0123456789"));
69 | response.cookie(new Cookie("userid", "123"));
70 | response.send("Set cookies ..");
71 | });
72 |
73 | app.get("/user/{id:\\d+}", (request, response) {
74 | response.send("User id: " ~ request.params["id"]);
75 | });
76 |
77 | app.get("/blog/{name}", (request, response) {
78 | response.send("Username: " ~ request.params["name"]);
79 | });
80 |
81 | app.post("/upload", (request, response) {
82 | response.send("Using post method!");
83 | });
84 |
85 | app.listen(8080);
86 | }
87 |
--------------------------------------------------------------------------------
/examples/parser/dub.sdl:
--------------------------------------------------------------------------------
1 | name "parser"
2 | description "A minimal D application."
3 | authors "zoujiaqing"
4 | copyright "Copyright © 2022, zoujiaqing"
5 | license "proprietary"
6 | dependency "archttp" path="../../"
7 |
--------------------------------------------------------------------------------
/examples/parser/reqeustdata:
--------------------------------------------------------------------------------
1 | POST /upload HTTP/1.1
2 | Host: localhost:8080
3 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0
4 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
5 | Accept-Language: zh-CN,en-US;q=0.7,en;q=0.3
6 | Accept-Encoding: gzip, deflate, br
7 | Content-Type: multipart/form-data; boundary=---------------------------268222700142472544581575924676
8 | Content-Length: 682
9 | Connection: keep-alive
10 | Upgrade-Insecure-Requests: 1
11 | Sec-Fetch-Dest: document
12 | Sec-Fetch-Mode: navigate
13 | Sec-Fetch-Site: cross-site
14 | Sec-Fetch-User: ?1
15 |
16 | -----------------------------268222700142472544581575924676
17 | Content-Disposition: form-data; name="username"
18 |
19 | zoujiaqing
20 | -----------------------------268222700142472544581575924676
21 | Content-Disposition: form-data; name="avatar"; filename=""
22 | Content-Type: application/octet-stream
23 |
24 |
25 | -----------------------------268222700142472544581575924676
26 | Content-Disposition: form-data; name="file[]"; filename=""
27 | Content-Type: application/octet-stream
28 |
29 |
30 | -----------------------------268222700142472544581575924676
31 | Content-Disposition: form-data; name="file[]"; filename=""
32 | Content-Type: application/octet-stream
33 |
34 |
35 | -----------------------------268222700142472544581575924676--
36 |
--------------------------------------------------------------------------------
/examples/parser/source/main.d:
--------------------------------------------------------------------------------
1 | module main;
2 |
3 | import archttp.HttpRequestParser;
4 | import archttp.Router;
5 | import archttp.HttpRequest;
6 |
7 | import std.stdio;
8 | import std.conv : to;
9 | import std.file : readText;
10 |
11 | void parseTest0()
12 | {
13 | string data = `POST /login?action=check HTTP/1.1
14 | Host: localhost:8080
15 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0
16 | Accept: */*
17 | Accept-Language: zh-CN,en-US;q=0.7,en;q=0.3
18 | Accept-Encoding: gzip, deflate, br
19 | Content-Type: multipart/form-data; boundary=---------------------------332604924416206460511787781889
20 | Content-Length: 697
21 | Connection: keep-alive
22 | Sec-Fetch-Dest: empty
23 | Sec-Fetch-Mode: cors
24 | Sec-Fetch-Site: same-origin
25 |
26 | `;
27 | data ~= "-----------------------------332604924416206460511787781889\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\nzoujiaqing\r\n-----------------------------332604924416206460511787781889\r\nContent-Disposition: form-data; name=\"avatar\"; filename=\"a.jpg\"\r\nContent-Type: image/jpeg\r\n\r\nthis is a avatar.\r\n-----------------------------332604924416206460511787781889\r\nContent-Disposition: form-data; name=\"file[]\"; filename=\"url.d\"\r\nContent-Type: application/octet-stream\r\n\r\n/* hello world */\r\n-----------------------------332604924416206460511787781889\r\nContent-Disposition: form-data; name=\"file[]\"; filename=\"a.jpg\"\r\nContent-Type: image/jpeg\r\n\r\nthis is a pic.\r\n-----------------------------332604924416206460511787781889--\r\n";
28 |
29 | // data = readText("./reqeustdata");
30 |
31 | auto parser = new HttpRequestParser;
32 |
33 | long result = parser.parse(data);
34 |
35 | writeln("data length: ", data.length);
36 | writeln("parsed data: ", result);
37 |
38 | auto request = parser.request();
39 |
40 | parser.reset();
41 |
42 | OnRequest(request);
43 | }
44 |
45 | void parseTest1()
46 | {
47 |
48 | string[] buf;
49 | buf ~= `P`;
50 | buf ~= `OST /login?action=check HTTP/1.1
51 | Host: localhost:8080`;
52 | buf ~= `
53 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0
54 | Accept: */*
55 | Accept-Language: zh-CN,en-US;q=0.7,en;q=0.3
56 | Accept-Encoding: gzip, deflate, br
57 | Content-Type: multipart/form-data; boundary=---------------------------332604924416206460511787781889
58 | Content-Length: 697
59 | Connection: keep-alive
60 | Sec-Fetch-Dest: empty`;
61 | buf ~= `
62 | Sec-Fetch-Mode: cors
63 | Sec-Fetch-Site: same-origin
64 |
65 | `;
66 | buf ~= "-----------------------------3326049244162064";
67 | buf ~= "60511787781889\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\nzouji";
68 | buf ~= "aqing\r\n-----------------------------332604924416206460511787781889\r\nContent-Disposi";
69 | buf ~= "tion: form-data; name=\"avatar\"; filename=\"a.jpg\"\r\nContent-Type: image/jpeg\r\n\r\nth";
70 | buf ~= "is is a avatar.\r\n-----------------------------332604924416206460511787781889\r\nContent-Disposit";
71 | buf ~= "ion: form-data; name=\"file[]\"; filename=\"url.d\"\r\nContent-Type: application/octet-stre";
72 | buf ~= "am\r\n\r\n/* hello world */\r\n-----------------------------332604924416206460511787781";
73 | buf ~= "889\r\nContent-Disposition: form-data; name=\"file[]\"; filename=\"a.jpg\"\r\nContent-T";
74 | buf ~= "ype: image/jpeg\r\n\r\nthis is a pic.\r\n-----------------------------3326";
75 | buf ~= "04924416206460511787781889--\r\n";
76 |
77 | auto parser = new HttpRequestParser;
78 |
79 | string data = "";
80 |
81 | foreach ( b ; buf)
82 | {
83 | data ~= b;
84 | ulong result = parser.parse(data);
85 |
86 | if (parser.parserStatus() == ParserStatus.PARTIAL)
87 | {
88 | continue;
89 | }
90 |
91 | if (parser.parserStatus() == ParserStatus.COMPLETED)
92 | {
93 | auto request = parser.request();
94 |
95 | parser.reset();
96 |
97 | OnRequest(request);
98 | }
99 |
100 | if (parser.parserStatus() == ParserStatus.FAILED)
101 | {
102 | writeln("Parsing error!");
103 | break;
104 | }
105 | }
106 |
107 | writeln("Request end.");
108 | }
109 |
110 | void OnRequest(HttpRequest request)
111 | {
112 | writeln(request.path());
113 | writeln(request.method());
114 |
115 | writeln("\nHeaders:");
116 | foreach ( name, value ; request.headers() )
117 | {
118 | writeln(name, " - ", value);
119 | }
120 |
121 | writeln("\nfields:");
122 | writeln(request.fields);
123 |
124 | writeln("\nfiles:");
125 | writeln(request.files);
126 | }
127 |
128 | void main()
129 | {
130 | parseTest1();
131 | }
132 |
--------------------------------------------------------------------------------
/source/archttp/Archttp.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.Archttp;
13 |
14 | import nbuff;
15 |
16 | import geario.codec;
17 |
18 | import geario.event;
19 | import geario.logging;
20 |
21 | import geario.net.TcpListener;
22 | import geario.net.TcpStream;
23 |
24 | import geario.util.worker;
25 | import geario.util.DateTime;
26 |
27 | import geario.system.Memory : totalCPUs;
28 |
29 | // for gear http
30 | import geario.codec.Framed;
31 | import archttp.codec.HttpCodec;
32 |
33 | public import archttp.HttpContext;
34 | public import archttp.HttpRequest;
35 | public import archttp.HttpResponse;
36 | public import archttp.HttpStatusCode;
37 | public import archttp.HttpContext;
38 | public import archttp.HttpMethod;
39 |
40 | import archttp.HttpRequestHandler;
41 | import archttp.MiddlewareExecutor;
42 |
43 | import std.socket;
44 | import std.experimental.allocator;
45 |
46 | import archttp.Router;
47 |
48 | alias Router!(HttpRequestHandler, HttpRequestMiddlewareHandler) Routing;
49 |
50 | class Archttp
51 | {
52 | private
53 | {
54 | uint _ioThreads;
55 | uint _workerThreads;
56 |
57 | Address _addr;
58 | string _host;
59 | ushort _port;
60 |
61 | bool _isRunning = false;
62 |
63 | TcpListener _listener;
64 | EventLoop _loop;
65 |
66 | Routing _router;
67 | Routing[string] _mountedRouters;
68 | ulong _mountedRoutersMaxLength;
69 | }
70 |
71 | this(uint ioThreads = totalCPUs, uint workerThreads = 0)
72 | {
73 | _ioThreads = ioThreads > 1 ? ioThreads : 1;
74 | _workerThreads = workerThreads;
75 |
76 | _router = new Routing;
77 | _loop = new EventLoop;
78 | }
79 |
80 | static Routing newRouter()
81 | {
82 | return new Routing;
83 | }
84 |
85 | Archttp use(HttpRequestMiddlewareHandler handler)
86 | {
87 | _router.use(handler);
88 | return this;
89 | }
90 |
91 | Archttp get(string route, HttpRequestHandler handler)
92 | {
93 | _router.add(route, HttpMethod.GET, handler);
94 | return this;
95 | }
96 |
97 | Archttp post(string route, HttpRequestHandler handler)
98 | {
99 | _router.add(route, HttpMethod.POST, handler);
100 | return this;
101 | }
102 |
103 | Archttp put(string route, HttpRequestHandler handler)
104 | {
105 | _router.add(route, HttpMethod.PUT, handler);
106 | return this;
107 | }
108 |
109 | Archttp Delete(string route, HttpRequestHandler handler)
110 | {
111 | _router.add(route, HttpMethod.DELETE, handler);
112 | return this;
113 | }
114 |
115 | Archttp use(string path, Routing router)
116 | {
117 | _mountedRouters[path] = router;
118 | router.onMount(this);
119 |
120 | return this;
121 | }
122 |
123 | private void handle(HttpContext httpContext)
124 | {
125 | import std.string : indexOf, stripRight;
126 |
127 | Routing router;
128 | string path = httpContext.request().path().length > 1 ? httpContext.request().path().stripRight("/") : httpContext.request().path();
129 |
130 | // check app mounted routers
131 | if (_mountedRouters.length && path.length > 1)
132 | {
133 | string mountpath = path;
134 |
135 | long index = path[1 .. $].indexOf("/");
136 |
137 | if (index > 0)
138 | {
139 | index++;
140 | mountpath = path[0 .. index];
141 | }
142 |
143 | router = _mountedRouters.get(mountpath, null);
144 | if (router !is null)
145 | {
146 | if (mountpath.length == path.length)
147 | path = "/";
148 | else
149 | path = path[index .. $];
150 | }
151 |
152 | // Tracef("mountpath: %s, path: %s", mountpath, path);
153 | }
154 |
155 | if (router is null)
156 | router = _router;
157 |
158 | // use middlewares for Router
159 | MiddlewareExecutor(httpContext.request(), httpContext.response(), router.middlewareHandlers()).execute();
160 |
161 | auto handler = router.match(path, httpContext.request().method(), httpContext.request().middlewareHandlers, httpContext.request().params);
162 |
163 | if (handler is null)
164 | {
165 | httpContext.response().code(HttpStatusCode.NOT_FOUND).send("404 Not Found.");
166 | }
167 | else
168 | {
169 | // use middlewares for HttpRequestHandler
170 | MiddlewareExecutor(httpContext.request(), httpContext.response(), httpContext.request().middlewareHandlers).execute();
171 | handler(httpContext.request(), httpContext.response());
172 |
173 | if (!httpContext.response().headerSent())
174 | httpContext.response().send();
175 | }
176 |
177 | if (!httpContext.keepAlive())
178 | httpContext.End();
179 | }
180 |
181 | private void accepted(TcpListener listener, TcpStream connection)
182 | {
183 | auto codec = new HttpCodec();
184 | auto framed = codec.CreateFramed(connection);
185 | auto context = new HttpContext(connection, framed);
186 |
187 | framed.OnFrame((HttpRequest request)
188 | {
189 | context.request(request);
190 | handle(context);
191 | });
192 |
193 | connection.Error((IoError error) {
194 | log.error("Error occurred: %d %s", error.errorCode, error.errorMsg);
195 | });
196 | }
197 |
198 | Archttp bind(string host, ushort port)
199 | {
200 | _host = host;
201 | _port = port;
202 | _addr = new InternetAddress(host, port);
203 |
204 | return this;
205 | }
206 |
207 | Archttp bind(ushort port)
208 | {
209 | return bind("0.0.0.0", port);
210 | }
211 |
212 | void listen(ushort port)
213 | {
214 | this.bind(port);
215 | this.run();
216 | }
217 |
218 | private void showHttpServiceInformation()
219 | {
220 | import std.stdio : writeln;
221 | import std.conv : to;
222 |
223 | string text = `
224 | # Archttp service has been started!
225 | - IO threads: ` ~ _ioThreads.to!string ~ `
226 | - Listening: ` ~ _addr.toString() ~ "\n";
227 |
228 | writeln(text);
229 | }
230 |
231 | void run()
232 | {
233 | DateTime.StartClock();
234 |
235 | showHttpServiceInformation();
236 |
237 | TcpListener _listener = new TcpListener(_loop, _addr.addressFamily);
238 |
239 | _listener.Threads(_ioThreads);
240 | _listener.Bind(_addr).Listen(1024);
241 | _listener.Accepted(&accepted);
242 | _listener.Start();
243 |
244 | _isRunning = true;
245 | _loop.Run();
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/source/archttp/Cookie.d:
--------------------------------------------------------------------------------
1 | module archttp.Cookie;
2 |
3 | import std.uri;
4 | import std.format;
5 | import std.array;
6 |
7 | class Cookie
8 | {
9 | @safe:
10 |
11 | private
12 | {
13 | string _name;
14 | string _value;
15 | string _domain;
16 | string _path;
17 | string _expires;
18 | long _maxAge;
19 | bool _secure;
20 | bool _httpOnly;
21 | }
22 |
23 | this(string name, string value = "", string path = "/", string domain = "", string expires = "", long maxAge = 3600, bool secure = false, bool httpOnly = false)
24 | {
25 | _name = name;
26 |
27 | this.value(value)
28 | .domain(domain)
29 | .path(path)
30 | .expires(expires)
31 | .maxAge(maxAge)
32 | .secure(secure)
33 | .httpOnly(httpOnly);
34 | }
35 |
36 | Cookie parse(string cookieString)
37 | {
38 | // if (!cookieString.length)
39 | // return null;
40 |
41 | // auto parts = cookieString.splitter(';');
42 | // auto idx = parts.front.indexOf('=');
43 | // if (idx == -1)
44 | // return null;
45 |
46 | // auto name = parts.front[0 .. idx].strip();
47 | // dst.m_value = parts.front[name.length + 1 .. $].strip();
48 | // parts.popFront();
49 |
50 | // if (!name.length)
51 | // return null;
52 |
53 | // foreach(part; parts) {
54 | // if (!part.length)
55 | // continue;
56 |
57 | // idx = part.indexOf('=');
58 | // if (idx == -1) {
59 | // idx = part.length;
60 | // }
61 | // auto key = part[0 .. idx].strip();
62 | // auto value = part[min(idx + 1, $) .. $].strip();
63 |
64 | // try {
65 | // if (key.sicmp("httponly") == 0) {
66 | // dst.m_httpOnly = true;
67 | // } else if (key.sicmp("secure") == 0) {
68 | // dst.m_secure = true;
69 | // } else if (key.sicmp("expires") == 0) {
70 | // // RFC 822 got updated by RFC 1123 (which is to be used) but is valid for this
71 | // // this parsing is just for validation
72 | // parseRFC822DateTimeString(value);
73 | // dst.m_expires = value;
74 | // } else if (key.sicmp("max-age") == 0) {
75 | // if (value.length && value[0] != '-')
76 | // dst.m_maxAge = value.to!long;
77 | // } else if (key.sicmp("domain") == 0) {
78 | // if (value.length && value[0] == '.')
79 | // value = value[1 .. $]; // the leading . must be stripped (5.2.3)
80 |
81 | // enforce!ConvException(value.all!(a => a >= 32), "Cookie Domain must not contain any control characters");
82 | // dst.m_domain = value.toLower; // must be lower (5.2.3)
83 | // } else if (key.sicmp("path") == 0) {
84 | // if (value.length && value[0] == '/') {
85 | // enforce!ConvException(value.all!(a => a >= 32), "Cookie Path must not contain any control characters");
86 | // dst.m_path = value;
87 | // } else {
88 | // dst.m_path = null;
89 | // }
90 | // } // else extension value...
91 | // } catch (DateTimeException) {
92 | // } catch (ConvException) {
93 | // }
94 | // // RFC 6265 says to ignore invalid values on all of these fields
95 | // }
96 | // return name;
97 | return null;
98 | }
99 |
100 | string name() const
101 | {
102 | return _name;
103 | }
104 |
105 | Cookie value(string value)
106 | {
107 | _value = encode(value);
108 | return this;
109 | }
110 |
111 | string value() const
112 | {
113 | return decode(_value);
114 | }
115 |
116 | Cookie domain(string value)
117 | {
118 | _domain = value;
119 | return this;
120 | }
121 |
122 | string domain() const
123 | {
124 | return _domain;
125 | }
126 |
127 | Cookie path(string value)
128 | {
129 | _path = value;
130 | return this;
131 | }
132 |
133 | string path() const
134 | {
135 | return _path;
136 | }
137 |
138 | Cookie expires(string value)
139 | {
140 | _expires = value;
141 | return this;
142 | }
143 |
144 | string expires() const
145 | {
146 | return _expires;
147 | }
148 |
149 | Cookie maxAge(long value)
150 | {
151 | _maxAge = value;
152 | return this;
153 | }
154 |
155 | long maxAge() const
156 | {
157 | return _maxAge;
158 | }
159 |
160 | Cookie secure(bool value)
161 | {
162 | _secure = value;
163 | return this;
164 | }
165 |
166 | bool secure() const
167 | {
168 | return _secure;
169 | }
170 |
171 | Cookie httpOnly(bool value)
172 | {
173 | _httpOnly = value;
174 | return this;
175 | }
176 |
177 | bool httpOnly() const
178 | {
179 | return _httpOnly;
180 | }
181 |
182 | override string toString()
183 | {
184 | auto text = appender!string;
185 | text ~= format!"%s=%s"(this._name, this.value());
186 |
187 | if (this._domain && this._domain != "")
188 | {
189 | text ~= format!"; Domain=%s"(this._domain);
190 | }
191 |
192 | if (this._path != "")
193 | {
194 | text ~= format!"; Path=%s"(this._path);
195 | }
196 |
197 | if (this.expires != "")
198 | {
199 | text ~= format!"; Expires=%s"(this._expires);
200 | }
201 |
202 | if (this.maxAge)
203 | text ~= format!"; Max-Age=%s"(this._maxAge);
204 |
205 | if (this.secure)
206 | text ~= "; Secure";
207 |
208 | if (this.httpOnly)
209 | text ~= "; HttpOnly";
210 |
211 | return text[];
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/source/archttp/HttpContext.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpContext;
13 |
14 | import archttp.HttpRequest;
15 | import archttp.HttpResponse;
16 | import archttp.HttpRequestHandler;
17 |
18 | import nbuff;
19 |
20 | import geario.net.TcpStream;
21 | import geario.codec.Framed;
22 | import geario.logging;
23 |
24 | alias Framed!(HttpRequest, HttpResponse) HttpFramed;
25 |
26 | class HttpContext
27 | {
28 | private HttpRequest _request;
29 | private HttpResponse _response;
30 | private TcpStream _connection;
31 | private HttpFramed _framed;
32 | private bool _keepAlive;
33 | private bool _keepAliveSetted;
34 |
35 | this(TcpStream connection, HttpFramed framed)
36 | {
37 | _connection = connection;
38 | _framed = framed;
39 | }
40 |
41 | HttpRequest request() {
42 | return _request;
43 | }
44 |
45 | void request(HttpRequest request)
46 | {
47 | _request = request;
48 | _request.context(this);
49 |
50 | initKeepAliveValue();
51 | }
52 |
53 | HttpResponse response()
54 | {
55 | if (_response is null)
56 | _response = new HttpResponse(this);
57 |
58 | return _response;
59 | }
60 |
61 | void response(HttpResponse response)
62 | {
63 | _response = response;
64 | }
65 |
66 | TcpStream connection()
67 | {
68 | return _connection;
69 | }
70 |
71 | bool keepAlive()
72 | {
73 | return _keepAlive;
74 | }
75 |
76 | private void initKeepAliveValue()
77 | {
78 | if (false == _keepAliveSetted)
79 | {
80 | string connectionType = _request.header("Connection");
81 | if (connectionType.length && connectionType == "close")
82 | _keepAlive = false;
83 | else
84 | _keepAlive = true;
85 |
86 | _keepAliveSetted = true;
87 | }
88 | }
89 |
90 | void Write(string data)
91 | {
92 | _connection.Write(cast(ubyte[])data);
93 | }
94 |
95 | void Write(NbuffChunk bytes)
96 | {
97 | _connection.Write(bytes);
98 | }
99 |
100 | void Send(HttpResponse response)
101 | {
102 | _framed.Send(response);
103 | }
104 |
105 | void End()
106 | {
107 | _connection.Close();
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/source/archttp/HttpHeader.d:
--------------------------------------------------------------------------------
1 | module archttp.HttpHeader;
2 |
3 | import geario.logging;
4 |
5 | import std.algorithm;
6 | import std.conv;
7 | import std.string;
8 |
9 | /**
10 | * Http header name
11 | */
12 | struct HttpHeader
13 | {
14 | enum string NULL = "Null";
15 |
16 | /**
17 | * General Fields.
18 | */
19 | enum string CONNECTION = "Connection";
20 | enum string CACHE_CONTROL = "Cache-Control";
21 | enum string DATE = "Date";
22 | enum string PRAGMA = "Pragma";
23 | enum string PROXY_CONNECTION = "Proxy-Connection";
24 | enum string TRAILER = "Trailer";
25 | enum string TRANSFER_ENCODING = "Transfer-Encoding";
26 | enum string UPGRADE = "Upgrade";
27 | enum string VIA = "Via";
28 | enum string WARNING = "Warning";
29 | enum string NEGOTIATE = "Negotiate";
30 |
31 | /**
32 | * Entity Fields.
33 | */
34 | enum string ALLOW = "Allow";
35 | enum string CONTENT_DISPOSITION = "Content-Disposition";
36 | enum string CONTENT_ENCODING = "Content-Encoding";
37 | enum string CONTENT_LANGUAGE = "Content-Language";
38 | enum string CONTENT_LENGTH = "Content-Length";
39 | enum string CONTENT_LOCATION = "Content-Location";
40 | enum string CONTENT_MD5 = "Content-MD5";
41 | enum string CONTENT_RANGE = "Content-Range";
42 | enum string CONTENT_TYPE = "Content-Type";
43 | enum string EXPIRES = "Expires";
44 | enum string LAST_MODIFIED = "Last-Modified";
45 |
46 | /**
47 | * Request Fields.
48 | */
49 | enum string ACCEPT = "Accept";
50 | enum string ACCEPT_CHARSET = "Accept-Charset";
51 | enum string ACCEPT_ENCODING = "Accept-Encoding";
52 | enum string ACCEPT_LANGUAGE = "Accept-Language";
53 | enum string AUTHORIZATION = "Authorization";
54 | enum string EXPECT = "Expect";
55 | enum string FORWARDED = "Forwarded";
56 | enum string FROM = "From";
57 | enum string HOST = "Host";
58 | enum string IF_MATCH = "If-Match";
59 | enum string IF_MODIFIED_SINCE = "If-Modified-Since";
60 | enum string IF_NONE_MATCH = "If-None-Match";
61 | enum string IF_RANGE = "If-Range";
62 | enum string IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
63 | enum string KEEP_ALIVE = "Keep-Alive";
64 | enum string MAX_FORWARDS = "Max-Forwards";
65 | enum string PROXY_AUTHORIZATION = "Proxy-Authorization";
66 | enum string RANGE = "Range";
67 | enum string REQUEST_RANGE = "Request-Range";
68 | enum string REFERER = "Referer";
69 | enum string TE = "TE";
70 | enum string USER_AGENT = "User-Agent";
71 | enum string X_FORWARDED_FOR = "X-Forwarded-For";
72 | enum string X_FORWARDED_PROTO = "X-Forwarded-Proto";
73 | enum string X_FORWARDED_SERVER = "X-Forwarded-Server";
74 | enum string X_FORWARDED_HOST = "X-Forwarded-Host";
75 |
76 | /**
77 | * Response Fields.
78 | */
79 | enum string ACCEPT_RANGES = "Accept-Ranges";
80 | enum string AGE = "Age";
81 | enum string ETAG = "ETag";
82 | enum string LOCATION = "Location";
83 | enum string PROXY_AUTHENTICATE = "Proxy-Authenticate";
84 | enum string RETRY_AFTER = "Retry-After";
85 | enum string SERVER = "Server";
86 | enum string SERVLET_ENGINE = "Servlet-Engine";
87 | enum string VARY = "Vary";
88 | enum string WWW_AUTHENTICATE = "WWW-Authenticate";
89 |
90 | /**
91 | * WebSocket Fields.
92 | */
93 | enum string ORIGIN = "Origin";
94 | enum string SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
95 | enum string SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
96 | enum string SEC_WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions";
97 | enum string SEC_WEBSOCKET_SUBPROTOCOL = "Sec-WebSocket-Protocol";
98 | enum string SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept";
99 |
100 | /**
101 | * Other Fields.
102 | */
103 | enum string COOKIE = "Cookie";
104 | enum string SET_COOKIE = "Set-Cookie";
105 | enum string SET_COOKIE2 = "Set-Cookie2";
106 | enum string MIME_VERSION = "MIME-Version";
107 | enum string IDENTITY = "identity";
108 |
109 | enum string X_POWERED_BY = "X-Powered-By";
110 | enum string HTTP2_SETTINGS = "HTTP2-Settings";
111 |
112 | enum string STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security";
113 |
114 | /**
115 | * HTTP2 Fields.
116 | */
117 | enum string C_METHOD = ":method";
118 | enum string C_SCHEME = ":scheme";
119 | enum string C_AUTHORITY = ":authority";
120 | enum string C_PATH = ":path";
121 | enum string C_STATUS = ":status";
122 |
123 | enum string UNKNOWN = "::UNKNOWN::";
124 | }
125 |
--------------------------------------------------------------------------------
/source/archttp/HttpMessageParser.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpMessageParser;
13 |
14 | nothrow @nogc:
15 |
16 | /// Parser error codes
17 | enum ParserError : int
18 | {
19 | partial = 1, /// not enough data to parse message
20 | newLine, /// invalid character in new line
21 | headerName, /// invalid character in header name
22 | headerValue, /// invalid header value
23 | status, /// invalid character in response status
24 | token, /// invalid character in token
25 | noHeaderName, /// empty header name
26 | noMethod, /// no method in request line
27 | noVersion, /// no version in request line / response status line
28 | noUri, /// no URI in request line
29 | noStatus, /// no status code or text in status line
30 | invalidMethod, /// invalid method in request line
31 | invalidVersion, /// invalid version for the protocol message
32 | }
33 |
34 | struct HttpRequestHeader
35 | {
36 | const(char)[] name;
37 | const(char)[] value;
38 | }
39 |
40 | interface HttpMessageHandler
41 | {
42 | void onMethod(const(char)[] method);
43 | void onUri(const(char)[] uri);
44 | int onVersion(const(char)[] ver);
45 | void onHeader(const(char)[] name, const(char)[] value);
46 | void onStatus(int status);
47 | void onStatusMsg(const(char)[] statusMsg);
48 | }
49 |
50 | /**
51 | * HTTP/RTSP message parser.
52 | */
53 | class HttpMessageParser
54 | {
55 | import std.traits : ForeachType, isArray, Unqual;
56 |
57 | this(HttpMessageHandler handler)
58 | {
59 | this._messageHandler = handler;
60 | }
61 |
62 | /**
63 | * Parses message request (request line + headers).
64 | *
65 | * Params:
66 | * - buffer = buffer to parse message from
67 | * - lastPos = optional argument to store / pass previous position to which message was
68 | * already parsed (speeds up parsing when message comes in parts)
69 | *
70 | * Returns:
71 | * * parsed message header length when parsed sucessfully
72 | * * `-ParserError` on error (ie. -1 when message header is not complete yet)
73 | */
74 | long parseRequest(T)(T buffer, ref ulong lastPos)
75 | if (isArray!T && (is(Unqual!(ForeachType!T) == char) || is(Unqual!(ForeachType!T) == ubyte)))
76 | {
77 | static if (is(Unqual!(ForeachType!T) == char)) return parse!parseRequestLine(cast(const(ubyte)[])buffer, lastPos);
78 | else return parse!parseRequestLine(buffer, lastPos);
79 | }
80 |
81 | /// ditto
82 | long parseRequest(T)(T buffer)
83 | if (isArray!T && (is(Unqual!(ForeachType!T) == char) || is(Unqual!(ForeachType!T) == ubyte)))
84 | {
85 | ulong lastPos;
86 | static if (is(Unqual!(ForeachType!T) == char)) return parse!parseRequestLine(cast(const(ubyte)[])buffer, lastPos);
87 | else return parse!parseRequestLine(buffer, lastPos);
88 | }
89 |
90 | /**
91 | * Parses message response (status line + headers).
92 | *
93 | * Params:
94 | * - buffer = buffer to parse message from
95 | * - lastPos = optional argument to store / pass previous position to which message was
96 | * already parsed (speeds up parsing when message comes in parts)
97 | *
98 | * Returns:
99 | * * parsed message header length when parsed sucessfully
100 | * * `-ParserError.partial` on error (ie. -1 when message header is not comlete yet)
101 | */
102 | long parseResponse(T)(T buffer, ref ulong lastPos)
103 | if (isArray!T && (is(Unqual!(ForeachType!T) == char) || is(Unqual!(ForeachType!T) == ubyte)))
104 | {
105 | static if (is(Unqual!(ForeachType!T) == char)) return parse!parseStatusLine(cast(const(ubyte)[])buffer, lastPos);
106 | else return parse!parseStatusLine(buffer, lastPos);
107 | }
108 |
109 | /// ditto
110 | int parseResponse(T)(T buffer)
111 | if (isArray!T && (is(Unqual!(ForeachType!T) == char) || is(Unqual!(ForeachType!T) == ubyte)))
112 | {
113 | ulong lastPos;
114 | static if (is(Unqual!(ForeachType!T) == char)) return parse!parseStatusLine(cast(const(ubyte)[])buffer, lastPos);
115 | else return parse!parseStatusLine(buffer, lastPos);
116 | }
117 |
118 | /// Gets provided structure used during parsing
119 | HttpMessageHandler messageHandler() { return _messageHandler; }
120 |
121 | private:
122 |
123 | // character map of valid characters for token, forbidden:
124 | // 0-SP, DEL, HT
125 | // ()<>@,;:\"/[]?={}
126 | enum tokenRanges = "\0 \"\"(),,//:@[]{}\x7f\xff";
127 | enum tokenSSERanges = "\0 \"\"(),,//:@[]{\xff"; // merge of last range due to the SSE register size limit
128 |
129 | enum versionRanges = "\0-:@[`{\xff"; // allow only [A-Za-z./] characters
130 |
131 | HttpMessageHandler _messageHandler;
132 |
133 | long parse(alias pred)(const(ubyte)[] buffer, ref ulong lastPos)
134 | {
135 | assert(buffer.length >= lastPos);
136 | immutable l = buffer.length;
137 |
138 | if (_expect(!lastPos, true))
139 | {
140 | if (_expect(!buffer.length, false)) return err(ParserError.partial);
141 |
142 | // skip first empty line (some clients add CRLF after POST content)
143 | if (_expect(buffer[0] == '\r', false))
144 | {
145 | if (_expect(buffer.length == 1, false)) return err(ParserError.partial);
146 | if (_expect(buffer[1] != '\n', false)) return err(ParserError.newLine);
147 | lastPos += 2;
148 | buffer = buffer[lastPos..$];
149 | }
150 | else if (_expect(buffer[0] == '\n', false))
151 | buffer = buffer[++lastPos..$];
152 |
153 | immutable res = pred(buffer);
154 | if (_expect(res < 0, false)) return res;
155 |
156 | lastPos = cast(int)(l - buffer.length); // store index of last parsed line
157 | }
158 | else buffer = buffer[lastPos..$]; // skip already parsed lines
159 |
160 | immutable hdrRes = parseHeaders(buffer);
161 | lastPos = cast(int)(l - buffer.length); // store index of last parsed line
162 |
163 | if (_expect(hdrRes < 0, false)) return hdrRes;
164 | return lastPos; // finished
165 | }
166 |
167 | int parseHeaders(ref const(ubyte)[] buffer)
168 | {
169 | bool hasHeader;
170 | size_t start, i;
171 | const(ubyte)[] name, value;
172 | while (true)
173 | {
174 | // check for msg headers end
175 | if (_expect(buffer.length == 0, false)) return err(ParserError.partial);
176 | if (buffer[0] == '\r')
177 | {
178 | if (_expect(buffer.length == 1, false)) return err(ParserError.partial);
179 | if (_expect(buffer[1] != '\n', false)) return err(ParserError.newLine);
180 |
181 | buffer = buffer[2..$];
182 | return 0;
183 | }
184 | if (_expect(buffer[0] == '\n', false))
185 | {
186 | buffer = buffer[1..$];
187 | return 0;
188 | }
189 |
190 | if (!hasHeader || (buffer[i] != ' ' && buffer[i] != '\t'))
191 | {
192 | auto ret = parseToken!(tokenRanges, ':', tokenSSERanges)(buffer, i);
193 | if (_expect(ret < 0, false)) return ret;
194 | if (_expect(start == i, false)) return err(ParserError.noHeaderName);
195 | name = buffer[start..i]; // store header name
196 | i++; // move index after colon
197 |
198 | // skip over SP and HT
199 | for (;; ++i)
200 | {
201 | if (_expect(i == buffer.length, false)) return err(ParserError.partial);
202 | if (buffer[i] != ' ' && buffer[i] != '\t') break;
203 | }
204 | start = i;
205 | }
206 | else name = null; // multiline header
207 |
208 | // parse value
209 | auto ret = parseToken!("\0\010\012\037\177\177", "\r\n")(buffer, i);
210 | if (_expect(ret < 0, false)) return ret;
211 | value = buffer[start..i];
212 | mixin(advanceNewline);
213 | hasHeader = true; // flag to define that we can now accept multiline header values
214 |
215 | // remove trailing SPs and HTABs
216 | if (_expect(value.length && (value[$-1] == ' ' || value[$-1] == '\t'), false))
217 | {
218 | int j = cast(int)value.length - 2;
219 | for (; j >= 0; --j)
220 | if (!(value[j] == ' ' || value[j] == '\t'))
221 | break;
222 | value = value[0..j+1];
223 | }
224 |
225 | static if (is(typeof(_messageHandler.onHeader("", "")) == void))
226 | _messageHandler.onHeader(cast(const(char)[])name, cast(const(char)[])value);
227 | else {
228 | auto r = _messageHandler.onHeader(cast(const(char)[])name, cast(const(char)[])value);
229 | if (_expect(r < 0, false)) return r;
230 | }
231 |
232 | // header line completed -> advance buffer
233 | buffer = buffer[i..$];
234 | start = i = 0;
235 | }
236 | assert(0);
237 | }
238 |
239 | auto parseRequestLine(ref const(ubyte)[] buffer)
240 | {
241 | size_t start, i;
242 |
243 | // METHOD
244 | auto ret = parseToken!(tokenRanges, ' ', tokenSSERanges)(buffer, i);
245 | if (_expect(ret < 0, false)) return ret;
246 | if (_expect(start == i, false)) return err(ParserError.noMethod);
247 |
248 | static if (is(typeof(_messageHandler.onMethod("")) == void))
249 | _messageHandler.onMethod(cast(const(char)[])buffer[start..i]);
250 | else {
251 | auto r = _messageHandler.onMethod(cast(const(char)[])buffer[start..i]);
252 | if (_expect(r < 0, false)) return r;
253 | }
254 |
255 | mixin(skipSpaces!(ParserError.noUri));
256 | start = i;
257 |
258 | // PATH
259 | ret = parseToken!("\000\040\177\177", ' ')(buffer, i);
260 | if (_expect(ret < 0, false)) return ret;
261 |
262 | static if (is(typeof(_messageHandler.onUri("")) == void))
263 | _messageHandler.onUri(cast(const(char)[])buffer[start..i]);
264 | else {
265 | auto ur = _messageHandler.onUri(cast(const(char)[])buffer[start..i]);
266 | if (_expect(ur < 0, false)) return ur;
267 | }
268 |
269 | mixin(skipSpaces!(ParserError.noVersion));
270 | start = i;
271 |
272 | // VERSION
273 | ret = parseToken!(versionRanges, "\r\n")(buffer, i);
274 | if (_expect(ret < 0, false)) return ret;
275 |
276 | static if (is(typeof(_messageHandler.onVersion("")) == void))
277 | _messageHandler.onVersion(cast(const(char)[])buffer[start..i]);
278 | else {
279 | auto vr = _messageHandler.onVersion(cast(const(char)[])buffer[start..i]);
280 | if (_expect(vr < 0, false)) return vr;
281 | }
282 |
283 | mixin(advanceNewline);
284 |
285 | // advance buffer after the request line
286 | buffer = buffer[i..$];
287 | return 0;
288 | }
289 |
290 | auto parseStatusLine(ref const(ubyte)[] buffer)
291 | {
292 | size_t start, i;
293 |
294 | // VERSION
295 | auto ret = parseToken!(versionRanges, ' ')(buffer, i);
296 | if (_expect(ret < 0, false)) return ret;
297 | if (_expect(start == i, false)) return err(ParserError.noVersion);
298 |
299 | static if (is(typeof(_messageHandler.onVersion("")) == void))
300 | _messageHandler.onVersion(cast(const(char)[])buffer[start..i]);
301 | else {
302 | auto r = _messageHandler.onVersion(cast(const(char)[])buffer[start..i]);
303 | if (_expect(r < 0, false)) return r;
304 | }
305 |
306 | mixin(skipSpaces!(ParserError.noStatus));
307 | start = i;
308 |
309 | // STATUS CODE
310 | if (_expect(i+3 >= buffer.length, false))
311 | return err(ParserError.partial); // not enough data - we want at least [:digit:][:digit:][:digit:] to try to parse
312 |
313 | int code;
314 | foreach (j, m; [100, 10, 1])
315 | {
316 | if (buffer[i+j] < '0' || buffer[i+j] > '9') return err(ParserError.status);
317 | code += (buffer[start+j] - '0') * m;
318 | }
319 | i += 3;
320 |
321 | static if (is(typeof(_messageHandler.onStatus(code)) == void))
322 | _messageHandler.onStatus(code);
323 | else {
324 | auto sr = _messageHandler.onStatus(code);
325 | if (_expect(sr < 0, false)) return sr;
326 | }
327 |
328 | if (_expect(i == buffer.length, false))
329 | return err(ParserError.partial);
330 | if (_expect(buffer[i] != ' ' && buffer[i] != '\r' && buffer[i] != '\n', false))
331 | return err(ParserError.status); // Garbage after status
332 |
333 | start = i;
334 |
335 | // MESSAGE
336 | ret = parseToken!("\0\010\012\037\177\177", "\r\n")(buffer, i);
337 | if (_expect(ret < 0, false)) return ret;
338 |
339 | // remove preceding space (we did't advance over spaces because possibly missing status message)
340 | if (i > start)
341 | {
342 | while (buffer[start] == ' ' && start < i) start++;
343 | if (i > start)
344 | {
345 | static if (is(typeof(_messageHandler.onStatusMsg("")) == void))
346 | _messageHandler.onStatusMsg(cast(const(char)[])buffer[start..i]);
347 | else {
348 | auto smr = _messageHandler.onStatusMsg(cast(const(char)[])buffer[start..i]);
349 | if (_expect(smr < 0, false)) return smr;
350 | }
351 | }
352 | }
353 |
354 | mixin(advanceNewline);
355 |
356 | // advance buffer after the status line
357 | buffer = buffer[i..$];
358 | return 0;
359 | }
360 |
361 | /*
362 | * Advances buffer over the token to the next character while checking for valid characters.
363 | * On success, buffer index is left on the next character.
364 | *
365 | * Params:
366 | * - ranges = ranges of characters to stop on
367 | * - sseRanges = if null, same ranges is used, but they are limited to 8 ranges
368 | * - next = next character/s to stop on (must be present in the provided ranges too)
369 | * Returns: 0 on success error code otherwise
370 | */
371 | int parseToken(string ranges, alias next, string sseRanges = null)(const(ubyte)[] buffer, ref size_t i) pure
372 | {
373 | version (DigitalMars) {
374 | static if (__VERSION__ >= 2094) pragma(inline, true); // older compilers can't inline this
375 | } else pragma(inline, true);
376 |
377 | immutable charMap = parseTokenCharMap!(ranges)();
378 |
379 | static if (LDC_with_SSE42)
380 | {
381 | // CT function to prepare input for SIMD vector enum
382 | static byte[16] padRanges()(string ranges)
383 | {
384 | byte[16] res;
385 | // res[0..ranges.length] = cast(byte[])ranges[]; - broken on macOS betterC tests
386 | foreach (i, c; ranges) res[i] = cast(byte)c;
387 | return res;
388 | }
389 |
390 | static if (sseRanges) alias usedRng = sseRanges;
391 | else alias usedRng = ranges;
392 | static assert(usedRng.length <= 16, "Ranges must be at most 16 characters long");
393 | static assert(usedRng.length % 2 == 0, "Ranges must have even number of characters");
394 | enum rangesSize = usedRng.length;
395 | enum byte16 rngE = padRanges(usedRng);
396 |
397 | if (_expect(buffer.length - i >= 16, true))
398 | {
399 | size_t left = (buffer.length - i) & ~15; // round down to multiple of 16
400 | byte16 ranges16 = rngE;
401 |
402 | do
403 | {
404 | byte16 b16 = () @trusted { return cast(byte16)_mm_loadu_si128(cast(__m128i*)&buffer[i]); }();
405 | immutable r = _mm_cmpestri(
406 | ranges16, rangesSize,
407 | b16, 16,
408 | _SIDD_LEAST_SIGNIFICANT | _SIDD_CMP_RANGES | _SIDD_UBYTE_OPS
409 | );
410 |
411 | if (r != 16)
412 | {
413 | i += r;
414 | goto FOUND;
415 | }
416 | i += 16;
417 | left -= 16;
418 | }
419 | while (_expect(left != 0, true));
420 | }
421 | }
422 | else
423 | {
424 | // faster unrolled loop to iterate over 8 characters
425 | loop: while (_expect(buffer.length - i >= 8, true))
426 | {
427 | static foreach (_; 0..8)
428 | {
429 | if (_expect(!charMap[buffer[i]], false)) goto FOUND;
430 | ++i;
431 | }
432 | }
433 | }
434 |
435 | // handle the rest
436 | if (_expect(i >= buffer.length, false)) return err(ParserError.partial);
437 |
438 | FOUND:
439 | while (true)
440 | {
441 | static if (is(typeof(next) == char)) {
442 | static assert(!charMap[next], "Next character is not in ranges");
443 | if (buffer[i] == next) return 0;
444 | } else {
445 | static assert(next.length > 0, "Next character not provided");
446 | static foreach (c; next) {
447 | static assert(!charMap[c], "Next character is not in ranges");
448 | if (buffer[i] == c) return 0;
449 | }
450 | }
451 | if (_expect(!charMap[buffer[i]], false)) return err(ParserError.token);
452 | if (_expect(++i == buffer.length, false)) return err(ParserError.partial);
453 | }
454 | }
455 |
456 | // advances over new line
457 | enum advanceNewline = q{
458 | assert(i < buffer.length);
459 | if (_expect(buffer[i] == '\r', true))
460 | {
461 | if (_expect(i+1 == buffer.length, false)) return err(ParserError.partial);
462 | if (_expect(buffer[i+1] != '\n', false)) return err(ParserError.newLine);
463 | i += 2;
464 | }
465 | else if (buffer[i] == '\n') ++i;
466 | else assert(0);
467 | };
468 |
469 | // skips over spaces in the buffer
470 | template skipSpaces(ParserError err)
471 | {
472 | enum skipSpaces = `
473 | do {
474 | ++i;
475 | if (_expect(buffer.length == i, false)) return err(ParserError.partial);
476 | if (_expect(buffer[i] == '\r' || buffer[i] == '\n', false)) return err(` ~ err.stringof ~ `);
477 | } while (buffer[i] == ' ');
478 | `;
479 | }
480 | }
481 |
482 | /**
483 | * Parses HTTP version from a slice returned in `onVersion` callback.
484 | *
485 | * Returns: minor version (0 for HTTP/1.0 or 1 for HTTP/1.1) on success or
486 | * `-ParserError.invalidVersion` on error
487 | */
488 | int parseHttpVersion(const(char)[] ver) pure
489 | {
490 | if (_expect(ver.length != 8, false)) return err(ParserError.invalidVersion);
491 |
492 | static foreach (i, c; "HTTP/1.")
493 | if (_expect(ver[i] != c, false)) return err(ParserError.invalidVersion);
494 |
495 | if (_expect(ver[7] < '0' || ver[7] > '9', false)) return err(ParserError.invalidVersion);
496 |
497 | return ver[7] - '0';
498 | }
499 |
500 | private:
501 |
502 | int err(ParserError e) pure { pragma(inline, true); return -(cast(int)e); }
503 |
504 | /// Builds valid char map from the provided ranges of invalid ones
505 | bool[256] buildValidCharMap()(string invalidRanges)
506 | {
507 | assert(invalidRanges.length % 2 == 0, "Uneven ranges");
508 | bool[256] res = true;
509 |
510 | for (int i=0; i < invalidRanges.length; i+=2)
511 | for (int j=invalidRanges[i]; j <= invalidRanges[i+1]; ++j)
512 | res[j] = false;
513 | return res;
514 | }
515 |
516 | immutable(bool[256]) parseTokenCharMap(string invalidRanges)() {
517 | static immutable charMap = buildValidCharMap(invalidRanges);
518 | return charMap;
519 | }
520 |
521 | //** used intrinsics **//
522 |
523 | version(LDC)
524 | {
525 | public import core.simd;
526 | public import ldc.intrinsics;
527 | import ldc.gccbuiltins_x86;
528 |
529 | enum LDC_with_SSE42 = __traits(targetHasFeature, "sse4.2");
530 |
531 | // These specify the type of data that we're comparing.
532 | enum _SIDD_UBYTE_OPS = 0x00;
533 | enum _SIDD_UWORD_OPS = 0x01;
534 | enum _SIDD_SBYTE_OPS = 0x02;
535 | enum _SIDD_SWORD_OPS = 0x03;
536 |
537 | // These specify the type of comparison operation.
538 | enum _SIDD_CMP_EQUAL_ANY = 0x00;
539 | enum _SIDD_CMP_RANGES = 0x04;
540 | enum _SIDD_CMP_EQUAL_EACH = 0x08;
541 | enum _SIDD_CMP_EQUAL_ORDERED = 0x0c;
542 |
543 | // These are used in _mm_cmpXstri() to specify the return.
544 | enum _SIDD_LEAST_SIGNIFICANT = 0x00;
545 | enum _SIDD_MOST_SIGNIFICANT = 0x40;
546 |
547 | // These macros are used in _mm_cmpXstri() to specify the return.
548 | enum _SIDD_BIT_MASK = 0x00;
549 | enum _SIDD_UNIT_MASK = 0x40;
550 |
551 | // some definition aliases to commonly used names
552 | alias __m128i = int4;
553 |
554 | // some used methods aliases
555 | alias _expect = llvm_expect;
556 | alias _mm_loadu_si128 = loadUnaligned!__m128i;
557 | alias _mm_cmpestri = __builtin_ia32_pcmpestri128;
558 | }
559 | else
560 | {
561 | enum LDC_with_SSE42 = false;
562 |
563 | T _expect(T)(T val, T expected_val) if (__traits(isIntegral, T))
564 | {
565 | pragma(inline, true);
566 | return val;
567 | }
568 | }
569 |
570 | pragma(msg, "SSE: ", LDC_with_SSE42);
571 |
--------------------------------------------------------------------------------
/source/archttp/HttpMessageParserTest.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpMessageParserTest;
13 |
14 | import archttp.HttpMessageParser;
15 |
16 | ///
17 | @("example")
18 | unittest
19 | {
20 | auto reqHandler = new HttpRequestParserHandler;
21 | auto reqParser = new HttpMessageParser(reqHandler);
22 |
23 | auto resHandler = new HttpRequestParserHandler;
24 | auto resParser = new HttpMessageParser(resHandler);
25 |
26 | // parse request
27 | string data = "GET /foo HTTP/1.1\r\nHost: 127.0.0.1:8090\r\n\r\n";
28 | // returns parsed message header length when parsed sucessfully, -ParserError on error
29 | int res = reqParser.parseRequest(data);
30 | assert(res == data.length);
31 | assert(reqHandler.method == "GET");
32 | assert(reqHandler.uri == "/foo");
33 | assert(reqHandler.minorVer == 1); // HTTP/1.1
34 | assert(reqHandler.headers.length == 1);
35 | assert(reqHandler.headers[0].name == "Host");
36 | assert(reqHandler.headers[0].value == "127.0.0.1:8090");
37 |
38 | // parse response
39 | data = "HTTP/1.0 200 OK\r\n";
40 | uint lastPos; // store last parsed position for next run
41 | res = resParser.parseResponse(data, lastPos);
42 | assert(res == -ParserError.partial); // no complete message header yet
43 | data = "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\n\r\nfoo";
44 | res = resParser.parseResponse(data, lastPos); // starts parsing from previous position
45 | assert(res == data.length - 3); // whole message header parsed, body left to be handled based on actual header values
46 | assert(resHandler.minorVer == 0); // HTTP/1.0
47 | assert(resHandler.status == 200);
48 | assert(resHandler.statusMsg == "OK");
49 | assert(resHandler.headers.length == 2);
50 | assert(resHandler.headers[0].name == "Content-Type");
51 | assert(resHandler.headers[0].value == "text/plain");
52 | assert(resHandler.headers[1].name == "Content-Length");
53 | assert(resHandler.headers[1].value == "3");
54 | }
55 |
56 | @("parseHttpVersion")
57 | unittest
58 | {
59 | assert(parseHttpVersion("FOO") < 0);
60 | assert(parseHttpVersion("HTTP/1.") < 0);
61 | assert(parseHttpVersion("HTTP/1.12") < 0);
62 | assert(parseHttpVersion("HTTP/1.a") < 0);
63 | assert(parseHttpVersion("HTTP/2.0") < 0);
64 | assert(parseHttpVersion("HTTP/1.00") < 0);
65 | assert(parseHttpVersion("HTTP/1.0") == 0);
66 | assert(parseHttpVersion("HTTP/1.1") == 1);
67 | }
68 |
69 | version (CI_MAIN)
70 | {
71 | // workaround for dub not supporting unittests with betterC
72 | version (D_BetterC)
73 | {
74 | extern(C) void main() @trusted {
75 | import core.stdc.stdio;
76 | static foreach(u; __traits(getUnitTests, httparsed))
77 | {
78 | static if (__traits(getAttributes, u).length)
79 | printf("unittest %s:%d | '" ~ __traits(getAttributes, u)[0] ~ "'\n", __traits(getLocation, u)[0].ptr, __traits(getLocation, u)[1]);
80 | else
81 | printf("unittest %s:%d\n", __traits(getLocation, u)[0].ptr, __traits(getLocation, u)[1]);
82 | u();
83 | }
84 | debug printf("All unit tests have been run successfully.\n");
85 | }
86 | }
87 | else
88 | {
89 | void main()
90 | {
91 | version (unittest) {} // run automagically
92 | else
93 | {
94 | import core.stdc.stdio;
95 |
96 | // just a compilation test
97 | auto reqParser = initParser();
98 | auto resParser = initParser();
99 |
100 | string data = "GET /foo HTTP/1.1\r\nHost: 127.0.0.1:8090\r\n\r\n";
101 | int res = reqHandler.parseRequest(data);
102 | assert(res == data.length);
103 |
104 | data = "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\n\r\nfoo";
105 | res = resHandler.parseResponse(data);
106 | assert(res == data.length - 3);
107 | () @trusted { printf("Test app works\n"); }();
108 | }
109 | }
110 | }
111 | }
112 |
113 |
114 | /// Builds valid char map from the provided ranges of invalid ones
115 | bool[256] buildValidCharMap()(string invalidRanges)
116 | {
117 | assert(invalidRanges.length % 2 == 0, "Uneven ranges");
118 | bool[256] res = true;
119 |
120 | for (int i=0; i < invalidRanges.length; i+=2)
121 | for (int j=invalidRanges[i]; j <= invalidRanges[i+1]; ++j)
122 | res[j] = false;
123 | return res;
124 | }
125 |
126 | @("buildValidCharMap")
127 | unittest
128 | {
129 | string ranges = "\0 \"\"(),,//:@[]{{}}\x7f\xff";
130 | assert(buildValidCharMap(ranges) ==
131 | cast(bool[])[
132 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
133 | 0,1,0,1,1,1,1,1,0,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,
134 | 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,
135 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,
136 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
137 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
138 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
139 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
140 | ]);
141 | }
142 |
143 | version (unittest) version = WITH_MSG;
144 | else version (CI_MAIN) version = WITH_MSG;
145 |
146 | version (WITH_MSG)
147 | {
148 | // define our message content handler
149 | struct Header
150 | {
151 | const(char)[] name;
152 | const(char)[] value;
153 | }
154 |
155 | // Just store slices of parsed message header
156 | class HttpRequestParserHandler : HttpMessageHandler
157 | {
158 | @safe pure nothrow @nogc:
159 | void onMethod(const(char)[] method) { this.method = method; }
160 | void onUri(const(char)[] uri) { this.uri = uri; }
161 | int onVersion(const(char)[] ver)
162 | {
163 | minorVer = parseHttpVersion(ver);
164 | return minorVer >= 0 ? 0 : minorVer;
165 | }
166 | void onHeader(const(char)[] name, const(char)[] value) {
167 | this.m_headers[m_headersLength].name = name;
168 | this.m_headers[m_headersLength++].value = value;
169 | }
170 | void onStatus(int status) { this.status = status; }
171 | void onStatusMsg(const(char)[] statusMsg) { this.statusMsg = statusMsg; }
172 |
173 | const(char)[] method;
174 | const(char)[] uri;
175 | int minorVer;
176 | int status;
177 | const(char)[] statusMsg;
178 |
179 | private {
180 | Header[32] m_headers;
181 | size_t m_headersLength;
182 | }
183 |
184 | Header[] headers() return { return m_headers[0..m_headersLength]; }
185 | }
186 |
187 | enum Test { err, complete, partial }
188 | }
189 |
190 | // Tests from https://github.com/h2o/picohttpparser/blob/master/test.c
191 |
192 | @("Request")
193 | unittest
194 | {
195 | auto parse(string data, Test test = Test.complete, int additional = 0)
196 | {
197 | auto parser = new HttpMessageParser(new HttpRequestParserHandler);
198 | auto res = parser.parseRequest(data);
199 | // if (res < 0) writeln("Err: ", cast(ParserError)(-res));
200 | final switch (test)
201 | {
202 | case Test.err: assert(res < -ParserError.partial); break;
203 | case Test.partial: assert(res == -ParserError.partial); break;
204 | case Test.complete: assert(res == data.length - additional); break;
205 | }
206 |
207 | return cast(HttpRequestParserHandler) parser.messageHandler();
208 | }
209 |
210 | // simple
211 | {
212 | auto req = parse("GET / HTTP/1.0\r\n\r\n");
213 | assert(req.headers.length == 0);
214 | assert(req.method == "GET");
215 | assert(req.uri == "/");
216 | assert(req.minorVer == 0);
217 | }
218 |
219 | // parse headers
220 | {
221 | auto req = parse("GET /hoge HTTP/1.1\r\nHost: example.com\r\nCookie: \r\n\r\n");
222 | assert(req.method == "GET");
223 | assert(req.uri == "/hoge");
224 | assert(req.minorVer == 1);
225 | assert(req.headers.length == 2);
226 | assert(req.headers[0] == Header("Host", "example.com"));
227 | assert(req.headers[1] == Header("Cookie", ""));
228 | }
229 |
230 | // multibyte included
231 | {
232 | auto req = parse("GET /hoge HTTP/1.1\r\nHost: example.com\r\nUser-Agent: \343\201\262\343/1.0\r\n\r\n");
233 | assert(req.method == "GET");
234 | assert(req.uri == "/hoge");
235 | assert(req.minorVer == 1);
236 | assert(req.headers.length == 2);
237 | assert(req.headers[0] == Header("Host", "example.com"));
238 | assert(req.headers[1] == Header("User-Agent", "\343\201\262\343/1.0"));
239 | }
240 |
241 | //multiline
242 | {
243 | auto req = parse("GET / HTTP/1.0\r\nfoo: \r\nfoo: b\r\n \tc\r\n\r\n");
244 | assert(req.method == "GET");
245 | assert(req.uri == "/");
246 | assert(req.minorVer == 0);
247 | assert(req.headers.length == 3);
248 | assert(req.headers[0] == Header("foo", ""));
249 | assert(req.headers[1] == Header("foo", "b"));
250 | assert(req.headers[2] == Header(null, " \tc"));
251 | }
252 |
253 | // header name with trailing space
254 | parse("GET / HTTP/1.0\r\nfoo : ab\r\n\r\n", Test.err);
255 |
256 | // incomplete
257 | assert(parse("\r", Test.partial).method == null);
258 | assert(parse("\r\n", Test.partial).method == null);
259 | assert(parse("\r\nGET", Test.partial).method == null);
260 | assert(parse("GET", Test.partial).method == null);
261 | assert(parse("GET ", Test.partial).method == "GET");
262 | assert(parse("GET /", Test.partial).uri == null);
263 | assert(parse("GET / ", Test.partial).uri == "/");
264 | assert(parse("GET / HTTP/1.1", Test.partial).minorVer == 0);
265 | assert(parse("GET / HTTP/1.1\r", Test.partial).minorVer == 1);
266 | assert(parse("GET / HTTP/1.1\r\n", Test.partial).minorVer == 1);
267 | parse("GET / HTTP/1.0\r\n\r", Test.partial);
268 | parse("GET / HTTP/1.0\r\n\r\n", Test.complete);
269 | parse(" / HTTP/1.0\r\n\r\n", Test.err); // empty method
270 | parse("GET HTTP/1.0\r\n\r\n", Test.err); // empty request target
271 | parse("GET / \r\n\r\n", Test.err); // empty version
272 | parse("GET / HTTP/1.0\r\n:a\r\n\r\n", Test.err); // empty header name
273 | parse("GET / HTTP/1.0\r\n :a\r\n\r\n", Test.err); // empty header name (space only)
274 | parse("G\0T / HTTP/1.0\r\n\r\n", Test.err); // NUL in method
275 | parse("G\tT / HTTP/1.0\r\n\r\n", Test.err); // tab in method
276 | parse("GET /\x7f HTTP/1.0\r\n\r\n", Test.err); // DEL in uri
277 | parse("GET / HTTP/1.0\r\na\0b: c\r\n\r\n", Test.err); // NUL in header name
278 | parse("GET / HTTP/1.0\r\nab: c\0d\r\n\r\n", Test.err); // NUL in header value
279 | parse("GET / HTTP/1.0\r\na\033b: c\r\n\r\n", Test.err); // CTL in header name
280 | parse("GET / HTTP/1.0\r\nab: c\033\r\n\r\n", Test.err); // CTL in header value
281 | parse("GET / HTTP/1.0\r\n/: 1\r\n\r\n", Test.err); // invalid char in header value
282 | parse("GET / HTTP/1.0\r\n\r\n", Test.complete); // multiple spaces between tokens
283 |
284 | // accept MSB chars
285 | {
286 | auto res = parse("GET /\xa0 HTTP/1.0\r\nh: c\xa2y\r\n\r\n");
287 | assert(res.method == "GET");
288 | assert(res.uri == "/\xa0");
289 | assert(res.minorVer == 0);
290 | assert(res.headers.length == 1);
291 | assert(res.headers[0] == Header("h", "c\xa2y"));
292 | }
293 |
294 | parse("GET / HTTP/1.0\r\n\x7b: 1\r\n\r\n", Test.err); // disallow '{'
295 |
296 | // exclude leading and trailing spaces in header value
297 | {
298 | auto req = parse("GET / HTTP/1.0\r\nfoo: a \t \r\n\r\n");
299 | assert(req.headers[0].value == "a");
300 | }
301 |
302 | // leave the body intact
303 | parse("GET / HTTP/1.0\r\n\r\nfoo bar baz", Test.complete, "foo bar baz".length);
304 |
305 | // realworld
306 | {
307 | auto req = parse("GET /cookies HTTP/1.1\r\nHost: 127.0.0.1:8090\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\nCookie: name=wookie\r\n\r\n");
308 | assert(req.method == "GET");
309 | assert(req.uri == "/cookies");
310 | assert(req.minorVer == 1);
311 | assert(req.headers[0] == Header("Host", "127.0.0.1:8090"));
312 | assert(req.headers[1] == Header("Connection", "keep-alive"));
313 | assert(req.headers[2] == Header("Cache-Control", "max-age=0"));
314 | assert(req.headers[3] == Header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"));
315 | assert(req.headers[4] == Header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17"));
316 | assert(req.headers[5] == Header("Accept-Encoding", "gzip,deflate,sdch"));
317 | assert(req.headers[6] == Header("Accept-Language", "en-US,en;q=0.8"));
318 | assert(req.headers[7] == Header("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.3"));
319 | assert(req.headers[8] == Header("Cookie", "name=wookie"));
320 | }
321 |
322 | // newline
323 | {
324 | auto req = parse("GET / HTTP/1.0\nfoo: a\n\n");
325 | }
326 | }
327 |
328 | @("Response")
329 | // Tests from https://github.com/h2o/picohttpparser/blob/master/test.c
330 | unittest
331 | {
332 | auto parse(string data, Test test = Test.complete, int additional = 0)
333 | {
334 | auto handler = new HttpRequestParserHandler;
335 | auto parser = new HttpMessageParser(handler);
336 |
337 | auto res = parser.parseResponse(data);
338 | // if (res < 0) writeln("Err: ", cast(ParserError)(-res));
339 | final switch (test)
340 | {
341 | case Test.err: assert(res < -ParserError.partial); break;
342 | case Test.partial: assert(res == -ParserError.partial); break;
343 | case Test.complete: assert(res == data.length - additional); break;
344 | }
345 |
346 | return handler;
347 | }
348 |
349 | // simple
350 | {
351 | auto res = parse("HTTP/1.0 200 OK\r\n\r\n");
352 | assert(res.headers.length == 0);
353 | assert(res.status == 200);
354 | assert(res.minorVer == 0);
355 | assert(res.statusMsg == "OK");
356 | }
357 |
358 | parse("HTTP/1.0 200 OK\r\n\r", Test.partial); // partial
359 |
360 | // parse headers
361 | {
362 | auto res = parse("HTTP/1.1 200 OK\r\nHost: example.com\r\nCookie: \r\n\r\n");
363 | assert(res.headers.length == 2);
364 | assert(res.minorVer == 1);
365 | assert(res.status == 200);
366 | assert(res.statusMsg == "OK");
367 | assert(res.headers[0] == Header("Host", "example.com"));
368 | assert(res.headers[1] == Header("Cookie", ""));
369 | }
370 |
371 | // parse multiline
372 | {
373 | auto res = parse("HTTP/1.0 200 OK\r\nfoo: \r\nfoo: b\r\n \tc\r\n\r\n");
374 | assert(res.headers.length == 3);
375 | assert(res.minorVer == 0);
376 | assert(res.status == 200);
377 | assert(res.statusMsg == "OK");
378 | assert(res.headers[0] == Header("foo", ""));
379 | assert(res.headers[1] == Header("foo", "b"));
380 | assert(res.headers[2] == Header(null, " \tc"));
381 | }
382 |
383 | // internal server error
384 | {
385 | auto res = parse("HTTP/1.0 500 Internal Server Error\r\n\r\n");
386 | assert(res.headers.length == 0);
387 | assert(res.minorVer == 0);
388 | assert(res.status == 500);
389 | assert(res.statusMsg == "Internal Server Error");
390 | }
391 |
392 | parse("H", Test.partial); // incomplete 1
393 | parse("HTTP/1.", Test.partial); // incomplete 2
394 | assert(parse("HTTP/1.1", Test.partial).minorVer == 0); // incomplete 3 - differs from picohttpparser as we don't parse exact version
395 | assert(parse("HTTP/1.1 ", Test.partial).minorVer == 1); // incomplete 4
396 | parse("HTTP/1.1 2", Test.partial); // incomplete 5
397 | assert(parse("HTTP/1.1 200", Test.partial).status == 0); // incomplete 6
398 | assert(parse("HTTP/1.1 200 ", Test.partial).status == 200); // incomplete 7
399 | assert(parse("HTTP/1.1 200\r", Test.partial).status == 200); // incomplete 7.1
400 | parse("HTTP/1.1 200 O", Test.partial); // incomplete 8
401 | assert(parse("HTTP/1.1 200 OK\r", Test.partial).statusMsg == "OK"); // incomplete 9 - differs from picohttpparser
402 | assert(parse("HTTP/1.1 200 OK\r\n", Test.partial).statusMsg == "OK"); // incomplete 10
403 | assert(parse("HTTP/1.1 200 OK\n", Test.partial).statusMsg == "OK"); // incomplete 11
404 | assert(parse("HTTP/1.1 200 OK\r\nA: 1\r", Test.partial).headers.length == 0); // incomplete 11
405 | parse("HTTP/1.1 200 OK\r\n\r\n", Test.complete); // multiple spaces between tokens
406 |
407 | // incomplete 12
408 | {
409 | auto res = parse("HTTP/1.1 200 OK\r\nA: 1\r\n", Test.partial);
410 | assert(res.headers.length == 1);
411 | assert(res.headers[0] == Header("A", "1"));
412 | }
413 |
414 | // slowloris (incomplete)
415 | {
416 | auto parser = new HttpMessageParser(new HttpRequestParserHandler);
417 | assert(parser.parseResponse("HTTP/1.0 200 OK\r\n") == -ParserError.partial);
418 | assert(parser.parseResponse("HTTP/1.0 200 OK\r\n\r") == -ParserError.partial);
419 | assert(parser.parseResponse("HTTP/1.0 200 OK\r\n\r\nblabla") == "HTTP/1.0 200 OK\r\n\r\n".length);
420 | }
421 |
422 | parse("HTTP/1. 200 OK\r\n\r\n", Test.err); // invalid http version
423 | parse("HTTP/1.2z 200 OK\r\n\r\n", Test.err); // invalid http version 2
424 | parse("HTTP/1.1 OK\r\n\r\n", Test.err); // no status code
425 |
426 | assert(parse("HTTP/1.1 200\r\n\r\n").statusMsg == ""); // accept missing trailing whitespace in status-line
427 | parse("HTTP/1.1 200X\r\n\r\n", Test.err); // garbage after status 1
428 | parse("HTTP/1.1 200X \r\n\r\n", Test.err); // garbage after status 2
429 | parse("HTTP/1.1 200X OK\r\n\r\n", Test.err); // garbage after status 3
430 |
431 | assert(parse("HTTP/1.1 200 OK\r\nbar: \t b\t \t\r\n\r\n").headers[0].value == "b"); // exclude leading and trailing spaces in header value
432 | }
433 |
434 | @("Incremental")
435 | unittest
436 | {
437 | string req = "GET /cookies HTTP/1.1\r\nHost: 127.0.0.1:8090\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\nCookie: name=wookie\r\n\r\n";
438 | auto handler = new HttpRequestParserHandler;
439 | auto parser = new HttpMessageParser(handler);
440 | uint parsed;
441 | auto res = parser.parseRequest(req[0.."GET /cookies HTTP/1.1\r\nHost: 127.0.0.1:8090\r\nConn".length], parsed);
442 | assert(res == -ParserError.partial);
443 | assert(handler.method == "GET");
444 | assert(handler.uri == "/cookies");
445 | assert(handler.minorVer == 1);
446 | assert(handler.headers.length == 1);
447 | assert(handler.headers[0] == Header("Host", "127.0.0.1:8090"));
448 |
449 | res = parser.parseRequest(req, parsed);
450 | assert(res == req.length);
451 | assert(handler.method == "GET");
452 | assert(handler.uri == "/cookies");
453 | assert(handler.minorVer == 1);
454 | assert(handler.headers[0] == Header("Host", "127.0.0.1:8090"));
455 | assert(handler.headers[1] == Header("Connection", "keep-alive"));
456 | assert(handler.headers[2] == Header("Cache-Control", "max-age=0"));
457 | assert(handler.headers[3] == Header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"));
458 | assert(handler.headers[4] == Header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17"));
459 | assert(handler.headers[5] == Header("Accept-Encoding", "gzip,deflate,sdch"));
460 | assert(handler.headers[6] == Header("Accept-Language", "en-US,en;q=0.8"));
461 | assert(handler.headers[7] == Header("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.3"));
462 | assert(handler.headers[8] == Header("Cookie", "name=wookie"));
463 | }
464 |
--------------------------------------------------------------------------------
/source/archttp/HttpMethod.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpMethod;
13 |
14 | /*
15 | * HTTP method enum
16 | */
17 | enum HttpMethod : ushort {
18 | GET,
19 | POST,
20 | HEAD,
21 | PUT,
22 | DELETE,
23 | OPTIONS,
24 | TRACE,
25 | CONNECT,
26 | BREW,
27 | PATCH
28 | }
29 |
30 | HttpMethod getHttpMethodFromString(string method)
31 | {
32 | switch (method)
33 | {
34 | case "GET":
35 | return HttpMethod.GET;
36 | case "POST":
37 | return HttpMethod.POST;
38 | case "HEAD":
39 | return HttpMethod.HEAD;
40 | case "PUT":
41 | return HttpMethod.PUT;
42 | case "DELETE":
43 | return HttpMethod.DELETE;
44 | case "OPTIONS":
45 | return HttpMethod.OPTIONS;
46 | case "TRACE":
47 | return HttpMethod.TRACE;
48 | case "CONNECT":
49 | return HttpMethod.CONNECT;
50 | case "BREW":
51 | return HttpMethod.BREW;
52 | case "PATCH":
53 | return HttpMethod.PATCH;
54 | default:
55 | return HttpMethod.GET; // show error?
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/source/archttp/HttpRequest.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpRequest;
13 |
14 | import geario.logging;
15 |
16 | import archttp.Url;
17 | import archttp.HttpContext;
18 | import archttp.HttpHeader;
19 | import archttp.HttpRequestHandler;
20 |
21 | public import archttp.HttpMethod;
22 | public import archttp.MultiPart;
23 |
24 | import std.uni : toLower;
25 |
26 | class HttpRequest
27 | {
28 | private
29 | {
30 | HttpMethod _method;
31 | Url _uri;
32 | string _path;
33 | string _httpVersion = "HTTP/1.1";
34 | string[string] _headers;
35 | string _body;
36 |
37 | HttpContext _httpContext;
38 | string[string] _cookies;
39 | bool _cookiesParsed;
40 | }
41 |
42 | public
43 | {
44 | // string[string] query;
45 | string[string] params;
46 | string[string] fields;
47 | MultiPart[] files;
48 | HttpRequestMiddlewareHandler[] middlewareHandlers;
49 | }
50 |
51 | public:
52 |
53 | ~ this()
54 | {
55 | reset();
56 | }
57 |
58 | HttpRequest context(HttpContext context)
59 | {
60 | _httpContext = context;
61 | return this;
62 | }
63 |
64 | void method(HttpMethod method)
65 | {
66 | _method = method;
67 | }
68 |
69 | void uri(Url uri)
70 | {
71 | _uri = uri;
72 | _path = _uri.path;
73 | }
74 |
75 | void path(string path)
76 | {
77 | _path = path;
78 | }
79 |
80 | string ip()
81 | {
82 | return _httpContext.connection().RemoteAddress().toAddrString();
83 | }
84 |
85 | string[string] cookies()
86 | {
87 | parseCookieWhenNeeded();
88 |
89 | return _cookies;
90 | }
91 |
92 | string cookie(T = string)(string name)
93 | {
94 | import std.conv : to;
95 |
96 | parseCookieWhenNeeded();
97 |
98 | return _cookies.get(name, "").to!T;
99 | }
100 |
101 | void parseCookieWhenNeeded()
102 | {
103 | if (_cookiesParsed)
104 | return;
105 |
106 | _cookiesParsed = true;
107 |
108 | string cookieString = header(HttpHeader.COOKIE);
109 |
110 | if (!cookieString.length)
111 | return;
112 |
113 | import std.array : split;
114 | import std.uri : decodeComponent;
115 | import std.string : strip;
116 |
117 | foreach (part; cookieString.split(";"))
118 | {
119 | auto c = part.split("=");
120 | _cookies[decodeComponent(c[0].strip)] = decodeComponent(c[1]);
121 | }
122 | }
123 |
124 | HttpRequest httpVersion(string httpVersion)
125 | {
126 | _httpVersion = httpVersion;
127 | return this;
128 | }
129 |
130 | HttpRequest header(string header, string value)
131 | {
132 | _headers[header.toLower] = value;
133 | return this;
134 | }
135 |
136 | HttpRequest body(string body)
137 | {
138 | _body = body;
139 | return this;
140 | }
141 |
142 | Url uri()
143 | {
144 | return _uri;
145 | }
146 |
147 | string path()
148 | {
149 | return _path;
150 | }
151 |
152 | HttpMethod method()
153 | {
154 | return _method;
155 | }
156 |
157 | string httpVersion()
158 | {
159 | return _httpVersion;
160 | }
161 |
162 | string header(string name)
163 | {
164 | return _headers.get(name.toLower, "");
165 | }
166 |
167 | string[string] headers()
168 | {
169 | return _headers;
170 | }
171 |
172 | string body()
173 | {
174 | return _body;
175 | }
176 |
177 | string query(string key)
178 | {
179 | return _uri.queryParams[key].front;
180 | }
181 |
182 | string param(string key)
183 | {
184 | return params.get(key, "");
185 | }
186 |
187 | void reset()
188 | {
189 | _headers = null;
190 | _body = null;
191 | _httpVersion = null;
192 | _cookies = null;
193 | _cookiesParsed = false;
194 |
195 | // query = null;
196 | params = null;
197 | fields = null;
198 | files = null;
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/source/archttp/HttpRequestHandler.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpRequestHandler;
13 |
14 | public import archttp.HttpContext;
15 | public import archttp.HttpResponse;
16 | public import archttp.HttpRequest;
17 |
18 | alias void delegate(HttpRequest, HttpResponse) HttpRequestHandler;
19 | alias void delegate(HttpRequest, HttpResponse, void delegate()) HttpRequestMiddlewareHandler;
20 |
--------------------------------------------------------------------------------
/source/archttp/HttpRequestParser.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpRequestParser;
13 |
14 | import archttp.HttpRequest;
15 | import archttp.HttpMessageParser;
16 | import archttp.MultiPart;
17 | import archttp.HttpRequestParserHandler;
18 |
19 | import std.conv : to;
20 | import std.array : split;
21 | import std.string : indexOf, stripRight;
22 | import std.algorithm : startsWith;
23 | import std.regex : regex, matchAll;
24 | import std.file : write, isDir, isFile, mkdir, mkdirRecurse, FileException;
25 |
26 | import std.stdio : writeln;
27 |
28 | enum ParserStatus : ushort {
29 | READY = 1,
30 | PARTIAL,
31 | COMPLETED,
32 | FAILED
33 | }
34 |
35 | class HttpRequestParser
36 | {
37 | private
38 | {
39 | string _data;
40 |
41 | long _parsedLength = 0;
42 | ParserStatus _parserStatus;
43 |
44 | bool _headerParsed = false;
45 |
46 | HttpRequestParserHandler _headerHandler;
47 | HttpMessageParser _headerParser;
48 |
49 | HttpRequest _request;
50 |
51 | string _contentType;
52 | long _contentLength = 0;
53 |
54 | string _fileUploadTempPath = "./tmp";
55 | }
56 |
57 | this()
58 | {
59 | _headerHandler = new HttpRequestParserHandler;
60 | _headerParser = new HttpMessageParser(_headerHandler);
61 |
62 | _parserStatus = ParserStatus.READY;
63 | }
64 |
65 | ParserStatus parserStatus()
66 | {
67 | return _parserStatus;
68 | }
69 |
70 | ulong parse(string data)
71 | {
72 | _data = data;
73 |
74 | if (_headerParsed == false && !parseHeader())
75 | {
76 | return 0;
77 | }
78 |
79 | // var for paring content
80 | _contentType = _request.header("Content-Type");
81 |
82 | string contentLengthString = _request.header("Content-Length");
83 | if (contentLengthString.length > 0)
84 | _contentLength = contentLengthString.to!long;
85 |
86 | if (_contentLength > 0)
87 | {
88 | if (!parseBody())
89 | {
90 | return 0;
91 | }
92 | }
93 |
94 | _parserStatus = ParserStatus.COMPLETED;
95 |
96 | return _parsedLength;
97 | }
98 |
99 | private bool parseHeader()
100 | {
101 | auto result = _headerParser.parseRequest(_data);
102 | if (result < 0)
103 | {
104 | if (result == -1)
105 | {
106 | _parserStatus = ParserStatus.PARTIAL;
107 | return false;
108 | }
109 |
110 | _parserStatus = ParserStatus.FAILED;
111 | return false;
112 | }
113 |
114 | _request = _headerHandler.request();
115 | // _headerHandler.reset();
116 | _parsedLength = result;
117 | _headerParsed = true;
118 |
119 | return true;
120 | }
121 |
122 | private bool parseBody()
123 | {
124 | if (_data.length < _parsedLength + _contentLength)
125 | {
126 | _parserStatus = ParserStatus.PARTIAL;
127 | return false;
128 | }
129 |
130 | if (_contentType.startsWith("application/json") || _contentType.startsWith("text/"))
131 | {
132 | _request.body(_data[_parsedLength.._parsedLength + _contentLength]);
133 | _parsedLength += _contentLength;
134 | return true;
135 | }
136 |
137 | if (_contentType.startsWith("application/x-www-form-urlencoded"))
138 | {
139 | if (!parseFormFields())
140 | return false;
141 |
142 | return true;
143 | }
144 |
145 | if (_contentType.startsWith("multipart/form-data"))
146 | {
147 | if (!parseMultipart())
148 | return false;
149 | }
150 |
151 | return true;
152 | }
153 |
154 | private bool parseFormFields()
155 | {
156 | foreach (fieldStr; _data[_parsedLength.._parsedLength + _contentLength].split("&"))
157 | {
158 | auto s = fieldStr.indexOf("=");
159 | if (s > 0)
160 | _request.fields[fieldStr[0..s]] = fieldStr[s+1..fieldStr.length];
161 | }
162 |
163 | _parsedLength += _contentLength;
164 |
165 | return true;
166 | }
167 |
168 | private bool parseMultipart()
169 | {
170 | string boundary = "--" ~ getBoundary();
171 |
172 | while (true)
173 | {
174 | bool isFile = false;
175 |
176 | long boundaryIndex = _data[_parsedLength .. $].indexOf(boundary);
177 | if (boundaryIndex == -1)
178 | break;
179 |
180 | boundaryIndex += _parsedLength;
181 |
182 | long boundaryEndIndex = boundaryIndex + boundary.length + 2; // boundary length + "--" length + "\r\n" length
183 | if (boundaryEndIndex + 2 == _data.length && _data[boundaryIndex .. boundaryEndIndex] == boundary ~ "--")
184 | {
185 | writeln("parse done");
186 | _parserStatus = ParserStatus.COMPLETED;
187 | _parsedLength = boundaryIndex + boundary.length + 2 + 2;
188 | break;
189 | }
190 |
191 | long ignoreBoundaryIndex = boundaryIndex + boundary.length + 2;
192 |
193 | long nextBoundaryIndex = _data[ignoreBoundaryIndex .. $].indexOf(boundary);
194 |
195 | if (nextBoundaryIndex == -1)
196 | {
197 | // not last boundary? parse error?
198 | writeln("not last boundary? parse error?");
199 | break;
200 | }
201 |
202 | nextBoundaryIndex += ignoreBoundaryIndex;
203 |
204 | long contentIndex = _data[ignoreBoundaryIndex .. nextBoundaryIndex].indexOf("\r\n\r\n");
205 | if (contentIndex == -1)
206 | {
207 | break;
208 | }
209 | contentIndex += ignoreBoundaryIndex + 4;
210 |
211 | string headerData = _data[ignoreBoundaryIndex .. contentIndex-4];
212 | MultiPart part;
213 |
214 | foreach (headerContent ; headerData.split("\r\n"))
215 | {
216 | long i = headerContent.indexOf(":");
217 | string headerKey = headerContent[0..i];
218 | string headerValue = headerContent[i..headerContent.length];
219 | if (headerKey != "Content-Disposition")
220 | {
221 | part.headers[headerKey] = headerValue;
222 |
223 | continue;
224 | }
225 |
226 | // for part.name
227 | string nameValuePrefix = "name=\"";
228 | long nameIndex = headerValue.indexOf(nameValuePrefix);
229 | if (nameIndex == -1)
230 | continue;
231 |
232 | long nameValueIndex = nameIndex + nameValuePrefix.length;
233 | long nameEndIndex = nameValueIndex + headerValue[nameValueIndex..$].indexOf("\"");
234 | part.name = headerValue[nameValueIndex..nameEndIndex];
235 |
236 | // for part.filename
237 | string filenameValuePrefix = "filename=\"";
238 | long filenameIndex = headerValue.indexOf(filenameValuePrefix);
239 | if (filenameIndex >= 0)
240 | {
241 | isFile = true;
242 |
243 | long filenameValueIndex = filenameIndex + filenameValuePrefix.length;
244 | long filenameEndIndex = filenameValueIndex + headerValue[filenameValueIndex..$].indexOf("\"");
245 | part.filename = headerValue[filenameValueIndex..filenameEndIndex];
246 | }
247 | }
248 |
249 | long contentSize = nextBoundaryIndex-2-contentIndex;
250 | if (isFile)
251 | {
252 | if (!part.filename.length == 0)
253 | {
254 | part.filesize = contentSize;
255 | // TODO: FreeBSD / macOS / Linux or Windows?
256 | string dirSeparator = "/";
257 | version (Windows)
258 | {
259 | dirSeparator = "\\";
260 | }
261 |
262 | if (!isDir(_fileUploadTempPath))
263 | {
264 | try
265 | {
266 | mkdir(_fileUploadTempPath);
267 | }
268 | catch (FileException e)
269 | {
270 | writeln("mkdir error: ", e);
271 | // throw e;
272 | }
273 | }
274 |
275 | string filepath = stripRight(_fileUploadTempPath, dirSeparator) ~ dirSeparator ~ part.filename;
276 | part.filepath = filepath;
277 | try
278 | {
279 | write(filepath, _data[contentIndex..nextBoundaryIndex-2]);
280 | }
281 | catch (FileException e)
282 | {
283 | writeln("file write error: ", e);
284 | }
285 |
286 | _request.files ~= part;
287 | }
288 | }
289 | else
290 | {
291 | _request.fields[part.name] = _data[contentIndex..nextBoundaryIndex-2];
292 | }
293 |
294 | _parsedLength = nextBoundaryIndex;
295 | }
296 |
297 | return true;
298 | }
299 |
300 | private string getBoundary()
301 | {
302 | string searchString = "boundary=";
303 | long index = _contentType.indexOf(searchString);
304 | if (index == -1)
305 | return "";
306 |
307 | return _contentType[index+searchString.length.._contentType.length];
308 | }
309 |
310 | HttpRequest request()
311 | {
312 | return _request;
313 | }
314 |
315 | void reset()
316 | {
317 | _contentLength = 0;
318 | _headerParsed = false;
319 | _parserStatus = ParserStatus.READY;
320 | _data = "";
321 | _contentType = "";
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/source/archttp/HttpRequestParserHandler.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpRequestParserHandler;
13 |
14 | import archttp.HttpRequest;
15 | import archttp.HttpMessageParser;
16 |
17 | import std.stdio : writeln;
18 | import std.conv : to;
19 |
20 | import archttp.Url;
21 |
22 | class HttpRequestParserHandler : HttpMessageHandler
23 | {
24 | this(HttpRequest request = null)
25 | {
26 | this._request = request;
27 | if (_request is null)
28 | _request = new HttpRequest;
29 | }
30 |
31 | void onMethod(const(char)[] method)
32 | {
33 | _request.method(getHttpMethodFromString(method.to!string));
34 | }
35 |
36 | void onUri(const(char)[] uri)
37 | {
38 | _request.uri(Url(uri.to!string));
39 | }
40 |
41 | int onVersion(const(char)[] ver)
42 | {
43 | auto minorVer = parseHttpVersion(ver);
44 | return minorVer >= 0 ? 0 : minorVer;
45 | }
46 |
47 | void onHeader(const(char)[] name, const(char)[] value)
48 | {
49 | _request.header(name.to!string, value.to!string);
50 | }
51 |
52 | void onStatus(int status) {
53 | // Request 不处理
54 | }
55 |
56 | void onStatusMsg(const(char)[] statusMsg) {
57 | // Request 不处理
58 | }
59 |
60 | HttpRequest request()
61 | {
62 | return _request;
63 | }
64 |
65 | // void reset()
66 | // {
67 | // _request.reset();
68 | // }
69 |
70 | private HttpRequest _request;
71 | }
72 |
--------------------------------------------------------------------------------
/source/archttp/HttpResponse.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpResponse;
13 |
14 | import archttp.HttpStatusCode;
15 | import archttp.HttpContext;
16 | import archttp.Cookie;
17 | import archttp.HttpHeader;
18 |
19 | import nbuff;
20 |
21 | import geario.util.DateTime;
22 | import geario.logging;
23 |
24 | import std.format;
25 | import std.array;
26 | import std.conv : to;
27 | import std.json;
28 |
29 |
30 | class HttpResponse
31 | {
32 | private
33 | {
34 | ushort _statusCode = HttpStatusCode.OK;
35 | string[string] _headers;
36 | string _body;
37 | ubyte[] _buffer;
38 | HttpContext _httpContext;
39 | Cookie[string] _cookies;
40 |
41 | // for ..
42 | bool _headersSent = false;
43 | }
44 |
45 | public:
46 | /*
47 | * Construct an empty response.
48 | */
49 | this(HttpContext ctx)
50 | {
51 | _httpContext = ctx;
52 | }
53 |
54 | bool headerSent()
55 | {
56 | return _headersSent;
57 | }
58 |
59 | HttpResponse header(string header, string value)
60 | {
61 | _headers[header] = value;
62 |
63 | return this;
64 | }
65 |
66 | HttpResponse code(HttpStatusCode statusCode)
67 | {
68 | _statusCode = statusCode;
69 |
70 | return this;
71 | }
72 |
73 | ushort code()
74 | {
75 | return _statusCode;
76 | }
77 |
78 | HttpResponse cookie(string name, string value, string path = "/", string domain = "", string expires = "", long maxAge = 604800, bool secure = false, bool httpOnly = false)
79 | {
80 | _cookies[name] = new Cookie(name, value, path, domain, expires, maxAge, secure, httpOnly);
81 | return this;
82 | }
83 |
84 | HttpResponse cookie(Cookie cookie)
85 | {
86 | _cookies[cookie.name()] = cookie;
87 | return this;
88 | }
89 |
90 | Cookie cookie(string name)
91 | {
92 | return _cookies.get(name, null);
93 | }
94 |
95 | void send(string body)
96 | {
97 | _body = body;
98 |
99 | header("Content-Type", "text/plain");
100 | send();
101 | }
102 |
103 | void send(JSONValue json)
104 | {
105 | _body = json.toString();
106 |
107 | header("Content-Type", "application/json");
108 | send();
109 | }
110 |
111 | void send()
112 | {
113 | if (_headersSent)
114 | {
115 | log.error("Can't set headers after they are sent");
116 | return;
117 | }
118 |
119 | if (sendHeader())
120 | sendBody();
121 | }
122 |
123 | HttpResponse json(JSONValue json)
124 | {
125 | _body = json.toString();
126 |
127 | header("Content-Type", "application/json");
128 |
129 | return this;
130 | }
131 |
132 | HttpResponse location(HttpStatusCode statusCode, string path)
133 | {
134 | code(statusCode);
135 | location(path);
136 |
137 | return this;
138 | }
139 |
140 | HttpResponse location(string path)
141 | {
142 | redirect(HttpStatusCode.SEE_OTHER, path);
143 | header("Location", path);
144 | return this;
145 | }
146 |
147 | HttpResponse redirect(HttpStatusCode statusCode, string path)
148 | {
149 | location(statusCode, path);
150 | return this;
151 | }
152 |
153 | HttpResponse redirect(string path)
154 | {
155 | redirect(HttpStatusCode.FOUND, path);
156 | return this;
157 | }
158 |
159 | HttpResponse sendFile(string path, string filename = "")
160 | {
161 | import std.stdio : File;
162 |
163 | auto file = File(path, "r");
164 | auto fileSize = file.size();
165 |
166 | if (filename.length == 0)
167 | {
168 | import std.array : split;
169 | import std.string : replace;
170 |
171 | auto parts = path.replace("\\", "/").split("/");
172 | if (parts.length == 1)
173 | {
174 | filename = path;
175 | }
176 | else
177 | {
178 | filename = parts[parts.length - 1];
179 | }
180 | }
181 |
182 | header(HttpHeader.CONTENT_DISPOSITION, "attachment; filename=" ~ filename ~ "; size=" ~ fileSize.to!string);
183 | header(HttpHeader.CONTENT_LENGTH, fileSize.to!string);
184 |
185 | _httpContext.Write(headerToString());
186 | _headersSent = true;
187 |
188 | auto buf = Nbuff.get(fileSize);
189 | file.rawRead(buf.data());
190 |
191 | _httpContext.Write(NbuffChunk(buf, fileSize));
192 | reset();
193 |
194 | return this;
195 | }
196 |
197 | void end()
198 | {
199 | reset();
200 | _httpContext.End();
201 | }
202 |
203 | private bool sendHeader()
204 | {
205 | if (_headersSent)
206 | {
207 | log.error("Can't set headers after they are sent");
208 | return false;
209 | }
210 |
211 | // if (_httpContext.keepAlive() && this.header(HttpHeader.CONTENT_LENGTH).length == 0)
212 | header(HttpHeader.CONTENT_LENGTH, _body.length.to!string);
213 |
214 | _httpContext.Write(headerToString());
215 | _headersSent = true;
216 |
217 | return true;
218 | }
219 |
220 | private void sendBody()
221 | {
222 | _httpContext.Write(_body);
223 | reset();
224 | }
225 |
226 | void reset()
227 | {
228 | // clear request object
229 | _httpContext.request().reset();
230 |
231 | _statusCode = HttpStatusCode.OK;
232 | _headers = null;
233 | _body = null;
234 | _buffer = null;
235 | _cookies = null;
236 | _headersSent = false;
237 | }
238 |
239 | string headerToString()
240 | {
241 | header(HttpHeader.SERVER, "Archttp");
242 | header(HttpHeader.DATE, DateTime.GetTimeAsGMT());
243 |
244 | auto text = appender!string;
245 | text ~= format!"HTTP/1.1 %d %s\r\n"(_statusCode, getHttpStatusMessage(_statusCode));
246 | foreach (name, value; _headers) {
247 | text ~= format!"%s: %s\r\n"(name, value);
248 | }
249 |
250 | if (_cookies.length)
251 | {
252 | foreach (cookie ; _cookies)
253 | {
254 | text ~= format!"Set-Cookie: %s\r\n"(cookie.toString());
255 | }
256 | }
257 |
258 | text ~= "\r\n";
259 |
260 | return text[];
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/source/archttp/HttpStatusCode.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.HttpStatusCode;
13 |
14 | enum HttpStatusCode : ushort {
15 | // 1XX - Informational
16 | CONTINUE = 100,
17 | SWITCHING_PROTOCOLS = 101,
18 | PROCESSING = 102,
19 |
20 | // 2XX - Success
21 | OK = 200,
22 | CREATED = 201,
23 | ACCEPTED = 202,
24 | NON_AUTHORITIVE_INFO = 203,
25 | NO_CONTENT = 204,
26 | RESET_CONTENT = 205,
27 | PARTIAL_CONTENT = 206,
28 | MULTI_STATUS = 207,
29 | ALREADY_REPORTED = 208,
30 | IM_USED = 226,
31 |
32 | // 3XX - Redirectional
33 | MULTI_CHOICES = 300,
34 | MOVED_PERMANENTLY = 301,
35 | FOUND = 302,
36 | SEE_OTHER = 303,
37 | NOT_MODIFIED = 304,
38 | USE_PROXY = 305,
39 | SWITCH_PROXY = 306,
40 | TEMP_REDIRECT = 307,
41 | PERM_REDIRECT = 308,
42 |
43 | // 4XX - Client error
44 | BAD_REQUEST = 400,
45 | UNAUTHORIZED = 401,
46 | PAYMENT_REQUIRED = 402,
47 | FORBIDDEN = 403,
48 | NOT_FOUND = 404,
49 | METHOD_NOT_ALLOWED = 405,
50 | NOT_ACCEPTABLE = 406,
51 | PROXY_AUTH_REQUIRED = 407,
52 | REQUEST_TIMEOUT = 408,
53 | CONFLICT = 409,
54 | GONE = 410,
55 | LENGTH_REQUIRED = 411,
56 | PRECONDITION_FAILED = 412,
57 | REQ_ENTITY_TOO_LARGE = 413,
58 | REQ_URI_TOO_LONG = 414,
59 | UNSUPPORTED_MEDIA_TYPE = 415,
60 | REQ_RANGE_NOT_SATISFYABLE = 416,
61 | EXPECTATION_FAILED = 417,
62 | IM_A_TEAPOT = 418,
63 | AUTH_TIMEOUT = 419, // not in RFC 2616
64 | UNPROCESSABLE_ENTITY = 422,
65 | LOCKED = 423,
66 | FAILED_DEPENDENCY = 424,
67 | UPGRADE_REQUIRED = 426,
68 | PRECONDITION_REQUIRED = 428,
69 | TOO_MANY_REQUESTS = 429,
70 | REQ_HEADER_FIELDS_TOO_LARGE = 431,
71 |
72 | // 5XX - Server error
73 | INTERNAL_SERVER_ERROR = 500,
74 | NOT_IMPLEMENTED = 501,
75 | BAD_GATEWAY = 502,
76 | SERVICE_UNAVAILABLE = 503,
77 | GATEWAY_TIMEOUT = 504,
78 | HTTP_VERSION_NOT_SUPPORTED = 505,
79 | VARIANT_ALSO_NEGOTIATES = 506,
80 | INSUFFICIENT_STORAGE = 507,
81 | LOOP_DETECTED = 508,
82 | NOT_EXTENDED = 510,
83 | NETWORK_AUTH_REQUIRED = 511,
84 | NETWORK_READ_TIMEOUT_ERR = 598,
85 | NETWORK_CONNECT_TIMEOUT_ERR = 599,
86 | }
87 |
88 | /*
89 | * Converts an HTTP status code into a known reason string.
90 | *
91 | * The reason string is a small line of text that gives a hint as to the underlying meaning of the
92 | * status code for debugging purposes.
93 | */
94 | string getHttpStatusMessage(ushort status_code)
95 | {
96 | switch ( status_code )
97 | {
98 | // 1XX - Informational
99 | case HttpStatusCode.CONTINUE:
100 | return "CONTINUE";
101 | case HttpStatusCode.SWITCHING_PROTOCOLS:
102 | return "SWITCHING PROTOCOLS";
103 | case HttpStatusCode.PROCESSING:
104 | return "PROCESSING";
105 | // 2XX - Success
106 | case HttpStatusCode.OK:
107 | return "OK";
108 | case HttpStatusCode.CREATED:
109 | return "CREATED";
110 | case HttpStatusCode.ACCEPTED:
111 | return "ACCEPTED";
112 | case HttpStatusCode.NON_AUTHORITIVE_INFO:
113 | return "NON AUTHORITIVE INFO";
114 | case HttpStatusCode.NO_CONTENT:
115 | return "NO CONTENT";
116 | case HttpStatusCode.RESET_CONTENT:
117 | return "RESET CONTENT";
118 | case HttpStatusCode.PARTIAL_CONTENT:
119 | return "PARTIAL CONTENT";
120 | case HttpStatusCode.MULTI_STATUS:
121 | return "MULTI STATUS";
122 | case HttpStatusCode.ALREADY_REPORTED:
123 | return "ALREADY REPORTED";
124 | case HttpStatusCode.IM_USED:
125 | return "IM USED";
126 | // 3XX - Redirectional
127 | case HttpStatusCode.MULTI_CHOICES:
128 | return "MULTI CHOICES";
129 | case HttpStatusCode.MOVED_PERMANENTLY:
130 | return "MOVED PERMANENTLY";
131 | case HttpStatusCode.FOUND:
132 | return "FOUND";
133 | case HttpStatusCode.SEE_OTHER:
134 | return "SEE OTHER";
135 | case HttpStatusCode.NOT_MODIFIED:
136 | return "NOT MODIFIED";
137 | case HttpStatusCode.USE_PROXY:
138 | return "USE PROXY";
139 | case HttpStatusCode.SWITCH_PROXY:
140 | return "SWITCH PROXY";
141 | case HttpStatusCode.TEMP_REDIRECT:
142 | return "TEMP REDIRECT";
143 | case HttpStatusCode.PERM_REDIRECT:
144 | return "PERM REDIRECT";
145 | // 4XX - Client error
146 | case HttpStatusCode.BAD_REQUEST:
147 | return "BAD REQUEST";
148 | case HttpStatusCode.UNAUTHORIZED:
149 | return "UNAUTHORIZED";
150 | case HttpStatusCode.PAYMENT_REQUIRED:
151 | return "PAYMENT REQUIRED";
152 | case HttpStatusCode.FORBIDDEN:
153 | return "FORBIDDEN";
154 | case HttpStatusCode.NOT_FOUND:
155 | return "NOT FOUND";
156 | case HttpStatusCode.METHOD_NOT_ALLOWED:
157 | return "METHOD NOT ALLOWED";
158 | case HttpStatusCode.NOT_ACCEPTABLE:
159 | return "NOT ACCEPTABLE";
160 | case HttpStatusCode.PROXY_AUTH_REQUIRED:
161 | return "PROXY AUTH REQUIRED";
162 | case HttpStatusCode.REQUEST_TIMEOUT:
163 | return "REQUEST TIMEOUT";
164 | case HttpStatusCode.CONFLICT:
165 | return "CONFLICT";
166 | case HttpStatusCode.GONE:
167 | return "GONE";
168 | case HttpStatusCode.LENGTH_REQUIRED:
169 | return "LENGTH REQUIRED";
170 | case HttpStatusCode.PRECONDITION_FAILED:
171 | return "PRECONDITION FAILED";
172 | case HttpStatusCode.REQ_ENTITY_TOO_LARGE:
173 | return "REQ ENTITY TOO LARGE";
174 | case HttpStatusCode.REQ_URI_TOO_LONG:
175 | return "REQ URI TOO LONG";
176 | case HttpStatusCode.UNSUPPORTED_MEDIA_TYPE:
177 | return "UNSUPPORTED MEDIA TYPE";
178 | case HttpStatusCode.REQ_RANGE_NOT_SATISFYABLE:
179 | return "REQ RANGE NOT SATISFYABLE";
180 | case HttpStatusCode.EXPECTATION_FAILED:
181 | return "EXPECTATION FAILED";
182 | case HttpStatusCode.IM_A_TEAPOT:
183 | return "IM A TEAPOT";
184 | case HttpStatusCode.AUTH_TIMEOUT: // not in RFC 2616
185 | return "AUTH TIMEOUT";
186 | case HttpStatusCode.UNPROCESSABLE_ENTITY:
187 | return "UNPROCESSABLE ENTITY";
188 | case HttpStatusCode.LOCKED:
189 | return "LOCKED";
190 | case HttpStatusCode.FAILED_DEPENDENCY:
191 | return "FAILED DEPENDENCY";
192 | case HttpStatusCode.UPGRADE_REQUIRED:
193 | return "UPGRADE REQUIRED";
194 | case HttpStatusCode.PRECONDITION_REQUIRED:
195 | return "PRECONDITION REQUIRED";
196 | case HttpStatusCode.TOO_MANY_REQUESTS:
197 | return "TOO MANY REQUESTS";
198 | case HttpStatusCode.REQ_HEADER_FIELDS_TOO_LARGE:
199 | return "REQ HEADER FIELDS TOO LARGE";
200 | // 5XX - Server error
201 | case HttpStatusCode.INTERNAL_SERVER_ERROR:
202 | return "INTERNAL SERVER ERROR";
203 | case HttpStatusCode.NOT_IMPLEMENTED:
204 | return "NOT IMPLEMENTED";
205 | case HttpStatusCode.BAD_GATEWAY:
206 | return "BAD GATEWAY";
207 | case HttpStatusCode.SERVICE_UNAVAILABLE:
208 | return "SERVICE UNAVAILABLE";
209 | case HttpStatusCode.GATEWAY_TIMEOUT:
210 | return "GATEWAY TIMEOUT";
211 | case HttpStatusCode.HTTP_VERSION_NOT_SUPPORTED:
212 | return "HTTP VERSION NOT SUPPORTED";
213 | case HttpStatusCode.VARIANT_ALSO_NEGOTIATES:
214 | return "VARIANT ALSO NEGOTIATES";
215 | case HttpStatusCode.INSUFFICIENT_STORAGE:
216 | return "INSUFFICIENT STORAGE";
217 | case HttpStatusCode.LOOP_DETECTED:
218 | return "LOOP DETECTED";
219 | case HttpStatusCode.NOT_EXTENDED:
220 | return "NOT EXTENDED";
221 | case HttpStatusCode.NETWORK_AUTH_REQUIRED:
222 | return "NETWORK AUTH REQUIRED";
223 | case HttpStatusCode.NETWORK_READ_TIMEOUT_ERR:
224 | return "NETWORK READ TIMEOUT ERR";
225 | case HttpStatusCode.NETWORK_CONNECT_TIMEOUT_ERR:
226 | return "NETWORK CONNECT TIMEOUT ERR";
227 | default:
228 | return "- UNKNOW STATUS CODE";
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/source/archttp/MiddlewareExecutor.d:
--------------------------------------------------------------------------------
1 | module archttp.MiddlewareExecutor;
2 |
3 | import archttp.HttpRequestHandler;
4 | import archttp.HttpRequest;
5 | import archttp.HttpResponse;
6 |
7 | struct MiddlewareExecutor
8 | {
9 | HttpRequest request;
10 | HttpResponse response;
11 | HttpRequestMiddlewareHandler[] middlewareHandlers;
12 | uint currentMiddlewareIndex;
13 | HttpRequestMiddlewareHandler currentMiddlewareHandler;
14 |
15 | this(HttpRequest request, HttpResponse response, HttpRequestMiddlewareHandler[] handlers)
16 | {
17 | this.request = request;
18 | this.response = response;
19 | this.middlewareHandlers = handlers;
20 | }
21 |
22 | void execute()
23 | {
24 | if (middlewareHandlers.length == 0)
25 | return;
26 |
27 | currentMiddlewareIndex = 0;
28 | currentMiddlewareHandler = middlewareHandlers[0];
29 |
30 | currentMiddlewareHandler(request, response, &next);
31 | }
32 |
33 | void next()
34 | {
35 | currentMiddlewareIndex++;
36 | if (currentMiddlewareIndex == middlewareHandlers.length)
37 | return;
38 |
39 | currentMiddlewareHandler = middlewareHandlers[currentMiddlewareIndex];
40 | currentMiddlewareHandler(request, response, &next);
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/source/archttp/MultiPart.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.MultiPart;
13 |
14 | struct MultiPart
15 | {
16 | string[string] headers;
17 | string name;
18 | string value;
19 | string filename;
20 | long filesize = 0;
21 | string filepath;
22 | }
23 |
--------------------------------------------------------------------------------
/source/archttp/Route.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.Route;
13 |
14 | import archttp.HttpMethod;
15 |
16 | import std.stdio;
17 |
18 | class Route(RoutingHandler, MiddlewareHandler)
19 | {
20 | private
21 | {
22 | string _path;
23 |
24 | RoutingHandler[HttpMethod] _handlers;
25 | MiddlewareHandler[] _middlewareHandlers;
26 | }
27 |
28 | public
29 | {
30 | // like uri path
31 | string pattern;
32 |
33 | // use regex?
34 | bool regular;
35 |
36 | // Regex template
37 | string urlTemplate;
38 |
39 | string[uint] paramKeys;
40 | }
41 |
42 | this(string path, HttpMethod method, RoutingHandler handler)
43 | {
44 | _path = path;
45 |
46 | bindMethod(method, handler);
47 | }
48 |
49 | Route bindMethod(HttpMethod method, RoutingHandler handler)
50 | {
51 | _handlers[method] = handler;
52 | return this;
53 | }
54 |
55 | Route use(MiddlewareHandler handler)
56 | {
57 | _middlewareHandlers ~= handler;
58 | return this;
59 | }
60 |
61 | MiddlewareHandler[] middlewareHandlers()
62 | {
63 | return _middlewareHandlers;
64 | }
65 |
66 | RoutingHandler find(HttpMethod method)
67 | {
68 | auto handler = _handlers.get(method, null);
69 |
70 | return cast(RoutingHandler) handler;
71 | }
72 |
73 | string path()
74 | {
75 | return _path;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/source/archttp/Router.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Router - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.Router;
13 |
14 | public import archttp.Route;
15 | public import archttp.HttpMethod;
16 |
17 | import archttp.Archttp;
18 |
19 | import std.stdio;
20 |
21 | import std.regex : regex, match, matchAll;
22 | import std.array : replaceFirst;
23 | import std.uri : decode;
24 |
25 | class Router(RoutingHandler, MiddlewareHandler)
26 | {
27 | private
28 | {
29 | Route!(RoutingHandler, MiddlewareHandler)[string] _routes;
30 | Route!(RoutingHandler, MiddlewareHandler)[string] _regexRoutes;
31 | MiddlewareHandler[] _middlewareHandlers;
32 | Archttp _app;
33 | }
34 |
35 | Router add(string path, HttpMethod method, RoutingHandler handler)
36 | {
37 | auto route = _routes.get(path, null);
38 |
39 | if (route is null)
40 | {
41 | route = _regexRoutes.get(path, null);
42 | }
43 |
44 | if (route is null)
45 | {
46 | route = CreateRoute(path, method, handler);
47 |
48 | if (route.regular)
49 | {
50 | _regexRoutes[path] = route;
51 | }
52 | else
53 | {
54 | _routes[path] = route;
55 | }
56 | }
57 | else
58 | {
59 | route.bindMethod(method, handler);
60 | }
61 |
62 | return this;
63 | }
64 |
65 | Router get(string route, RoutingHandler handler)
66 | {
67 | add(route, HttpMethod.GET, handler);
68 | return this;
69 | }
70 |
71 | Router post(string route, RoutingHandler handler)
72 | {
73 | add(route, HttpMethod.POST, handler);
74 | return this;
75 | }
76 |
77 | Router put(string route, RoutingHandler handler)
78 | {
79 | add(route, HttpMethod.PUT, handler);
80 | return this;
81 | }
82 |
83 | Router Delete(string route, RoutingHandler handler)
84 | {
85 | add(route, HttpMethod.DELETE, handler);
86 | return this;
87 | }
88 |
89 | Router use(MiddlewareHandler handler)
90 | {
91 | _middlewareHandlers ~= handler;
92 | return this;
93 | }
94 |
95 | void onMount(Archttp app)
96 | {
97 | _app = app;
98 | }
99 |
100 | MiddlewareHandler[] middlewareHandlers()
101 | {
102 | return _middlewareHandlers;
103 | }
104 |
105 | private Route!(RoutingHandler, MiddlewareHandler) CreateRoute(string path, HttpMethod method, RoutingHandler handler)
106 | {
107 | auto route = new Route!(RoutingHandler, MiddlewareHandler)(path, method, handler);
108 |
109 | auto matches = path.matchAll(regex(`\{(\w+)(:([^\}]+))?\}`));
110 | if (matches)
111 | {
112 | string[uint] paramKeys;
113 | int paramCount = 0;
114 | string pattern = path;
115 | string urlTemplate = path;
116 |
117 | foreach (m; matches)
118 | {
119 | paramKeys[paramCount] = m[1];
120 | string reg = m[3].length ? m[3] : "\\w+";
121 | pattern = pattern.replaceFirst(m[0], "(" ~ reg ~ ")");
122 | urlTemplate = urlTemplate.replaceFirst(m[0], "{" ~ m[1] ~ "}");
123 | paramCount++;
124 | }
125 |
126 | route.pattern = pattern;
127 | route.paramKeys = paramKeys;
128 | route.regular = true;
129 | route.urlTemplate = urlTemplate;
130 | }
131 |
132 | return route;
133 | }
134 |
135 | RoutingHandler match(string path, HttpMethod method, ref MiddlewareHandler[] middlewareHandlers, ref string[string] params)
136 | {
137 | auto route = _routes.get(path, null);
138 |
139 | if (route is null)
140 | {
141 | foreach ( r ; _regexRoutes )
142 | {
143 | auto matched = path.match(regex(r.pattern));
144 |
145 | if (matched)
146 | {
147 | route = r;
148 |
149 | foreach ( i, key ; route.paramKeys )
150 | {
151 | params[key] = decode(matched.captures[i + 1]);
152 | }
153 | }
154 | }
155 | }
156 |
157 | if (route is null)
158 | {
159 | writeln(path, " is Not Found.");
160 | return cast(RoutingHandler) null;
161 | }
162 |
163 | RoutingHandler handler;
164 | handler = route.find(method);
165 |
166 | if (handler is null)
167 | {
168 | writeln("Request: ", path, " method ", method, " is Not Allowed.");
169 | return cast(RoutingHandler) null;
170 | }
171 |
172 | middlewareHandlers = route.middlewareHandlers();
173 |
174 | return handler;
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/source/archttp/Url.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.Url;
13 |
14 | // THanks dhasenan, Copy from https://github.com/dhasenan/urld
15 |
16 | import std.conv;
17 | import std.string;
18 |
19 | pure:
20 | @safe:
21 |
22 | /// An exception thrown when something bad happens with Urls.
23 | class UrlException : Exception
24 | {
25 | this(string msg) pure { super(msg); }
26 | }
27 |
28 | /**
29 | * A mapping from schemes to their default ports.
30 | *
31 | * This is not exhaustive. Not all schemes use ports. Not all schemes uniquely identify a port to
32 | * use even if they use ports. Entries here should be treated as best guesses.
33 | */
34 | enum ushort[string] schemeToDefaultPort = [
35 | "aaa": 3868,
36 | "aaas": 5658,
37 | "acap": 674,
38 | "amqp": 5672,
39 | "cap": 1026,
40 | "coap": 5683,
41 | "coaps": 5684,
42 | "dav": 443,
43 | "dict": 2628,
44 | "ftp": 21,
45 | "git": 9418,
46 | "go": 1096,
47 | "gopher": 70,
48 | "http": 80,
49 | "https": 443,
50 | "ws": 80,
51 | "wss": 443,
52 | "iac": 4569,
53 | "icap": 1344,
54 | "imap": 143,
55 | "ipp": 631,
56 | "ipps": 631, // yes, they're both mapped to port 631
57 | "irc": 6667, // De facto default port, not the IANA reserved port.
58 | "ircs": 6697,
59 | "iris": 702, // defaults to iris.beep
60 | "iris.beep": 702,
61 | "iris.lwz": 715,
62 | "iris.xpc": 713,
63 | "iris.xpcs": 714,
64 | "jabber": 5222, // client-to-server
65 | "ldap": 389,
66 | "ldaps": 636,
67 | "msrp": 2855,
68 | "msrps": 2855,
69 | "mtqp": 1038,
70 | "mupdate": 3905,
71 | "news": 119,
72 | "nfs": 2049,
73 | "pop": 110,
74 | "redis": 6379,
75 | "reload": 6084,
76 | "rsync": 873,
77 | "rtmfp": 1935,
78 | "rtsp": 554,
79 | "shttp": 80,
80 | "sieve": 4190,
81 | "sip": 5060,
82 | "sips": 5061,
83 | "smb": 445,
84 | "smtp": 25,
85 | "snews": 563,
86 | "snmp": 161,
87 | "soap.beep": 605,
88 | "ssh": 22,
89 | "stun": 3478,
90 | "stuns": 5349,
91 | "svn": 3690,
92 | "teamspeak": 9987,
93 | "telnet": 23,
94 | "tftp": 69,
95 | "tip": 3372,
96 | ];
97 |
98 | /**
99 | * A collection of query parameters.
100 | *
101 | * This is effectively a multimap of string -> strings.
102 | */
103 | struct QueryParams
104 | {
105 | hash_t toHash() const nothrow @safe
106 | {
107 | return typeid(params).getHash(¶ms);
108 | }
109 |
110 | pure:
111 | import std.typecons;
112 | alias Tuple!(string, "key", string, "value") Param;
113 | Param[] params;
114 |
115 | @property size_t length() const {
116 | return params.length;
117 | }
118 |
119 | /// Get a range over the query parameter values for the given key.
120 | auto opIndex(string key) const
121 | {
122 | import std.algorithm.searching : find;
123 | import std.algorithm.iteration : map;
124 | return params.find!(x => x.key == key).map!(x => x.value);
125 | }
126 |
127 | /// Add a query parameter with the given key and value.
128 | /// If one already exists, there will now be two query parameters with the given name.
129 | void add(string key, string value) {
130 | params ~= Param(key, value);
131 | }
132 |
133 | /// Add a query parameter with the given key and value.
134 | /// If there are any existing parameters with the same key, they are removed and overwritten.
135 | void overwrite(string key, string value) {
136 | for (int i = 0; i < params.length; i++) {
137 | if (params[i].key == key) {
138 | params[i] = params[$-1];
139 | params.length--;
140 | }
141 | }
142 | params ~= Param(key, value);
143 | }
144 |
145 | private struct QueryParamRange
146 | {
147 | pure:
148 | size_t i;
149 | const(Param)[] params;
150 | bool empty() { return i >= params.length; }
151 | void popFront() { i++; }
152 | Param front() { return params[i]; }
153 | }
154 |
155 | /**
156 | * A range over the query parameters.
157 | *
158 | * Usage:
159 | * ---
160 | * foreach (key, value; url.queryParams) {}
161 | * ---
162 | */
163 | auto range() const
164 | {
165 | return QueryParamRange(0, this.params);
166 | }
167 | /// ditto
168 | alias range this;
169 |
170 | /// Convert this set of query parameters into a query string.
171 | string toString() const {
172 | import std.array : Appender;
173 | Appender!string s;
174 | bool first = true;
175 | foreach (tuple; this) {
176 | if (!first) {
177 | s ~= '&';
178 | }
179 | first = false;
180 | s ~= tuple.key.percentEncode;
181 | if (tuple.value.length > 0) {
182 | s ~= '=';
183 | s ~= tuple.value.percentEncode;
184 | }
185 | }
186 | return s.data;
187 | }
188 |
189 | /// Clone this set of query parameters.
190 | QueryParams dup()
191 | {
192 | QueryParams other = this;
193 | other.params = params.dup;
194 | return other;
195 | }
196 |
197 | int opCmp(const ref QueryParams other) const
198 | {
199 | for (int i = 0; i < params.length && i < other.params.length; i++)
200 | {
201 | auto c = cmp(params[i].key, other.params[i].key);
202 | if (c != 0) return c;
203 | c = cmp(params[i].value, other.params[i].value);
204 | if (c != 0) return c;
205 | }
206 | if (params.length > other.params.length) return 1;
207 | if (params.length < other.params.length) return -1;
208 | return 0;
209 | }
210 | }
211 |
212 | /**
213 | * A Unique Resource Locator.
214 | *
215 | * Urls can be parsed (see parseUrl) and implicitly convert to strings.
216 | */
217 | struct Url
218 | {
219 | private
220 | {
221 | bool _isValid = false;
222 | }
223 |
224 | hash_t toHash() const @safe nothrow
225 | {
226 | return asTuple().toHash();
227 | }
228 |
229 | this(string url)
230 | {
231 | if (this.parse(url))
232 | {
233 | _isValid = true;
234 | }
235 | else
236 | {
237 | throw new UrlException("failed to parse Url " ~ url);
238 | }
239 | }
240 |
241 | bool isValid()
242 | {
243 | return _isValid;
244 | }
245 |
246 | /**
247 | * Parse a Url from a string.
248 | *
249 | * This attempts to parse a wide range of Urls as people might actually type them. Some mistakes
250 | * may be made. However, any Url in a correct format will be parsed correctly.
251 | */
252 | private bool parse(string value)
253 | {
254 | // scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
255 | // Scheme is optional in common use. We infer 'http' if it's not given.
256 | auto i = value.indexOf("//");
257 | if (i > -1) {
258 | if (i > 1) {
259 | this.scheme = value[0..i-1];
260 | }
261 | value = value[i+2 .. $];
262 | } else {
263 | this.scheme = "http";
264 | }
265 | // Check for an ipv6 hostname.
266 | // [user:password@]host[:port]][/]path[?query][#fragment
267 | i = value.indexOfAny([':', '/', '[']);
268 | if (i == -1) {
269 | // Just a hostname.
270 | this.host = value.fromPuny;
271 | return true;
272 | }
273 |
274 | if (value[i] == ':') {
275 | // This could be between username and password, or it could be between host and port.
276 | auto j = value.indexOfAny(['@', '/']);
277 | if (j > -1 && value[j] == '@') {
278 | try {
279 | this.user = value[0..i].percentDecode;
280 | this.pass = value[i+1 .. j].percentDecode;
281 | } catch (UrlException) {
282 | return false;
283 | }
284 | value = value[j+1 .. $];
285 | }
286 | }
287 |
288 | // It's trying to be a host/port, not a user/pass.
289 | i = value.indexOfAny([':', '/', '[']);
290 | if (i == -1) {
291 | this.host = value.fromPuny;
292 | return true;
293 | }
294 |
295 | // Find the hostname. It's either an ipv6 address (which has special rules) or not (which doesn't
296 | // have special rules). -- The main sticking point is that ipv6 addresses have colons, which we
297 | // handle specially, and are offset with square brackets.
298 | if (value[i] == '[') {
299 | auto j = value[i..$].indexOf(']');
300 | if (j < 0) {
301 | // unterminated ipv6 addr
302 | return false;
303 | }
304 | // includes square brackets
305 | this.host = value[i .. i+j+1];
306 | value = value[i+j+1 .. $];
307 | if (value.length == 0) {
308 | // read to end of string; we finished parse
309 | return true;
310 | }
311 | if (value[0] != ':' && value[0] != '?' && value[0] != '/') {
312 | return false;
313 | }
314 | } else {
315 | // Normal host.
316 | this.host = value[0..i].fromPuny;
317 | value = value[i .. $];
318 | }
319 |
320 | if (value[0] == ':') {
321 | auto end = value.indexOf('/');
322 | if (end == -1) {
323 | end = value.length;
324 | }
325 | try {
326 | this.port = value[1 .. end].to!ushort;
327 | } catch (ConvException) {
328 | return false;
329 | }
330 | value = value[end .. $];
331 | if (value.length == 0) {
332 | return true;
333 | }
334 | }
335 |
336 | return parsePathAndQuery(value);
337 | }
338 |
339 | private bool parsePathAndQuery(string value)
340 | {
341 | auto i = value.indexOfAny("?#");
342 | if (i == -1)
343 | {
344 | this.path = value.percentDecode;
345 | return true;
346 | }
347 |
348 | try
349 | {
350 | this.path = value[0..i].percentDecode;
351 | }
352 | catch (UrlException)
353 | {
354 | return false;
355 | }
356 |
357 | auto c = value[i];
358 | value = value[i + 1 .. $];
359 | if (c == '?')
360 | {
361 | i = value.indexOf('#');
362 | string query;
363 | if (i < 0)
364 | {
365 | query = value;
366 | value = null;
367 | }
368 | else
369 | {
370 | query = value[0..i];
371 | value = value[i + 1 .. $];
372 | }
373 | auto queries = query.split('&');
374 | foreach (q; queries)
375 | {
376 | auto j = q.indexOf('=');
377 | string key, val;
378 | if (j < 0)
379 | {
380 | key = q;
381 | }
382 | else
383 | {
384 | key = q[0..j];
385 | val = q[j + 1 .. $];
386 | }
387 | try
388 | {
389 | key = key.percentDecode;
390 | val = val.percentDecode;
391 | }
392 | catch (UrlException)
393 | {
394 | return false;
395 | }
396 | this.queryParams.add(key, val);
397 | }
398 | }
399 |
400 | try
401 | {
402 | this.fragment = value.percentDecode;
403 | }
404 | catch (UrlException)
405 | {
406 | return false;
407 | }
408 |
409 | return true;
410 | }
411 |
412 | pure:
413 | /// The Url scheme. For instance, ssh, ftp, or https.
414 | string scheme;
415 |
416 | /// The username in this Url. Usually absent. If present, there will also be a password.
417 | string user;
418 |
419 | /// The password in this Url. Usually absent.
420 | string pass;
421 |
422 | /// The hostname.
423 | string host;
424 |
425 | /**
426 | * The port.
427 | *
428 | * This is inferred from the scheme if it isn't present in the Url itself.
429 | * If the scheme is not known and the port is not present, the port will be given as 0.
430 | * For some schemes, port will not be sensible -- for instance, file or chrome-extension.
431 | *
432 | * If you explicitly need to detect whether the user provided a port, check the providedPort
433 | * field.
434 | */
435 | @property ushort port() const nothrow
436 | {
437 | if (providedPort != 0) {
438 | return providedPort;
439 | }
440 | if (auto p = scheme in schemeToDefaultPort) {
441 | return *p;
442 | }
443 | return 0;
444 | }
445 |
446 | /**
447 | * Set the port.
448 | *
449 | * This sets the providedPort field and is provided for convenience.
450 | */
451 | @property ushort port(ushort value) nothrow
452 | {
453 | return providedPort = value;
454 | }
455 |
456 | /// The port that was explicitly provided in the Url.
457 | ushort providedPort;
458 |
459 | /**
460 | * The path.
461 | *
462 | * For instance, in the Url https://cnn.com/news/story/17774?visited=false, the path is
463 | * "/news/story/17774".
464 | */
465 | string path;
466 |
467 | /**
468 | * The query parameters associated with this Url.
469 | */
470 | QueryParams queryParams;
471 |
472 | /**
473 | * The fragment. In web documents, this typically refers to an anchor element.
474 | * For instance, in the Url https://cnn.com/news/story/17774#header2, the fragment is "header2".
475 | */
476 | string fragment;
477 |
478 | /**
479 | * Convert this Url to a string.
480 | * The string is properly formatted and usable for, eg, a web request.
481 | */
482 | string toString() const
483 | {
484 | return toString(false);
485 | }
486 |
487 | /**
488 | * Convert this Url to a string.
489 | *
490 | * The string is intended to be human-readable rather than machine-readable.
491 | */
492 | string toHumanReadableString() const
493 | {
494 | return toString(true);
495 | }
496 |
497 | ///
498 | unittest
499 | {
500 | auto url = "https://xn--m3h.xn--n3h.org/?hi=bye".parseUrl;
501 | assert(url.toString == "https://xn--m3h.xn--n3h.org/?hi=bye", url.toString);
502 | assert(url.toHumanReadableString == "https://☂.☃.org/?hi=bye", url.toString);
503 | }
504 |
505 | unittest
506 | {
507 | assert("http://example.org/some_path".parseUrl.toHumanReadableString ==
508 | "http://example.org/some_path");
509 | }
510 |
511 | /**
512 | * Convert the path and query string of this Url to a string.
513 | */
514 | string toPathAndQueryString() const
515 | {
516 | if (queryParams.length > 0)
517 | {
518 | return path ~ '?' ~ queryParams.toString;
519 | }
520 | return path;
521 | }
522 |
523 | ///
524 | unittest
525 | {
526 | auto u = "http://example.org/index?page=12".parseUrl;
527 | auto pathAndQuery = u.toPathAndQueryString();
528 | assert(pathAndQuery == "/index?page=12", pathAndQuery);
529 | }
530 |
531 | private string toString(bool humanReadable) const
532 | {
533 | import std.array : Appender;
534 | Appender!string s;
535 | s ~= scheme;
536 | s ~= "://";
537 | if (user) {
538 | s ~= humanReadable ? user : user.percentEncode;
539 | s ~= ":";
540 | s ~= humanReadable ? pass : pass.percentEncode;
541 | s ~= "@";
542 | }
543 | s ~= humanReadable ? host : host.toPuny;
544 | if (providedPort) {
545 | if ((scheme in schemeToDefaultPort) == null || schemeToDefaultPort[scheme] != providedPort) {
546 | s ~= ":";
547 | s ~= providedPort.to!string;
548 | }
549 | }
550 | string p = path;
551 | if (p.length == 0 || p == "/") {
552 | s ~= '/';
553 | } else {
554 | if (humanReadable) {
555 | s ~= p;
556 | } else {
557 | if (p[0] == '/') {
558 | p = p[1..$];
559 | }
560 | foreach (part; p.split('/')) {
561 | s ~= '/';
562 | s ~= part.percentEncode;
563 | }
564 | }
565 | }
566 | if (queryParams.length) {
567 | s ~= '?';
568 | s ~= queryParams.toString;
569 | } if (fragment) {
570 | s ~= '#';
571 | s ~= fragment.percentEncode;
572 | }
573 | return s.data;
574 | }
575 |
576 | /// Implicitly convert Urls to strings.
577 | alias toString this;
578 |
579 | /**
580 | Compare two Urls.
581 |
582 | I tried to make the comparison produce a sort order that seems natural, so it's not identical
583 | to sorting based on .toString(). For instance, username/password have lower priority than
584 | host. The scheme has higher priority than port but lower than host.
585 |
586 | While the output of this is guaranteed to provide a total ordering, and I've attempted to make
587 | it human-friendly, it isn't guaranteed to be consistent between versions. The implementation
588 | and its results can change without a minor version increase.
589 | */
590 | int opCmp(const Url other) const
591 | {
592 | return asTuple.opCmp(other.asTuple);
593 | }
594 |
595 | private auto asTuple() const nothrow
596 | {
597 | import std.typecons : tuple;
598 | return tuple(host, scheme, port, user, pass, path, queryParams);
599 | }
600 |
601 | /// Equality checks.
602 | // bool opEquals(string other) const
603 | // {
604 | // Url o = parseUrl(other);
605 | // if (!parseUrl(other))
606 | // {
607 | // return false;
608 | // }
609 |
610 | // return asTuple() == o.asTuple();
611 | // }
612 |
613 | /// Ditto
614 | bool opEquals(ref const Url other) const
615 | {
616 | return asTuple() == other.asTuple();
617 | }
618 |
619 | /// Ditto
620 | bool opEquals(const Url other) const
621 | {
622 | return asTuple() == other.asTuple();
623 | }
624 |
625 | unittest
626 | {
627 | import std.algorithm, std.array, std.format;
628 | assert("http://example.org/some_path".parseUrl > "http://example.org/other_path".parseUrl);
629 | alias sorted = std.algorithm.sort;
630 | auto parsedUrls =
631 | [
632 | "http://example.org/some_path",
633 | "http://example.org:81/other_path",
634 | "http://example.org/other_path",
635 | "https://example.org/first_path",
636 | "http://example.xyz/other_other_path",
637 | "http://me:secret@blog.ikeran.org/wp_admin",
638 | ].map!(x => x.parseUrl).array;
639 | auto urls = sorted(parsedUrls).map!(x => x.toHumanReadableString).array;
640 | auto expected =
641 | [
642 | "http://me:secret@blog.ikeran.org/wp_admin",
643 | "http://example.org/other_path",
644 | "http://example.org/some_path",
645 | "http://example.org:81/other_path",
646 | "https://example.org/first_path",
647 | "http://example.xyz/other_other_path",
648 | ];
649 | assert(cmp(urls, expected) == 0, "expected:\n%s\ngot:\n%s".format(expected, urls));
650 | }
651 |
652 | unittest
653 | {
654 | auto a = "http://x.org/a?b=c".parseUrl;
655 | auto b = "http://x.org/a?d=e".parseUrl;
656 | auto c = "http://x.org/a?b=a".parseUrl;
657 | assert(a < b);
658 | assert(c < b);
659 | assert(c < a);
660 | }
661 |
662 | /**
663 | * The append operator (~).
664 | *
665 | * The append operator for Urls returns a new Url with the given string appended as a path
666 | * element to the Url's path. It only adds new path elements (or sequences of path elements).
667 | *
668 | * Don't worry about path separators; whether you include them or not, it will just work.
669 | *
670 | * Query elements are copied.
671 | *
672 | * Examples:
673 | * ---
674 | * auto random = "http://testdata.org/random".parseUrl;
675 | * auto randInt = random ~ "int";
676 | * writeln(randInt); // prints "http://testdata.org/random/int"
677 | * ---
678 | */
679 | Url opBinary(string op : "~")(string subsequentPath) {
680 | Url other = this;
681 | other ~= subsequentPath;
682 | other.queryParams = queryParams.dup;
683 | return other;
684 | }
685 |
686 | /**
687 | * The append-in-place operator (~=).
688 | *
689 | * The append operator for Urls adds a path element to this Url. It only adds new path elements
690 | * (or sequences of path elements).
691 | *
692 | * Don't worry about path separators; whether you include them or not, it will just work.
693 | *
694 | * Examples:
695 | * ---
696 | * auto random = "http://testdata.org/random".parseUrl;
697 | * random ~= "int";
698 | * writeln(random); // prints "http://testdata.org/random/int"
699 | * ---
700 | */
701 | Url opOpAssign(string op : "~")(string subsequentPath) {
702 | if (path.endsWith("/")) {
703 | if (subsequentPath.startsWith("/")) {
704 | path ~= subsequentPath[1..$];
705 | } else {
706 | path ~= subsequentPath;
707 | }
708 | } else {
709 | if (!subsequentPath.startsWith("/")) {
710 | path ~= '/';
711 | }
712 | path ~= subsequentPath;
713 | }
714 | return this;
715 | }
716 |
717 | /**
718 | * Convert a relative Url to an absolute Url.
719 | *
720 | * This is designed so that you can scrape a webpage and quickly convert links within the
721 | * page to Urls you can actually work with, but you're clever; I'm sure you'll find more uses
722 | * for it.
723 | *
724 | * It's biased toward HTTP family Urls; as one quirk, "//" is interpreted as "same scheme,
725 | * different everything else", which might not be desirable for all schemes.
726 | *
727 | * This only handles Urls, not URIs; if you pass in 'mailto:bob.dobbs@subgenius.org', for
728 | * instance, this will give you our best attempt to parse it as a Url.
729 | *
730 | * Examples:
731 | * ---
732 | * auto base = "https://example.org/passworddb?secure=false".parseUrl;
733 | *
734 | * // Download https://example.org/passworddb/by-username/dhasenan
735 | * download(base.resolve("by-username/dhasenan"));
736 | *
737 | * // Download https://example.org/static/style.css
738 | * download(base.resolve("/static/style.css"));
739 | *
740 | * // Download https://cdn.example.net/jquery.js
741 | * download(base.resolve("https://cdn.example.net/jquery.js"));
742 | * ---
743 | */
744 | // Url resolve(string other)
745 | // {
746 | // if (other.length == 0) return this;
747 | // if (other[0] == '/')
748 | // {
749 | // if (other.length > 1 && other[1] == '/')
750 | // {
751 | // // Uncommon syntax: a link like "//wikimedia.org" means "same scheme, switch Url"
752 | // return parseUrl(this.scheme ~ ':' ~ other);
753 | // }
754 | // }
755 | // else
756 | // {
757 | // auto schemeSep = other.indexOf("://");
758 | // if (schemeSep >= 0 && schemeSep < other.indexOf("/"))
759 | // // separate Url
760 | // {
761 | // return other.parseUrl;
762 | // }
763 | // }
764 |
765 | // Url ret = this;
766 | // ret.path = "";
767 | // ret.queryParams = ret.queryParams.init;
768 | // if (other[0] != '/')
769 | // {
770 | // // relative to something
771 | // if (!this.path.length)
772 | // {
773 | // // nothing to be relative to
774 | // other = "/" ~ other;
775 | // }
776 | // else if (this.path[$-1] == '/')
777 | // {
778 | // // directory-style path for the current thing
779 | // // resolve relative to this directory
780 | // other = this.path ~ other;
781 | // }
782 | // else
783 | // {
784 | // // this is a file-like thing
785 | // // find the 'directory' and relative to that
786 | // other = this.path[0..this.path.lastIndexOf('/') + 1] ~ other;
787 | // }
788 | // }
789 | // // collapse /foo/../ to /
790 | // if (other.indexOf("/../") >= 0)
791 | // {
792 | // import std.array : Appender, array;
793 | // import std.string : split;
794 | // import std.algorithm.iteration : joiner, filter;
795 | // string[] parts = other.split('/');
796 | // for (int i = 0; i < parts.length; i++)
797 | // {
798 | // if (parts[i] == "..")
799 | // {
800 | // for (int j = i - 1; j >= 0; j--)
801 | // {
802 | // if (parts[j] != null)
803 | // {
804 | // parts[j] = null;
805 | // parts[i] = null;
806 | // break;
807 | // }
808 | // }
809 | // }
810 | // }
811 | // other = "/" ~ parts.filter!(x => x != null).joiner("/").to!string;
812 | // }
813 | // parsePathAndQuery(ret, other);
814 | // return ret;
815 | // }
816 |
817 | unittest
818 | {
819 | auto a = "http://alcyius.com/dndtools/index.html".parseUrl;
820 | auto b = a.resolve("contacts/index.html");
821 | assert(b.toString == "http://alcyius.com/dndtools/contacts/index.html");
822 | }
823 |
824 | unittest
825 | {
826 | auto a = "http://alcyius.com/dndtools/index.html?a=b".parseUrl;
827 | auto b = a.resolve("contacts/index.html?foo=bar");
828 | assert(b.toString == "http://alcyius.com/dndtools/contacts/index.html?foo=bar");
829 | }
830 |
831 | unittest
832 | {
833 | auto a = "http://alcyius.com/dndtools/index.html".parseUrl;
834 | auto b = a.resolve("../index.html");
835 | assert(b.toString == "http://alcyius.com/index.html", b.toString);
836 | }
837 |
838 | unittest
839 | {
840 | auto a = "http://alcyius.com/dndtools/foo/bar/index.html".parseUrl;
841 | auto b = a.resolve("../index.html");
842 | assert(b.toString == "http://alcyius.com/dndtools/foo/index.html", b.toString);
843 | }
844 | }
845 |
846 | unittest {
847 | {
848 | // Basic.
849 | Url url;
850 | with (url) {
851 | scheme = "https";
852 | host = "example.org";
853 | path = "/foo/bar";
854 | queryParams.add("hello", "world");
855 | queryParams.add("gibe", "clay");
856 | fragment = "frag";
857 | }
858 | assert(
859 | // Not sure what order it'll come out in.
860 | url.toString == "https://example.org/foo/bar?hello=world&gibe=clay#frag" ||
861 | url.toString == "https://example.org/foo/bar?gibe=clay&hello=world#frag",
862 | url.toString);
863 | }
864 | {
865 | // Percent encoded.
866 | Url url;
867 | with (url) {
868 | scheme = "https";
869 | host = "example.org";
870 | path = "/f☃o";
871 | queryParams.add("❄", "❀");
872 | queryParams.add("[", "]");
873 | fragment = "ş";
874 | }
875 | assert(
876 | // Not sure what order it'll come out in.
877 | url.toString == "https://example.org/f%E2%98%83o?%E2%9D%84=%E2%9D%80&%5B=%5D#%C5%9F" ||
878 | url.toString == "https://example.org/f%E2%98%83o?%5B=%5D&%E2%9D%84=%E2%9D%80#%C5%9F",
879 | url.toString);
880 | }
881 | {
882 | // Port, user, pass.
883 | Url url;
884 | with (url) {
885 | scheme = "https";
886 | host = "example.org";
887 | user = "dhasenan";
888 | pass = "itsasecret";
889 | port = 17;
890 | }
891 | assert(
892 | url.toString == "https://dhasenan:itsasecret@example.org:17/",
893 | url.toString);
894 | }
895 | {
896 | // Query with no path.
897 | Url url;
898 | with (url) {
899 | scheme = "https";
900 | host = "example.org";
901 | queryParams.add("hi", "bye");
902 | }
903 | assert(
904 | url.toString == "https://example.org/?hi=bye",
905 | url.toString);
906 | }
907 | }
908 |
909 | unittest
910 | {
911 | auto url = "//foo/bar".parseUrl;
912 | assert(url.host == "foo", "expected host foo, got " ~ url.host);
913 | assert(url.path == "/bar");
914 | }
915 |
916 | unittest
917 | {
918 | import std.stdio : writeln;
919 | auto url = "file:///foo/bar".parseUrl;
920 | assert(url.host == null);
921 | assert(url.port == 0);
922 | assert(url.scheme == "file");
923 | assert(url.path == "/foo/bar");
924 | assert(url.toString == "file:///foo/bar");
925 | assert(url.queryParams.empty);
926 | assert(url.fragment == null);
927 | }
928 |
929 | unittest
930 | {
931 | // ipv6 hostnames!
932 | {
933 | // full range of data
934 | auto url = parseUrl("https://bob:secret@[::1]:2771/foo/bar");
935 | assert(url.scheme == "https", url.scheme);
936 | assert(url.user == "bob", url.user);
937 | assert(url.pass == "secret", url.pass);
938 | assert(url.host == "[::1]", url.host);
939 | assert(url.port == 2771, url.port.to!string);
940 | assert(url.path == "/foo/bar", url.path);
941 | }
942 |
943 | // minimal
944 | {
945 | auto url = parseUrl("[::1]");
946 | assert(url.host == "[::1]", url.host);
947 | }
948 |
949 | // some random bits
950 | {
951 | auto url = parseUrl("http://[::1]/foo");
952 | assert(url.scheme == "http", url.scheme);
953 | assert(url.host == "[::1]", url.host);
954 | assert(url.path == "/foo", url.path);
955 | }
956 |
957 | {
958 | auto url = parseUrl("https://[2001:0db8:0:0:0:0:1428:57ab]/?login=true#justkidding");
959 | assert(url.scheme == "https");
960 | assert(url.host == "[2001:0db8:0:0:0:0:1428:57ab]");
961 | assert(url.path == "/");
962 | assert(url.fragment == "justkidding");
963 | }
964 | }
965 |
966 | unittest
967 | {
968 | auto url = "localhost:5984".parseUrl;
969 | auto url2 = url ~ "db1";
970 | assert(url2.toString == "http://localhost:5984/db1", url2.toString);
971 | auto url3 = url2 ~ "_all_docs";
972 | assert(url3.toString == "http://localhost:5984/db1/_all_docs", url3.toString);
973 | }
974 |
975 | ///
976 | unittest {
977 | {
978 | // Basic.
979 | Url url;
980 | with (url) {
981 | scheme = "https";
982 | host = "example.org";
983 | path = "/foo/bar";
984 | queryParams.add("hello", "world");
985 | queryParams.add("gibe", "clay");
986 | fragment = "frag";
987 | }
988 | assert(
989 | // Not sure what order it'll come out in.
990 | url.toString == "https://example.org/foo/bar?hello=world&gibe=clay#frag" ||
991 | url.toString == "https://example.org/foo/bar?gibe=clay&hello=world#frag",
992 | url.toString);
993 | }
994 | {
995 | // Passing an array of query values.
996 | Url url;
997 | with (url) {
998 | scheme = "https";
999 | host = "example.org";
1000 | path = "/foo/bar";
1001 | queryParams.add("hello", "world");
1002 | queryParams.add("hello", "aether");
1003 | fragment = "frag";
1004 | }
1005 | assert(
1006 | // Not sure what order it'll come out in.
1007 | url.toString == "https://example.org/foo/bar?hello=world&hello=aether#frag" ||
1008 | url.toString == "https://example.org/foo/bar?hello=aether&hello=world#frag",
1009 | url.toString);
1010 | }
1011 | {
1012 | // Percent encoded.
1013 | Url url;
1014 | with (url) {
1015 | scheme = "https";
1016 | host = "example.org";
1017 | path = "/f☃o";
1018 | queryParams.add("❄", "❀");
1019 | queryParams.add("[", "]");
1020 | fragment = "ş";
1021 | }
1022 | assert(
1023 | // Not sure what order it'll come out in.
1024 | url.toString == "https://example.org/f%E2%98%83o?%E2%9D%84=%E2%9D%80&%5B=%5D#%C5%9F" ||
1025 | url.toString == "https://example.org/f%E2%98%83o?%5B=%5D&%E2%9D%84=%E2%9D%80#%C5%9F",
1026 | url.toString);
1027 | }
1028 | {
1029 | // Port, user, pass.
1030 | Url url;
1031 | with (url) {
1032 | scheme = "https";
1033 | host = "example.org";
1034 | user = "dhasenan";
1035 | pass = "itsasecret";
1036 | port = 17;
1037 | }
1038 | assert(
1039 | url.toString == "https://dhasenan:itsasecret@example.org:17/",
1040 | url.toString);
1041 | }
1042 | {
1043 | // Query with no path.
1044 | Url url;
1045 | with (url) {
1046 | scheme = "https";
1047 | host = "example.org";
1048 | queryParams.add("hi", "bye");
1049 | }
1050 | assert(
1051 | url.toString == "https://example.org/?hi=bye",
1052 | url.toString);
1053 | }
1054 | }
1055 |
1056 | unittest {
1057 | // Percent decoding.
1058 |
1059 | // http://#:!:@
1060 | auto urlString = "http://%23:%21%3A@example.org/%7B/%7D?%3B&%26=%3D#%23hash%EF%BF%BD";
1061 | auto url = urlString.parseUrl;
1062 | assert(url.user == "#");
1063 | assert(url.pass == "!:");
1064 | assert(url.host == "example.org");
1065 | assert(url.path == "/{/}");
1066 | assert(url.queryParams[";"].front == "");
1067 | assert(url.queryParams["&"].front == "=");
1068 | assert(url.fragment == "#hash�");
1069 |
1070 | // Round trip.
1071 | assert(urlString == urlString.parseUrl.toString, urlString.parseUrl.toString);
1072 | assert(urlString == urlString.parseUrl.toString.parseUrl.toString);
1073 | }
1074 |
1075 | unittest {
1076 | auto url = "https://xn--m3h.xn--n3h.org/?hi=bye".parseUrl;
1077 | assert(url.host == "☂.☃.org", url.host);
1078 | }
1079 |
1080 | unittest {
1081 | auto url = "https://☂.☃.org/?hi=bye".parseUrl;
1082 | assert(url.toString == "https://xn--m3h.xn--n3h.org/?hi=bye");
1083 | }
1084 |
1085 | ///
1086 | unittest {
1087 | // There's an existing path.
1088 | auto url = parseUrl("http://example.org/foo");
1089 | Url url2;
1090 | // No slash? Assume it needs a slash.
1091 | assert((url ~ "bar").toString == "http://example.org/foo/bar");
1092 | // With slash? Don't add another.
1093 | url2 = url ~ "/bar";
1094 | assert(url2.toString == "http://example.org/foo/bar", url2.toString);
1095 | url ~= "bar";
1096 | assert(url.toString == "http://example.org/foo/bar");
1097 |
1098 | // Path already ends with a slash; don't add another.
1099 | url = parseUrl("http://example.org/foo/");
1100 | assert((url ~ "bar").toString == "http://example.org/foo/bar");
1101 | // Still don't add one even if you're appending with a slash.
1102 | assert((url ~ "/bar").toString == "http://example.org/foo/bar");
1103 | url ~= "/bar";
1104 | assert(url.toString == "http://example.org/foo/bar");
1105 |
1106 | // No path.
1107 | url = parseUrl("http://example.org");
1108 | assert((url ~ "bar").toString == "http://example.org/bar");
1109 | assert((url ~ "/bar").toString == "http://example.org/bar");
1110 | url ~= "bar";
1111 | assert(url.toString == "http://example.org/bar");
1112 |
1113 | // Path is just a slash.
1114 | url = parseUrl("http://example.org/");
1115 | assert((url ~ "bar").toString == "http://example.org/bar");
1116 | assert((url ~ "/bar").toString == "http://example.org/bar");
1117 | url ~= "bar";
1118 | assert(url.toString == "http://example.org/bar", url.toString);
1119 |
1120 | // No path, just fragment.
1121 | url = "ircs://irc.freenode.com/#d".parseUrl;
1122 | assert(url.toString == "ircs://irc.freenode.com/#d", url.toString);
1123 | }
1124 | unittest
1125 | {
1126 | // basic resolve()
1127 | {
1128 | auto base = "https://example.org/this/".parseUrl;
1129 | assert(base.resolve("that") == "https://example.org/this/that");
1130 | assert(base.resolve("/that") == "https://example.org/that");
1131 | assert(base.resolve("//example.net/that") == "https://example.net/that");
1132 | }
1133 |
1134 | // ensure we don't preserve query params
1135 | {
1136 | auto base = "https://example.org/this?query=value&other=value2".parseUrl;
1137 | assert(base.resolve("that") == "https://example.org/that");
1138 | assert(base.resolve("/that") == "https://example.org/that");
1139 | assert(base.resolve("tother/that") == "https://example.org/tother/that");
1140 | assert(base.resolve("//example.net/that") == "https://example.net/that");
1141 | }
1142 | }
1143 |
1144 |
1145 | unittest
1146 | {
1147 | import std.net.curl;
1148 | auto url = "http://example.org".parseUrl;
1149 | assert(is(typeof(std.net.curl.get(url))));
1150 | }
1151 |
1152 | /**
1153 | * Parse the input string as a Url.
1154 | *
1155 | * Throws:
1156 | * UrlException if the string was in an incorrect format.
1157 | */
1158 | // Url parseUrl(string value) {
1159 | // return Url(value);
1160 | // }
1161 |
1162 | ///
1163 | unittest {
1164 | {
1165 | // Infer scheme
1166 | auto u1 = parseUrl("example.org");
1167 | assert(u1.scheme == "http");
1168 | assert(u1.host == "example.org");
1169 | assert(u1.path == "");
1170 | assert(u1.port == 80);
1171 | assert(u1.providedPort == 0);
1172 | assert(u1.fragment == "");
1173 | }
1174 | {
1175 | // Simple host and scheme
1176 | auto u1 = parseUrl("https://example.org");
1177 | assert(u1.scheme == "https");
1178 | assert(u1.host == "example.org");
1179 | assert(u1.path == "");
1180 | assert(u1.port == 443);
1181 | assert(u1.providedPort == 0);
1182 | }
1183 | {
1184 | // With path
1185 | auto u1 = parseUrl("https://example.org/foo/bar");
1186 | assert(u1.scheme == "https");
1187 | assert(u1.host == "example.org");
1188 | assert(u1.path == "/foo/bar", "expected /foo/bar but got " ~ u1.path);
1189 | assert(u1.port == 443);
1190 | assert(u1.providedPort == 0);
1191 | }
1192 | {
1193 | // With explicit port
1194 | auto u1 = parseUrl("https://example.org:1021/foo/bar");
1195 | assert(u1.scheme == "https");
1196 | assert(u1.host == "example.org");
1197 | assert(u1.path == "/foo/bar", "expected /foo/bar but got " ~ u1.path);
1198 | assert(u1.port == 1021);
1199 | assert(u1.providedPort == 1021);
1200 | }
1201 | {
1202 | // With user
1203 | auto u1 = parseUrl("https://bob:secret@example.org/foo/bar");
1204 | assert(u1.scheme == "https");
1205 | assert(u1.host == "example.org");
1206 | assert(u1.path == "/foo/bar");
1207 | assert(u1.port == 443);
1208 | assert(u1.user == "bob");
1209 | assert(u1.pass == "secret");
1210 | }
1211 | {
1212 | // With user, Url-encoded
1213 | auto u1 = parseUrl("https://bob%21:secret%21%3F@example.org/foo/bar");
1214 | assert(u1.scheme == "https");
1215 | assert(u1.host == "example.org");
1216 | assert(u1.path == "/foo/bar");
1217 | assert(u1.port == 443);
1218 | assert(u1.user == "bob!");
1219 | assert(u1.pass == "secret!?");
1220 | }
1221 | {
1222 | // With user and port and path
1223 | auto u1 = parseUrl("https://bob:secret@example.org:2210/foo/bar");
1224 | assert(u1.scheme == "https");
1225 | assert(u1.host == "example.org");
1226 | assert(u1.path == "/foo/bar");
1227 | assert(u1.port == 2210);
1228 | assert(u1.user == "bob");
1229 | assert(u1.pass == "secret");
1230 | assert(u1.fragment == "");
1231 | }
1232 | {
1233 | // With query string
1234 | auto u1 = parseUrl("https://example.org/?login=true");
1235 | assert(u1.scheme == "https");
1236 | assert(u1.host == "example.org");
1237 | assert(u1.path == "/", "expected path: / actual path: " ~ u1.path);
1238 | assert(u1.queryParams["login"].front == "true");
1239 | assert(u1.fragment == "");
1240 | }
1241 | {
1242 | // With query string and fragment
1243 | auto u1 = parseUrl("https://example.org/?login=true#justkidding");
1244 | assert(u1.scheme == "https");
1245 | assert(u1.host == "example.org");
1246 | assert(u1.path == "/", "expected path: / actual path: " ~ u1.path);
1247 | assert(u1.queryParams["login"].front == "true");
1248 | assert(u1.fragment == "justkidding");
1249 | }
1250 | {
1251 | // With Url-encoded values
1252 | auto u1 = parseUrl("https://example.org/%E2%98%83?%E2%9D%84=%3D#%5E");
1253 | assert(u1.scheme == "https");
1254 | assert(u1.host == "example.org");
1255 | assert(u1.path == "/☃", "expected path: /☃ actual path: " ~ u1.path);
1256 | assert(u1.queryParams["❄"].front == "=");
1257 | assert(u1.fragment == "^");
1258 | }
1259 | }
1260 |
1261 | unittest {
1262 | assert(parseUrl("http://example.org").port == 80);
1263 | assert(parseUrl("http://example.org:5326").port == 5326);
1264 |
1265 | auto url = parseUrl("redis://admin:password@redisbox.local:2201/path?query=value#fragment");
1266 | assert(url.scheme == "redis");
1267 | assert(url.user == "admin");
1268 | assert(url.pass == "password");
1269 |
1270 | assert(parseUrl("example.org").toString == "http://example.org/");
1271 | assert(parseUrl("http://example.org:80").toString == "http://example.org/");
1272 |
1273 | assert(parseUrl("localhost:8070").toString == "http://localhost:8070/");
1274 | }
1275 |
1276 | /**
1277 | * Percent-encode a string.
1278 | *
1279 | * Url components cannot contain non-ASCII characters, and there are very few characters that are
1280 | * safe to include as Url components. Domain names using Unicode values use Punycode. For
1281 | * everything else, there is percent encoding.
1282 | */
1283 | string percentEncode(string raw) {
1284 | // We *must* encode these characters: :/?#[]@!$&'()*+,;="
1285 | // We *can* encode any other characters.
1286 | // We *should not* encode alpha, numeric, or -._~.
1287 | import std.utf : encode;
1288 | import std.array : Appender;
1289 | Appender!string app;
1290 | foreach (dchar d; raw) {
1291 | if (('a' <= d && 'z' >= d) ||
1292 | ('A' <= d && 'Z' >= d) ||
1293 | ('0' <= d && '9' >= d) ||
1294 | d == '-' || d == '.' || d == '_' || d == '~') {
1295 | app ~= d;
1296 | continue;
1297 | }
1298 | // Something simple like a space character? Still in 7-bit ASCII?
1299 | // Then we get a single-character string out of it and just encode
1300 | // that one bit.
1301 | // Something not in 7-bit ASCII? Then we percent-encode each octet
1302 | // in the UTF-8 encoding (and hope the server understands UTF-8).
1303 | char[] c;
1304 | encode(c, d);
1305 | auto bytes = cast(ubyte[])c;
1306 | foreach (b; bytes) {
1307 | app ~= format("%%%02X", b);
1308 | }
1309 | }
1310 | return cast(string)app.data;
1311 | }
1312 |
1313 | ///
1314 | unittest {
1315 | assert(percentEncode("IDontNeedNoPercentEncoding") == "IDontNeedNoPercentEncoding");
1316 | assert(percentEncode("~~--..__") == "~~--..__");
1317 | assert(percentEncode("0123456789") == "0123456789");
1318 |
1319 | string e;
1320 |
1321 | e = percentEncode("☃");
1322 | assert(e == "%E2%98%83", "expected %E2%98%83 but got" ~ e);
1323 | }
1324 |
1325 | /**
1326 | * Percent-decode a string.
1327 | *
1328 | * Url components cannot contain non-ASCII characters, and there are very few characters that are
1329 | * safe to include as Url components. Domain names using Unicode values use Punycode. For
1330 | * everything else, there is percent encoding.
1331 | *
1332 | * This explicitly ensures that the result is a valid UTF-8 string.
1333 | */
1334 | string percentDecode(string encoded)
1335 | {
1336 | import std.utf : validate, UTFException;
1337 | auto raw = percentDecodeRaw(encoded);
1338 | auto s = cast(string) raw;
1339 | try
1340 | {
1341 | validate(s);
1342 | }
1343 | catch (UTFException e)
1344 | {
1345 | throw new UrlException(
1346 | "The percent-encoded data `" ~ encoded ~ "` does not represent a valid UTF-8 sequence.");
1347 | }
1348 | return s;
1349 | }
1350 |
1351 | ///
1352 | unittest {
1353 | assert(percentDecode("IDontNeedNoPercentDecoding") == "IDontNeedNoPercentDecoding");
1354 | assert(percentDecode("~~--..__") == "~~--..__");
1355 | assert(percentDecode("0123456789") == "0123456789");
1356 |
1357 | string e;
1358 |
1359 | e = percentDecode("%E2%98%83");
1360 | assert(e == "☃", "expected a snowman but got" ~ e);
1361 |
1362 | e = percentDecode("%e2%98%83");
1363 | assert(e == "☃", "expected a snowman but got" ~ e);
1364 |
1365 | try {
1366 | // %ES is an invalid percent sequence: 'S' is not a hex digit.
1367 | percentDecode("%es");
1368 | assert(false, "expected exception not thrown");
1369 | } catch (UrlException) {
1370 | }
1371 |
1372 | try {
1373 | percentDecode("%e");
1374 | assert(false, "expected exception not thrown");
1375 | } catch (UrlException) {
1376 | }
1377 | }
1378 |
1379 | /**
1380 | * Percent-decode a string into a ubyte array.
1381 | *
1382 | * Url components cannot contain non-ASCII characters, and there are very few characters that are
1383 | * safe to include as Url components. Domain names using Unicode values use Punycode. For
1384 | * everything else, there is percent encoding.
1385 | *
1386 | * This yields a ubyte array and will not perform validation on the output. However, an improperly
1387 | * formatted input string will result in a UrlException.
1388 | */
1389 | immutable(ubyte)[] percentDecodeRaw(string encoded)
1390 | {
1391 | // We're dealing with possibly incorrectly encoded UTF-8. Mark it down as ubyte[] for now.
1392 | import std.array : Appender;
1393 | Appender!(immutable(ubyte)[]) app;
1394 | for (int i = 0; i < encoded.length; i++) {
1395 | if (encoded[i] != '%') {
1396 | app ~= encoded[i];
1397 | continue;
1398 | }
1399 | if (i >= encoded.length - 2) {
1400 | throw new UrlException("Invalid percent encoded value: expected two characters after " ~
1401 | "percent symbol. Error at index " ~ i.to!string);
1402 | }
1403 | if (isHex(encoded[i + 1]) && isHex(encoded[i + 2])) {
1404 | auto b = fromHex(encoded[i + 1]);
1405 | auto c = fromHex(encoded[i + 2]);
1406 | app ~= cast(ubyte)((b << 4) | c);
1407 | } else {
1408 | throw new UrlException("Invalid percent encoded value: expected two hex digits after " ~
1409 | "percent symbol. Error at index " ~ i.to!string);
1410 | }
1411 | i += 2;
1412 | }
1413 | return app.data;
1414 | }
1415 |
1416 | private bool isHex(char c) {
1417 | return ('0' <= c && '9' >= c) ||
1418 | ('a' <= c && 'f' >= c) ||
1419 | ('A' <= c && 'F' >= c);
1420 | }
1421 |
1422 | private ubyte fromHex(char s) {
1423 | enum caseDiff = 'a' - 'A';
1424 | if (s >= 'a' && s <= 'z') {
1425 | s -= caseDiff;
1426 | }
1427 | return cast(ubyte)("0123456789ABCDEF".indexOf(s));
1428 | }
1429 |
1430 | private string toPuny(string unicodeHostname)
1431 | {
1432 | if (unicodeHostname.length == 0) return "";
1433 | if (unicodeHostname[0] == '[')
1434 | {
1435 | // It's an ipv6 name.
1436 | return unicodeHostname;
1437 | }
1438 | bool mustEncode = false;
1439 | foreach (i, dchar d; unicodeHostname) {
1440 | auto c = cast(uint) d;
1441 | if (c > 0x80) {
1442 | mustEncode = true;
1443 | break;
1444 | }
1445 | if (c < 0x2C || (c >= 0x3A && c <= 40) || (c >= 0x5B && c <= 0x60) || (c >= 0x7B)) {
1446 | throw new UrlException(
1447 | format(
1448 | "domain name '%s' contains illegal character '%s' at position %s",
1449 | unicodeHostname, d, i));
1450 | }
1451 | }
1452 | if (!mustEncode) {
1453 | return unicodeHostname;
1454 | }
1455 | import std.algorithm.iteration : map;
1456 | return unicodeHostname.split('.').map!punyEncode.join(".");
1457 | }
1458 |
1459 | private string fromPuny(string hostname)
1460 | {
1461 | import std.algorithm.iteration : map;
1462 | return hostname.split('.').map!punyDecode.join(".");
1463 | }
1464 |
1465 | private {
1466 | enum delimiter = '-';
1467 | enum marker = "xn--";
1468 | enum ulong damp = 700;
1469 | enum ulong tmin = 1;
1470 | enum ulong tmax = 26;
1471 | enum ulong skew = 38;
1472 | enum ulong base = 36;
1473 | enum ulong initialBias = 72;
1474 | enum dchar initialN = cast(dchar)128;
1475 |
1476 | ulong adapt(ulong delta, ulong numPoints, bool firstTime) {
1477 | if (firstTime) {
1478 | delta /= damp;
1479 | } else {
1480 | delta /= 2;
1481 | }
1482 | delta += delta / numPoints;
1483 | ulong k = 0;
1484 | while (delta > ((base - tmin) * tmax) / 2) {
1485 | delta /= (base - tmin);
1486 | k += base;
1487 | }
1488 | return k + (((base - tmin + 1) * delta) / (delta + skew));
1489 | }
1490 | }
1491 |
1492 | /**
1493 | * Encode the input string using the Punycode algorithm.
1494 | *
1495 | * Punycode is used to encode UTF domain name segment. A Punycode-encoded segment will be marked
1496 | * with "xn--". Each segment is encoded separately. For instance, if you wish to encode "☂.☃.com"
1497 | * in Punycode, you will get "xn--m3h.xn--n3h.com".
1498 | *
1499 | * In order to puny-encode a domain name, you must split it into its components. The following will
1500 | * typically suffice:
1501 | * ---
1502 | * auto domain = "☂.☃.com";
1503 | * auto encodedDomain = domain.splitter(".").map!(punyEncode).join(".");
1504 | * ---
1505 | */
1506 | string punyEncode(string input)
1507 | {
1508 | import std.array : Appender;
1509 | ulong delta = 0;
1510 | dchar n = initialN;
1511 | auto i = 0;
1512 | auto bias = initialBias;
1513 | Appender!string output;
1514 | output ~= marker;
1515 | auto pushed = 0;
1516 | auto codePoints = 0;
1517 | foreach (dchar c; input) {
1518 | codePoints++;
1519 | if (c <= initialN) {
1520 | output ~= c;
1521 | pushed++;
1522 | }
1523 | }
1524 | if (pushed < codePoints) {
1525 | if (pushed > 0) {
1526 | output ~= delimiter;
1527 | }
1528 | } else {
1529 | // No encoding to do.
1530 | return input;
1531 | }
1532 | bool first = true;
1533 | while (pushed < codePoints) {
1534 | auto best = dchar.max;
1535 | foreach (dchar c; input) {
1536 | if (n <= c && c < best) {
1537 | best = c;
1538 | }
1539 | }
1540 | if (best == dchar.max) {
1541 | throw new UrlException("failed to find a new codepoint to process during punyencode");
1542 | }
1543 | delta += (best - n) * (pushed + 1);
1544 | if (delta > uint.max) {
1545 | // TODO better error message
1546 | throw new UrlException("overflow during punyencode");
1547 | }
1548 | n = best;
1549 | foreach (dchar c; input) {
1550 | if (c < n) {
1551 | delta++;
1552 | }
1553 | if (c == n) {
1554 | ulong q = delta;
1555 | auto k = base;
1556 | while (true) {
1557 | ulong t;
1558 | if (k <= bias) {
1559 | t = tmin;
1560 | } else if (k >= bias + tmax) {
1561 | t = tmax;
1562 | } else {
1563 | t = k - bias;
1564 | }
1565 | if (q < t) {
1566 | break;
1567 | }
1568 | output ~= digitToBasic(t + ((q - t) % (base - t)));
1569 | q = (q - t) / (base - t);
1570 | k += base;
1571 | }
1572 | output ~= digitToBasic(q);
1573 | pushed++;
1574 | bias = adapt(delta, pushed, first);
1575 | first = false;
1576 | delta = 0;
1577 | }
1578 | }
1579 | delta++;
1580 | n++;
1581 | }
1582 | return cast(string)output.data;
1583 | }
1584 |
1585 | /**
1586 | * Decode the input string using the Punycode algorithm.
1587 | *
1588 | * Punycode is used to encode UTF domain name segment. A Punycode-encoded segment will be marked
1589 | * with "xn--". Each segment is encoded separately. For instance, if you wish to encode "☂.☃.com"
1590 | * in Punycode, you will get "xn--m3h.xn--n3h.com".
1591 | *
1592 | * In order to puny-decode a domain name, you must split it into its components. The following will
1593 | * typically suffice:
1594 | * ---
1595 | * auto domain = "xn--m3h.xn--n3h.com";
1596 | * auto decodedDomain = domain.splitter(".").map!(punyDecode).join(".");
1597 | * ---
1598 | */
1599 | string punyDecode(string input) {
1600 | if (!input.startsWith(marker)) {
1601 | return input;
1602 | }
1603 | input = input[marker.length..$];
1604 |
1605 | // let n = initial_n
1606 | dchar n = cast(dchar)128;
1607 |
1608 | // let i = 0
1609 | // let bias = initial_bias
1610 | // let output = an empty string indexed from 0
1611 | size_t i = 0;
1612 | auto bias = initialBias;
1613 | dchar[] output;
1614 | // This reserves a bit more than necessary, but it should be more efficient overall than just
1615 | // appending and inserting volo-nolo.
1616 | output.reserve(input.length);
1617 |
1618 | // consume all code points before the last delimiter (if there is one)
1619 | // and copy them to output, fail on any non-basic code point
1620 | // if more than zero code points were consumed then consume one more
1621 | // (which will be the last delimiter)
1622 | auto end = input.lastIndexOf(delimiter);
1623 | if (end > -1) {
1624 | foreach (dchar c; input[0..end]) {
1625 | output ~= c;
1626 | }
1627 | input = input[end+1 .. $];
1628 | }
1629 |
1630 | // while the input is not exhausted do begin
1631 | size_t pos = 0;
1632 | while (pos < input.length) {
1633 | // let oldi = i
1634 | // let w = 1
1635 | auto oldi = i;
1636 | auto w = 1;
1637 | // for k = base to infinity in steps of base do begin
1638 | for (ulong k = base; k < uint.max; k += base) {
1639 | // consume a code point, or fail if there was none to consume
1640 | // Note that the input is all ASCII, so we can simply index the input string bytewise.
1641 | auto c = input[pos];
1642 | pos++;
1643 | // let digit = the code point's digit-value, fail if it has none
1644 | auto digit = basicToDigit(c);
1645 | // let i = i + digit * w, fail on overflow
1646 | i += digit * w;
1647 | // let t = tmin if k <= bias {+ tmin}, or
1648 | // tmax if k >= bias + tmax, or k - bias otherwise
1649 | ulong t;
1650 | if (k <= bias) {
1651 | t = tmin;
1652 | } else if (k >= bias + tmax) {
1653 | t = tmax;
1654 | } else {
1655 | t = k - bias;
1656 | }
1657 | // if digit < t then break
1658 | if (digit < t) {
1659 | break;
1660 | }
1661 | // let w = w * (base - t), fail on overflow
1662 | w *= (base - t);
1663 | // end
1664 | }
1665 | // let bias = adapt(i - oldi, length(output) + 1, test oldi is 0?)
1666 | bias = adapt(i - oldi, output.length + 1, oldi == 0);
1667 | // let n = n + i div (length(output) + 1), fail on overflow
1668 | n += i / (output.length + 1);
1669 | // let i = i mod (length(output) + 1)
1670 | i %= (output.length + 1);
1671 | // {if n is a basic code point then fail}
1672 | // (We aren't actually going to fail here; it's clear what this means.)
1673 | // insert n into output at position i
1674 | import std.array : insertInPlace;
1675 | (() @trusted { output.insertInPlace(i, cast(dchar)n); })(); // should be @safe but isn't marked
1676 | // increment i
1677 | i++;
1678 | // end
1679 | }
1680 | return output.to!string;
1681 | }
1682 |
1683 | // Lifted from punycode.js.
1684 | private dchar digitToBasic(ulong digit) {
1685 | return cast(dchar)(digit + 22 + 75 * (digit < 26));
1686 | }
1687 |
1688 | // Lifted from punycode.js.
1689 | private uint basicToDigit(char c) {
1690 | auto codePoint = cast(uint)c;
1691 | if (codePoint - 48 < 10) {
1692 | return codePoint - 22;
1693 | }
1694 | if (codePoint - 65 < 26) {
1695 | return codePoint - 65;
1696 | }
1697 | if (codePoint - 97 < 26) {
1698 | return codePoint - 97;
1699 | }
1700 | return base;
1701 | }
1702 |
1703 | unittest {
1704 | {
1705 | auto a = "b\u00FCcher";
1706 | assert(punyEncode(a) == "xn--bcher-kva");
1707 | }
1708 | {
1709 | auto a = "b\u00FCc\u00FCher";
1710 | assert(punyEncode(a) == "xn--bcher-kvab");
1711 | }
1712 | {
1713 | auto a = "ýbücher";
1714 | auto b = punyEncode(a);
1715 | assert(b == "xn--bcher-kvaf", b);
1716 | }
1717 |
1718 | {
1719 | auto a = "mañana";
1720 | assert(punyEncode(a) == "xn--maana-pta");
1721 | }
1722 |
1723 | {
1724 | auto a = "\u0644\u064A\u0647\u0645\u0627\u0628\u062A\u0643\u0644"
1725 | ~ "\u0645\u0648\u0634\u0639\u0631\u0628\u064A\u061F";
1726 | auto b = punyEncode(a);
1727 | assert(b == "xn--egbpdaj6bu4bxfgehfvwxn", b);
1728 | }
1729 | import std.stdio;
1730 | }
1731 |
1732 | unittest {
1733 | {
1734 | auto b = punyDecode("xn--egbpdaj6bu4bxfgehfvwxn");
1735 | assert(b == "ليهمابتكلموشعربي؟", b);
1736 | }
1737 | {
1738 | assert(punyDecode("xn--maana-pta") == "mañana");
1739 | }
1740 | }
1741 |
1742 | unittest {
1743 | import std.string, std.algorithm, std.array, std.range;
1744 | {
1745 | auto domain = "xn--m3h.xn--n3h.com";
1746 | auto decodedDomain = domain.splitter(".").map!(punyDecode).join(".");
1747 | assert(decodedDomain == "☂.☃.com", decodedDomain);
1748 | }
1749 | {
1750 | auto domain = "☂.☃.com";
1751 | auto decodedDomain = domain.splitter(".").map!(punyEncode).join(".");
1752 | assert(decodedDomain == "xn--m3h.xn--n3h.com", decodedDomain);
1753 | }
1754 | }
1755 |
1756 |
--------------------------------------------------------------------------------
/source/archttp/codec/HttpCodec.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.codec.HttpCodec;
13 |
14 | import geario.codec.Codec;
15 | import geario.codec.Encoder;
16 | import geario.codec.Decoder;
17 |
18 | import archttp.codec.HttpDecoder;
19 | import archttp.codec.HttpEncoder;
20 |
21 | import archttp.HttpRequest;
22 | import archttp.HttpResponse;
23 |
24 | /**
25 | *
26 | */
27 | class HttpCodec : Codec!(HttpRequest, HttpResponse)
28 | {
29 | private
30 | {
31 | HttpEncoder _encoder;
32 | HttpDecoder _decoder;
33 | }
34 |
35 | this()
36 | {
37 | _decoder = new HttpDecoder();
38 | _encoder = new HttpEncoder();
39 | }
40 |
41 | override Decoder!HttpRequest decoder()
42 | {
43 | return _decoder;
44 | }
45 |
46 | override Encoder!HttpResponse encoder()
47 | {
48 | return _encoder;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/source/archttp/codec/HttpDecoder.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.codec.HttpDecoder;
13 |
14 | import geario.codec.Decoder;
15 | import geario.codec.Encoder;
16 |
17 | import nbuff;
18 |
19 | import geario.event;
20 |
21 | import archttp.HttpRequestParser;
22 | import archttp.HttpRequest;
23 | import archttp.HttpContext;
24 |
25 | import geario.logging;
26 |
27 | class HttpDecoder : Decoder!HttpRequest
28 | {
29 | private HttpRequestParser _parser;
30 |
31 | this()
32 | {
33 | _parser = new HttpRequestParser;
34 | }
35 |
36 | override long Decode(ref Nbuff buffer, ref HttpRequest request)
37 | {
38 | long result = _parser.parse(cast(string) buffer.data().data());
39 |
40 | if ( ParserStatus.COMPLETED == _parser.parserStatus() )
41 | {
42 | request = _parser.request();
43 |
44 | _parser.reset();
45 | buffer.pop(result);
46 |
47 | return result;
48 | }
49 |
50 | if ( ParserStatus.PARTIAL == _parser.parserStatus() )
51 | {
52 | return 0;
53 | }
54 |
55 | return -1;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/source/archttp/codec/HttpEncoder.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp.codec.HttpEncoder;
13 |
14 | import geario.codec.Encoder;
15 |
16 | import nbuff;
17 |
18 | import archttp.HttpRequest;
19 | import archttp.HttpResponse;
20 |
21 | class HttpEncoder : Encoder!HttpResponse
22 | {
23 | override NbuffChunk Encode(HttpResponse response)
24 | {
25 | return NbuffChunk(response.toString());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/source/archttp/package.d:
--------------------------------------------------------------------------------
1 | /*
2 | * Archttp - A highly performant web framework written in D.
3 | *
4 | * Copyright (C) 2021-2022 Kerisy.com
5 | *
6 | * Website: https://www.kerisy.com
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 |
12 | module archttp;
13 |
14 | public import archttp.Archttp;
15 | public import archttp.HttpContext;
16 | public import archttp.HttpMessageParser;
17 | public import archttp.HttpMethod;
18 | public import archttp.HttpRequest;
19 | public import archttp.HttpRequestHandler;
20 | public import archttp.HttpRequestParser;
21 | public import archttp.HttpRequestParserHandler;
22 | public import archttp.HttpResponse;
23 | public import archttp.HttpStatusCode;
24 | public import archttp.MultiPart;
25 | public import archttp.Router;
26 | public import archttp.Route;
27 | public import archttp.Url;
28 | public import archttp.Cookie;
29 |
--------------------------------------------------------------------------------
/workspace.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | },
6 | {
7 | "path": "examples/parser"
8 | },
9 | {
10 | "path": "examples/httpserver"
11 | }
12 | ],
13 | "settings": {}
14 | }
--------------------------------------------------------------------------------