├── embed
├── extraParams.hxml
├── submit.sh
└── haxelib.json
├── tests.hxml
├── .gitignore
├── docs
└── console.png
├── examples
├── 02_static_file_server
│ ├── public
│ │ └── index.html
│ ├── build_cpp.hxml
│ ├── run_neko.hxml
│ ├── run_node.hxml
│ ├── run_java.hxml
│ ├── Server.hx
│ └── README.md
└── 01_hello_world
│ ├── build_cpp.hxml
│ ├── run_neko.hxml
│ ├── run_php.hxml
│ ├── run_java.hxml
│ ├── run_node.hxml
│ ├── Server.hx
│ └── README.md
├── submit.sh
├── src
├── Monsoon.hx
└── monsoon
│ ├── middleware
│ ├── ThreadServer.hx
│ ├── Console.hx
│ ├── BasicAuth.hx
│ ├── Static.hx
│ ├── ByteRange.hx
│ └── Compression.hx
│ ├── macro
│ └── RequestBuilder.hx
│ ├── Request.hx
│ ├── Layer.hx
│ ├── Monsoon.hx
│ └── Response.hx
├── tests
├── RunTests.hx
├── TestCompression.hx
├── TestStatic.hx
├── TestMiddleware.hx
├── TestTools.hx
├── TestRouteController.hx
├── TestBasicAuth.hx
├── TestRequest.hx
├── TestByteRange.hx
├── TestResponse.hx
└── TestRouter.hx
├── haxelib.json
├── .travis.yml
└── README.md
/embed/extraParams.hxml:
--------------------------------------------------------------------------------
1 | -D concurrent
2 | -D embed
--------------------------------------------------------------------------------
/tests.hxml:
--------------------------------------------------------------------------------
1 | -cp ./tests/
2 | -lib buddy
3 | -main RunTests
4 | -dce full
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | /tests_old
3 | /build
4 | *.hxproj
5 | /modd.conf
6 | *.mp4
--------------------------------------------------------------------------------
/docs/console.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benmerckx/monsoon/HEAD/docs/console.png
--------------------------------------------------------------------------------
/examples/02_static_file_server/public/index.html:
--------------------------------------------------------------------------------
1 |
Static file server
2 |
3 | Example
--------------------------------------------------------------------------------
/examples/01_hello_world/build_cpp.hxml:
--------------------------------------------------------------------------------
1 | -lib monsoon-embed
2 | -main Server
3 | -cpp bin/cpp
4 | -dce full
5 | -D static
--------------------------------------------------------------------------------
/examples/02_static_file_server/build_cpp.hxml:
--------------------------------------------------------------------------------
1 | -lib monsoon-embed
2 | -main Server
3 | -cpp bin/cpp
4 | -dce full
5 | -D static
--------------------------------------------------------------------------------
/examples/01_hello_world/run_neko.hxml:
--------------------------------------------------------------------------------
1 | -lib monsoon-embed
2 | -main Server
3 | -neko bin/neko/server.n
4 | -cmd neko bin/neko/server.n
--------------------------------------------------------------------------------
/examples/01_hello_world/run_php.hxml:
--------------------------------------------------------------------------------
1 | -lib monsoon
2 | -main Server
3 | -php bin/php
4 | -cmd php -S 0.0.0.0:3000 bin/php/index.php
--------------------------------------------------------------------------------
/examples/02_static_file_server/run_neko.hxml:
--------------------------------------------------------------------------------
1 | -lib monsoon-embed
2 | -main Server
3 | -neko bin/neko/server.n
4 | -cmd neko bin/neko/server.n
--------------------------------------------------------------------------------
/submit.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | zip -r monsoon.zip src haxelib.json README.md -x "*/\.*"
3 | haxelib submit monsoon.zip
4 | rm monsoon.zip 2> /dev/null
--------------------------------------------------------------------------------
/examples/01_hello_world/run_java.hxml:
--------------------------------------------------------------------------------
1 | -lib monsoon-embed
2 | -lib hxjava
3 | -main Server
4 | -java bin/java/
5 | -cmd java -jar bin/java/Server.jar
--------------------------------------------------------------------------------
/examples/01_hello_world/run_node.hxml:
--------------------------------------------------------------------------------
1 | -lib monsoon
2 | -lib hxnodejs
3 | -main Server
4 | -js bin/node/server.js
5 | -cmd node bin/node/server.js
--------------------------------------------------------------------------------
/examples/02_static_file_server/run_node.hxml:
--------------------------------------------------------------------------------
1 | -lib monsoon
2 | -lib hxnodejs
3 | -main Server
4 | -js bin/node/server.js
5 | -cmd node bin/node/server.js
--------------------------------------------------------------------------------
/embed/submit.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | zip -r monsoon.zip src haxelib.json extraParams.hxml -x "*/\.*"
3 | haxelib submit monsoon.zip
4 | rm monsoon.zip 2> /dev/null
--------------------------------------------------------------------------------
/examples/02_static_file_server/run_java.hxml:
--------------------------------------------------------------------------------
1 | -lib monsoon-embed
2 | -lib hxjava
3 | -main Server
4 | -java bin/java/
5 | -cmd java -jar bin/java/Server.jar
--------------------------------------------------------------------------------
/src/Monsoon.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | typedef Monsoon = monsoon.Monsoon;
4 | typedef Response = monsoon.Response;
5 | typedef Layer = monsoon.Layer;
6 |
7 | @:genericBuild(monsoon.macro.RequestBuilder.buildGeneric())
8 | class Request {}
--------------------------------------------------------------------------------
/examples/01_hello_world/Server.hx:
--------------------------------------------------------------------------------
1 | using Monsoon;
2 |
3 | class Server {
4 | public static function main() {
5 | var app = new Monsoon();
6 |
7 | app.route('/', function (req, res)
8 | res.send('Hello World')
9 | );
10 |
11 | app.listen(3000);
12 | }
13 | }
--------------------------------------------------------------------------------
/examples/02_static_file_server/Server.hx:
--------------------------------------------------------------------------------
1 | using Monsoon;
2 |
3 | import monsoon.middleware.Static;
4 |
5 | class Server {
6 | static function main() {
7 | var port = 5000;
8 | var app = new Monsoon();
9 | app.use(Static.serve('public'));
10 | app.listen(port);
11 | trace('Server ready and listening on http://localhost:${port}');
12 | }
13 | }
--------------------------------------------------------------------------------
/examples/01_hello_world/README.md:
--------------------------------------------------------------------------------
1 | # 01
2 |
3 | ### Hello world
4 |
5 | The classic hello world. Should respond with 'Hello world' on http://localhost:3000
6 |
7 | #### Run
8 |
9 | To run on `java`, `neko`, `php` or `node` simply run the corresponding hxml file with haxe.
10 |
11 | To run the `cpp` version build it first (`haxe build_cpp.hxml`).
12 | An executable will be available in bin/cpp.
--------------------------------------------------------------------------------
/tests/RunTests.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 |
5 | @colors
6 | class RunTests implements Buddy<[
7 | TestRequest,
8 | TestResponse,
9 | TestRouter,
10 | TestMiddleware,
11 | TestRouteController,
12 | TestStatic,
13 | TestBasicAuth,
14 | TestByteRange,
15 | TestCompression
16 | ]> {
17 | #if php
18 | static function __init__()
19 | untyped __call__('ini_set', 'xdebug.max_nesting_level', 100000);
20 | #end
21 | }
--------------------------------------------------------------------------------
/embed/haxelib.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monsoon-embed",
3 | "url" : "https://github.com/benmerckx/monsoon",
4 | "license": "MIT",
5 | "tags": ["js", "html", "web"],
6 | "description": "Embedded webserver",
7 | "version": "0.0.1",
8 | "releasenote": "Alpha",
9 | "contributors": ["benmerckx"],
10 | "dependencies": {
11 | "tink_tcp": "",
12 | "tink_runloop": "",
13 | "tink_concurrent": "",
14 | "tink_http": "",
15 | "monsoon": ""
16 | }
17 | }
--------------------------------------------------------------------------------
/examples/02_static_file_server/README.md:
--------------------------------------------------------------------------------
1 | # 02
2 |
3 | ### Static file server
4 |
5 | Monsoon can serve static files using the Static middleware.
6 | This example contains a public directory with an index.html file.
7 |
8 | #### Run
9 |
10 | To run on `java`, `neko` or `node` simply run the corresponding hxml file with haxe.
11 |
12 | To run the `cpp` version build it first (`haxe build_cpp.hxml`).
13 | An executable will be available in bin/cpp.
14 | Move the executable to this directory (so the server can find the public dir) and execute it.
--------------------------------------------------------------------------------
/haxelib.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monsoon",
3 | "url" : "https://github.com/benmerckx/monsoon",
4 | "license": "MIT",
5 | "tags": ["js", "html", "web"],
6 | "description": "Minimal haxe web framework",
7 | "version": "0.5.0",
8 | "releasenote": "Don't change body when ending response",
9 | "contributors": ["benmerckx"],
10 | "classPath": "src",
11 | "dependencies": {
12 | "tink_url": "",
13 | "tink_http": "",
14 | "tink_macro": "",
15 | "asys": "",
16 | "mime": "",
17 | "path2ereg": "",
18 | "http-status": ""
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 |
4 | language: haxe
5 |
6 | os:
7 | - linux
8 | - osx
9 |
10 | haxe:
11 | - "3.2.1"
12 | - development
13 |
14 | matrix:
15 | allow_failures:
16 | - haxe: development
17 |
18 | install:
19 | - haxelib install travix
20 | - haxelib install monsoon-embed --always
21 | - haxelib run travix install
22 |
23 | script:
24 | - haxelib run travix php
25 | - haxelib run travix neko
26 | - haxelib run travix node
27 | - haxelib run travix neko -lib monsoon-embed
28 | - haxelib run travix java -lib monsoon-embed
29 | - haxelib run travix cpp -lib monsoon-embed
--------------------------------------------------------------------------------
/src/monsoon/middleware/ThreadServer.hx:
--------------------------------------------------------------------------------
1 | package monsoon.middleware;
2 |
3 | import monsoon.Response;
4 | import tink.http.Request.IncomingRequest;
5 | import tink.http.Response.OutgoingResponse;
6 |
7 | using tink.CoreApi;
8 |
9 | class ThreadServer {
10 |
11 | public static function serve(threads: Int): Layer {
12 | var queue = new tink.concurrent.Queue Void>();
13 |
14 | for (i in 0 ... threads) {
15 | new tink.concurrent.Thread(function ()
16 | while (true) {
17 | var next = queue.await();
18 | next();
19 | }
20 | );
21 | }
22 |
23 | return function (request: Request, response: Response, next: Void -> Void) {
24 | queue.push(next);
25 | }
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/src/monsoon/macro/RequestBuilder.hx:
--------------------------------------------------------------------------------
1 | package monsoon.macro;
2 |
3 | import haxe.macro.Context;
4 | import haxe.macro.Expr;
5 | import haxe.macro.Type;
6 | import haxe.macro.TypeTools;
7 |
8 | class RequestBuilder {
9 |
10 | static public function buildGeneric() {
11 | switch (Context.getLocalType()) {
12 | case TInst(cl, params):
13 | if (params.length == 0)
14 | return (macro: monsoon.Request.MonsoonRequest);
15 | if (params.length == 1)
16 | return ComplexType.TPath({
17 | pack: ['monsoon'],
18 | name: 'Request',
19 | sub: 'MonsoonRequest',
20 | params: [TPType(TypeTools.toComplexType(Context.follow(params[0])))]
21 | });
22 | if (params.length > 1)
23 | Context.error('Too many type parameters, expected 0 or 1', Context.currentPos());
24 | default:
25 | Context.error('Type expected', Context.currentPos());
26 | }
27 | return null;
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/tests/TestCompression.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 | import haxe.Json;
5 | import monsoon.middleware.Compression;
6 | import tink.http.Method;
7 | import TestTools.*;
8 | import TestTools.TinkResponse;
9 | import monsoon.middleware.Static;
10 | import tink.io.Buffer;
11 |
12 | using Monsoon;
13 | using buddy.Should;
14 | using tink.CoreApi;
15 |
16 | class TestCompression extends BuddySuite {
17 |
18 | public function new() {
19 | var app = new Monsoon();
20 |
21 | describe('Compression middleware', {
22 | it('should compress via gzip', function(done) {
23 | app.use(Compression.serve());
24 | app.get(function(req: Request, res: Response)
25 | res.html('ok')
26 | );
27 | app.serve(request('/', ['accept-encoding' => 'gzip'])).handle(function(res: TinkResponse) {
28 | res.header.byName('content-encoding').should.equal(Success('gzip'));
29 | done();
30 | });
31 | });
32 | });
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/tests/TestStatic.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 | import haxe.Json;
5 | import tink.http.Method;
6 | import TestTools.*;
7 | import TestTools.TinkResponse;
8 | import monsoon.middleware.Static;
9 |
10 | using Monsoon;
11 | using buddy.Should;
12 | using tink.CoreApi;
13 |
14 | class TestStatic extends BuddySuite {
15 |
16 | public function new() {
17 | var app = new Monsoon();
18 |
19 | describe('Static middleware', {
20 | it('should serve files', function(done) {
21 | app.use(Static.serve('.'));
22 | app.serve(request('/haxelib.json')).handle(function(res: TinkResponse) {
23 | res.status.should.be(200);
24 | done();
25 | });
26 | });
27 |
28 | it('should serve index files for a directory', function(done) {
29 | app.use(Static.serve('.', {index: ['haxelib.json']}));
30 | app.serve(request('/')).handle(function(res: TinkResponse) {
31 | res.status.should.be(200);
32 | done();
33 | });
34 | });
35 | });
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/src/monsoon/middleware/Console.hx:
--------------------------------------------------------------------------------
1 | package monsoon.middleware;
2 |
3 | import haxe.Json;
4 | import haxe.PosInfos;
5 | import monsoon.Response;
6 | using tink.CoreApi;
7 | using Monsoon;
8 |
9 | typedef Log = Pair;
10 |
11 | @:access(monsoon.Response)
12 | class Console {
13 |
14 | static var logs: Array = [];
15 |
16 | public function new() {
17 | haxe.Log.trace =
18 | function(v: Dynamic, ?info: PosInfos)
19 | logs.push(new Log(v, info));
20 | }
21 |
22 | public function process(req: Request, res: Response, next) {
23 | res.after(function(res) {
24 | var type = req.get('content-type');
25 | if (type != null && type.indexOf('text/html') > -1)
26 | res.body.append('\n');
27 | return Future.sync(res);
28 | });
29 | next();
30 | }
31 |
32 | function logLine(log: Log) {
33 | return 'console.log("%c '+log.b.fileName+':'+log.b.lineNumber+' ", "background: #222; color: white", '+Json.stringify(log.a)+');';
34 | }
35 |
36 | public static function serve()
37 | return new Console().process;
38 |
39 | }
--------------------------------------------------------------------------------
/tests/TestMiddleware.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 | import haxe.Json;
5 | import tink.http.Method;
6 | import TestTools.*;
7 | import TestTools.TinkResponse;
8 | import monsoon.middleware.Console;
9 |
10 | using Monsoon;
11 | using buddy.Should;
12 | using tink.CoreApi;
13 |
14 | class TestMiddleware extends BuddySuite {
15 |
16 | public function new() {
17 | describe('Middleware', {
18 | it('app.use should accept Request -> Response -> Void', function(done) {
19 | var app = new Monsoon();
20 | app.use(setTestHeader);
21 | app.route('/', function(req, res) res.end());
22 | app.serve(request('/')).handle(function(res: TinkResponse) {
23 | res.header.byName('test').should.equal(Success('ok'));
24 | done();
25 | });
26 | });
27 |
28 | it('app.use should accept Middleware', function(done) {
29 | var app = new Monsoon();
30 | app.use({
31 | process: setTestHeader
32 | });
33 | app.route('/', function(req, res) res.end());
34 | app.serve(request('/')).handle(function(res: TinkResponse) {
35 | res.header.byName('test').should.equal(Success('ok'));
36 | done();
37 | });
38 | });
39 | });
40 | }
41 |
42 | function setTestHeader(req: Request, res: Response, next) {
43 | res.set('test', 'ok');
44 | next();
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/src/monsoon/middleware/BasicAuth.hx:
--------------------------------------------------------------------------------
1 | package monsoon.middleware;
2 |
3 | import haxe.crypto.Base64;
4 |
5 | using tink.CoreApi;
6 | using StringTools;
7 |
8 | class BasicAuth {
9 |
10 | static var SHEME = 'Basic ';
11 |
12 | public static function serve(validate: String -> String -> Promise, realm = 'Authorization required')
13 | return function(req: Request, res: Response, next: Void -> Void) {
14 | inline function auth() {
15 | res.status(401)
16 | .set('www-authenticate', 'Basic realm="${realm.split('"').join('\"')}"')
17 | .end();
18 | }
19 | var authorization = req.get('authorization');
20 | if (authorization == null) {
21 | return auth();
22 | } else {
23 | if (!authorization.startsWith(SHEME))
24 | return res.error(400, 'Malformed authorization header');
25 | var encoded = authorization.substr(SHEME.length);
26 | var decoded = Base64.decode(encoded).toString().split(':');
27 | var user = decoded[0], pass = decoded[1];
28 | if (pass == null) return res.error(400, 'Malformed authorization header');
29 | validate(user, pass).handle(function (result)
30 | switch result {
31 | case Success(valid):
32 | if (valid) next();
33 | else auth();
34 | case Failure(msg):
35 | res.error('$msg');
36 | }
37 | );
38 | }
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/tests/TestTools.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import haxe.Json;
4 | import tink.http.Header.HeaderField;
5 | import tink.http.Method;
6 | import tink.http.Request;
7 | import tink.http.Response.OutgoingResponse;
8 | import tink.io.Buffer;
9 |
10 | class TestTools {
11 |
12 | public static function request(?method: Method, path: String, ?fields: TinkHeaderFields, body = '')
13 | return new IncomingRequest('127.0.0.1',
14 | new IncomingRequestHeader(method == null ? GET : method, path, '1.1', fields == null ? [] : fields),
15 | IncomingRequestBody.Plain(body)
16 | );
17 |
18 | }
19 |
20 | @:forward
21 | abstract TinkResponse(OutgoingResponse) from OutgoingResponse {
22 | public var status(get, never): Int;
23 | function get_status() return this.header.statusCode;
24 |
25 | public var body(get, never): String;
26 | function get_body() {
27 | var buffer = Buffer.alloc();
28 | this.body.read(buffer);
29 | return buffer.content().toString();
30 | }
31 |
32 | public var bodyJson(get, never): Dynamic;
33 | function get_bodyJson()
34 | return Json.parse((this: TinkResponse).body);
35 | }
36 |
37 | abstract TinkHeaderFields(Array) from Array to Array {
38 | @:from public static function fromMap(map: Map)
39 | return ([for (key in map.keys()) new HeaderField(key, map.get(key))]: TinkHeaderFields);
40 | }
--------------------------------------------------------------------------------
/tests/TestRouteController.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 | import haxe.Json;
5 | import tink.http.Method;
6 | import TestTools.*;
7 | import TestTools.TinkResponse;
8 |
9 | using Monsoon;
10 | using buddy.Should;
11 | using tink.CoreApi;
12 |
13 | class TestRouteController extends BuddySuite {
14 |
15 | public function new() {
16 | describe('RouteController', {
17 | it('app.use should accept RouteController', function(done) {
18 | var app = new Monsoon();
19 | app.use(this);
20 | app.serve(request('/')).handle(function(res: TinkResponse) {
21 | res.body.should.be('ok');
22 | done();
23 | });
24 | });
25 |
26 | it('app.use should accept a prefix', function(done) {
27 | var app = new Monsoon();
28 | app.use('/prefix', this);
29 | app.serve(request('/prefix')).handle(function(res: TinkResponse) {
30 | res.body.should.be('ok');
31 | done();
32 | });
33 | });
34 |
35 | it('app.use should change the request path', function(done) {
36 | var app = new Monsoon();
37 | app.use('/prefix', function(req: Request, res: Response) {
38 | req.path.should.be('/abc');
39 | res.end();
40 | });
41 | app.use(function(req: Request, res: Response) {
42 | req.path.should.be('/abc');
43 | res.end();
44 | });
45 | Future.ofMany([
46 | app.serve(request('/prefix/abc')),
47 | app.serve(request('/abc'))
48 | ]).handle(function(_) done());
49 | });
50 | });
51 | }
52 |
53 | public function createRoutes(router: Monsoon) {
54 | router.route('/', function(req, res) res.send('ok'));
55 | }
56 |
57 | }
--------------------------------------------------------------------------------
/src/monsoon/Request.hx:
--------------------------------------------------------------------------------
1 | package monsoon;
2 |
3 | import tink.http.Request;
4 | import tink.Url;
5 | import tink.http.Method;
6 |
7 | using tink.CoreApi;
8 |
9 | @:allow(monsoon.Monsoon)
10 | class MonsoonRequest extends IncomingRequest {
11 |
12 | public function new(req: IncomingRequest) {
13 | super(req.clientIp, req.header, req.body);
14 | path = header.uri.path;
15 | url = header.uri;
16 | query = cast url.query.toMap();
17 | method = header.method;
18 | for (header in header.get('set-cookie')) {
19 | var line = (header: String).split(';')[0].split('=');
20 | cookies.set(StringTools.urlDecode(line[0]), (line.length > 1 ? StringTools.urlDecode(line[1]) : null));
21 | }
22 | }
23 |
24 | public var params(default, null): T;
25 | public var path(default, null): String;
26 |
27 | public var url(default, null): Url;
28 |
29 | public var method(default, null): Method;
30 |
31 | public var hostname(get, never): Null;
32 | inline function get_hostname(): Null {
33 | var headers = this.header.get('host');
34 | if (headers.length > 0)
35 | return (headers[0]: String).split(':')[0];
36 | return null;
37 | }
38 |
39 | public var ip(get, never): String;
40 | inline function get_ip(): String
41 | return this.clientIp;
42 |
43 | public var query(default, null): Map = new Map();
44 |
45 | public function get(name: String): Null {
46 | var found = this.header.get(name);
47 | return found.length > 0 ? found[0] : null;
48 | }
49 |
50 | public var cookies(default, null): Map = new Map();
51 |
52 | }
53 |
54 | @:genericBuild(monsoon.macro.RequestBuilder.buildGeneric())
55 | class Request {}
--------------------------------------------------------------------------------
/src/monsoon/middleware/Static.hx:
--------------------------------------------------------------------------------
1 | package monsoon.middleware;
2 |
3 | import asys.FileSystem;
4 | import haxe.io.Path;
5 | import monsoon.Request;
6 | import monsoon.Response;
7 | import tink.http.Handler;
8 | import tink.http.Method;
9 | import tink.http.Request.IncomingRequest;
10 | import tink.http.Response.OutgoingResponse;
11 |
12 | using tink.CoreApi;
13 |
14 | typedef StaticOptions = {
15 | index: Array
16 | }
17 |
18 | class Static {
19 |
20 | var directory: String;
21 | var options = {
22 | index: ['index.html', 'index.htm']
23 | };
24 |
25 | private function new(directory: String, ?options: StaticOptions) {
26 | this.directory = directory;
27 | if (options != null)
28 | this.options = options;
29 | }
30 |
31 | public function process(req: Request, res: Response, next: Void -> Void) {
32 | var path = FileSystem.absolutePath(directory+req.path);
33 |
34 | if (req.method != GET) {
35 | next(); return;
36 | }
37 |
38 | FileSystem.exists(path).handle(function(exists) {
39 | if (!exists) {
40 | next(); return;
41 | }
42 | FileSystem.isDirectory(path).handle(function(isDir) {
43 | if (!isDir) {
44 | res.sendFile(path);
45 | return;
46 | }
47 | var iter = options.index.iterator();
48 | function tryNext() {
49 | if (!iter.hasNext()) {
50 | next();
51 | return;
52 | }
53 | var location = Path.join([path, iter.next()]);
54 | FileSystem.exists(location).handle(function(isFile) {
55 | if (!isFile) {
56 | tryNext();
57 | return;
58 | }
59 | res.sendFile(location);
60 | });
61 | }
62 | tryNext();
63 | });
64 | });
65 | }
66 |
67 | public static function serve(directory: String, ?options: StaticOptions) {
68 | return new Static(directory, options);
69 | }
70 |
71 | }
--------------------------------------------------------------------------------
/tests/TestBasicAuth.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 | import haxe.Json;
5 | import tink.http.Method;
6 | import TestTools.*;
7 | import TestTools.TinkResponse;
8 | import monsoon.middleware.BasicAuth;
9 | import haxe.crypto.Base64;
10 | import haxe.io.Bytes;
11 |
12 | using Monsoon;
13 | using buddy.Should;
14 | using tink.CoreApi;
15 |
16 | class TestBasicAuth extends BuddySuite {
17 |
18 | public function new() {
19 | var app = new Monsoon();
20 | var credentials = {
21 | user: 'user',
22 | password: 'password'
23 | }
24 |
25 | app.use(BasicAuth.serve(function(user: String, pass: String)
26 | return user == credentials.user && credentials.password == pass
27 | ));
28 |
29 | app.get('/', function(req, res: Response)
30 | res.html('protected')
31 | );
32 |
33 | describe('BasicAuth middleware', {
34 | it('should request authorization', function(done) {
35 | app.serve(request('/')).handle(function(res: TinkResponse) {
36 | res.header.byName('www-authenticate').should.equal(Success('Basic realm="Authorization required"'));
37 | done();
38 | });
39 | });
40 |
41 | it('should error on incorrect credentials', function(done) {
42 | app.serve(request('/', [
43 | 'authorization' => 'uh oh'
44 | ])).handle(function(res: TinkResponse) {
45 | res.status.should.be(400);
46 | done();
47 | });
48 | });
49 |
50 | it('should deny access for incorrect credentials', function(done) {
51 | app.serve(request('/', [
52 | 'authorization' => 'Basic '+Base64.encode(Bytes.ofString('a:b'))
53 | ])).handle(function(res: TinkResponse) {
54 | res.status.should.be(401);
55 | done();
56 | });
57 | });
58 |
59 | it('should pass the route for correct credentials', function(done) {
60 | app.serve(request('/', [
61 | 'authorization' => 'Basic '+Base64.encode(Bytes.ofString(credentials.user+':'+credentials.password))
62 | ])).handle(function(res: TinkResponse) {
63 | res.status.should.be(200);
64 | res.body.should.be('protected');
65 | done();
66 | });
67 | });
68 | });
69 | }
70 |
71 | }
--------------------------------------------------------------------------------
/src/monsoon/Layer.hx:
--------------------------------------------------------------------------------
1 | package monsoon;
2 |
3 | import monsoon.Layer.Out;
4 | import tink.core.Any;
5 | import tink.http.Request;
6 | import tink.http.Response;
7 | import tink.http.Handler;
8 | import tink.core.Future;
9 | import haxe.Constraints.Function;
10 | import monsoon.Request;
11 |
12 | typedef In = Request;
13 | typedef Out = Response;
14 |
15 | typedef LayerBase = In -> Out -> (Void -> Void) -> Void;
16 | typedef WithProcess = {function process(request: In, response: Out, next: Void -> Void): Void;}
17 | typedef WithCreateRoutes = {function createRoutes(router: Monsoon): Void;}
18 |
19 | @:callable
20 | abstract Layer(LayerBase) from LayerBase to LayerBase {
21 |
22 | inline function new(func)
23 | this = func;
24 |
25 | @:from
26 | public inline static function fromMiddleware(middleware: Handler -> Handler)
27 | return new Layer(
28 | function(req, res, next)
29 | middleware(function(req) {
30 | next();
31 | return res.future;
32 | })
33 | .process(req)
34 | .handle(function(outgoing)
35 | @:privateAccess
36 | res.ofOutgoingResponse(outgoing).end()
37 | )
38 | );
39 |
40 | @:from
41 | public inline static function fromHandler(func: Handler)
42 | return new Layer(function(req, res, next)
43 | func.process(req).handle(function(outgoing)
44 | @:privateAccess
45 | res.ofOutgoingResponse(outgoing).end()
46 | )
47 | );
48 |
49 | @:from
50 | public inline static function fromCreateRoutes(cr: WithCreateRoutes) {
51 | var router = new Monsoon();
52 | cr.createRoutes(router);
53 | return router.toLayer();
54 | }
55 |
56 | @:from
57 | public inline static function fromProcess(mw: WithProcess)
58 | return new Layer(mw.process);
59 |
60 | @:from
61 | public inline static function fromBasic(cb: In -> Out -> Void)
62 | return new Layer(
63 | function(req, res, next)
64 | return cb(req, res)
65 | );
66 |
67 | @:from
68 | public inline static function fromTypedBasic(cb: Request -> Out -> Void)
69 | return new Layer(fromBasic(cast cb));
70 |
71 | @:from
72 | public inline static function fromTyped(cb: Request -> Out -> (Void -> Void) -> Void)
73 | return new Layer(cast cb);
74 |
75 | @:from
76 | public inline static function fromMap(map: Map): Layer {
77 | var router = new Monsoon();
78 | for (path in map.keys())
79 | router.route(path, map.get(path));
80 | return router;
81 | }
82 |
83 | @:to
84 | public function toHandler(): Handler
85 | return function(req) {
86 | var res = new Out();
87 | this(new In(req), res, function()
88 | res.error(404, 'Not found')
89 | );
90 | return res.future;
91 | }
92 |
93 | }
--------------------------------------------------------------------------------
/tests/TestRequest.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 | import haxe.EnumTools;
5 | import haxe.Json;
6 | import tink.http.Header.HeaderField;
7 | import tink.http.Method;
8 | import TestTools.*;
9 | import TestTools.TinkResponse;
10 |
11 | using Monsoon;
12 | using buddy.Should;
13 | using tink.CoreApi;
14 |
15 | class TestRequest extends BuddySuite {
16 | public function new() {
17 | var app = new Monsoon();
18 | describe('Request', {
19 | it('should contain the http method');
20 | // Todo: renable after this gets fixed:
21 | // https://github.com/HaxeFoundation/haxe/issues/5706
22 | /*it('should contain the http method', function(done) {
23 | app.route('/http-method/:method', function(req: Request<{method: String}>, res: Response) {
24 | req.method.should.be(req.params.method.toUpperCase());
25 | res.end();
26 | });
27 |
28 | Future.ofMany(
29 | [GET, HEAD, OPTIONS, POST, PUT, PATCH, DELETE].map(function (m)
30 | return app.serve(request(m, '/http-method/'+(m: String).toLowerCase()))
31 | )
32 | ).handle(done);
33 | });*/
34 |
35 | it('should parse query parameters', function(done) {
36 | app.route('/parse-query', function(req: Request, res: Response) {
37 | req.query.get('a').should.be('1');
38 | req.query.get('b').should.be('2');
39 | res.end();
40 | });
41 |
42 | app.serve(request('/parse-query?a=1&b=2')).handle(done);
43 | });
44 |
45 | it('should parse cookies', function(done) {
46 | var value = 'testvalue#é_$<>Ϸ';
47 | app.route('/cookies', function(req: Request, res: Response) {
48 | req.cookies.get('name').should.be(value);
49 | res.end();
50 | });
51 | app.serve(request('/cookies', ['set-cookie' => 'name='+StringTools.urlEncode(value)])).handle(done);
52 | });
53 |
54 | it('should set cookies', function(done) {
55 | var value = 'testvalue#é_$<>Ϸ';
56 | app.route('/cookies-set', function(req: Request, res: Response) {
57 | res.cookie('name', value).end();
58 | });
59 | app.serve(request('/cookies-set')).handle(function(res: TinkResponse) {
60 | res.header.byName('set-cookie').should.equal(Success('name=testvalue%23%C3%A9_%24%3C%3E%CF%B7; HttpOnly'));
61 | done();
62 | });
63 | });
64 |
65 | it('should get client headers', function(done) {
66 | app.route('/client-headers', function(req: Request, res: Response) {
67 | req.get('x-test-header').should.be('ok');
68 | req.get('unknown').should.be(null);
69 | res.end();
70 | });
71 | app.serve(request('/client-headers', ['x-test-header' => 'ok'])).handle(done);
72 | });
73 |
74 | });
75 | }
76 | }
--------------------------------------------------------------------------------
/tests/TestByteRange.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 | import haxe.Json;
5 | import tink.http.Method;
6 | import TestTools.*;
7 | import TestTools.TinkResponse;
8 | import monsoon.middleware.ByteRange;
9 | import monsoon.middleware.Static;
10 |
11 | using Monsoon;
12 | using buddy.Should;
13 | using tink.CoreApi;
14 |
15 | class TestByteRange extends BuddySuite {
16 |
17 | public function new() {
18 | var app = new Monsoon();
19 |
20 | app.use(ByteRange.serve());
21 | app.get('/', function(req, res: Response)
22 | res.set('content-length', '10').send('0123456789')
23 | );
24 |
25 | describe('Range middleware', {
26 | it('should set the accept-ranges header', function(done) {
27 | app.serve(request('/')).handle(function(res: TinkResponse) {
28 | res.header.byName('accept-ranges').should.equal(Success('bytes'));
29 | done();
30 | });
31 | });
32 |
33 | it('should serve 0-end range', function(done) {
34 | app.serve(request('/', ['range' => 'bytes=0-3'])).handle(function(res: TinkResponse) {
35 | res.status.should.be(206);
36 | res.header.byName('content-length').should.equal(Success('4'));
37 | res.header.byName('content-range').should.equal(Success('bytes 0-3/10'));
38 | res.body.should.be('0123');
39 | done();
40 | });
41 | });
42 |
43 | it('should serve start-end range', function(done) {
44 | app.serve(request('/', ['range' => 'bytes=1-4'])).handle(function(res: TinkResponse) {
45 | res.status.should.be(206);
46 | res.header.byName('content-length').should.equal(Success('4'));
47 | res.header.byName('content-range').should.equal(Success('bytes 1-4/10'));
48 | res.body.should.be('1234');
49 | done();
50 | });
51 | });
52 |
53 | it('should serve start- range', function(done) {
54 | app.serve(request('/', ['range' => 'bytes=8-'])).handle(function(res: TinkResponse) {
55 | res.status.should.be(206);
56 | res.header.byName('content-length').should.equal(Success('2'));
57 | res.header.byName('content-range').should.equal(Success('bytes 8-9/10'));
58 | res.body.should.be('89');
59 | done();
60 | });
61 | });
62 |
63 | it('should serve -end range', function(done) {
64 | app.serve(request('/', ['range' => 'bytes=-2'])).handle(function(res: TinkResponse) {
65 | res.status.should.be(206);
66 | res.header.byName('content-length').should.equal(Success('2'));
67 | res.header.byName('content-range').should.equal(Success('bytes 8-9/10'));
68 | res.body.should.be('89');
69 | done();
70 | });
71 | });
72 |
73 | it('should fail on incorrect ranges', function(done) {
74 | app.serve(request('/', ['range' => 'bytes=0-11'])).handle(function(res: TinkResponse) {
75 | res.status.should.be(416);
76 | done();
77 | });
78 | });
79 | });
80 | }
81 |
82 | }
--------------------------------------------------------------------------------
/tests/TestResponse.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 | import haxe.Json;
5 | import tink.http.Method;
6 | import TestTools.*;
7 | import TestTools.TinkResponse;
8 | import asys.FileSystem;
9 |
10 | using Monsoon;
11 | using buddy.Should;
12 | using tink.CoreApi;
13 |
14 | class TestResponse extends BuddySuite {
15 | public function new() {
16 | var app = new Monsoon();
17 | describe('Response', {
18 | it('should send a response body', function(done) {
19 | app.get('/response', function(req, res) res.send('ok'));
20 | app.serve(request('/response')).handle(function(res: TinkResponse) {
21 | res.status.should.be(200);
22 | res.body.should.be('ok');
23 | done();
24 | });
25 | });
26 |
27 | it('should set the status', function(done) {
28 | app.get('/status', function(req, res) res.status(600).end());
29 | app.serve(request('/status')).handle(function(res: TinkResponse) {
30 | res.status.should.be(600);
31 | done();
32 | });
33 | });
34 |
35 | it('should respond json', function(done) {
36 | var obj = {test: '123'};
37 | app.get('/json', function(req, res: Response) res.json(obj));
38 | app.serve(request('/json')).handle(function(res: TinkResponse) {
39 | res.header.byName('content-type').should.equal(Success('application/json'));
40 | res.body.should.be(Json.stringify(obj));
41 | done();
42 | });
43 | });
44 |
45 | it('should set headers', function(done) {
46 | app.get('/set-headers', function(req, res: Response)
47 | res.set('x-test', 'abc').set('x-more', '123').end()
48 | );
49 | app.serve(request('/set-headers')).handle(function(res: TinkResponse) {
50 | res.header.byName('x-test').should.equal(Success('abc'));
51 | res.header.byName('x-more').should.equal(Success('123'));
52 | done();
53 | });
54 | });
55 |
56 | it('should create proper error responses', function(done) {
57 | app.get('/error', function(req, res: Response)
58 | res.error(500, 'failed')
59 | );
60 | app.serve(request('/error')).handle(function(res: TinkResponse) {
61 | res.status.should.be(500);
62 | res.body.should.be('failed');
63 | done();
64 | });
65 | });
66 |
67 | it('should serve files with content-type and content-length', function(done) {
68 | var file = 'haxelib.json';
69 | FileSystem.stat(file).handle(function (res) switch res {
70 | case Success(stat):
71 | app.get('/file', function(req, res: Response) res.sendFile('haxelib.json'));
72 | app.serve(request('/file')).handle(function(res: TinkResponse) {
73 | res.header.byName('content-type').should.equal(Success('application/json; charset=utf-8'));
74 | res.header.byName('content-length').should.equal(Success('${stat.size}'));
75 | done();
76 | });
77 | default:
78 | fail('Could not read haxelib.json');
79 | });
80 | });
81 | });
82 | }
83 | }
--------------------------------------------------------------------------------
/tests/TestRouter.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import buddy.*;
4 | import haxe.Json;
5 | import tink.http.Method;
6 | import TestTools.*;
7 | import TestTools.TinkResponse;
8 | import haxe.DynamicAccess;
9 |
10 | using Monsoon;
11 | using buddy.Should;
12 |
13 | class TestRouter extends BuddySuite {
14 | public function new() {
15 | var app = new Monsoon();
16 | describe('Router', {
17 | it('should serve requests', function(done) {
18 | app.get('/', function(req, res) res.send('hello'));
19 | app.serve(request('/')).handle(function(res: TinkResponse) {
20 | res.status.should.be(200);
21 | res.body.should.be('hello');
22 | done();
23 | });
24 | });
25 |
26 | it('should add layers in map notation', function(done) {
27 | app.get([
28 | '/route1' => function(req: Request, res: Response, next) res.send('1'),
29 | '/route2' => function(req: Request, res: Response, next) res.send('2')
30 | ]);
31 | app.serve(request('/route2')).handle(function(res: TinkResponse) {
32 | res.body.should.be('2');
33 | done();
34 | });
35 | });
36 |
37 | it('should parse params', function(done) {
38 | app.get('/param/:arg', function(req: Request<{arg: String}>, res: Response)
39 | res.json({param: req.params.arg})
40 | );
41 | app.serve(request('/param/string')).handle(function(res: TinkResponse) {
42 | res.body.should.be(Json.stringify({param: 'string'}));
43 | done();
44 | });
45 | });
46 |
47 | /*it('should parse params of different types', function(done) {
48 | app.get('/arg/:arg', function(req: Request<{arg: Int}>, res: Response) res.send('Int: '+req.params.arg));
49 | app.get('/arg/:arg', function(req: Request<{arg: Float}>, res: Response) res.send('Float: '+req.params.arg));
50 |
51 | app.serve(request(GET, '/arg/123')).handle(function(res: TinkResponse) {
52 | res.body.should.be('Int: 123');
53 | app.serve(request(GET, '/arg/12.05')).handle(function(res: TinkResponse) {
54 | res.body.should.be('Float: 12.05');
55 | done();
56 | });
57 | });
58 | });*/
59 |
60 | it('should parse splat', function(done) {
61 | app.get('/splat/*', function(req: Request>, res: Response)
62 | res.json({splat: req.params.get('0')})
63 | );
64 | app.serve(request('/splat/more/than/one/dir')).handle(function(res: TinkResponse) {
65 | res.body.should.be(Json.stringify({splat: 'more/than/one/dir'}));
66 | done();
67 | });
68 | });
69 |
70 | it('should respond with 404 if a page is not found', function(done) {
71 | app.serve(request('/unknown')).handle(function(res: TinkResponse) {
72 | res.status.should.be(404);
73 | done();
74 | });
75 | });
76 |
77 | it('should respond a server error if something goes wrong', function(done) {
78 | app.get('/fail', function(req: Request, res: Response)
79 | throw 'fail'
80 | );
81 | app.serve(request('/fail')).handle(function(res: TinkResponse) {
82 | res.status.should.be(500);
83 | done();
84 | });
85 | });
86 | });
87 | }
88 | }
--------------------------------------------------------------------------------
/src/monsoon/middleware/ByteRange.hx:
--------------------------------------------------------------------------------
1 | package monsoon.middleware;
2 |
3 | import httpstatus.HttpStatusMessage;
4 | import tink.io.Buffer;
5 | import tink.io.Source;
6 | import tink.io.IdealSink.BlackHole;
7 | import haxe.io.Bytes;
8 | import tink.io.StreamParser;
9 |
10 | using tink.CoreApi;
11 |
12 | typedef Range = {
13 | start: Int,
14 | length: Int
15 | }
16 |
17 | class ByteRange {
18 |
19 | static var HEADER_START = 'bytes=';
20 |
21 | static function bytePos(input: String): Option {
22 | if (input == '') return None;
23 | var number = Std.parseInt(input);
24 | if (number == null) return None;
25 | return Some(number);
26 | }
27 |
28 | static function parseRange(header: String, length: Int): Outcome, Noise> {
29 | if (header.substr(0, HEADER_START.length) != HEADER_START)
30 | return Failure(Noise);
31 |
32 | var ranges = header.substr(HEADER_START.length).split(','),
33 | response = [];
34 | for (range in ranges) {
35 | var parts = range.split('-');
36 | if (parts.length != 2)
37 | return Failure(Noise);
38 |
39 | switch [bytePos(parts[0]), bytePos(parts[1])] {
40 | case [Some(start), Some(end)]:
41 | response.push({start: start, length: (end-start)+1});
42 | case [Some(start), None]:
43 | response.push({start: start, length: length-start});
44 | case [None, Some(end)]:
45 | response.push({start: length-end, length: end});
46 | default:
47 | return Failure(Noise);
48 | }
49 | }
50 | return Success(response);
51 | }
52 |
53 | public static function serve()
54 | return function(req: Request, res: Response, next: Void -> Void) {
55 | function done()
56 | return Future.sync(res);
57 |
58 | function fail() {
59 | res.error(416, HttpStatusMessage.fromCode(416));
60 | return Future.sync(res);
61 | }
62 |
63 | res.after(function (res) {
64 | if (res.get('content-length') == null)
65 | return done();
66 |
67 | var length = Std.parseInt(res.get('content-length'));
68 | res.set('accept-ranges', 'bytes');
69 |
70 | var header = req.get('range');
71 | if (header == null)
72 | return done();
73 |
74 | switch parseRange(req.get('range'), length) {
75 | case Success(ranges):
76 | if (ranges.length != 1)
77 | return fail();
78 | var range = ranges[0];
79 | if (range.start+range.length > length)
80 | return fail();
81 |
82 | var body: Source = res.body;
83 | if (range.start > 0) {
84 | var limited = body.limit(range.start);
85 | limited.pipeTo(BlackHole.INST);
86 | }
87 |
88 | @:privateAccess
89 | res.body = body.limit(range.length).idealize(fail);
90 |
91 | res
92 | .status(206)
93 | .set('content-length', '${range.length}')
94 | .set('content-range', 'bytes ${range.start}-${range.start+range.length-1}/${length}');
95 |
96 | return done();
97 | default:
98 | return fail();
99 | }
100 |
101 | return done();
102 | });
103 |
104 | next();
105 | }
106 |
107 | }
--------------------------------------------------------------------------------
/src/monsoon/middleware/Compression.hx:
--------------------------------------------------------------------------------
1 | package monsoon.middleware;
2 |
3 | import haxe.io.BytesOutput;
4 | import haxe.io.Bytes;
5 | import mime.Mime;
6 | import tink.io.IdealSource;
7 | import tink.io.Sink;
8 | import tink.io.Worker;
9 | #if nodejs
10 | import js.node.Buffer;
11 | import js.node.Zlib;
12 | #else
13 | import haxe.io.BytesBuffer;
14 | import haxe.crypto.Crc32;
15 | #end
16 |
17 | using Monsoon;
18 | using tink.CoreApi;
19 |
20 | @:access(monsoon.Response)
21 | class Compression {
22 |
23 | var level: Int = 9;
24 |
25 | public function new(?level: Int) {
26 | if (level != null)
27 | this.level = level;
28 | }
29 |
30 | #if !nodejs
31 | function writeGzipHeader(buffer: BytesBuffer) {
32 | buffer.addByte(0x1f);
33 | buffer.addByte(0x8b);
34 | buffer.addByte(8);
35 | buffer.addByte(0);
36 | buffer.addByte(0);
37 | buffer.addByte(0);
38 | buffer.addByte(0);
39 | buffer.addByte(0);
40 | buffer.addByte(level == 9 ? 2 : 4);
41 | buffer.addByte(0x03);
42 | }
43 | #end
44 |
45 | function finalizeResponse(response: Response, bytes: Bytes) {
46 | response.set('content-encoding', 'gzip');
47 | response.set('content-length', Std.string(bytes.length));
48 | response.body = bytes;
49 | }
50 |
51 | public function process(request: Request, response: Response, next: Void -> Void) {
52 | var accept = request.get('accept-encoding');
53 |
54 | if (accept == null || accept.indexOf('gzip') == -1) {
55 | next(); return;
56 | }
57 |
58 | response.after(function(res) {
59 | // Only compress types for which it makes sense
60 | var type = res.get('content-type').split(';')[0]; // Todo: use header / contentType for this
61 | if (type == null)
62 | return Future.sync(res);
63 | var mime = Mime.db.get(type);
64 | if (mime == null)
65 | return Future.sync(res);
66 | if (mime.compressible == null || !mime.compressible)
67 | return Future.sync(res);
68 |
69 | var trigger = Future.trigger();
70 | var out = response.body;
71 | var buffer = new BytesOutput();
72 | var input: Bytes;
73 |
74 | out.pipeTo(Sink.ofOutput('response output buffer', buffer, Worker.EAGER))
75 | .handle(function (x) switch x {
76 | case AllWritten:
77 | input = buffer.getBytes();
78 | #if nodejs
79 | Zlib.gzip(Buffer.hxFromBytes(input), {level: level}, function(err, buffer) {
80 | if (err != null) {
81 | response.error(err.message);
82 | return;
83 | }
84 | finalizeResponse(response, buffer.hxToBytes());
85 | trigger.trigger(res);
86 | });
87 | #else
88 | var buffer = new BytesBuffer();
89 | writeGzipHeader(buffer);
90 | var compressed;
91 | #if !php
92 | compressed = haxe.zip.Compress.run(input, level);
93 | #else
94 | var c = untyped __call__("gzcompress", input.toString(), level);
95 | compressed = haxe.io.Bytes.ofString(c);
96 | #end
97 | // Remove zlib header/checksum
98 | buffer.addBytes(compressed, 2, compressed.length-6);
99 | buffer.addInt32(Crc32.make(input));
100 | buffer.addInt32(input.length);
101 | var bytes = buffer.getBytes();
102 | finalizeResponse(response, bytes);
103 | trigger.trigger(res);
104 | #end
105 | default:
106 | trigger.trigger(res);
107 | });
108 | return trigger.asFuture();
109 | });
110 |
111 | next();
112 | }
113 |
114 | public static function serve(?level: Int)
115 | return new Compression(level).process;
116 |
117 | }
--------------------------------------------------------------------------------
/src/monsoon/Monsoon.hx:
--------------------------------------------------------------------------------
1 | package monsoon;
2 |
3 | import haxe.DynamicAccess;
4 | import tink.http.Container;
5 | import tink.http.containers.*;
6 | import tink.http.Request;
7 | import tink.http.Response;
8 | import tink.http.Handler;
9 | import tink.http.Method;
10 | import monsoon.Request;
11 | import monsoon.Response;
12 | import monsoon.Layer;
13 | import path2ereg.Path2EReg;
14 |
15 | using Lambda;
16 | using tink.CoreApi;
17 |
18 | @:forward
19 | abstract Monsoon(List) from List {
20 |
21 | public inline function new()
22 | this = new List();
23 |
24 | public inline function add(layer: Layer): Monsoon {
25 | this.add(layer);
26 | return this;
27 | }
28 |
29 | public inline function get(?path: String, callback: Layer)
30 | return route(GET, path, callback);
31 |
32 | public inline function post(?path: String, callback: Layer)
33 | return route(POST, path, callback);
34 |
35 | public inline function delete(?path: String, callback: Layer)
36 | return route(DELETE, path, callback);
37 |
38 | public inline function head(?path: String, callback: Layer)
39 | return route(HEAD, path, callback);
40 |
41 | public inline function options(?path: String, callback: Layer)
42 | return route(OPTIONS, path, callback);
43 |
44 | public inline function patch(?path: String, callback: Layer)
45 | return route(PATCH, path, callback);
46 |
47 | public inline function put(?path: String, callback: Layer)
48 | return route(PUT, path, callback);
49 |
50 | public function route(?method: Method, ?path: String, callback: Layer, end = true)
51 | return add(function(req: Request, res: Response, next: Void -> Void) {
52 | return
53 | if (path != null) {
54 | var matcher = Path2EReg.toEReg(path, {end: end});
55 | if (!matcher.ereg.match(req.path)) {
56 | next();
57 | } else {
58 | var params: DynamicAccess = {};
59 | for (i in 0 ... matcher.keys.length)
60 | params.set(matcher.keys[i].name, matcher.ereg.matched(i+1));
61 | req.params = cast params;
62 | req.path = end ? req.path : matcher.ereg.matchedRight();
63 | callback(req, res, next);
64 | }
65 | } else {
66 | req.params = null;
67 | req.path = req.path == null ? req.url.path : req.path;
68 | callback(req, res, next);
69 | }
70 | });
71 |
72 | public inline function use(?path: String, callback: Layer)
73 | return route(path, callback, false);
74 |
75 | public function serve(req: IncomingRequest) {
76 | return
77 | try toHandler().process(req)
78 | catch (e: Dynamic) {
79 | var res = new Response();
80 | res.error('Unexpected exception: $e');
81 | res.future;
82 | }
83 | }
84 |
85 | public function layer(req, res, last) {
86 | var iter = this.iterator();
87 | function next()
88 | if (iter.hasNext())
89 | iter.next()(req, res, next)
90 | else
91 | last();
92 | next();
93 | }
94 |
95 | @:to
96 | public inline function toLayer(): Layer
97 | return layer;
98 |
99 | @:to
100 | public inline function toHandler(): Handler
101 | return toLayer();
102 |
103 | inline function container(port)
104 | return (
105 | #if embed
106 | new TcpContainer(port)
107 | #elseif php
108 | PhpContainer.inst
109 | #elseif neko
110 | ModnekoContainer.inst
111 | #elseif nodejs
112 | new NodeContainer(port)
113 | #else
114 | #error
115 | #end
116 | );
117 |
118 | // Todo: add options (watch, notfound, etc)
119 | public inline function listen(port: Int = 80): Future {
120 | var container = container(port);
121 |
122 | #if (embed && haxe_ver < 3.300)
123 |
124 | // Work around for https://github.com/haxetink/tink_runloop/issues/4
125 | var trigger = Future.trigger();
126 | @:privateAccess tink.RunLoop.create(function()
127 | container.run(serve).handle(trigger.trigger)
128 | );
129 | return trigger.asFuture();
130 |
131 | #else
132 |
133 | return container.run(serve);
134 |
135 | #end
136 | }
137 |
138 | }
--------------------------------------------------------------------------------
/src/monsoon/Response.hx:
--------------------------------------------------------------------------------
1 | package monsoon;
2 |
3 | import haxe.io.Bytes;
4 | import haxe.Json;
5 | import tink.core.Future.FutureTrigger;
6 | import tink.http.Header.HeaderName;
7 | import tink.http.Response;
8 | import tink.http.Header.HeaderField;
9 | import tink.io.IdealSource;
10 | import asys.io.File;
11 | import asys.FileSystem;
12 | import mime.Mime;
13 | import httpstatus.HttpStatusMessage;
14 |
15 | using tink.CoreApi;
16 |
17 | typedef CookieOptions = {
18 | ?expires: Date,
19 | ?domain: String,
20 | ?path: String,
21 | ?secure: Bool,
22 | ?scriptable: Bool
23 | }
24 |
25 | class Response {
26 |
27 | public var future(default, null): Future;
28 | public var header(default, null): ResponseHeader;
29 | public var body(default, null): IdealSource;
30 | var done: FutureTrigger;
31 | var transform: Response -> Future;
32 |
33 | public function new()
34 | clear();
35 |
36 | public function status(code: Int) {
37 | @:privateAccess
38 | header.statusCode = code;
39 | @:privateAccess
40 | header.reason = (code: HttpStatusMessage);
41 | return this;
42 | }
43 |
44 | public function clear() {
45 | header = new ResponseHeader(200, 'OK', []);
46 | body = '';
47 | done = Future.trigger();
48 | future = done.asFuture();
49 | transform = function (res) return Future.sync(this);
50 | return this;
51 | }
52 |
53 | public function cookie(name: String, value: String, ?options: CookieOptions) {
54 | header.fields.push(HeaderField.setCookie(name, value, options));
55 | return this;
56 | }
57 |
58 | public function json(output: Dynamic, ?space) {
59 | set('content-type', 'application/json');
60 | send(Json.stringify(output, null, space));
61 | }
62 |
63 | public function redirect(code = 302, url: String) {
64 | clear()
65 | .status(302)
66 | .set('location', url)
67 | .end();
68 | }
69 |
70 | public function error(code = 500, message: String) {
71 | clear()
72 | .set('content-type', 'text/plain; charset=utf-8')
73 | .status(code)
74 | .send(message);
75 | }
76 |
77 | public function set(key: String, value: String) {
78 | var name: HeaderName = key;
79 | switch header.byName(name) {
80 | case Success(line):
81 | for (field in header.fields)
82 | if (field.name == name)
83 | @:privateAccess field.value = value;
84 | default:
85 | header.fields.push(new HeaderField(name, value));
86 | }
87 | return this;
88 | }
89 |
90 | public function get(key: String): Null
91 | return switch header.byName(key) {
92 | case Success(v): v;
93 | default: null;
94 | }
95 |
96 | public function end()
97 | finalize();
98 |
99 | public function send(output: String) {
100 | body = output;
101 | finalize();
102 | }
103 |
104 | public function html(output: String) {
105 | set('content-type', 'text/html; charset=utf-8');
106 | send(output);
107 | }
108 |
109 | function finalize() {
110 | transform(this).handle(function(res)
111 | done.trigger(res.toOutgoingResponse())
112 | );
113 | }
114 |
115 | public function sendFile(path: String, ?contentType: String) {
116 | function fail(_)
117 | error('Could not read file: '+path);
118 |
119 | FileSystem.stat(path).handle(function (res) switch res {
120 | case Success(stat):
121 | if (contentType == null) {
122 | var type = Mime.lookup(path);
123 | if (type == null)
124 | type = 'application/octet-stream';
125 | var info = Mime.db.get(type);
126 | contentType = type;
127 | if (info.charset != null)
128 | contentType += '; charset='+info.charset.toLowerCase();
129 | }
130 | set('content-type', contentType);
131 | set('content-length', '${stat.size}');
132 | body = File.readStream(path).idealize(fail);
133 | finalize();
134 | default:
135 | fail(null);
136 | });
137 | }
138 |
139 | public static function fromOutgoingResponse(res: OutgoingResponse) {
140 | var result = new Response();
141 | result.header = res.header;
142 | result.body = res.body;
143 | return result;
144 | }
145 |
146 | public function after(cb: Response -> Future) {
147 | var old = transform;
148 | transform = function(res) {
149 | return old(res).flatMap(function(res) {
150 | return cb(res);
151 | });
152 | }
153 | }
154 |
155 | function ofOutgoingResponse(res: OutgoingResponse) {
156 | header = res.header;
157 | body = res.body;
158 | return this;
159 | }
160 |
161 | function toOutgoingResponse()
162 | return new OutgoingResponse(header, body);
163 |
164 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Monsoon [](https://travis-ci.org/benmerckx/monsoon)
2 |
3 | A minimal haxe web framework and embedded webserver using [tink_http](https://github.com/haxetink/tink_http).
4 |
5 | ```haxe
6 | using Monsoon;
7 |
8 | class Main {
9 | public static function main() {
10 | var app = new Monsoon();
11 |
12 | app.route('/', function (req, res)
13 | res.send('Hello World')
14 | );
15 |
16 | app.listen(3000);
17 | }
18 | }
19 |
20 | ```
21 |
22 | # Setup
23 |
24 | Choose a target and lib of one of the implementations below.
25 |
26 | ### Default
27 |
28 | Monsoon runs on platforms that provide their own http implementation.
29 | Runs on: `nodejs`, `php`, `neko` *(mod_neko, mod_tora)*
30 | ```
31 | haxelib install monsoon
32 | ```
33 | *Add `-lib monsoon` to your hxml.*
34 |
35 | ### Embedded
36 |
37 | A tcp webserver will be embedded into your application.
38 | Runs on: `neko`, `cpp`, `java`
39 | ```
40 | haxelib install monsoon-embed
41 | ```
42 | *Add `-lib monsoon-embed` to your hxml.*
43 |
44 | ### Usage
45 |
46 | You can import all relevant classes with `using Monsoon;`.
47 |
48 | # Routing
49 |
50 | ### Basic routing
51 |
52 | The following http request methods can be used to add routes to your app:
53 | `get`, `post`, `delete`, `put`, `patch`, `head`, `options`
54 |
55 | ```haxe
56 | app.get('/', function (req, res) res.send('Get'));
57 | app.post('/submit', function (req, res) res.send('Got post'));
58 | ```
59 |
60 | To match all http methods use `route`
61 |
62 | ```haxe
63 | app.route('/', function (req, res)
64 | res.send('Method used: '+req.method)
65 | );
66 | ```
67 |
68 | ### Matching
69 |
70 | Matching is done using [a port](https://github.com/benmerckx/path2ereg) of [path-to-regex](https://github.com/pillarjs/path-to-regexp).
71 | You can refer to the [express docs](https://expressjs.com/en/guide/routing.html#route-paths) on routing and test the rules with [Express Route Tester](http://forbeslindesay.github.io/express-route-tester/).
72 |
73 | #### Parameters
74 |
75 | A segment of the path can be matched by using a `:param`. To use the parameter later in your callback, it has to be typed in the type parameter of `Request`.
76 | ```haxe
77 | app.get('/blog/:item', function(req: Request<{item: String}>, res)
78 | res.send('Blog item: '+req.params.item)
79 | );
80 | ```
81 |
82 | # Middleware
83 |
84 | ### Bundled middleware
85 |
86 | Bundled middleware can be found in `monsoon.middleware`.
87 |
88 | #### Compression
89 |
90 | Compresses the result of your response using gzip, if accepted by the client.
91 | Takes one optional argument: `?level: Int`, the compression level of 0-9.
92 |
93 | ```haxe
94 | app.use(Compression.serve());
95 | ```
96 |
97 | #### Static
98 |
99 | The static middleware can be used to serve static files (js, css, html etc.). It is recommended to use seperate software (nginx, varnish) to serve your static files but this can be used during development or on low traffic websites.
100 |
101 | If a file is found it will be served with the correct content-type. If no file is found the route is passed.
102 |
103 | ```haxe
104 | // Any file in the public folder will be served
105 | app.use(Static.serve('public'));
106 | // You can change the index files it looks for (default is index.html, index.htm)
107 | app.use(Static.serve('public', {index: ['index.txt', 'index.html']}));
108 | // It can be prefixed like any other route
109 | app.use('/assets', Static.serve('public'));
110 | ```
111 |
112 | #### ByteRange
113 |
114 | Supports client requests for ranged responses.
115 |
116 | ```haxe
117 | app.use(ByteRange.serve());
118 | ```
119 |
120 | #### Basic Authentication
121 |
122 | Request basic authentication. Pass a function which validates the username and password given by the user.
123 | It expects a [`Promise`](https://github.com/haxetink/tink_core/blob/master/src/tink/core/Promise.hx) (returning `Bool` will automatically be cast to a `Promise`).
124 |
125 | ```haxe
126 | app.use(BasicAuth.serve(function (user, password)
127 | return user = 'monsoon' && password = 'mypassword'
128 | ));
129 | ```
130 |
131 | #### Console
132 |
133 | The Console is a debugging tool which will bundle any traces created during the processing of the request and send them with your response to the browser. They are packaged as a single `