├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── Makefile ├── README.md ├── TODO.md ├── genLocalRockspec.lua ├── moonmint.lua ├── moonmint ├── agent.lua ├── cookie.lua ├── deps │ ├── coro-wrapper.lua │ ├── http-headers.lua │ ├── httpCodec.lua │ ├── mimetypes.lua │ ├── pathjoin.lua │ ├── secure-socket.lua │ ├── secure-socket │ │ ├── biowrap.lua │ │ ├── context.lua │ │ ├── init.lua │ │ ├── root_ca.dat │ │ └── root_ca.lua │ └── stream-wrap.lua ├── fs.lua ├── html.lua ├── init.lua ├── response.lua ├── router.lua ├── server.lua ├── static.lua ├── template.lua ├── url.lua └── util.lua ├── rockspecs ├── moonmint-scm-3.rockspec ├── moonmint-scm-4.rockspec └── moonmint-scm-5.rockspec ├── spec ├── helper.lua ├── html_spec.lua ├── query_spec.lua ├── simpleServer_spec.lua └── url_spec.lua ├── test.lua └── tools └── oft.lua /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/lua 3 | 4 | ### Lua ### 5 | # Compiled Lua sources 6 | luac.out 7 | 8 | # luarocks build files 9 | *.src.rock 10 | *.zip 11 | *.tar.gz 12 | 13 | # Object files 14 | *.o 15 | *.os 16 | *.ko 17 | *.obj 18 | *.elf 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Libraries 25 | *.lib 26 | *.a 27 | *.la 28 | *.lo 29 | *.def 30 | *.exp 31 | 32 | # Shared objects (inc. Windows DLLs) 33 | *.dll 34 | *.so 35 | *.so.* 36 | *.dylib 37 | 38 | # Executables 39 | *.exe 40 | *.out 41 | *.app 42 | *.i*86 43 | *.x86_64 44 | *.hex 45 | 46 | # Local rockspec for testing 47 | moonmint-local-0.rockspec 48 | 49 | # Generated documentation 50 | docout 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python # Can use any language here, but if it's not 'python' 2 | # it becomes necessary to pass '--user' to pip when installing hererocks. 3 | sudo: false 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - cmake 9 | sources: 10 | - kalakris-cmake 11 | 12 | env: 13 | - LUA="lua 5.2" 14 | - LUA="lua 5.3" 15 | - LUA="luajit 2.0" 16 | - LUA="luajit 2.1" 17 | 18 | before_install: 19 | - pip install hererocks 20 | - hererocks env --$LUA -rlatest # Use latest LuaRocks, install into 'env' directory. 21 | - source env/bin/activate # Add directory with all installed binaries to PATH. 22 | 23 | install: 24 | - lua genLocalRockspec.lua 25 | - luarocks make # Install the rock, assuming there is a rockspec 26 | 27 | script: 28 | - lua test.lua 29 | 30 | notifications: 31 | email: 32 | on_success: change 33 | on_failure: always 34 | 35 | dist: trusty 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Calvin Rose 2 | Permission is hereby granted, free of charge, to any person obtaining a copy of 3 | this software and associated documentation files (the "Software"), to deal in 4 | the Software without restriction, including without limitation the rights to 5 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 6 | the Software, and to permit persons to whom the Software is furnished to do so, 7 | subject to the following conditions: 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 12 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 14 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 15 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | luvit/lit license 18 | code at 19 | https://github.com/luvit/luvit 20 | https://github.com/luvit/lit 21 | 22 | Apache License 23 | Version 2.0, January 2004 24 | http://www.apache.org/licenses/ 25 | 26 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 27 | 28 | 1. Definitions. 29 | 30 | "License" shall mean the terms and conditions for use, reproduction, 31 | and distribution as defined by Sections 1 through 9 of this document. 32 | 33 | "Licensor" shall mean the copyright owner or entity authorized by 34 | the copyright owner that is granting the License. 35 | 36 | "Legal Entity" shall mean the union of the acting entity and all 37 | other entities that control, are controlled by, or are under common 38 | control with that entity. For the purposes of this definition, 39 | "control" means (i) the power, direct or indirect, to cause the 40 | direction or management of such entity, whether by contract or 41 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 42 | outstanding shares, or (iii) beneficial ownership of such entity. 43 | 44 | "You" (or "Your") shall mean an individual or Legal Entity 45 | exercising permissions granted by this License. 46 | 47 | "Source" form shall mean the preferred form for making modifications, 48 | including but not limited to software source code, documentation 49 | source, and configuration files. 50 | 51 | "Object" form shall mean any form resulting from mechanical 52 | transformation or translation of a Source form, including but 53 | not limited to compiled object code, generated documentation, 54 | and conversions to other media types. 55 | 56 | "Work" shall mean the work of authorship, whether in Source or 57 | Object form, made available under the License, as indicated by a 58 | copyright notice that is included in or attached to the work 59 | (an example is provided in the Appendix below). 60 | 61 | "Derivative Works" shall mean any work, whether in Source or Object 62 | form, that is based on (or derived from) the Work and for which the 63 | editorial revisions, annotations, elaborations, or other modifications 64 | represent, as a whole, an original work of authorship. For the purposes 65 | of this License, Derivative Works shall not include works that remain 66 | separable from, or merely link (or bind by name) to the interfaces of, 67 | the Work and Derivative Works thereof. 68 | 69 | "Contribution" shall mean any work of authorship, including 70 | the original version of the Work and any modifications or additions 71 | to that Work or Derivative Works thereof, that is intentionally 72 | submitted to Licensor for inclusion in the Work by the copyright owner 73 | or by an individual or Legal Entity authorized to submit on behalf of 74 | the copyright owner. For the purposes of this definition, "submitted" 75 | means any form of electronic, verbal, or written communication sent 76 | to the Licensor or its representatives, including but not limited to 77 | communication on electronic mailing lists, source code control systems, 78 | and issue tracking systems that are managed by, or on behalf of, the 79 | Licensor for the purpose of discussing and improving the Work, but 80 | excluding communication that is conspicuously marked or otherwise 81 | designated in writing by the copyright owner as "Not a Contribution." 82 | 83 | "Contributor" shall mean Licensor and any individual or Legal Entity 84 | on behalf of whom a Contribution has been received by Licensor and 85 | subsequently incorporated within the Work. 86 | 87 | 2. Grant of Copyright License. Subject to the terms and conditions of 88 | this License, each Contributor hereby grants to You a perpetual, 89 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 90 | copyright license to reproduce, prepare Derivative Works of, 91 | publicly display, publicly perform, sublicense, and distribute the 92 | Work and such Derivative Works in Source or Object form. 93 | 94 | 3. Grant of Patent License. Subject to the terms and conditions of 95 | this License, each Contributor hereby grants to You a perpetual, 96 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 97 | (except as stated in this section) patent license to make, have made, 98 | use, offer to sell, sell, import, and otherwise transfer the Work, 99 | where such license applies only to those patent claims licensable 100 | by such Contributor that are necessarily infringed by their 101 | Contribution(s) alone or by combination of their Contribution(s) 102 | with the Work to which such Contribution(s) was submitted. If You 103 | institute patent litigation against any entity (including a 104 | cross-claim or counterclaim in a lawsuit) alleging that the Work 105 | or a Contribution incorporated within the Work constitutes direct 106 | or contributory patent infringement, then any patent licenses 107 | granted to You under this License for that Work shall terminate 108 | as of the date such litigation is filed. 109 | 110 | 4. Redistribution. You may reproduce and distribute copies of the 111 | Work or Derivative Works thereof in any medium, with or without 112 | modifications, and in Source or Object form, provided that You 113 | meet the following conditions: 114 | 115 | (a) You must give any other recipients of the Work or 116 | Derivative Works a copy of this License; and 117 | 118 | (b) You must cause any modified files to carry prominent notices 119 | stating that You changed the files; and 120 | 121 | (c) You must retain, in the Source form of any Derivative Works 122 | that You distribute, all copyright, patent, trademark, and 123 | attribution notices from the Source form of the Work, 124 | excluding those notices that do not pertain to any part of 125 | the Derivative Works; and 126 | 127 | (d) If the Work includes a "NOTICE" text file as part of its 128 | distribution, then any Derivative Works that You distribute must 129 | include a readable copy of the attribution notices contained 130 | within such NOTICE file, excluding those notices that do not 131 | pertain to any part of the Derivative Works, in at least one 132 | of the following places: within a NOTICE text file distributed 133 | as part of the Derivative Works; within the Source form or 134 | documentation, if provided along with the Derivative Works; or, 135 | within a display generated by the Derivative Works, if and 136 | wherever such third-party notices normally appear. The contents 137 | of the NOTICE file are for informational purposes only and 138 | do not modify the License. You may add Your own attribution 139 | notices within Derivative Works that You distribute, alongside 140 | or as an addendum to the NOTICE text from the Work, provided 141 | that such additional attribution notices cannot be construed 142 | as modifying the License. 143 | 144 | You may add Your own copyright statement to Your modifications and 145 | may provide additional or different license terms and conditions 146 | for use, reproduction, or distribution of Your modifications, or 147 | for any such Derivative Works as a whole, provided Your use, 148 | reproduction, and distribution of the Work otherwise complies with 149 | the conditions stated in this License. 150 | 151 | 5. Submission of Contributions. Unless You explicitly state otherwise, 152 | any Contribution intentionally submitted for inclusion in the Work 153 | by You to the Licensor shall be under the terms and conditions of 154 | this License, without any additional terms or conditions. 155 | Notwithstanding the above, nothing herein shall supersede or modify 156 | the terms of any separate license agreement you may have executed 157 | with Licensor regarding such Contributions. 158 | 159 | 6. Trademarks. This License does not grant permission to use the trade 160 | names, trademarks, service marks, or product names of the Licensor, 161 | except as required for reasonable and customary use in describing the 162 | origin of the Work and reproducing the content of the NOTICE file. 163 | 164 | 7. Disclaimer of Warranty. Unless required by applicable law or 165 | agreed to in writing, Licensor provides the Work (and each 166 | Contributor provides its Contributions) on an "AS IS" BASIS, 167 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 168 | implied, including, without limitation, any warranties or conditions 169 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 170 | PARTICULAR PURPOSE. You are solely responsible for determining the 171 | appropriateness of using or redistributing the Work and assume any 172 | risks associated with Your exercise of permissions under this License. 173 | 174 | 8. Limitation of Liability. In no event and under no legal theory, 175 | whether in tort (including negligence), contract, or otherwise, 176 | unless required by applicable law (such as deliberate and grossly 177 | negligent acts) or agreed to in writing, shall any Contributor be 178 | liable to You for damages, including any direct, indirect, special, 179 | incidental, or consequential damages of any character arising as a 180 | result of this License or out of the use or inability to use the 181 | Work (including but not limited to damages for loss of goodwill, 182 | work stoppage, computer failure or malfunction, or any and all 183 | other commercial damages or losses), even if such Contributor 184 | has been advised of the possibility of such damages. 185 | 186 | 9. Accepting Warranty or Additional Liability. While redistributing 187 | the Work or Derivative Works thereof, You may choose to offer, 188 | and charge a fee for, acceptance of support, warranty, indemnity, 189 | or other liability obligations and/or rights consistent with this 190 | License. However, in accepting such obligations, You may act only 191 | on Your own behalf and on Your sole responsibility, not on behalf 192 | of any other Contributor, and only if You agree to indemnify, 193 | defend, and hold each Contributor harmless for any liability 194 | incurred by, or claims asserted against, such Contributor by reason 195 | of your accepting any such warranty or additional liability. 196 | 197 | END OF TERMS AND CONDITIONS 198 | 199 | APPENDIX: How to apply the Apache License to your work. 200 | 201 | To apply the Apache License to your work, attach the following 202 | boilerplate notice, with the fields enclosed by brackets "[]" 203 | replaced with your own identifying information. (Don't include 204 | the brackets!) The text should be enclosed in the appropriate 205 | comment syntax for the file format. We also recommend that a 206 | file or class name and description of purpose be included on the 207 | same "printed page" as the copyright notice for easier 208 | identification within third-party archives. 209 | 210 | Copyright [yyyy] [name of copyright owner] 211 | 212 | Licensed under the Apache License, Version 2.0 (the "License"); 213 | you may not use this file except in compliance with the License. 214 | You may obtain a copy of the License at 215 | 216 | http://www.apache.org/licenses/LICENSE-2.0 217 | 218 | Unless required by applicable law or agreed to in writing, software 219 | distributed under the License is distributed on an "AS IS" BASIS, 220 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 221 | See the License for the specific language governing permissions and 222 | limitations under the License. 223 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC=moonmint.lua moonmint 2 | 3 | test: ; lua test.lua 4 | 5 | lint: ; luacheck $(SRC) 6 | 7 | count: ; cloc $(SRC) 8 | 9 | .PHONY: test lint count 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moonmint 2 | 3 | ![Travis](https://travis-ci.org/bakpakin/moonmint.svg?branch=master) 4 | 5 | __moonmint__ is an HTTP web framework for Lua. 6 | Use complex routing, static file serving, and templating with a 7 | minimal code base. Harness the power of libuv to perform asynchronous operations. 8 | 9 | Check out the [wiki](https://github.com/bakpakin/moonmint/wiki) for more information. 10 | 11 | ## Features 12 | 13 | * Simple and flexible express-like routing 14 | * Middleware 15 | * Static file server 16 | * Nonblocking operations with coroutines and libuv 17 | * Supports Lua 5.2, 5.3, LuaJIT 2.0, LuaJIT 2.1 18 | * Powerful asynchronous agent for making HTTP requests 19 | * Templating engine 20 | 21 | ## Quick Install 22 | 23 | In order to install moonmint, the following dependencies are needed. 24 | 25 | * Luarocks (the package manager) 26 | * OpenSSL (for the bkopenssl dependecy) 27 | * CMake (for the luv libuv binding) 28 | 29 | Also, make sure that the Lua dev packages are installed on linux. 30 | On OSX using brew openssl, you may need to provide the openssl 31 | directory to luarocks to install bkopenssl. 32 | 33 | Use luarocks to install 34 | ``` 35 | luarocks install --server=http://luarocks.org/dev moonmint 36 | ``` 37 | 38 | See the wiki for more information. 39 | 40 | ## Example 41 | 42 | moonmint is really simple - probably the simplest way to get a running webserver in Lua out there! 43 | Install with luarocks, write your server script, and run it! 44 | The following example servers serve "Hello, World!" on the default port 8080. 45 | 46 | ```lua 47 | local moonmint = require 'moonmint' 48 | local app = moonmint() 49 | 50 | app:get("/", 'Hello, World!') 51 | 52 | app:start() 53 | ``` 54 | 55 | This can be even shorter if you use chaining. 56 | 57 | ```lua 58 | require('moonmint')() 59 | :get('/', 'Hello, World!') 60 | :start() 61 | ``` 62 | ## Credits 63 | 64 | A lot of code was modified from the [Luvit](https://luvit.io/) project and from [Tim Caswell](https://twitter.com/creationix), the main author. 65 | moonmint depends on the luv library, a Lua binding to libuv. 66 | 67 | Another important dependency is lua-openssl, which is a very useful openssl binding for Lua created and maintained 68 | by [George Zhao](https://github.com/zhaozg). Many thanks. 69 | 70 | Bundled the mimetypes dependency under `moonmint/deps/mimetypes.lua` since 71 | the original repository has disappeared. 72 | 73 | ## License 74 | 75 | MIT 76 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Goals 4 | * Add testing for server code 5 | * Add more testing in general 6 | * Remove some luvit and lit libraries depenencies (httpCodec, coro-net, maybe secure socket.) 7 | * Etags, either only with static or as utility middleware (or both). 8 | * View engine that is integrated with built in templates, but can eventually use others like etlua. Partials support. 9 | * Better documentation, preferably LuaDoc like (source generated) or on the wiki. Add docs as we go. 10 | * Contributing guide 11 | * Convert HTTP parser over to http\_parser, a C library. 12 | * Add more general pattern support in routing. 13 | * Websockets - easy to integrate and of course coro-style 14 | * Get a logo! 15 | 16 | ## Maybe 17 | * Compatibility with all modern Lua versions (5.1, 5.2, 5.3, LuaJIT). LuaJIT is currently the primary target. 18 | * Pretty homepage. 19 | * Full Windows compatibiliy 20 | * Make moonmint run both as a Luarocks package, and as a luvit/lit module (backport to lit). Ensure that tests run on both platforms. 21 | * Refactor dependent packages for easier building on platforms (remove CMake dependency?) 22 | * CLI tool for project templates, packaging, and deploying. 23 | * Tutorials and example projects. 24 | * Useful addons and libraries like body parsing, database connectors, authentication, etc. 25 | -------------------------------------------------------------------------------- /genLocalRockspec.lua: -------------------------------------------------------------------------------- 1 | local format = [[ 2 | package = "moonmint" 3 | version = "%s" 4 | source = { 5 | url = "file:////0.0.0.0%s/%s" 6 | } 7 | description = { 8 | homepage = "https://github.com/bakpakin/moonmint", 9 | summary = "Web framework for Lua", 10 | license = "MIT", 11 | } 12 | dependencies = { 13 | "lua >= 5.1", 14 | "luv ~> 1.8", 15 | "mimetypes >= 1.0", 16 | "bit32" 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["moonmint"] = "moonmint/init.lua", 22 | 23 | ["moonmint.agent"] = "moonmint/agent.lua", 24 | ["moonmint.cookie"] = "moonmint/cookie.lua", 25 | ["moonmint.fs"] = "moonmint/fs.lua", 26 | ["moonmint.html"] = "moonmint/html.lua", 27 | ["moonmint.response"] = "moonmint/response.lua", 28 | ["moonmint.router"] = "moonmint/router.lua", 29 | ["moonmint.server"] = "moonmint/server.lua", 30 | ["moonmint.static"] = "moonmint/static.lua", 31 | ["moonmint.template"] = "moonmint/template.lua", 32 | ["moonmint.url"] = "moonmint/url.lua", 33 | ["moonmint.util"] = "moonmint/util.lua", 34 | 35 | ["moonmint.deps.http-headers"] = "moonmint/deps/http-headers.lua", 36 | ["moonmint.deps.coro-wrapper"] = "moonmint/deps/coro-wrapper.lua", 37 | ["moonmint.deps.httpCodec"] = "moonmint/deps/httpCodec.lua", 38 | ["moonmint.deps.stream-wrap"] = "moonmint/deps/stream-wrap.lua", 39 | ["moonmint.deps.secure-socket.biowrap"] = "moonmint/deps/secure-socket/biowrap.lua", 40 | ["moonmint.deps.secure-socket.context"] = "moonmint/deps/secure-socket/context.lua", 41 | ["moonmint.deps.secure-socket.root_ca"] = "moonmint/deps/secure-socket/root_ca.lua", 42 | ["moonmint.deps.secure-socket"] = "moonmint/deps/secure-socket/init.lua", 43 | ["moonmint.deps.pathjoin"] = "moonmint/deps/pathjoin.lua" 44 | } 45 | } 46 | ]] 47 | 48 | local currentDirectory = io.popen('pwd'):read'*l' 49 | if not currentDirectory then 50 | currentDirectory = io.popen('cd'):read'*l' 51 | end 52 | local version = 'local-0' 53 | local target = ('moonmint-%s.rockspec'):format(version) 54 | 55 | local f = assert(io.open(target, 'w')) 56 | f:write(format:format(version, currentDirectory, target)) 57 | f:close(); 58 | print(('Wrote to %s.'):format(target)) 59 | -------------------------------------------------------------------------------- /moonmint.lua: -------------------------------------------------------------------------------- 1 | return require 'moonmint.init' 2 | -------------------------------------------------------------------------------- /moonmint/agent.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | -- Connection and Request code modified from 20 | -- https://github.com/luvit/lit/blob/master/deps/coro-http.lua 21 | 22 | --- @classmod Agent 23 | -- 24 | -- The Agent class is used to make HTTP requests. 25 | -- Query APIs on a server or make your own HTTP client. 26 | -- 27 | -- @author Calvin Rose 28 | -- @license MIT 29 | -- @copyright 2016 30 | -- 31 | 32 | local uv = require 'luv' 33 | local response = require 'moonmint.response' 34 | local httpHeaders = require 'moonmint.deps.http-headers' 35 | local url = require 'moonmint.url' 36 | local pathJoin = require 'moonmint.deps.pathjoin'.pathJoin 37 | local httpCodec = require 'moonmint.deps.httpCodec' 38 | local coroWrap = require 'moonmint.deps.coro-wrapper' 39 | local wrapStream = require 'moonmint.deps.stream-wrap' 40 | local setmetatable = setmetatable 41 | local match = string.match 42 | 43 | -- Pulled from coro-net 44 | local function makeCallback(timeout) 45 | local thread = coroutine.running() 46 | local timer, done 47 | if timeout then 48 | timer = uv.new_timer() 49 | timer:start(timeout, 0, function () 50 | if done then return end 51 | done = true 52 | timer:close() 53 | return assert(coroutine.resume(thread, nil, "timeout")) 54 | end) 55 | end 56 | return function (err, data) 57 | if done then return end 58 | done = true 59 | if timer then timer:close() end 60 | if err then 61 | return assert(coroutine.resume(thread, nil, err)) 62 | end 63 | return assert(coroutine.resume(thread, data or true)) 64 | end 65 | end 66 | 67 | local function connect(options) 68 | local socket, success, err 69 | success, err = uv.getaddrinfo(options.host, options.port, { 70 | socktype = options.socktype or "stream", 71 | family = options.family or "inet", 72 | }, makeCallback(options.timeout)) 73 | if not success then return nil, err end 74 | local res 75 | res, err = coroutine.yield() 76 | if not res then return nil, err end 77 | socket = uv.new_tcp() 78 | socket:connect(res[1].addr, res[1].port, makeCallback(options.timeout)) 79 | success, err = coroutine.yield() 80 | if not success then return nil, err end 81 | if options.tls then 82 | local secureSocket = require('moonmint.deps.secure-socket') 83 | socket, err = secureSocket(socket, options.tls) 84 | if not socket then 85 | return nil, err 86 | end 87 | end 88 | local read, write, close = wrapStream(socket) 89 | return read, write, socket, close 90 | end 91 | 92 | local connections = {} 93 | 94 | local function clearConnections() 95 | for i = 1, #connections do 96 | local connection = connections[i] 97 | if not connection.socket:is_closing() then 98 | connection.socket:close() 99 | end 100 | end 101 | connections = {} 102 | end 103 | 104 | local function getConnection(host, port, tls) 105 | for i = #connections, 1, -1 do 106 | local connection = connections[i] 107 | if connection.host == host and connection.port == port and connection.tls == tls then 108 | table.remove(connections, i) 109 | -- make sure the connection is still alive before reusing it. 110 | if not connection.socket:is_closing() then 111 | connection.reused = true 112 | connection.socket:ref() 113 | return connection 114 | end 115 | end 116 | end 117 | local read, write, socket = assert(connect { 118 | host = host, 119 | port = port, 120 | tls = tls 121 | }) 122 | local updateDecoder, updateEncoder 123 | read, updateDecoder = coroWrap.reader(read, httpCodec.decoder()) 124 | write, updateEncoder = coroWrap.writer(write, httpCodec.encoder()) 125 | return { 126 | socket = socket, 127 | host = host, 128 | port = port, 129 | tls = tls, 130 | read = read, 131 | write = write, 132 | updateEncoder = updateEncoder, 133 | updateDecoder = updateDecoder, 134 | reset = function () 135 | -- This is called after parsing the response head from a HEAD request. 136 | -- If you forget, the codec might hang waiting for a body that doesn't exist. 137 | updateDecoder(httpCodec.decoder()) 138 | end 139 | } 140 | end 141 | 142 | local function saveConnection(connection) 143 | if connection.socket:is_closing() then return end 144 | connections[#connections + 1] = connection 145 | connection.socket:unref() 146 | end 147 | 148 | local function makeHead(self, host, path, body) 149 | -- Use GET as default method 150 | local method = self:getConf('method') or 'GET' 151 | local head = { 152 | method = method, 153 | path = path, 154 | {"Host", host} 155 | } 156 | local contentLength 157 | local chunked 158 | local headers = self.headers 159 | if headers then 160 | for i = 1, #headers do 161 | local key, value = unpack(headers[i]) 162 | key = key:lower() 163 | if key == "content-length" then 164 | contentLength = value 165 | elseif key == "content-encoding" and value:lower() == "chunked" then 166 | chunked = true 167 | end 168 | head[#head + 1] = headers[i] 169 | end 170 | end 171 | if type(body) == "string" then 172 | if not chunked and not contentLength then 173 | head[#head + 1] = {"Content-Length", #body} 174 | end 175 | end 176 | return head 177 | end 178 | 179 | local function requestImpl(self, tls, hostname, port, head, body) 180 | -- Get a connection 181 | local connection = getConnection(hostname, port, tls) 182 | local read = connection.read 183 | local write = connection.write 184 | 185 | write(head) 186 | if body then write(body) end 187 | local res = read() 188 | if not res then 189 | if not connection.socket:is_closing() then 190 | connection.socket:close() 191 | end 192 | -- If we get an immediate close on a reused socket, try again with a new socket. 193 | -- TODO: think about if this could resend requests with side effects and cause 194 | -- them to double execute in the remote server. 195 | if connection.reused then 196 | return requestImpl(self, tls, hostname, port, head, body) 197 | end 198 | error("connection closed") 199 | end 200 | 201 | body = {} 202 | if head.method == "HEAD" then 203 | connection.reset() 204 | else 205 | while true do 206 | local item = read() 207 | if not item then 208 | res.keepAlive = false 209 | break 210 | end 211 | if #item == 0 then 212 | break 213 | end 214 | body[#body + 1] = item 215 | end 216 | end 217 | 218 | if res.keepAlive then 219 | saveConnection(connection) 220 | else 221 | write() 222 | end 223 | 224 | -- Follow redirects 225 | if not self:getConf('noFollow') and 226 | head.method == "GET" and 227 | (res.code == 302 or res.code == 307) then 228 | for i = 1, #res do 229 | local key, location = unpack(res[i]) 230 | if key:lower() == "location" then 231 | head.path = location 232 | return requestImpl(self, tls, hostname, port, head, body) 233 | end 234 | end 235 | end 236 | 237 | return res, table.concat(body) 238 | end 239 | 240 | local function sendImpl(self, body) 241 | body = body or self:getConf('body') 242 | local uri = self:getConf('url') 243 | local proto = match(uri, '^https?://') 244 | if proto then 245 | uri = uri:sub(#proto + 1) 246 | end 247 | local hostname, path = match(uri, '^([%w%:%.%-]*)(.-)$') 248 | hostname, path = hostname or '', path or '' 249 | 250 | -- Make path 251 | local selfPath = self:getConf('path') 252 | if selfPath then 253 | path = pathJoin(path, selfPath) 254 | end 255 | if path == '' then 256 | path = '/' 257 | end 258 | local alreadyHasQuery = match(path, '%?') 259 | local rawQuery = url.queryEncode(self.params) 260 | if rawQuery ~= '' then 261 | path = path .. (alreadyHasQuery and '&' or '?') .. rawQuery 262 | end 263 | 264 | -- Get port, host, and protocol 265 | local tls = proto == 'https://' 266 | local host, port = match(hostname, '^([^:]+):?(%d*)$') 267 | if port == '' then 268 | port = tls and 443 or 80 269 | else 270 | port = tonumber(port) or 80 271 | end 272 | 273 | -- Resolve localhost 274 | if host == '' or host == 'localhost' or not host then 275 | host = '127.0.0.1' 276 | end 277 | 278 | local head = makeHead(self, host, path, body) 279 | local resHead, resBody = requestImpl(self, tls, host, port, head, body) 280 | 281 | return response { 282 | body = resBody, 283 | code = resHead.code, 284 | headers = httpHeaders.getHeaders(resHead), 285 | version = resHead.version or 1.1, 286 | config = self 287 | } 288 | end 289 | 290 | local Agent = {} 291 | 292 | --- Configure a key value pair on the Agent 293 | --@param key 294 | --@param value 295 | -- @return self 296 | function Agent:conf(key, value) 297 | self.config[key] = value 298 | return self 299 | end 300 | 301 | --- Get a configured option on the Agent 302 | -- @param key 303 | -- @return the value 304 | function Agent:getConf(key) 305 | return self.config[key] 306 | end 307 | 308 | --- Set the body of the Agent. 309 | -- @param body the body of the agent 310 | -- @return self 311 | function Agent:body(body) 312 | return self:conf('body', body) 313 | end 314 | 315 | --- Send a request with the Agent, with an optional body. 316 | -- @param body the (optional) body of the request 317 | -- @return self 318 | function Agent:send(body) 319 | return sendImpl(self, body) 320 | end 321 | 322 | --- Send a request with the Agent, with an optional body. 323 | -- Block while waiting for a response. 324 | -- @param body the (optional) body of the request 325 | -- @return self 326 | function Agent:sendSync(body) 327 | local ret, err 328 | local loop = true 329 | coroutine.wrap(function() 330 | ret, err = sendImpl(self, body) 331 | loop = false 332 | end)() 333 | while loop do 334 | uv.run('once') 335 | end 336 | return ret, err 337 | end 338 | 339 | --- Set a header to a certain value. Headers are case-insensitive. 340 | -- @param header the Header to set 341 | -- @param value the value of the header 342 | -- @return self 343 | function Agent:set(header, value) 344 | self.headers[header] = value 345 | return self 346 | end 347 | 348 | --- Sets a query parameter. The value should be a string. 349 | -- @param key the name of the parameter 350 | -- @param value the value of the parameter 351 | -- @return self 352 | function Agent:param(key, value) 353 | self.params[key] = value or true 354 | return self 355 | end 356 | 357 | --- Sets the url of the Agent. Can either just the url host, path, pr a combination of both. 358 | -- @param uri 359 | -- @return self 360 | function Agent:url(uri) 361 | if match(uri, '^[a-z]+://') then 362 | return self:conf('url', uri) 363 | else 364 | return self:conf('path', uri) 365 | end 366 | end 367 | 368 | function Agent:pathjoin(path) 369 | if match(path, '^[a-z]+://') then 370 | return self:conf('url', path) 371 | else 372 | local oldPath = self:getConf('path') 373 | if oldPath then 374 | return self:conf('path', pathJoin(oldPath, path)) 375 | else 376 | return self:conf('path', path) 377 | end 378 | end 379 | end 380 | 381 | --- Sets the HTTP method of the request. 382 | -- @param method 383 | -- @return self 384 | function Agent:method(method) 385 | return self:conf('method', tostring(method):upper()) 386 | end 387 | 388 | function Agent:get(uri) 389 | return self:method('GET'):url(uri) 390 | end 391 | 392 | function Agent:delete(uri) 393 | return self:method('DELETE'):url(uri) 394 | end 395 | Agent.del = Agent.delete 396 | 397 | function Agent:put(uri) 398 | return self:method('PUT'):url(uri) 399 | end 400 | 401 | function Agent:post(uri) 402 | return self:method('POST'):url(uri) 403 | end 404 | 405 | -- Just some useful shortcuts 406 | -- TODO Add more 407 | local acceptMappings = { 408 | json = 'application/json', 409 | text = 'text/*', 410 | html = 'text/html', 411 | js = 'text/javascript', 412 | xml = 'text/xml', 413 | css = 'text/css', 414 | all = '*/*' 415 | } 416 | 417 | local typeMappings = { 418 | text = 'text/plain' 419 | } 420 | 421 | function Agent:accept(...) 422 | local currentlyAccepts = self.headers.Accept or '' 423 | if not match(currentlyAccepts, ';%s*$') then 424 | currentlyAccepts = currentlyAccepts .. '; ' 425 | end 426 | local toAccept = {} 427 | for i = 1, select('#', ...) do 428 | local tp = select(i, ...) 429 | toAccept[#toAccept + 1] = acceptMappings[tp] or tp 430 | end 431 | self.headers.Accept = currentlyAccepts .. table.concat(toAccept, '; ') 432 | return self 433 | end 434 | 435 | --- Sets the content type of the Agent. 436 | -- @param tp the content type. Can be a full mime type or a short alias, like 'json' 437 | -- @return self 438 | function Agent:type(tp) 439 | self.headers['Content-Type'] = typeMappings[tp] or acceptMappings[tp] or tp 440 | return self 441 | end 442 | 443 | --- Creates a module out of an Agent. The module behaves 444 | -- the same as the original Agent, but creates a copy 445 | -- of the original Agent on all methods. This is very 446 | -- useful for creating interfaces to APIs, or otherwise 447 | -- reusing complex Agent configurations with modifications. 448 | -- @return a new module 449 | function Agent:module() 450 | local newModule = {} 451 | return setmetatable(newModule, { 452 | __index = function(child, key) 453 | local value = rawget(child, key) 454 | if value ~= nil then 455 | return value 456 | end 457 | value = self[key] 458 | if type(value) == 'function' then 459 | local method = function(self1, ...) 460 | if self1 == newModule then 461 | local new = self() 462 | return new[key](new, ...) 463 | else 464 | return value(self1, ...) 465 | end 466 | end 467 | child[key] = method 468 | return method 469 | end 470 | return value 471 | end 472 | }) 473 | end 474 | 475 | local function makeRequest(parent) 476 | local params = {} 477 | if parent and parent.params then 478 | for k, v in pairs(parent.params) do 479 | params[k] = v 480 | end 481 | end 482 | return setmetatable({ 483 | params = params, 484 | config = setmetatable({}, { 485 | __index = parent.config 486 | }) 487 | }, { 488 | __index = parent, 489 | __call = makeRequest 490 | }) 491 | end 492 | 493 | local M = {} 494 | 495 | for k, v in pairs(Agent) do 496 | M[k] = function(self, ...) 497 | if self == M then 498 | return v(makeRequest(Agent), ...) 499 | else 500 | return v(makeRequest(Agent), self, ...) 501 | end 502 | end 503 | end 504 | 505 | function M.close() 506 | clearConnections() 507 | end 508 | 509 | return M 510 | -------------------------------------------------------------------------------- /moonmint/cookie.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | local byte = string.byte 20 | local sub = string.sub 21 | local tonumber = tonumber 22 | local assert = assert 23 | local insert = table.insert 24 | local type = type 25 | local pairs = pairs 26 | 27 | local function skipWhitespace(str, i) 28 | while i <= #str do 29 | local b = byte(str, i) 30 | if b == 32 or (b >= 9 and b <= 13) then 31 | i = i + 1 32 | else 33 | return i 34 | end 35 | end 36 | return i 37 | end 38 | 39 | local function skipWhitespaceBack(str, i) 40 | while i > 0 do 41 | local b = byte(str, i) 42 | if b == 32 or (b >= 9 and b <= 13) then 43 | i = i - 1 44 | else 45 | return i 46 | end 47 | end 48 | return i 49 | end 50 | 51 | local function getNext(str, i) 52 | while i <= #str do 53 | local b = byte(str, i) 54 | if b == 59 or b == 61 then 55 | return i, b 56 | end 57 | i = i + 1 58 | end 59 | return i 60 | end 61 | 62 | local function coerceString(str) 63 | if str == 'true' then return true end 64 | if str == 'false' then return false end 65 | local maybeNumber = tonumber(str) 66 | if maybeNumber then return maybeNumber end 67 | return str 68 | end 69 | 70 | local function readKVPair(str, i) 71 | i = skipWhitespace(str, i) 72 | local kStart, charCheck = i 73 | i, charCheck = getNext(str, i) 74 | local kEnd = skipWhitespaceBack(str, i - 1) 75 | local key = sub(str, kStart, kEnd) 76 | if charCheck == 61 then -- = sign 77 | local vStart = skipWhitespace(str, i + 1) 78 | i = getNext(str, vStart) 79 | local vEnd = skipWhitespaceBack(str, i - 1) 80 | local value = sub(str, vStart, vEnd) 81 | return i + 1, key, value 82 | else -- no =, just ends 83 | print 'fack' 84 | return i + 1, key 85 | end 86 | end 87 | 88 | local function parseCookie(str) 89 | local ret = {} 90 | local i, len = 1, #str 91 | local kString, vString 92 | while i <= len do 93 | i, kString, vString = readKVPair(str, i) 94 | if vString then 95 | ret[coerceString(kString)] = coerceString(vString) 96 | else 97 | ret[''] = coerceString(kString) 98 | end 99 | end 100 | return ret 101 | end 102 | 103 | local function makeSetCookie(cookie) 104 | assert(cookie.value, 'Expected cookie value.') 105 | local str = (cookie.key and (cookie.key .. '=') or '') .. cookie.value 106 | .. (cookie.expires and '; Expires=' .. cookie.expires or '') 107 | .. (cookie.maxAge and '; Max-Age=' .. cookie.max_age or '') 108 | .. (cookie.domain and '; Domain=' .. cookie.domain or '') 109 | .. (cookie.path and '; Path=' .. cookie.path or '') 110 | .. (cookie.secure and '; Secure' or '') 111 | .. (cookie.httpOnly and '; HttpOnly' or '') 112 | .. (cookie.sameSite and '; SameSite=' .. cookie.sameSite or '') 113 | .. (cookie.extension and '; ' .. cookie.extension or '') 114 | return str 115 | end 116 | 117 | local function middleware(options) 118 | options = options or {} 119 | local optionsVector = {} 120 | for k, v in pairs(options) do 121 | optionsVector[#optionsVector + 1] = k 122 | optionsVector[#optionsVector + 1] = v 123 | end 124 | return function(req, go) 125 | local cookieHeader = req.headers['cookie'] 126 | if cookieHeader then 127 | req.cookies = parseCookie(cookieHeader) 128 | else 129 | req.cookies = {} 130 | end 131 | local res = go() 132 | local resCookies = res.cookies 133 | if resCookies then 134 | for k, v in pairs(resCookies) do 135 | local theOptions = { 136 | key = k 137 | } 138 | for i = 1, #optionsVector, 2 do 139 | theOptions[optionsVector[i]] = optionsVector[i + 1] 140 | end 141 | if type(v) == 'table' then 142 | for optionKey, optionValue in pairs(v) do 143 | theOptions[optionKey] = optionValue 144 | end 145 | else 146 | theOptions.value = v 147 | end 148 | insert(res.headers, {'Set-Cookie', makeSetCookie(theOptions)}) 149 | end 150 | end 151 | return res 152 | end 153 | end 154 | 155 | return { 156 | parse = parseCookie, 157 | makeSet = makeSetCookie, 158 | middleware = middleware, 159 | defualtMiddleware = middleware() 160 | } 161 | -------------------------------------------------------------------------------- /moonmint/deps/coro-wrapper.lua: -------------------------------------------------------------------------------- 1 | -- Modified from https://github.com/luvit/lit/blob/master/deps/coro-wrapper.lua 2 | 3 | --[[lit-meta 4 | name = "creationix/coro-wrapper" 5 | version = "2.0.0" 6 | homepage = "https://github.com/luvit/lit/blob/master/deps/coro-wrapper.lua" 7 | description = "An adapter for applying decoders to coro-streams." 8 | tags = {"coro", "decoder", "adapter"} 9 | license = "MIT" 10 | author = { name = "Tim Caswell" } 11 | ]] 12 | 13 | local function reader(read, decode) 14 | local buffer = "" 15 | return function () 16 | while true do 17 | local item, extra = decode(buffer) 18 | if item then 19 | buffer = extra 20 | return item 21 | end 22 | local chunk = read() 23 | if not chunk then return end 24 | buffer = buffer .. chunk 25 | end 26 | end, 27 | function (newDecode) 28 | decode = newDecode 29 | end 30 | end 31 | 32 | local function writer(write, encode) 33 | return function (item) 34 | if not item then 35 | return write() 36 | end 37 | return write(encode(item)) 38 | end, 39 | function (newEncode) 40 | encode = newEncode 41 | end 42 | end 43 | 44 | return { 45 | reader = reader, 46 | writer = writer 47 | } 48 | -------------------------------------------------------------------------------- /moonmint/deps/http-headers.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Copyright 2015 The Luvit Authors. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS-IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | --]] 18 | 19 | --[[lit-meta 20 | name = "luvit/http-header" 21 | version = "1.0.0" 22 | license = "Apache 2" 23 | homepage = "https://github.com/luvit/luvit/blob/master/deps/http-header.lua" 24 | description = "Utilities for dealing with HTTP headers in Luvit" 25 | tags = {"luvit", "http"} 26 | ]] 27 | 28 | -- Provide a nice case insensitive interface to headers. 29 | -- Pulled from https://github.com/creationix/weblit/blob/master/libs/weblit-app.lua 30 | local headerMeta = { 31 | __index = function (list, name) 32 | if type(name) ~= "string" then 33 | return rawget(list, name) 34 | end 35 | name = name:lower() 36 | for i = 1, #list do 37 | local key, value = unpack(list[i]) 38 | if key:lower() == name then return value end 39 | end 40 | end, 41 | __newindex = function (list, name, value) 42 | -- non-string keys go through as-is. 43 | if type(name) ~= "string" then 44 | return rawset(list, name, value) 45 | end 46 | -- First remove any existing pairs with matching key 47 | local lowerName = name:lower() 48 | for i = #list, 1, -1 do 49 | if list[i][1]:lower() == lowerName then 50 | table.remove(list, i) 51 | end 52 | end 53 | -- If value is nil, we're done 54 | if value == nil then return end 55 | -- Otherwise, set the key(s) 56 | if (type(value) == "table") then 57 | -- We accept a table of strings 58 | for i = 1, #value do 59 | rawset(list, #list + 1, {name, tostring(value[i])}) 60 | end 61 | else 62 | -- Or a single value interperted as string 63 | rawset(list, #list + 1, {name, tostring(value)}) 64 | end 65 | end, 66 | } 67 | 68 | -- Creates a new headers table or sets the metatable of `tbl` to headerMeta 69 | local function newHeaders(tbl) 70 | return setmetatable(tbl or {}, headerMeta) 71 | end 72 | 73 | -- Adds all header information found in `tbl` into `headers`, which should be a table 74 | -- with headerMeta as its metatable. 75 | -- 76 | -- Note: String keys in `tbl` will overwrite the key's value(s) in `headers` if it exists 77 | local function appendToHeaders(tbl, headers) 78 | if tbl then 79 | for k,v in pairs(tbl) do 80 | if type(k) == "number" then k = #headers + 1 end 81 | headers[k] = v 82 | end 83 | end 84 | return headers 85 | end 86 | 87 | -- Converts a table of headers into a headers table. 88 | -- The input tables can have keys in any of the following formats: 89 | -- 90 | -- { 91 | -- ["name"] = value, 92 | -- ["name"] = {multiple, values}, 93 | -- {"name", value}, 94 | -- } 95 | local function toHeaders(tbl) 96 | return appendToHeaders(tbl, newHeaders()) 97 | end 98 | 99 | -- Converts and combines any table(s) of headers to a single headers table. 100 | -- The input tables can have keys in any of the following formats: 101 | -- 102 | -- { 103 | -- ["name"] = value, 104 | -- ["name"] = {multiple, values}, 105 | -- {"name", value}, 106 | -- } 107 | -- 108 | -- Note: Duplicate string keys will overwrite eachother, with the last duplicate 109 | -- key of the last table taking precedence 110 | local function combineHeaders(...) 111 | local combined = newHeaders() 112 | for _, tbl in ipairs({...}) do 113 | appendToHeaders(tbl, combined) 114 | end 115 | return combined 116 | end 117 | 118 | -- Extracts headers from a table that has array-like keys of {headerName, value} tables. 119 | -- Ignores any non-array-like keys of the table. 120 | local function getHeaders(tbl) 121 | local headers = newHeaders() 122 | if tbl then 123 | for i,header in ipairs(tbl) do 124 | if type(header) == "table" then 125 | headers[i] = header 126 | end 127 | end 128 | end 129 | return headers 130 | end 131 | 132 | return { 133 | headerMeta = headerMeta, 134 | newHeaders = newHeaders, 135 | toHeaders = toHeaders, 136 | combineHeaders = combineHeaders, 137 | getHeaders = getHeaders 138 | } 139 | -------------------------------------------------------------------------------- /moonmint/deps/httpCodec.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Modified from luvit/http-codec 3 | -- https://github.com/luvit/luvit/blob/master/deps/http-codec.lua 4 | -- 5 | 6 | --[[ 7 | 8 | Copyright 2014-2015 The Luvit Authors. All Rights Reserved. 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS-IS" BASIS, 18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | See the License for the specific language governing permissions and 20 | limitations under the License. 21 | 22 | --]] 23 | 24 | --[[lit-meta 25 | name = "luvit/http-codec" 26 | version = "2.0.0" 27 | homepage = "https://github.com/luvit/luvit/blob/master/deps/http-codec.lua" 28 | description = "A simple pair of functions for converting between hex and raw strings." 29 | tags = {"codec", "http"} 30 | license = "Apache 2" 31 | author = { name = "Tim Caswell" } 32 | ]] 33 | 34 | local sub = string.sub 35 | local gsub = string.gsub 36 | local lower = string.lower 37 | local find = string.find 38 | local format = string.format 39 | local concat = table.concat 40 | local match = string.match 41 | local unpack = unpack or table.unpack 42 | 43 | local STATUS_CODES = { 44 | [100] = 'Continue', 45 | [101] = 'Switching Protocols', 46 | [102] = 'Processing', -- RFC 2518, obsoleted by RFC 4918 47 | [200] = 'OK', 48 | [201] = 'Created', 49 | [202] = 'Accepted', 50 | [203] = 'Non-Authoritative Information', 51 | [204] = 'No Content', 52 | [205] = 'Reset Content', 53 | [206] = 'Partial Content', 54 | [207] = 'Multi-Status', -- RFC 4918 55 | [300] = 'Multiple Choices', 56 | [301] = 'Moved Permanently', 57 | [302] = 'Moved Temporarily', 58 | [303] = 'See Other', 59 | [304] = 'Not Modified', 60 | [305] = 'Use Proxy', 61 | [307] = 'Temporary Redirect', 62 | [400] = 'Bad Request', 63 | [401] = 'Unauthorized', 64 | [402] = 'Payment Required', 65 | [403] = 'Forbidden', 66 | [404] = 'Not Found', 67 | [405] = 'Method Not Allowed', 68 | [406] = 'Not Acceptable', 69 | [407] = 'Proxy Authentication Required', 70 | [408] = 'Request Time-out', 71 | [409] = 'Conflict', 72 | [410] = 'Gone', 73 | [411] = 'Length Required', 74 | [412] = 'Precondition Failed', 75 | [413] = 'Request Entity Too Large', 76 | [414] = 'Request-URI Too Large', 77 | [415] = 'Unsupported Media Type', 78 | [416] = 'Requested Range Not Satisfiable', 79 | [417] = 'Expectation Failed', 80 | [418] = "I'm a teapot", -- RFC 2324 81 | [422] = 'Unprocessable Entity', -- RFC 4918 82 | [423] = 'Locked', -- RFC 4918 83 | [424] = 'Failed Dependency', -- RFC 4918 84 | [425] = 'Unordered Collection', -- RFC 4918 85 | [426] = 'Upgrade Required', -- RFC 2817 86 | [500] = 'Internal Server Error', 87 | [501] = 'Not Implemented', 88 | [502] = 'Bad Gateway', 89 | [503] = 'Service Unavailable', 90 | [504] = 'Gateway Time-out', 91 | [505] = 'HTTP Version not supported', 92 | [506] = 'Variant Also Negotiates', -- RFC 2295 93 | [507] = 'Insufficient Storage', -- RFC 4918 94 | [509] = 'Bandwidth Limit Exceeded', 95 | [510] = 'Not Extended' -- RFC 2774 96 | } 97 | 98 | local function encoder() 99 | 100 | local mode 101 | local encodeHead, encodeRaw, encodeChunked 102 | 103 | function encodeHead(item) 104 | if not item or item == "" then 105 | return item 106 | elseif not (type(item) == "table") then 107 | error("expected a table but got a " .. type(item) .. " when encoding data") 108 | end 109 | local head, chunkedEncoding 110 | local version = item.version or 1.1 111 | if item.method then 112 | local path = item.path 113 | assert(path and #path > 0, "expected non-empty path") 114 | head = { item.method .. ' ' .. item.path .. ' HTTP/' .. version .. '\r\n' } 115 | else 116 | local reason = item.reason or STATUS_CODES[item.code] 117 | head = { 'HTTP/' .. version .. ' ' .. item.code .. ' ' .. reason .. '\r\n' } 118 | end 119 | for i = 1, #item do 120 | local key, value = unpack(item[i]) 121 | local lowerKey = lower(key) 122 | if lowerKey == "transfer-encoding" then 123 | chunkedEncoding = lower(value) == "chunked" 124 | end 125 | value = gsub(tostring(value), "[\r\n]+", " ") 126 | head[#head + 1] = key .. ': ' .. tostring(value) .. '\r\n' 127 | end 128 | head[#head + 1] = '\r\n' 129 | 130 | mode = chunkedEncoding and encodeChunked or encodeRaw 131 | return concat(head) 132 | end 133 | 134 | function encodeRaw(item) 135 | if type(item) ~= "string" then 136 | mode = encodeHead 137 | return encodeHead(item) 138 | end 139 | return item 140 | end 141 | 142 | function encodeChunked(item) 143 | if type(item) ~= "string" then 144 | mode = encodeHead 145 | local extra = encodeHead(item) 146 | if extra then 147 | return "0\r\n\r\n" .. extra 148 | else 149 | return "0\r\n\r\n" 150 | end 151 | end 152 | if #item == 0 then 153 | mode = encodeHead 154 | end 155 | return format("%x", #item) .. "\r\n" .. item .. "\r\n" 156 | end 157 | 158 | mode = encodeHead 159 | return function (item) 160 | return mode(item) 161 | end 162 | end 163 | 164 | local function decoder() 165 | 166 | -- This decoder is somewhat stateful with 5 different parsing states. 167 | local decodeHead, decodeEmpty, decodeRaw, decodeChunked, decodeCounted 168 | local mode -- state variable that points to various decoders 169 | local bytesLeft -- For counted decoder 170 | 171 | -- This state is for decoding the status line and headers. 172 | function decodeHead(chunk) 173 | if not chunk then return end 174 | 175 | local _, length = find(chunk, "\r?\n\r?\n", 1) 176 | -- First make sure we have all the head before continuing 177 | if not length then 178 | if #chunk < 8 * 1024 then return end 179 | -- But protect against evil clients by refusing heads over 8K long. 180 | error("entity too large") 181 | end 182 | 183 | -- Parse the status/request line 184 | local head = {} 185 | local _, offset 186 | local version 187 | _, offset, version, head.code, head.reason = 188 | find(chunk, "^HTTP/(%d%.%d) (%d+) ([^\r\n]+)\r?\n") 189 | if offset then 190 | head.code = tonumber(head.code) 191 | else 192 | _, offset, head.method, head.path, version = 193 | find(chunk, "^(%u+) ([^ ]+) HTTP/(%d%.%d)\r?\n") 194 | if not offset then 195 | error("expected HTTP data") 196 | end 197 | end 198 | version = tonumber(version) 199 | head.version = version 200 | head.keepAlive = version > 1.0 201 | 202 | -- We need to inspect some headers to know how to parse the body. 203 | local contentLength 204 | local chunkedEncoding 205 | 206 | -- Parse the header lines 207 | while true do 208 | local key, value 209 | _, offset, key, value = find(chunk, "^([^:\r\n]+): *([^\r\n]+)\r?\n", offset + 1) 210 | if not offset then break end 211 | local lowerKey = lower(key) 212 | 213 | -- Inspect a few headers and remember the values 214 | if lowerKey == "content-length" then 215 | contentLength = tonumber(value) 216 | elseif lowerKey == "transfer-encoding" then 217 | chunkedEncoding = lower(value) == "chunked" 218 | elseif lowerKey == "connection" then 219 | head.keepAlive = lower(value) == "keep-alive" 220 | end 221 | head[#head + 1] = {key, value} 222 | end 223 | 224 | if head.keepAlive and (not (chunkedEncoding or (contentLength and contentLength > 0))) 225 | or (head.method == "GET" or head.method == "HEAD") then 226 | mode = decodeEmpty 227 | elseif chunkedEncoding then 228 | mode = decodeChunked 229 | elseif contentLength then 230 | bytesLeft = contentLength 231 | mode = decodeCounted 232 | elseif not head.keepAlive then 233 | mode = decodeRaw 234 | end 235 | 236 | return head, sub(chunk, length + 1) 237 | 238 | end 239 | 240 | -- This is used for inserting a single empty string into the output string for known empty bodies 241 | function decodeEmpty(chunk) 242 | mode = decodeHead 243 | return "", chunk or "" 244 | end 245 | 246 | function decodeRaw(chunk) 247 | if not chunk then return "", "" end 248 | if #chunk == 0 then return end 249 | return chunk, "" 250 | end 251 | 252 | function decodeChunked(chunk) 253 | local len, term 254 | len, term = match(chunk, "^(%x+)(..)") 255 | if not len then return end 256 | assert(term == "\r\n") 257 | local length = tonumber(len, 16) 258 | if #chunk < length + 4 + #len then return end 259 | if length == 0 then 260 | mode = decodeHead 261 | end 262 | chunk = sub(chunk, #len + 3) 263 | assert(sub(chunk, length + 1, length + 2) == "\r\n") 264 | return sub(chunk, 1, length), sub(chunk, length + 3) 265 | end 266 | 267 | function decodeCounted(chunk) 268 | if bytesLeft == 0 then 269 | mode = decodeEmpty 270 | return mode(chunk) 271 | end 272 | local length = #chunk 273 | -- Make sure we have at least one byte to process 274 | if length == 0 then return end 275 | 276 | if length >= bytesLeft then 277 | mode = decodeEmpty 278 | end 279 | 280 | -- If the entire chunk fits, pass it all through 281 | if length <= bytesLeft then 282 | bytesLeft = bytesLeft - length 283 | return chunk, "" 284 | end 285 | 286 | return sub(chunk, 1, bytesLeft), sub(chunk, bytesLeft + 1) 287 | end 288 | 289 | -- Switch between states by changing which decoder mode points to 290 | mode = decodeHead 291 | return function (chunk) 292 | return mode(chunk) 293 | end 294 | 295 | end 296 | 297 | return { 298 | encoder = encoder, 299 | decoder = decoder, 300 | } 301 | -------------------------------------------------------------------------------- /moonmint/deps/mimetypes.lua: -------------------------------------------------------------------------------- 1 | -- mimetypes.lua 2 | -- Version 1.0.0 3 | 4 | --[[ 5 | Copyright (c) 2011 Matthew "LeafStorm" Frazier 6 | 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | ====== 29 | 30 | In addition, the MIME types contained in the Software were 31 | originally obtained from the Apache HTTP Server available under the 32 | Apache Software License, Version 2.0 license 33 | (http://directory.fsf.org/wiki/License:Apache2.0) 34 | ]] 35 | 36 | -- This table is the one that actually contains the exported functions. 37 | 38 | local mimetypes = {} 39 | 40 | mimetypes.version = '1.0.0' 41 | 42 | 43 | -- Extracts the extension from a filename and returns it. 44 | -- The extension must be at the end of the string, and preceded by a dot and 45 | -- at least one other character. Only the last part will be returned (so 46 | -- "package-1.2.tar.gz" will return "gz"). 47 | -- If there is no extension, this function will return nil. 48 | 49 | local function extension (filename) 50 | return filename:match(".+%.([%a%d]+)$") 51 | end 52 | 53 | 54 | -- Creates a deep copy of the given table. 55 | 56 | local function copy (tbl) 57 | local ntbl = {} 58 | for key, value in pairs(tbl) do 59 | if type(value) == 'table' then 60 | ntbl[key] = copy(value) 61 | else 62 | ntbl[key] = value 63 | end 64 | end 65 | return ntbl 66 | end 67 | 68 | 69 | -- This is the default MIME type database. 70 | -- It is a table with two members - "extensions" and "filenames". 71 | -- The filenames table maps complete file names (like README) to MIME types. 72 | -- The extensions just maps the files' extensions (like jpg) to types. 73 | 74 | local defaultdb = { 75 | -- The MIME types. Remember to not include the dot on the extension. 76 | extensions = { 77 | ['mods'] = 'application/mods+xml', 78 | ['hps'] = 'application/vnd.hp-hps', 79 | ['sfv'] = 'text/x-sfv', 80 | ['pcl'] = 'application/vnd.hp-pcl', 81 | ['oth'] = 'application/vnd.oasis.opendocument.text-web', 82 | ['arc'] = 'application/x-freearc', 83 | ['txd'] = 'application/vnd.genomatix.tuxedo', 84 | ['tcl'] = 'application/x-tcl', 85 | ['apr'] = 'application/vnd.lotus-approach', 86 | ['viv'] = 'video/vnd.vivo', 87 | ['wmlsc'] = 'application/vnd.wap.wmlscriptc', 88 | ['inkml'] = 'application/inkml+xml', 89 | ['sdc'] = 'application/vnd.stardivision.calc', 90 | ['dll'] = 'application/x-msdownload', 91 | ['sdkd'] = 'application/vnd.solent.sdkm+xml', 92 | ['kpr'] = 'application/vnd.kde.kpresenter', 93 | ['uvd'] = 'application/vnd.dece.data', 94 | ['gtw'] = 'model/vnd.gtw', 95 | ['svd'] = 'application/vnd.svd', 96 | ['m14'] = 'application/x-msmediaview', 97 | ['spp'] = 'application/scvp-vp-response', 98 | ['qt'] = 'video/quicktime', 99 | ['c4g'] = 'application/vnd.clonk.c4group', 100 | ['dp'] = 'application/vnd.osgi.dp', 101 | ['oda'] = 'application/oda', 102 | ['log'] = 'text/plain', 103 | ['gre'] = 'application/vnd.geometry-explorer', 104 | ['sda'] = 'application/vnd.stardivision.draw', 105 | ['rms'] = 'application/vnd.jcp.javame.midlet-rms', 106 | ['ico'] = 'image/x-icon', 107 | ['cab'] = 'application/vnd.ms-cab-compressed', 108 | ['p7c'] = 'application/pkcs7-mime', 109 | ['cb7'] = 'application/x-cbr', 110 | ['src'] = 'application/x-wais-source', 111 | ['uvf'] = 'application/vnd.dece.data', 112 | ['dms'] = 'application/octet-stream', 113 | ['ccxml'] = 'application/ccxml+xml', 114 | ['uvvd'] = 'application/vnd.dece.data', 115 | ['abw'] = 'application/x-abiword', 116 | ['gex'] = 'application/vnd.geometry-explorer', 117 | ['es3'] = 'application/vnd.eszigno3+xml', 118 | ['mmr'] = 'image/vnd.fujixerox.edmics-mmr', 119 | ['wgt'] = 'application/widget', 120 | ['mp4a'] = 'audio/mp4', 121 | ['gram'] = 'application/srgs', 122 | ['p7m'] = 'application/pkcs7-mime', 123 | ['org'] = 'application/vnd.lotus-organizer', 124 | ['silo'] = 'model/mesh', 125 | ['scq'] = 'application/scvp-cv-request', 126 | ['cxx'] = 'text/x-c', 127 | ['ots'] = 'application/vnd.oasis.opendocument.spreadsheet-template', 128 | ['fhc'] = 'image/x-freehand', 129 | ['uvvf'] = 'application/vnd.dece.data', 130 | ['jpeg'] = 'image/jpeg', 131 | ['ma'] = 'application/mathematica', 132 | ['odm'] = 'application/vnd.oasis.opendocument.text-master', 133 | ['uvvh'] = 'video/vnd.dece.hd', 134 | ['bed'] = 'application/vnd.realvnc.bed', 135 | ['doc'] = 'application/msword', 136 | ['ice'] = 'x-conference/x-cooltalk', 137 | ['fg5'] = 'application/vnd.fujitsu.oasysgp', 138 | ['ustar'] = 'application/x-ustar', 139 | ['mesh'] = 'model/mesh', 140 | ['smv'] = 'video/x-smv', 141 | ['imp'] = 'application/vnd.accpac.simply.imp', 142 | ['movie'] = 'video/x-sgi-movie', 143 | ['cmp'] = 'application/vnd.yellowriver-custom-menu', 144 | ['avi'] = 'video/x-msvideo', 145 | ['cpt'] = 'application/mac-compactpro', 146 | ['lha'] = 'application/x-lzh-compressed', 147 | ['cmdf'] = 'chemical/x-cmdf', 148 | ['wvx'] = 'video/x-ms-wvx', 149 | ['prf'] = 'application/pics-rules', 150 | ['wmx'] = 'video/x-ms-wmx', 151 | ['wmv'] = 'video/x-ms-wmv', 152 | ['ggt'] = 'application/vnd.geogebra.tool', 153 | ['kpt'] = 'application/vnd.kde.kpresenter', 154 | ['kmz'] = 'application/vnd.google-earth.kmz', 155 | ['mpt'] = 'application/vnd.ms-project', 156 | ['caf'] = 'audio/x-caf', 157 | ['mp4'] = 'video/mp4', 158 | ['dcr'] = 'application/x-director', 159 | ['wm'] = 'video/x-ms-wm', 160 | ['ai'] = 'application/postscript', 161 | ['lrf'] = 'application/octet-stream', 162 | ['qbo'] = 'application/vnd.intu.qbo', 163 | ['vob'] = 'video/x-ms-vob', 164 | ['nml'] = 'application/vnd.enliven', 165 | ['xlf'] = 'application/x-xliff+xml', 166 | ['dcurl'] = 'text/vnd.curl.dcurl', 167 | ['hpid'] = 'application/vnd.hp-hpid', 168 | ['kwd'] = 'application/vnd.kde.kword', 169 | ['asx'] = 'video/x-ms-asf', 170 | ['asf'] = 'video/x-ms-asf', 171 | ['acu'] = 'application/vnd.acucobol', 172 | ['fm'] = 'application/vnd.framemaker', 173 | ['plf'] = 'application/vnd.pocketlearn', 174 | ['mng'] = 'video/x-mng', 175 | ['gramps'] = 'application/x-gramps-xml', 176 | ['mks'] = 'video/x-matroska', 177 | ['vcard'] = 'text/vcard', 178 | ['sgml'] = 'text/sgml', 179 | ['mvb'] = 'application/x-msmediaview', 180 | ['rp9'] = 'application/vnd.cloanto.rp9', 181 | ['mkv'] = 'video/x-matroska', 182 | ['xhtml'] = 'application/xhtml+xml', 183 | ['dssc'] = 'application/dssc+der', 184 | ['m4v'] = 'video/x-m4v', 185 | ['xml'] = 'application/xml', 186 | ['fli'] = 'video/x-fli', 187 | ['mpm'] = 'application/vnd.blueice.multipass', 188 | ['pskcxml'] = 'application/pskc+xml', 189 | ['webm'] = 'video/webm', 190 | ['pml'] = 'application/vnd.ctc-posml', 191 | ['cu'] = 'application/cu-seeme', 192 | ['rm'] = 'application/vnd.rn-realmedia', 193 | ['spx'] = 'audio/ogg', 194 | ['dic'] = 'text/x-c', 195 | ['esa'] = 'application/vnd.osgi.subsystem', 196 | ['odi'] = 'application/vnd.oasis.opendocument.image', 197 | ['irm'] = 'application/vnd.ibm.rights-management', 198 | ['m4u'] = 'video/vnd.mpegurl', 199 | ['cdmid'] = 'application/cdmi-domain', 200 | ['elc'] = 'application/octet-stream', 201 | ['mxu'] = 'video/vnd.mpegurl', 202 | ['mdi'] = 'image/vnd.ms-modi', 203 | ['rld'] = 'application/resource-lists-diff+xml', 204 | ['sdw'] = 'application/vnd.stardivision.writer', 205 | ['fvt'] = 'video/vnd.fvt', 206 | ['pptx'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 207 | ['dvb'] = 'video/vnd.dvb.file', 208 | ['book'] = 'application/vnd.framemaker', 209 | ['pfx'] = 'application/x-pkcs12', 210 | ['qwd'] = 'application/vnd.quark.quarkxpress', 211 | ['nbp'] = 'application/vnd.wolfram.player', 212 | ['vcx'] = 'application/vnd.vcx', 213 | ['gpx'] = 'application/gpx+xml', 214 | ['uvvs'] = 'video/vnd.dece.sd', 215 | ['shf'] = 'application/shf+xml', 216 | ['n-gage'] = 'application/vnd.nokia.n-gage.symbian.install', 217 | ['hvd'] = 'application/vnd.yamaha.hv-dic', 218 | ['lostxml'] = 'application/lost+xml', 219 | ['knp'] = 'application/vnd.kinar', 220 | ['dvi'] = 'application/x-dvi', 221 | ['uvvp'] = 'video/vnd.dece.pd', 222 | ['psb'] = 'application/vnd.3gpp.pic-bw-small', 223 | ['asc'] = 'application/pgp-signature', 224 | ['wsdl'] = 'application/wsdl+xml', 225 | ['stk'] = 'application/hyperstudio', 226 | ['x3dbz'] = 'model/x3d+binary', 227 | ['uvvm'] = 'video/vnd.dece.mobile', 228 | ['3gp'] = 'video/3gpp', 229 | ['uvm'] = 'video/vnd.dece.mobile', 230 | ['sfd-hdstx'] = 'application/vnd.hydrostatix.sof-data', 231 | ['mpeg'] = 'video/mpeg', 232 | ['uvh'] = 'video/vnd.dece.hd', 233 | ['mov'] = 'video/quicktime', 234 | ['cla'] = 'application/vnd.claymore', 235 | ['ogv'] = 'video/ogg', 236 | ['m2v'] = 'video/mpeg', 237 | ['sse'] = 'application/vnd.kodak-descriptor', 238 | ['atomsvc'] = 'application/atomsvc+xml', 239 | ['djv'] = 'image/vnd.djvu', 240 | ['fe_launch'] = 'application/vnd.denovo.fcselayout-link', 241 | ['mpe'] = 'video/mpeg', 242 | ['x3dz'] = 'model/x3d+xml', 243 | ['et3'] = 'application/vnd.eszigno3+xml', 244 | ['u32'] = 'application/x-authorware-bin', 245 | ['mpg'] = 'video/mpeg', 246 | ['mpg4'] = 'video/mp4', 247 | ['mp4v'] = 'video/mp4', 248 | ['nsf'] = 'application/vnd.lotus-notes', 249 | ['dwg'] = 'image/vnd.dwg', 250 | ['teicorpus'] = 'application/tei+xml', 251 | ['sus'] = 'application/vnd.sus-calendar', 252 | ['uvvz'] = 'application/vnd.dece.zip', 253 | ['uvvx'] = 'application/vnd.dece.unspecified', 254 | ['psd'] = 'image/vnd.adobe.photoshop', 255 | ['ami'] = 'application/vnd.amiga.ami', 256 | ['nb'] = 'application/mathematica', 257 | ['nzb'] = 'application/x-nzb', 258 | ['tsd'] = 'application/timestamped-data', 259 | ['f'] = 'text/x-fortran', 260 | ['c11amc'] = 'application/vnd.cluetrust.cartomobile-config', 261 | ['torrent'] = 'application/x-bittorrent', 262 | ['cdkey'] = 'application/vnd.mediastation.cdkey', 263 | ['mj2'] = 'video/mj2', 264 | ['nns'] = 'application/vnd.noblenet-sealer', 265 | ['jpgm'] = 'video/jpm', 266 | ['jpm'] = 'video/jpm', 267 | ['jpgv'] = 'video/jpeg', 268 | ['h264'] = 'video/h264', 269 | ['mus'] = 'application/vnd.musician', 270 | ['ppsm'] = 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', 271 | ['appcache'] = 'text/cache-manifest', 272 | ['rpss'] = 'application/vnd.nokia.radio-presets', 273 | ['smi'] = 'application/smil+xml', 274 | ['h263'] = 'video/h263', 275 | ['h261'] = 'video/h261', 276 | ['3g2'] = 'video/3gpp2', 277 | ['rtx'] = 'text/richtext', 278 | ['vcf'] = 'text/x-vcard', 279 | ['texi'] = 'application/x-texinfo', 280 | ['cdbcmsg'] = 'application/vnd.contact.cmsg', 281 | ['uu'] = 'text/x-uuencode', 282 | ['nsc'] = 'application/x-conference', 283 | ['pic'] = 'image/x-pict', 284 | ['cil'] = 'application/vnd.ms-artgalry', 285 | ['t'] = 'text/troff', 286 | ['psf'] = 'application/x-font-linux-psf', 287 | ['p'] = 'text/x-pascal', 288 | ['vsf'] = 'application/vnd.vsf', 289 | ['odc'] = 'application/vnd.oasis.opendocument.chart', 290 | ['lbe'] = 'application/vnd.llamagraphics.life-balance.exchange+xml', 291 | ['java'] = 'text/x-java-source', 292 | ['tfm'] = 'application/x-tex-tfm', 293 | ['cpio'] = 'application/x-cpio', 294 | ['gnumeric'] = 'application/x-gnumeric', 295 | ['for'] = 'text/x-fortran', 296 | ['uvu'] = 'video/vnd.uvvu.mp4', 297 | ['qxt'] = 'application/vnd.quark.quarkxpress', 298 | ['fxpl'] = 'application/vnd.adobe.fxp', 299 | ['hh'] = 'text/x-c', 300 | ['uvi'] = 'image/vnd.dece.graphic', 301 | ['cpp'] = 'text/x-c', 302 | ['x3db'] = 'model/x3d+binary', 303 | ['obd'] = 'application/x-msbinder', 304 | ['cc'] = 'text/x-c', 305 | ['rtf'] = 'application/rtf', 306 | ['ddd'] = 'application/vnd.fujixerox.ddd', 307 | ['ttf'] = 'application/x-font-ttf', 308 | ['iif'] = 'application/vnd.shana.informed.interchange', 309 | ['tao'] = 'application/vnd.tao.intent-module-archive', 310 | ['potm'] = 'application/vnd.ms-powerpoint.template.macroenabled.12', 311 | ['oxt'] = 'application/vnd.openofficeorg.extension', 312 | ['mif'] = 'application/vnd.mif', 313 | ['mk3d'] = 'video/x-matroska', 314 | ['mrc'] = 'application/marc', 315 | ['cxt'] = 'application/x-director', 316 | ['thmx'] = 'application/vnd.ms-officetheme', 317 | ['ext'] = 'application/vnd.novadigm.ext', 318 | ['tr'] = 'text/troff', 319 | ['gxt'] = 'application/vnd.geonext', 320 | ['rcprofile'] = 'application/vnd.ipunplugged.rcprofile', 321 | ['xlsx'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 322 | ['wmls'] = 'text/vnd.wap.wmlscript', 323 | ['wml'] = 'text/vnd.wap.wml', 324 | ['dpg'] = 'application/vnd.dpgraph', 325 | ['wri'] = 'application/x-mswrite', 326 | ['cif'] = 'chemical/x-cif', 327 | ['wks'] = 'application/vnd.ms-works', 328 | ['ods'] = 'application/vnd.oasis.opendocument.spreadsheet', 329 | ['lasxml'] = 'application/vnd.las.las+xml', 330 | ['jpg'] = 'image/jpeg', 331 | ['ltf'] = 'application/vnd.frogans.ltf', 332 | ['spot'] = 'text/vnd.in3d.spot', 333 | ['3dml'] = 'text/vnd.in3d.3dml', 334 | ['curl'] = 'text/vnd.curl', 335 | ['xdp'] = 'application/vnd.adobe.xdp+xml', 336 | ['flx'] = 'text/vnd.fmi.flexstor', 337 | ['fly'] = 'text/vnd.fly', 338 | ['uvva'] = 'audio/vnd.dece.audio', 339 | ['p7r'] = 'application/x-pkcs7-certreqresp', 340 | ['cryptonote'] = 'application/vnd.rig.cryptonote', 341 | ['cdmio'] = 'application/cdmi-object', 342 | ['igx'] = 'application/vnd.micrografx.igx', 343 | ['smzip'] = 'application/vnd.stepmania.package', 344 | ['ogx'] = 'application/ogg', 345 | ['scs'] = 'application/scvp-cv-response', 346 | ['gv'] = 'text/vnd.graphviz', 347 | ['urls'] = 'text/uri-list', 348 | ['otc'] = 'application/vnd.oasis.opendocument.chart-template', 349 | ['png'] = 'image/png', 350 | ['uri'] = 'text/uri-list', 351 | ['ttl'] = 'text/turtle', 352 | ['metalink'] = 'application/metalink+xml', 353 | ['cmc'] = 'application/vnd.cosmocaller', 354 | ['ahead'] = 'application/vnd.ahead.space', 355 | ['taglet'] = 'application/vnd.mynfc', 356 | ['list'] = 'text/plain', 357 | ['dae'] = 'model/vnd.collada+xml', 358 | ['me'] = 'text/troff', 359 | ['srt'] = 'application/x-subrip', 360 | ['fgd'] = 'application/x-director', 361 | ['man'] = 'text/troff', 362 | ['jpe'] = 'image/jpeg', 363 | ['clkw'] = 'application/vnd.crick.clicker.wordbank', 364 | ['mfm'] = 'application/vnd.mfmp', 365 | ['pfb'] = 'application/x-font-type1', 366 | ['roff'] = 'text/troff', 367 | ['dtd'] = 'application/xml-dtd', 368 | ['ifm'] = 'application/vnd.shana.informed.formdata', 369 | ['pas'] = 'text/x-pascal', 370 | ['bz'] = 'application/x-bzip', 371 | ['ras'] = 'image/x-cmu-raster', 372 | ['car'] = 'application/vnd.curl.car', 373 | ['list3820'] = 'application/vnd.ibm.modcap', 374 | ['mgz'] = 'application/vnd.proteus.magazine', 375 | ['crt'] = 'application/x-x509-ca-cert', 376 | ['sbml'] = 'application/sbml+xml', 377 | ['pcurl'] = 'application/vnd.curl.pcurl', 378 | ['dbk'] = 'application/docbook+xml', 379 | ['sgm'] = 'text/sgml', 380 | ['ac'] = 'application/pkix-attr-cert', 381 | ['opf'] = 'application/oebps-package+xml', 382 | ['mar'] = 'application/octet-stream', 383 | ['asm'] = 'text/x-asm', 384 | ['wav'] = 'audio/x-wav', 385 | ['x3dvz'] = 'model/x3d+vrml', 386 | ['bpk'] = 'application/octet-stream', 387 | ['dtb'] = 'application/x-dtbook+xml', 388 | ['dsc'] = 'text/prs.lines.tag', 389 | ['wpd'] = 'application/vnd.wordperfect', 390 | ['xsl'] = 'application/xml', 391 | ['in'] = 'text/plain', 392 | ['stc'] = 'application/vnd.sun.xml.calc.template', 393 | ['musicxml'] = 'application/vnd.recordare.musicxml+xml', 394 | ['sc'] = 'application/vnd.ibm.secure-container', 395 | ['def'] = 'text/plain', 396 | ['uvs'] = 'video/vnd.dece.sd', 397 | ['wma'] = 'audio/x-ms-wma', 398 | ['text'] = 'text/plain', 399 | ['obj'] = 'application/x-tgif', 400 | ['cap'] = 'application/vnd.tcpdump.pcap', 401 | ['lvp'] = 'audio/vnd.lucent.voice', 402 | ['kfo'] = 'application/vnd.kde.kformula', 403 | ['sitx'] = 'application/x-stuffitx', 404 | ['jnlp'] = 'application/x-java-jnlp-file', 405 | ['rmvb'] = 'application/vnd.rn-realmedia-vbr', 406 | ['blb'] = 'application/x-blorb', 407 | ['evy'] = 'application/x-envoy', 408 | ['htm'] = 'text/html', 409 | ['html'] = 'text/html', 410 | ['sv4cpio'] = 'application/x-sv4cpio', 411 | ['mpkg'] = 'application/vnd.apple.installer+xml', 412 | ['xer'] = 'application/patch-ops-error+xml', 413 | ['cdmic'] = 'application/cdmi-container', 414 | ['csv'] = 'text/csv', 415 | ['aso'] = 'application/vnd.accpac.simply.aso', 416 | ['css'] = 'text/css', 417 | ['pki'] = 'application/pkixcmp', 418 | ['sdkm'] = 'application/vnd.solent.sdkm+xml', 419 | ['ifb'] = 'text/calendar', 420 | ['mcd'] = 'application/vnd.mcd', 421 | ['nnd'] = 'application/vnd.noblenet-directory', 422 | ['pbm'] = 'image/x-portable-bitmap', 423 | ['srx'] = 'application/sparql-results+xml', 424 | ['cdmiq'] = 'application/cdmi-queue', 425 | ['qxl'] = 'application/vnd.quark.quarkxpress', 426 | ['ics'] = 'text/calendar', 427 | ['icm'] = 'application/vnd.iccprofile', 428 | ['x3dv'] = 'model/x3d+vrml', 429 | ['vrml'] = 'model/vrml', 430 | ['xbm'] = 'image/x-xbitmap', 431 | ['spl'] = 'application/x-futuresplash', 432 | ['gtm'] = 'application/vnd.groove-tool-message', 433 | ['paw'] = 'application/vnd.pawaafile', 434 | ['csp'] = 'application/vnd.commonspace', 435 | ['jlt'] = 'application/vnd.hp-jlyt', 436 | ['rip'] = 'audio/vnd.rip', 437 | ['mxl'] = 'application/vnd.recordare.musicxml', 438 | ['rep'] = 'application/vnd.businessobjects', 439 | ['mts'] = 'model/vnd.mts', 440 | ['mads'] = 'application/mads+xml', 441 | ['gdl'] = 'model/vnd.gdl', 442 | ['ksp'] = 'application/vnd.kde.kspread', 443 | ['dwf'] = 'model/vnd.dwf', 444 | ['ms'] = 'text/troff', 445 | ['udeb'] = 'application/x-debian-package', 446 | ['iges'] = 'model/iges', 447 | ['fcdt'] = 'application/vnd.adobe.formscentral.fcdt', 448 | ['xo'] = 'application/vnd.olpc-sugar', 449 | ['xlt'] = 'application/vnd.ms-excel', 450 | ['gtar'] = 'application/x-gtar', 451 | ['p12'] = 'application/x-pkcs12', 452 | ['gqf'] = 'application/vnd.grafeq', 453 | ['wbmp'] = 'image/vnd.wap.wbmp', 454 | ['mime'] = 'message/rfc822', 455 | ['sv4crc'] = 'application/x-sv4crc', 456 | ['xwd'] = 'image/x-xwindowdump', 457 | ['qxb'] = 'application/vnd.quark.quarkxpress', 458 | ['xpm'] = 'image/x-xpixmap', 459 | ['tga'] = 'image/x-tga', 460 | ['seed'] = 'application/vnd.fdsn.seed', 461 | ['rss'] = 'application/rss+xml', 462 | ['ppm'] = 'image/x-portable-pixmap', 463 | ['fsc'] = 'application/vnd.fsc.weblaunch', 464 | ['cat'] = 'application/vnd.ms-pki.seccat', 465 | ['mwf'] = 'application/vnd.mfer', 466 | ['pnm'] = 'image/x-portable-anymap', 467 | ['pct'] = 'image/x-pict', 468 | ['pcx'] = 'image/x-pcx', 469 | ['gph'] = 'application/vnd.flographit', 470 | ['sid'] = 'image/x-mrsid-image', 471 | ['class'] = 'application/java-vm', 472 | ['c4p'] = 'application/vnd.clonk.c4group', 473 | ['mp3'] = 'audio/mpeg', 474 | ['uvvi'] = 'image/vnd.dece.graphic', 475 | ['fh4'] = 'image/x-freehand', 476 | ['rgb'] = 'image/x-rgb', 477 | ['vcg'] = 'application/vnd.groove-vcard', 478 | ['cmx'] = 'image/x-cmx', 479 | ['3ds'] = 'image/x-3ds', 480 | ['cdy'] = 'application/vnd.cinderella', 481 | ['webp'] = 'image/webp', 482 | ['xif'] = 'image/vnd.xiff', 483 | ['p10'] = 'application/pkcs10', 484 | ['wax'] = 'audio/x-ms-wax', 485 | ['npx'] = 'image/vnd.net-fpx', 486 | ['ufdl'] = 'application/vnd.ufdl', 487 | ['rlc'] = 'image/vnd.fujixerox.edmics-rlc', 488 | ['fst'] = 'image/vnd.fst', 489 | ['setreg'] = 'application/set-registration-initiation', 490 | ['mbox'] = 'application/mbox', 491 | ['kpxx'] = 'application/vnd.ds-keypoint', 492 | ['fbs'] = 'image/vnd.fastbidsheet', 493 | ['z1'] = 'application/x-zmachine', 494 | ['weba'] = 'audio/webm', 495 | ['clkt'] = 'application/vnd.crick.clicker.template', 496 | ['m13'] = 'application/x-msmediaview', 497 | ['cww'] = 'application/prs.cww', 498 | ['sxd'] = 'application/vnd.sun.xml.draw', 499 | ['xpw'] = 'application/vnd.intercon.formnet', 500 | ['dd2'] = 'application/vnd.oma.dd2+xml', 501 | ['odf'] = 'application/vnd.oasis.opendocument.formula', 502 | ['fzs'] = 'application/vnd.fuzzysheet', 503 | ['portpkg'] = 'application/vnd.macports.portpkg', 504 | ['oti'] = 'application/vnd.oasis.opendocument.image-template', 505 | ['hlp'] = 'application/winhlp', 506 | ['cst'] = 'application/x-director', 507 | ['mpn'] = 'application/vnd.mophun.application', 508 | ['xvm'] = 'application/xv+xml', 509 | ['cgm'] = 'image/cgm', 510 | ['fh5'] = 'image/x-freehand', 511 | ['h'] = 'text/x-c', 512 | ['xar'] = 'application/vnd.xara', 513 | ['vis'] = 'application/vnd.visionary', 514 | ['wqd'] = 'application/vnd.wqd', 515 | ['cbr'] = 'application/x-cbr', 516 | ['svgz'] = 'image/svg+xml', 517 | ['wpl'] = 'application/vnd.ms-wpl', 518 | ['lwp'] = 'application/vnd.lotus-wordpro', 519 | ['igm'] = 'application/vnd.insors.igm', 520 | ['flo'] = 'application/vnd.micrografx.flo', 521 | ['zirz'] = 'application/vnd.zul', 522 | ['svg'] = 'image/svg+xml', 523 | ['m3u8'] = 'application/vnd.apple.mpegurl', 524 | ['dra'] = 'audio/vnd.dra', 525 | ['oprc'] = 'application/vnd.palm', 526 | ['vxml'] = 'application/voicexml+xml', 527 | ['dis'] = 'application/vnd.mobius.dis', 528 | ['qps'] = 'application/vnd.publishare-delta-tree', 529 | ['vsw'] = 'application/vnd.visio', 530 | ['ntf'] = 'application/vnd.nitf', 531 | ['btif'] = 'image/prs.btif', 532 | ['uris'] = 'text/uri-list', 533 | ['mxf'] = 'application/mxf', 534 | ['wps'] = 'application/vnd.ms-works', 535 | ['aas'] = 'application/x-authorware-seg', 536 | ['c4d'] = 'application/vnd.clonk.c4group', 537 | ['zip'] = 'application/zip', 538 | ['gif'] = 'image/gif', 539 | ['susp'] = 'application/vnd.sus-calendar', 540 | ['air'] = 'application/vnd.adobe.air-application-installer-package+zip', 541 | ['ris'] = 'application/x-research-info-systems', 542 | ['pgn'] = 'application/x-chess-pgn', 543 | ['gxf'] = 'application/gxf', 544 | ['g3'] = 'image/g3fax', 545 | ['sdd'] = 'application/vnd.stardivision.impress', 546 | ['bmp'] = 'image/bmp', 547 | ['otg'] = 'application/vnd.oasis.opendocument.graphics-template', 548 | ['eps'] = 'application/postscript', 549 | ['xyz'] = 'chemical/x-xyz', 550 | ['sema'] = 'application/vnd.sema', 551 | ['csml'] = 'chemical/x-csml', 552 | ['swf'] = 'application/x-shockwave-flash', 553 | ['qxd'] = 'application/vnd.quark.quarkxpress', 554 | ['cdx'] = 'chemical/x-cdx', 555 | ['123'] = 'application/vnd.lotus-1-2-3', 556 | ['dotx'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 557 | ['xm'] = 'audio/xm', 558 | ['spq'] = 'application/scvp-vp-request', 559 | ['jisp'] = 'application/vnd.jisp', 560 | ['rmp'] = 'audio/x-pn-realaudio-plugin', 561 | ['tex'] = 'application/x-tex', 562 | ['xlm'] = 'application/vnd.ms-excel', 563 | ['ppam'] = 'application/vnd.ms-powerpoint.addin.macroenabled.12', 564 | ['xht'] = 'application/xhtml+xml', 565 | ['odb'] = 'application/vnd.oasis.opendocument.database', 566 | ['ram'] = 'audio/x-pn-realaudio', 567 | ['xfdl'] = 'application/vnd.xfdl', 568 | ['mka'] = 'audio/x-matroska', 569 | ['flac'] = 'audio/x-flac', 570 | ['aifc'] = 'audio/x-aiff', 571 | ['sfs'] = 'application/vnd.spotfire.sfs', 572 | ['rl'] = 'application/resource-lists+xml', 573 | ['wdb'] = 'application/vnd.ms-works', 574 | ['tra'] = 'application/vnd.trueapp', 575 | ['dfac'] = 'application/vnd.dreamfactory', 576 | ['aif'] = 'audio/x-aiff', 577 | ['mp2'] = 'audio/mpeg', 578 | ['vtu'] = 'model/vnd.vtu', 579 | ['mny'] = 'application/x-msmoney', 580 | ['ecelp7470'] = 'audio/vnd.nuera.ecelp7470', 581 | ['ppsx'] = 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 582 | ['kon'] = 'application/vnd.kde.kontour', 583 | ['odp'] = 'application/vnd.oasis.opendocument.presentation', 584 | ['twds'] = 'application/vnd.simtech-mindmapper', 585 | ['dtshd'] = 'audio/vnd.dts.hd', 586 | ['dts'] = 'audio/vnd.dts', 587 | ['sit'] = 'application/x-stuffit', 588 | ['ipfix'] = 'application/ipfix', 589 | ['sgi'] = 'image/sgi', 590 | ['application'] = 'application/x-ms-application', 591 | ['roa'] = 'application/rpki-roa', 592 | ['xaml'] = 'application/xaml+xml', 593 | ['sil'] = 'audio/silk', 594 | ['smil'] = 'application/smil+xml', 595 | ['s3m'] = 'audio/s3m', 596 | ['ogg'] = 'audio/ogg', 597 | ['chm'] = 'application/vnd.ms-htmlhelp', 598 | ['mscml'] = 'application/mediaservercontrol+xml', 599 | ['atom'] = 'application/atom+xml', 600 | ['eva'] = 'application/x-eva', 601 | ['oga'] = 'audio/ogg', 602 | ['prc'] = 'application/x-mobipocket-ebook', 603 | ['sldm'] = 'application/vnd.ms-powerpoint.slide.macroenabled.12', 604 | ['m2a'] = 'audio/mpeg', 605 | ['frame'] = 'application/vnd.framemaker', 606 | ['fh7'] = 'image/x-freehand', 607 | ['mp2a'] = 'audio/mpeg', 608 | ['aac'] = 'audio/x-aac', 609 | ['mpga'] = 'audio/mpeg', 610 | ['rmi'] = 'audio/midi', 611 | ['m21'] = 'application/mp21', 612 | ['kar'] = 'audio/midi', 613 | ['p7b'] = 'application/x-pkcs7-certificates', 614 | ['gac'] = 'application/vnd.groove-account', 615 | ['midi'] = 'audio/midi', 616 | ['mid'] = 'audio/midi', 617 | ['ott'] = 'application/vnd.oasis.opendocument.text-template', 618 | ['rif'] = 'application/reginfo+xml', 619 | ['cfs'] = 'application/x-cfs-compressed', 620 | ['au'] = 'audio/basic', 621 | ['adp'] = 'audio/adpcm', 622 | ['pfr'] = 'application/font-tdpfr', 623 | ['ief'] = 'image/ief', 624 | ['xul'] = 'application/vnd.mozilla.xul+xml', 625 | ['dart'] = 'application/vnd.dart', 626 | ['yin'] = 'application/yin+xml', 627 | ['pya'] = 'audio/vnd.ms-playready.media.pya', 628 | ['uvg'] = 'image/vnd.dece.graphic', 629 | ['str'] = 'application/vnd.pg.format', 630 | ['tiff'] = 'image/tiff', 631 | ['crd'] = 'application/x-mscardfile', 632 | ['c4f'] = 'application/vnd.clonk.c4group', 633 | ['xhvml'] = 'application/xv+xml', 634 | ['ssdl'] = 'application/ssdl+xml', 635 | ['mxml'] = 'application/xv+xml', 636 | ['g2w'] = 'application/vnd.geoplan', 637 | ['xslt'] = 'application/xslt+xml', 638 | ['nnw'] = 'application/vnd.noblenet-web', 639 | ['xop'] = 'application/xop+xml', 640 | ['flv'] = 'video/x-flv', 641 | ['ra'] = 'audio/x-pn-realaudio', 642 | ['tpt'] = 'application/vnd.trid.tpt', 643 | ['bdm'] = 'application/vnd.syncml.dm+wbxml', 644 | ['davmount'] = 'application/davmount+xml', 645 | ['z6'] = 'application/x-zmachine', 646 | ['xenc'] = 'application/xenc+xml', 647 | ['yang'] = 'application/yang', 648 | ['uva'] = 'audio/vnd.dece.audio', 649 | ['ez2'] = 'application/vnd.ezpix-album', 650 | ['dmp'] = 'application/vnd.tcpdump.pcap', 651 | ['mets'] = 'application/mets+xml', 652 | ['z8'] = 'application/x-zmachine', 653 | ['xfdf'] = 'application/vnd.adobe.xfdf', 654 | ['fcs'] = 'application/vnd.isac.fcs', 655 | ['z7'] = 'application/x-zmachine', 656 | ['xdssc'] = 'application/dssc+xml', 657 | ['z5'] = 'application/x-zmachine', 658 | ['z4'] = 'application/x-zmachine', 659 | ['daf'] = 'application/vnd.mobius.daf', 660 | ['sql'] = 'application/x-sql', 661 | ['z3'] = 'application/x-zmachine', 662 | ['z2'] = 'application/x-zmachine', 663 | ['uvvt'] = 'application/vnd.dece.ttml+xml', 664 | ['xz'] = 'application/x-xz', 665 | ['xdm'] = 'application/vnd.syncml.dm+xml', 666 | ['xpi'] = 'application/x-xpinstall', 667 | ['fig'] = 'application/x-xfig', 668 | ['der'] = 'application/x-x509-ca-cert', 669 | ['chrt'] = 'application/vnd.kde.kchart', 670 | ['vcs'] = 'text/x-vcalendar', 671 | ['pgp'] = 'application/pgp-encrypted', 672 | ['onetmp'] = 'application/onenote', 673 | ['mjp2'] = 'video/mj2', 674 | ['pvb'] = 'application/vnd.3gpp.pic-bw-var', 675 | ['f90'] = 'text/x-fortran', 676 | ['tar'] = 'application/x-tar', 677 | ['hvp'] = 'application/vnd.yamaha.hv-voice', 678 | ['afm'] = 'application/x-font-type1', 679 | ['semd'] = 'application/vnd.semd', 680 | ['mp4s'] = 'application/mp4', 681 | ['eml'] = 'message/rfc822', 682 | ['otp'] = 'application/vnd.oasis.opendocument.presentation-template', 683 | ['gml'] = 'application/gml+xml', 684 | ['itp'] = 'application/vnd.shana.informed.formtemplate', 685 | ['st'] = 'application/vnd.sailingtracker.track', 686 | ['svc'] = 'application/vnd.dvb.service', 687 | ['cml'] = 'chemical/x-cml', 688 | ['pkipath'] = 'application/pkix-pkipath', 689 | ['sh'] = 'application/x-sh', 690 | ['spf'] = 'application/vnd.yamaha.smaf-phrase', 691 | ['mcurl'] = 'text/vnd.curl.mcurl', 692 | ['spc'] = 'application/x-pkcs7-certificates', 693 | ['rq'] = 'application/sparql-query', 694 | ['uvvv'] = 'video/vnd.dece.video', 695 | ['cdf'] = 'application/x-netcdf', 696 | ['atomcat'] = 'application/atomcat+xml', 697 | ['jad'] = 'text/vnd.sun.j2me.app-descriptor', 698 | ['gqs'] = 'application/vnd.grafeq', 699 | ['bin'] = 'application/octet-stream', 700 | ['m3u'] = 'audio/x-mpegurl', 701 | ['scd'] = 'application/x-msschedule', 702 | ['pub'] = 'application/x-mspublisher', 703 | ['sm'] = 'application/vnd.stepmania.stepchart', 704 | ['ecelp9600'] = 'audio/vnd.nuera.ecelp9600', 705 | ['mpy'] = 'application/vnd.ibm.minipay', 706 | ['xlam'] = 'application/vnd.ms-excel.addin.macroenabled.12', 707 | ['vox'] = 'application/x-authorware-bin', 708 | ['emf'] = 'application/x-msmetafile', 709 | ['mc1'] = 'application/vnd.medcalcdata', 710 | ['zmm'] = 'application/vnd.handheld-entertainment+xml', 711 | ['mxs'] = 'application/vnd.triscape.mxs', 712 | ['msi'] = 'application/x-msdownload', 713 | ['mseq'] = 'application/vnd.mseq', 714 | ['emma'] = 'application/emma+xml', 715 | ['bat'] = 'application/x-msdownload', 716 | ['com'] = 'application/x-msdownload', 717 | ['exe'] = 'application/x-msdownload', 718 | ['xpx'] = 'application/vnd.intercon.formnet', 719 | ['twd'] = 'application/vnd.simtech-mindmapper', 720 | ['clp'] = 'application/x-msclip', 721 | ['mdb'] = 'application/x-msaccess', 722 | ['xbap'] = 'application/x-ms-xbap', 723 | ['xlc'] = 'application/vnd.ms-excel', 724 | ['fpx'] = 'image/vnd.fpx', 725 | ['wmd'] = 'application/x-ms-wmd', 726 | ['lnk'] = 'application/x-ms-shortcut', 727 | ['lrm'] = 'application/vnd.ms-lrm', 728 | ['latex'] = 'application/x-latex', 729 | ['xdw'] = 'application/vnd.fujixerox.docuworks', 730 | ['dump'] = 'application/octet-stream', 731 | ['plc'] = 'application/vnd.mobius.plc', 732 | ['cdxml'] = 'application/vnd.chemdraw+xml', 733 | ['umj'] = 'application/vnd.umajin', 734 | ['osfpvg'] = 'application/vnd.yamaha.openscoreformat.osfpvg+xml', 735 | ['mobi'] = 'application/x-mobipocket-ebook', 736 | ['dxr'] = 'application/x-director', 737 | ['mie'] = 'application/x-mie', 738 | ['ei6'] = 'application/vnd.pg.osasli', 739 | ['lzh'] = 'application/x-lzh-compressed', 740 | ['dxp'] = 'application/vnd.spotfire.dxp', 741 | ['iso'] = 'application/x-iso9660-image', 742 | ['install'] = 'application/x-install-instructions', 743 | ['hdf'] = 'application/x-hdf', 744 | ['igs'] = 'model/iges', 745 | ['ims'] = 'application/vnd.ms-ims', 746 | ['dist'] = 'application/octet-stream', 747 | ['efif'] = 'application/vnd.picsel', 748 | ['wrl'] = 'model/vrml', 749 | ['esf'] = 'application/vnd.epson.esf', 750 | ['link66'] = 'application/vnd.route66.link66+xml', 751 | ['woff'] = 'application/font-woff', 752 | ['gim'] = 'application/vnd.groove-identity-message', 753 | ['t3'] = 'application/x-t3vm-image', 754 | ['mp21'] = 'application/mp21', 755 | ['xla'] = 'application/vnd.ms-excel', 756 | ['odft'] = 'application/vnd.oasis.opendocument.formula-template', 757 | ['msf'] = 'application/vnd.epson.msf', 758 | ['ktr'] = 'application/vnd.kahootz', 759 | ['box'] = 'application/vnd.previewsystems.box', 760 | ['crl'] = 'application/pkix-crl', 761 | ['skp'] = 'application/vnd.koan', 762 | ['ttc'] = 'application/x-font-ttf', 763 | ['osf'] = 'application/vnd.yamaha.openscoreformat', 764 | ['oa2'] = 'application/vnd.fujitsu.oasys2', 765 | ['pcf'] = 'application/x-font-pcf', 766 | ['fxp'] = 'application/vnd.adobe.fxp', 767 | ['trm'] = 'application/x-msterminal', 768 | ['oxps'] = 'application/oxps', 769 | ['gsf'] = 'application/x-font-ghostscript', 770 | ['swa'] = 'application/x-director', 771 | ['mb'] = 'application/mathematica', 772 | ['bdf'] = 'application/x-font-bdf', 773 | ['mmf'] = 'application/vnd.smaf', 774 | ['ufd'] = 'application/vnd.ufdl', 775 | ['n3'] = 'text/n3', 776 | ['rdz'] = 'application/vnd.data-vision.rdz', 777 | ['pot'] = 'application/vnd.ms-powerpoint', 778 | ['ncx'] = 'application/x-dtbncx+xml', 779 | ['wad'] = 'application/x-doom', 780 | ['w3d'] = 'application/x-director', 781 | ['mpc'] = 'application/vnd.mophun.certificate', 782 | ['ktz'] = 'application/vnd.kahootz', 783 | ['cct'] = 'application/x-director', 784 | ['dir'] = 'application/x-director', 785 | ['distz'] = 'application/octet-stream', 786 | ['rpst'] = 'application/vnd.nokia.radio-preset', 787 | ['csh'] = 'application/x-csh', 788 | ['skd'] = 'application/vnd.koan', 789 | ['jsonml'] = 'application/jsonml+json', 790 | ['sisx'] = 'application/vnd.symbian.install', 791 | ['boz'] = 'application/x-bzip2', 792 | ['deploy'] = 'application/octet-stream', 793 | ['msh'] = 'model/mesh', 794 | ['epub'] = 'application/epub+zip', 795 | ['smf'] = 'application/vnd.stardivision.math', 796 | ['dmg'] = 'application/x-apple-diskimage', 797 | ['deb'] = 'application/x-debian-package', 798 | ['f77'] = 'text/x-fortran', 799 | ['ssf'] = 'application/vnd.epson.ssf', 800 | ['etx'] = 'text/x-setext', 801 | ['chat'] = 'application/x-chat', 802 | ['shar'] = 'application/x-shar', 803 | ['wbxml'] = 'application/vnd.wap.wbxml', 804 | ['atx'] = 'application/vnd.antix.game-component', 805 | ['unityweb'] = 'application/vnd.unity', 806 | ['cbz'] = 'application/x-cbr', 807 | ['cbt'] = 'application/x-cbr', 808 | ['ait'] = 'application/vnd.dvb.ait', 809 | ['cba'] = 'application/x-cbr', 810 | ['bz2'] = 'application/x-bzip2', 811 | ['blorb'] = 'application/x-blorb', 812 | ['txt'] = 'text/plain', 813 | ['bcpio'] = 'application/x-bcpio', 814 | ['ktx'] = 'image/ktx', 815 | ['scm'] = 'application/vnd.lotus-screencam', 816 | ['lbd'] = 'application/vnd.llamagraphics.life-balance.desktop', 817 | ['mmd'] = 'application/vnd.chipnuts.karaoke-mmd', 818 | ['x32'] = 'application/x-authorware-bin', 819 | ['ecma'] = 'application/ecmascript', 820 | ['pbd'] = 'application/vnd.powerbuilder6', 821 | ['docx'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 822 | ['stf'] = 'application/vnd.wt.stf', 823 | ['hal'] = 'application/vnd.hal+xml', 824 | ['aab'] = 'application/x-authorware-bin', 825 | ['json'] = 'application/json', 826 | ['uvt'] = 'application/vnd.dece.ttml+xml', 827 | ['pfa'] = 'application/x-font-type1', 828 | ['xbd'] = 'application/vnd.fujixerox.docuworks.binder', 829 | ['7z'] = 'application/x-7z-compressed', 830 | ['wspolicy'] = 'application/wspolicy+xml', 831 | ['zaz'] = 'application/vnd.zzazz.deck+xml', 832 | ['aam'] = 'application/x-authorware-map', 833 | ['rar'] = 'application/x-rar-compressed', 834 | ['saf'] = 'application/vnd.yamaha.smaf-audio', 835 | ['exi'] = 'application/exi', 836 | ['azf'] = 'application/vnd.airzip.filesecure.azf', 837 | ['msty'] = 'application/vnd.muvee.style', 838 | ['gam'] = 'application/x-tads', 839 | ['grxml'] = 'application/srgs+xml', 840 | ['ghf'] = 'application/vnd.groove-help', 841 | ['conf'] = 'text/plain', 842 | ['otf'] = 'application/x-font-otf', 843 | ['hqx'] = 'application/mac-binhex40', 844 | ['ssml'] = 'application/ssml+xml', 845 | ['tif'] = 'image/tiff', 846 | ['xvml'] = 'application/xv+xml', 847 | ['pkg'] = 'application/octet-stream', 848 | ['qam'] = 'application/vnd.epson.quickanime', 849 | ['bmi'] = 'application/vnd.bmi', 850 | ['pcap'] = 'application/vnd.tcpdump.pcap', 851 | ['ftc'] = 'application/vnd.fluxtime.clip', 852 | ['wmlc'] = 'application/vnd.wap.wmlc', 853 | ['vss'] = 'application/vnd.visio', 854 | ['std'] = 'application/vnd.sun.xml.draw.template', 855 | ['vst'] = 'application/vnd.visio', 856 | ['vsd'] = 'application/vnd.visio', 857 | ['msl'] = 'application/vnd.mobius.msl', 858 | ['uvv'] = 'video/vnd.dece.video', 859 | ['uoml'] = 'application/vnd.uoml+xml', 860 | ['sig'] = 'application/pgp-signature', 861 | ['utz'] = 'application/vnd.uiq.theme', 862 | ['wdp'] = 'image/vnd.ms-photo', 863 | ['tmo'] = 'application/vnd.tmobile-livetv', 864 | ['mqy'] = 'application/vnd.mobius.mqy', 865 | ['skt'] = 'application/vnd.koan', 866 | ['sdp'] = 'application/sdp', 867 | ['vor'] = 'application/vnd.stardivision.writer', 868 | ['geo'] = 'application/vnd.dynageo', 869 | ['sru'] = 'application/sru+xml', 870 | ['tei'] = 'application/tei+xml', 871 | ['xap'] = 'application/x-silverlight-app', 872 | ['c'] = 'text/x-c', 873 | ['ez'] = 'application/andrew-inset', 874 | ['ink'] = 'application/inkml+xml', 875 | ['edm'] = 'application/vnd.novadigm.edm', 876 | ['gca'] = 'application/x-gca-compressed', 877 | ['cii'] = 'application/vnd.anser-web-certificate-issue-initiation', 878 | ['oas'] = 'application/vnd.fujitsu.oasys', 879 | ['onetoc'] = 'application/onenote', 880 | ['oa3'] = 'application/vnd.fujitsu.oasys3', 881 | ['pptm'] = 'application/vnd.ms-powerpoint.presentation.macroenabled.12', 882 | ['wtb'] = 'application/vnd.webturbo', 883 | ['xlw'] = 'application/vnd.ms-excel', 884 | ['xsm'] = 'application/vnd.syncml+xml', 885 | ['dataless'] = 'application/vnd.fdsn.seed', 886 | ['iota'] = 'application/vnd.astraea-software.iota', 887 | ['dotm'] = 'application/vnd.ms-word.template.macroenabled.12', 888 | ['odt'] = 'application/vnd.oasis.opendocument.text', 889 | ['sxi'] = 'application/vnd.sun.xml.impress', 890 | ['uvp'] = 'video/vnd.dece.pd', 891 | ['pfm'] = 'application/x-font-type1', 892 | ['gbr'] = 'application/rpki-ghostbusters', 893 | ['pyv'] = 'video/vnd.ms-playready.media.pyv', 894 | ['f4v'] = 'video/x-f4v', 895 | ['uvvg'] = 'image/vnd.dece.graphic', 896 | ['pdf'] = 'application/pdf', 897 | ['apk'] = 'application/vnd.android.package-archive', 898 | ['swi'] = 'application/vnd.aristanetworks.swi', 899 | ['nc'] = 'application/x-netcdf', 900 | ['mrcx'] = 'application/marcxml+xml', 901 | ['meta4'] = 'application/metalink4+xml', 902 | ['ulx'] = 'application/x-glulx', 903 | ['rs'] = 'application/rls-services+xml', 904 | ['tcap'] = 'application/vnd.3gpp2.tcap', 905 | ['ipk'] = 'application/vnd.shana.informed.package', 906 | ['acc'] = 'application/vnd.americandynamics.acc', 907 | ['potx'] = 'application/vnd.openxmlformats-officedocument.presentationml.template', 908 | ['bh2'] = 'application/vnd.fujitsu.oasysprs', 909 | ['nfo'] = 'text/x-nfo', 910 | ['ppd'] = 'application/vnd.cups-ppd', 911 | ['mathml'] = 'application/mathml+xml', 912 | ['onepkg'] = 'application/onenote', 913 | ['azs'] = 'application/vnd.airzip.filesecure.azs', 914 | ['icc'] = 'application/vnd.iccprofile', 915 | ['eol'] = 'audio/vnd.digital-winds', 916 | ['fti'] = 'application/vnd.anser-web-funds-transfer-initiation', 917 | ['joda'] = 'application/vnd.joost.joda-archive', 918 | ['ivp'] = 'application/vnd.immervision-ivp', 919 | ['acutc'] = 'application/vnd.acucorp', 920 | ['uvvu'] = 'video/vnd.uvvu.mp4', 921 | ['ser'] = 'application/java-serialized-object', 922 | ['fnc'] = 'application/vnd.frogans.fnc', 923 | ['wg'] = 'application/vnd.pmi.widget', 924 | ['irp'] = 'application/vnd.irepository.package+xml', 925 | ['karbon'] = 'application/vnd.kde.karbon', 926 | ['p7s'] = 'application/pkcs7-signature', 927 | ['mseed'] = 'application/vnd.fdsn.mseed', 928 | ['pre'] = 'application/vnd.lotus-freelance', 929 | ['pwn'] = 'application/vnd.3m.post-it-notes', 930 | ['semf'] = 'application/vnd.semf', 931 | ['nitf'] = 'application/vnd.nitf', 932 | ['m1v'] = 'video/mpeg', 933 | ['aw'] = 'application/applixware', 934 | ['jar'] = 'application/java-archive', 935 | ['hvs'] = 'application/vnd.yamaha.hv-script', 936 | ['kia'] = 'application/vnd.kidspiration', 937 | ['flw'] = 'application/vnd.kde.kivio', 938 | ['ace'] = 'application/x-ace-compressed', 939 | ['vcd'] = 'application/x-cdlink', 940 | ['s'] = 'text/x-asm', 941 | ['plb'] = 'application/vnd.3gpp.pic-bw-large', 942 | ['i2g'] = 'application/vnd.intergeo', 943 | ['pclxl'] = 'application/vnd.hp-pclxl', 944 | ['qfx'] = 'application/vnd.intu.qfx', 945 | ['qwt'] = 'application/vnd.quark.quarkxpress', 946 | ['snd'] = 'audio/basic', 947 | ['xls'] = 'application/vnd.ms-excel', 948 | ['listafp'] = 'application/vnd.ibm.modcap', 949 | ['mag'] = 'application/vnd.ecowin.chart', 950 | ['slt'] = 'application/vnd.epson.salt', 951 | ['tfi'] = 'application/thraud+xml', 952 | ['x3d'] = 'model/x3d+xml', 953 | ['sxm'] = 'application/vnd.sun.xml.math', 954 | ['teacher'] = 'application/vnd.smart.teacher', 955 | ['pls'] = 'application/pls+xml', 956 | ['rsd'] = 'application/rsd+xml', 957 | ['ps'] = 'application/postscript', 958 | ['so'] = 'application/octet-stream', 959 | ['wmz'] = 'application/x-msmetafile', 960 | ['aiff'] = 'audio/x-aiff', 961 | ['nlu'] = 'application/vnd.neurolanguage.nlu', 962 | ['les'] = 'application/vnd.hhe.lesson-player', 963 | ['grv'] = 'application/vnd.groove-injector', 964 | ['wcm'] = 'application/vnd.ms-works', 965 | ['omdoc'] = 'application/omdoc+xml', 966 | ['hpgl'] = 'application/vnd.hp-hpgl', 967 | ['kwt'] = 'application/vnd.kde.kword', 968 | ['res'] = 'application/x-dtbresource+xml', 969 | ['cer'] = 'application/pkix-cert', 970 | ['ecelp4800'] = 'audio/vnd.nuera.ecelp4800', 971 | ['ggb'] = 'application/vnd.geogebra.file', 972 | ['edx'] = 'application/vnd.novadigm.edx', 973 | ['xltm'] = 'application/vnd.ms-excel.template.macroenabled.12', 974 | ['ivu'] = 'application/vnd.immervision-ivu', 975 | ['djvu'] = 'image/vnd.djvu', 976 | ['ngdat'] = 'application/vnd.nokia.n-gage.data', 977 | ['dna'] = 'application/vnd.dna', 978 | ['snf'] = 'application/x-font-snf', 979 | ['js'] = 'application/javascript', 980 | ['xpr'] = 'application/vnd.is-xpr', 981 | ['jam'] = 'application/vnd.jam', 982 | ['stl'] = 'application/vnd.ms-pki.stl', 983 | ['afp'] = 'application/vnd.ibm.modcap', 984 | ['p8'] = 'application/pkcs8', 985 | ['kne'] = 'application/vnd.kinar', 986 | ['xltx'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 987 | ['ez3'] = 'application/vnd.ezpix-package', 988 | ['dgc'] = 'application/x-dgc-compressed', 989 | ['sxc'] = 'application/vnd.sun.xml.calc', 990 | ['clkk'] = 'application/vnd.crick.clicker.keyboard', 991 | ['c11amz'] = 'application/vnd.cluetrust.cartomobile-config-pkg', 992 | ['hbci'] = 'application/vnd.hbci', 993 | ['clkp'] = 'application/vnd.crick.clicker.palette', 994 | ['skm'] = 'application/vnd.koan', 995 | ['zir'] = 'application/vnd.zul', 996 | ['atc'] = 'application/vnd.acucorp', 997 | ['sxg'] = 'application/vnd.sun.xml.writer.global', 998 | ['wmf'] = 'application/x-msmetafile', 999 | ['texinfo'] = 'application/x-texinfo', 1000 | ['mbk'] = 'application/vnd.mobius.mbk', 1001 | ['wbs'] = 'application/vnd.criticaltools.wbs+xml', 1002 | ['g3w'] = 'application/vnd.geospace', 1003 | ['aep'] = 'application/vnd.audiograph', 1004 | ['sxw'] = 'application/vnd.sun.xml.writer', 1005 | ['emz'] = 'application/x-msmetafile', 1006 | ['mlp'] = 'application/vnd.dolby.mlp', 1007 | ['odg'] = 'application/vnd.oasis.opendocument.graphics', 1008 | ['xlsb'] = 'application/vnd.ms-excel.sheet.binary.macroenabled.12', 1009 | ['ptid'] = 'application/vnd.pvi.ptid1', 1010 | ['xlsm'] = 'application/vnd.ms-excel.sheet.macroenabled.12', 1011 | ['setpay'] = 'application/set-payment-initiation', 1012 | ['maker'] = 'application/vnd.framemaker', 1013 | ['igl'] = 'application/vnd.igloader', 1014 | ['dot'] = 'application/msword', 1015 | ['eot'] = 'application/vnd.ms-fontobject', 1016 | ['fdf'] = 'application/vnd.fdf', 1017 | ['ppt'] = 'application/vnd.ms-powerpoint', 1018 | ['uvx'] = 'application/vnd.dece.unspecified', 1019 | ['sgl'] = 'application/vnd.stardivision.writer-global', 1020 | ['rdf'] = 'application/rdf+xml', 1021 | ['m3a'] = 'audio/mpeg', 1022 | ['mft'] = 'application/rpki-manifest', 1023 | ['mpp'] = 'application/vnd.ms-project', 1024 | ['docm'] = 'application/vnd.ms-word.document.macroenabled.12', 1025 | ['tsv'] = 'text/tab-separated-values', 1026 | ['tpl'] = 'application/vnd.groove-tool-template', 1027 | ['rnc'] = 'application/relax-ng-compact-syntax', 1028 | ['onetoc2'] = 'application/onenote', 1029 | ['clkx'] = 'application/vnd.crick.clicker', 1030 | ['xpl'] = 'application/xproc+xml', 1031 | ['c4u'] = 'application/vnd.clonk.c4group', 1032 | ['opml'] = 'text/x-opml', 1033 | ['cdmia'] = 'application/cdmi-capability', 1034 | ['xdf'] = 'application/xcap-diff+xml', 1035 | ['gmx'] = 'application/vnd.gmx', 1036 | ['pgm'] = 'image/x-portable-graymap', 1037 | ['sub'] = 'text/vnd.dvb.subtitle', 1038 | ['sldx'] = 'application/vnd.openxmlformats-officedocument.presentationml.slide', 1039 | ['pps'] = 'application/vnd.ms-powerpoint', 1040 | ['txf'] = 'application/vnd.mobius.txf', 1041 | ['mgp'] = 'application/vnd.osgeo.mapguide.package', 1042 | ['pdb'] = 'application/vnd.palm', 1043 | ['pqa'] = 'application/vnd.palm', 1044 | ['xspf'] = 'application/xspf+xml', 1045 | ['cod'] = 'application/vnd.rim.cod', 1046 | ['htke'] = 'application/vnd.kenameaapp', 1047 | ['xps'] = 'application/vnd.ms-xpsdocument', 1048 | ['kml'] = 'application/vnd.google-earth.kml+xml', 1049 | ['scurl'] = 'text/vnd.curl.scurl', 1050 | ['uvz'] = 'application/vnd.dece.zip', 1051 | ['fh'] = 'image/x-freehand', 1052 | ['sis'] = 'application/vnd.symbian.install', 1053 | ['azw'] = 'application/vnd.amazon.ebook', 1054 | ['see'] = 'application/vnd.seemail', 1055 | ['stw'] = 'application/vnd.sun.xml.writer.template', 1056 | ['dxf'] = 'image/vnd.dxf', 1057 | ['sti'] = 'application/vnd.sun.xml.impress.template', 1058 | 1059 | -- aditional extensions 1060 | 1061 | ['vtt'] = 'text/vtt', 1062 | ['crx'] = 'application/x-chrome-extension', 1063 | ['htc'] = 'text/x-component', 1064 | ['manifest'] = 'text/cache-manifest', 1065 | ['buffer'] = 'application/octet-stream', 1066 | ['m4p'] = 'application/mp4', 1067 | ['m4a'] = 'audio/mp4', 1068 | ['ts'] = 'video/MP2T', 1069 | ['webapp'] = 'application/x-web-app-manifest+json', 1070 | ['lua'] = 'text/x-lua', 1071 | ['luac'] = 'application/x-lua-bytecode', 1072 | ['markdown'] = 'text/x-markdown', 1073 | ['md'] = 'text/x-markdown', 1074 | ['mkd'] = 'text/x-markdown', 1075 | ['ini'] = 'text/plain', 1076 | ['mdp'] = 'application/dash+xml', 1077 | ['map'] = 'application/json', 1078 | ['xsd'] = 'application/xml', 1079 | ['opus'] = 'audio/ogg', 1080 | ['gz'] = 'application/x-gzip' 1081 | }, 1082 | 1083 | -- This contains filename overrides for certain files, like README files. 1084 | -- Sort them in the same order as extensions. 1085 | 1086 | filenames = { 1087 | ['COPYING'] = 'text/plain', 1088 | ['LICENSE'] = 'text/plain', 1089 | ['Makefile'] = 'text/x-makefile', 1090 | ['README'] = 'text/plain' 1091 | } 1092 | } 1093 | 1094 | 1095 | -- Creates a copy of the MIME types database for customization. 1096 | 1097 | function mimetypes.copy (db) 1098 | db = db or defaultdb 1099 | return copy(db) 1100 | end 1101 | 1102 | 1103 | -- Guesses the MIME type of the file with the given name. 1104 | -- It is returned as a string. If the type cannot be guessed, then nil is 1105 | -- returned. 1106 | 1107 | function mimetypes.guess (filename, db) 1108 | db = db or defaultdb 1109 | if db.filenames[filename] then 1110 | return db.filenames[filename] 1111 | end 1112 | local ext = extension(filename) 1113 | if ext then 1114 | return db.extensions[ext] 1115 | end 1116 | return nil 1117 | end 1118 | 1119 | return mimetypes 1120 | -------------------------------------------------------------------------------- /moonmint/deps/pathjoin.lua: -------------------------------------------------------------------------------- 1 | --[[lit-meta 2 | name = "creationix/pathjoin" 3 | description = "The path utilities that used to be part of luvi" 4 | version = "2.0.0" 5 | tags = {"path"} 6 | license = "MIT" 7 | author = { name = "Tim Caswell" } 8 | ]] 9 | 10 | local getPrefix, splitPath, joinParts 11 | 12 | local isWindows 13 | if _G.jit then 14 | isWindows = _G.jit.os == "Windows" 15 | else 16 | isWindows = not not package.path:match("\\") 17 | end 18 | 19 | if isWindows then 20 | -- Windows aware path utilities 21 | function getPrefix(path) 22 | return path:match("^%a:\\") or 23 | path:match("^/") or 24 | path:match("^\\+") 25 | end 26 | function splitPath(path) 27 | local parts = {} 28 | for part in string.gmatch(path, '([^/\\]+)') do 29 | table.insert(parts, part) 30 | end 31 | return parts 32 | end 33 | function joinParts(prefix, parts, i, j) 34 | if not prefix then 35 | return table.concat(parts, '/', i, j) 36 | elseif prefix ~= '/' then 37 | return prefix .. table.concat(parts, '\\', i, j) 38 | else 39 | return prefix .. table.concat(parts, '/', i, j) 40 | end 41 | end 42 | else 43 | -- Simple optimized versions for UNIX systems 44 | function getPrefix(path) 45 | return path:match("^/") 46 | end 47 | function splitPath(path) 48 | local parts = {} 49 | for part in string.gmatch(path, '([^/]+)') do 50 | table.insert(parts, part) 51 | end 52 | return parts 53 | end 54 | function joinParts(prefix, parts, i, j) 55 | if prefix then 56 | return prefix .. table.concat(parts, '/', i, j) 57 | end 58 | return table.concat(parts, '/', i, j) 59 | end 60 | end 61 | 62 | local function pathJoin(...) 63 | local inputs = {...} 64 | local l = #inputs 65 | 66 | -- Find the last segment that is an absolute path 67 | -- Or if all are relative, prefix will be nil 68 | local i = l 69 | local prefix 70 | while true do 71 | prefix = getPrefix(inputs[i]) 72 | if prefix or i <= 1 then break end 73 | i = i - 1 74 | end 75 | 76 | -- If there was one, remove its prefix from its segment 77 | if prefix then 78 | inputs[i] = inputs[i]:sub(#prefix) 79 | end 80 | 81 | -- Split all the paths segments into one large list 82 | local parts = {} 83 | while i <= l do 84 | local sub = splitPath(inputs[i]) 85 | for j = 1, #sub do 86 | parts[#parts + 1] = sub[j] 87 | end 88 | i = i + 1 89 | end 90 | 91 | -- Evaluate special segments in reverse order. 92 | local skip = 0 93 | local reversed = {} 94 | for idx = #parts, 1, -1 do 95 | local part = parts[idx] 96 | if part ~= '.' then 97 | if part == '..' then 98 | skip = skip + 1 99 | elseif skip > 0 then 100 | skip = skip - 1 101 | else 102 | reversed[#reversed + 1] = part 103 | end 104 | end 105 | end 106 | 107 | -- Reverse the list again to get the correct order 108 | parts = reversed 109 | for idx = 1, #parts / 2 do 110 | local j = #parts - idx + 1 111 | parts[idx], parts[j] = parts[j], parts[idx] 112 | end 113 | 114 | local path = joinParts(prefix, parts) 115 | return path 116 | end 117 | 118 | return { 119 | isWindows = isWindows, 120 | getPrefix = getPrefix, 121 | splitPath = splitPath, 122 | joinParts = joinParts, 123 | pathJoin = pathJoin, 124 | } 125 | -------------------------------------------------------------------------------- /moonmint/deps/secure-socket.lua: -------------------------------------------------------------------------------- 1 | -- Alias so luajit path does need modification (for testing) 2 | -- 3 | return require 'moonmint.deps.secure-socket.init' 4 | -------------------------------------------------------------------------------- /moonmint/deps/secure-socket/biowrap.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Copyright 2016 The Luvit Authors. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS-IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | --]] 18 | local openssl = require('openssl') 19 | 20 | -- writeCipher is called when ssl needs something written on the socket 21 | -- handshakeComplete is called when the handhake is complete and it's safe 22 | -- onPlain is called when plaintext comes out. 23 | return function (ctx, isServer, socket, handshakeComplete) 24 | 25 | local bin, bout = openssl.bio.mem(8192), openssl.bio.mem(8192) 26 | local ssl = ctx:ssl(bin, bout, isServer) 27 | 28 | local ssocket = {tls=true} 29 | local onPlain 30 | 31 | local function flush(callback) 32 | local chunks = {} 33 | local i = 0 34 | while bout:pending() > 0 do 35 | i = i + 1 36 | chunks[i] = bout:read() 37 | end 38 | if i == 0 then 39 | if callback then callback() end 40 | return true 41 | end 42 | return socket:write(chunks, callback) 43 | end 44 | 45 | local function handshake(callback) 46 | if ssl:handshake() then 47 | local success, result = ssl:getpeerverification() 48 | socket:read_stop() 49 | if not success and result then 50 | handshakeComplete("Error verifying peer: " .. result[1].error_string) 51 | end 52 | handshakeComplete(nil, ssocket) 53 | end 54 | return flush(callback) 55 | end 56 | 57 | local function onCipher(err, data) 58 | if not onPlain then 59 | if err or not data then 60 | return handshakeComplete(err or "Peer aborted the SSL handshake", data) 61 | end 62 | bin:write(data) 63 | return handshake() 64 | end 65 | if err or not data then 66 | return onPlain(err, data) 67 | end 68 | bin:write(data) 69 | while true do 70 | local plain = ssl:read() 71 | if not plain then break end 72 | onPlain(nil, plain) 73 | end 74 | end 75 | 76 | -- When requested to start reading, start the real socket and setup 77 | -- onPlain handler 78 | function ssocket.read_start(_, onRead) 79 | onPlain = onRead 80 | return socket:read_start(onCipher) 81 | end 82 | 83 | -- When requested to write plain data, encrypt it and write to socket 84 | function ssocket.write(_, plain, callback) 85 | ssl:write(plain) 86 | return flush(callback) 87 | end 88 | 89 | function ssocket.shutdown(_, ...) 90 | return socket:shutdown(...) 91 | end 92 | function ssocket.read_stop(_, ...) 93 | return socket:read_stop(...) 94 | end 95 | function ssocket.is_closing(_, ...) 96 | return socket:is_closing(...) 97 | end 98 | function ssocket.close(_, ...) 99 | return socket:close(...) 100 | end 101 | 102 | handshake() 103 | socket:read_start(onCipher) 104 | 105 | end 106 | -------------------------------------------------------------------------------- /moonmint/deps/secure-socket/context.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Copyright 2016 The Luvit Authors. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS-IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | --]] 18 | local openssl 19 | do 20 | local ok 21 | ok, openssl = pcall(require, 'openssl') 22 | if not ok then 23 | error 'Could not find package "openssl". Install it from luarocks, or install bkopenssl.' 24 | end 25 | end 26 | 27 | local bit 28 | do 29 | local status 30 | status, bit = pcall(require, 'bit') 31 | if not status then bit = require 'bit32' end 32 | end 33 | 34 | local DEFAULT_CIPHERS = 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' .. -- TLS 1.2 35 | '!RC4:HIGH:!MD5:!aNULL:!EDH' -- TLS 1.0 36 | 37 | local DEFAULT_CA_STORE 38 | do 39 | 40 | local data = require("moonmint.deps.secure-socket.root_ca") 41 | DEFAULT_CA_STORE = openssl.x509.store:new() 42 | local index = 1 43 | local dataLength = #data 44 | while index < dataLength do 45 | local len = bit.bor(bit.lshift(data:byte(index), 8), data:byte(index + 1)) 46 | index = index + 2 47 | local cert = assert(openssl.x509.read(data:sub(index, index + len))) 48 | index = index + len 49 | assert(DEFAULT_CA_STORE:add(cert)) 50 | end 51 | end 52 | 53 | local function returnOne() 54 | return 1 55 | end 56 | 57 | return function (options) 58 | local ctx = openssl.ssl.ctx_new( 59 | options.protocol or 'TLSv1_2', 60 | options.ciphers or DEFAULT_CIPHERS) 61 | 62 | local key, cert, ca 63 | if options.key then 64 | key = assert(openssl.pkey.read(options.key, true, 'pem')) 65 | end 66 | if options.cert then 67 | cert = {} 68 | for chunk in options.cert:gmatch("%-+BEGIN[^-]+%-+[^-]+%-+END[^-]+%-+") do 69 | cert[#cert + 1] = assert(openssl.x509.read(chunk)) 70 | end 71 | end 72 | if options.ca then 73 | if type(options.ca) == "string" then 74 | ca = { assert(openssl.x509.read(options.ca)) } 75 | elseif type(options.ca) == "table" then 76 | ca = {} 77 | for i = 1, #options.ca do 78 | ca[i] = assert(openssl.x509.read(options.ca[i])) 79 | end 80 | else 81 | error("options.ca must be string or table of strings") 82 | end 83 | end 84 | if key and cert then 85 | local first = table.remove(cert, 1) 86 | assert(ctx:use(key, first)) 87 | if #cert > 0 then 88 | -- TODO: find out if there is a way to not need to duplicate the last cert here 89 | -- as a dummy fill for the root CA cert 90 | assert(ctx:add(cert[#cert], cert)) 91 | end 92 | end 93 | if ca then 94 | local store = openssl.x509.store:new() 95 | for i = 1, #ca do 96 | assert(store:add(ca[i])) 97 | end 98 | ctx:cert_store(store) 99 | elseif DEFAULT_CA_STORE then 100 | ctx:cert_store(DEFAULT_CA_STORE) 101 | end 102 | if not (options.insecure or options.key) then 103 | ctx:verify_mode(openssl.ssl.peer, returnOne) 104 | end 105 | 106 | ctx:options(bit.bor( 107 | openssl.ssl.no_sslv2, 108 | openssl.ssl.no_sslv3, 109 | openssl.ssl.no_compression)) 110 | 111 | return ctx 112 | end 113 | -------------------------------------------------------------------------------- /moonmint/deps/secure-socket/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Copyright 2016 The Luvit Authors. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS-IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | --]] 18 | local getContext = require('moonmint.deps.secure-socket.context') 19 | local bioWrap = require('moonmint.deps.secure-socket.biowrap') 20 | 21 | return function (socket, options, callback) 22 | if options == true then options = {} end 23 | local ctx = getContext(options) 24 | local thread 25 | if not callback then 26 | thread = coroutine.running() 27 | end 28 | bioWrap(ctx, options.server, socket, callback or function (err, ssocket) 29 | return assert(coroutine.resume(thread, ssocket, err)) 30 | end) 31 | if not callback then 32 | return coroutine.yield() 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /moonmint/deps/secure-socket/root_ca.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/moonmint/24fc2e5856c6d6e86e0f6eccb7e3db03c54b77b6/moonmint/deps/secure-socket/root_ca.dat -------------------------------------------------------------------------------- /moonmint/deps/stream-wrap.lua: -------------------------------------------------------------------------------- 1 | local corunning = coroutine.running 2 | 3 | local function makeCloser(socket) 4 | local closer = { 5 | read = false, 6 | written = false, 7 | errored = false, 8 | } 9 | 10 | local closed = false 11 | 12 | local function close() 13 | if closed then return end 14 | closed = true 15 | if not closer.readClosed then 16 | closer.readClosed = true 17 | if closer.onClose then 18 | closer.onClose() 19 | end 20 | end 21 | if not socket:is_closing() then 22 | socket:close() 23 | end 24 | end 25 | 26 | closer.close = close 27 | 28 | function closer.check() 29 | if closer.errored or (closer.read and closer.written) then 30 | return close() 31 | end 32 | end 33 | 34 | return closer 35 | end 36 | 37 | local unpack = table.unpack or unpack 38 | 39 | local function makeRead(socket, closer) 40 | local paused = true 41 | local queue = {} 42 | local tindex = 0 43 | local dindex = 0 44 | local function dispatch(data) 45 | if tindex > dindex then 46 | local thread = queue[dindex] 47 | queue[dindex] = nil 48 | dindex = dindex + 1 49 | assert(coroutine.resume(thread, unpack(data))) 50 | else 51 | queue[dindex] = data 52 | dindex = dindex + 1 53 | if not paused then 54 | paused = true 55 | assert(socket:read_stop()) 56 | end 57 | end 58 | end 59 | closer.onClose = function () 60 | if not closer.read then 61 | closer.read = true 62 | return dispatch {nil, closer.errored} 63 | end 64 | end 65 | local function onRead(err, chunk) 66 | if err then 67 | closer.errored = err 68 | return closer.check() 69 | end 70 | if not chunk then 71 | if closer.read then return end 72 | closer.read = true 73 | dispatch {} 74 | return closer.check() 75 | end 76 | return dispatch {chunk} 77 | end 78 | return function() 79 | if dindex > tindex then 80 | local data = queue[tindex] 81 | queue[tindex] = nil 82 | tindex = tindex + 1 83 | return unpack(data) 84 | end 85 | queue[tindex] = corunning() 86 | tindex = tindex + 1 87 | if paused then 88 | paused = false 89 | assert(socket:read_start(onRead)) 90 | end 91 | return coroutine.yield() 92 | end 93 | end 94 | 95 | local function makeWrite(socket, closer) 96 | local function wait() 97 | local thread = corunning() 98 | return function (err) 99 | assert(coroutine.resume(thread, err)) 100 | end 101 | end 102 | return function(chunk) 103 | if closer.written then 104 | return nil, "already shutdown" 105 | end 106 | if chunk == nil then 107 | closer.written = true 108 | closer.check() 109 | socket:shutdown(wait()) 110 | return coroutine.yield() 111 | end 112 | local success, err = socket:write(chunk, wait()) 113 | if not success then 114 | closer.errored = err 115 | closer.check() 116 | return nil, err 117 | end 118 | err = coroutine.yield() 119 | return not err, err 120 | end 121 | end 122 | 123 | return function(socket) 124 | assert(socket 125 | and socket.write 126 | and socket.shutdown 127 | and socket.read_start 128 | and socket.read_stop 129 | and socket.is_closing 130 | and socket.close) 131 | 132 | local closer = makeCloser(socket) 133 | local read = makeRead(socket, closer) 134 | local write = makeWrite(socket, closer) 135 | return read, write, closer.close 136 | end 137 | -------------------------------------------------------------------------------- /moonmint/fs.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | -- A filesystem abstraction on top of luv. Uses non-blocking coroutines 20 | -- when available, otherwise uses blocking calls. 21 | -- 22 | -- Modified from https://github.com/luvit/lit/blob/master/deps/coro-fs.lua 23 | 24 | --- Abstractions around the libuv filesystem. 25 | -- @module moonmint.fs 26 | -- @author Calvin Rose 27 | -- @copyright 2016 28 | -- @license MIT 29 | 30 | local uv = require 'luv' 31 | local pathJoin = require('moonmint.deps.pathjoin').pathJoin 32 | 33 | local fs = {} 34 | 35 | local corunning = coroutine.running 36 | local coresume = coroutine.resume 37 | local coyield = coroutine.yield 38 | 39 | local function noop() end 40 | 41 | local function makeCallback(sync) 42 | local thread = corunning() 43 | if thread and not sync then -- Non Blocking callback (coroutines) 44 | return function (err, value, ...) 45 | if err then 46 | assert(coresume(thread, nil, err)) 47 | else 48 | assert(coresume(thread, value == nil and true or value, ...)) 49 | end 50 | end 51 | else -- Blocking callback 52 | local context = {} 53 | return function (err, first, ...) 54 | context.done = true 55 | context.err = err 56 | context.first = first 57 | context.values = {...} 58 | end, context 59 | end 60 | end 61 | 62 | local unpack = unpack or table.unpack 63 | local function tryYield(context) 64 | if context then -- Blocking 65 | while not context.done do 66 | uv.run('once') -- Should we use the default event loop? 67 | end 68 | local err = context.err 69 | local first = context.first 70 | if err then 71 | return nil, err 72 | else 73 | return first == nil and true or first, unpack(context.values) 74 | end 75 | else -- Non blocking 76 | return coyield() 77 | end 78 | end 79 | 80 | --- Wrapper around uv.fs_mkdir 81 | function fs.mkdir(sync, path, mode) 82 | local cb, context = makeCallback(sync) 83 | uv.fs_mkdir(path, mode or 511, cb) 84 | return tryYield(context) 85 | end 86 | 87 | --- Wrapper around uv.fs_open 88 | function fs.open(sync, path, flags, mode) 89 | local cb, context = makeCallback(sync) 90 | uv.fs_open(path, flags or "r", mode or 428, cb) 91 | return tryYield(context) 92 | end 93 | 94 | --- Wrapper around uv.fs_unlink 95 | function fs.unlink(sync, path) 96 | local cb, context = makeCallback(sync) 97 | uv.fs_unlink(path, cb) 98 | return tryYield(context) 99 | end 100 | 101 | --- Wrapper around uv.fs_stat 102 | function fs.stat(sync, path) 103 | local cb, context = makeCallback(sync) 104 | uv.fs_stat(path, cb) 105 | return tryYield(context) 106 | end 107 | 108 | --- Wrapper around uv.fs_lstat 109 | function fs.lstat(sync, path) 110 | local cb, context = makeCallback(sync) 111 | uv.fs_lstat(path, cb) 112 | return tryYield(context) 113 | end 114 | 115 | --- Wrapper around uv.fs_fstat 116 | function fs.fstat(sync, fd) 117 | local cb, context = makeCallback(sync) 118 | uv.fs_fstat(fd, cb) 119 | return tryYield(context) 120 | end 121 | 122 | --- Wrapper around uv.fs_chmod 123 | function fs.chmod(sync, path) 124 | local cb, context = makeCallback(sync) 125 | uv.fs_chmod(path, cb) 126 | return tryYield(context) 127 | end 128 | 129 | --- Wrapper around uv.fs_fchmod 130 | function fs.fchmod(sync, path) 131 | local cb, context = makeCallback(sync) 132 | uv.fs_fchmod(path, cb) 133 | return tryYield(context) 134 | end 135 | 136 | --- Wrapper around uv.fs_read 137 | function fs.read(sync, fd, length, offset) 138 | local cb, context = makeCallback(sync) 139 | uv.fs_read(fd, length or 1024 * 48, offset or -1, cb) 140 | return tryYield(context) 141 | end 142 | 143 | --- Wrapper around uv.fs_write 144 | function fs.write(sync, fd, data, offset) 145 | local cb, context = makeCallback(sync) 146 | uv.fs_write(fd, data, offset or -1, cb) 147 | return tryYield(context) 148 | end 149 | 150 | --- Wrapper around uv.fs_close 151 | function fs.close(sync, fd) 152 | local cb, context = makeCallback(sync) 153 | uv.fs_close(fd, cb) 154 | return tryYield(context) 155 | end 156 | 157 | --- Wrapper around uv.fs_symlink 158 | function fs.symlink(sync, target, path) 159 | local cb, context = makeCallback(sync) 160 | uv.fs_symlink(target, path, cb) 161 | return tryYield(context) 162 | end 163 | 164 | --- Wrapper around uv.fs_readlink 165 | function fs.readlink(sync, path) 166 | local cb, context = makeCallback(sync) 167 | uv.fs_readlink(path, cb) 168 | return tryYield(context) 169 | end 170 | 171 | --- Wrapper around uv.fs_access 172 | function fs.access(sync, path, flags) 173 | local cb, context = makeCallback(sync) 174 | uv.fs_access(path, flags or '', cb) 175 | return tryYield(context) 176 | end 177 | 178 | --- Wrapper around uv.fs_rmdir 179 | function fs.rmdir(sync, path) 180 | local cb, context = makeCallback(sync) 181 | uv.fs_rmdir(path, cb) 182 | return tryYield(context) 183 | end 184 | 185 | --- Remove directories recursively like the UNIX command `rm -rf`. 186 | function fs.rmrf(sync, path) 187 | local success, err = fs.rmdir(sync, path) 188 | if success then 189 | return success, err 190 | end 191 | if err:match('^ENOTDIR:') then return fs.unlink(sync, path) end 192 | if not err:match('^ENOTEMPTY:') then return success, err end 193 | for entry in assert(fs.scandir(sync, path)) do 194 | local subPath = pathJoin(path, entry.name) 195 | if entry.type == 'directory' then 196 | success, err = fs.rmrf(sync, pathJoin(path, entry.name)) 197 | else 198 | success, err = fs.unlink(sync, subPath) 199 | end 200 | if not success then return success, err end 201 | end 202 | return fs.rmdir(sync, path) 203 | end 204 | 205 | --- Smart wrapper around uv.fs_scandir. 206 | -- @treturn function an iterator over file objects 207 | -- in a directory. Each file table has a `name` property 208 | -- and a `type` property. 209 | function fs.scandir(sync, path) 210 | local cb, context = makeCallback(sync) 211 | uv.fs_scandir(path, cb) 212 | local req, err = tryYield(context) 213 | if not req then return nil, err end 214 | return function () 215 | local name, typ = uv.fs_scandir_next(req) 216 | if not name then return name, typ end 217 | if type(name) == "table" then return name end 218 | return { 219 | name = name, 220 | type = typ 221 | } 222 | end 223 | end 224 | 225 | --- Reads a file into a string 226 | function fs.readFile(sync, path) 227 | local fd, stat, data, err 228 | fd, err = fs.open(sync, path) 229 | if err then return nil, err end 230 | stat, err = fs.fstat(sync, fd) 231 | if stat then 232 | data, err = fs.read(sync, fd, stat.size) 233 | end 234 | uv.fs_close(fd, noop) 235 | return data, err 236 | end 237 | 238 | --- Writes a string to a file. Overwrites the file 239 | -- if it already exists. 240 | function fs.writeFile(sync, path, data, mkdir) 241 | local fd, success, err 242 | fd, err = fs.open(sync, path, "w") 243 | if err then 244 | if mkdir and err:match("^ENOENT:") then 245 | success, err = fs.mkdirp(sync, pathJoin(path, "..")) 246 | if success then return fs.writeFile(sync, path, data) end 247 | end 248 | return nil, err 249 | end 250 | success, err = fs.write(sync, fd, data) 251 | uv.fs_close(fd, noop) 252 | return success, err 253 | end 254 | 255 | --- Append a string to a file. 256 | function fs.appendFile(sync, path, data, mkdir) 257 | local fd, success, err 258 | fd, err = fs.open(sync, path, "w+") 259 | if err then 260 | if mkdir and err:match("^ENOENT:") then 261 | success, err = fs.mkdirp(sync, pathJoin(path, "..")) 262 | if success then return fs.appendFile(sync, path, data) end 263 | end 264 | return nil, err 265 | end 266 | success, err = fs.write(sync, fd, data) 267 | uv.fs_close(fd, noop) 268 | return success, err 269 | end 270 | 271 | --- Make directories recursively. Similar to the UNIX `mkdir -p`. 272 | function fs.mkdirp(sync, path, mode) 273 | local success, err = fs.mkdir(sync, path, mode) 274 | if success or err:match("^EEXIST") then 275 | return true 276 | end 277 | if err:match("^ENOENT:") then 278 | success, err = fs.mkdirp(sync, pathJoin(path, ".."), mode) 279 | if not success then return nil, err end 280 | return fs.mkdir(sync, path, mode) 281 | end 282 | return nil, err 283 | end 284 | 285 | -- Make sync and async versions 286 | local function makeAliases(module) 287 | local ret = {} 288 | local ext = {} 289 | local sync = {} 290 | for k, v in pairs(module) do 291 | if type(v) == 'function' then 292 | if k == 'chroot' then 293 | sync[k], ext[k], ret[k] = v, v, v 294 | else 295 | sync[k] = function(...) 296 | return v(true, ...) 297 | end 298 | ext[k] = v 299 | ret[k] = function(...) 300 | return v(false, ...) 301 | end 302 | end 303 | end 304 | end 305 | ret.s = sync 306 | ret.sync = sync 307 | ret.ext = ext 308 | return ret 309 | end 310 | 311 | --- Creates a clone of fs, but with a different base directory. 312 | function fs.chroot(base) 313 | local chroot = { 314 | base = base, 315 | fstat = fs.fstat, 316 | fchmod = fs.fchmod, 317 | read = fs.read, 318 | write = fs.write, 319 | close = fs.close, 320 | } 321 | local function resolve(path) 322 | assert(path, "path missing") 323 | return pathJoin(base, pathJoin(path)) 324 | end 325 | function chroot.mkdir(sync, path, mode) 326 | return fs.mkdir(sync, resolve(path), mode) 327 | end 328 | function chroot.mkdirp(sync, path, mode) 329 | return fs.mkdirp(sync, resolve(path), mode) 330 | end 331 | function chroot.open(sync, path, flags, mode) 332 | return fs.open(sync, resolve(path), flags, mode) 333 | end 334 | function chroot.unlink(sync, path) 335 | return fs.unlink(sync, resolve(path)) 336 | end 337 | function chroot.stat(sync, path) 338 | return fs.stat(sync, resolve(path)) 339 | end 340 | function chroot.lstat(sync, path) 341 | return fs.lstat(sync, resolve(path)) 342 | end 343 | function chroot.symlink(sync, target, path) 344 | -- TODO: should we resolve absolute target paths or treat it as opaque data? 345 | return fs.symlink(sync, target, resolve(path)) 346 | end 347 | function chroot.readlink(sync, path) 348 | return fs.readlink(sync, resolve(path)) 349 | end 350 | function chroot.chmod(sync, path, mode) 351 | return fs.chmod(sync, resolve(path), mode) 352 | end 353 | function chroot.access(sync, path, flags) 354 | return fs.access(sync, resolve(path), flags) 355 | end 356 | function chroot.rename(sync, path, newPath) 357 | return fs.rename(sync, resolve(path), resolve(newPath)) 358 | end 359 | function chroot.rmdir(sync, path) 360 | return fs.rmdir(sync, resolve(path)) 361 | end 362 | function chroot.rmrf(sync, path) 363 | return fs.rmrf(sync, resolve(path)) 364 | end 365 | function chroot.scandir(sync, path) 366 | return fs.scandir(sync, resolve(path)) 367 | end 368 | function chroot.readFile(sync, path) 369 | return fs.readFile(sync, resolve(path)) 370 | end 371 | function chroot.writeFile(sync, path, data, mkdir) 372 | return fs.writeFile(sync, resolve(path), data, mkdir) 373 | end 374 | function chroot.appendFile(sync, path, data, mkdir) 375 | return fs.appendFile(sync, resolve(path), data, mkdir) 376 | end 377 | function chroot.chroot(sync, newBase) 378 | return fs.chroot(sync, resolve(newBase)) 379 | end 380 | return makeAliases(chroot) 381 | end 382 | 383 | return makeAliases(fs) 384 | -------------------------------------------------------------------------------- /moonmint/html.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | --- HTML utilities. 20 | -- @module moonmint.html 21 | 22 | local pairs = pairs 23 | local gsub = string.gsub 24 | local char = string.char 25 | local tonumber = tonumber 26 | 27 | local entityToRaw = { 28 | lt = '<', 29 | gt = '>', 30 | amp = '&', 31 | quot = '"', 32 | apos = "'", 33 | } 34 | 35 | local rawToEntity = {} 36 | for k, v in pairs(entityToRaw) do 37 | rawToEntity[v] = '&' .. k .. ';' 38 | end 39 | 40 | rawToEntity["'"] = "'" 41 | rawToEntity["/"] = "/" 42 | 43 | local function htmlEscape(str) 44 | return (gsub(str, "[}{\">/<'&]", rawToEntity)) 45 | end 46 | 47 | -- Convert codepoint to string 48 | -- http://stackoverflow.com/questions/26071104/more-elegant-simpler-way-to-convert-code-point-to-utf-8 49 | local function utf8_char(cp) 50 | if cp < 128 then 51 | return char(cp) 52 | end 53 | local suffix = cp % 64 54 | local c4 = 128 + suffix 55 | cp = (cp - suffix) / 64 56 | if cp < 32 then 57 | return char(192 + cp, c4) 58 | end 59 | suffix = cp % 64 60 | local c3 = 128 + suffix 61 | cp = (cp - suffix) / 64 62 | if cp < 16 then 63 | return char(224 + cp, c3, c4) 64 | end 65 | suffix = cp % 64 66 | cp = (cp - suffix) / 64 67 | return char(240 + cp, 128 + suffix, c3, c4) 68 | end 69 | 70 | local function htmlUnescapeHelper(n, s) 71 | if n == '#' then 72 | return utf8_char(tonumber(s, 10)) -- Decimal 73 | elseif n == '#x' then 74 | return utf8_char(tonumber(s, 16)) -- Hex 75 | elseif n == 'x' then 76 | return htmlUnescapeHelper('', n .. s) 77 | elseif entityToRaw[s] then 78 | return entityToRaw[s] 79 | else 80 | -- Default to returning the potential entity as is. 81 | return ('&%s%s;'):format(n, s) 82 | end 83 | end 84 | 85 | local function htmlUnescape(str) 86 | local ret = gsub(str, '&(#?x?)(%w+);', htmlUnescapeHelper) 87 | return ret 88 | end 89 | 90 | return { 91 | encode = htmlEscape, 92 | decode = htmlUnescape, 93 | escape = htmlEscape, 94 | unescape = htmlUnescape 95 | } 96 | -------------------------------------------------------------------------------- /moonmint/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | local moonmint_mt = {} 20 | local moonmint = setmetatable({}, moonmint_mt) 21 | 22 | local server = require "moonmint.server" 23 | moonmint.server = server 24 | moonmint.static = require "moonmint.static" 25 | moonmint.template = require "moonmint.template" 26 | moonmint.router = require "moonmint.router" 27 | moonmint.fs = require "moonmint.fs" 28 | moonmint.util = require "moonmint.util" 29 | moonmint.url = require "moonmint.url" 30 | moonmint.html = require "moonmint.html" 31 | moonmint.response = require "moonmint.response" 32 | moonmint.agent = require "moonmint.agent" 33 | moonmint.cookie = require "moonmint.cookie" 34 | 35 | function moonmint.go(fn, ...) 36 | if fn then 37 | coroutine.wrap(fn)(...) 38 | end 39 | return require('luv').run() 40 | end 41 | 42 | function moonmint_mt.__call(_, ...) 43 | return server(...) 44 | end 45 | 46 | return moonmint 47 | -------------------------------------------------------------------------------- /moonmint/response.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | --- Utilities for creating HTTP responses. 20 | -- @module moonmint.response 21 | 22 | local mime = require('moonmint.deps.mimetypes').guess 23 | local fs = require 'moonmint.fs' 24 | local headers = require 'moonmint.deps.http-headers' 25 | local setmetatable = setmetatable 26 | local tonumber = tonumber 27 | local type = type 28 | 29 | -- For coercing responses into tables 30 | local toResponse = { 31 | ['string'] = function(res, code, mimetype) 32 | return { 33 | code = code and tonumber(code) or 200, 34 | headers = headers.newHeaders { 35 | {'Content-Length', #res}, 36 | {'Content-Type', mimetype or 'text/html'} 37 | }, 38 | body = res 39 | } 40 | end, 41 | ['number'] = function(res) 42 | return { 43 | code = res, 44 | headers = headers.newHeaders() 45 | } 46 | end, 47 | ['table'] = function (res) 48 | res.headers = headers.newHeaders(res.headers) 49 | return res 50 | end 51 | } 52 | 53 | -- Coerce response into a table 54 | local function responsify(...) 55 | local converter = toResponse[type(...)] 56 | if converter then 57 | return converter(...) 58 | else 59 | error('unable to create response from "' .. type(...) .. '"') 60 | end 61 | end 62 | 63 | local function normalizer(_, go) 64 | return responsify(go()) 65 | end 66 | 67 | -- Refactor to use uv.sendfile? 68 | local function file(path, sync) 69 | local body, err = fs.ext.readFile(sync, path) 70 | if not body then 71 | error(err) 72 | end 73 | local mimetype = mime(path) 74 | return { 75 | code = 200, 76 | headers = headers.newHeaders { 77 | {'Content-Length', #body}, 78 | {'Content-Type', mimetype} 79 | }, 80 | body = body 81 | } 82 | end 83 | 84 | local function fileSync(path) 85 | return file(path, true) 86 | end 87 | 88 | local function fileAsync(path) 89 | return file(path, false) 90 | end 91 | 92 | local function redirect(location, code) 93 | return { 94 | code = code or 302, 95 | headers = headers.newHeaders { 96 | {'Location', location} 97 | } 98 | } 99 | end 100 | 101 | return setmetatable({ 102 | fileSync = fileSync, 103 | file = fileAsync, 104 | normalizer = normalizer, 105 | redirect = redirect, 106 | create = responsify 107 | }, { 108 | __call = function(_, ...) 109 | return responsify(...) 110 | end 111 | }) 112 | -------------------------------------------------------------------------------- /moonmint/router.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | --- Routing and sub apps. 20 | -- @module moonmint.router 21 | 22 | local response = require 'moonmint.response' 23 | 24 | local byte = string.byte 25 | local match = string.match 26 | local gmatch = string.gmatch 27 | local gsub = string.gsub 28 | local sub = string.sub 29 | local concat = table.concat 30 | local setmetatable = setmetatable 31 | 32 | local router_mt 33 | 34 | -- Calling router creates a new router 35 | local router = setmetatable({}, { 36 | __call = function(_, options) 37 | options = options or { 38 | mergeParams = true 39 | } 40 | return setmetatable({ 41 | mergeParams = not not options.mergeParams, 42 | mergeName = options.mergeName 43 | }, router_mt) 44 | end 45 | }) 46 | 47 | -- Helper function to compile a function that matches certain hosts. Used 48 | -- internally by router:route 49 | local function compileHost(hostPattern) 50 | -- Escape everything except globs 51 | local pat = gsub(hostPattern, "[%%%^%$%(%)%.%[%]%+%-%?]", "%%%1") 52 | pat = gsub(pat, "%*%*", ".+") 53 | pat = gsub(pat, "%*", "[^%%.]+") 54 | return function(str) 55 | return #match(str, pat) > 0 56 | end 57 | end 58 | 59 | -- Turn a url routing pattern into a Lua pattern. Also returns an array of the 60 | -- names of the capture groups. 61 | local function makeRoutePattern(urlPattern) 62 | local pathCaptures = {} 63 | local tab = {} 64 | for part in gmatch(urlPattern, "[^/]+") do 65 | if byte(part) == byte(":") then 66 | if byte(part, #part) == byte(":") then 67 | assert(match(part, "^:[_%w]+:$"), ("Invalid capture: %s"):format(part)) 68 | pathCaptures[#pathCaptures + 1] = sub(part, 2, -2) 69 | tab[#tab + 1] = "(.+)" 70 | else 71 | assert(match(part, "^:[_%w]+$"), ("Invalid capture: %s"):format(part)) 72 | pathCaptures[#pathCaptures + 1] = sub(part, 2, -1) 73 | tab[#tab + 1] = "([^/]+)" 74 | end 75 | else 76 | tab[#tab + 1] = gsub(part, "[%%%^%$%(%)%.%[%]%*%+%-%?]", "%%%1") 77 | end 78 | end 79 | local pattern 80 | if #tab == 0 then 81 | pattern = "^/?$" 82 | else 83 | pattern = "^/" .. concat(tab, "/") .. "/?$" 84 | end 85 | return pattern, pathCaptures 86 | end 87 | 88 | -- Helper function to compile a route matching function that also returns 89 | -- captures. Used internally by router:route 90 | local function compileRoute(urlPattern) 91 | local pattern, pathCaptures = makeRoutePattern(urlPattern) 92 | if #pathCaptures == 0 then 93 | return function(str) 94 | return (str and match(str, pattern)) and {} 95 | end 96 | else 97 | return function(str) 98 | if not str then return false end 99 | local matches = { match(str, pattern) } 100 | if #matches ~= 0 then 101 | for i = 1, #pathCaptures do 102 | matches[pathCaptures[i]] = matches[i] 103 | end 104 | return matches 105 | end 106 | end 107 | end 108 | end 109 | 110 | -- Helper function to add parameters to a request without deleting previous 111 | -- parameters 112 | local function addParams(req, params) 113 | local p = req.params 114 | if p then 115 | for k, v in pairs(params) do 116 | p[k] = v 117 | end 118 | else 119 | req.params = params 120 | end 121 | end 122 | 123 | -- If a middleware calls go multiple times, restart the chain at that point. 124 | local function chain(middlewares, i, req, go) 125 | local mw = middlewares[i] 126 | if mw then 127 | return mw(req, function() 128 | return chain(middlewares, i + 1, req, go) 129 | end) 130 | elseif go then 131 | return go() 132 | end 133 | end 134 | 135 | -- Converts values besides functions to middleware 136 | local function toMiddleware(mw) 137 | if type(mw) == 'function' then 138 | return mw 139 | elseif type(mw) == 'table' and 140 | getmetatable(mw) and 141 | getmetatable(mw).__call then 142 | return mw 143 | else 144 | local res = response(mw) 145 | return function() 146 | return res 147 | end 148 | end 149 | end 150 | 151 | -- Collapse multiple middlewares into one. 152 | local function makeChain(...) 153 | if select('#', ...) < 2 then 154 | return toMiddleware(...) 155 | else 156 | local middlewares = {...} 157 | -- Convert string middlewares to functions 158 | for i = 1, #middlewares do 159 | middlewares[i] = toMiddleware(middlewares[i]) 160 | end 161 | return function(req, go) 162 | return chain(middlewares, 1, req, go) 163 | end 164 | end 165 | end 166 | 167 | -- Helper function to wrap middleware under a sub-route. 168 | local function subroute(self, path, ...) 169 | local pat, captures = makeRoutePattern(path) 170 | pat = sub(pat, 1, -4) .. "(/?.*)/?$" 171 | 172 | local middleware = makeChain(...) 173 | 174 | return self:use(function(req, go) 175 | local oldPath = req.path 176 | local matches = { match(oldPath, pat) } 177 | if #matches == 0 then 178 | return go() 179 | end 180 | local newPath = matches[#matches] 181 | if newPath == "" then newPath = "/" end 182 | if byte(newPath, -1) == byte("/") then 183 | newPath = newPath:sub(1, -2) 184 | end 185 | if byte(newPath) ~= byte("/") then 186 | newPath = "/" .. newPath 187 | end 188 | req.path = newPath 189 | if self.mergeParams then 190 | req.params = req.params or {} 191 | local reqp 192 | if self.mergeName then 193 | reqp = req.params[self.mergeName] or {} 194 | self.params[self.mergeName] = reqp 195 | else 196 | reqp = self.params 197 | end 198 | for i = 1, #captures do 199 | reqp[#reqp + 1] = matches[i] 200 | reqp[captures[i]] = matches[i] 201 | end 202 | end 203 | local res = middleware(req, function() 204 | req.path = oldPath 205 | return go() 206 | end) 207 | req.path = oldPath 208 | return res 209 | end) 210 | end 211 | 212 | -- Make this router use middleware. Returns the router. 213 | function router:use(...) 214 | if type(...) == "string" then 215 | return subroute(self, ...) 216 | end 217 | for i = 1, select("#", ...) do 218 | local mw = toMiddleware(select(i, ...)) 219 | self[#self + 1] = mw 220 | end 221 | return self 222 | end 223 | 224 | -- Creates a new route for the router. 225 | function router:route(options, ...) 226 | 227 | -- Type checking 228 | assert(..., "Expected a callback function as the second argument.") 229 | if type(options) == "string" then 230 | options = {path = options} 231 | end 232 | assert(type(options) == "table", "Expected the first argument options to be a table or string.") 233 | 234 | local checkMethod = options.method and (options.method ~= "*") 235 | local routeF = nil 236 | local hostF = nil 237 | local methodF = nil 238 | 239 | if options.path then 240 | if type(options.path) == 'function' then 241 | routeF = options.path 242 | else 243 | routeF = compileRoute(options.path) 244 | end 245 | end 246 | 247 | if options.host then 248 | if type(options.host) == 'function' then 249 | hostF = options.host 250 | else 251 | hostF = compileHost(options.host) 252 | end 253 | end 254 | 255 | if options.method then 256 | if type(options.method) == 'function' then 257 | methodF = options.method 258 | elseif type(options.method) == 'table' then 259 | local okMethods = {} 260 | for _, m in ipairs(options.methods) do 261 | okMethods[m:upper()] = true 262 | end 263 | function methodF(method) 264 | return okMethods[method] 265 | end 266 | else 267 | local m = options.method 268 | function methodF(method) 269 | return method == m 270 | end 271 | end 272 | end 273 | 274 | local callback = makeChain(...) 275 | return self:use(function(req, go) 276 | if checkMethod and not methodF(req.method) then return go() end 277 | if hostF and not hostF(req.host) then return go() end 278 | if routeF then 279 | local matches = routeF(req.path) 280 | if not matches then return go() end 281 | addParams(req, matches) 282 | end 283 | return callback(req, go) 284 | end) 285 | end 286 | 287 | -- Routes the request through the appropriate middlewares. 288 | function router:doRoute(req, go) 289 | return chain(self, 1, req, go) 290 | end 291 | 292 | -- Helper to make aliases for different common routes. 293 | local function makeAlias(method, alias) 294 | router[alias] = function(self, path, ...) 295 | return self:route({ 296 | method = method, 297 | path = path 298 | }, ...) 299 | end 300 | end 301 | 302 | local function alias(method) 303 | makeAlias(method:upper(), method:lower()) 304 | end 305 | 306 | makeAlias("*", "all") 307 | 308 | -- HTTP version 1.1 309 | alias('get') 310 | alias('put') 311 | alias('post') 312 | alias('delete') 313 | alias('head') 314 | alias('options') 315 | alias('trace') 316 | alias('connect') 317 | 318 | -- WebDAV 319 | alias('bcopy') 320 | alias('bdelete') 321 | alias('bmove') 322 | alias('bpropfind') 323 | alias('bproppatch') 324 | alias('copy') 325 | alias('lock') 326 | alias('mkcol') 327 | alias('move') 328 | alias('notify') 329 | alias('poll') 330 | alias('propfind') 331 | alias('search') 332 | alias('subscribe') 333 | alias('unlock') 334 | alias('unsubscribe') 335 | 336 | router_mt = { 337 | __index = router, 338 | __call = router.doRoute 339 | } 340 | 341 | router.metatable = router_mt 342 | 343 | return router 344 | -------------------------------------------------------------------------------- /moonmint/server.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | --- The main application object. 20 | -- @module moonmint.server 21 | 22 | local uv = require 'luv' 23 | local httpCodec = require 'moonmint.deps.httpCodec' 24 | local router = require 'moonmint.router' 25 | local httpHeaders = require 'moonmint.deps.http-headers' 26 | local getHeaders = httpHeaders.getHeaders 27 | local coroWrap = require 'moonmint.deps.coro-wrapper' 28 | local wrapStream = require 'moonmint.deps.stream-wrap' 29 | 30 | local setmetatable = setmetatable 31 | local type = type 32 | local match = string.match 33 | local tostring = tostring 34 | local assert = assert 35 | local xpcall = xpcall 36 | local wrap = coroutine.wrap 37 | 38 | local Server = {} 39 | local Server_mt = { 40 | __index = Server 41 | } 42 | 43 | local function makeServer() 44 | return setmetatable({ 45 | bindings = {}, 46 | netServers = {}, 47 | _router = router() 48 | }, Server_mt) 49 | end 50 | 51 | local function makeResponseHead(res) 52 | local head = { 53 | code = res.code or 200 54 | } 55 | local contentLength 56 | local chunked 57 | local headers = res.headers 58 | if headers then 59 | for i = 1, #headers do 60 | local header = headers[i] 61 | local key, value = tostring(header[1]), tostring(header[2]) 62 | key = key:lower() 63 | if key == "content-length" then 64 | contentLength = value 65 | elseif key == "content-encoding" and value:lower() == "chunked" then 66 | chunked = true 67 | end 68 | head[#head + 1] = headers[i] 69 | end 70 | end 71 | local body = res.body 72 | if type(body) == "string" then 73 | if not chunked and not contentLength then 74 | head[#head + 1] = {"Content-Length", #body} 75 | end 76 | end 77 | return head 78 | end 79 | 80 | local function onConnect(self, binding, socket) 81 | local rawRead, rawWrite, close = wrapStream(socket) 82 | local read, updateDecoder = coroWrap.reader(rawRead, httpCodec.decoder()) 83 | local write, updateEncoder = coroWrap.writer(rawWrite, httpCodec.encoder()) 84 | while true do 85 | local head = read() 86 | if type(head) ~= 'table' then 87 | break 88 | end 89 | local url = head.path or "" 90 | local path, rawQuery = match(url, "^([^%?]*)[%?]?(.*)$") 91 | local req = { 92 | app = self, 93 | socket = socket, 94 | method = head.method, 95 | url = url, 96 | path = path, 97 | originalPath = path, 98 | rawQuery = rawQuery, 99 | binding = binding, 100 | read = read, 101 | close = close, 102 | headers = getHeaders(head), 103 | version = head.version, 104 | keepAlive = head.keepAlive 105 | } 106 | local res = self._router:doRoute(req) 107 | if type(res) ~= 'table' then 108 | break 109 | end 110 | -- Write response 111 | write(makeResponseHead(res)) 112 | local body = res.body 113 | write(body and tostring(body) or nil) 114 | write() 115 | -- Drop non-keepalive and unhandled requets 116 | if not (res.keepAlive and head.keepAlive) then 117 | break 118 | end 119 | -- Handle upgrade requests 120 | if res.upgrade then 121 | res.upgrade(read, write, updateDecoder, updateEncoder, socket) 122 | break 123 | end 124 | end 125 | return close() 126 | end 127 | 128 | function Server:bind(options) 129 | options = options or {} 130 | if not options.host then 131 | options.host = '0.0.0.0' 132 | end 133 | if not options.port then 134 | options.port = uv.getuid() == 0 and 135 | (options.tls and 443 or 80) or 136 | (options.tls and 8443 or 8080) 137 | end 138 | self.bindings[#self.bindings + 1] = options 139 | return self 140 | end 141 | 142 | function Server:close() 143 | local netServers = self.netServers 144 | for i = 1, #netServers do 145 | local s = netServers[i] 146 | s:close() 147 | end 148 | end 149 | 150 | local function addOnStart(fn, a, b) 151 | if fn then 152 | local timer = uv.new_timer() 153 | timer:start(0, 0, coroutine.wrap(function() 154 | timer:close() 155 | return fn(a, b) 156 | end)) 157 | end 158 | end 159 | 160 | local function defaultOnErr(msg) 161 | if debug then 162 | print(debug.traceback(msg)) 163 | else 164 | print(msg) 165 | end 166 | end 167 | 168 | function Server:startLater(options) 169 | if not options then 170 | options = {} 171 | end 172 | if options.port or options.host then 173 | self:bind(options) 174 | end 175 | if #self.bindings < 1 then 176 | self:bind() 177 | end 178 | local bindings = self.bindings 179 | for i = 1, #bindings do 180 | local binding = bindings[i] 181 | local tls = binding.tls or options.tls 182 | binding.tls = not not tls 183 | local socketWrap 184 | if tls then 185 | local secureSocket = require('moonmint.deps.secure-socket') 186 | local tlsOptions = {} 187 | for k, v in pairs(tls) do tlsOptions[k] = v end 188 | tlsOptions.server = true 189 | socketWrap = function(x) return assert(secureSocket(x, tlsOptions)) end 190 | else 191 | socketWrap = function(x) return x end 192 | end 193 | local server = uv.new_tcp() 194 | -- Set the err handler. False explicitely disables it. 195 | local onErr = binding.errHand 196 | if onErr == nil then 197 | onErr = options.errHand 198 | if onErr == nil then 199 | onErr = defaultOnErr 200 | end 201 | end 202 | table.insert(self.netServers, server) 203 | assert(server:bind(binding.host, binding.port)) 204 | assert(server:listen(256, function(err) 205 | assert(not err, err) 206 | local socket = uv.new_tcp() 207 | server:accept(socket) 208 | wrap(function() 209 | if onErr then 210 | return xpcall(function() 211 | return onConnect(self, binding, socketWrap(socket)) 212 | end, onErr) 213 | else 214 | return onConnect(self, binding, socketWrap(socket)) 215 | end 216 | end)() 217 | end)) 218 | addOnStart(binding.onStart, self, binding) 219 | end 220 | addOnStart(options.onStart, self) 221 | self._startPending = true 222 | return self 223 | end 224 | 225 | function Server:start(options) 226 | if not self._startPending then 227 | self:startLater(options) 228 | self._startPending = false 229 | end 230 | uv.run() 231 | return self 232 | end 233 | 234 | -- Duplicate router functions for the server, so routes and middleware can be placed 235 | -- directly on the server. 236 | local function alias(fname) 237 | Server[fname] = function(self, ...) 238 | local r = self._router 239 | r[fname](r, ...) 240 | return self 241 | end 242 | end 243 | 244 | alias("use") 245 | alias("route") 246 | alias("all") 247 | 248 | -- HTTP version 1.1 249 | alias('get') 250 | alias('put') 251 | alias('post') 252 | alias('delete') 253 | alias('head') 254 | alias('options') 255 | alias('trace') 256 | alias('connect') 257 | 258 | -- WebDAV 259 | alias('bcopy') 260 | alias('bdelete') 261 | alias('bmove') 262 | alias('bpropfind') 263 | alias('bproppatch') 264 | alias('copy') 265 | alias('lock') 266 | alias('mkcol') 267 | alias('move') 268 | alias('notify') 269 | alias('poll') 270 | alias('propfind') 271 | alias('search') 272 | alias('subscribe') 273 | alias('unlock') 274 | alias('unsubscribe') 275 | 276 | return makeServer 277 | -------------------------------------------------------------------------------- /moonmint/static.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | --- Middleware for serving static files. 20 | -- @module moonmint.static 21 | 22 | local uv = require 'luv' 23 | local mime = require('moonmint.deps.mimetypes').guess 24 | local newHeaders = require 'moonmint.deps.http-headers'.newHeaders 25 | local response = require 'moonmint.response' 26 | local pathJoin = require('moonmint.deps.pathjoin').pathJoin 27 | local fs = require 'moonmint.fs' 28 | local setmetatable = setmetatable 29 | 30 | local Static = {} 31 | local Static_mt = { 32 | __index = Static 33 | } 34 | 35 | function Static:_notFound(_, go) 36 | if self.fallthrough then 37 | return go() 38 | else 39 | return response('Not Found', 404) 40 | end 41 | end 42 | 43 | function Static:renderFile(req, go, path) 44 | local body = self.fs.readFile(path) 45 | if not body then return self:_notFound(req, go) end 46 | local mimetype = mime(path) 47 | return { 48 | code = 200, 49 | headers = newHeaders { 50 | {'Content-Length', #body}, 51 | {'Content-Type', mimetype} 52 | }, 53 | body = body 54 | } 55 | end 56 | 57 | function Static:renderDirectory(req, go, path) 58 | local ri = self.renderIndex 59 | local rit = type(ri) 60 | if rit == 'table' then 61 | for i = 1, #ri do 62 | local testIndex = pathJoin(path, ri[i]) 63 | local stat = self.fs.stat(testIndex); 64 | if stat and stat.type ~= 'directory' then 65 | return self:renderFile(req, go, testIndex) 66 | end 67 | end 68 | return self:_notFound(req, go) 69 | elseif rit == 'function' then 70 | local iter = self.fs.scandir(path) 71 | return ri(self, path, iter, req, go) 72 | else 73 | return self:renderFile(req, go, pathJoin(path, ri)) 74 | end 75 | end 76 | 77 | function Static:doRoute(req, go) 78 | if req.method ~= "GET" then return go() end 79 | local path = req.path 80 | if path:byte() == 47 then 81 | path = '.' .. path 82 | end 83 | local stat = self.fs.stat(path) 84 | if not stat then 85 | return self:_notFound(req, go) 86 | end 87 | if stat.type == "directory" then 88 | if self.renderIndex then 89 | return self:renderDirectory(req, go, path) 90 | else 91 | return self:_notFound(req, go) 92 | end 93 | else 94 | return self:renderFile(req, go, path) 95 | end 96 | end 97 | 98 | Static_mt.__call = Static.doRoute 99 | 100 | return function(options) 101 | if type(options) == 'string' then 102 | options = { 103 | base = options 104 | } 105 | end 106 | options = options or {} 107 | local base = options.base or '.' 108 | if not options.absolute then 109 | base = pathJoin(uv.cwd(), base) 110 | end 111 | local fallthrough = true 112 | if options.fallthrough ~= nil then 113 | fallthrough = options.fallthrough 114 | end 115 | local renderIndex = 'index.html' 116 | if options.renderIndex ~= nil and 117 | options.renderIndex ~= true then 118 | renderIndex = options.renderIndex 119 | end 120 | return setmetatable({ 121 | fs = fs.chroot(base), 122 | fallthrough = fallthrough, 123 | renderIndex = renderIndex 124 | }, Static_mt) 125 | end 126 | -------------------------------------------------------------------------------- /moonmint/template.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2015 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | --- String templating that is optimized for HTML. 20 | -- @module moonmint.template 21 | 22 | local setmetatable = setmetatable 23 | local rawget = rawget 24 | local byte = string.byte 25 | local tostring = tostring 26 | 27 | local bracketTypes = { 28 | [byte("{")] = "variables", 29 | [byte("%")] = "lua", 30 | [byte("$")] = "luaexpr", 31 | [byte("#")] = "comment", 32 | [byte('"')] = "literal", 33 | [byte("'")] = "literal", 34 | } 35 | 36 | local btypeClosers = { 37 | [byte("{")] = byte("}") 38 | } 39 | 40 | local function skipToCharWithEscapes(str, i, ch, escape) 41 | escape = escape or 92 42 | while(byte(str, i) ~= ch) do 43 | i = i + 1 44 | if not byte(str, i) then return 0 end 45 | if byte(str, i) == escape then 46 | i = i + 1 47 | end 48 | end 49 | return i 50 | end 51 | 52 | local htmlEscapeMap = { 53 | ['>'] = '>', 54 | ['<'] = '<', 55 | ['"'] = '"', 56 | ["'"] = ''', 57 | ['/'] = '/', 58 | ['&'] = '&', 59 | } 60 | 61 | local function htmlEscape(str) 62 | str = str or '' 63 | return str:gsub("[\">/<'&]", htmlEscapeMap) 64 | end 65 | 66 | local backslashEscapeMap = { 67 | n = '\n', 68 | t = '\t', 69 | r = '\r' 70 | } 71 | 72 | local function backslashSub(c) 73 | return backslashEscapeMap[c] or c 74 | end 75 | 76 | local function backslashEscape(str) 77 | return str:gsub('%\\(.)', backslashSub) 78 | end 79 | 80 | local function skipToBracket(str, i) 81 | local j = i 82 | while true do 83 | j = skipToCharWithEscapes(str, j, 123) 84 | if not j or j == 0 then return 0 end 85 | if bracketTypes[byte(str, j + 1)] or byte(str, j + 1) == 45 then 86 | return j 87 | end 88 | j = j + 1 89 | end 90 | end 91 | 92 | local function getchr(c) 93 | return "\\" .. c:byte() 94 | end 95 | 96 | local function make_safe(text) 97 | return ("%q"):format(tostring(text)):gsub('\n', 'n'):gsub("[\128-\255]", getchr) 98 | end 99 | 100 | local function trim_start(s) 101 | return s:match("^%s*(.-)$") 102 | end 103 | 104 | local function trim_end(s) 105 | return s:match("^(.-)%s*$") 106 | end 107 | 108 | local function trim(s) 109 | return s:match("^%s*(.-)%s*$") 110 | end 111 | 112 | local function primaryEscape(str) 113 | local buffer = {} 114 | local index = 1 115 | local trim_whitespace = false 116 | while true do 117 | 118 | -- Skip to an open bracket 119 | local j = skipToBracket(str, index) 120 | local bnext = str:sub(index, j - 1) 121 | if trim_whitespace then 122 | bnext = trim_start(bnext) 123 | end 124 | buffer[#buffer + 1] = bnext 125 | if not j or j == 0 then break end 126 | 127 | -- Get the bracket type 128 | index = j + 1 129 | local nextindex = index 130 | local btype = byte(str, index) 131 | if btype == 45 then 132 | nextindex = nextindex + 1 133 | buffer[#buffer] = trim_end(buffer[#buffer]) 134 | btype = byte(str, nextindex) 135 | end 136 | local btypeCloser = btypeClosers[btype] or btype 137 | local btypename = bracketTypes[btype] 138 | 139 | -- Check for non escape 140 | if not btypename then 141 | trim_whitespace = false 142 | else 143 | index = nextindex 144 | 145 | -- Skip to the next closing bracket of the same type. 146 | j = index 147 | while byte(str, j) ~= 125 do 148 | trim_whitespace = false 149 | j = skipToCharWithEscapes(str, j, btypeCloser) 150 | if j == 0 then return nil, "Unexpected end of template." end 151 | j = j + 1 152 | if byte(str, j) == 45 then 153 | j = j + 1 154 | trim_whitespace = true 155 | end 156 | end 157 | 158 | -- Append the body of the brackets 159 | local htmlEscaped = true 160 | local body = str:sub(index + 1, j - 2 - (trim_whitespace and 1 or 0)) 161 | if btypename ~= 'literal' then 162 | body = trim(body) 163 | if body:byte() == 38 then 164 | htmlEscaped = false 165 | body = body:sub(2, -1) 166 | end 167 | end 168 | buffer[#buffer + 1] = { 169 | type = btypename, 170 | htmlEscape = htmlEscaped, 171 | body = body 172 | } 173 | 174 | index = j + 1 175 | end 176 | end 177 | return buffer 178 | end 179 | 180 | local function wrapOptions(options) 181 | return setmetatable({}, { 182 | __index = function(self, key) 183 | local raw = rawget(self, key) 184 | if raw ~= nil then 185 | return raw 186 | end 187 | raw = htmlEscape(tostring(options[key])) 188 | self[key] = raw 189 | return raw 190 | end 191 | }) 192 | end 193 | 194 | return function(body) 195 | 196 | local ast, err = primaryEscape(body) 197 | if err then 198 | return error(err) 199 | end 200 | 201 | local b = {"local a={}\nlocal context, e=...\nlocal c=context\n"} 202 | 203 | for i = 1, #ast do 204 | local part = ast[i] 205 | if type(part) == "string" then 206 | b[#b + 1] = ("a[#a+1]=%s\n"):format(make_safe(backslashEscape(part))) 207 | elseif part.type == "variables" then 208 | b[#b + 1] = ("a[#a+1]=%s[%s]\n") 209 | :format(part.htmlEscape and 'e' or 'c', make_safe(part.body)) 210 | elseif part.type == "lua" then 211 | b[#b + 1] = ("\n%s\n"):format(part.body) 212 | elseif part.type == "luaexpr" then 213 | b[#b + 1] = ("a[#a+1]=(%s)\n"):format(part.body) 214 | elseif part.type == "literal" then 215 | local pbody = backslashEscape(part.body) 216 | if part.htmlEscape then 217 | pbody = htmlEscape(pbody) 218 | end 219 | b[#b + 1] = ("a[#a+1]=%s\n"):format(make_safe(pbody)) 220 | end 221 | end 222 | 223 | b[#b + 1] = "return table.concat(a)" 224 | local code = table.concat(b) 225 | local ret 226 | ret, err = (loadstring or load)(code) 227 | if ret then 228 | return function(options) 229 | return ret(options, wrapOptions(options)) 230 | end 231 | else 232 | error(err) 233 | end 234 | 235 | end 236 | -------------------------------------------------------------------------------- /moonmint/url.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | --- URL manipulation utilities. 20 | -- @module moonmint.url 21 | 22 | local char = string.char 23 | local byte = string.byte 24 | local tonumber = tonumber 25 | local gsub = string.gsub 26 | local pairs = pairs 27 | local type = type 28 | local tostring = tostring 29 | local concat = table.concat 30 | local gmatch = string.gmatch 31 | local format = string.format 32 | 33 | -- Encodes a Lua string into a url-safe string component by escaping characters. 34 | local function urlEncode(str) 35 | if str then 36 | str = gsub(str, '\n', '\r\n') 37 | str = gsub(str, '[^%w ]', function (c) 38 | return format('%%%02X', byte(c)) 39 | end) 40 | str = gsub(str, ' ', '+') 41 | end 42 | return str 43 | end 44 | 45 | -- Converts a url-string component into a Lua string by unescaping escaped characters in 46 | -- the url. Maps 1 to 1 with util.urlEncode 47 | local function urlDecode(str) 48 | str = gsub(str, "%+", " ") 49 | str = gsub(str, "%%(%x%x)", function (c) 50 | return char(tonumber(c, 16)) 51 | end) 52 | str = gsub(str, '\r\n', '\n') 53 | return str 54 | end 55 | 56 | -- Encode a Lua object as query string in a URL. The returned query string can be appended 57 | -- to a url by append a '?' character as well as the query string to the original url. 58 | local queryValidKeyTypes = { string = true, number = true, boolean = true } 59 | local queryValidValueTypes = { string = true, number = true, boolean = true } 60 | local function queryEncode(object) 61 | local t = {} 62 | local len = 0 63 | for k, v in pairs(object) do 64 | local kstr, vstr 65 | if not queryValidKeyTypes[type(k)] then 66 | error("Invalid query key type '" .. type(k) .. "'.") 67 | else 68 | kstr = tostring(k) 69 | end 70 | local vtype = type(v) 71 | if not queryValidValueTypes[vtype] then 72 | error("Invalid query value type '" .. vtype .. "'.") 73 | else 74 | vstr = tostring(v) 75 | end 76 | t[len + 1] = urlEncode(kstr) 77 | t[len + 2] = "=" 78 | t[len + 3] = urlEncode(vstr) 79 | t[len + 4] = "&" 80 | len = len + 4 81 | end 82 | if #t == 0 then 83 | return "" 84 | else 85 | t[#t] = nil 86 | return concat(t) 87 | end 88 | end 89 | 90 | -- Converts a query string as produced by util.queryEncode to a Lua table. 91 | -- Strings will be converted to numbers if possible. 92 | local function queryDecode(str) 93 | local ret = nil 94 | for key, value in gmatch(str, "([^%&%=]+)=([^%&%=]*)") do 95 | ret = ret or {} 96 | local keyDecoded = urlDecode(key) 97 | local valueDecoded = urlDecode(value) 98 | 99 | -- Attempt number conversion 100 | valueDecoded = tonumber(valueDecoded) or valueDecoded 101 | keyDecoded = tonumber(keyDecoded) or keyDecoded 102 | 103 | ret[keyDecoded] = valueDecoded 104 | end 105 | return ret 106 | end 107 | 108 | return { 109 | encode = urlEncode, 110 | decode = urlDecode, 111 | queryEncode = queryEncode, 112 | queryDecode = queryDecode 113 | } 114 | -------------------------------------------------------------------------------- /moonmint/util.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | --- Various utility functions and middleware. 20 | -- @module moonmint.util 21 | -- @author Calvin Rose 22 | -- @copyright 2016 23 | 24 | local uv = require 'luv' 25 | local format = string.format 26 | local concat = table.concat 27 | local queryDecode = require('moonmint.url').queryDecode 28 | 29 | local util = {} 30 | 31 | --- A middleware for getting the body of an HTTP request. 32 | -- The body is a string placed in req.body. 33 | function util.bodyParser(req, go) 34 | local parts = {} 35 | local read = req.read 36 | for chunk in read do 37 | if #chunk > 0 then 38 | parts[#parts + 1] = chunk 39 | else 40 | break 41 | end 42 | end 43 | req.body = #parts > 0 and concat(parts) or nil 44 | return go() 45 | end 46 | 47 | --- Simple logger middleware. 48 | -- Logs in the format 'time | method | path | return status'. 49 | function util.logger(req, go) 50 | local time = os.date() 51 | local res = go() 52 | local status = pcall( 53 | print, 54 | format("%s | %s | %s | %s", 55 | time, 56 | req.method, 57 | req.path, 58 | res and (res.code or 200) or 'No Response')) 59 | if not status then 60 | print 'Could not log response.' 61 | end 62 | return res 63 | end 64 | 65 | --- A middleware for parsing queries on requests. 66 | -- Places the queries in req.query. 67 | function util.queryParser(req, go) 68 | req.query = queryDecode(req.rawQuery or "") 69 | return go() 70 | end 71 | 72 | local unpack = unpack or table.unpack 73 | local cwrap = coroutine.wrap 74 | 75 | --- Calls a coroutine function that is asynchronous with libuv 76 | -- and makes it blocking 77 | function util.callSync(fn, ...) 78 | local ret 79 | local loop = true 80 | cwrap(function(...) 81 | ret = {fn(...)} 82 | loop = false 83 | end)(...) 84 | while loop do 85 | uv.run('once') 86 | end 87 | return unpack(ret) 88 | end 89 | 90 | function util.wrapSync(fn) 91 | return function(...) 92 | return util.callSync(fn, ...) 93 | end 94 | end 95 | 96 | return util 97 | -------------------------------------------------------------------------------- /rockspecs/moonmint-scm-3.rockspec: -------------------------------------------------------------------------------- 1 | package = "moonmint" 2 | version = "scm-3" 3 | source = { 4 | url = "git://github.com/bakpakin/moonmint.git", 5 | tag = "master" 6 | } 7 | description = { 8 | homepage = "https://github.com/bakpakin/moonmint", 9 | summary = "Web framework for Lua", 10 | license = "MIT", 11 | } 12 | dependencies = { 13 | "lua >= 5.1", 14 | "luv ~> 1.8", 15 | "mimetypes >= 1.0", 16 | "bkopenssl >= 0.0", 17 | "bit32" 18 | } 19 | build = { 20 | type = "builtin", 21 | modules = { 22 | ["moonmint"] = "moonmint/init.lua", 23 | 24 | ["moonmint.agent"] = "moonmint/agent.lua", 25 | ["moonmint.fs"] = "moonmint/fs.lua", 26 | ["moonmint.html"] = "moonmint/html.lua", 27 | ["moonmint.response"] = "moonmint/response.lua", 28 | ["moonmint.router"] = "moonmint/router.lua", 29 | ["moonmint.server"] = "moonmint/server.lua", 30 | ["moonmint.static"] = "moonmint/static.lua", 31 | ["moonmint.template"] = "moonmint/template.lua", 32 | ["moonmint.url"] = "moonmint/url.lua", 33 | ["moonmint.util"] = "moonmint/util.lua", 34 | 35 | ["moonmint.deps.http-headers"] = "moonmint/deps/http-headers.lua", 36 | ["moonmint.deps.coro-wrapper"] = "moonmint/deps/coro-wrapper.lua", 37 | ["moonmint.deps.coro-channel"] = "moonmint/deps/coro-channel.lua", 38 | ["moonmint.deps.coro-net"] = "moonmint/deps/coro-net.lua", 39 | ["moonmint.deps.httpCodec"] = "moonmint/deps/httpCodec.lua", 40 | ["moonmint.deps.secure-socket.biowrap"] = "moonmint/deps/secure-socket/biowrap.lua", 41 | ["moonmint.deps.secure-socket.context"] = "moonmint/deps/secure-socket/context.lua", 42 | ["moonmint.deps.secure-socket.root_ca"] = "moonmint/deps/secure-socket/root_ca.lua", 43 | ["moonmint.deps.secure-socket"] = "moonmint/deps/secure-socket/init.lua", 44 | ["moonmint.deps.pathjoin"] = "moonmint/deps/pathjoin.lua" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rockspecs/moonmint-scm-4.rockspec: -------------------------------------------------------------------------------- 1 | package = "moonmint" 2 | version = "scm-4" 3 | source = { 4 | url = "git://github.com/bakpakin/moonmint.git", 5 | tag = "master" 6 | } 7 | description = { 8 | homepage = "https://github.com/bakpakin/moonmint", 9 | summary = "Web framework for Lua", 10 | license = "MIT", 11 | } 12 | dependencies = { 13 | "lua >= 5.1", 14 | "luv ~> 1.8", 15 | "mimetypes >= 1.0", 16 | "bkopenssl >= 0.0", 17 | "bit32" 18 | } 19 | build = { 20 | type = "builtin", 21 | modules = { 22 | ["moonmint"] = "moonmint/init.lua", 23 | 24 | ["moonmint.agent"] = "moonmint/agent.lua", 25 | ["moonmint.fs"] = "moonmint/fs.lua", 26 | ["moonmint.html"] = "moonmint/html.lua", 27 | ["moonmint.response"] = "moonmint/response.lua", 28 | ["moonmint.router"] = "moonmint/router.lua", 29 | ["moonmint.server"] = "moonmint/server.lua", 30 | ["moonmint.static"] = "moonmint/static.lua", 31 | ["moonmint.template"] = "moonmint/template.lua", 32 | ["moonmint.url"] = "moonmint/url.lua", 33 | ["moonmint.util"] = "moonmint/util.lua", 34 | 35 | ["moonmint.deps.http-headers"] = "moonmint/deps/http-headers.lua", 36 | ["moonmint.deps.coro-wrapper"] = "moonmint/deps/coro-wrapper.lua", 37 | ["moonmint.deps.httpCodec"] = "moonmint/deps/httpCodec.lua", 38 | ["moonmint.deps.stream-wrap"] = "moonmint/deps/stream-wrap.lua", 39 | ["moonmint.deps.secure-socket.biowrap"] = "moonmint/deps/secure-socket/biowrap.lua", 40 | ["moonmint.deps.secure-socket.context"] = "moonmint/deps/secure-socket/context.lua", 41 | ["moonmint.deps.secure-socket.root_ca"] = "moonmint/deps/secure-socket/root_ca.lua", 42 | ["moonmint.deps.secure-socket"] = "moonmint/deps/secure-socket/init.lua", 43 | ["moonmint.deps.pathjoin"] = "moonmint/deps/pathjoin.lua" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rockspecs/moonmint-scm-5.rockspec: -------------------------------------------------------------------------------- 1 | package = "moonmint" 2 | version = "scm-5" 3 | source = { 4 | url = "git://github.com/bakpakin/moonmint.git", 5 | tag = "master" 6 | } 7 | description = { 8 | homepage = "https://github.com/bakpakin/moonmint", 9 | summary = "Web framework for Lua", 10 | license = "MIT", 11 | } 12 | dependencies = { 13 | "lua >= 5.1", 14 | "luv ~> 1.8", 15 | "mimetypes >= 1.0", 16 | "bit32" 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["moonmint"] = "moonmint/init.lua", 22 | 23 | ["moonmint.agent"] = "moonmint/agent.lua", 24 | ["moonmint.cookie"] = "moonmint/cookie.lua", 25 | ["moonmint.fs"] = "moonmint/fs.lua", 26 | ["moonmint.html"] = "moonmint/html.lua", 27 | ["moonmint.response"] = "moonmint/response.lua", 28 | ["moonmint.router"] = "moonmint/router.lua", 29 | ["moonmint.server"] = "moonmint/server.lua", 30 | ["moonmint.static"] = "moonmint/static.lua", 31 | ["moonmint.template"] = "moonmint/template.lua", 32 | ["moonmint.url"] = "moonmint/url.lua", 33 | ["moonmint.util"] = "moonmint/util.lua", 34 | 35 | ["moonmint.deps.http-headers"] = "moonmint/deps/http-headers.lua", 36 | ["moonmint.deps.coro-wrapper"] = "moonmint/deps/coro-wrapper.lua", 37 | ["moonmint.deps.httpCodec"] = "moonmint/deps/httpCodec.lua", 38 | ["moonmint.deps.stream-wrap"] = "moonmint/deps/stream-wrap.lua", 39 | ["moonmint.deps.secure-socket.biowrap"] = "moonmint/deps/secure-socket/biowrap.lua", 40 | ["moonmint.deps.secure-socket.context"] = "moonmint/deps/secure-socket/context.lua", 41 | ["moonmint.deps.secure-socket.root_ca"] = "moonmint/deps/secure-socket/root_ca.lua", 42 | ["moonmint.deps.secure-socket"] = "moonmint/deps/secure-socket/init.lua", 43 | ["moonmint.deps.pathjoin"] = "moonmint/deps/pathjoin.lua" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/helper.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | local util = require 'moonmint.util' 20 | 21 | local function deepEquals(a, b) 22 | if type(a) ~= type(b) then 23 | return false 24 | end 25 | if type(a) == 'table' then 26 | for k, v in pairs(a) do 27 | if not deepEquals(v, b[k]) then 28 | return false 29 | end 30 | end 31 | -- Check if b has extra kv pairs 32 | for k in pairs(b) do 33 | if a[k] == nil then 34 | return false 35 | end 36 | end 37 | return true 38 | end 39 | return a == b 40 | end 41 | 42 | local function testEncodeGen(encode, decode) 43 | return function(data, encoded_form) 44 | local encoded_data = encode(data) 45 | if encoded_form then 46 | assert(deepEquals(encoded_data, encoded_form)) 47 | end 48 | assert(deepEquals(data, decode(encoded_data))) 49 | end 50 | end 51 | 52 | local function agentTester(cb) 53 | local moonmint = require 'moonmint' 54 | local agent = require 'moonmint.agent' 55 | local testServer = moonmint():startLater { 56 | errHand = false 57 | } 58 | local testAgent = agent:url('http://localhost:8080'):module() 59 | local ok, err 60 | moonmint.go(function() 61 | ok, err = pcall(cb, testAgent, testServer) 62 | agent:close() 63 | testServer:close() 64 | end) 65 | if not ok then 66 | error(err) 67 | end 68 | end 69 | 70 | return { 71 | agentTester = agentTester, 72 | encode = testEncodeGen, 73 | deepEquals = deepEquals 74 | } 75 | -------------------------------------------------------------------------------- /spec/html_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | local html = require 'moonmint.html' 20 | local helper = require 'spec.helper' 21 | local testHTML = helper.encode(html.encode, html.decode, assert) 22 | 23 | test("HTML encoding/decoding", function() 24 | 25 | test("Encodes/decodes simple html strings.", function() 26 | testHTML("zyxwvut") 27 | testHTML("1233456") 28 | end) 29 | 30 | test("Encodes/decodes strings with tags.", function() 31 | testHTML("zyxwvutakjsk") 32 | testHTML("1233456
") 33 | end) 34 | 35 | test("Encodes/decodes all bytes.", function() 36 | for i = 0, 255 do 37 | testHTML(string.char(i)) 38 | end 39 | end) 40 | 41 | test("Encodes/decodes strings of all bytes.", function() 42 | for i = 0, 250 do 43 | testHTML(string.char(i, i + 1, i + 2, i + 3, i + 4, i + 5)) 44 | end 45 | end) 46 | 47 | end) 48 | -------------------------------------------------------------------------------- /spec/query_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | local url = require 'moonmint.url' 20 | local helper = require 'spec.helper' 21 | local testQuery = helper.encode(url.queryEncode, url.queryDecode) 22 | 23 | test("Query encoding/decoding", function() 24 | 25 | test("Encodes and decodes strings", function() 26 | testQuery({ 27 | hello = "kitty", 28 | cat = "yum" 29 | }) 30 | end) 31 | 32 | test("Encodes and decodes numbers and strings", function() 33 | testQuery({ 34 | [0] = "go", 35 | jump = "How high?++++", 36 | anumber = 145.2 37 | }) 38 | end) 39 | 40 | end) 41 | -------------------------------------------------------------------------------- /spec/simpleServer_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | local helper = require 'spec.helper' 20 | 21 | test("Simple server.", function() 22 | 23 | test("Can run a basic echo server", function() 24 | helper.agentTester(function(agent, app) 25 | app:get('/', 'hi') 26 | assert(helper.deepEquals(agent:get('/'):send().body, 'hi')) 27 | end) 28 | end) 29 | 30 | end) 31 | -------------------------------------------------------------------------------- /spec/url_spec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | ]] 18 | 19 | local url = require 'moonmint.url' 20 | local helper = require 'spec.helper' 21 | local testURL = helper.encode(url.encode, url.decode) 22 | 23 | test("Url encoding/decoding", function() 24 | 25 | test("Does basic encoding/decoding of simple strings", function() 26 | testURL("abcdefg") 27 | testURL("1233456") 28 | end) 29 | 30 | test("Encodes/decodes url special characters", function() 31 | testURL("!@#$%^&*()_+.,?:;\\") 32 | end) 33 | 34 | test("Encodes/decodes those darned plus signs", function() 35 | testURL("+ + +", "%2B+%2B+%2B") 36 | end) 37 | 38 | test("Encodes/decodes all possible bytes", function() 39 | for i = 0, 255 do 40 | testURL(string.char(i)) 41 | end 42 | end) 43 | 44 | test("Encodes/decodes strings of all possible bytes", function() 45 | for i = 0, 250 do 46 | testURL(string.char(i, i + 1, i + 2, i + 3, i + 4, i + 5)) 47 | end 48 | end) 49 | 50 | end) 51 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | -- Expose the test function globally 2 | test = require 'tools.oft' 3 | 4 | local tests = { 5 | 'html', 6 | 'query', 7 | 'simpleServer', 8 | 'url' 9 | } 10 | 11 | local res = test(function() 12 | for i, v in ipairs(tests) do 13 | require('spec.' .. tests[i] .. '_spec') 14 | end 15 | end) 16 | 17 | -- Give a proper exit code for CI 18 | os.exit(res.failed == 0 and 0 or 1) 19 | -------------------------------------------------------------------------------- /tools/oft.lua: -------------------------------------------------------------------------------- 1 | -- oft.lua 2 | -- 3 | -- One Function Tests 4 | -- 5 | 6 | -- Check if we are on windows. If so, check for ANSICON env variable 7 | -- for color support. Otherwise, assume color support. 8 | local cs = package.config:sub(1,1) ~= '\\' 9 | if not cs then cs = os.getenv("ANSICON") end 10 | 11 | -- Rendering 12 | 13 | local RED = cs and '\27[31m' or '' 14 | local GREEN = cs and '\27[32m' or '' 15 | local RESET = cs and '\27[0m' or '' 16 | 17 | local checkmark = '\226\156\147' 18 | local xmark = '\226\156\151' 19 | 20 | local greenCheck = RESET .. GREEN .. checkmark .. RESET 21 | 22 | local function report(description, ok) 23 | if ok then 24 | return ('%s%s %s%s'):format(GREEN, checkmark, description, RESET) 25 | else 26 | return ('%s%s %s%s'):format(RED, xmark, description, RESET) 27 | end 28 | end 29 | 30 | local function progressStart(description) 31 | io.write('\n' .. description .. ' progress: ') 32 | end 33 | 34 | local function progress(ok, index) 35 | if ok then 36 | io.write(greenCheck) 37 | else 38 | io.write(('%s%s(%d)%s'):format(RESET, RED, index, RESET)) 39 | end 40 | end 41 | 42 | local function progressEnd() 43 | print '\n' 44 | end 45 | 46 | local function renderLevel(ast, level) 47 | local res = ast.result 48 | local rep = report(res.description, res.status) 49 | print(string.rep(' ', level) .. rep) 50 | for i = 1, #ast do 51 | renderLevel(ast[i], level + 1) 52 | end 53 | end 54 | 55 | local function render(ast) 56 | renderLevel(ast, 0) 57 | local errors, numFailed, numPassed = ast.errors, ast.failed, ast.passed 58 | if #errors > 0 then 59 | print(('\n%s%d Errors: \n'):format(RED, #errors)) 60 | for i = 1, #errors do 61 | print(i .. ': ' .. errors[i].long) 62 | end 63 | end 64 | print(('\n%s%d%s Failed, %s%d%s Passed.'):format( 65 | RED, numFailed, RESET, 66 | GREEN, numPassed, RESET)) 67 | end 68 | 69 | -- Testing 70 | 71 | local dynamicTest 72 | 73 | local function test(description, fn, errors) 74 | if not fn then 75 | fn = description 76 | description = 'sub test' 77 | end 78 | local ast = {} 79 | local failed, passed = 0, 0 80 | local function subtest(desc, f) 81 | local ret = test(desc, f, errors) 82 | ast[#ast + 1] = ret 83 | passed = passed + ret.passed 84 | failed = failed + ret.failed 85 | return ret 86 | end 87 | local oldDyanmic = dynamicTest 88 | dynamicTest = subtest 89 | local ok, err = pcall(fn) 90 | dynamicTest = oldDyanmic 91 | local errorIndex = #errors + 1 92 | progress(ok, errorIndex) 93 | if not ok then 94 | errors[errorIndex] = { 95 | short = err, 96 | long = debug.traceback(err) 97 | } 98 | end 99 | ast.result = { 100 | description = description, 101 | status = ok, 102 | err = err 103 | } 104 | ast.passed = passed + (ok and 1 or 0) 105 | ast.failed = failed + (ok and 0 or 1) 106 | return ast 107 | end 108 | 109 | local function rootTest(description, fn) 110 | if not fn then 111 | fn = description 112 | description = 'root test' 113 | end 114 | local errors = {} 115 | progressStart(description) 116 | local results = test(description, fn, errors) 117 | progressEnd(description) 118 | results.errors = errors 119 | render(results) 120 | return results 121 | end 122 | dynamicTest = rootTest 123 | 124 | return function(...) 125 | return dynamicTest(...) 126 | end 127 | --------------------------------------------------------------------------------