Merge branch 'api' of ../etherpad-lite.dev/

This commit is contained in:
Peter 'Pita' Martischka 2011-08-16 17:22:15 +01:00
commit 1151ec3278
28 changed files with 2172 additions and 190 deletions

3
.gitignore vendored
View file

@ -1,7 +1,8 @@
node_modules
settings.json
static/js/jquery.min.js
APIKEY.txt
bin/abiword.exe
bin/node.exe
etherpad-lite-win.zip
var/dirty.db
var/dirty.db

69
doc/database.md Normal file
View file

@ -0,0 +1,69 @@
# Database structure
## Used so far
### pad:$PADID
Saves all informations about pads
* **atext** - the latest attributed text
* **pool** - the attribute pool
* **head** - the number of the latest revision
* **chatHead** - the number of the latest chat entry
*planed:*
* **public** - flag that disables security for this pad
* **passwordHash** - string that contains a bcrypt hashed password for this pad
### pad:$PADID:revs:$REVNUM
Saves a revision $REVNUM of pad $PADID
* **meta**
* **author** - the autorID of this revision
* **timestamp** - the timestamp of when this revision was created
* **changeset** - the changeset of this revision
### pad:$PADID:chat:$CHATNUM
Saves a chatentry with num $CHATNUM of pad $PADID
* **text** - the text of this chat entry
* **userId** - the autorID of this chat entry
* **time** - the timestamp of this chat entry
### pad2readonly:$PADID
Translates a padID to a readonlyID
### readonly2pad:$READONLYID
Translates a readonlyID to a padID
### token2author:$TOKENID
Translates a token to an authorID
### globalAuthor:$AUTHORID
Information about an author
* **name** - the name of this author as shown in the pad
* **colorID** - the colorID of this author as shown in the pad
## Planed
### mapper2group:$MAPPER
Maps an external application identifier to an internal group
### mapper2author:$MAPPER
Maps an external application identifier to an internal author
### group:$GROUPID
a group of pads
* **pads** - object with pad names in it, values are 1
### session:$SESSIONID
a session between an author and a group
* **groupID** - the groupID the session belongs too
* **authorID** - the authorID the session belongs too
* **validUntil** - the timestamp until this session is valid
### author2sessions:$AUTHORID
saves the sessions of an author
* **sessionsIDs** - object with sessionIDs in it, values are 1
### group2sessions:$GROUPID
* **sessionsIDs** - object with sessionIDs in it, values are 1

432
node/db/API.js Normal file
View file

@ -0,0 +1,432 @@
/**
* This module provides all API functions
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var padManager = require("./PadManager");
var padMessageHandler = require("../handler/PadMessageHandler");
var readOnlyManager = require("./ReadOnlyManager");
var groupManager = require("./GroupManager");
var authorManager = require("./AuthorManager");
var sessionManager = require("./SessionManager");
var async = require("async");
/**********************/
/**GROUP FUNCTIONS*****/
/**********************/
exports.createGroup = groupManager.createGroup;
exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;
exports.deleteGroup = groupManager.deleteGroup;
exports.listPads = groupManager.listPads;
exports.createGroupPad = groupManager.createGroupPad;
/**********************/
/**AUTHOR FUNCTIONS****/
/**********************/
exports.createAuthor = authorManager.createAuthor;
exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
/**********************/
/**SESSION FUNCTIONS***/
/**********************/
exports.createSession = sessionManager.createSession;
exports.deleteSession = sessionManager.deleteSession;
exports.getSessionInfo = sessionManager.getSessionInfo;
exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup;
exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
/************************/
/**PAD CONTENT FUNCTIONS*/
/************************/
/**
getText(padID, [rev]) returns the text of a pad
Example returns:
{code: 0, message:"ok", data: {text:"Welcome Text"}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getText = function(padID, rev, callback)
{
//check if rev is set
if(typeof rev == "function")
{
callback = rev;
rev = undefined;
}
//check if rev is a number
if(rev !== undefined && typeof rev != "number")
{
//try to parse the number
if(!isNaN(parseInt(rev)))
{
rev = parseInt(rev);
}
else
{
callback({stop: "rev is not a number"});
return;
}
}
//ensure this is not a negativ number
if(rev !== undefined && rev < 0)
{
callback({stop: "rev is a negativ number"});
return;
}
//ensure this is not a float value
if(rev !== undefined && !is_int(rev))
{
callback({stop: "rev is a float value"});
return;
}
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(err)
{
callback(err);
return;
}
//the client asked for a special revision
if(rev !== undefined)
{
//check if this is a valid revision
if(rev > pad.getHeadRevisionNumber())
{
callback({stop: "rev is higher than the head revision of the pad"});
return;
}
//get the text of this revision
pad.getInternalRevisionAText(rev, function(err, atext)
{
if(!err)
{
data = {text: atext.text};
}
callback(err, data);
})
}
//the client wants the latest text, lets return it to him
else
{
callback(null, {"text": pad.text()});
}
});
}
/**
setText(padID, text) sets the text of a pad
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null}
*/
exports.setText = function(padID, text, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(err)
{
callback(err);
return;
}
//set the text
pad.setText(text);
//update the clients on the pad
padMessageHandler.updatePadClients(pad, callback);
});
}
/*****************/
/**PAD FUNCTIONS */
/*****************/
/**
getRevisionsCount(padID) returns the number of revisions of this pad
Example returns:
{code: 0, message:"ok", data: {revisions: 56}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getRevisionsCount = function(padID, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(err)
{
callback(err);
return;
}
callback(null, {revisions: pad.getHeadRevisionNumber()});
});
}
/**
createPad(padName [, text]) creates a new pad in this group
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"pad does already exist", data: null}
*/
exports.createPad = function(padID, text, callback)
{
//ensure there is no $ in the padID
if(padID.indexOf("$") != -1)
{
callback({stop: "createPad can't create group pads"});
return;
}
//create pad
getPadSafe(padID, false, text, function(err)
{
callback(err);
});
}
/**
deletePad(padID) deletes a pad
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.deletePad = function(padID, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(err)
{
callback(err);
return;
}
});
}
/**
getReadOnlyLink(padID) returns the read only link of a pad
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getReadOnlyID = function(padID, callback)
{
//we don't need the pad object, but this function does all the security stuff for us
getPadSafe(padID, true, function(err)
{
if(err)
{
callback(err);
return;
}
//get the readonlyId
readOnlyManager.getReadOnlyId(padID, function(err, readOnlyId)
{
callback(err, {readOnlyID: readOnlyId});
});
});
}
/**
setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.setPublicStatus = function(padID, publicStatus, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(err)
{
callback(err);
return;
}
//convert string to boolean
if(typeof publicStatus == "string")
publicStatus = publicStatus == "true" ? true : false;
//set the password
pad.setPublicStatus(publicStatus);
callback();
});
}
/**
getPublicStatus(padID) return true of false
Example returns:
{code: 0, message:"ok", data: {publicStatus: true}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.getPublicStatus = function(padID, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(err)
{
callback(err);
return;
}
callback(null, {publicStatus: pad.getPublicStatus()});
});
}
/**
setPassword(padID, password) returns ok or a error message
Example returns:
{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.setPassword = function(padID, password, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(err)
{
callback(err);
return;
}
//set the password
pad.setPassword(password);
callback();
});
}
/**
isPasswordProtected(padID) returns true or false
Example returns:
{code: 0, message:"ok", data: {passwordProtection: true}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.isPasswordProtected = function(padID, callback)
{
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(err)
{
callback(err);
return;
}
callback(null, {isPasswordProtected: pad.isPasswordProtected()});
});
}
/******************************/
/** INTERNAL HELPER FUNCTIONS */
/******************************/
//checks if a number is an int
function is_int(value)
{
return (parseFloat(value) == parseInt(value)) && !isNaN(value)
}
//gets a pad safe
function getPadSafe(padID, shouldExist, text, callback)
{
if(typeof text == "function")
{
callback = text;
text = null;
}
//check if padID is a string
if(typeof padID != "string")
{
callback({stop: "padID is not a string"});
return;
}
//check if the padID maches the requirements
if(!padManager.isValidPadId(padID))
{
callback({stop: "padID did not match requirements"});
return;
}
//check if the pad exists
padManager.doesPadExists(padID, function(err, exists)
{
//error
if(err)
{
callback(err);
}
//does not exist, but should
else if(exists == false && shouldExist == true)
{
callback({stop: "padID does not exist"});
}
//does exists, but shouldn't
else if(exists == true && shouldExist == false)
{
callback({stop: "padID does already exist"});
}
//pad exists, let's get it
else
{
padManager.getPad(padID, text, callback);
}
});
}

View file

@ -3,7 +3,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,94 +22,122 @@ var db = require("./DB").db;
var async = require("async");
/**
* Returns the Author Id for a token. If the token is unkown,
* it creates a author for the token
* Checks if the author exists
*/
exports.doesAuthorExists = function (authorID, callback)
{
//check if the database entry of this author exists
db.get("globalAuthor:" + authorID, function (err, author)
{
callback(err, author != null);
});
}
/**
* Returns the AuthorID for a token.
* @param {String} token The token
* @param {Function} callback callback (err, author)
* The callback function that is called when the result is here
*/
exports.getAuthor4Token = function (token, callback)
{
var author;
async.series([
//try to get the author for this token
function(callback)
{
db.get("token2author:" + token, function (err, _author)
{
author = _author;
callback(err);
});
},
function(callback)
{
//there is no author with this token, so create one
if(author == null)
{
createAuthor(token, function(err, _author)
{
author = _author;
callback(err);
});
}
//there is a author with this token
else
{
//check if there is also an author object for this token, if not, create one
db.get("globalAuthor:" + author, function(err, authorObject)
{
if(authorObject == null)
{
createAuthor(token, function(err, _author)
{
author = _author;
callback(err);
});
}
//the author exists, update the timestamp of this author
else
{
db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime());
callback();
}
});
}
}
], function(err)
{
mapAuthorWithDBKey("token2author", token, function(err, author)
{
callback(err, author);
//return only the sub value authorID
callback(err, author ? author.authorID : author);
});
}
/**
* Returns the AuthorID for a mapper.
* @param {String} token The mapper
* @param {Function} callback callback (err, author)
*/
exports.createAuthorIfNotExistsFor = function (authorMapper, name, callback)
{
mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author)
{
//error?
if(err)
{
callback(err);
return;
}
//set the name of this author
if(name)
exports.setAuthorName(author.authorID, name);
//return the authorID
callback(null, author);
});
}
/**
* Returns the AuthorID for a mapper. We can map using a mapperkey,
* so far this is token2author and mapper2author
* @param {String} mapperkey The database key name for this mapper
* @param {String} mapper The mapper
* @param {Function} callback callback (err, author)
*/
function mapAuthorWithDBKey (mapperkey, mapper, callback)
{
//try to map to an author
db.get(mapperkey + ":" + mapper, function (err, author)
{
//error?
if(err)
{
callback(err);
return;
}
//there is no author with this mapper, so create one
if(author == null)
{
exports.createAuthor(null, function(err, author)
{
//error?
if(err)
{
callback(err);
return;
}
//create the token2author relation
db.set(mapperkey + ":" + mapper, author.authorID);
//return the author
callback(null, author);
});
}
//there is a author with this mapper
else
{
//update the timestamp of this author
db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime());
//return the author
callback(null, {authorID: author});
}
});
}
/**
* Internal function that creates the database entry for an author
* @param {String} token The token
* @param {String} name The name of the author
*/
function createAuthor (token, callback)
exports.createAuthor = function(name, callback)
{
//create the new author name
var author = "g." + _randomString(16);
var author = "a." + randomString(16);
//create the globalAuthors db entry
var authorObj = {colorId : Math.floor(Math.random()*32), name: null, timestamp: new Date().getTime()};
var authorObj = {"colorId" : Math.floor(Math.random()*32), "name": name, "timestamp": new Date().getTime()};
//we do this in series to ensure this db entries are written in the correct order
async.series([
//set the global author db entry
function(callback)
{
db.set("globalAuthor:" + author, authorObj, callback);
},
//set the token2author db entry
function(callback)
{
db.set("token2author:" + token, author, callback);
}
], function(err)
{
callback(err, author);
});
//set the global author db entry
db.set("globalAuthor:" + author, authorObj);
callback(null, {authorID: author});
}
/**
@ -165,11 +193,14 @@ exports.setAuthorName = function (author, name, callback)
/**
* Generates a random String with the given length. Is needed to generate the Author Ids
*/
function _randomString(len) {
// use only numbers and lowercase letters
var pieces = [];
for(var i=0;i<len;i++) {
pieces.push(Math.floor(Math.random()*36).toString(36).slice(-1));
function randomString(len)
{
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var randomstring = '';
for (var i = 0; i < len; i++)
{
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return pieces.join('');
return randomstring;
}

View file

@ -4,7 +4,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

196
node/db/GroupManager.js Normal file
View file

@ -0,0 +1,196 @@
/**
* The Group Manager provides functions to manage groups in the database
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var db = require("./DB").db;
var async = require("async");
var padManager = require("./PadManager");
exports.doesGroupExist = function(groupID, callback)
{
//try to get the group entry
db.get("group:" + groupID, function (err, group)
{
callback(err, group != null);
});
}
exports.createGroup = function(callback)
{
//search for non existing groupID
var groupID = "g." + randomString(16);
//create the group
db.set("group:" + groupID, {pads: {}});
callback(null, {groupID: groupID});
}
exports.createGroupIfNotExistsFor = function(groupMapper, callback)
{
//ensure mapper is optional
if(typeof groupMapper != "string")
{
callback({stop: "groupMapper is no string"});
return;
}
//try to get a group for this mapper
db.get("mapper2group:"+groupMapper, function(err, groupID)
{
if(err)
{
callback(err);
return;
}
//there is no group for this mapper, let's create a group
if(groupID == null)
{
exports.createGroup(function(err, responseObj)
{
//check for errors
if(err)
{
callback(err);
return;
}
//create the mapper entry for this group
db.set("mapper2group:"+groupMapper, responseObj.groupID);
callback(null, responseObj);
});
}
//there is a group for this mapper, let's return it
else
{
callback(err, {groupID: groupID});
}
});
}
exports.createGroupPad = function(groupID, padName, text, callback)
{
//create the padID
var padID = groupID + "$" + padName;
async.series([
//ensure group exists
function (callback)
{
exports.doesGroupExist(groupID, function(err, exists)
{
//error
if(err)
{
callback(err);
}
//group does not exist
else if(exists == false)
{
callback({stop: "groupID does not exist"});
}
//group exists, everything is fine
else
{
callback();
}
});
},
//ensure pad does not exists
function (callback)
{
padManager.doesPadExists(padID, function(err, exists)
{
//error
if(err)
{
callback(err);
}
//pad exists already
else if(exists == true)
{
callback({stop: "padName does already exist"});
}
//pad does not exist, everything is fine
else
{
callback();
}
});
},
//create the pad
function (callback)
{
padManager.getPad(padID, text, function(err)
{
callback(err);
});
},
//create an entry in the group for this pad
function (callback)
{
db.setSub("group:" + groupID, ["pads", padID], 1);
callback();
}
], function(err)
{
callback(err, {padID: padID});
});
}
exports.listPads = function(groupID, callback)
{
exports.doesGroupExist(groupID, function(err, exists)
{
//error
if(err)
{
callback(err);
}
//group does not exist
else if(exists == false)
{
callback({stop: "groupID does not exist"});
}
//group exists, let's get the pads
else
{
db.getSub("group:" + groupID, ["pads"], function(err, pads)
{
callback(err, {padIDs: pads});
});
}
});
}
/**
* Generates a random String with the given length. Is needed to generate the Author Ids
*/
function randomString(len)
{
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var randomstring = '';
for (var i = 0; i < len; i++)
{
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return randomstring;
}

View file

@ -8,6 +8,7 @@ var db = require("./DB").db;
var async = require("async");
var settings = require('../utils/Settings');
var authorManager = require("./AuthorManager");
var crypto = require("crypto");
/**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces
@ -44,6 +45,17 @@ Class('Pad', {
init: -1
}, // chatHead
publicStatus : {
is: 'rw',
init: false,
getterName : 'getPublicStatus'
}, //publicStatus
passwordHash : {
is: 'rw',
init: null
}, // passwordHash
id : { is : 'r' }
},
@ -82,7 +94,12 @@ Class('Pad', {
}
db.set("pad:"+this.id+":revs:"+newRev, newRevData);
db.set("pad:"+this.id, {atext: this.atext, pool: this.pool.toJsonable(), head: this.head, chatHead: this.chatHead});
db.set("pad:"+this.id, {atext: this.atext,
pool: this.pool.toJsonable(),
head: this.head,
chatHead: this.chatHead,
publicStatus: this.publicStatus,
passwordHash: this.passwordHash});
}, //appendRevision
getRevisionChangeset : function(revNum, callback)
@ -310,9 +327,15 @@ Class('Pad', {
});
},
init : function (callback)
init : function (text, callback)
{
var _this = this;
//replace text with default text if text isn't set
if(text == null)
{
text = settings.defaultPadText;
}
//try to load the pad
db.get("pad:"+this.id, function(err, value)
@ -330,22 +353,80 @@ Class('Pad', {
_this.atext = value.atext;
_this.pool = _this.pool.fromJsonable(value.pool);
//ensure we have a local chatHead variable
if(value.chatHead != null)
_this.chatHead = value.chatHead;
else
_this.chatHead = -1;
//ensure we have a local publicStatus variable
if(value.publicStatus != null)
_this.publicStatus = value.publicStatus;
else
_this.publicStatus = false;
//ensure we have a local passwordHash variable
if(value.passwordHash != null)
_this.passwordHash = value.passwordHash;
else
_this.passwordHash = null;
}
//this pad doesn't exist, so create it
else
{
var firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(settings.defaultPadText));
var firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text));
_this.appendRevision(firstChangeset, '');
}
callback(null);
});
}
},
//set in db
setPublicStatus: function(publicStatus)
{
this.publicStatus = publicStatus;
db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus);
},
setPassword: function(password)
{
this.passwordHash = password == null ? null : hash(password, generateSalt());
db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash);
},
isCorrectPassword: function(password)
{
return compare(this.passwordHash, password)
},
isPasswordProtected: function()
{
return this.passwordHash != null;
}
}, // methods
});
/* Crypto helper methods */
function hash(password, salt)
{
var shasum = crypto.createHash('sha512');
shasum.update(password + salt);
return shasum.digest("hex") + "$" + salt;
}
function generateSalt()
{
var len = 86;
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./";
var randomstring = '';
for (var i = 0; i < len; i++)
{
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return randomstring;
}
function compare(hashStr, password)
{
return hash(password, hashStr.split("$")[1]) === hashStr;
}

View file

@ -3,7 +3,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@
*/
require("../db/Pad");
var db = require("./DB").db;
/**
* A Array with all known Pads
@ -30,8 +31,40 @@ globalPads = [];
* @param id A String with the id of the pad
* @param {Function} callback
*/
exports.getPad = function(id, callback)
exports.getPad = function(id, text, callback)
{
//check if this is a valid padId
if(!exports.isValidPadId(id))
{
callback({stop: id + " is not a valid padId"});
return;
}
//make text an optional parameter
if(typeof text == "function")
{
callback = text;
text = null;
}
//check if this is a valid text
if(text != null)
{
//check if text is a string
if(typeof text != "string")
{
callback({stop: "text is not a string"});
return;
}
//check if text is less than 100k chars
if(text.length > 100000)
{
callback({stop: "text must be less than 100k chars"});
return;
}
}
var pad = globalPads[id];
//return pad if its already loaded
@ -45,7 +78,7 @@ exports.getPad = function(id, callback)
pad = new Pad(id);
//initalize the pad
pad.init(function(err)
pad.init(text, function(err)
{
if(err)
{
@ -58,6 +91,19 @@ exports.getPad = function(id, callback)
}
});
}
//globalPads[id].timestamp = new Date().getTime();
}
//checks if a pad exists
exports.doesPadExists = function(padId, callback)
{
db.get("pad:"+padId, function(err, value)
{
callback(err, value != null);
});
}
exports.isValidPadId = function(padId)
{
return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
}

View file

@ -3,7 +3,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -40,7 +40,7 @@ exports.getReadOnlyId = function (padId, callback)
//there is no readOnly Entry in the database, let's create one
if(dbReadOnlyId == null)
{
readOnlyId = randomString(10);
readOnlyId = "r." + randomString(16);
db.set("pad2readonly:" + padId, readOnlyId);
db.set("readonly2pad:" + readOnlyId, padId);
@ -74,10 +74,12 @@ exports.getPadId = function(readOnlyId, callback)
*/
function randomString(len)
{
// use only numbers and lowercase letters
var pieces = [];
for(var i=0;i<len;i++) {
pieces.push(Math.floor(Math.random()*36).toString(36).slice(-1));
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var randomstring = '';
for (var i = 0; i < len; i++)
{
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return pieces.join('');
return randomstring;
}

235
node/db/SecurityManager.js Normal file
View file

@ -0,0 +1,235 @@
/**
* Controls the security of pad access
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var db = require("./DB").db;
var async = require("async");
var authorManager = require("./AuthorManager");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");
/**
* This function controlls the access to a pad, it checks if the user can access a pad.
* @param padID the pad the user wants to access
* @param sesssionID the session the user has (set via api)
* @param token the token of the author (randomly generated at client side, used for public pads)
* @param password the password the user has given to access this pad, can be null
* @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx})
*/
exports.checkAccess = function (padID, sessionID, token, password, callback)
{
// it's not a group pad, means we can grant access
if(padID.indexOf("$") == -1)
{
//get author for this token
authorManager.getAuthor4Token(token, function(err, author)
{
// grant access, with author of token
callback(err, {accessStatus: "grant", authorID: author});
})
//don't continue
return;
}
var groupID = padID.split("$")[0];
var padExists = false;
var validSession = false;
var sessionAuthor;
var tokenAuthor;
var isPublic;
var isPasswordProtected;
var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
var statusObject;
async.series([
//get basic informations from the database
function(callback)
{
async.parallel([
//does pad exists
function(callback)
{
padManager.doesPadExists(padID, function(err, exists)
{
padExists = exists;
callback(err);
});
},
//get informations about this session
function(callback)
{
sessionManager.getSessionInfo(sessionID, function(err, sessionInfo)
{
//skip session validation if the session doesn't exists
if(err && err.stop == "sessionID does not exist")
{
callback();
return;
}
if(err) {callback(err); return}
var now = Math.floor(new Date().getTime()/1000);
//is it for this group? and is validUntil still ok? --> validSession
if(sessionInfo.groupID == groupID && sessionInfo.validUntil > now)
{
validSession = true;
}
sessionAuthor = sessionInfo.authorID;
callback();
});
},
//get author for token
function(callback)
{
//get author for this token
authorManager.getAuthor4Token(token, function(err, author)
{
tokenAuthor = author;
callback(err);
});
}
], callback);
},
//get more informations of this pad, if avaiable
function(callback)
{
//skip this if the pad doesn't exists
if(padExists == false)
{
callback();
return;
}
padManager.getPad(padID, function(err, pad)
{
if(err) {callback(err); return}
//is it a public pad?
isPublic = pad.getPublicStatus();
//is it password protected?
isPasswordProtected = pad.isPasswordProtected();
//is password correct?
if(isPasswordProtected && password && pad.isCorrectPassword(password))
{
passwordStatus = "correct";
}
callback();
});
},
function(callback)
{
//- a valid session for this group is avaible AND pad exists
if(validSession && padExists)
{
//- the pad is not password protected
if(!isPasswordProtected)
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
}
//- the pad is password protected and password is correct
else if(isPasswordProtected && passwordStatus == "correct")
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
}
//- the pad is password protected but wrong password given
else if(isPasswordProtected && passwordStatus == "wrong")
{
//--> deny access, ask for new password and tell them that the password is wrong
statusObject = {accessStatus: "wrongPassword"};
}
//- the pad is password protected but no password given
else if(isPasswordProtected && passwordStatus == "notGiven")
{
//--> ask for password
statusObject = {accessStatus: "needPassword"};
}
else
{
throw new Error("Ops, something wrong happend");
}
}
//- a valid session for this group avaible but pad doesn't exists
else if(validSession && !padExists)
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
}
// there is no valid session avaiable AND pad exists
else if(!validSession && padExists)
{
//-- its public and not password protected
if(isPublic && !isPasswordProtected)
{
//--> grant access, with author of token
statusObject = {accessStatus: "grant", authorID: tokenAuthor};
}
//- its public and password protected and password is correct
else if(isPublic && isPasswordProtected && passwordStatus == "correct")
{
//--> grant access, with author of token
statusObject = {accessStatus: "grant", authorID: tokenAuthor};
}
//- its public and the pad is password protected but wrong password given
else if(isPublic && isPasswordProtected && passwordStatus == "wrong")
{
//--> deny access, ask for new password and tell them that the password is wrong
statusObject = {accessStatus: "wrongPassword"};
}
//- its public and the pad is password protected but no password given
else if(isPublic && isPasswordProtected && passwordStatus == "notGiven")
{
//--> ask for password
statusObject = {accessStatus: "needPassword"};
}
//- its not public
else if(!isPublic)
{
//--> deny access
statusObject = {accessStatus: "deny"};
}
else
{
throw new Error("Ops, something wrong happend");
}
}
// there is no valid session avaiable AND pad doesn't exists
else
{
//--> deny access
statusObject = {accessStatus: "deny"};
}
callback();
}
], function(err)
{
callback(err, statusObject);
});
}

397
node/db/SessionManager.js Normal file
View file

@ -0,0 +1,397 @@
/**
* The Session Manager provides functions to manage session in the database
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var db = require("./DB").db;
var async = require("async");
var groupMangager = require("./GroupManager");
var authorMangager = require("./AuthorManager");
exports.doesSessionExist = function(sessionID, callback)
{
//check if the database entry of this session exists
db.get("session:" + sessionID, function (err, session)
{
callback(err, session != null);
});
}
/**
* Creates a new session between an author and a group
*/
exports.createSession = function(groupID, authorID, validUntil, callback)
{
var sessionID;
async.series([
//check if group exists
function(callback)
{
groupMangager.doesGroupExist(groupID, function(err, exists)
{
//error
if(err)
{
callback(err);
}
//group does not exist
else if(exists == false)
{
callback({stop: "groupID does not exist"});
}
//everything is fine, continue
else
{
callback();
}
});
},
//check if author exists
function(callback)
{
authorMangager.doesAuthorExists(authorID, function(err, exists)
{
//error
if(err)
{
callback(err);
}
//author does not exist
else if(exists == false)
{
callback({stop: "authorID does not exist"});
}
//everything is fine, continue
else
{
callback();
}
});
},
//check validUntil and create the session db entry
function(callback)
{
//check if rev is a number
if(typeof validUntil != "number")
{
//try to parse the number
if(!isNaN(parseInt(validUntil)))
{
validUntil = parseInt(validUntil);
}
else
{
callback({stop: "validUntil is not a number"});
return;
}
}
//ensure this is not a negativ number
if(validUntil < 0)
{
callback({stop: "validUntil is a negativ number"});
return;
}
//ensure this is not a float value
if(!is_int(validUntil))
{
callback({stop: "validUntil is a float value"});
return;
}
//check if validUntil is in the future
if(Math.floor(new Date().getTime()/1000) > validUntil)
{
callback({stop: "validUntil is in the past"});
return;
}
//generate sessionID
sessionID = "s." + randomString(16);
//set the session into the database
db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil});
callback();
},
//set the group2sessions entry
function(callback)
{
//get the entry
db.get("group2sessions:" + groupID, function(err, group2sessions)
{
//did a error happen?
if(err)
{
callback(err);
return;
}
//the entry doesn't exist so far, let's create it
if(group2sessions == null)
{
group2sessions = {sessionIDs : {}};
}
//add the entry for this session
group2sessions.sessionIDs[sessionID] = 1;
//save the new element back
db.set("group2sessions:" + groupID, group2sessions);
callback();
});
},
//set the author2sessions entry
function(callback)
{
//get the entry
db.get("author2sessions:" + authorID, function(err, author2sessions)
{
//did a error happen?
if(err)
{
callback(err);
return;
}
//the entry doesn't exist so far, let's create it
if(author2sessions == null)
{
author2sessions = {sessionIDs : {}};
}
//add the entry for this session
author2sessions.sessionIDs[sessionID] = 1;
//save the new element back
db.set("author2sessions:" + authorID, author2sessions);
callback();
});
}
], function(err)
{
//return error and sessionID
callback(err, {sessionID: sessionID});
})
}
exports.getSessionInfo = function(sessionID, callback)
{
//check if the database entry of this session exists
db.get("session:" + sessionID, function (err, session)
{
//error
if(err)
{
callback(err);
}
//session does not exists
else if(session == null)
{
callback({stop: "sessionID does not exist"})
}
//everything is fine, return the sessioninfos
else
{
callback(null, session);
}
});
}
/**
* Deletes a session
*/
exports.deleteSession = function(sessionID, callback)
{
var authorID, groupID;
var group2sessions, author2sessions;
async.series([
function(callback)
{
//get the session entry
db.get("session:" + sessionID, function (err, session)
{
//error
if(err)
{
callback(err);
}
//session does not exists
else if(session == null)
{
callback({stop: "sessionID does not exist"})
}
//everything is fine, return the sessioninfos
else
{
authorID = session.authorID;
groupID = session.groupID;
callback();
}
});
},
//get the group2sessions entry
function(callback)
{
db.get("group2sessions:" + groupID, function (err, _group2sessions)
{
group2sessions = _group2sessions;
callback(err);
});
},
//get the author2sessions entry
function(callback)
{
db.get("author2sessions:" + authorID, function (err, _author2sessions)
{
author2sessions = _author2sessions;
callback(err);
});
},
//remove the values from the database
function(callback)
{
//remove the session
db.remove("session:" + sessionID);
//remove session from group2sessions
delete group2sessions.sessionIDs[sessionID];
db.set("group2sessions:" + groupID, group2sessions);
//remove session from author2sessions
delete author2sessions.sessionIDs[sessionID];
db.set("author2sessions:" + authorID, author2sessions);
callback();
}
], function(err)
{
callback(err);
})
}
exports.listSessionsOfGroup = function(groupID, callback)
{
groupMangager.doesGroupExist(groupID, function(err, exists)
{
//error
if(err)
{
callback(err);
}
//group does not exist
else if(exists == false)
{
callback({stop: "groupID does not exist"});
}
//everything is fine, continue
else
{
listSessionsWithDBKey("group2sessions:" + groupID, callback);
}
});
}
exports.listSessionsOfAuthor = function(authorID, callback)
{
authorMangager.doesAuthorExists(authorID, function(err, exists)
{
//error
if(err)
{
callback(err);
}
//group does not exist
else if(exists == false)
{
callback({stop: "authorID does not exist"});
}
//everything is fine, continue
else
{
listSessionsWithDBKey("author2sessions:" + authorID, callback);
}
});
}
//this function is basicly the code listSessionsOfAuthor and listSessionsOfGroup has in common
function listSessionsWithDBKey (dbkey, callback)
{
var sessions;
async.series([
function(callback)
{
//get the group2sessions entry
db.get(dbkey, function(err, sessionObject)
{
sessions = sessionObject ? sessionObject.sessionIDs : null;
callback(err);
});
},
function(callback)
{
//collect all sessionIDs in an arrary
var sessionIDs = [];
for (var i in sessions)
{
sessionIDs.push(i);
}
//foreach trough the sessions and get the sessioninfos
async.forEach(sessionIDs, function(sessionID, callback)
{
exports.getSessionInfo(sessionID, function(err, sessionInfo)
{
sessions[sessionID] = sessionInfo;
callback(err);
});
}, callback);
}
], function(err)
{
callback(err, sessions);
});
}
/**
* Generates a random String with the given length. Is needed to generate the SessionIDs
*/
function randomString(len)
{
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var randomstring = '';
for (var i = 0; i < len; i++)
{
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return randomstring;
}
//checks if a number is an int
function is_int(value)
{
return (parseFloat(value) == parseInt(value)) && !isNaN(value)
}

View file

@ -5,7 +5,7 @@
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

144
node/handler/APIHandler.js Normal file
View file

@ -0,0 +1,144 @@
/**
* The API Handler handles all API http requests
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var fs = require("fs");
var api = require("../db/API");
//ensure we have an apikey
var apikey = null;
try
{
apikey = fs.readFileSync("../APIKEY.txt","utf8");
}
catch(e)
{
apikey = randomString(32);
fs.writeFileSync("../APIKEY.txt",apikey,"utf8");
}
//a list of all functions
var functions = {
"createGroup" : [],
"createGroupIfNotExistsFor" : ["groupMapper"],
// "deleteGroup" : ["groupID"],
"listPads" : ["groupID"],
"createPad" : ["padID", "text"],
"createGroupPad" : ["groupID", "padName", "text"],
"createAuthor" : ["name"],
"createAuthorIfNotExistsFor" : ["authorMapper" , "name"],
"createSession" : ["groupID", "authorID", "validUntil"],
"deleteSession" : ["sessionID"],
"getSessionInfo" : ["sessionID"],
"listSessionsOfGroup" : ["groupID"],
"listSessionsOfAuthor" : ["authorID"],
"getText" : ["padID", "rev"],
"setText" : ["padID", "text"],
"getRevisionsCount" : ["padID"],
// "deletePad" : ["padID"],
"getReadOnlyID" : ["padID"],
"setPublicStatus" : ["padID", "publicStatus"],
"getPublicStatus" : ["padID"],
"setPassword" : ["padID", "password"],
"isPasswordProtected" : ["padID"]
};
/**
* Handles a HTTP API call
* @param functionName the name of the called function
* @param fields the params of the called function
* @req express request object
* @res express response object
*/
exports.handle = function(functionName, fields, req, res)
{
//check the api key!
if(fields["apikey"] != apikey)
{
res.send({code: 4, message: "no or wrong API Key", data: null});
return;
}
//check if this is a valid function name
var isKnownFunctionname = false;
for(var knownFunctionname in functions)
{
if(knownFunctionname == functionName)
{
isKnownFunctionname = true;
break;
}
}
//say goodbye if this is a unkown function
if(!isKnownFunctionname)
{
res.send({code: 3, message: "no such function", data: null});
return;
}
//put the function parameters in an array
var functionParams = [];
for(var i=0;i<functions[functionName].length;i++)
{
functionParams.push(fields[functions[functionName][i]]);
}
//add a callback function to handle the response
functionParams.push(function(err, data)
{
// no error happend, everything is fine
if(err == null)
{
if(!data)
data = null;
res.send({code: 0, message: "ok", data: data});
}
// parameters were wrong and the api stopped execution, pass the error
else if(err.stop)
{
res.send({code: 1, message: err.stop, data: null});
}
//an unkown error happend
else
{
res.send({code: 2, message: "internal error", data: null});
throw (err);
}
});
//call the api function
api[functionName](functionParams[0],functionParams[1],functionParams[2],functionParams[3],functionParams[4]);
}
/**
* Generates a random String with the given length. Is needed to generate the Author Ids
*/
function randomString(len)
{
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var randomstring = '';
for (var i = 0; i < len; i++)
{
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1);
}
return randomstring;
}

View file

@ -3,7 +3,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -3,7 +3,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -3,7 +3,7 @@
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -25,6 +25,7 @@ var AttributePoolFactory = require("../utils/AttributePoolFactory");
var authorManager = require("../db/AuthorManager");
var readOnlyManager = require("../db/ReadOnlyManager");
var settings = require('../utils/Settings');
var securityManager = require("../db/SecurityManager");
/**
* A associative array that translates a session to a pad
@ -585,51 +586,65 @@ function handleClientReady(client, message)
var chatMessages;
async.series([
//check permissions
function(callback)
{
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
{
if(err) {callback(err); return}
//access was granted
if(statusObject.accessStatus == "grant")
{
author = statusObject.authorID;
callback();
}
//no access, send the client a message that tell him why
else
{
client.json.send({accessStatus: statusObject.accessStatus})
}
});
},
//get all authordata of this new user
function(callback)
{
//Ask the author Manager for a author of this token.
authorManager.getAuthor4Token(message.token, function(err,value)
{
author = value;
async.parallel([
//get colorId
function(callback)
async.parallel([
//get colorId
function(callback)
{
authorManager.getAuthorColorId(author, function(err, value)
{
authorManager.getAuthorColorId(author, function(err, value)
{
authorColorId = value;
callback(err);
});
},
//get author name
function(callback)
authorColorId = value;
callback(err);
});
},
//get author name
function(callback)
{
authorManager.getAuthorName(author, function(err, value)
{
authorManager.getAuthorName(author, function(err, value)
{
authorName = value;
callback(err);
});
},
function(callback)
authorName = value;
callback(err);
});
},
function(callback)
{
padManager.getPad(message.padId, function(err, value)
{
padManager.getPad(message.padId, function(err, value)
{
pad = value;
callback(err);
});
},
function(callback)
pad = value;
callback(err);
});
},
function(callback)
{
readOnlyManager.getReadOnlyId(message.padId, function(err, value)
{
readOnlyManager.getReadOnlyId(message.padId, function(err, value)
{
readOnlyId = value;
callback(err);
});
}
], callback);
});
readOnlyId = value;
callback(err);
});
}
], callback);
},
//these db requests all need the pad object
function(callback)

View file

@ -4,7 +4,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@
var log4js = require('log4js');
var messageLogger = log4js.getLogger("message");
var securityManager = require("../db/SecurityManager");
/**
* Saves all components
@ -53,11 +54,13 @@ exports.setSocketIO = function(_socket)
socket.sockets.on('connection', function(client)
{
var clientAuthorized = false;
//wrap the original send function to log the messages
client._send = client.send;
client.send = function(message)
{
messageLogger.info("to " + client.id + ": " + JSON.stringify(message));
messageLogger.info("to " + client.id + ": " + stringifyWithoutPassword(message));
client._send(message);
}
@ -67,34 +70,72 @@ exports.setSocketIO = function(_socket)
components[i].handleConnect(client);
}
client.on('message', function(message)
//try to handle the message of this client
function handleMessage(message)
{
if(message.protocolVersion && message.protocolVersion != 2)
{
messageLogger.warn("Protocolversion header is not correct:" + JSON.stringify(message));
return;
}
//route this message to the correct component, if possible
if(message.component && components[message.component])
{
messageLogger.info("from " + client.id + ": " + JSON.stringify(message));
//check if component is registered in the components array
if(components[message.component])
{
messageLogger.info("from " + client.id + ": " + stringifyWithoutPassword(message));
components[message.component].handleMessage(client, message);
}
}
else
{
messageLogger.error("Can't route the message:" + JSON.stringify(message));
messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message));
}
}
client.on('message', function(message)
{
if(message.protocolVersion && message.protocolVersion != 2)
{
messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message));
return;
}
//client is authorized, everything ok
if(clientAuthorized)
{
handleMessage(message);
}
//try to authorize the client
else
{
//this message has everything to try an authorization
if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined)
{
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
{
if(err) throw err;
//access was granted, mark the client as authorized and handle the message
if(statusObject.accessStatus == "grant")
{
clientAuthorized = true;
handleMessage(message);
}
//no access, send the client a message that tell him why
else
{
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
client.json.send({accessStatus: statusObject.accessStatus});
}
});
}
//drop message
else
{
messageLogger.warn("Droped message cause of bad permissions:" + stringifyWithoutPassword(message));
}
}
});
client.on('disconnect', function()
{
//tell all components about this disconnect
//tell all components about this disconnect
for(var i in components)
{
components[i].handleDisconnect(client);
@ -102,3 +143,20 @@ exports.setSocketIO = function(_socket)
});
});
}
//returns a stringified representation of a message, removes the password
//this ensures there are no passwords in the log
function stringifyWithoutPassword(message)
{
var newMessage = {};
for(var i in message)
{
if(i == "password" && message[i] != null)
newMessage["password"] = "xxx";
else
newMessage[i]=message[i];
}
return JSON.stringify(newMessage);
}

View file

@ -3,7 +3,7 @@
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -5,7 +5,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,21 +22,24 @@
require('joose');
var log4js = require('log4js');
var socketio = require('socket.io');
var fs = require('fs');
var settings = require('./utils/Settings');
var socketIORouter = require("./handler/SocketIORouter");
var db = require('./db/DB');
var async = require('async');
var express = require('express');
var path = require('path');
var minify = require('./utils/Minify');
var formidable = require('formidable');
var log4js = require('log4js');
var apiHandler;
var exportHandler;
var importHandler;
var exporthtml;
var readOnlyManager;
var padManager;
var securityManager;
var socketIORouter;
//try to get the git version
var version = "";
@ -74,12 +77,17 @@ async.waterfall([
exporthtml = require("./utils/ExportHtml");
exportHandler = require('./handler/ExportHandler');
importHandler = require('./handler/ImportHandler');
apiHandler = require('./handler/APIHandler');
padManager = require('./db/PadManager');
securityManager = require('./db/SecurityManager');
socketIORouter = require("./handler/SocketIORouter");
//install logging
var httpLogger = log4js.getLogger("http");
app.configure(function()
{
app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
app.use(express.cookieParser());
});
//serve static files
@ -156,11 +164,31 @@ async.waterfall([
});
});
//checks for padAccess
function hasPadAccess(req, res, callback)
{
securityManager.checkAccess(req.params.pad, req.cookies.sessionid, req.cookies.token, req.cookies.password, function(err, accessObj)
{
if(err) throw err;
//there is access, continue
if(accessObj.accessStatus == "grant")
{
callback();
}
//no access
else
{
res.send("403 - Can't touch this", 403);
}
});
}
//serve pad.html under /p
app.get('/p/:pad', function(req, res, next)
{
//ensure the padname is valid and the url doesn't end with a /
if(!isValidPadname(req.params.pad) || /\/$/.test(req.url))
if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url))
{
res.send('Such a padname is forbidden', 404);
return;
@ -175,7 +203,7 @@ async.waterfall([
app.get('/p/:pad/timeslider', function(req, res, next)
{
//ensure the padname is valid and the url doesn't end with a /
if(!isValidPadname(req.params.pad) || /\/$/.test(req.url))
if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url))
{
res.send('Such a padname is forbidden', 404);
return;
@ -190,7 +218,7 @@ async.waterfall([
app.get('/p/:pad/export/:type', function(req, res, next)
{
//ensure the padname is valid and the url doesn't end with a /
if(!isValidPadname(req.params.pad) || /\/$/.test(req.url))
if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url))
{
res.send('Such a padname is forbidden', 404);
return;
@ -213,14 +241,18 @@ async.waterfall([
res.header("Access-Control-Allow-Origin", "*");
res.header("Server", serverName);
exportHandler.doExport(req, res, req.params.pad, req.params.type);
hasPadAccess(req, res, function()
{
exportHandler.doExport(req, res, req.params.pad, req.params.type);
});
});
//handle import requests
app.post('/p/:pad/import', function(req, res, next)
{
//ensure the padname is valid and the url doesn't end with a /
if(!isValidPadname(req.params.pad) || /\/$/.test(req.url))
if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url))
{
res.send('Such a padname is forbidden', 404);
return;
@ -234,11 +266,36 @@ async.waterfall([
}
res.header("Server", serverName);
importHandler.doImport(req, res, req.params.pad);
hasPadAccess(req, res, function()
{
importHandler.doImport(req, res, req.params.pad);
});
});
var apiLogger = log4js.getLogger("API");
//This is a api call, collect all post informations and pass it to the apiHandler
app.get('/api/1/:func', function(req, res)
{
res.header("Server", serverName);
apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(req.query));
//wrap the send function so we can log the response
res._send = res.send;
res.send = function(response)
{
response = JSON.stringify(response);
apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
res._send(response);
}
//call the api handler
apiHandler.handle(req.params.func, req.query, req, res);
});
//The Etherpad client side sends information about how a disconnect happen
//I don't know how to use them, but maybe there usefull, so we should print them out to the log
app.post('/ep/pad/connection-diagnostic-info', function(req, res)
{
new formidable.IncomingForm().parse(req, function(err, fields, files)
@ -319,12 +376,3 @@ async.waterfall([
callback(null);
}
]);
function isValidPadname(padname)
{
//ensure there is no dollar sign in the pad name
if(padname.indexOf("$")!=-1)
return false;
return true;
}

View file

@ -3,7 +3,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -7,7 +7,7 @@
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -4,7 +4,7 @@
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -4,7 +4,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -4,7 +4,7 @@
*/
/*
* 2011 Peter 'Pita' Martischka
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -77,7 +77,7 @@ for(var i in settings)
//test if the setting start with a low character
if(i.charAt(0).search("[a-z]") !== 0)
{
console.error("WARNING: Settings should start with a low character: '" + i + "'");
console.warn("Settings should start with a low character: '" + i + "'");
}
//we know this setting, so we overwrite it
@ -88,7 +88,7 @@ for(var i in settings)
//this setting is unkown, output a warning and throw it away
else
{
console.error("WARNING: Unkown Setting: '" + i + "'");
console.error("This setting doesn't exist or it was removed");
console.warn("Unkown Setting: '" + i + "'");
console.warn("This setting doesn't exist or it was removed");
}
}

View file

@ -1,3 +1,19 @@
/**
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var chat = (function()
{
var self = {

View file

@ -1,5 +1,5 @@
/**
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -34,7 +34,7 @@ $(window).unload(function()
pad.dispose();
});
function createCookie(name, value, days)
function createCookie(name, value, days, path)
{
if (days)
{
@ -43,7 +43,11 @@ function createCookie(name, value, days)
var expires = "; expires=" + date.toGMTString();
}
else var expires = "";
document.cookie = name + "=" + value + expires + "; path=/";
if(!path)
path = "/";
document.cookie = name + "=" + value + expires + "; path=" + path;
}
function readCookie(name)
@ -133,6 +137,14 @@ function getUrlVars()
return vars;
}
function savePassword()
{
//set the password cookie
createCookie("password",$("#passwordinput").val(),null,document.location.pathname);
//reload
document.location=document.location;
}
function handshake()
{
var loc = document.location;
@ -161,10 +173,15 @@ function handshake()
createCookie("token", token, 60);
}
var sessionID = readCookie("sessionID");
var password = readCookie("password");
var msg = {
"component": "pad",
"type": "CLIENT_READY",
"padId": padId,
"sessionID": sessionID,
"password": password,
"token": token,
"protocolVersion": 2
};
@ -176,17 +193,41 @@ function handshake()
socket.on('message', function(obj)
{
//if we haven't recieved the clientVars yet, then this message should it be
if (!receivedClientVars)
//the access was not granted, give the user a message
if(!receivedClientVars && obj.accessStatus)
{
if(obj.accessStatus == "deny")
{
$("#editorloadingbox").html("<b>You have no permission to access this pad</b>");
}
else if(obj.accessStatus == "needPassword")
{
$("#editorloadingbox").html("<b>You need a password to access this pad</b><br>" +
"<input id='passwordinput' type='password' name='password'>"+
"<button type='button' onclick='savePassword()'>ok</button>");
}
else if(obj.accessStatus == "wrongPassword")
{
$("#editorloadingbox").html("<b>You're password was wrong</b><br>" +
"<input id='passwordinput' type='password' name='password'>"+
"<button type='button' onclick='savePassword()'>ok</button>");
}
}
//if we haven't recieved the clientVars yet, then this message should it be
else if (!receivedClientVars)
{
//log the message
if (window.console) console.log(obj);
receivedClientVars = true;
//set some client vars
clientVars = obj;
clientVars.userAgent = "Anonymous";
clientVars.collab_client_vars.clientAgent = "Anonymous";
//initalize the pad
pad.init();
initalized = true;
@ -195,20 +236,17 @@ function handshake()
{
pad.changeViewOption('showLineNumbers', false);
}
// If the Monospacefont value is set to true then change it to monospace.
if (useMonospaceFontGlobal == true)
{
pad.changeViewOption('useMonospaceFont', true);
}
// if the globalUserName value is set we need to tell the server and the client about the new authorname
if (globalUserName !== false)
{
pad.notifyChangeName(globalUserName); // Notifies the server
$('#myusernameedit').attr({"value":globalUserName}); // Updates the current users UI
}
}
//This handles every Message after the clientVars
else

164
static/tests.html Normal file
View file

@ -0,0 +1,164 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>API Test and Examples Page</title>
<script type="text/javascript" src="js/jquery.min.js"></script>
<style type="text/css">
body {
font-size:9pt;
background: rgba(0, 0, 0, .05);
color: #333;
text-shadow: 0 1px 0 #fff;
font: 14px helvetica,sans-serif;
background: #ccc;
background: -moz-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed;
background: -webkit-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed;
background: -ms-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed;
background: -o-radial-gradient(circle, #aaa, #eee) no-repeat center center fixed;
width: 1000px;
}
.define, #template {
display: none;
}
.test_group {
overflow: auto;
width: 300px;
float:left;
color: #555;
border-top: 1px solid #999;
margin: 4px;
padding: 4px 10px 4px 10px;
background: #eee;
background: -webkit-linear-gradient(#fff, #ccc);
background: -moz-linear-gradient(#fff, #ccc);
background: -ms-linear-gradient(#fff, #ccc);
background: -o-linear-gradient(#fff, #ccc);
opacity: .9;
box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.3);
}
.test_group h2 {
font-size: 10pt;
}
.test_group table {
width: 100%;
}
#apikeyDIV {
width: 100%
}
</style>
<script type="text/javascript">
$(document).ready(function() {
$('input[type=button]').live('click', function() {
var $test_group = $(this).closest('.test_group');
var name = parseName($test_group.find('h2').text());
var results_node = $test_group.find('.results');
var params = {};
$test_group.find('input[type=text]').each(function() {
params[$(this).attr('name')] = $(this).val();
});
callFunction(name, results_node, params);
});
$('.define').each(function() {
var functionName = parseName($(this).text());
var parameters = parseParameters($(this).text());
var $template = $('#template').clone();
$template.find('h2').text(functionName + "()");
var $table = $template.find('table');
$(parameters).each(function(index, el) {
$table.prepend('<tr><td>' + el + ':</td>' +
'<td style="width:200px"><input type="text" size="10" name="' + el + '" /></td></tr>');
});
$template.css({display: "block"});
$template.appendTo('body');
});
});
function parseName(str)
{
return str.substring(0, str.indexOf('('));
}
function parseParameters(str)
{
// parse out the parameters by looking for parens
var parens = str.substring(str.indexOf("("));
// return empty array if there are no paremeters
if(parens.length < 3)
{
return [];
}
// remove parens from string
parens = parens.substring(1);
parens = parens.substring(0, parens.length-1);
return parens.split(',');
}
function callFunction(memberName, results_node, params)
{
$('#result').text('Calling ' + memberName + "()...");
params["apikey"]=$("#apikey").val();
$.ajax({
type: "GET",
url: "/api/1/" + memberName,
data: params,
success: function(json) {
results_node.text(json);
},
error: function(jqXHR, textStatus, errorThrown) {
results_node.html("textStatus: " + textStatus + "<br />errorThrown: " + errorThrown);
}
});
}
</script>
</head>
<body>
<div id="apikeyDIV" class="test_group"><b>APIKEY: </b><input type="text" id="apikey"></div>
<div class="test_group" id="template">
<h2>createGroup()</h2>
<table>
<tr>
<td class="buttonBox" colspan="2" style="text-align:right;"><input type="button" value="Run" /></td>
</tr>
</table>
<div class="results"/>
</div>
<div class="define">createGroup()</div>
<div class="define">createGroupIfNotExistsFor(groupMapper)</div>
<div class="define">listPads(groupID)</div>
<div class="define">createPad(padID,text)</div>
<div class="define">createGroupPad(groupID,padName,text)</div>
<div class="define">createAuthor(name)</div>
<div class="define">createAuthorIfNotExistsFor(authorMapper,name)</div>
<div class="define">createSession(groupID,authorID,validUntil)</div>
<div class="define">deleteSession(sessionID)</div>
<div class="define">getSessionInfo(sessionID)</div>
<div class="define">listSessionsOfGroup(groupID)</div>
<div class="define">listSessionsOfAuthor(authorID)</div>
<div class="define">getText(padID,rev)</div>
<div class="define">setText(padID,text)</div>
<div class="define">getRevisionsCount(padID)</div>
<div class="define">deletePad(padID)</div>
<div class="define">getReadOnlyID(padID)</div>
<div class="define">setPublicStatus(padID,publicStatus)</div>
<div class="define">getPublicStatus(padID)</div>
<div class="define">setPassword(padID,password)</div>
<div class="define">isPasswordProtected(padID)</div>
</body>
</html>

View file

@ -105,17 +105,26 @@
{
changesetLoader.handleSocketResponse(message);
}
else if(message.accessStatus)
{
$("body").html("<h2>You have no permission to access this pad</h2>")
}
});
});
//sends a message over the socket
function sendSocketMsg(type, data)
{
var sessionID = readCookie("sessionID");
var password = readCookie("password");
var msg = { "component" : "timeslider",
"type": type,
"data": data,
"padId": padId,
"token": token,
"sessionID": sessionID,
"password": password,
"protocolVersion": 2};
socket.json.send(msg);