├── .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 | } --------------------------------------------------------------------------------