Initial commit
This commit is contained in:
commit
9e167c5ed9
21 changed files with 3012 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
5
.npmignore
Normal file
5
.npmignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
docs
|
||||
benchmarks
|
||||
examples
|
||||
test
|
||||
Makefile
|
22
LICENSE
Normal file
22
LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2009-2011 TJ Holowaychuk <tj@vision-media.ca>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
11
Makefile
Normal file
11
Makefile
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
test:
|
||||
@NODE_ENV=test ./node_modules/.bin/mocha \
|
||||
--require should \
|
||||
--harmony-generators \
|
||||
--bail
|
||||
|
||||
bench:
|
||||
@$(MAKE) -C benchmarks
|
||||
|
||||
.PHONY: test bench
|
75
Readme.md
Normal file
75
Readme.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
|
||||
![koa middleware framework for nodejs](https://dsz91cxz97a03.cloudfront.net/uXIzgVnPWG-150x150.png)
|
||||
|
||||
Expressive middleware for node.js using generators via [co](https://github.com/visionmedia/co)
|
||||
to make writing web applications and REST APIs more enjoyable to write.
|
||||
|
||||
Koa provides a useful set of methods that make day to day web application and API design much
|
||||
faster, and less error-prone than "raw" nodejs. Many of these utilities were extracted from [Express](https://github.com/visionmedia/express),
|
||||
however moving them to this layer allows middleware developers to avoid boilerplate and refrain from re-implementing
|
||||
many of these features, sometimes incorrectly or incomplete.
|
||||
|
||||
Only methods that are common to nearly all HTTP servers are integrated directly into Koa's small ~400 SLOC codebase. No middleware
|
||||
are bundled with koa. If you prefer to only define a single dependency for common middleware, much like Connect, you may use
|
||||
[koa-common](https://github.com/koajs/common).
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ npm install koa
|
||||
```
|
||||
|
||||
To use Koa you must be running __node 0.11.4__ or higher for generator support, and must run node(1)
|
||||
with the `--harmony-generators` flag. If you don't like typing this, add an alias to your shell profile:
|
||||
|
||||
```
|
||||
alias node='node --harmony-generators'
|
||||
```
|
||||
|
||||
## Community
|
||||
|
||||
- [API](docs/api.md) documentation
|
||||
- [Middleware](https://github.com/koajs/koa/wiki/Middleware) list
|
||||
- [Wiki](https://github.com/koajs/koa/wiki)
|
||||
- [G+ Community](https://plus.google.com/communities/101845768320796750641)
|
||||
- [Mailing list](https://groups.google.com/forum/#!forum/koajs)
|
||||
- __#koajs__ on freenode
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
var koa = require('koa');
|
||||
var app = koa();
|
||||
|
||||
// logger
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
var start = new Date;
|
||||
yield next;
|
||||
var ms = new Date - start;
|
||||
console.log('%s %s - %s', this.method, this.url, ms);
|
||||
}
|
||||
});
|
||||
|
||||
// response
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
this.body = 'Hello World';
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
```
|
||||
$ make test
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
13
benchmarks/Makefile
Normal file
13
benchmarks/Makefile
Normal file
|
@ -0,0 +1,13 @@
|
|||
|
||||
all:
|
||||
@./run 1 middleware
|
||||
@./run 5 middleware
|
||||
@./run 10 middleware
|
||||
@./run 15 middleware
|
||||
@./run 20 middleware
|
||||
@./run 30 middleware
|
||||
@./run 50 middleware
|
||||
@./run 100 middleware
|
||||
@echo
|
||||
|
||||
.PHONY: all
|
28
benchmarks/middleware.js
Normal file
28
benchmarks/middleware.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
var http = require('http');
|
||||
var koa = require('..');
|
||||
var app = koa();
|
||||
|
||||
// number of middleware
|
||||
|
||||
var n = parseInt(process.env.MW || '1', 10);
|
||||
console.log(' %s middleware', n);
|
||||
|
||||
while (n--) {
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var body = new Buffer('Hello World');
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
this.body = body;
|
||||
}
|
||||
});
|
||||
|
||||
http.createServer(app.callback()).listen(3000);
|
16
benchmarks/run
Executable file
16
benchmarks/run
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
echo
|
||||
MW=$1 node --harmony-generators $2 &
|
||||
pid=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
wrk http://localhost:3000/ \
|
||||
-r 2000 \
|
||||
-c 50 \
|
||||
-t 4 \
|
||||
| grep 'Requests/sec' \
|
||||
| awk '{ print " " $2 }'
|
||||
|
||||
kill $pid
|
511
docs/api.md
Normal file
511
docs/api.md
Normal file
|
@ -0,0 +1,511 @@
|
|||
|
||||
## Application
|
||||
|
||||
A Koa application is not a 1-to-1 representation of an HTTP server,
|
||||
as one or more Koa applications may be mounted together to form larger
|
||||
applications, with a single HTTP server.
|
||||
|
||||
The following is a useless Koa application bound to port `3000`:
|
||||
|
||||
```js
|
||||
var koa = require('koa');
|
||||
var app = koa();
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
The `app.listen(...)` method is simply sugar for the following:
|
||||
|
||||
```js
|
||||
var http = require('http');
|
||||
var koa = require('koa');
|
||||
var app = koa();
|
||||
http.createServer(app.callback()).listen(3000);
|
||||
```
|
||||
|
||||
This means you can spin up the same application as both HTTP and HTTPS,
|
||||
or on multiple addresses:
|
||||
|
||||
```js
|
||||
var http = require('http');
|
||||
var koa = require('koa');
|
||||
var app = koa();
|
||||
http.createServer(app.callback()).listen(3000);
|
||||
http.createServer(app.callback()).listen(3001);
|
||||
```
|
||||
|
||||
### Settings
|
||||
|
||||
Application settings are properties on the `app` instance, currently
|
||||
the following are supported:
|
||||
|
||||
- `app.env` defaulting to the __NODE_ENV__ or "development"
|
||||
- `app.proxy` when true proxy header fields will be trusted
|
||||
- `app.subdomainOffset` offset of `.subdomains` to ignore [2]
|
||||
- `app.jsonSpaces` default JSON response spaces [2]
|
||||
- `app.outputErrors` output err.stack to stderr [true in "development"]
|
||||
|
||||
### app.listen(...)
|
||||
|
||||
Create and return an HTTP server, passing the given arguments to
|
||||
`Server#listen()`. These arguments are documented on [nodejs.org](http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback).
|
||||
|
||||
### app.callback()
|
||||
|
||||
Return a callback function suitable for the `http.createServer()`
|
||||
method to handle a request.
|
||||
|
||||
### app.use(function)
|
||||
|
||||
Add the given middleware function to this application. See [Middleware](#middleware) for
|
||||
more information.
|
||||
|
||||
## Context
|
||||
|
||||
A Koa Context encapsulates node's `request` and `response` objects
|
||||
into a single object which provides many helpful methods for writing
|
||||
web applications and APIs.
|
||||
|
||||
These operations are used so frequently in HTTP server development
|
||||
that they are added at this level, instead of a higher level framework,
|
||||
which would force middlware to re-implement this common functionality.
|
||||
|
||||
A `Context` is created _per_ request, and is referenced in middleware
|
||||
as the receiver, or the `this` variable.
|
||||
|
||||
### ctx.req
|
||||
|
||||
Node's `request` object.
|
||||
|
||||
### ctx.res
|
||||
|
||||
Node's `response` object.
|
||||
|
||||
### ctx.app
|
||||
|
||||
Application instance reference.
|
||||
|
||||
### ctx.header
|
||||
|
||||
Request header object.
|
||||
|
||||
### ctx.responseHeader
|
||||
|
||||
Response header object.
|
||||
|
||||
### ctx.method
|
||||
|
||||
Request method.
|
||||
|
||||
### ctx.method=
|
||||
|
||||
Set request method, useful for implementing middleware
|
||||
such as `methodOverride()`.
|
||||
|
||||
### ctx.status
|
||||
|
||||
Get response status.
|
||||
|
||||
### ctx.status=
|
||||
|
||||
Set response status via numeric code or string:
|
||||
|
||||
- 100 "continue"
|
||||
- 101 "switching protocols"
|
||||
- 102 "processing"
|
||||
- 200 "ok"
|
||||
- 201 "created"
|
||||
- 202 "accepted"
|
||||
- 203 "non-authoritative information"
|
||||
- 204 "no content"
|
||||
- 205 "reset content"
|
||||
- 206 "partial content"
|
||||
- 207 "multi-status"
|
||||
- 300 "multiple choices"
|
||||
- 301 "moved permanently"
|
||||
- 302 "moved temporarily"
|
||||
- 303 "see other"
|
||||
- 304 "not modified"
|
||||
- 305 "use proxy"
|
||||
- 307 "temporary redirect"
|
||||
- 400 "bad request"
|
||||
- 401 "unauthorized"
|
||||
- 402 "payment required"
|
||||
- 403 "forbidden"
|
||||
- 404 "not found"
|
||||
- 405 "method not allowed"
|
||||
- 406 "not acceptable"
|
||||
- 407 "proxy authentication required"
|
||||
- 408 "request time-out"
|
||||
- 409 "conflict"
|
||||
- 410 "gone"
|
||||
- 411 "length required"
|
||||
- 412 "precondition failed"
|
||||
- 413 "request entity too large"
|
||||
- 414 "request-uri too large"
|
||||
- 415 "unsupported media type"
|
||||
- 416 "requested range not satisfiable"
|
||||
- 417 "expectation failed"
|
||||
- 418 "i'm a teapot"
|
||||
- 422 "unprocessable entity"
|
||||
- 423 "locked"
|
||||
- 424 "failed dependency"
|
||||
- 425 "unordered collection"
|
||||
- 426 "upgrade required"
|
||||
- 428 "precondition required"
|
||||
- 429 "too many requests"
|
||||
- 431 "request header fields too large"
|
||||
- 500 "internal server error"
|
||||
- 501 "not implemented"
|
||||
- 502 "bad gateway"
|
||||
- 503 "service unavailable"
|
||||
- 504 "gateway time-out"
|
||||
- 505 "http version not supported"
|
||||
- 506 "variant also negotiates"
|
||||
- 507 "insufficient storage"
|
||||
- 509 "bandwidth limit exceeded"
|
||||
- 510 "not extended"
|
||||
- 511 "network authentication required"
|
||||
|
||||
__NOTE__: don't worry too much about memorizing these strings,
|
||||
if you have a typo an error will be thrown, displaying this list
|
||||
so you can make a correction.
|
||||
|
||||
### ctx.length
|
||||
|
||||
Return request Content-Length as a number when present, or undefined.
|
||||
|
||||
### ctx.responseLength
|
||||
|
||||
Return response Content-Length as a number when present, or deduce
|
||||
from `ctx.body` when possible, or undefined.
|
||||
|
||||
### ctx.body
|
||||
|
||||
Get response body. When `ctx.body` is `null` and `ctx.status` is still
|
||||
200 it is considered a 404. This is to prevent the developer from manually
|
||||
specifying `this.status = 200` on every response.
|
||||
|
||||
### ctx.body=
|
||||
|
||||
Set response body to one of the following:
|
||||
|
||||
- `string` written
|
||||
- `Buffer` written
|
||||
- `Stream` piped
|
||||
- `Object` json stringified
|
||||
|
||||
When a Koa application is created it injects
|
||||
a middleware named `respond`, which handles
|
||||
each of these `ctx.body` values. The `Content-Length`
|
||||
header field is set when possible, and objects are
|
||||
passed through `JSON.stringify()`.
|
||||
|
||||
To alter the JSON response formatting use the `app.jsonSpaces`
|
||||
setting, for example to compress JSON responses set:
|
||||
|
||||
```js
|
||||
app.jsonSpaces = 0;
|
||||
```
|
||||
|
||||
### ctx.get(field)
|
||||
|
||||
Get a request header field value with case-insensitive `field`.
|
||||
|
||||
```js
|
||||
var etag = this.get('If-None-Match');
|
||||
```
|
||||
|
||||
### ctx.set(field, value)
|
||||
|
||||
Set response header `field` to `value`:
|
||||
|
||||
```js
|
||||
this.set('Cache-Control', 'no-cache');
|
||||
```
|
||||
|
||||
### ctx.set(fields)
|
||||
|
||||
Set several response header `fields` with an object:
|
||||
|
||||
```js
|
||||
this.set({
|
||||
'Etag': '1234',
|
||||
'Last-Modified': date
|
||||
});
|
||||
```
|
||||
|
||||
### ctx.type
|
||||
|
||||
Get request `Content-Type` void of parameters such as "charset".
|
||||
|
||||
```js
|
||||
var ct = this.type;
|
||||
// => "image/png"
|
||||
```
|
||||
|
||||
### ctx.type=
|
||||
|
||||
Set response `Content-Type` via mime string or file extension.
|
||||
|
||||
```js
|
||||
this.type = 'image/png';
|
||||
this.type = '.png';
|
||||
this.type = 'png';
|
||||
```
|
||||
|
||||
__NOTE__: when `ctx.body` is an object the content-type is set for you.
|
||||
|
||||
### ctx.url
|
||||
|
||||
Get request URL.
|
||||
|
||||
### ctx.url=
|
||||
|
||||
Set request URL, useful for url rewrites.
|
||||
|
||||
### ctx.path
|
||||
|
||||
Get request pathname.
|
||||
|
||||
### ctx.path=
|
||||
|
||||
Set request pathname and retain query-string when present.
|
||||
|
||||
### ctx.query
|
||||
|
||||
Get parsed query-string using [qs](https://github.com/visionmedia/node-querystring). For example with the url "/shoes?page=2&sort=asc&filters[color]=blue"
|
||||
`this.query` would be the following object:
|
||||
|
||||
```js
|
||||
{
|
||||
page: '2',
|
||||
sort: 'asc',
|
||||
filters: {
|
||||
color: 'blue'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
__NOTE__: this property returns `{}` when no query-string is present.
|
||||
|
||||
### ctx.query=
|
||||
|
||||
Set query-string to the given object.
|
||||
|
||||
```js
|
||||
this.query = { next: '/login' };
|
||||
```
|
||||
|
||||
### ctx.querystring
|
||||
|
||||
Get raw query string void of `?`.
|
||||
|
||||
### ctx.querystring=
|
||||
|
||||
Set raw query string.
|
||||
|
||||
### ctx.host
|
||||
|
||||
Get host void of port number when present. Supports `X-Forwarded-Host`
|
||||
when `app.proxy` is __true__, otherwise `Host` is used.
|
||||
|
||||
### ctx.fresh
|
||||
|
||||
Check if a request cache is "fresh", aka the contents have not changed. This
|
||||
method is for cache negotiation between `If-None-Match` / `ETag`, and `If-Modified-Since` and `Last-Modified`. It should be referenced after setting one or more of these response headers.
|
||||
|
||||
```js
|
||||
this.set('ETag', '123');
|
||||
|
||||
// cache is ok
|
||||
if (this.fresh) {
|
||||
this.status = 304;
|
||||
return;
|
||||
}
|
||||
|
||||
// cache is stale
|
||||
// fetch new data
|
||||
this.body = yield db.find('something');
|
||||
```
|
||||
|
||||
### ctx.stale
|
||||
|
||||
Inverse of `ctx.fresh`.
|
||||
|
||||
### ctx.protocol
|
||||
|
||||
Return request protocol, "https" or "http". Supports `X-Forwarded-Proto`
|
||||
when `app.proxy` is __true__.
|
||||
|
||||
### ctx.secure
|
||||
|
||||
Shorthand for `this.protocol == "https"` to check if a requset was
|
||||
issued via TLS.
|
||||
|
||||
### ctx.ip
|
||||
|
||||
Request remote address. Supports `X-Forwarded-For` when `app.proxy`
|
||||
is __true__.
|
||||
|
||||
### ctx.ips
|
||||
|
||||
When `X-Forwarded-For` is present and `app.proxy` is enabled an array
|
||||
of these ips is returned, ordered from upstream -> downstream. When disabled
|
||||
an empty array is returned.
|
||||
|
||||
### ctx.auth
|
||||
|
||||
Parses basic auth credentials and returns an object with `.username` and `.password`. It's recommended to use `ctx.secure` to ensure basic auth is only
|
||||
used over HTTPS.
|
||||
|
||||
### ctx.subdomains
|
||||
|
||||
Return subdomains as an array.
|
||||
|
||||
Subdomains are the dot-separated parts of the host before the main domain of
|
||||
the app. By default, the domain of the app is assumed to be the last two
|
||||
parts of the host. This can be changed by setting `app.subdomainOffset`.
|
||||
|
||||
For example, if the domain is "tobi.ferrets.example.com":
|
||||
If `app.subdomainOffset` is not set, this.subdomains is `["ferrets", "tobi"]`.
|
||||
If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
|
||||
|
||||
### ctx.is(type)
|
||||
|
||||
Check if the incoming request contains the `Content-Type`
|
||||
header field, and it contains the give mime `type`.
|
||||
|
||||
```js
|
||||
// With Content-Type: text/html; charset=utf-8
|
||||
this.is('html');
|
||||
this.is('.html');
|
||||
this.is('text/html');
|
||||
this.is('text/*');
|
||||
// => true
|
||||
|
||||
// When Content-Type is application/json
|
||||
this.is('json');
|
||||
this.is('.json');
|
||||
this.is('application/json');
|
||||
this.is('application/*');
|
||||
// => true
|
||||
|
||||
this.is('html');
|
||||
// => false
|
||||
```
|
||||
|
||||
### ctx.redirect(url, [alt])
|
||||
|
||||
Perform a 302 redirect to `url`.
|
||||
|
||||
The string "back" is special-cased
|
||||
to provide Referrer support, when Referrer
|
||||
is not present `alt` or "/" is used.
|
||||
|
||||
```js
|
||||
this.redirect('back');
|
||||
this.redirect('back', '/index.html');
|
||||
this.redirect('/login');
|
||||
this.redirect('http://google.com');
|
||||
```
|
||||
|
||||
To alter the default status of `302` or the response
|
||||
body simply re-assign after this call:
|
||||
|
||||
```js
|
||||
this.redirect('/cart');
|
||||
this.status = 301;
|
||||
this.body = 'Redirecting to shopping cart';
|
||||
```
|
||||
|
||||
### ctx.attachment([filename])
|
||||
|
||||
Set `Content-Disposition` to "attachment" to signal the client
|
||||
to prompt for download. Optionally specify the `filename` of the
|
||||
download.
|
||||
|
||||
### ctx.accept(types)
|
||||
|
||||
Check if the given `type(s)` is acceptable, returning
|
||||
the best match when true, otherwise `undefined`, in which
|
||||
case you should respond with 406 "Not Acceptable".
|
||||
|
||||
The `type` value may be one or more mime type string
|
||||
such as "application/json", the extension name
|
||||
such as "json", or an array `["json", "html", "text/plain"]`. When a list or array is given the _best_ match, if any is returned.
|
||||
|
||||
```js
|
||||
// Accept: text/html
|
||||
this.accepts('html');
|
||||
// => "html"
|
||||
|
||||
// Accept: text/*, application/json
|
||||
this.accepts('html');
|
||||
// => "html"
|
||||
this.accepts('text/html');
|
||||
// => "text/html"
|
||||
this.accepts('json', 'text');
|
||||
// => "json"
|
||||
this.accepts('application/json');
|
||||
// => "application/json"
|
||||
|
||||
// Accept: text/*, application/json
|
||||
this.accepts('image/png');
|
||||
this.accepts('png');
|
||||
// => undefined
|
||||
|
||||
// Accept: text/*;q=.5, application/json
|
||||
this.accepts(['html', 'json']);
|
||||
this.accepts('html', 'json');
|
||||
// => "json"
|
||||
```
|
||||
|
||||
You may call `this.accepts()` as may times as you like,
|
||||
or use a switch:
|
||||
|
||||
```js
|
||||
switch (this.accepts('json, html, text')) {
|
||||
case 'json': break;
|
||||
case 'html': break;
|
||||
case 'text': break;
|
||||
}
|
||||
```
|
||||
|
||||
### ctx.accepted
|
||||
|
||||
Return accepted mime types ordered by quality.
|
||||
|
||||
### ctx.acceptedEncodings
|
||||
|
||||
Return accepted content encodings ordered by quality.
|
||||
|
||||
### ctx.acceptedCharsets
|
||||
|
||||
Return accepted charsets ordered by quality.
|
||||
|
||||
### ctx.acceptedLanguages
|
||||
|
||||
Return accepted languages ordered by quality.
|
||||
|
||||
### ctx.headerSent
|
||||
|
||||
Check if a response header has already been sent. Useful for seeing
|
||||
if the client may be notified on error.
|
||||
|
||||
### ctx.socket
|
||||
|
||||
Request socket object.
|
||||
|
||||
## Notes
|
||||
|
||||
### HEAD support
|
||||
|
||||
Koa's upstream response middleware supports __HEAD__ for you,
|
||||
however expensive requests would benefit from custom handling. For
|
||||
example instead of reading a file into memory and piping it to the
|
||||
client, you may wish to `stat()` and set the `Content-*` header fields
|
||||
appropriately to bypass the read.
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
58
examples/compose.js
Normal file
58
examples/compose.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
|
||||
var compose = require('koa-compose');
|
||||
var http = require('http');
|
||||
var koa = require('..');
|
||||
var app = koa();
|
||||
var calls = [];
|
||||
|
||||
// x-response-time
|
||||
|
||||
function responseTime(){
|
||||
return function responseTime(next){
|
||||
return function *(){
|
||||
var start = new Date;
|
||||
yield next;
|
||||
var ms = new Date - start;
|
||||
this.set('X-Response-Time', ms + 'ms');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logger
|
||||
|
||||
function logger(){
|
||||
return function logger(next){
|
||||
return function *(){
|
||||
var start = new Date;
|
||||
yield next;
|
||||
var ms = new Date - start;
|
||||
console.log('%s %s - %s', this.method, this.url, ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// response
|
||||
|
||||
function respond() {
|
||||
return function respond(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
if ('/' != this.url) return;
|
||||
this.body = 'Hello World';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// composed middleware
|
||||
|
||||
function all() {
|
||||
return compose([
|
||||
responseTime(),
|
||||
logger(),
|
||||
respond()
|
||||
]);
|
||||
}
|
||||
|
||||
app.use(all());
|
||||
|
||||
http.createServer(app.callback()).listen(3000);
|
87
examples/negotiation.js
Normal file
87
examples/negotiation.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
|
||||
var koa = require('..');
|
||||
var fs = require('fs');
|
||||
var app = koa();
|
||||
|
||||
var tobi = {
|
||||
_id: '123',
|
||||
name: 'tobi',
|
||||
species: 'ferret'
|
||||
};
|
||||
|
||||
var loki = {
|
||||
_id: '321',
|
||||
name: 'loki',
|
||||
species: 'ferret'
|
||||
};
|
||||
|
||||
var users = {
|
||||
tobi: tobi,
|
||||
loki: loki
|
||||
};
|
||||
|
||||
// content negotiation middleware.
|
||||
// note that you should always check for
|
||||
// presence of a body, and sometimes you
|
||||
// may want to check the type, as it may
|
||||
// be a stream, buffer, string, etc.
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
|
||||
// responses vary on accepted type
|
||||
this.vary('Accept');
|
||||
this.status = 'bad request';
|
||||
|
||||
// no body? nothing to format, early return
|
||||
if (!this.body) return;
|
||||
|
||||
// accepts json, koa handles this for us,
|
||||
// so just return
|
||||
if (this.accepts('json')) return;
|
||||
|
||||
// accepts xml
|
||||
if (this.accepts('xml')) {
|
||||
this.type = 'xml';
|
||||
this.body = '<name>' + this.body.name + '</name>';
|
||||
return;
|
||||
}
|
||||
|
||||
// accepts html
|
||||
if (this.accepts('html')) {
|
||||
this.type = 'html';
|
||||
this.body = '<h1>' + this.body.name + '</h1>';
|
||||
return;
|
||||
}
|
||||
|
||||
// default to text
|
||||
this.body = this.body.name;
|
||||
}
|
||||
});
|
||||
|
||||
// filter responses, in this case remove ._id
|
||||
// since it's private
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
|
||||
if (!this.body) return;
|
||||
|
||||
delete this.body._id;
|
||||
}
|
||||
});
|
||||
|
||||
// try $ GET /tobi
|
||||
// try $ GET /loki
|
||||
|
||||
app.use(function(){
|
||||
return function *(){
|
||||
var name = this.path.slice(1);
|
||||
var user = users[name];
|
||||
this.body = user;
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000);
|
68
examples/route.js
Normal file
68
examples/route.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
|
||||
var http = require('http');
|
||||
var koa = require('..');
|
||||
var app = koa();
|
||||
|
||||
var data;
|
||||
|
||||
// logger
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
var start = new Date;
|
||||
yield next;
|
||||
var ms = new Date - start;
|
||||
console.log('%s %s - %s', this.method, this.url, ms);
|
||||
}
|
||||
});
|
||||
|
||||
// response
|
||||
|
||||
app.use(get('/', function *(){
|
||||
if (data) {
|
||||
this.body = data;
|
||||
} else {
|
||||
this.body = 'POST to /';
|
||||
}
|
||||
}));
|
||||
|
||||
app.use(post('/', function *(){
|
||||
data = yield buffer(this);
|
||||
this.status = 201;
|
||||
this.body = 'created';
|
||||
}));
|
||||
|
||||
function buffer(ctx) {
|
||||
return function(done){
|
||||
var buf = '';
|
||||
|
||||
ctx.req.setEncoding('utf8');
|
||||
ctx.req.on('data', function(chunk){
|
||||
buf += chunk;
|
||||
});
|
||||
|
||||
ctx.req.on('end', function(){
|
||||
done(null, buf);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function get(path, fn) {
|
||||
return route('GET', path, fn);
|
||||
}
|
||||
|
||||
function post(path, fn) {
|
||||
return route('POST', path, fn);
|
||||
}
|
||||
|
||||
function route(method, path, fn) {
|
||||
return function(next){
|
||||
return function *() {
|
||||
var match = method == this.method && this.path == path;
|
||||
if (match) return yield fn;
|
||||
yield next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.createServer(app.callback()).listen(3000);
|
59
examples/simple.js
Normal file
59
examples/simple.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
|
||||
var http = require('http');
|
||||
var koa = require('..');
|
||||
var app = koa();
|
||||
|
||||
// x-response-time
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
var start = new Date;
|
||||
yield next;
|
||||
var ms = new Date - start;
|
||||
this.set('X-Response-Time', ms + 'ms');
|
||||
}
|
||||
});
|
||||
|
||||
// logger
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
var start = new Date;
|
||||
yield next;
|
||||
var ms = new Date - start;
|
||||
console.log('%s %s - %s', this.method, this.url, ms);
|
||||
}
|
||||
});
|
||||
|
||||
// content-length
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
this.set('Content-Length', Buffer.byteLength(this.body));
|
||||
}
|
||||
});
|
||||
|
||||
// response
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
if ('/' != this.url) return;
|
||||
this.status = 200;
|
||||
this.body = 'Hello World';
|
||||
}
|
||||
});
|
||||
|
||||
// custom 404 handler
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
this.status = 404;
|
||||
this.body = 'Sorry cannot find that!';
|
||||
console.log(this);
|
||||
}
|
||||
});
|
||||
|
||||
http.createServer(app.callback()).listen(3000);
|
15
examples/streams.js
Normal file
15
examples/streams.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
|
||||
var koa = require('..');
|
||||
var fs = require('fs');
|
||||
var app = koa();
|
||||
|
||||
// try GET /streams.js
|
||||
|
||||
app.use(function(){
|
||||
return function *(){
|
||||
var path = __dirname + this.path;
|
||||
this.body = fs.createReadStream(path);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000);
|
2
index.js
Normal file
2
index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
module.exports = require('./lib/application');
|
171
lib/application.js
Normal file
171
lib/application.js
Normal file
|
@ -0,0 +1,171 @@
|
|||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var debug = require('debug')('koa:app');
|
||||
var Emitter = require('events').EventEmitter;
|
||||
var compose = require('koa-compose');
|
||||
var Context = require('./context');
|
||||
var Stream = require('stream');
|
||||
var http = require('http');
|
||||
var co = require('co');
|
||||
|
||||
/**
|
||||
* Application prototype.
|
||||
*/
|
||||
|
||||
var app = Application.prototype;
|
||||
|
||||
/**
|
||||
* Expose `Application`.
|
||||
*/
|
||||
|
||||
exports = module.exports = Application;
|
||||
|
||||
/**
|
||||
* Initialize a new `Application`.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Application() {
|
||||
if (!(this instanceof Application)) return new Application;
|
||||
this.env = process.env.NODE_ENV || 'development';
|
||||
this.outputErrors = 'development' == this.env;
|
||||
this.subdomainOffset = 2;
|
||||
this.poweredBy = true;
|
||||
this.jsonSpaces = 2;
|
||||
this.middleware = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit from `Emitter.prototype`.
|
||||
*/
|
||||
|
||||
Application.prototype.__proto__ = Emitter.prototype;
|
||||
|
||||
/**
|
||||
* Shorthand for:
|
||||
*
|
||||
* http.createServer(app.callback()).listen(...)
|
||||
*
|
||||
* @param {Mixed} ...
|
||||
* @return {Server}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
app.listen = function(){
|
||||
var server = http.createServer(this.callback());
|
||||
return server.listen.apply(server, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* Use the given middleware `fn`.
|
||||
*
|
||||
* @param {Function} fn
|
||||
* @return {Application} self
|
||||
* @api public
|
||||
*/
|
||||
|
||||
app.use = function(fn){
|
||||
debug('use %s', fn.name || 'unnamed');
|
||||
this.middleware.push(fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a request handler callback
|
||||
* for node's native htto server.
|
||||
*
|
||||
* @return {Function}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
app.callback = function(){
|
||||
var mw = [respond].concat(this.middleware);
|
||||
var fn = compose(mw)(downstream);
|
||||
var self = this;
|
||||
|
||||
return function(req, res){
|
||||
var ctx = new Context(self, req, res);
|
||||
|
||||
function done(err) {
|
||||
if (err) ctx.error(err);
|
||||
}
|
||||
|
||||
co.call(ctx, function *(){
|
||||
yield fn;
|
||||
}, done);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Response middleware.
|
||||
*/
|
||||
|
||||
function respond(next){
|
||||
return function *(){
|
||||
yield next;
|
||||
|
||||
var res = this.res;
|
||||
var body = this.body;
|
||||
var head = 'HEAD' == this.method;
|
||||
var ignore = 204 == this.status || 304 == this.status;
|
||||
|
||||
// 404
|
||||
if (null == body && 200 == this.status) {
|
||||
this.status = 404;
|
||||
}
|
||||
|
||||
// ignore body
|
||||
if (ignore) return res.end();
|
||||
|
||||
// status body
|
||||
if (null == body) {
|
||||
this.set('Content-Type', 'text/plain');
|
||||
body = http.STATUS_CODES[this.status];
|
||||
}
|
||||
|
||||
// Buffer body
|
||||
if (Buffer.isBuffer(body)) {
|
||||
var ct = this.responseHeader['content-type'];
|
||||
if (!ct) this.set('Content-Type', 'application/octet-stream');
|
||||
this.set('Content-Length', body.length);
|
||||
if (head) return res.end();
|
||||
return res.end(body);
|
||||
}
|
||||
|
||||
// string body
|
||||
if ('string' == typeof body) {
|
||||
var ct = this.responseHeader['content-type'];
|
||||
if (!ct) this.set('Content-Type', 'text/plain; charset=utf-8');
|
||||
this.set('Content-Length', Buffer.byteLength(body));
|
||||
if (head) return res.end();
|
||||
return res.end(body);
|
||||
}
|
||||
|
||||
// Stream body
|
||||
if (body instanceof Stream) {
|
||||
body.on('error', this.error.bind(this));
|
||||
if (head) return res.end();
|
||||
return body.pipe(res);
|
||||
}
|
||||
|
||||
// body: json
|
||||
body = JSON.stringify(body, null, this.app.jsonSpaces);
|
||||
this.set('Content-Length', body.length);
|
||||
this.set('Content-Type', 'application/json');
|
||||
if (head) return res.end();
|
||||
res.end(body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default downstream middleware.
|
||||
*/
|
||||
|
||||
function *downstream() {
|
||||
this.status = 200;
|
||||
if (this.app.poweredBy) this.set('X-Powered-By', 'koa');
|
||||
}
|
907
lib/context.js
Normal file
907
lib/context.js
Normal file
|
@ -0,0 +1,907 @@
|
|||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var debug = require('debug')('koa:context');
|
||||
var Negotiator = require('negotiator');
|
||||
var statuses = require('./status');
|
||||
var fresh = require('fresh');
|
||||
var http = require('http');
|
||||
var path = require('path');
|
||||
var mime = require('mime');
|
||||
var basename = path.basename;
|
||||
var extname = path.extname;
|
||||
var url = require('url');
|
||||
var qs = require('qs');
|
||||
var parse = url.parse;
|
||||
var stringify = url.format;
|
||||
var ctx = Context.prototype;
|
||||
|
||||
/**
|
||||
* Expose `Context`.
|
||||
*/
|
||||
|
||||
module.exports = Context;
|
||||
|
||||
/**
|
||||
* Initialize a new Context with raw node
|
||||
* request and response objects.
|
||||
*
|
||||
* @param {Application} app
|
||||
* @param {Request} req
|
||||
* @param {Request} res
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Context(app, req, res) {
|
||||
this.app = app;
|
||||
this.req = req;
|
||||
this.res = res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prototype.
|
||||
*/
|
||||
|
||||
Context.prototype = {
|
||||
|
||||
/**
|
||||
* Return request header.
|
||||
*
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get header() {
|
||||
return this.req.headers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return response header.
|
||||
*
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get responseHeader() {
|
||||
// TODO: wtf
|
||||
return this.res._headers || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get response status code.
|
||||
*
|
||||
* @return {Number}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get status() {
|
||||
return this.res.statusCode;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set response status code.
|
||||
*
|
||||
* @param {Number|String} val
|
||||
* @api public
|
||||
*/
|
||||
|
||||
set status(val) {
|
||||
if ('string' == typeof val) {
|
||||
var n = statuses[val];
|
||||
if (!n) throw new Error(statusError(val));
|
||||
val = n;
|
||||
}
|
||||
|
||||
this.res.statusCode = val;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return response status string.
|
||||
*
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get statusString() {
|
||||
return http.STATUS_CODES[this.status];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get request URL.
|
||||
*
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get url() {
|
||||
return this.req.url;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set request URL.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
set url(val) {
|
||||
this.req.url = val;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get request method.
|
||||
*
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get method() {
|
||||
return this.req.method;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set request method.
|
||||
*
|
||||
* @param {String} val
|
||||
* @api public
|
||||
*/
|
||||
|
||||
set method(val) {
|
||||
this.req.method = val;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get request pathname.
|
||||
*
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get path() {
|
||||
// TODO: memoize
|
||||
return parse(this.url).pathname;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set pathname, retaining the query-string when present.
|
||||
*
|
||||
* @param {String} path
|
||||
* @api public
|
||||
*/
|
||||
|
||||
set path(path) {
|
||||
var url = parse(this.url);
|
||||
url.pathname = path;
|
||||
this.url = stringify(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get parsed query-string.
|
||||
*
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get query() {
|
||||
var str = this.querystring;
|
||||
if (!str) return {};
|
||||
return qs.parse(str);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set query-string as an object.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @api public
|
||||
*/
|
||||
|
||||
set query(obj) {
|
||||
this.querystring = qs.stringify(obj);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get query string.
|
||||
*
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get querystring() {
|
||||
return parse(this.url).query || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Set querystring.
|
||||
*
|
||||
* @param {String} str
|
||||
* @api public
|
||||
*/
|
||||
|
||||
set querystring(str) {
|
||||
var url = parse(this.url);
|
||||
url.search = str;
|
||||
this.url = stringify(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse the "Host" header field hostname
|
||||
* and support X-Forwarded-Host when a
|
||||
* proxy is enabled.
|
||||
*
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get host() {
|
||||
var proxy = this.app.proxy;
|
||||
var host = proxy && this.get('X-Forwarded-Host');
|
||||
host = host || this.get('Host');
|
||||
if (!host) return;
|
||||
return host.split(/\s*,\s*/)[0].split(':')[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the request is fresh, aka
|
||||
* Last-Modified and/or the ETag
|
||||
* still match.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get fresh() {
|
||||
var method = this.method;
|
||||
var s = this.status;
|
||||
|
||||
// GET or HEAD for weak freshness validation only
|
||||
if ('GET' != method && 'HEAD' != method) return false;
|
||||
|
||||
// 2xx or 304 as per rfc2616 14.26
|
||||
if ((s >= 200 && s < 300) || 304 == s) {
|
||||
return fresh(this.header, this.responseHeader);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the request is stale, aka
|
||||
* "Last-Modified" and / or the "ETag" for the
|
||||
* resource has changed.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get stale() {
|
||||
return !this.fresh;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the request is idempotent.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get idempotent() {
|
||||
return 'GET' == this.method
|
||||
|| 'HEAD' == this.method;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the request socket.
|
||||
*
|
||||
* @return {Connection}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get socket() {
|
||||
// TODO: TLS
|
||||
return this.req.socket;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return parsed Content-Length when present.
|
||||
*
|
||||
* @return {Number}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get length() {
|
||||
var len = this.get('Content-Length');
|
||||
if (null == len) return;
|
||||
return ~~len;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return parsed response Content-Length when present.
|
||||
*
|
||||
* @return {Number}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get responseLength() {
|
||||
var len = this.responseHeader['content-length'];
|
||||
var body = this.body;
|
||||
|
||||
if (null == len) {
|
||||
if (!body) return;
|
||||
if ('string' == typeof body) return Buffer.byteLength(body);
|
||||
return body.length;
|
||||
}
|
||||
|
||||
return ~~len;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the protocol string "http" or "https"
|
||||
* when requested with TLS. When the proxy setting
|
||||
* is enabled the "X-Forwarded-Proto" header
|
||||
* field will be trusted. If you're running behind
|
||||
* a reverse proxy that supplies https for you this
|
||||
* may be enabled.
|
||||
*
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get protocol() {
|
||||
var proxy = this.app.proxy;
|
||||
if (this.socket.encrypted) return 'https';
|
||||
if (!proxy) return 'http';
|
||||
var proto = this.get('X-Forwarded-Proto') || 'http';
|
||||
return proto.split(/\s*,\s*/)[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Short-hand for:
|
||||
*
|
||||
* this.protocol == 'https'
|
||||
*
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get secure() {
|
||||
return 'https' == this.protocol;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the remote address, or when
|
||||
* `app.proxy` is `true` return
|
||||
* the upstream addr.
|
||||
*
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get ip() {
|
||||
return this.ips[0] || this.connection.remoteAddress;
|
||||
},
|
||||
|
||||
/**
|
||||
* When `app.proxy` is `true`, parse
|
||||
* the "X-Forwarded-For" ip address list.
|
||||
*
|
||||
* For example if the value were "client, proxy1, proxy2"
|
||||
* you would receive the array `["client", "proxy1", "proxy2"]`
|
||||
* where "proxy2" is the furthest down-stream.
|
||||
*
|
||||
* @return {Array}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get ips() {
|
||||
var proxy = this.app.proxy;
|
||||
var val = this.get('X-Forwarded-For');
|
||||
return proxy && val
|
||||
? val.split(/ *, */)
|
||||
: [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Return basic auth credentials.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* // http://tobi:hello@example.com
|
||||
* this.auth
|
||||
* // => { username: 'tobi', password: 'hello' }
|
||||
*
|
||||
* @return {Object} or undefined
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get auth() {
|
||||
// missing
|
||||
var auth = this.get('Authorization');
|
||||
if (!auth) return;
|
||||
|
||||
// malformed
|
||||
var parts = auth.split(' ');
|
||||
if ('basic' != parts[0].toLowerCase()) return;
|
||||
if (!parts[1]) return;
|
||||
auth = parts[1];
|
||||
|
||||
// credentials
|
||||
auth = new Buffer(auth, 'base64').toString().match(/^([^:]*):(.*)$/);
|
||||
if (!auth) return;
|
||||
return { username: auth[1], password: auth[2] };
|
||||
},
|
||||
|
||||
/**
|
||||
* Return subdomains as an array.
|
||||
*
|
||||
* Subdomains are the dot-separated parts of the host before the main domain of
|
||||
* the app. By default, the domain of the app is assumed to be the last two
|
||||
* parts of the host. This can be changed by setting `app.subdomainOffset`.
|
||||
*
|
||||
* For example, if the domain is "tobi.ferrets.example.com":
|
||||
* If `app.subdomainOffset` is not set, this.subdomains is `["ferrets", "tobi"]`.
|
||||
* If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
|
||||
*
|
||||
* @return {Array}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get subdomains() {
|
||||
var offset = this.app.subdomainOffset;
|
||||
return (this.host || '')
|
||||
.split('.')
|
||||
.reverse()
|
||||
.slice(offset);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the given `type(s)` is acceptable, returning
|
||||
* the best match when true, otherwise `undefined`, in which
|
||||
* case you should respond with 406 "Not Acceptable".
|
||||
*
|
||||
* The `type` value may be a single mime type string
|
||||
* such as "application/json", the extension name
|
||||
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
|
||||
* or array is given the _best_ match, if any is returned.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* // Accept: text/html
|
||||
* this.accepts('html');
|
||||
* // => "html"
|
||||
*
|
||||
* // Accept: text/*, application/json
|
||||
* this.accepts('html');
|
||||
* // => "html"
|
||||
* this.accepts('text/html');
|
||||
* // => "text/html"
|
||||
* this.accepts('json', 'text');
|
||||
* // => "json"
|
||||
* this.accepts('application/json');
|
||||
* // => "application/json"
|
||||
*
|
||||
* // Accept: text/*, application/json
|
||||
* this.accepts('image/png');
|
||||
* this.accepts('png');
|
||||
* // => undefined
|
||||
*
|
||||
* // Accept: text/*;q=.5, application/json
|
||||
* this.accepts(['html', 'json']);
|
||||
* this.accepts('html', 'json');
|
||||
* // => "json"
|
||||
*
|
||||
* @param {String|Array} type(s)...
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
accepts: function(types){
|
||||
// TODO: memoize
|
||||
if (!Array.isArray(types)) types = [].slice.call(arguments);
|
||||
var normalized = types.map(extToMime);
|
||||
var n = new Negotiator(this.req);
|
||||
var accepts = n.preferredMediaTypes(normalized);
|
||||
var first = accepts[0];
|
||||
if (!first) return false;
|
||||
return types[normalized.indexOf(first)];
|
||||
},
|
||||
|
||||
/**
|
||||
* Return accepted encodings.
|
||||
*
|
||||
* Given `Accept-Encoding: gzip, deflate`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['gzip', 'deflate']
|
||||
*
|
||||
* @return {Array}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get acceptedEncodings() {
|
||||
var n = new Negotiator(this.req);
|
||||
return n.preferredEncodings();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return accepted charsets.
|
||||
*
|
||||
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['utf-8', 'utf-7', 'iso-8859-1']
|
||||
*
|
||||
* @return {Array}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get acceptedCharsets() {
|
||||
var n = new Negotiator(this.req);
|
||||
return n.preferredCharsets();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return accepted languages.
|
||||
*
|
||||
* Given `Accept-Language: en;q=0.8, es, pt`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['es', 'pt', 'en']
|
||||
*
|
||||
* @return {Array}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get acceptedLanguages() {
|
||||
var n = new Negotiator(this.req);
|
||||
return n.preferredLanguages();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return accepted media types.
|
||||
*
|
||||
* Given `Accept: application/*;q=0.2, image/jpeg;q=0.8, text/html`
|
||||
* an array sorted by quality is returned:
|
||||
*
|
||||
* ['text/html', 'image/jpeg', 'application/*']
|
||||
*
|
||||
* @return {Array}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get accepted() {
|
||||
var n = new Negotiator(this.req);
|
||||
return n.preferredMediaTypes();
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a header has been written to the socket.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get headerSent() {
|
||||
return this.res.headersSent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Default error handling.
|
||||
*
|
||||
* @param {Error} err
|
||||
* @api private
|
||||
*/
|
||||
|
||||
error: function(err){
|
||||
if (this.app.outputErrors) console.error(err.stack);
|
||||
// TODO: change name
|
||||
// TODO: header sent check
|
||||
this.status = 500;
|
||||
this.res.end('Internal Server Error');
|
||||
},
|
||||
|
||||
/**
|
||||
* Vary on `field`.
|
||||
*
|
||||
* @param {String} field
|
||||
* @api public
|
||||
*/
|
||||
|
||||
vary: function(field){
|
||||
this.append('Vary', field);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the incoming request contains the "Content-Type"
|
||||
* header field, and it contains the give mime `type`.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* // With Content-Type: text/html; charset=utf-8
|
||||
* this.is('html');
|
||||
* this.is('text/html');
|
||||
* this.is('text/*');
|
||||
* // => true
|
||||
*
|
||||
* // When Content-Type is application/json
|
||||
* this.is('json');
|
||||
* this.is('application/json');
|
||||
* this.is('application/*');
|
||||
* // => true
|
||||
*
|
||||
* this.is('html');
|
||||
* // => false
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
is: function(type){
|
||||
var ct = this.type;
|
||||
if (!ct) return false;
|
||||
ct = ct.split(';')[0];
|
||||
|
||||
// extension given
|
||||
if (!~type.indexOf('/')) type = mime.lookup(type);
|
||||
|
||||
// type or subtype match
|
||||
if (~type.indexOf('*')) {
|
||||
type = type.split('/');
|
||||
ct = ct.split('/');
|
||||
if ('*' == type[0] && type[1] == ct[1]) return true;
|
||||
if ('*' == type[1] && type[0] == ct[0]) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// exact match
|
||||
return type == ct;
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a 302 redirect to `url`.
|
||||
*
|
||||
* The string "back" is special-cased
|
||||
* to provide Referrer support, when Referrer
|
||||
* is not present `alt` or "/" is used.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* this.redirect('back');
|
||||
* this.redirect('back', '/index.html');
|
||||
* this.redirect('/login');
|
||||
* this.redirect('http://google.com');
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {String} alt
|
||||
* @api public
|
||||
*/
|
||||
|
||||
redirect: function(url, alt){
|
||||
if ('back' == url) url = this.get('Referrer') || alt || '/';
|
||||
this.set('Location', url);
|
||||
this.status = 302;
|
||||
|
||||
// html
|
||||
if (this.accepts('html')) {
|
||||
url = escape(url);
|
||||
this.type = 'text/html; charset=utf-8';
|
||||
this.body = 'Redirecting to <a href="' + url + '">' + url + '</a>.';
|
||||
return;
|
||||
}
|
||||
|
||||
// text
|
||||
this.body = 'Redirecting to ' + url + '.';
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Content-Disposition header to "attachment" with optional `filename`.
|
||||
*
|
||||
* @param {String} filename
|
||||
* @api public
|
||||
*/
|
||||
|
||||
attachment: function(filename){
|
||||
if (filename) this.type = extname(filename);
|
||||
this.set('Content-Disposition', filename
|
||||
? 'attachment; filename="' + basename(filename) + '"'
|
||||
: 'attachment');
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Content-Type response header with `type` through `mime.lookup()`
|
||||
* when it does not contain "/", or set the Content-Type to `type` otherwise.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* this.type = '.html';
|
||||
* this.type = 'html';
|
||||
* this.type = 'json';
|
||||
* this.type = 'application/json';
|
||||
* this.type = 'png';
|
||||
*
|
||||
* @param {String} type
|
||||
* @api public
|
||||
*/
|
||||
|
||||
set type(type){
|
||||
if (!~type.indexOf('/')) type = mime.lookup(type);
|
||||
this.set('Content-Type', type);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the request mime type void of
|
||||
* parameters such as "charset".
|
||||
*
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get type() {
|
||||
var type = this.get('Content-Type');
|
||||
if (!type) return;
|
||||
return type.split(';')[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Return request header.
|
||||
*
|
||||
* The `Referrer` header field is special-cased,
|
||||
* both `Referrer` and `Referer` are interchangeable.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* this.get('Content-Type');
|
||||
* // => "text/plain"
|
||||
*
|
||||
* this.get('content-type');
|
||||
* // => "text/plain"
|
||||
*
|
||||
* this.get('Something');
|
||||
* // => undefined
|
||||
*
|
||||
* @param {String} name
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
get: function(name){
|
||||
var req = this.req;
|
||||
switch (name = name.toLowerCase()) {
|
||||
case 'referer':
|
||||
case 'referrer':
|
||||
return req.headers.referrer || req.headers.referer;
|
||||
default:
|
||||
return req.headers[name];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set header `field` to `val`, or pass
|
||||
* an object of header fields.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* this.set('Foo', ['bar', 'baz']);
|
||||
* this.set('Accept', 'application/json');
|
||||
* this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
|
||||
*
|
||||
* @param {String|Object|Array} field
|
||||
* @param {String} val
|
||||
* @api public
|
||||
*/
|
||||
|
||||
set: function(field, val){
|
||||
if (2 == arguments.length) {
|
||||
if (Array.isArray(val)) val = val.map(String);
|
||||
else val = String(val);
|
||||
this.res.setHeader(field, val);
|
||||
} else {
|
||||
for (var key in field) {
|
||||
this.set(key, field[key]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Append `val` to header `field`.
|
||||
*
|
||||
* @param {String} field
|
||||
* @param {String} val
|
||||
* @api public
|
||||
*/
|
||||
|
||||
append: function(field, val){
|
||||
field = field.toLowerCase();
|
||||
var header = this.responseHeader;
|
||||
var list = header[field];
|
||||
|
||||
// not set
|
||||
if (!list) return this.set(field, val);
|
||||
|
||||
// append
|
||||
list = list.split(/ *, */);
|
||||
if (!~list.indexOf(val)) list.push(val);
|
||||
this.set(field, list.join(', '));
|
||||
},
|
||||
|
||||
/**
|
||||
* Inspect implementation.
|
||||
*
|
||||
* TODO: add tests
|
||||
*
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
inspect: function(){
|
||||
var o = this.toJSON();
|
||||
o.body = this.body;
|
||||
o.statusString = this.statusString;
|
||||
return o;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return JSON representation.
|
||||
*
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
toJSON: function(){
|
||||
return {
|
||||
method: this.method,
|
||||
status: this.status,
|
||||
header: this.header,
|
||||
responseHeader: this.responseHeader
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert extnames to mime.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function extToMime(type) {
|
||||
if (~type.indexOf('/')) return type;
|
||||
return mime.lookup(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return status error message.
|
||||
*
|
||||
* @param {String} val
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function statusError(val) {
|
||||
var s = 'invalid status string "' + val + '", try:\n\n';
|
||||
|
||||
Object.keys(statuses).forEach(function(name){
|
||||
var n = statuses[name];
|
||||
s += ' - ' + n + ' "' + name + '"\n';
|
||||
});
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special characters in the given string of html.
|
||||
*
|
||||
* @param {String} html
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function escape(html) {
|
||||
return String(html)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
17
lib/status.js
Normal file
17
lib/status.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var http = require('http');
|
||||
var codes = http.STATUS_CODES;
|
||||
|
||||
/**
|
||||
* Produce exports[STATUS] = CODE map.
|
||||
*/
|
||||
|
||||
Object.keys(codes).forEach(function(code){
|
||||
var n = ~~code;
|
||||
var s = codes[n].toLowerCase();
|
||||
exports[s] = n;
|
||||
});
|
42
package.json
Normal file
42
package.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "koa",
|
||||
"version": "0.0.1",
|
||||
"description": "Koa web app framework",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make test"
|
||||
},
|
||||
"repository": "",
|
||||
"keywords": [
|
||||
"web",
|
||||
"app",
|
||||
"http",
|
||||
"application",
|
||||
"framework",
|
||||
"middleware",
|
||||
"rack"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"co": "1.5.1",
|
||||
"debug": "*",
|
||||
"mime": "1.2.10",
|
||||
"qs": "0.6.5",
|
||||
"fresh": "0.2.0",
|
||||
"negotiator": "0.2.7",
|
||||
"koa-compose": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bytes": "*",
|
||||
"should": "1.2.2",
|
||||
"mocha": "1.12.0",
|
||||
"supertest": "0.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "> 0.11.4"
|
||||
}
|
||||
}
|
255
test/application.js
Normal file
255
test/application.js
Normal file
|
@ -0,0 +1,255 @@
|
|||
|
||||
var request = require('supertest');
|
||||
var assert = require('assert');
|
||||
var http = require('http');
|
||||
var koa = require('..');
|
||||
var fs = require('fs');
|
||||
|
||||
describe('app.use(fn)', function(){
|
||||
it('should compose middleware', function(done){
|
||||
var app = koa();
|
||||
var calls = [];
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
calls.push(1);
|
||||
yield next;
|
||||
calls.push(6);
|
||||
}
|
||||
});
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
calls.push(2);
|
||||
yield next;
|
||||
calls.push(5);
|
||||
}
|
||||
});
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
calls.push(3);
|
||||
yield next;
|
||||
calls.push(4);
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.end(function(err){
|
||||
if (err) return done(err);
|
||||
calls.should.eql([1,2,3,4,5,6]);
|
||||
done();
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
describe('app.respond', function(){
|
||||
describe('when HEAD is used', function(){
|
||||
it('should not respond with the body', function(done){
|
||||
var app = koa();
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
this.body = 'Hello';
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.head('/')
|
||||
.expect(200)
|
||||
.end(function(err, res){
|
||||
if (err) return done(err);
|
||||
res.should.have.header('Content-Length', '5');
|
||||
assert(0 == res.text.length);
|
||||
done();
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
describe('when no middleware are present', function(){
|
||||
it('should 404', function(done){
|
||||
var app = koa();
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect(404, done);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when .body is missing', function(){
|
||||
it('should respond with the associated status message', function(done){
|
||||
var app = koa();
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
this.status = 400;
|
||||
this.body = null;
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect(400)
|
||||
.expect('Bad Request', done);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when .body is a string', function(){
|
||||
it('should respond', function(done){
|
||||
var app = koa();
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
this.body = 'Hello';
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect('Hello', done);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when .body is a Buffer', function(){
|
||||
it('should respond', function(done){
|
||||
var app = koa();
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
this.body = new Buffer('Hello');
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect('Hello', done);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when .body is a Stream', function(){
|
||||
it('should respond', function(done){
|
||||
var app = koa();
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
this.body = fs.createReadStream('package.json');
|
||||
this.set('Content-Type', 'application/json');
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect('Content-Type', 'application/json')
|
||||
.end(function(err, res){
|
||||
if (err) return done(err);
|
||||
var pkg = require('../package');
|
||||
res.body.should.eql(pkg);
|
||||
done();
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
describe('when .body is an Object', function(){
|
||||
it('should respond with json', function(done){
|
||||
var app = koa();
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
this.body = { hello: 'world' };
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect('{\n "hello": "world"\n}', done);
|
||||
})
|
||||
|
||||
describe('when app.jsonSpaces is altered', function(){
|
||||
it('should reflect in the formatting', function(done){
|
||||
var app = koa();
|
||||
|
||||
app.jsonSpaces = 0;
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
this.body = { hello: 'world' };
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect('Content-Type', 'application/json')
|
||||
.expect('{"hello":"world"}', done);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when an error occurs', function(){
|
||||
it('should respond with 500', function(done){
|
||||
var app = koa();
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
throw new Error('boom!');
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect(500, 'Internal Server Error')
|
||||
.end(done);
|
||||
})
|
||||
|
||||
it('should be catchable', function(done){
|
||||
var app = koa();
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
try {
|
||||
yield next;
|
||||
this.body = 'Hello';
|
||||
} catch (err) {
|
||||
error = err;
|
||||
this.body = 'Got error';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use(function(next){
|
||||
return function *(){
|
||||
throw new Error('boom!');
|
||||
this.body = 'Oh no';
|
||||
}
|
||||
});
|
||||
|
||||
var server = http.createServer(app.callback());
|
||||
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect(200, 'Got error')
|
||||
.end(done);
|
||||
})
|
||||
})
|
||||
})
|
649
test/context.js
Normal file
649
test/context.js
Normal file
|
@ -0,0 +1,649 @@
|
|||
|
||||
var Context = require('../lib/context');
|
||||
var assert = require('assert');
|
||||
var koa = require('..');
|
||||
var fs = require('fs');
|
||||
|
||||
function context(req, res) {
|
||||
req = req || { headers: {} };
|
||||
res = res || { _headers: {} };
|
||||
res.setHeader = function(k, v){ res._headers[k.toLowerCase()] = v };
|
||||
var ctx = new Context({}, req, res);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
describe('ctx.length', function(){
|
||||
describe('when Content-Length is defined', function(){
|
||||
it('should return a number', function(){
|
||||
var ctx = context();
|
||||
ctx.header['content-length'] = '120';
|
||||
ctx.length.should.equal(120);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.responseLength', function(){
|
||||
describe('when Content-Length is defined', function(){
|
||||
it('should return a number', function(){
|
||||
var ctx = context();
|
||||
ctx.set('Content-Length', '1024');
|
||||
ctx.responseLength.should.equal(1024);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Content-Length is not defined', function(){
|
||||
describe('and a .body is set', function(){
|
||||
it('should return a number', function(){
|
||||
var ctx = context();
|
||||
|
||||
ctx.body = 'foo';
|
||||
ctx.responseLength.should.equal(3);
|
||||
|
||||
ctx.body = new Buffer('foo');
|
||||
ctx.responseLength.should.equal(3);
|
||||
|
||||
ctx.body = {};
|
||||
assert(null == ctx.responseLength);
|
||||
})
|
||||
})
|
||||
|
||||
describe('and .body is not', function(){
|
||||
it('should return undefined', function(){
|
||||
var ctx = context();
|
||||
assert(null == ctx.responseLength);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.header', function(){
|
||||
it('should return the request header object', function(){
|
||||
var ctx = context();
|
||||
ctx.header.should.equal(ctx.req.headers);
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.auth', function(){
|
||||
it('should parse basic auth', function(){
|
||||
var ctx = context();
|
||||
ctx.header.authorization = 'basic Zm9vOmJhcg==';
|
||||
ctx.auth.username.should.equal('foo');
|
||||
ctx.auth.password.should.equal('bar');
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.protocol', function(){
|
||||
describe('when encrypted', function(){
|
||||
it('should return "https"', function(){
|
||||
var ctx = context();
|
||||
ctx.req.socket = { encrypted: true };
|
||||
ctx.protocol.should.equal('https');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when unencrypted', function(){
|
||||
it('should return "http"', function(){
|
||||
var ctx = context();
|
||||
ctx.req.socket = {};
|
||||
ctx.protocol.should.equal('http');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when X-Forwarded-Proto is set', function(){
|
||||
describe('and proxy is trusted', function(){
|
||||
it('should be used', function(){
|
||||
var ctx = context();
|
||||
ctx.app.proxy = true;
|
||||
ctx.req.socket = {};
|
||||
ctx.header['x-forwarded-proto'] = 'https, http';
|
||||
ctx.protocol.should.equal('https');
|
||||
})
|
||||
})
|
||||
|
||||
describe('and proxy is not trusted', function(){
|
||||
it('should not be used', function(){
|
||||
var ctx = context();
|
||||
ctx.req.socket = {};
|
||||
ctx.header['x-forwarded-proto'] = 'https, http';
|
||||
ctx.protocol.should.equal('http');
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.secure', function(){
|
||||
it('should return true when encrypted', function(){
|
||||
var ctx = context();
|
||||
ctx.req.socket = { encrypted: true };
|
||||
ctx.secure.should.be.true;
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.host', function(){
|
||||
it('should return host void of port', function(){
|
||||
var ctx = context();
|
||||
ctx.header.host = 'foo.com:3000';
|
||||
ctx.host.should.equal('foo.com');
|
||||
})
|
||||
|
||||
describe('when X-Forwarded-Host is present', function(){
|
||||
describe('and proxy is not trusted', function(){
|
||||
it('should be ignored', function(){
|
||||
var ctx = context();
|
||||
ctx.header['x-forwarded-host'] = 'bar.com';
|
||||
ctx.header['host'] = 'foo.com';
|
||||
ctx.host.should.equal('foo.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('and proxy is trusted', function(){
|
||||
it('should be used', function(){
|
||||
var ctx = context();
|
||||
ctx.app.proxy = true;
|
||||
ctx.header['x-forwarded-host'] = 'bar.com, baz.com';
|
||||
ctx.header['host'] = 'foo.com';
|
||||
ctx.host.should.equal('bar.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.status=', function(){
|
||||
describe('when a status string', function(){
|
||||
describe('and valid', function(){
|
||||
it('should set the status', function(){
|
||||
var ctx = context();
|
||||
ctx.status = 'forbidden';
|
||||
ctx.status.should.equal(403);
|
||||
})
|
||||
})
|
||||
|
||||
describe('and invalid', function(){
|
||||
it('should throw', function(){
|
||||
var ctx = context();
|
||||
var err;
|
||||
|
||||
try {
|
||||
ctx.status = 'maru';
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert(err);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.stale', function(){
|
||||
it('should be the inverse of ctx.fresh', function(){
|
||||
var ctx = context();
|
||||
ctx.status = 200;
|
||||
ctx.req.method = 'GET';
|
||||
ctx.req.headers['if-none-match'] = '123';
|
||||
ctx.res._headers['etag'] = '123';
|
||||
ctx.fresh.should.be.true;
|
||||
ctx.stale.should.be.false;
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.fresh', function(){
|
||||
describe('the response is non-2xx', function(){
|
||||
it('should return false', function(){
|
||||
var ctx = context();
|
||||
ctx.status = 404;
|
||||
ctx.req.method = 'GET';
|
||||
ctx.req.headers['if-none-match'] = '123';
|
||||
ctx.res._headers['etag'] = '123';
|
||||
ctx.fresh.should.be.false;
|
||||
})
|
||||
});
|
||||
|
||||
describe('the response is 2xx', function(){
|
||||
describe('and etag matches', function(){
|
||||
it('should return true', function(){
|
||||
var ctx = context();
|
||||
ctx.status = 200;
|
||||
ctx.req.method = 'GET';
|
||||
ctx.req.headers['if-none-match'] = '123';
|
||||
ctx.res._headers['etag'] = '123';
|
||||
ctx.fresh.should.be.true;
|
||||
})
|
||||
})
|
||||
|
||||
describe('and etag do not match', function(){
|
||||
it('should return false', function(){
|
||||
var ctx = context();
|
||||
ctx.status = 200;
|
||||
ctx.req.method = 'GET';
|
||||
ctx.req.headers['if-none-match'] = '123';
|
||||
ctx.res._headers['etag'] = 'hey';
|
||||
ctx.fresh.should.be.false;
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.accepted', function(){
|
||||
describe('when Accept is populated', function(){
|
||||
it('should return accepted types', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain';
|
||||
ctx.accepted.should.eql(['text/plain', 'text/html', 'image/jpeg', 'application/*']);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Accept is not populated', function(){
|
||||
it('should return an empty array', function(){
|
||||
var ctx = context();
|
||||
ctx.accepted.should.eql([]);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.vary(field)', function(){
|
||||
describe('when Vary is not set', function(){
|
||||
it('should set it', function(){
|
||||
var ctx = context();
|
||||
ctx.vary('Accept');
|
||||
ctx.responseHeader.vary.should.equal('Accept');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Vary is set', function(){
|
||||
it('should append', function(){
|
||||
var ctx = context();
|
||||
ctx.vary('Accept');
|
||||
ctx.vary('Accept-Encoding');
|
||||
ctx.responseHeader.vary.should.equal('Accept, Accept-Encoding');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Vary already contains the value', function(){
|
||||
it('should not append', function(){
|
||||
var ctx = context();
|
||||
ctx.vary('Accept');
|
||||
ctx.vary('Accept-Encoding');
|
||||
ctx.vary('Accept');
|
||||
ctx.vary('Accept-Encoding');
|
||||
ctx.responseHeader.vary.should.equal('Accept, Accept-Encoding');
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.accepts(types)', function(){
|
||||
describe('with no types', function(){
|
||||
it('should return false', function(){
|
||||
var ctx = context();
|
||||
ctx.accepts('').should.be.false;
|
||||
})
|
||||
})
|
||||
|
||||
describe('when extensions are given', function(){
|
||||
it('should convert to mime types', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers.accept = 'text/plain, text/html';
|
||||
ctx.accepts('html').should.equal('html');
|
||||
ctx.accepts('.html').should.equal('.html');
|
||||
ctx.accepts('txt').should.equal('txt');
|
||||
ctx.accepts('.txt').should.equal('.txt');
|
||||
ctx.accepts('png').should.be.false;
|
||||
})
|
||||
})
|
||||
|
||||
describe('when an array is given', function(){
|
||||
it('should return the first match', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers.accept = 'text/plain, text/html';
|
||||
ctx.accepts(['png', 'text', 'html']).should.equal('text');
|
||||
ctx.accepts(['png', 'html']).should.equal('html');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when multiple arguments are given', function(){
|
||||
it('should return the first match', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers.accept = 'text/plain, text/html';
|
||||
ctx.accepts('png', 'text', 'html').should.equal('text');
|
||||
ctx.accepts('png', 'html').should.equal('html');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when present in Accept as an exact match', function(){
|
||||
it('should return the type', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers.accept = 'text/plain, text/html';
|
||||
ctx.accepts('text/html').should.equal('text/html');
|
||||
ctx.accepts('text/plain').should.equal('text/plain');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when present in Accept as a type match', function(){
|
||||
it('should return the type', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers.accept = 'application/json, */*';
|
||||
ctx.accepts('text/html').should.equal('text/html');
|
||||
ctx.accepts('text/plain').should.equal('text/plain');
|
||||
ctx.accepts('image/png').should.equal('image/png');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when present in Accept as a subtype match', function(){
|
||||
it('should return the type', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers.accept = 'application/json, text/*';
|
||||
ctx.accepts('text/html').should.equal('text/html');
|
||||
ctx.accepts('text/plain').should.equal('text/plain');
|
||||
ctx.accepts('image/png').should.be.false;
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.acceptedLanguages', function(){
|
||||
describe('when Accept-Language is populated', function(){
|
||||
it('should return accepted types', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt';
|
||||
ctx.acceptedLanguages.should.eql(['pt', 'es', 'en']);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Accept-Language is not populated', function(){
|
||||
it('should return an empty array', function(){
|
||||
var ctx = context();
|
||||
ctx.acceptedLanguages.should.eql([]);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.acceptedCharsets', function(){
|
||||
describe('when Accept-Charset is populated', function(){
|
||||
it('should return accepted types', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5';
|
||||
ctx.acceptedCharsets.should.eql(['utf-8', 'utf-7', 'iso-8859-1']);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Accept-Charset is not populated', function(){
|
||||
it('should return an empty array', function(){
|
||||
var ctx = context();
|
||||
ctx.acceptedCharsets.should.eql([]);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.acceptedEncodings', function(){
|
||||
describe('when Accept-Encoding is populated', function(){
|
||||
it('should return accepted types', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2';
|
||||
ctx.acceptedEncodings.should.eql(['gzip', 'compress', 'identity']);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Accept-Encoding is not populated', function(){
|
||||
it('should return identity', function(){
|
||||
var ctx = context();
|
||||
ctx.acceptedEncodings.should.eql(['identity']);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.path', function(){
|
||||
it('should return the pathname', function(){
|
||||
var ctx = new Context(null, {
|
||||
url: '/login?next=/dashboard'
|
||||
});
|
||||
|
||||
ctx.path.should.equal('/login');
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.path=', function(){
|
||||
it('should set the pathname', function(){
|
||||
var ctx = new Context(null, {
|
||||
url: '/login?next=/dashboard'
|
||||
});
|
||||
|
||||
ctx.path = '/logout';
|
||||
ctx.path.should.equal('/logout');
|
||||
ctx.url.should.equal('/logout?next=/dashboard');
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.get(name)', function(){
|
||||
it('should return the field value', function(){
|
||||
var req = { headers: { 'host': 'http://google.com' } };
|
||||
var ctx = new Context(null, req);
|
||||
ctx.get('HOST').should.equal('http://google.com');
|
||||
ctx.get('Host').should.equal('http://google.com');
|
||||
ctx.get('host').should.equal('http://google.com');
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.set(name, val)', function(){
|
||||
it('should set a field value', function(){
|
||||
var ctx = context();
|
||||
ctx.set('x-foo', 'bar');
|
||||
ctx.responseHeader['x-foo'].should.equal('bar');
|
||||
})
|
||||
|
||||
it('should coerce to a string', function(){
|
||||
var ctx = context();
|
||||
ctx.set('x-foo', 5);
|
||||
ctx.responseHeader['x-foo'].should.equal('5');
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.set(object)', function(){
|
||||
it('should set multiple fields', function(){
|
||||
var ctx = context();
|
||||
|
||||
ctx.set({
|
||||
foo: '1',
|
||||
bar: '2'
|
||||
});
|
||||
|
||||
ctx.responseHeader.foo.should.equal('1');
|
||||
ctx.responseHeader.bar.should.equal('2');
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.query', function(){
|
||||
describe('when missing', function(){
|
||||
it('should return an empty object', function(){
|
||||
var ctx = context({ url: '/' });
|
||||
ctx.query.should.eql({});
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a parsed query-string', function(){
|
||||
var ctx = context({ url: '/?page=2' });
|
||||
ctx.query.page.should.equal('2');
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.query=', function(){
|
||||
it('should stringify and replace the querystring', function(){
|
||||
var ctx = context({ url: '/store/shoes' });
|
||||
ctx.query = { page: 2, color: 'blue' };
|
||||
ctx.url.should.equal('/store/shoes?page=2&color=blue');
|
||||
ctx.querystring.should.equal('page=2&color=blue');
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.querystring=', function(){
|
||||
it('should replace the querystring', function(){
|
||||
var ctx = context({ url: '/store/shoes' });
|
||||
ctx.querystring = 'page=2&color=blue';
|
||||
ctx.url.should.equal('/store/shoes?page=2&color=blue');
|
||||
ctx.querystring.should.equal('page=2&color=blue');
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.type=', function(){
|
||||
describe('with a mime', function(){
|
||||
it('should set the Content-Type', function(){
|
||||
var ctx = context();
|
||||
ctx.type = 'text/plain';
|
||||
ctx.responseHeader['content-type'].should.equal('text/plain');
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an extension', function(){
|
||||
it('should lookup the mime', function(){
|
||||
var ctx = context();
|
||||
ctx.type = 'json';
|
||||
ctx.responseHeader['content-type'].should.equal('application/json');
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.type', function(){
|
||||
describe('with no Content-Type', function(){
|
||||
it('should return null', function(){
|
||||
var ctx = context();
|
||||
assert(null == ctx.type);
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a Content-Type', function(){
|
||||
it('should return the mime', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers['content-type'] = 'text/html; charset=utf8';
|
||||
ctx.type.should.equal('text/html');
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.is(type)', function(){
|
||||
it('should ignore params', function(){
|
||||
var ctx = context();
|
||||
ctx.header['content-type'] = 'text/html; charset=utf-8';
|
||||
ctx.is('text/*').should.be.true;
|
||||
})
|
||||
|
||||
describe('given a mime', function(){
|
||||
it('should check the type', function(){
|
||||
var ctx = context();
|
||||
ctx.header['content-type'] = 'image/png';
|
||||
|
||||
ctx.is('image/png').should.be.true;
|
||||
ctx.is('image/*').should.be.true;
|
||||
ctx.is('*/png').should.be.true;
|
||||
|
||||
ctx.is('image/jpeg').should.be.false;
|
||||
ctx.is('text/*').should.be.false;
|
||||
ctx.is('*/jpeg').should.be.false;
|
||||
})
|
||||
})
|
||||
|
||||
describe('given an extension', function(){
|
||||
it('should check the type', function(){
|
||||
var ctx = context();
|
||||
ctx.header['content-type'] = 'image/png';
|
||||
|
||||
ctx.is('png').should.be.true;
|
||||
ctx.is('.png').should.be.true;
|
||||
|
||||
ctx.is('jpeg').should.be.false;
|
||||
ctx.is('.jpeg').should.be.false;
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.attachment([filename])', function(){
|
||||
describe('when given a filename', function(){
|
||||
it('should set the filename param', function(){
|
||||
var ctx = context();
|
||||
ctx.attachment('path/to/tobi.png');
|
||||
var str = 'attachment; filename="tobi.png"';
|
||||
ctx.responseHeader['content-disposition'].should.equal(str);
|
||||
})
|
||||
})
|
||||
|
||||
describe('when omitting filename', function(){
|
||||
it('should not set filename param', function(){
|
||||
var ctx = context();
|
||||
ctx.attachment();
|
||||
ctx.responseHeader['content-disposition'].should.equal('attachment');
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ctx.redirect(url)', function(){
|
||||
it('should redirect to the given url', function(){
|
||||
var ctx = context();
|
||||
ctx.redirect('http://google.com');
|
||||
ctx.responseHeader.location.should.equal('http://google.com');
|
||||
ctx.status.should.equal(302);
|
||||
})
|
||||
|
||||
describe('with "back"', function(){
|
||||
it('should redirect to Referrer', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers.referrer = '/login';
|
||||
ctx.redirect('back');
|
||||
ctx.responseHeader.location.should.equal('/login');
|
||||
})
|
||||
|
||||
it('should redirect to Referer', function(){
|
||||
var ctx = context();
|
||||
ctx.req.headers.referer = '/login';
|
||||
ctx.redirect('back');
|
||||
ctx.responseHeader.location.should.equal('/login');
|
||||
})
|
||||
|
||||
it('should default to alt', function(){
|
||||
var ctx = context();
|
||||
ctx.redirect('back', '/index.html');
|
||||
ctx.responseHeader.location.should.equal('/index.html');
|
||||
})
|
||||
|
||||
it('should default redirect to /', function(){
|
||||
var ctx = context();
|
||||
ctx.redirect('back');
|
||||
ctx.responseHeader.location.should.equal('/');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when html is accepted', function(){
|
||||
it('should respond with html', function(){
|
||||
var ctx = context();
|
||||
var url = 'http://google.com';
|
||||
ctx.header.accept = 'text/html';
|
||||
ctx.redirect(url);
|
||||
ctx.responseHeader['content-type'].should.equal('text/html; charset=utf-8');
|
||||
ctx.body.should.equal('Redirecting to <a href="' + url + '">' + url + '</a>.');
|
||||
})
|
||||
|
||||
it('should escape the url', function(){
|
||||
var ctx = context();
|
||||
var url = '<script>';
|
||||
ctx.header.accept = 'text/html';
|
||||
ctx.redirect(url);
|
||||
url = escape(url);
|
||||
ctx.responseHeader['content-type'].should.equal('text/html; charset=utf-8');
|
||||
ctx.body.should.equal('Redirecting to <a href="' + url + '">' + url + '</a>.');
|
||||
})
|
||||
})
|
||||
|
||||
describe('when text is accepted', function(){
|
||||
it('should respond with text', function(){
|
||||
var ctx = context();
|
||||
var url = 'http://google.com';
|
||||
ctx.header.accept = 'text/plain';
|
||||
ctx.redirect(url);
|
||||
ctx.body.should.equal('Redirecting to ' + url + '.');
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function escape(html) {
|
||||
return String(html)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
Loading…
Reference in a new issue