Date post: | 18-Jan-2015 |
Category: |
Technology |
Upload: | wo-community |
View: | 4,837 times |
Download: | 2 times |
MONTREAL 1/3 JULY 2011
ERRestPascal RobertConatus/MacTI
• What's new in ERRest
• Security
• Versioning
• HTML routing
• Debugging
• Caching (Sunday!)
• Optimistic locking (Sunday!)
• Using the correct HTTP verbs and codes (Sunday!)
The Menu
What's New in ERRest
Anymous updates
• No need to send the ids of nested objects anymore
• Call ERXKeyFilter.setAnonymousUpdateEnabled(true)
• If 1:N relationship, will replace existing values for all nested objects
Anonymous updateprotected ERXKeyFilter filter() {
ERXKeyFilter filter = ERXKeyFilter.filterWithAttributes(); ERXKeyFilter personFilter = ERXKeyFilter.filterWithAttributes(); personFilter.include(Person.FIRST_NAME); personFilter.setAnonymousUpdateEnabled(true); filter.include(BlogEntry.PERSON, personFilter); return filter; }
curl -X PUT -d "{ title: 'New Post', person: {firstName: 'Test'} }" http://127.0.0.1/cgi-bin/WebObjects/SimpleBlog.woa/ra/posts/23.json
Sort ordering on 1:N
• You can now sort a 1:N relationship
• Call ERXKeyFilter.setSortOrderings()
Sort ordering ERXKeyFilter filter = ERXKeyFilter.filterWithAttributes();
ERXKeyFilter categoryFilter = ERXKeyFilter.filterWithAttributes(); categoryFilter.setSortOrderings(BlogCategory.SHORT_NAME.ascs());
filter.include(BlogEntry.CATEGORIES, categoryFilter);
Ignoring unknow keys
• By default, returns status 500 if unknow attribute is found in request
• To ignore those errors, call:
yourErxKeyFilter.setUnknownKeyIgnored(true)
ERXRouteController.performActionName
That method have been split in 5 methods to make it easier to override on the method.
ERXRestContext
• Hold a userInfo dict + the editing context
• Can pass a different date format per controller
• Override createRestContext to do that
ERXRestContextpublic class BlogEntryController extends BaseRestController {
... @Override protected ERXRestContext createRestContext() { ERXRestContext restContext = new ERXRestContext(editingContext()); restContext.setUserInfoForKey("yyyy-MM-dd", "er.rest.dateFormat"); restContext.setUserInfoForKey("yyyy-MM-dd", "er.rest.timestampFormat"); return restContext; }}
• More strict HTTP status code in responses
• Support for @QueryParam, @CookieParam and @HeaderParam for JSR-311 annotations
• Indexed bean properties are supported in bean class descriptions
• updateObjectWithFilter will update subobjects
Other new stuff
Security
What other REST services uses?
• Twitter and Google: OAuth
• Amazon S3: signature
• Campaign Monitor: Basic Authentication
• MailChimp: API Key
Security
• Basic Authentification
• Sessions
• Tokens
USE SSL!
Basic Auth
Basic Auth
• Pros:
• 99.9% of HTTP clients can work with it
• Easy to implement
• Cons:
• It's just a Base64 representation of your credentials!
• No logout option (must close the browser)
• No styling of the user/pass box
Implementing Basic Auth
protected void initAuthentication() throws MemberException, NotAuthorizedException { String authValue = request().headerForKey( "authorization" ); if( authValue != null ) { try { byte[] authBytes = new BASE64Decoder().decodeBuffer( authValue.replace( "Basic ", "" ) ); String[] parts = new String( authBytes ).split( ":", 2 ); String username = parts[0]; String password = parts[1]; setAuthenticatedUser(Member.validateLogin(editingContext(), username, password)); } catch ( IOException e ) { log.error( "Could not decode basic auth data: " + e.getMessage() ); e.printStackTrace(); } } else { throw new NotAuthorizedException(); } } public class NotAuthorizedException extends Exception { public NotAuthorizedException() { super(); } }
Implementing Basic Auth
@Override public WOActionResults performActionNamed(String actionName, boolean throwExceptions) { // This is if you don't want to use Basic Auth for HTML apps if (!(ERXRestFormat.html().name().equals(this.format().name()))) { try { initAuthentication(); } catch (UserLoginException ex) { WOResponse response = (WOResponse)errorResponse(401); response.setHeader("Basic realm=\"ERBlog\"", "WWW-Authenticate"); return response; } catch (NotAuthorizedException ex) { WOResponse response = (WOResponse)errorResponse(401); response.setHeader("Basic realm=\"ERBlog\"", "WWW-Authenticate"); return response; } } return super.performActionNamed(actionName, throwExceptions); }
Sessions• Pros:
• Can store other data on the server-side (but REST is suppose to be stateless)
• Easy to implement
• Cons:
• Timeouts...
• Sessions are bind to a specific instance of the app
• State on the server
• Non-browser clients have to store the session ID
Login with a session
curl -X GET http://127.0.0.1/cgi-bin/WebObjects/App.woa/ra/users/login.json?username=auser&password=md5pass public Session() { setStoresIDsInCookies(true); }
public WOActionResults loginAction() throws Throwable { try { String username = request().stringFormValueForKey("username"); String password = request().stringFormValueForKey("password"); Member member = Member.validateLogin(session().defaultEditingContext(), username, password); return response(member, ERXKeyFilter.filterWithNone()); } catch (MemberException ex) { return errorResponse(401); } }
(This only works on a version of ERRest after June 9 2011)
Login with a session
protected void initAuthentication() throws MemberException, NotAuthorizedException { if (context().hasSession()) { Session session = (Session)context()._session(); if (session.member() == null) { throw new NotAuthorizedException(); } } else { throw new NotAuthorizedException(); } }
@Override public WOActionResults performActionNamed(String actionName, boolean throwExceptions) { try { initAuthentication(); } catch (MemberException ex) { return pageWithName(Login.class); } catch (NotAuthorizedException ex) { return pageWithName(Login.class); } return super.performActionNamed(actionName, throwExceptions); }
Tokens
• Pros:
• No timeout based on inactivity (unless you want to)
• Cons:
• More work involved
• Client must request a token
• Can store the token in a cookie, Authorization header or as a query argument
Login with a token
curl -X GET http://127.0.0.1/cgi-bin/WebObjects/App.woa/ra/members/login.json?username=auser&password=md5pass
public static final ERXBlowfishCrypter crypter = new ERXBlowfishCrypter();
public WOActionResults loginAction() throws Throwable { try { String username = request().stringFormValueForKey("username"); String password = request().stringFormValueForKey("password"); Member member = Member.validateLogin(editingContext(), username, password); String hash = crypter.encrypt(member.username()); if (hash != null) { return response(hash, ERXKeyFilter.filterWithAll()); } } catch (MemberException ex) { return errorResponse(401); } }
Login with a token
public static final ERXBlowfishCrypter crypter = new ERXBlowfishCrypter();
protected void initTokenAuthentication() throws MemberException, NotAuthorizedException { String tokenValue = this.request().cookieValueForKey("someCookieKeyForToken"); if (tokenValue != null) { String username = crypter.decrypt(tokenValue); Member member = Member.fetchMember(editingContext(), Member.USERNAME.eq(username)); if (member == null) { throw new NotAuthorizedException(); } } else { throw new NotAuthorizedException(); } }
@Override public WOActionResults performActionNamed(String actionName, boolean throwExceptions) { try { initTokenAuthentication(); } catch (MemberException ex) { return pageWithName(Login.class); } catch (NotAuthorizedException ex) { return pageWithName(Login.class); } return super.performActionNamed(actionName, throwExceptions); }
Browser vs System-to-System
It near impossible to have a REST backend with security that works well with both browsers-based and "system-to-system" applications.
• For browser apps: use cookies
• For system-to-system: use the Authorization header
Handling HTML and routes auth @Override
protected WOActionResults performHtmlActionNamed(String actionName) throws Exception { try { initCookieAuthentication(); } catch (MemberException ex) { return pageWithName(LoginPage.class); } catch (NotAuthorizedException ex) { return pageWithName(LoginPage.class); } return super.performHtmlActionNamed(actionName); } @Override protected WOActionResults performRouteActionNamed(String actionName) throws Exception { try { initTokenAuthentication(); } catch (MemberException ex) { return errorResponse(401); } catch (NotAuthorizedException ex) { return errorResponse(401); } return super.performRouteActionNamed(actionName); }
Other options
• OAuth
• Custom HTTP Authentication scheme
• Digest Authentification
• OpenID
• API Key (similar to token)
Versioning
Versioning
• Try hard to not having to version your REST services...
• ... but life is never as planified
• Use mod_rewrite and ERXApplication._rewriteURL to make it easier
• Use mod_rewrite even if you are not versionning! It makes shorter and nicer URLs
VersioningIn Apache config:
RewriteEngine On RewriteRule ^/your-service/v1/(.*)$ /cgi-bin/WebObjects/YourApp-v1.woa/ra$1 [PT,L] RewriteRule ^/your-service/v2/(.*)$ /cgi-bin/WebObjects/YourApp-v2.woa/ra$1 [PT,L]
In Application.java:
public String _rewriteURL(String url) { String processedURL = url; if (url != null && _replaceApplicationPathPattern != null && _replaceApplicationPathReplace != null) { processedURL = processedURL.replaceFirst(_replaceApplicationPathPattern, _replaceApplicationPathReplace); } return processedURL; }
In the Properties of YourApp-v1.woa:
er.extensions.ERXApplication.replaceApplicationPath.pattern=/cgi-bin/WebObjects/YourApp-v1.woa/ra er.extensions.ERXApplication.replaceApplicationPath.replace=/your-service/v1/
In the Properties of YourApp-v2.woa:
er.extensions.ERXApplication.replaceApplicationPath.pattern=/cgi-bin/WebObjects/YourApp-v2.woa/ra er.extensions.ERXApplication.replaceApplicationPath.replace=/your-service/v2/
Versioning: the gotcha
Watch out for schema changes or other changes that can break old versions if all versions use the same database schema!
HTML routing
HTML routing?
• Power of ERRest + WO/EOF + clean URLs!
• Like DirectActions, but with a lot of work done for you
• Useful for small public apps that can be cacheable (or accessible offline)
Automatic routing: damn easy
• Create a REST controller for your entity and set isAutomaticHtmlRoutingEnabled() to true
• Create a <EntityName><Action>Page (eg, MemberIndexPage.wo) component
• Register your controller
• Your component must implements IERXRouteComponent
• Run your app
• Profits!
Passing data to the component
Use the ERXRouteParameter annotation to tag methods to receive data:
@ERXRouteParameter
public void setMember(Member member) { this.member = member; }
Automatic HTML routing
If the <EntityName><Action>Page component is not found, it will default back to the controller and try to execute the requested method.
HTML routing gotchas
• When submitting forms, you're back to the stateful request handler
• ERXRouteUrlUtils doesn't create rewritten URLs
Manual HTML routing
That's easy: same as a DirectAction:
public WOActionResults indexAction() throws Throwable {
return pageWithName(Main.class); }
100% RESTpublic Application() { ERXRouteRequestHandler restRequestHandler = new ERXRouteRequestHandler(); requestHandler.insertRoute(new ERXRoute("Main","", MainController.class, "index"));... setDefaultRequestHandler(requestHandler);}
public class MainController extends BaseController { public MainController(WORequest request) { super(request); } @Override protected boolean isAutomaticHtmlRoutingEnabled() { return true; }
@Override public WOActionResults indexAction() throws Throwable { return pageWithName(Main.class); } @Override protected ERXRestFormat defaultFormat() { return ERXRestFormat.html(); }...
HTML routing: demo
Cool trick: Application Cache Manifest
• Let you specify that some URLs of your app can be available offline
• URLs in the CACHE section will be available offline until you change the manifest and remove the URLs from the CACHE section
• Use a DirectAction or a static file to create the manifest
• One cool reason to use the HTML routing stuff
Cache ManifestIn your DirectAction class:
public WOActionResults manifestAction() { EOEditingContext ec = ERXEC.newEditingContext(); WOResponse response = new WOResponse(); response.appendContentString("CACHE MANIFEST\n"); response.appendContentString("CACHE:\n"); response.appendContentString(ERXRouteUrlUtils.actionUrlForEntityType(this.context(), Entity.ENTITY_NAME, "index", ERXRestFormat.HTML_KEY, null, false, false) + "\n"); response.appendContentString("NETWORK:\n"); response.setHeader("text/cache-manifest", "Content-Type"); return response; }
In your component:
<wo:WOGenericContainer elementName="html" manifest=$urlToManifest" lang="en" xmlns="http://www.w3.org/1999/xhtml">
public String urlForManifest() { return this.context().directActionURLForActionNamed("manifest", null); }
Debugging
Debugging REST problems
• curl -v : will display all headers and content, for both request and response
• Firebug and WebKit Inspector : useful to see the XMLHttpRequest calls
• tcpflow : see all trafic on a network interface, can do filters
• Apache DUMPIO (mod_dumpio) : dump ALL requests and responses data to Apache's error log
Debugging Demo
Q&A
MONTREAL 1/3 JULY 2011