From c0edecf2a031b987f657daeee29df9bf1f6f9f70 Mon Sep 17 00:00:00 2001 From: Joe Conway Date: Tue, 24 Nov 2015 22:09:03 -0500 Subject: [PATCH] Fixed issue #20. Added x-www-form-urlencoded ContentType to default of HttpController. Allow x-www-form-urlencoded key-values to be sent to HttpController optional arguments like query string params. --- lib/auth/auth_controller.dart | 61 +++++--------- lib/auth/authentication_server.dart | 8 +- lib/auth/authenticator.dart | 2 +- lib/auth/authorization_parser.dart | 15 ++-- lib/auth/client.dart | 2 +- lib/auth/protocols.dart | 2 +- lib/auth/token_generator.dart | 2 +- lib/base/application.dart | 62 +++++---------- lib/base/http_controller.dart | 109 ++++++++++++-------------- lib/base/http_response_exception.dart | 6 +- lib/base/request_handler.dart | 36 ++++----- lib/base/resource_pattern.dart | 34 +++----- lib/base/response.dart | 30 +++---- lib/base/router.dart | 3 +- lib/monadart.dart | 2 +- lib/utilities/test_client.dart | 29 +++---- test/auth_controller_tests.dart | 37 ++++++++- test/controller_tests.dart | 16 ++++ 18 files changed, 209 insertions(+), 247 deletions(-) diff --git a/lib/auth/auth_controller.dart b/lib/auth/auth_controller.dart index aac5e55dc..510a9caed 100644 --- a/lib/auth/auth_controller.dart +++ b/lib/auth/auth_controller.dart @@ -1,7 +1,7 @@ part of monadart; class AuthController extends HttpController { - static String get RoutePattern => "/auth/token/[:action(refresh)]"; + static String get RoutePattern => "/auth/token"; AuthenticationServer authenticationServer; @@ -11,59 +11,40 @@ class AuthController refreshToken(String _) async { - var authorizationHeader = request.innerRequest.headers[HttpHeaders - .AUTHORIZATION]?.first; + Future create({String grant_type, String username, String password, String refresh_token}) async { + var authorizationHeader = request.innerRequest.headers[HttpHeaders.AUTHORIZATION]?.first; var rec = new AuthorizationBasicParser(authorizationHeader); if (rec.errorResponse != null) { return rec.errorResponse; } - var grantType = requestBody["grant_type"]; - if (grantType != "refresh_token") { - return new Response.badRequest(body: {"error" : "grant_type must be refresh_token"}); - } - - var refreshToken = requestBody["refresh_token"]; - var token = await authenticationServer.refresh(refreshToken, rec.username, rec.password); + if (grant_type == "password") { + if (username == null || password == null) { + return new Response.badRequest(body: {"error" : "username and password required"}); + } - return AuthController.tokenResponse(token); - } + var token = await authenticationServer.authenticate(username, password, rec.username, rec.password); + return AuthController.tokenResponse(token); + } else if (grant_type == "refresh") { + if (refresh_token == null) { + return new Response.badRequest(body: {"error" : "missing refresh_token"}); + } - @httpPost - Future createToken() async { - var authorizationHeader = request.innerRequest.headers[HttpHeaders - .AUTHORIZATION]?.first; - - var rec = new AuthorizationBasicParser(authorizationHeader); - if (rec.errorResponse != null) { - return rec.errorResponse; - } - - if (requestBody["grant_type"] != "password") { - return new Response.badRequest(body: {"error" : "grant_type must be password"}); + var token = await authenticationServer.refresh(refresh_token, rec.username, rec.password); + return AuthController.tokenResponse(token); } - var username = requestBody["username"]; - var password = requestBody["password"]; - if (username == null || password == null) { - return new Response.badRequest(body: {"error" : "username and password required"}); - } - - var token = await authenticationServer.authenticate( - username, password, rec.username, rec.password); - - return AuthController.tokenResponse(token); + return new Response.badRequest(body: {"error": "invalid grant_type"}); } static Response tokenResponse(Tokenizable token) { var jsonToken = { - "access_token" : token.accessToken, - "token_type" : token.type, - "expires_in" : token.expirationDate.difference(new DateTime.now().toUtc()).inSeconds, - "refresh_token" : token.refreshToken + "access_token": token.accessToken, + "token_type": token.type, + "expires_in": token.expirationDate.difference(new DateTime.now().toUtc()).inSeconds, + "refresh_token": token.refreshToken }; - return new Response(200, {"Cache-Control" : "no-store", "Pragma" : "no-cache"}, jsonToken); + return new Response(200, {"Cache-Control": "no-store", "Pragma": "no-cache"}, jsonToken); } } diff --git a/lib/auth/authentication_server.dart b/lib/auth/authentication_server.dart index 67e374981..9bc13ac6f 100644 --- a/lib/auth/authentication_server.dart +++ b/lib/auth/authentication_server.dart @@ -1,12 +1,10 @@ part of monadart; - class AuthenticationServer { AuthenticationServerDelegate delegate; Map _clientCache = {}; - AuthenticationServer(this.delegate) { - } + AuthenticationServer(this.delegate) {} Authenticator authenticator({List strategies: const [Authenticator.StrategyResourceOwner]}) { return new Authenticator(this, strategies); @@ -70,7 +68,7 @@ class AuthenticationServer processResourceOwnerRequest(ResourceRequest req) async { var parser = new AuthorizationBearerParser(req.innerRequest.headers[HttpHeaders.AUTHORIZATION]?.first); - if(parser.errorResponse != null) { + if (parser.errorResponse != null) { return parser.errorResponse; } diff --git a/lib/auth/authorization_parser.dart b/lib/auth/authorization_parser.dart index 174c75d95..bfbb156c2 100644 --- a/lib/auth/authorization_parser.dart +++ b/lib/auth/authorization_parser.dart @@ -4,7 +4,7 @@ abstract class AuthorizationFailable { Response errorResponse; } -class AuthorizationBearerParser extends AuthorizationFailable{ +class AuthorizationBearerParser extends AuthorizationFailable { String bearerToken; AuthorizationBearerParser(String authorizationHeader) { @@ -13,7 +13,7 @@ class AuthorizationBearerParser extends AuthorizationFailable{ Response parse(String authorizationHeader) { if (authorizationHeader == null) { - return new Response.unauthorized(body: {"error" : "No authorization header."}); + return new Response.unauthorized(body: {"error": "No authorization header."}); } var matcher = new RegExp("Bearer (.*)"); @@ -38,7 +38,7 @@ class AuthorizationBasicParser extends AuthorizationFailable { Response parse(String authorizationHeader) { if (authorizationHeader == null) { - return new Response.unauthorized(body: {"error" : "No authorization header."}); + return new Response.unauthorized(body: {"error": "No authorization header."}); } var matcher = new RegExp("Basic (.*)"); @@ -50,15 +50,14 @@ class AuthorizationBasicParser extends AuthorizationFailable { var base64String = match[1]; var decodedCredentials = null; try { - decodedCredentials = new String.fromCharCodes( - CryptoUtils.base64StringToBytes(base64String)); + decodedCredentials = new String.fromCharCodes(CryptoUtils.base64StringToBytes(base64String)); } catch (e) { - return new Response.badRequest(body: {"error" : "Improper authorization header."}); + return new Response.badRequest(body: {"error": "Improper authorization header."}); } var splitCredentials = decodedCredentials.split(":"); if (splitCredentials.length != 2) { - return new Response.badRequest(body: {"error" : "Improper client credentials."}); + return new Response.badRequest(body: {"error": "Improper client credentials."}); } username = splitCredentials.first; @@ -66,4 +65,4 @@ class AuthorizationBasicParser extends AuthorizationFailable { return null; } -} \ No newline at end of file +} diff --git a/lib/auth/client.dart b/lib/auth/client.dart index 23a1676b1..2e428798f 100644 --- a/lib/auth/client.dart +++ b/lib/auth/client.dart @@ -6,4 +6,4 @@ class Client { String salt; Client(this.id, this.hashedSecret, this.salt); -} \ No newline at end of file +} diff --git a/lib/auth/protocols.dart b/lib/auth/protocols.dart index 32f613cf5..927e2c2f0 100644 --- a/lib/auth/protocols.dart +++ b/lib/auth/protocols.dart @@ -37,4 +37,4 @@ abstract class AuthenticationServerDelegate { /// The configuration for the HTTP(s) server this application is running. /// /// This must be configured prior to [start]ing the [Application]. - ApplicationInstanceConfiguration configuration = - new ApplicationInstanceConfiguration(); + ApplicationInstanceConfiguration configuration = new ApplicationInstanceConfiguration(); /// Starts the application by spawning Isolates that listen for HTTP(s) requests. /// @@ -129,8 +131,7 @@ class Application { configuration._shared = numberOfInstances > 1; for (int i = 0; i < numberOfInstances; i++) { - var config = - new ApplicationInstanceConfiguration.fromConfiguration(configuration); + var config = new ApplicationInstanceConfiguration.fromConfiguration(configuration); var serverRecord = await _spawn(config, i + 1); servers.add(serverRecord); @@ -143,20 +144,15 @@ class Application { await Future.wait(futures); } - Future<_ServerRecord> _spawn( - ApplicationInstanceConfiguration config, int identifier) async { + Future<_ServerRecord> _spawn(ApplicationInstanceConfiguration config, int identifier) async { var receivePort = new ReceivePort(); var pipelineTypeMirror = reflectType(PipelineType); var pipelineLibraryURI = (pipelineTypeMirror.owner as LibraryMirror).uri; var pipelineTypeName = MirrorSystem.getName(pipelineTypeMirror.simpleName); - var initialMessage = new _InitialServerMessage( - pipelineTypeName, - pipelineLibraryURI, - config, identifier, receivePort.sendPort); - var isolate = - await Isolate.spawn(_Server.entry, initialMessage, paused: true); + var initialMessage = new _InitialServerMessage(pipelineTypeName, pipelineLibraryURI, config, identifier, receivePort.sendPort); + var isolate = await Isolate.spawn(_Server.entry, initialMessage, paused: true); isolate.addErrorListener(receivePort.sendPort); return new _ServerRecord(isolate, receivePort, identifier); @@ -172,11 +168,7 @@ class _Server { ApplicationPipeline pipeline; int identifier; - _Server(this.pipeline, - this.configuration, this.identifier, - this.supervisingApplicationPort); - - + _Server(this.pipeline, this.configuration, this.identifier, this.supervisingApplicationPort); ResourceRequest createRequest(HttpRequest req) { return new ResourceRequest(req); @@ -209,37 +201,24 @@ class _Server { if (configuration.serverCertificateName != null) { HttpServer .bindSecure(configuration.address, configuration.port, - certificateName: configuration.serverCertificateName, - v6Only: configuration.isIpv6Only, - shared: configuration._shared) + certificateName: configuration.serverCertificateName, v6Only: configuration.isIpv6Only, shared: configuration._shared) .then(onBind); } else if (configuration.isUsingClientCertificate) { HttpServer .bindSecure(configuration.address, configuration.port, - requestClientCertificate: true, - v6Only: configuration.isIpv6Only, - shared: configuration._shared) + requestClientCertificate: true, v6Only: configuration.isIpv6Only, shared: configuration._shared) .then(onBind); } else { - HttpServer - .bind(configuration.address, configuration.port, - v6Only: configuration.isIpv6Only, shared: configuration._shared) - .then(onBind); + HttpServer.bind(configuration.address, configuration.port, v6Only: configuration.isIpv6Only, shared: configuration._shared).then(onBind); } } static void entry(_InitialServerMessage params) { - var pipelineSourceLibraryMirror = - currentMirrorSystem().libraries[params.pipelineLibraryURI]; - var pipelineTypeMirror = pipelineSourceLibraryMirror.declarations[ - new Symbol(params.pipelineTypeName)] as ClassMirror; + var pipelineSourceLibraryMirror = currentMirrorSystem().libraries[params.pipelineLibraryURI]; + var pipelineTypeMirror = pipelineSourceLibraryMirror.declarations[new Symbol(params.pipelineTypeName)] as ClassMirror; var app = pipelineTypeMirror.newInstance(new Symbol(""), []).reflectee; - var server = new _Server( - app, - params.configuration, - params.identifier, - params.parentMessagePort); + var server = new _Server(app, params.configuration, params.identifier, params.parentMessagePort); server.start(); } @@ -271,6 +250,5 @@ class _InitialServerMessage { SendPort parentMessagePort; int identifier; - _InitialServerMessage(this.pipelineTypeName, this.pipelineLibraryURI, - this.configuration, this.identifier, this.parentMessagePort); + _InitialServerMessage(this.pipelineTypeName, this.pipelineLibraryURI, this.configuration, this.identifier, this.parentMessagePort); } diff --git a/lib/base/http_controller.dart b/lib/base/http_controller.dart index 3ee954cf7..01a2f79df 100644 --- a/lib/base/http_controller.dart +++ b/lib/base/http_controller.dart @@ -4,16 +4,18 @@ part of monadart; /// /// Subclasses of this class can process and respond to an HTTP request. abstract class HttpController extends RequestHandler { + static ContentType _applicationWWWFormURLEncodedContentType = new ContentType("application", "x-www-form-urlencoded"); + /// An optional exception handler for this controller that generates an HTTP error response. /// /// By default, this handler is null. Exceptions will be handled by the default implementation /// of [RequestHandler]. You may provide another exception handler to override the default behavior. /// This exception handler MUST return a [Response] object, no matter what. Function get exceptionHandler => _exceptionHandler; - void set exceptionHandler(Response handler(ResourceRequest resourceRequest, - dynamic exceptionOrError, StackTrace stacktrace)) { + void set exceptionHandler(Response handler(ResourceRequest resourceRequest, dynamic exceptionOrError, StackTrace stacktrace)) { _exceptionHandler = handler; } + Function _exceptionHandler = null; /// The request being processed by this [HttpController]. @@ -26,11 +28,11 @@ abstract class HttpController extends RequestHandler { /// Types of content this [HttpController] will accept. /// - /// By default, a resource controller will accept 'application/json' requests. + /// By default, a resource controller will accept 'application/json' and 'application/x-www-form-urlencoded' requests. /// If a request is sent to an instance of [HttpController] and has an HTTP request body, /// but the Content-Type of the request isn't within this list, the [HttpController] /// will automatically respond with an Unsupported Media Type response. - List acceptedContentTypes = [ContentType.JSON]; + List acceptedContentTypes = [ContentType.JSON, _applicationWWWFormURLEncodedContentType]; /// The content type of responses from this [HttpController]. /// @@ -38,6 +40,8 @@ abstract class HttpController extends RequestHandler { /// HTTP header. ContentType responseContentType = ContentType.JSON; + // TODO: Privatize these and make method that requires both + /// Encodes the HTTP response body object that is part of the [Response] returned from this request handler methods. /// /// By default, this encoder will convert the body object as JSON. @@ -74,19 +78,12 @@ abstract class HttpController extends RequestHandler { for (var key in decls.keys) { var decl = decls[key]; if (decl is MethodMirror) { - var methodAttrs = decl.metadata.firstWhere( - (attr) => attr.reflectee is HttpMethod, - orElse: () => null); + var methodAttrs = decl.metadata.firstWhere((attr) => attr.reflectee is HttpMethod, orElse: () => null); if (methodAttrs != null) { - var params = (decl as MethodMirror) - .parameters - .where((pm) => !pm.isOptional) - .map((pm) => MirrorSystem.getName(pm.simpleName)) - .toList(); + var params = (decl as MethodMirror).parameters.where((pm) => !pm.isOptional).map((pm) => MirrorSystem.getName(pm.simpleName)).toList(); - HttpMethod r = - new HttpMethod._fromMethod(methodAttrs.reflectee, params); + HttpMethod r = new HttpMethod._fromMethod(methodAttrs.reflectee, params); if (r.matchesRequest(req)) { symbol = key; @@ -97,9 +94,7 @@ abstract class HttpController extends RequestHandler { } if (symbol == null) { - throw new _InternalControllerException( - "No handler for request method and parameters available.", - HttpStatus.NOT_FOUND); + throw new _InternalControllerException("No handler for request method and parameters available.", HttpStatus.NOT_FOUND); } return symbol; @@ -109,24 +104,20 @@ abstract class HttpController extends RequestHandler { if (request.innerRequest.contentLength > 0) { var incomingContentType = request.innerRequest.headers.contentType; var matchingContentType = acceptedContentTypes.firstWhere((ct) { - return ct.primaryType == incomingContentType.primaryType && - ct.subType == incomingContentType.subType; + return ct.primaryType == incomingContentType.primaryType && ct.subType == incomingContentType.subType; }, orElse: () => null); if (matchingContentType != null) { - return (await HttpBodyHandler.processRequest(request.innerRequest)) - .body; + return (await HttpBodyHandler.processRequest(request.innerRequest)).body; } else { - throw new _InternalControllerException( - "Unsupported Content-Type", HttpStatus.UNSUPPORTED_MEDIA_TYPE); + throw new _InternalControllerException("Unsupported Content-Type", HttpStatus.UNSUPPORTED_MEDIA_TYPE); } } return null; } - dynamic _convertParameterWithMirror( - String parameterValue, ParameterMirror parameterMirror) { + dynamic _convertParameterWithMirror(String parameterValue, ParameterMirror parameterMirror) { var typeMirror = parameterMirror.type; if (typeMirror.isSubtypeOf(reflectType(String))) { return parameterValue; @@ -140,54 +131,56 @@ abstract class HttpController extends RequestHandler { var reflValue = cm.invoke(parseDecl.simpleName, [parameterValue]); return reflValue.reflectee; } catch (e) { - throw new _InternalControllerException( - "Invalid value for parameter type", HttpStatus.BAD_REQUEST, - responseMessage: "URI parameter is wrong type"); + throw new _InternalControllerException("Invalid value for parameter type", HttpStatus.BAD_REQUEST, responseMessage: "URI parameter is wrong type"); } } } // If we get here, then it wasn't a string and couldn't be parsed, and we should throw? - throw new _InternalControllerException( - "Invalid path parameter type, types must be String or implement parse", - HttpStatus.INTERNAL_SERVER_ERROR, + throw new _InternalControllerException("Invalid path parameter type, types must be String or implement parse", HttpStatus.INTERNAL_SERVER_ERROR, responseMessage: "URI parameter is wrong type"); return null; } - List _parametersForRequest( - ResourceRequest req, Symbol handlerMethodSymbol) { - var handlerMirror = - reflect(this).type.declarations[handlerMethodSymbol] as MethodMirror; + List _parametersForRequest(ResourceRequest req, Symbol handlerMethodSymbol) { + var handlerMirror = reflect(this).type.declarations[handlerMethodSymbol] as MethodMirror; - return handlerMirror.parameters - .where((methodParmeter) => !methodParmeter.isOptional) - .map((methodParameter) { - var value = this.request.path.variables[ - MirrorSystem.getName(methodParameter.simpleName)]; + return handlerMirror.parameters.where((methodParmeter) => !methodParmeter.isOptional).map((methodParameter) { + var value = this.request.path.variables[MirrorSystem.getName(methodParameter.simpleName)]; return _convertParameterWithMirror(value, methodParameter); }).toList(); } - Map _queryParametersForRequest( - ResourceRequest req, Symbol handlerMethodSymbol) { - var queryParams = req.innerRequest.uri.queryParameters; + Map _queryParametersForRequest(ResourceRequest req, dynamic body, Symbol handlerMethodSymbol) { + Map queryParams = {}; + + var contentTypeString = req.innerRequest.headers.value(HttpHeaders.CONTENT_TYPE); + var contentType = null; + if (contentTypeString != null) { + contentType = ContentType.parse(contentTypeString); + } + + if (contentType != null && + contentType.primaryType == _applicationWWWFormURLEncodedContentType.primaryType && + contentType.subType == _applicationWWWFormURLEncodedContentType.subType) + { + queryParams = requestBody; + } else { + queryParams = req.innerRequest.uri.queryParameters; + } + if (queryParams.length == 0) { return null; } - var optionalParams = (reflect(this).type.declarations[handlerMethodSymbol] - as MethodMirror) - .parameters - .where((methodParameter) => methodParameter.isOptional) - .toList(); + var optionalParams = + (reflect(this).type.declarations[handlerMethodSymbol] as MethodMirror).parameters.where((methodParameter) => methodParameter.isOptional).toList(); var retMap = {}; queryParams.forEach((k, v) { var keySymbol = new Symbol(k); - var matchingParameter = optionalParams - .firstWhere((p) => p.simpleName == keySymbol, orElse: () => null); + var matchingParameter = optionalParams.firstWhere((p) => p.simpleName == keySymbol, orElse: () => null); if (matchingParameter != null) { retMap[keySymbol] = _convertParameterWithMirror(v, matchingParameter); } @@ -199,28 +192,23 @@ abstract class HttpController extends RequestHandler { Future _process() async { try { var methodSymbol = _routeMethodSymbolForRequest(request); - var handlerParameters = - _parametersForRequest(request, methodSymbol); - var handlerQueryParameters = - _queryParametersForRequest(request, methodSymbol); + var handlerParameters = _parametersForRequest(request, methodSymbol); requestBody = await _readRequestBodyForRequest(request); + var handlerQueryParameters = _queryParametersForRequest(request, requestBody, methodSymbol); if (requestBody != null) { didDecodeRequestBody(requestBody); } - Future eventualResponse = reflect(this) - .invoke(methodSymbol, handlerParameters, handlerQueryParameters) - .reflectee; + Future eventualResponse = reflect(this).invoke(methodSymbol, handlerParameters, handlerQueryParameters).reflectee; var response = await eventualResponse; willSendResponse(response); response.body = responseBodyEncoder(response.body); - response.headers[HttpHeaders.CONTENT_TYPE] = - responseContentType.toString(); + response.headers[HttpHeaders.CONTENT_TYPE] = responseContentType.toString(); return response; } on _InternalControllerException catch (e) { @@ -268,8 +256,7 @@ class _InternalControllerException { final HttpHeaders additionalHeaders; final String responseMessage; - _InternalControllerException(this.message, this.statusCode, - {HttpHeaders additionalHeaders: null, String responseMessage: null}) + _InternalControllerException(this.message, this.statusCode, {HttpHeaders additionalHeaders: null, String responseMessage: null}) : this.additionalHeaders = additionalHeaders, this.responseMessage = responseMessage; } diff --git a/lib/base/http_response_exception.dart b/lib/base/http_response_exception.dart index 825411a0f..1db0af7ab 100644 --- a/lib/base/http_response_exception.dart +++ b/lib/base/http_response_exception.dart @@ -7,8 +7,6 @@ class HttpResponseException implements Exception { HttpResponseException(this.statusCode, this.message); Response response() { - return new Response(statusCode, {HttpHeaders.CONTENT_TYPE : "application/json"}, JSON.encode({ - "error" : message - })); + return new Response(statusCode, {HttpHeaders.CONTENT_TYPE: "application/json"}, JSON.encode({"error": message})); } -} \ No newline at end of file +} diff --git a/lib/base/request_handler.dart b/lib/base/request_handler.dart index 69cd8c2f5..4f357db2e 100644 --- a/lib/base/request_handler.dart +++ b/lib/base/request_handler.dart @@ -61,23 +61,22 @@ class RequestHandler { void deliver(ResourceRequest req) { logger.finest("$this received request $req."); - processRequest(req) - .then((result) { - if (result is ResourceRequest && nextHandler != null) { - nextHandler.deliver(req); - } else if (result is Response) { - req.respond(result as Response); - } - }) - .catchError((err, st) { - if (err is HttpResponseException) { - req.respond(err.response()); - } else { - logger.severe("$this generated internal error for request ${req.toDebugString()}. $err\n Stacktrace:\n${st.toString()}"); - req.respond(new Response.serverError(headers: {HttpHeaders.CONTENT_TYPE : "application/json"}, - body: JSON.encode({"error" : "Unexpected exception in ${this.runtimeType}.", "stacktrace" : st.toString()}))); - } - }); + processRequest(req).then((result) { + if (result is ResourceRequest && nextHandler != null) { + nextHandler.deliver(req); + } else if (result is Response) { + req.respond(result as Response); + } + }).catchError((err, st) { + if (err is HttpResponseException) { + req.respond(err.response()); + } else { + logger.severe("$this generated internal error for request ${req.toDebugString()}. $err\n Stacktrace:\n${st.toString()}"); + req.respond(new Response.serverError( + headers: {HttpHeaders.CONTENT_TYPE: "application/json"}, + body: JSON.encode({"error": "Unexpected exception in ${this.runtimeType}.", "stacktrace": st.toString()}))); + } + }); } /// Overridden by subclasses to modify or respond to an incoming request. @@ -104,8 +103,7 @@ class RequestHandlerGenerator extends RequestHandler { @override void deliver(ResourceRequest req) { logger.finest("Generating handler $T with arguments $arguments."); - var handler = reflectClass(T).newInstance(new Symbol(""), arguments).reflectee - as RequestHandler; + var handler = reflectClass(T).newInstance(new Symbol(""), arguments).reflectee as RequestHandler; handler.nextHandler = this.nextHandler; handler.deliver(req); } diff --git a/lib/base/resource_pattern.dart b/lib/base/resource_pattern.dart index 54cbfc088..57a9f01f2 100644 --- a/lib/base/resource_pattern.dart +++ b/lib/base/resource_pattern.dart @@ -42,14 +42,12 @@ class ResourcePattern { ResourcePattern(String patternString) { List pathElements = splitPattern(patternString); - List<_ResourcePatternElement> elems = - resourceElementsFromPathElements(patternString); + List<_ResourcePatternElement> elems = resourceElementsFromPathElements(patternString); matchHead = matchSpecFromElements(pathElements, elems); } - static _ResourcePatternSet matchSpecFromElements(List pathElements, - List<_ResourcePatternElement> resourceElements) { + static _ResourcePatternSet matchSpecFromElements(List pathElements, List<_ResourcePatternElement> resourceElements) { // These two arrays should be identical in size by the time they get here _ResourcePatternSet setHead = new _ResourcePatternSet(); @@ -84,8 +82,7 @@ class ResourcePattern { } static List splitPattern(String patternString) { - return patternString.split("/").fold(new List(), - (List accum, String e) { + return patternString.split("/").fold(new List(), (List accum, String e) { var trimmed = e.trim(); if (trimmed.length > 0) { accum.add(trimmed); @@ -94,13 +91,9 @@ class ResourcePattern { }); } - static List<_ResourcePatternElement> resourceElementsFromPathElements( - String patternString) { + static List<_ResourcePatternElement> resourceElementsFromPathElements(String patternString) { var expr = new RegExp(r"[\[\]]"); - return splitPattern(patternString) - .map((segment) => - new _ResourcePatternElement(segment.replaceAll(expr, ""))) - .toList(); + return splitPattern(patternString).map((segment) => new _ResourcePatternElement(segment.replaceAll(expr, ""))).toList(); } ResourcePatternMatch matchUri(Uri uri) { @@ -117,9 +110,7 @@ class ResourcePattern { var resourceSegment = resourceIterator.current; if (resourceSegment.matchesRemaining) { - var remainingString = incomingPathSegments - .sublist(incomingPathIndex, incomingPathSegments.length) - .join("/"); + var remainingString = incomingPathSegments.sublist(incomingPathIndex, incomingPathSegments.length).join("/"); match.remainingPath = remainingString; return match; @@ -196,14 +187,12 @@ class _ResourcePatternElement { // I'd prefer that this used a outer non-capturing group on the parantheses after the name; // but apparently this regex parser won't pick up the capture group inside the noncapturing group for some reason - static RegExp patternFinder = - new RegExp(r"^(\w+)(\(([^\)]+)\))?$", caseSensitive: false); + static RegExp patternFinder = new RegExp(r"^(\w+)(\(([^\)]+)\))?$", caseSensitive: false); void constructFromString(String str) { Match m = patternFinder.firstMatch(str); if (m == null) { - throw new ArgumentError( - "invalid resource pattern segment ${str}, available formats are the following: literal, *, {name}, {name(pattern)}"); + throw new ArgumentError("invalid resource pattern segment ${str}, available formats are the following: literal, *, {name}, {name(pattern)}"); } name = m.group(1); @@ -215,17 +204,14 @@ class _ResourcePatternElement { } } - matchRegex = - new RegExp(r"^" + "${matchString}" + r"$", caseSensitive: false); + matchRegex = new RegExp(r"^" + "${matchString}" + r"$", caseSensitive: false); } bool fullMatchForString(String pathSegment) { var iter = matchRegex.allMatches(pathSegment).iterator; if (iter.moveNext()) { var match = iter.current; - if (match.start == 0 && - match.end == pathSegment.length && - !iter.moveNext()) { + if (match.start == 0 && match.end == pathSegment.length && !iter.moveNext()) { return true; } else { return false; diff --git a/lib/base/response.dart b/lib/base/response.dart index 69236e9ed..967ecb9f9 100644 --- a/lib/base/response.dart +++ b/lib/base/response.dart @@ -30,10 +30,8 @@ class Response implements RequestHandlerResult { } } - Response.ok(dynamic body, {Map headers}) - : this(HttpStatus.OK, headers, body); - Response.created(String location, - {dynamic body, Map headers}) { + Response.ok(dynamic body, {Map headers}) : this(HttpStatus.OK, headers, body); + Response.created(String location, {dynamic body, Map headers}) { this.headers = headers; this.body = body; this.statusCode = HttpStatus.CREATED; @@ -44,24 +42,16 @@ class Response implements RequestHandlerResult { this.headers[HttpHeaders.LOCATION] = location; } } - Response.accepted({Map headers}) - : this(HttpStatus.ACCEPTED, headers, null); + Response.accepted({Map headers}) : this(HttpStatus.ACCEPTED, headers, null); - Response.badRequest({Map headers, dynamic body}) - : this(HttpStatus.BAD_REQUEST, headers, body); - Response.unauthorized({Map headers, dynamic body}) - : this(HttpStatus.UNAUTHORIZED, headers, body); - Response.forbidden({Map headers, dynamic body}) - : this(HttpStatus.FORBIDDEN, headers, body); - Response.notFound({Map headers, dynamic body}) - : this(HttpStatus.NOT_FOUND, headers, body); - Response.conflict({Map headers, dynamic body}) - : this(HttpStatus.CONFLICT, headers, body); - Response.gone({Map headers, dynamic body}) - : this(HttpStatus.GONE, headers, body); + Response.badRequest({Map headers, dynamic body}) : this(HttpStatus.BAD_REQUEST, headers, body); + Response.unauthorized({Map headers, dynamic body}) : this(HttpStatus.UNAUTHORIZED, headers, body); + Response.forbidden({Map headers, dynamic body}) : this(HttpStatus.FORBIDDEN, headers, body); + Response.notFound({Map headers, dynamic body}) : this(HttpStatus.NOT_FOUND, headers, body); + Response.conflict({Map headers, dynamic body}) : this(HttpStatus.CONFLICT, headers, body); + Response.gone({Map headers, dynamic body}) : this(HttpStatus.GONE, headers, body); - Response.serverError({Map headers, dynamic body}) - : this(HttpStatus.INTERNAL_SERVER_ERROR, headers, body); + Response.serverError({Map headers, dynamic body}) : this(HttpStatus.INTERNAL_SERVER_ERROR, headers, body); String toString() { return "$statusCode $headers"; diff --git a/lib/base/router.dart b/lib/base/router.dart index 6aceec933..ad31b814d 100644 --- a/lib/base/router.dart +++ b/lib/base/router.dart @@ -77,8 +77,7 @@ class Router extends RequestHandler { // Strip out any extraneous /s var finalPattern = pattern.split("/").where((c) => c != "").join("/"); - var route = new _ResourceRoute( - new ResourcePattern(finalPattern), new RequestHandler()); + var route = new _ResourceRoute(new ResourcePattern(finalPattern), new RequestHandler()); _routes.add(route); return route; diff --git a/lib/monadart.dart b/lib/monadart.dart index d8cbbc951..e708553cd 100644 --- a/lib/monadart.dart +++ b/lib/monadart.dart @@ -33,4 +33,4 @@ part 'auth/auth_controller.dart'; part 'auth/authorization_parser.dart'; part 'auth/authentication_server.dart'; -part 'utilities/test_client.dart'; \ No newline at end of file +part 'utilities/test_client.dart'; diff --git a/lib/utilities/test_client.dart b/lib/utilities/test_client.dart index 716dae2c1..01799143e 100644 --- a/lib/utilities/test_client.dart +++ b/lib/utilities/test_client.dart @@ -9,7 +9,6 @@ part of monadart; expect(response.jsonObject.hasKeys(["id", "name", "email"]), true); */ - class TestClient { String host; @@ -20,10 +19,10 @@ class TestClient { JSONTestRequest jsonRequest(String path) { JSONTestRequest r = new JSONTestRequest() - ..host = this.host - ..path = path - ..contentType = "application/json;charset=utf-8" - ..accept = "application/json"; + ..host = this.host + ..path = path + ..contentType = "application/json;charset=utf-8" + ..accept = "application/json"; return r; } @@ -38,16 +37,14 @@ class TestClient { clientID ??= defaultClientID; clientSecret ??= defaultClientSecret; - var req = request(path) - ..basicAuthorization = "$clientID:$clientSecret"; + var req = request(path)..basicAuthorization = "$clientID:$clientSecret"; return req; } TestRequest authenticatedRequest(String path, {String accessToken: null}) { accessToken ??= token["access_token"]; - var req = request(path) - ..bearerAuthorization = accessToken; + var req = request(path)..bearerAuthorization = accessToken; return req; } @@ -55,31 +52,32 @@ class TestClient { clientID ??= defaultClientID; clientSecret ??= defaultClientSecret; - var req = jsonRequest(path) - ..basicAuthorization = "$clientID:$clientSecret"; + var req = jsonRequest(path)..basicAuthorization = "$clientID:$clientSecret"; return req; } JSONTestRequest authenticatedJSONRequest(String path, {String accessToken: null}) { accessToken ??= token["access_token"]; - var req = jsonRequest(path) - ..bearerAuthorization = accessToken; + var req = jsonRequest(path)..bearerAuthorization = accessToken; return req; } } -class TestRequest { +class TestRequest { String host; void set basicAuthorization(String str) { addHeader(HttpHeaders.AUTHORIZATION, "Basic ${CryptoUtils.bytesToBase64(str.codeUnits)}"); } + void set bearerAuthorization(String str) { addHeader(HttpHeaders.AUTHORIZATION, "Bearer $str"); } + void set contentType(String str) { addHeader(HttpHeaders.CONTENT_TYPE, str); } + void set accept(String str) { addHeader(HttpHeaders.ACCEPT, str); } @@ -126,7 +124,6 @@ class JSONTestRequest extends TestRequest { var res = await response; return new JSONTestResponse(res.statusCode, res.headers, res.body); } - } class TestResponse { @@ -194,4 +191,4 @@ class JSONTestResponse extends TestResponse { return hasValues(values); } -} \ No newline at end of file +} diff --git a/test/auth_controller_tests.dart b/test/auth_controller_tests.dart index ec4870e61..f37c48ba4 100644 --- a/test/auth_controller_tests.dart +++ b/test/auth_controller_tests.dart @@ -31,6 +31,7 @@ void main() { server.listen((req) { var resReq = new ResourceRequest(req); var authController = new AuthController(authenticationServer); + authController.deliver(resReq); }); }); @@ -53,7 +54,7 @@ void main() { body += "$k=${Uri.encodeQueryComponent(v)}&"; }); - var res = await http.post("http://localhost:8080/token", + var res = await http.post("http://localhost:8080/auth/token", headers: {"Content-Type" : "application/x-www-form-urlencoded", "Authorization" : "Basic ${CryptoUtils.bytesToBase64("com.stablekernel.app1:kilimanjaro".codeUnits)}"}, body: body); @@ -144,7 +145,41 @@ void main() { "Authorization" : "Basic ${CryptoUtils.bytesToBase64("com.stablekernel.app1:kilimanjaro".codeUnits)}"}, body: encoder({"grant_type" : "password", "username" : "bob+0@stablekernel.com", "password" : "fobar%"})); expect(res.statusCode, 401); + }); + + test("Refresh token responds with token on correct input", () async { + await createUsers(adapter, 1); + + var m = {"grant_type" : "password", "username" : "bob+0@stablekernel.com", "password" : "foobaraxegrind21%"}; + var body = ""; + m.forEach((k, v) { + body += "$k=${Uri.encodeQueryComponent(v)}&"; + }); + + var res = await http.post("http://localhost:8080/auth/token", + headers: {"Content-Type" : "application/x-www-form-urlencoded", + "Authorization" : "Basic ${CryptoUtils.bytesToBase64("com.stablekernel.app1:kilimanjaro".codeUnits)}"}, + body: body); + var json = JSON.decode(res.body); + + m = {"grant_type" : "refresh", "refresh_token" : json["refresh_token"]}; + body = ""; + m.forEach((k, v) { + body += "$k=${Uri.encodeQueryComponent(v)}&"; + }); + res = await http.post("http://localhost:8080/auth/token", + headers: {"Content-Type" : "application/x-www-form-urlencoded", + "Authorization" : "Basic ${CryptoUtils.bytesToBase64("com.stablekernel.app1:kilimanjaro".codeUnits)}"}, + body: body); + + expect(res.statusCode, 200); + json = JSON.decode(res.body); + + expect(json["access_token"].length, greaterThan(0)); + expect(json["refresh_token"].length, greaterThan(0)); + expect(json["expires_in"], greaterThan(3500)); + expect(json["token_type"], "bearer"); }); } diff --git a/test/controller_tests.dart b/test/controller_tests.dart index 8980586a6..dce912fad 100644 --- a/test/controller_tests.dart +++ b/test/controller_tests.dart @@ -170,7 +170,17 @@ void main() { res = await http.get("http://localhost:4040/a?foo=2001-01-01T00:00:00.000000Z"); expect(res.statusCode, 200); + + server.close(); + }); + + test("Query parameters can be obtained from x-www-form-urlencoded", () async { + var server = await enableController("/a", new RequestHandlerGenerator()); + var res = await http.post("http://localhost:4040/a", headers: {"Content-Type" : "application/x-www-form-urlencoded"}, body: "opt=7"); + expect(res.body, '"7"'); + server.close(); }); + } @@ -235,6 +245,11 @@ class IntController extends HttpController { Future getAll({int opt: null}) async { return new Response.ok("${opt}"); } + + @httpPost + Future create({int opt: null}) async { + return new Response.ok("${opt}"); + } } class DateTimeController extends HttpController { @@ -250,6 +265,7 @@ class DateTimeController extends HttpController { } Future enableController(String pattern, RequestHandler controller) async { + var router = new Router(); router.route(pattern).then(controller);