Skip to content
Browse files

init import

  • Loading branch information...
0 parents commit 07a79f2bcf7ce39048f4314a2996e95dd482243b @kilianc committed
Showing with 405 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +22 −0 LICENSE
  3. +121 −0 README.md
  4. +44 −0 lib/apimodule.js
  5. +213 −0 lib/apiserver.js
  6. +4 −0 package.json
1 .gitignore
@@ -0,0 +1 @@
+bump.js
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2011 Kilian Ciuffolo, me@nailik.org
+
+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.
121 README.md
@@ -0,0 +1,121 @@
+# node-apiserver ![project status](http://dl.dropbox.com/u/2208502/maintained.png)
+
+A ready to go modular http API Server.
+
+#transports
+
+- JSON
+- JSONP,
+- JSONP+iFrame _(you can make cross-domain POST inside the same parent domain: 's1.example.com, s2.example.com, ...')_
+
+## Dependencies
+
+- nodejs v0.4.11+
+- http _native module_
+- querystring _native module_
+- node-bufferjoiner [repository](https://github.com/kilianc/node-bufferjoiner)
+
+## Installation and first run
+
+ $ git clone git://github.com/kilianc/node-apiserver.git
+ $ cd node-apiserver
+ $ node example.js
+
+## Usage
+
+ var util = require('util'),
+ querystring = require('querystring'),
+ ApiServer = require('node-apiserver'),
+ ApiModule = require('node-apiserver').ApiModule;
+
+ ...
+
+ var UserModule = function(userCollection, myotherObj, myCustomConfig) {
+ ...
+ this.userCollection = userCollection;
+ ApiModule.call(this);
+ };
+
+ util.inherits(UserModule, ApiModule);
+
+ // this is a private method, it means that you must send a right Authorization header within your request.
+ // your method can be reached to http://yourserver.com/user/my_first_camel_case_method
+ UserModule.prototype.myFirstCamelCaseMethodPrivate = function(request, response) {
+
+ var self = this;
+ var postData = this.parsePost(request); //inherited from ApiModule.
+
+ if(!/^([a-z0-9_\.\-])+\@(([a-z0-9\-])+\.)+([a-z0-9]{2,4})+$/.test(postData.email)){
+ self.emit('responseReady', request, response, 200, null, { success: false, reason: 'email not valid' });
+ return this;
+ }
+
+ this.userCollection.insert({ email: postData.email }, { safe: true }, function(err, user) {
+
+ if(err){
+ self.emit('responseReady', request, response, 200, 'this is the http status message', { success: false, reason: 'email not unique' });
+ return this;
+ }
+
+ self.emit('responseReady', request, response, 200, 'this is the http status message', { email: postData.email, success: true });
+ });
+ };
+
+ // this is a public method
+ // the server will check for the [Private|Public] word at the end of the method name.
+ // your method can be reached to http://yourserver.com/user/my_second_camel_case_method
+ UserModule.prototype.mySecondCamelCaseMethodPublic = function(request, response) {
+
+ var self = this;
+ var postData = this.parsePost(request);
+
+ if(!/^([a-z0-9_\.\-])+\@(([a-z0-9\-])+\.)+([a-z0-9]{2,4})+$/.test(postData.email)){
+ self.emit('responseReady', request, response, 200, null, { success: false, reason: 'email not valid' });
+ return this;
+ }
+
+ this.userCollection.insert({ email: postData.email }, { safe: true }, function(err, user) {
+
+ if(err){
+ self.emit('responseReady', request, response, 200, 'this is the http status message', { success: false, reason: 'email not unique' });
+ return this;
+ }
+
+ self.emit('responseReady', request, response, 200, 'this is the http status message', { email: postData.email, success: true });
+ });
+ };
+
+ ...
+
+ var apiServer = new ApiServer('mydomain.com', standardHeaders, "username:password");
+ apiServer.addModule('user', new UserModule(new mongodb.Collection(mongodbClient, 'users'), { foo: 'bar' }, { cool: 'sure' }));
+ apiServer.listen(80);
+
+ ...
+
+## License
+
+_This software is released under the MIT license cited below_.
+
+ Copyright (c) 2010 Kilian Ciuffolo, me@nailik.org. All Rights Reserved.
+
+ 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.
44 lib/apimodule.js
@@ -0,0 +1,44 @@
+var util = require('util'),
+ events = require('events'),
+ querystring = require('querystring');
+
+var ApiModule = function(mongodbClient, collections) {
+
+ this.mongodbClient = mongodbClient;
+ this.collections = collections;
+
+ events.EventEmitter.call(this);
+};
+
+util.inherits(ApiModule, events.EventEmitter);
+
+ApiModule.prototype.parseAuthorization = function(request, response){
+
+ var authorization;
+
+ if(!request.headers.authorization) {
+ this.emit('responseReady', request, response, 401, 'Authorization Required', { error: { type: 'ApiException', message: 'missing authorization' } }, { 'www-authenticate': 'Basic realm=\'Please Authenticate\''});
+ return false;
+ }
+
+ authorization = new Buffer(request.headers.authorization.replace('Basic ', ''), 'base64').toString('utf8');
+ authorization = authorization.split(':');
+
+ return { email: authorization[0], password: authorization[1] };
+};
+
+ApiModule.prototype.parsePost = function(request){
+
+ var postData;
+
+ try{
+ postData = querystring.parse(request.postData.toString('utf8'));
+ }
+ catch(e){
+ postData = {};
+ }
+
+ return postData;
+};
+
+module.exports = ApiModule;
213 lib/apiserver.js
@@ -0,0 +1,213 @@
+var http = require('http'),
+ url = require('url'),
+ querystring = require('querystring'),
+ BufferJoiner = require('node-bufferjoiner'),
+ ApiModule = require('./apimodule');
+
+var ApiServer = function(domain, standardHeaders, credentials) {
+
+ this.credentials = 'Basic ' + new Buffer(credentials, 'utf8').toString('base64');
+
+ this.iframeHtmlTemplate = "<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'><html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'/><title></title><script type='text/javascript' charset='utf-8'>document.domain = '" + domain + "';parent.[CALLBACK]([DATA]);</script></head><body></body></html>";
+ this.activeApiModules = {};
+ this.standardHeaders = standardHeaders || {
+ 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate',
+ 'expires': 0,
+ 'pragma': 'no-cache'
+ };
+
+ this.activeApiModules = {};
+ this.server = http.createServer(this.onRequest.bind(this));
+};
+
+ApiServer.VERSION = 'v0.0.1';
+ApiServer.ApiModule = ApiModule;
+
+ApiServer.prototype.addModule = function(moduleName, apiModule){
+
+ apiModule.on('responseReady', this.serveResponse.bind(this, apiModule));
+ apiModule.on('responseStreamStart', this.streamResponseStart.bind(this, apiModule));
+ apiModule.on('responseStreamData', this.streamResponseData.bind(this, apiModule));
+ apiModule.on('responseStreamEnd', this.streamResponseEnd.bind(this, apiModule));
+
+ this.activeApiModules[moduleName] = apiModule;
+};
+
+ApiServer.prototype.listen = function(port){
+ this.server.listen(port);
+};
+
+ApiServer.prototype.onRequest = function(request, response) {
+
+ var self = this;
+ var parsedUrl = url.parse(request.url, true);
+
+ request.pathname = parsedUrl.pathname.split('/');
+ request.querystring = parsedUrl.query;
+ request.postData = new BufferJoiner();
+
+ request.apiModule = request.pathname[1] || '';
+ request.apiAction = request.pathname[2] || '';
+
+ request.apiModule = request.apiModule.replace(/(\_[a-z])/g, function(match){ return match.toUpperCase()[1]; });
+ request.apiAction = request.apiAction.replace(/(\_[a-z])/g, function(match){ return match.toUpperCase()[1]; });
+
+ if(!this.checkApiExists(request)) {
+ this.onApiNotFound(request, response);
+ return;
+ }
+
+ if(!this.authorizedApi(request, response))
+ return;
+
+ request.connection.setTimeout(15000);
+
+ if(request.method == 'GET'){
+ request.postData = request.postData.join();
+ this.onRequestComplete(request, response);
+ }
+ else {
+ request.on('data', function(chunk){
+ request.postData.add(chunk);
+ });
+
+ request.once('end', function(){
+ request.postData = request.postData.join();
+ self.onRequestComplete(request, response);
+ });
+ }
+};
+
+ApiServer.prototype.checkApiExists = function(request) {
+
+ return (this.activeApiModules[request.apiModule] && (this.activeApiModules[request.apiModule][request.apiAction + 'Public'] || this.activeApiModules[request.apiModule][request.apiAction + 'Private']));
+};
+
+ApiServer.prototype.onApiNotFound = function(request, response) {
+
+ this.serveResponse(this, request, response, 500, 'errore', {
+ error: {
+ type: 'ApiException',
+ message: request.url + ' api not found'
+ }
+ });
+}
+
+ApiServer.prototype.authorizedApi = function(request, response) {
+
+ if(!this.activeApiModules[request.apiModule][request.apiAction + 'Private']) {
+
+ request.apiAction += 'Public';
+ return true;
+ }
+
+ if(!request.headers.authorization || request.headers.authorization != this.credentials) {
+
+ response.writeHead(401, 'Authorization Required', { 'www-authenticate': 'Basic realm=\'Please Authenticate\''});
+ response.end();
+
+ return false;
+ }
+
+ request.apiAction += 'Private'
+
+ return true;
+};
+
+ApiServer.prototype.onRequestComplete = function(request, response) {
+
+ try {
+ this.activeApiModules[request.apiModule][request.apiAction](request, response);
+ } catch (err) {
+
+ throw err;
+
+ this.serveResponse(this, request, response, 500, 'errore', {
+ error: {
+ type: 'ApiException',
+ message: 'something went wrong: ' + request.url
+ }
+ });
+ }
+};
+
+ApiServer.prototype.serveResponse = function(sender, request, response, httpStatusCode, httpStatusMessage, responseContent, responseHeaders) {
+
+ if(request.querystring.callback) {
+
+ if(request.method == 'POST') {
+
+ contentType = 'text/html';
+ responseContent = this.iframeHtmlTemplate.replace('[CALLBACK]', request.querystring.callback).replace('[DATA]', JSON.stringify(responseContent));
+ }
+ else {
+ contentType = 'text/javascript';
+ responseContent = request.querystring.callback + '(' + JSON.stringify(responseContent, null, ' ') + ');';
+ }
+ }
+ else {
+ contentType = 'text/javascript';
+ responseContent = JSON.stringify(responseContent, null, ' ');
+ }
+
+ responseHeaders = responseHeaders || {};
+
+ for(var headerKey in this.standardHeaders)
+ (responseHeaders[headerKey] === undefined) && (responseHeaders[headerKey] = this.standardHeaders[headerKey]);
+
+ responseHeaders['content-type'] = contentType + '; charset=UTF-8';
+ responseHeaders['content-length'] = responseContent.length;
+
+ response.writeHead(httpStatusCode, httpStatusMessage ? httpStatusMessage : '', responseHeaders);
+ response.end(responseContent, 'utf8');
+};
+
+ApiServer.prototype.streamResponseStart = function(sender, request, response, httpStatusCode, httpStatusMessage, responseHeaders) {
+
+ if(request.querystring.callback){
+
+ if(request.method == 'POST'){
+
+ contentType = 'text/html';
+
+ var parts = this.iframeHtmlTemplate.replace('[CALLBACK]', request.querystring.callback).split('[DATA]');
+
+ response.streamStart = parts[0] + '[';
+ response.streamEnd = ']' + parts[1];
+
+ } else {
+
+ contentType = 'text/javascript';
+ response.streamStart = request.querystring.callback + '([';
+ response.streamEnd = ']);';
+ }
+
+ } else {
+
+ contentType = 'text/javascript';
+ response.streamStart = '[';
+ response.streamEnd = ']';
+ }
+
+ responseHeaders = responseHeaders || {};
+
+ for(var headerKey in this.standardHeaders)
+ (responseHeaders[headerKey] === undefined) && (responseHeaders[headerKey] = this.standardHeaders[headerKey]);
+
+ responseHeaders['content-type'] = contentType + '; charset=UTF-8';
+
+ response.writeHead(httpStatusCode, httpStatusMessage ? httpStatusMessage : '', responseHeaders);
+ response.write(response.streamStart, 'utf8');
+
+ delete response.streamStart;
+};
+
+ApiServer.prototype.streamResponseData = function(sender, request, response, responseChunk, isLast) {
+ response.write(JSON.stringify(responseChunk, null, ' ') + (isLast ? '' : ',\n'), 'utf8');
+};
+
+ApiServer.prototype.streamResponseEnd = function(sender, request, response){
+ response.end(response.streamEnd, 'utf8');
+};
+
+module.exports = ApiServer;
4 package.json
@@ -0,0 +1,4 @@
+{
+ "name" : "apiserver",
+ "main" : "./lib/apiserver.js"
+}

0 comments on commit 07a79f2

Please sign in to comment.
Something went wrong with that request. Please try again.