=encoding utf8 =head1 NAME OpenResty::Spec::Overview - Overview of the OpenResty service platform =head1 INTRODUCTION OpenResty is a general-purpose RESTful web service platform for web applications. It provides the following important functionalities for a common nontrivial web app: =over =item * (scalable) relational data storage, =item * truely RESTy interface and JSON/YAML data transfer format, =item * SQL-based reusable views, =item * a REST-oriented role system for access control, =item * view-based RSS feeds, =item * user-defined actions in the RestyScript language, =item * captchas, =item * and cross-site AJAX support. =back =head2 What OpenResty is =over =item * A REST wrapper for relational databases =item * A web runtime for 100% JavaScript web sites and other RIAs. =item * A "meta web site" supporting other sites via web services. =item * A handy personal or company database which can be accessed from anywhere on the web. =item * A (sort of) competitor for the Facebook Data Store API. =back =head2 What OpenResty is NOT =over =item * A server-side web application framework. =item * A relacement for highly scalable semi-structured data storage solutions like Amazon SimpleDB or CouchDB. =back =head1 REST API This section just gives a conceptual overview for the REST API probably with some samples. For detailed spec for the various REST request syntax, see L and L. =head2 Accounts An openresty server typically distributes its data in terms of accounts, especially when the backend is a database cluster. An account is an atomic namespace for other OpenResty first-class objects like models and views. (In the current Pg and PgFarm backends, accounts are actually implemented by Pg schemas.) These objects are shared in the same account and different accounts can have different models, views, actions, and etc. with the same names. Operations like creating and removing accounts are not part of the OpenResty web service API. Basically the sysadmin uses the following command to create an account on his server terminal: $ bin/openresty adduser marry and a similar command to remove one: $ bin/openresty deluser marry =head2 Roles Multiple users can share the same set of objects in an account by logging in as different roles. And fine-grained access control can be achieved by specifying different sets of ACL rules for each role. Every OpenResty account has two builtin roles throughout its lifetime: C and C. The C role always owns the most privileges and its properties and ACL rule set are always read only. C role is always anonymous but its ACL rule set can be modified by a role with enough privileges. An OpenResty role with access to the Role API (such as C) can create new roles, remove existing roles (except the two builtin roles explained above, of course), and modify the properties and ACL rules of other roles or even itself. For instance, to allow the C role to perform the request C<< GET /=/model/Post/id/ >> under the same account, the C role could insert a corresponding access rule to the C role's ACL rule set, like this: POST /=/role/Public/~/~ HTTP/1.0 Content-Type: text/json Content-Length: 45 {"method":"GET", "url":"/=/model/Post/id/~"} The JSON structure in the POST content specifies an ACL rule. The tild (C<~>) character in the C value serves as a wildcard which matches "anything". So both C and C are allowed to be performed by the C role. Interestingly it's also possible to grant the C role privileges to augment its own ACL rule set in a similar way: POST /=/role/Public/~/~ HTTP/1.0 Content-Type: text/json Content-Length: 46 {"method":"POST", "url":"/=/role/Public/~/~"} =head1 Login Every user accessing an OpenResty server must specify both its account name and its role name unless he or she has already logged in and got a session ID. For example, a typical HTTP request may look like this: GET /=/model/Post/id/3?_user=agentzh.Public HTTP/1.0 In the above example, the C<_user> parameter has the value C where C is the account name and C the role name. In addition, the C role is an anonymous role, or a C<_password> or a C<_captcha> parameter would be required here as well. This authentication method is called "per-request login". Alternatively, the user can login with his user name and MD5'd password first so as to obtain a session ID which can be used for subsequence requests. For example: GET /=/login/agentzh.Admin/5f4dcc3b5aa765d61d8327deb882cf99 HTTP/1.0 will yield an HTTP response from the OpenResty server like this: HTTP/1.0 200 OK Connection: close Content-Type: text/json; charset=UTF-8 Content-Length: 133 Date: Mon, 21 Apr 2008 11:51:49 GMT { "success": 1, "session": "535F265E-0F99-11DD-B185-1A3EB9E8D9B0", "account": "agentzh","role":"Admin" } And subsequent requests can be made by using the resulting session ID: GET /=/model/Post/id/3?_session=535F265E-0F99-11DD-B185-1A3EB9E8D9B0 For convenience, the sample HTTP requests given throughout this document will not specify the C<_user> nor the C<_session> parameter explicitly. It's worth mentioning that the simple MD5 treatment of passwords in the current implementation is merely a hack and will be changed in the near future. It's highly recommended to use SSL for the password login method for any serious uses. =head2 Models An OpenResty model is just an abstract concept of tables found in common relational databases. An instance of an OpenResty model could be a blog post: { "description":"Blog post", "columns": [ { "name":"title", "label":"Post title", "type":"text" }, { "name":"content", "label":"Post content", "type":"text" }, { "name":"author", "label":"Post author", "type":"text" }, { "name":"created", "default":["now()"], "type":"timestamp (0) with time zone", "label":"Post creation time" }, { "name":"comments", "label":"Number of comments", "type":"integer", "default":0 } ], } This is approximately the C model used in my personal blog site L. The rough SQL equivalence could be as follows: create table "Post" ( title text, content text, author text, created timestamp (0) with time zone default now(), comments integer default 0 ) Although the data storage backend may be truly implmented this way, the column types and names that can be used here are well defined and reasonably limited. After creating a model, one can insert data via an HTTP POST request: POST /=/model/Post/~/~ HTTP/1.0 Content-Type: text/json Content-Length: 111 { "title":"My first post", "content":"Blah blah blah...", "author":"Agent Zhang" } Multiple rows can be inserted at a time as well, but there's a limit. The model API not only offers interfaces to perform CRUD operations on models, columns, and rows, but also gives some simple but still powerful query functionalities. Here's an example: GET /=/model/Post/author/agentzh?_order_by=created:desc&_count=10 HTTP/1.0 which is roughly equivalent to the following standard SQL query: select * from "Post" where "author" = 'agentzh' order by created desc count 10 =head2 Views To address the problem of extending the limited data query interface provided by the model API, OpenResty integrates a view system in which the user can define reusable SQL-like queries by means of the RestyScript language. Here is an example: POST /=/view/~ HTTP/1.0 Content-Type: text/json Content-Length: 312 { "name": "CommentsToAuthor", "description": "Recent comments for the blog", "definition": " select Comment.sender as guest, Comment.body as comment_body from Comment, Post where Comment.id = Post.id and Post.author = $author" } In this sample, the string literal for the C slot has been splitted into multiple lines for readability. The RestyScript language for views is just a (non-strict) subset of the standard SQL language, thus giving powerful strucutred query capability to the user, which is often a missing feature in those highly-distributed and semi-structured data storage solutions like CouchDB and SimpleDB. Unlike SQL, however, the view definition can take one or more parameters (or named place-holders) which are required to feed values while invoking the view (unless they have a default value): GET /=/view/CommentsToAuthor/author/agentzh Or equivalently GET /=/view/CommentsToAuthor/~/~?author=agentzh The HTTP response from the OpenResty server might be HTTP/1.0 200 OK Connection: close Content-Type: text/json; charset=UTF-8 Content-Length: 187 Date: Mon, 21 Apr 2008 12:42:15 GMT [ {"guest":"laser","comment_body":"super cool!"}, {"guest":"carriezh","comment_body":"hello?hello?"}, {"guest":"agentzh":"comment_body":"Thanks for commenting!"} ] =head2 Feeds OpenResty offers the feed objects which can be used to map OpenResty views to RSS 2.0 feeds. For instance, the OpenResty feed object for my blog posts looks like this: { "description": "Feed for blog posts", "author": "agentzh", "copyright": "Copyright 2008 by Yahoo! China EEEE Works", "language": "zh-cn", "title": "Posts for Human & Machine", "link": "http://blog.agentzh.org", "logo": "http://blog.agentzh.org/me.jpg", "view": "PostFeed" } and the C view used to generate this feed has the following definition: { "description":"View for post feed", "definition": " select author, title, 'http://blog.agentzh.org/#post-' || id as link, content, created as published, 'http://blog.agentzh.org/#post-' || id || ':comments' as comments from Post order by created desc limit $count | 20 " } Here the C view takes one optional parameter C<$count> (with the default value C<20>), which controls the number of resulting rows returned. Not every view can be used to drive feed generation. The resulting rows of the view must have the columns that make sense to the feed, like C, C, C<link>, C<content>, C<published>, and C<comments>. After creating the C<Post> feed in my C<agentzh> account, one can subscribe to the feed by the following GET request: GET /=/feed/Post/~/~ HTTP/1.0 Check L<http://api.eeeeworks.org/=/feed/Post/~/~> to see what the actual response looks like. One nice thing about the feed object is that it can forward arguments to the view that drives it: GET /=/feed/Post/count/100 HTTP/1.0 This will produce the RSS 2.0 feed for the last 100 post entries rather than the default 20, giving more options to my blog readers. =head2 Actions An openresty action is a bunch of RestyScript commands with a name attached to it. Such a command can be either a SQL-like statement or an HTTP-like command. An example for SQL-like commands could be update Post set comments = comments + 1 where id = $post_id In this C<update> command, C<Post> is the name of an OpenResty model (assuming it's already there), C<comments> is one of its columns, and C<$post_id> is a parameter for the whole action (similar to parameters for views). An instance of HTTP-like commands could be POST '/=/model/Comment/~/~' { "sender": $sender, "body": $body, "post": $post_id } Here the C<http://blah.blah.blah> part is omitted in the POST URL, so it's default to the current OpenResty server being requested. If a full URL is specified here, one can do some really cool things by invoking the resources of some other OpenResty server. Similarly, for the SQL-like command such as: delete from Comment where post = $post_id and sender = $spammer [TODO: a optional C<run on> clause might be specified to run "SQL" on remote OpenResty servers if permitted.] Furthermore, we can put multiple RestyScript commands together using the C<;> separator to define a full action object: { "name": "PostComment", "description": "Action for posting a comment", "parameters":[ {"name":"post_id","label":"Post ID","type":"literal"}, {"name":"sender","label":"Sender","type":"literal","default":"agentzh"}, {"name":"body","label":"Body","type":"literal"} ], "definition": " update Post set comments = comments + 1 where id = $post_id; POST '/=/model/Comment/~/~' { "sender": $sender, "body": $body, "post": $post_id } " } We still split the definition string into multiple lines for readability. The C<PostComment> action defined here takes 3 parameters, i.e. C<$post_id>, C<$sender>, and C<$body>. One can invoke the C<PostComment> action like this: GET /=/action/PostComment/~/~?post_id=3&sender=marry&body=Haha HTTP/1.0 The server response would be an array of results for each command. If any of the commands fails, the whole action would fail. Even preious successfully executed commands would get rolled back. That is, actions always run in a transaction. With actions the user can encapsulate multiple OpenResty REST requests as well as SQL-like C<update> and C<delete> statements as a whole and reuse as many times as he wishes. Such atomicity would be very useful in the context of captcha authentication. (See the L</Captchas> section for more information.) More interestingly it would be possilbe to call other actions or views in an action, or even call the action itself (i.e. recursive calling). Special constraints would be imposed on the length of the action call chain though. There would also be some limit regarding the total number of commands grouped in an action. =head2 Captchas Captchas are just another way to do "per-request login" in addition to the C<anonymous> and C<password> login methods. Captcha support must be associated with some user-defined role whose "login" attribute is set to the value "C<captcha>", like this: POST /=/role/CommentPoster HTTP/1.0 Content-Type: text/json Content-Length: 64 {"description":"Role for posting comments","login":"captcha"} Therefore, it's not hard to see that it's not possible to do captchas with roles like C<Public> or C<Admin>. Then we should grant priviledges to the operations that need to performed by solving a capthca challenge for the C<CommentPoster> role: POST /=/role/CommentPoster/~/~ HTTP/1.0 Content-Type: text/json Content-Length: 48 {"method":"POST","url":"/=/model/Comment/~/~"} Then the clients (like the JavaScript code in a web page) could do the following: =over =item 1. Use C<GET /=/captcha/id> to obtain a captcha ID from the OpenResty server. =item 2. Use the captcha ID, say, C<B44572D0-1038-11DD-B185-1A3EB9E8D9B0>, to fetch the catpcha image: GET /=/captcha/id/B44572D0-1038-11DD-B185-1A3EB9E8D9B0 HTTP/1.0 =item 3. With the captcha ID along with the captcha solution given by the I<end user> the following operation can be tried: POST /=/model/Comment/~/~?_user=agentzh.CommentPoster\ &_captcha=B44572D0-1038-11DD-B185-1A3EB9E8D9B0:pretty%20cat \ HTTP/1.0 Content-Type: text/json Content-Length: 52 {"sender":"agentzh","body":"Good post!","post":25} =back If the user solution C<pretty cat> (i.e. "C<pretty%20cat>") provided in the C<captcha> URL parameter is incorrect, the server would reject the whole POST operation. =head2 Cross-site AJAX The OpenResty server opens a special door to the JavaScript code in a web page coming from other domains, so as to allow REST requests get directly initiated from the I<end> user's web browser. For GET requests, it's the common practice to do cross-domain AJAX requests via dynamically created C<< <script> >> tags. To help the page owner do this trick with an OpenResty server, the special C<_callback> URL parameter is supported to make the server returning JSON data wrapped by C<some_callback_func(> and C<);>. For example, the request GET /=/view/RecentPosts/~/~?_user=agentzh.Public&_callback=foo HTTP/1.0 yields something like this HTTP/1.0 200 OK Connection: close Content-Type: application/x-javascript; charset=UTF-8 Content-Length: 74 Date: Mon, 21 Apr 2008 12:42:15 GMT foo( [ {"title":"My first post","id":1}, {"title":"My second one","id":2} ] ); Note the extra stuff C<foo(...)> around the JSON data. POST, PUT, and DELETE requests all have their GET variations: GET /=/post/... GET /=/put/... GET /=/delete/... where C<...> is the normal stuff in C<POST /=/...>, C<PUT /=/...>, and C<DELETE /=/...>, respectively. Some people might be nervous about GET requests doing data modification but I can't think of a better way. Cookies for authentication should always be excluded due to the risk of XSS attacks. To overcome the length limit of URLs, cross-site POST interface is also supported. Basically the user could use an HTML form in his web page like below: <form action="/=/model/Comment/~/~?_last_response=69bc45ec71ca7dc83cc" method="POST" onclick="onPostComment()" target="myHiddenFrame"> <input type="hidden" name="data" value="{some JSON goes here...}"> </form> <iframe id="myHiddenFrame" style="display: none"></iframe> and the browser may initiate a POST request when submitting this form: POST /=/model/Comment/~/~?_last_response=69bc45ec71ca7dc83cc HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 23 data={some JSON goes here...} which is functionally equivalent to POST /=/model/Comment/~/~ Content-Type: text/json Content-Length: 23 {some JSON goes here...} with the exception that the OpenResty serser would save the response of the current POST request and allow the user to retrieve it later (using the same C<_last_response> key): GET /=/last/response/69bc45ec71ca7dc83cc Note that the C<_last_response> key C<69bc45ec71ca7dc83cc> used here should be randomly selected and globally unique enough. The "last response" stuff is essential for the web page client to obtain the response of its POST request because there's no (known) way for the JavaScript code to directly "look" into the target frame (i.e. the C<myHiddenFrame> iframe in the above sample) belonging to the OpenResty server's domain. As you might have already noticed, two HTTP round-trips are required to do a true POST, which is a bit expensive. We'll use the cross-site cookies (as well as p3p headers for IE) to deliver the response of POSTs to the JavaScript initiator. =head1 CLIENTS In theory, any programming languages or tools with basic HTTP 1.0/1.1 support would have access to 100% of the OpenResty services. But to make things even easier, there are currently two ad-hoc OpenResty client library for JavaScript and Perl: =over =item openresty.js See L<http://svn.openfoundry.org/openapi/trunk/clients/js/>. =item WWW::OpenResty See the L<WWW::OpenResty> module on CPAN. Most of the time one would just use its subclass L<WWW::OpenResty::Simple> which is much more handy IMHO ;) =back =head1 SAMPLE APPLICATIONS =over =item OpenResty's admin site L<http://openresty.org/admin/> =item agentzh's blog and EEEE Works' blog L<http://blog.agentzh.org> L<http://eeeeworks.org> =item Yisou BBS L<http://www.yisou.com/opi/post.html> =item An IRC bot, springbot L<http://svn.openfoundry.org/openapi/trunk/demo/Springbot/> =back Most of the sample apps' source code can be found at L<http://svn.openfoundry.org/openapi/trunk/demo/>. =head1 AUTHOR Agent Zhang C<< <agentzh@yahoo.cn> >> =head1 COPYRIGHT AND LICENSE Copyright (c) 2008 Yahoo! China EEEE Works, Alibaba Inc. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license can be found at http://www.gnu.org/licenses/fdl.html =head1 SEE ALSO L<OpenResty::Spec::REST_cn>, L<OpenResty>, L<OpenResty::CheatSheet>.