Backend class
class Backend {
static final String COOKIE_PATH = "/";
static final bool COOKIE_HTTP_ONLY = true;
/**
* Register routes which are matched with request.uri .
*/
final Router router;
/**
* Calls handlers associated with a particular routeName.
*/
final RequestNavigator _requestNavigator;
/**
* Handles the incoming stream of [HttpRequest]s
*/
final HttpServer _server;
List _defaulHttpHeaders = new List();
/**
* For cookies.
*/
final _hmacFactory;
/**
* HttpBodyHandler.processRequest
*/
final _httpBodyExtractor;
/**
* Default handler for NotFoundView.
*/
RequestHandler _notFoundViewHandler = (Request request) {
request.response
..statusCode = HttpStatus.NOT_FOUND
..close();
};
/**
* Constructor.
*/
Backend.config(this._server, this.router, this._requestNavigator,
this._hmacFactory, this._httpBodyExtractor) {
// Add default handler for not found views. Before not found view response
// is sent, check if URI ends with a slash and if not, try to add it.
_requestNavigator.registerDefaultHandler((httpRequest, urlParams)
=> prepareRequestHandler(httpRequest, urlParams, (Request request) {
var uri = request.httpRequest.uri;
if (!uri.path.endsWith('/')) {
//we set request.httpRequest.response because of consistency
// as (request.response := request.httpRequest.response)
request.httpRequest.response.redirect(new Uri(
scheme: uri.scheme,
host: uri.host,
port: uri.port,
path: uri.path + '/',
query: uri.query
));
}
else{
_notFoundViewHandler(request);
}
}));
}
/**
* Creates a new backend.
*/
static Future<Backend> bind(key, hashMethod, {String host: "0.0.0.0", int port: 8080}){
return HttpServer.bind(host, port).then((httpServer) {
var router = new Router("http://$host:$port", {});
var requestNavigator = new RequestNavigator(httpServer.asBroadcastStream(), router);
return new Backend.config(httpServer, router, requestNavigator,
() => new HMAC(hashMethod, key), HttpBodyHandler.processRequest);
});
}
/**
* Adds header which will be attached to each response. Could be overwritten.
*/
void addDefaultHttpHeader(name, value) {
_defaulHttpHeaders.add({'name': name, 'value': value});
}
/**
* Transforms [httpRequest] with [urlParams] and creates [Request] which is
* passed asynchronously to [handler].
*/
Future prepareRequestHandler(HttpRequest httpRequest, Map urlParams,
RequestHandler handler) {
return _httpBodyExtractor(httpRequest).then((HttpBody body) {
if (_defaulHttpHeaders != null) {
_defaulHttpHeaders.forEach((header) =>
httpRequest.response.headers.add(header['name'], header['value']));
}
Request request = new Request(body.type, body.body, httpRequest.response,
httpRequest.headers, httpRequest, urlParams);
request.authenticatedUserId = getAuthenticatedUser(request.headers);
handler(request);
return true;
});
}
/**
* Adds [route] for a particular [route] so handler could be attached to [routeName]s.
*/
void addRoute(String routeName, Route route){
router.addRoute(routeName, route);
}
/**
* Adds [handler] for a particular [routeName].
*/
void addView(String routeName, RequestHandler handler) {
_requestNavigator.registerHandler(routeName, (httpRequest, urlParams)
=> prepareRequestHandler(httpRequest, urlParams, handler));
}
/**
* Corresponding [Route] for [routeName] should be in the prefix format,
* i.e. "/uploads/*", as backend will look for files documentRoot/matchedSufix
*/*/
void addStaticView(String routeName, String documentRoot) {
var vd = new VirtualDirectory(documentRoot);
_requestNavigator.registerHandler(routeName, (httpRequest, urlParams) {
var relativePath = urlParams['_tail'];
if (relativePath.split('/').contains('..')) {
httpRequest.response.statusCode = HttpStatus.NOT_FOUND;
httpRequest.response.close();
return;
}
String path = p.join(documentRoot, relativePath);
FileSystemEntity.type(path).then((type) {
switch (type) {
case FileSystemEntityType.FILE:
// If file, serve as such.
vd.serveFile(new File(path), httpRequest);
break;
case FileSystemEntityType.DIRECTORY:
// File not found, fall back to 404.
httpRequest.response.statusCode = HttpStatus.NOT_FOUND;
httpRequest.response.close();
break;
default:
// File not found, fall back to 404.
httpRequest.response.statusCode = HttpStatus.NOT_FOUND;
httpRequest.response.close();
break;
}
});
});
}
/**
* If nothing is matched. There is a default [_notFoundViewHandler], but it
* can be overwritten by this method.
*/
void addNotFoundView(RequestHandler handler) {
_notFoundViewHandler = handler;
}
List<int> sign(String msg) {
HMAC hmac = _hmacFactory();
_stringToHash(msg, hmac);
return hmac.close();
}
bool verifySignature(String msg, List<int> signature) {
HMAC hmac = _hmacFactory();
_stringToHash(msg, hmac);
return hmac.verify(signature);
}
void _stringToHash(String value, HMAC hmac) {
Utf8Codec codec = new Utf8Codec();
List<int> encodedUserId = codec.encode(value);
hmac.add(encodedUserId);
}
void authenticate(HttpResponse response, String userId) {
List<int> userIdSignature = sign(userId);
Cookie cookie = new Cookie('authentication', JSON.encode({
'userID': userId, 'signature': userIdSignature}));
cookie.expires = new DateTime.now().add(new Duration(days: 365));
cookie.path = COOKIE_PATH;
cookie.httpOnly = COOKIE_HTTP_ONLY;
response.headers.add(HttpHeaders.SET_COOKIE, cookie);
}
String getAuthenticatedUser(HttpHeaders headers) {
if (headers[HttpHeaders.COOKIE] == null) {
return null;
}
for (String cookieString in headers[HttpHeaders.COOKIE]) {
Cookie cookie = new Cookie.fromSetCookieValue(cookieString);
if (cookie.name == 'authentication') {
Map authentication = JSON.decode(cookie.value);
if (verifySignature(authentication['userID'], authentication['signature'])) {
return authentication['userID'];
}
}
}
return null;
}
void logout(Request request) {
Cookie cookie = new Cookie('authentication', "");
cookie.expires = new DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
cookie.path = COOKIE_PATH;
cookie.httpOnly = COOKIE_HTTP_ONLY;
request.response.headers.add(HttpHeaders.SET_COOKIE, cookie);
}
}
Static Properties
Static Methods
Future<Backend> bind(key, hashMethod, {String host: "0.0.0.0", int port: 8080}) #
Creates a new backend.
static Future<Backend> bind(key, hashMethod, {String host: "0.0.0.0", int port: 8080}){
return HttpServer.bind(host, port).then((httpServer) {
var router = new Router("http://$host:$port", {});
var requestNavigator = new RequestNavigator(httpServer.asBroadcastStream(), router);
return new Backend.config(httpServer, router, requestNavigator,
() => new HMAC(hashMethod, key), HttpBodyHandler.processRequest);
});
}
Constructors
new Backend.config(HttpServer _server, Router router, RequestNavigator _requestNavigator, _hmacFactory, _httpBodyExtractor) #
Constructor.
Backend.config(this._server, this.router, this._requestNavigator,
this._hmacFactory, this._httpBodyExtractor) {
// Add default handler for not found views. Before not found view response
// is sent, check if URI ends with a slash and if not, try to add it.
_requestNavigator.registerDefaultHandler((httpRequest, urlParams)
=> prepareRequestHandler(httpRequest, urlParams, (Request request) {
var uri = request.httpRequest.uri;
if (!uri.path.endsWith('/')) {
//we set request.httpRequest.response because of consistency
// as (request.response := request.httpRequest.response)
request.httpRequest.response.redirect(new Uri(
scheme: uri.scheme,
host: uri.host,
port: uri.port,
path: uri.path + '/',
query: uri.query
));
}
else{
_notFoundViewHandler(request);
}
}));
}
Properties
Methods
void addDefaultHttpHeader(name, value) #
Adds header which will be attached to each response. Could be overwritten.
void addDefaultHttpHeader(name, value) {
_defaulHttpHeaders.add({'name': name, 'value': value});
}
void addNotFoundView(RequestHandler handler) #
If nothing is matched. There is a default _notFoundViewHandler, but it can be overwritten by this method.
void addNotFoundView(RequestHandler handler) {
_notFoundViewHandler = handler;
}
void addRoute(String routeName, Route route) #
Adds route for a particular route so handler could be attached to routeNames.
void addRoute(String routeName, Route route){
router.addRoute(routeName, route);
}
void addStaticView(String routeName, String documentRoot) #
Corresponding Route for routeName should be in the prefix format, i.e. "/uploads/*", as backend will look for files documentRoot/matchedSufix /
void addStaticView(String routeName, String documentRoot) {
var vd = new VirtualDirectory(documentRoot);
_requestNavigator.registerHandler(routeName, (httpRequest, urlParams) {
var relativePath = urlParams['_tail'];
if (relativePath.split('/').contains('..')) {
httpRequest.response.statusCode = HttpStatus.NOT_FOUND;
httpRequest.response.close();
return;
}
String path = p.join(documentRoot, relativePath);
FileSystemEntity.type(path).then((type) {
switch (type) {
case FileSystemEntityType.FILE:
// If file, serve as such.
vd.serveFile(new File(path), httpRequest);
break;
case FileSystemEntityType.DIRECTORY:
// File not found, fall back to 404.
httpRequest.response.statusCode = HttpStatus.NOT_FOUND;
httpRequest.response.close();
break;
default:
// File not found, fall back to 404.
httpRequest.response.statusCode = HttpStatus.NOT_FOUND;
httpRequest.response.close();
break;
}
});
});
}
void addView(String routeName, RequestHandler handler) #
Adds handler for a particular routeName.
void addView(String routeName, RequestHandler handler) {
_requestNavigator.registerHandler(routeName, (httpRequest, urlParams)
=> prepareRequestHandler(httpRequest, urlParams, handler));
}
void authenticate(HttpResponse response, String userId) #
void authenticate(HttpResponse response, String userId) {
List<int> userIdSignature = sign(userId);
Cookie cookie = new Cookie('authentication', JSON.encode({
'userID': userId, 'signature': userIdSignature}));
cookie.expires = new DateTime.now().add(new Duration(days: 365));
cookie.path = COOKIE_PATH;
cookie.httpOnly = COOKIE_HTTP_ONLY;
response.headers.add(HttpHeaders.SET_COOKIE, cookie);
}
String getAuthenticatedUser(HttpHeaders headers) #
String getAuthenticatedUser(HttpHeaders headers) {
if (headers[HttpHeaders.COOKIE] == null) {
return null;
}
for (String cookieString in headers[HttpHeaders.COOKIE]) {
Cookie cookie = new Cookie.fromSetCookieValue(cookieString);
if (cookie.name == 'authentication') {
Map authentication = JSON.decode(cookie.value);
if (verifySignature(authentication['userID'], authentication['signature'])) {
return authentication['userID'];
}
}
}
return null;
}
void logout(Request request) #
void logout(Request request) {
Cookie cookie = new Cookie('authentication', "");
cookie.expires = new DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
cookie.path = COOKIE_PATH;
cookie.httpOnly = COOKIE_HTTP_ONLY;
request.response.headers.add(HttpHeaders.SET_COOKIE, cookie);
}
Future prepareRequestHandler(HttpRequest httpRequest, Map urlParams, RequestHandler handler) #
Transforms httpRequest with urlParams and creates Request which is passed asynchronously to handler.
Future prepareRequestHandler(HttpRequest httpRequest, Map urlParams,
RequestHandler handler) {
return _httpBodyExtractor(httpRequest).then((HttpBody body) {
if (_defaulHttpHeaders != null) {
_defaulHttpHeaders.forEach((header) =>
httpRequest.response.headers.add(header['name'], header['value']));
}
Request request = new Request(body.type, body.body, httpRequest.response,
httpRequest.headers, httpRequest, urlParams);
request.authenticatedUserId = getAuthenticatedUser(request.headers);
handler(request);
return true;
});
}