mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-28 11:26:16 -04:00
Merge branch 'develop' of github.com:ether/etherpad-lite into test-plugins
This commit is contained in:
commit
f823f48607
19 changed files with 542 additions and 402 deletions
|
@ -370,7 +370,8 @@ A plugin's authFailure function is only called if all of the following are true:
|
||||||
Calling the provided callback with `[true]` tells Etherpad that the failure was
|
Calling the provided callback with `[true]` tells Etherpad that the failure was
|
||||||
handled and no further error handling is required. Calling the callback with
|
handled and no further error handling is required. Calling the callback with
|
||||||
`[]` or `undefined` defers error handling to the next authFailure plugin (if
|
`[]` or `undefined` defers error handling to the next authFailure plugin (if
|
||||||
any, otherwise fall back to HTTP basic authentication).
|
any, otherwise fall back to HTTP basic authentication for an authentication
|
||||||
|
failure or a generic 403 page for an authorization failure).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,8 @@
|
||||||
"pad.modals.deleted.explanation": "Ena ped wedariye",
|
"pad.modals.deleted.explanation": "Ena ped wedariye",
|
||||||
"pad.modals.rateLimited": "Nısbeto kemeyeyın",
|
"pad.modals.rateLimited": "Nısbeto kemeyeyın",
|
||||||
"pad.modals.rateLimited.explanation": "Na pad re ßıma vêşi mesac rışto, coki ra irtibat bıriyayo.",
|
"pad.modals.rateLimited.explanation": "Na pad re ßıma vêşi mesac rışto, coki ra irtibat bıriyayo.",
|
||||||
|
"pad.modals.rejected.explanation": "Server, terefê browseri ra rışiyaye yew mesac red kerdo.",
|
||||||
|
"pad.modals.rejected.cause": "Şıma wexto ke ped weyniyayış de server belka biyo rocane ya ziEtherpad de yew xeta bena. Pela reyna bar kerê.",
|
||||||
"pad.modals.disconnected": "İrtibata şıma reyê",
|
"pad.modals.disconnected": "İrtibata şıma reyê",
|
||||||
"pad.modals.disconnected.explanation": "Rovıteri ya irtibata şıma reyyê",
|
"pad.modals.disconnected.explanation": "Rovıteri ya irtibata şıma reyyê",
|
||||||
"pad.modals.disconnected.cause": "Qay rovıtero nêkarên o. Ena xerpey deqam kena se idarekaranê sistemiya irtibat kewê",
|
"pad.modals.disconnected.cause": "Qay rovıtero nêkarên o. Ena xerpey deqam kena se idarekaranê sistemiya irtibat kewê",
|
||||||
|
|
|
@ -104,6 +104,8 @@
|
||||||
"pad.modals.deleted.explanation": "Ce bloc-notes a été supprimé.",
|
"pad.modals.deleted.explanation": "Ce bloc-notes a été supprimé.",
|
||||||
"pad.modals.rateLimited": "Taux limité.",
|
"pad.modals.rateLimited": "Taux limité.",
|
||||||
"pad.modals.rateLimited.explanation": "Vous avez envoyé trop de messages à ce bloc, il vous a donc déconnecté.",
|
"pad.modals.rateLimited.explanation": "Vous avez envoyé trop de messages à ce bloc, il vous a donc déconnecté.",
|
||||||
|
"pad.modals.rejected.explanation": "Le serveur a rejeté un message qui a été envoyé par votre navigateur.",
|
||||||
|
"pad.modals.rejected.cause": "Le serveur peut avoir été mis à jour pendant que vous regardiez le bloc, ou il y a peut-être un bogue dans Etherpad. Essayez de recharger la page.",
|
||||||
"pad.modals.disconnected": "Vous avez été déconnecté.",
|
"pad.modals.disconnected": "Vous avez été déconnecté.",
|
||||||
"pad.modals.disconnected.explanation": "La connexion au serveur a échoué.",
|
"pad.modals.disconnected.explanation": "La connexion au serveur a échoué.",
|
||||||
"pad.modals.disconnected.cause": "Il se peut que le serveur soit indisponible. Si le problème persiste, veuillez en informer l’administrateur du service.",
|
"pad.modals.disconnected.cause": "Il se peut que le serveur soit indisponible. Si le problème persiste, veuillez en informer l’administrateur du service.",
|
||||||
|
|
|
@ -81,6 +81,8 @@
|
||||||
"pad.modals.deleted.explanation": "Оваа тетратка е отстранета.",
|
"pad.modals.deleted.explanation": "Оваа тетратка е отстранета.",
|
||||||
"pad.modals.rateLimited": "Ограничено по стапка.",
|
"pad.modals.rateLimited": "Ограничено по стапка.",
|
||||||
"pad.modals.rateLimited.explanation": "Испративте премногу пораки на тетраткава, па затоа таа ве исклучи.",
|
"pad.modals.rateLimited.explanation": "Испративте премногу пораки на тетраткава, па затоа таа ве исклучи.",
|
||||||
|
"pad.modals.rejected.explanation": "Опслужувачот ја отфрли пораката што му беше испратена од вашиот прелистувач.",
|
||||||
|
"pad.modals.rejected.cause": "Опслужувачот може да бил подновен додека ја гледавте тетратката, или пак Etherpad има некоја грешка. Пробајте со превчитување на страницата.",
|
||||||
"pad.modals.disconnected": "Врската е прекината.",
|
"pad.modals.disconnected": "Врската е прекината.",
|
||||||
"pad.modals.disconnected.explanation": "Врската со опслужувачот е прекината",
|
"pad.modals.disconnected.explanation": "Врската со опслужувачот е прекината",
|
||||||
"pad.modals.disconnected.cause": "Опслужувачот може да е недостапен. Известете го администраторот ако ова продолжи да ви се случува.",
|
"pad.modals.disconnected.cause": "Опслужувачот може да е недостапен. Известете го администраторот ако ова продолжи да ви се случува.",
|
||||||
|
|
|
@ -78,6 +78,8 @@
|
||||||
"pad.modals.deleted.explanation": "Ës feuj a l'é stàit eliminà.",
|
"pad.modals.deleted.explanation": "Ës feuj a l'é stàit eliminà.",
|
||||||
"pad.modals.rateLimited": "Tass limità.",
|
"pad.modals.rateLimited": "Tass limità.",
|
||||||
"pad.modals.rateLimited.explanation": "A l'ha mandà tròpi mëssagi a 's blòch-sì, antlora a l'ha dëscolegalo.",
|
"pad.modals.rateLimited.explanation": "A l'ha mandà tròpi mëssagi a 's blòch-sì, antlora a l'ha dëscolegalo.",
|
||||||
|
"pad.modals.rejected.explanation": "Ël servent a l'ha arpossà un mëssagi mandà da sò navigador.",
|
||||||
|
"pad.modals.rejected.cause": "Ël servent a podrìa esse stàit agiornà antramentre che chiel a beicava ël blòch, o peul desse ch'a-i é un givo an Etherpad. Ch'a preuva a carié torna la pàgina.",
|
||||||
"pad.modals.disconnected": "A l'é stàit dëscolegà",
|
"pad.modals.disconnected": "A l'é stàit dëscolegà",
|
||||||
"pad.modals.disconnected.explanation": "La conession al servent a l'é perdusse",
|
"pad.modals.disconnected.explanation": "La conession al servent a l'é perdusse",
|
||||||
"pad.modals.disconnected.cause": "Ël servent a podrìa esse indisponìbil. Për piasì, ch'a anforma l'aministrator dël servissi si ël problema a persist.",
|
"pad.modals.disconnected.cause": "Ël servent a podrìa esse indisponìbil. Për piasì, ch'a anforma l'aministrator dël servissi si ël problema a persist.",
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"Lpagliari",
|
"Lpagliari",
|
||||||
"Luckas",
|
"Luckas",
|
||||||
"Macofe",
|
"Macofe",
|
||||||
|
"Nsharkey",
|
||||||
"Prilopes",
|
"Prilopes",
|
||||||
"Rafaelff",
|
"Rafaelff",
|
||||||
"Rodrigo codignoli",
|
"Rodrigo codignoli",
|
||||||
|
@ -94,6 +95,7 @@
|
||||||
"pad.modals.deleted.explanation": "Esta nota foi removida.",
|
"pad.modals.deleted.explanation": "Esta nota foi removida.",
|
||||||
"pad.modals.rateLimited": "Limitado.",
|
"pad.modals.rateLimited": "Limitado.",
|
||||||
"pad.modals.rateLimited.explanation": "Você enviou muitas mensagens para este pad por isso será desconectado.",
|
"pad.modals.rateLimited.explanation": "Você enviou muitas mensagens para este pad por isso será desconectado.",
|
||||||
|
"pad.modals.rejected.explanation": "O servidor rejeitou uma mensagem que foi enviada pelo seu navegador.",
|
||||||
"pad.modals.disconnected": "Você foi desconectado.",
|
"pad.modals.disconnected": "Você foi desconectado.",
|
||||||
"pad.modals.disconnected.explanation": "A conexão com o servidor foi perdida",
|
"pad.modals.disconnected.explanation": "A conexão com o servidor foi perdida",
|
||||||
"pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador caso isso continue.",
|
"pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador caso isso continue.",
|
||||||
|
|
|
@ -83,6 +83,8 @@
|
||||||
"pad.modals.deleted.explanation": "Detta block har tagits bort.",
|
"pad.modals.deleted.explanation": "Detta block har tagits bort.",
|
||||||
"pad.modals.rateLimited": "Begränsad frekvens.",
|
"pad.modals.rateLimited": "Begränsad frekvens.",
|
||||||
"pad.modals.rateLimited.explanation": "Du skickade för många meddelanden till detta block så du blev frånkopplad.",
|
"pad.modals.rateLimited.explanation": "Du skickade för många meddelanden till detta block så du blev frånkopplad.",
|
||||||
|
"pad.modals.rejected.explanation": "Servern avvisade ett meddelande som skickades av din webbläsare.",
|
||||||
|
"pad.modals.rejected.cause": "Servern kan ha uppdaterats medan du visade blocket, eller så finns det kanske en bugg i Etherpad. Försök att ladda om sidan.",
|
||||||
"pad.modals.disconnected": "Du har blivit frånkopplad.",
|
"pad.modals.disconnected": "Du har blivit frånkopplad.",
|
||||||
"pad.modals.disconnected.explanation": "Anslutningen till servern avbröts",
|
"pad.modals.disconnected.explanation": "Anslutningen till servern avbröts",
|
||||||
"pad.modals.disconnected.cause": "Servern kanske är otillgänglig. Var god meddela tjänstadministratören om detta fortsätter att hända.",
|
"pad.modals.disconnected.cause": "Servern kanske är otillgänglig. Var god meddela tjänstadministratören om detta fortsätter att hända.",
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"Joseph",
|
"Joseph",
|
||||||
"McAang",
|
"McAang",
|
||||||
"Meelo",
|
"Meelo",
|
||||||
|
"MuratTheTurkish",
|
||||||
"Trockya",
|
"Trockya",
|
||||||
"Vito Genovese"
|
"Vito Genovese"
|
||||||
]
|
]
|
||||||
|
@ -88,6 +89,8 @@
|
||||||
"pad.modals.deleted.explanation": "Bu bloknot kaldırılmış.",
|
"pad.modals.deleted.explanation": "Bu bloknot kaldırılmış.",
|
||||||
"pad.modals.rateLimited": "Oran Sınırlı.",
|
"pad.modals.rateLimited": "Oran Sınırlı.",
|
||||||
"pad.modals.rateLimited.explanation": "Bu pad'e çok fazla mesaj gönderdiniz, böylece bağlantı kesildi.",
|
"pad.modals.rateLimited.explanation": "Bu pad'e çok fazla mesaj gönderdiniz, böylece bağlantı kesildi.",
|
||||||
|
"pad.modals.rejected.explanation": "Sunucu, tarayıcınız tarafından gönderilen bir mesajı reddetti.",
|
||||||
|
"pad.modals.rejected.cause": "Siz pedi görüntülerken sunucu güncellenmiş olabilir veya Etherpad'de bir hata olabilir. Sayfayı yeniden yüklemeyi deneyin.",
|
||||||
"pad.modals.disconnected": "Bağlantınız koptu.",
|
"pad.modals.disconnected": "Bağlantınız koptu.",
|
||||||
"pad.modals.disconnected.explanation": "Sunucu bağlantısı kaybedildi",
|
"pad.modals.disconnected.explanation": "Sunucu bağlantısı kaybedildi",
|
||||||
"pad.modals.disconnected.cause": "Sunucu kullanılamıyor olabilir. Bunun devam etmesi durumunda servis yöneticisine bildirin.",
|
"pad.modals.disconnected.cause": "Sunucu kullanılamıyor olabilir. Bunun devam etmesi durumunda servis yöneticisine bildirin.",
|
||||||
|
|
|
@ -87,6 +87,8 @@
|
||||||
"pad.modals.deleted.explanation": "此記事本已被移除。",
|
"pad.modals.deleted.explanation": "此記事本已被移除。",
|
||||||
"pad.modals.rateLimited": "比例限制。",
|
"pad.modals.rateLimited": "比例限制。",
|
||||||
"pad.modals.rateLimited.explanation": "您發送太多訊息到此記事本,因此中斷了您的連結。",
|
"pad.modals.rateLimited.explanation": "您發送太多訊息到此記事本,因此中斷了您的連結。",
|
||||||
|
"pad.modals.rejected.explanation": "伺服器拒絕了由您的瀏覽器發送的訊息。",
|
||||||
|
"pad.modals.rejected.cause": "當您在檢視記事本時伺服器可能正在更新,或是在 Etherpad 裡有臭蟲。請嘗試重新載入頁面。",
|
||||||
"pad.modals.disconnected": "您已中斷連線。",
|
"pad.modals.disconnected": "您已中斷連線。",
|
||||||
"pad.modals.disconnected.explanation": "伺服器連接曾中斷",
|
"pad.modals.disconnected.explanation": "伺服器連接曾中斷",
|
||||||
"pad.modals.disconnected.cause": "伺服器可能無法使用。若此情況持續發生,請通知伺服器管理員。",
|
"pad.modals.disconnected.cause": "伺服器可能無法使用。若此情況持續發生,請通知伺服器管理員。",
|
||||||
|
|
|
@ -53,7 +53,7 @@ const rateLimiter = new RateLimiterMemory({
|
||||||
* readonlyPadId = The readonly pad id of the pad
|
* readonlyPadId = The readonly pad id of the pad
|
||||||
* readonly = Wether the client has only read access (true) or read/write access (false)
|
* readonly = Wether the client has only read access (true) or read/write access (false)
|
||||||
* rev = That last revision that was send to this client
|
* rev = That last revision that was send to this client
|
||||||
* author = the author name of this session
|
* author = the author ID used for this session
|
||||||
*/
|
*/
|
||||||
var sessioninfos = {};
|
var sessioninfos = {};
|
||||||
exports.sessioninfos = sessioninfos;
|
exports.sessioninfos = sessioninfos;
|
||||||
|
@ -219,7 +219,7 @@ exports.handleMessage = async function(client, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {session: {user} = {}} = client.client.request;
|
const {session: {user} = {}} = client.client.request;
|
||||||
const {accessStatus} =
|
const {accessStatus, authorID} =
|
||||||
await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password, user);
|
await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password, user);
|
||||||
|
|
||||||
if (accessStatus !== "grant") {
|
if (accessStatus !== "grant") {
|
||||||
|
@ -227,6 +227,19 @@ exports.handleMessage = async function(client, message)
|
||||||
client.json.send({ accessStatus });
|
client.json.send({ accessStatus });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (thisSession.author != null && thisSession.author !== authorID) {
|
||||||
|
messageLogger.warn(
|
||||||
|
'Rejecting message from client because the author ID changed mid-session.' +
|
||||||
|
' Bad or missing token or sessionID?' +
|
||||||
|
` socket:${client.id}` +
|
||||||
|
` IP:${settings.disableIPlogging ? ANONYMOUS : remoteAddress[client.id]}` +
|
||||||
|
` originalAuthorID:${thisSession.author}` +
|
||||||
|
` newAuthorID:${authorID}` +
|
||||||
|
` message:${message}`);
|
||||||
|
client.json.send({disconnect: 'rejected'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thisSession.author = authorID;
|
||||||
|
|
||||||
// Allow plugins to bypass the readonly message blocker
|
// Allow plugins to bypass the readonly message blocker
|
||||||
if ((await hooks.aCallAll('handleMessageSecurity', {client, message})).some((w) => w === true)) {
|
if ((await hooks.aCallAll('handleMessageSecurity', {client, message})).some((w) => w === true)) {
|
||||||
|
@ -246,9 +259,9 @@ exports.handleMessage = async function(client, message)
|
||||||
|
|
||||||
// Check what type of message we get and delegate to the other methods
|
// Check what type of message we get and delegate to the other methods
|
||||||
if (message.type === "CLIENT_READY") {
|
if (message.type === "CLIENT_READY") {
|
||||||
handleClientReady(client, message);
|
await handleClientReady(client, message);
|
||||||
} else if (message.type === "CHANGESET_REQ") {
|
} else if (message.type === "CHANGESET_REQ") {
|
||||||
handleChangesetRequest(client, message);
|
await handleChangesetRequest(client, message);
|
||||||
} else if(message.type === "COLLABROOM") {
|
} else if(message.type === "COLLABROOM") {
|
||||||
if (thisSession.readonly) {
|
if (thisSession.readonly) {
|
||||||
messageLogger.warn("Dropped message, COLLABROOM for readonly pad");
|
messageLogger.warn("Dropped message, COLLABROOM for readonly pad");
|
||||||
|
@ -256,13 +269,13 @@ exports.handleMessage = async function(client, message)
|
||||||
stats.counter('pendingEdits').inc()
|
stats.counter('pendingEdits').inc()
|
||||||
padChannels.emit(message.padId, {client: client, message: message}); // add to pad queue
|
padChannels.emit(message.padId, {client: client, message: message}); // add to pad queue
|
||||||
} else if (message.data.type === "USERINFO_UPDATE") {
|
} else if (message.data.type === "USERINFO_UPDATE") {
|
||||||
handleUserInfoUpdate(client, message);
|
await handleUserInfoUpdate(client, message);
|
||||||
} else if (message.data.type === "CHAT_MESSAGE") {
|
} else if (message.data.type === "CHAT_MESSAGE") {
|
||||||
handleChatMessage(client, message);
|
await handleChatMessage(client, message);
|
||||||
} else if (message.data.type === "GET_CHAT_MESSAGES") {
|
} else if (message.data.type === "GET_CHAT_MESSAGES") {
|
||||||
handleGetChatMessages(client, message);
|
await handleGetChatMessages(client, message);
|
||||||
} else if (message.data.type === "SAVE_REVISION") {
|
} else if (message.data.type === "SAVE_REVISION") {
|
||||||
handleSaveRevisionMessage(client, message);
|
await handleSaveRevisionMessage(client, message);
|
||||||
} else if (message.data.type === "CLIENT_MESSAGE" &&
|
} else if (message.data.type === "CLIENT_MESSAGE" &&
|
||||||
message.data.payload != null &&
|
message.data.payload != null &&
|
||||||
message.data.payload.type === "suggestUserName") {
|
message.data.payload.type === "suggestUserName") {
|
||||||
|
@ -271,7 +284,7 @@ exports.handleMessage = async function(client, message)
|
||||||
messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type);
|
messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type);
|
||||||
}
|
}
|
||||||
} else if(message.type === "SWITCH_TO_PAD") {
|
} else if(message.type === "SWITCH_TO_PAD") {
|
||||||
handleSwitchToPad(client, message);
|
await handleSwitchToPad(client, message);
|
||||||
} else {
|
} else {
|
||||||
messageLogger.warn("Dropped message, unknown Message Type " + message.type);
|
messageLogger.warn("Dropped message, unknown Message Type " + message.type);
|
||||||
}
|
}
|
||||||
|
@ -334,14 +347,14 @@ exports.handleCustomMessage = function(padID, msgString) {
|
||||||
* @param client the client that send this message
|
* @param client the client that send this message
|
||||||
* @param message the message from the client
|
* @param message the message from the client
|
||||||
*/
|
*/
|
||||||
function handleChatMessage(client, message)
|
async function handleChatMessage(client, message)
|
||||||
{
|
{
|
||||||
var time = Date.now();
|
var time = Date.now();
|
||||||
var userId = sessioninfos[client.id].author;
|
var userId = sessioninfos[client.id].author;
|
||||||
var text = message.data.text;
|
var text = message.data.text;
|
||||||
var padId = sessioninfos[client.id].padId;
|
var padId = sessioninfos[client.id].padId;
|
||||||
|
|
||||||
exports.sendChatMessageToPadClients(time, userId, text, padId);
|
await exports.sendChatMessageToPadClients(time, userId, text, padId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -450,7 +463,7 @@ function handleSuggestUserName(client, message)
|
||||||
* @param client the client that send this message
|
* @param client the client that send this message
|
||||||
* @param message the message from the client
|
* @param message the message from the client
|
||||||
*/
|
*/
|
||||||
function handleUserInfoUpdate(client, message)
|
async function handleUserInfoUpdate(client, message)
|
||||||
{
|
{
|
||||||
// check if all ok
|
// check if all ok
|
||||||
if (message.data.userInfo == null) {
|
if (message.data.userInfo == null) {
|
||||||
|
@ -481,8 +494,10 @@ function handleUserInfoUpdate(client, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell the authorManager about the new attributes
|
// Tell the authorManager about the new attributes
|
||||||
authorManager.setAuthorColorId(author, message.data.userInfo.colorId);
|
const p = Promise.all([
|
||||||
authorManager.setAuthorName(author, message.data.userInfo.name);
|
authorManager.setAuthorColorId(author, message.data.userInfo.colorId),
|
||||||
|
authorManager.setAuthorName(author, message.data.userInfo.name),
|
||||||
|
]);
|
||||||
|
|
||||||
var padId = session.padId;
|
var padId = session.padId;
|
||||||
|
|
||||||
|
@ -504,6 +519,9 @@ function handleUserInfoUpdate(client, message)
|
||||||
|
|
||||||
// Send the other clients on the pad the update message
|
// Send the other clients on the pad the update message
|
||||||
client.broadcast.to(padId).json.send(infoMsg);
|
client.broadcast.to(padId).json.send(infoMsg);
|
||||||
|
|
||||||
|
// Block until the authorManager has stored the new attributes.
|
||||||
|
await p;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -800,7 +818,7 @@ function _correctMarkersInPad(atext, apool) {
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSwitchToPad(client, message)
|
async function handleSwitchToPad(client, message)
|
||||||
{
|
{
|
||||||
// clear the session and leave the room
|
// clear the session and leave the room
|
||||||
const currentSessionInfo = sessioninfos[client.id];
|
const currentSessionInfo = sessioninfos[client.id];
|
||||||
|
@ -817,7 +835,7 @@ function handleSwitchToPad(client, message)
|
||||||
// start up the new pad
|
// start up the new pad
|
||||||
const newSessionInfo = sessioninfos[client.id];
|
const newSessionInfo = sessioninfos[client.id];
|
||||||
createSessionInfoAuth(newSessionInfo, message);
|
createSessionInfoAuth(newSessionInfo, message);
|
||||||
handleClientReady(client, message);
|
await handleClientReady(client, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates/replaces the auth object in the given session info.
|
// Creates/replaces the auth object in the given session info.
|
||||||
|
@ -1124,8 +1142,6 @@ async function handleClientReady(client, message)
|
||||||
// Save the current revision in sessioninfos, should be the same as in clientVars
|
// Save the current revision in sessioninfos, should be the same as in clientVars
|
||||||
sessionInfo.rev = pad.getHeadRevisionNumber();
|
sessionInfo.rev = pad.getHeadRevisionNumber();
|
||||||
|
|
||||||
sessionInfo.author = authorID;
|
|
||||||
|
|
||||||
// prepare the notification for the other users on the pad, that this user joined
|
// prepare the notification for the other users on the pad, that this user joined
|
||||||
let messageToTheOtherUsers = {
|
let messageToTheOtherUsers = {
|
||||||
"type": "COLLABROOM",
|
"type": "COLLABROOM",
|
||||||
|
|
|
@ -87,7 +87,7 @@ exports.setSocketIO = function(_socket) {
|
||||||
|
|
||||||
if (clientAuthorized) {
|
if (clientAuthorized) {
|
||||||
// client is authorized, everything ok
|
// client is authorized, everything ok
|
||||||
handleMessage(client, message);
|
await handleMessage(client, message);
|
||||||
} else {
|
} else {
|
||||||
// try to authorize the client
|
// try to authorize the client
|
||||||
if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) {
|
if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) {
|
||||||
|
@ -104,7 +104,7 @@ exports.setSocketIO = function(_socket) {
|
||||||
if (accessStatus === "grant") {
|
if (accessStatus === "grant") {
|
||||||
// access was granted, mark the client as authorized and handle the message
|
// access was granted, mark the client as authorized and handle the message
|
||||||
clientAuthorized = true;
|
clientAuthorized = true;
|
||||||
handleMessage(client, message);
|
await handleMessage(client, message);
|
||||||
} else {
|
} else {
|
||||||
// no access, send the client a message that tells him why
|
// no access, send the client a message that tells him why
|
||||||
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
|
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
|
||||||
|
@ -127,13 +127,13 @@ exports.setSocketIO = function(_socket) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to handle the message of this client
|
// try to handle the message of this client
|
||||||
function handleMessage(client, message)
|
async function handleMessage(client, message)
|
||||||
{
|
{
|
||||||
if (message.component && components[message.component]) {
|
if (message.component && components[message.component]) {
|
||||||
// check if component is registered in the components array
|
// check if component is registered in the components array
|
||||||
if (components[message.component]) {
|
if (components[message.component]) {
|
||||||
messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message));
|
messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message));
|
||||||
components[message.component].handleMessage(client, message);
|
await components[message.component].handleMessage(client, message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message));
|
messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message));
|
||||||
|
|
|
@ -8,6 +8,7 @@ var padMessageHandler = require("../../handler/PadMessageHandler");
|
||||||
|
|
||||||
var cookieParser = require('cookie-parser');
|
var cookieParser = require('cookie-parser');
|
||||||
var sessionModule = require('express-session');
|
var sessionModule = require('express-session');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
//init socket.io and redirect all requests to the MessageHandler
|
//init socket.io and redirect all requests to the MessageHandler
|
||||||
|
@ -48,32 +49,34 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
// check whether the user has authenticated, then any random person on the Internet can read,
|
// check whether the user has authenticated, then any random person on the Internet can read,
|
||||||
// modify, or create any pad (unless the pad is password protected or an HTTP API session is
|
// modify, or create any pad (unless the pad is password protected or an HTTP API session is
|
||||||
// required).
|
// required).
|
||||||
var cookieParserFn = cookieParser(webaccess.secret, {});
|
const cookieParserFn = util.promisify(cookieParser(webaccess.secret, {}));
|
||||||
io.use((socket, next) => {
|
const getSession = util.promisify(args.app.sessionStore.get).bind(args.app.sessionStore);
|
||||||
var data = socket.request;
|
io.use(async (socket, next) => {
|
||||||
if (!data.headers.cookie) {
|
const req = socket.request;
|
||||||
|
if (!req.headers.cookie) {
|
||||||
// socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the
|
// socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the
|
||||||
// token and express_sid cookies have to be passed via a query parameter for unit tests.
|
// token and express_sid cookies have to be passed via a query parameter for unit tests.
|
||||||
data.headers.cookie = socket.handshake.query.cookie;
|
req.headers.cookie = socket.handshake.query.cookie;
|
||||||
}
|
}
|
||||||
if (!data.headers.cookie && settings.loadTest) {
|
if (!req.headers.cookie && settings.loadTest) {
|
||||||
console.warn('bypassing socket.io authentication check due to settings.loadTest');
|
console.warn('bypassing socket.io authentication check due to settings.loadTest');
|
||||||
return next(null, true);
|
return next(null, true);
|
||||||
}
|
}
|
||||||
const fail = (msg) => { return next(new Error(msg), false); };
|
try {
|
||||||
cookieParserFn(data, {}, function(err) {
|
await cookieParserFn(req, {});
|
||||||
if (err) return fail('access denied: unable to parse express_sid cookie');
|
const expressSid = req.signedCookies.express_sid;
|
||||||
const expressSid = data.signedCookies.express_sid;
|
const needAuthn = settings.requireAuthentication;
|
||||||
if (!expressSid) return fail ('access denied: signed express_sid cookie is required');
|
if (needAuthn && !expressSid) throw new Error('signed express_sid cookie is required');
|
||||||
args.app.sessionStore.get(expressSid, (err, session) => {
|
if (expressSid) {
|
||||||
if (err || !session) return fail('access denied: bad session or session has expired');
|
const session = await getSession(expressSid);
|
||||||
data.session = new sessionModule.Session(data, session);
|
if (!session) throw new Error('bad session or session has expired');
|
||||||
if (settings.requireAuthentication && data.session.user == null) {
|
req.session = new sessionModule.Session(req, session);
|
||||||
return fail('access denied: authentication required');
|
if (needAuthn && req.session.user == null) throw new Error('authentication required');
|
||||||
}
|
}
|
||||||
next(null, true);
|
} catch (err) {
|
||||||
});
|
return next(new Error(`access denied: ${err}`), false);
|
||||||
});
|
}
|
||||||
|
return next(null, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// var socketIOLogger = log4js.getLogger("socket.io");
|
// var socketIOLogger = log4js.getLogger("socket.io");
|
||||||
|
|
|
@ -39,12 +39,13 @@ exports.checkAccess = (req, res, next) => {
|
||||||
if (!level) return fail();
|
if (!level) return fail();
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
if (user == null) return next(); // This will happen if authentication is not required.
|
if (user == null) return next(); // This will happen if authentication is not required.
|
||||||
const padID = (req.path.match(/^\/p\/(.*)$/) || [])[1];
|
const encodedPadId = (req.path.match(/^\/p\/(.*)$/) || [])[1];
|
||||||
if (padID == null) return next();
|
if (encodedPadId == null) return next();
|
||||||
|
const padId = decodeURIComponent(encodedPadId);
|
||||||
// The user was granted access to a pad. Remember the authorization level in the user's
|
// The user was granted access to a pad. Remember the authorization level in the user's
|
||||||
// settings so that SecurityManager can approve or deny specific actions.
|
// settings so that SecurityManager can approve or deny specific actions.
|
||||||
if (user.padAuthorizations == null) user.padAuthorizations = {};
|
if (user.padAuthorizations == null) user.padAuthorizations = {};
|
||||||
user.padAuthorizations[padID] = level;
|
user.padAuthorizations[padId] = level;
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -58,19 +59,6 @@ exports.checkAccess = (req, res, next) => {
|
||||||
hooks.aCallFirst('authorize', {req, res, next, resource: req.path}, hookResultMangle(grant));
|
hooks.aCallFirst('authorize', {req, res, next, resource: req.path}, hookResultMangle(grant));
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Authentication OR authorization failed. */
|
|
||||||
const failure = () => {
|
|
||||||
return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => {
|
|
||||||
if (ok) return;
|
|
||||||
// No plugin handled the authn/authz failure. Fall back to basic authentication.
|
|
||||||
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
|
||||||
// Delay the error response for 1s to slow down brute force attacks.
|
|
||||||
setTimeout(() => {
|
|
||||||
res.status(401).send('Authentication Required');
|
|
||||||
}, 1000);
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Access checking is done in three steps:
|
// Access checking is done in three steps:
|
||||||
//
|
//
|
||||||
// 1) Try to just access the thing. If access fails (perhaps authentication has not yet completed,
|
// 1) Try to just access the thing. If access fails (perhaps authentication has not yet completed,
|
||||||
|
@ -78,7 +66,7 @@ exports.checkAccess = (req, res, next) => {
|
||||||
// 2) Try to authenticate. (Or, if already logged in, reauthenticate with different credentials if
|
// 2) Try to authenticate. (Or, if already logged in, reauthenticate with different credentials if
|
||||||
// supported by the authn scheme.) If authentication fails, give the user a 401 error to
|
// supported by the authn scheme.) If authentication fails, give the user a 401 error to
|
||||||
// request new credentials. Otherwise, go to the next step.
|
// request new credentials. Otherwise, go to the next step.
|
||||||
// 3) Try to access the thing again. If this fails, give the user a 401 error.
|
// 3) Try to access the thing again. If this fails, give the user a 403 error.
|
||||||
//
|
//
|
||||||
// Plugins can use the 'next' callback (from the hook's context) to break out at any point (e.g.,
|
// Plugins can use the 'next' callback (from the hook's context) to break out at any point (e.g.,
|
||||||
// to process an OAuth callback). Plugins can use the authFailure hook to override the default
|
// to process an OAuth callback). Plugins can use the authFailure hook to override the default
|
||||||
|
@ -103,6 +91,17 @@ exports.checkAccess = (req, res, next) => {
|
||||||
}
|
}
|
||||||
hooks.aCallFirst('authenticate', ctx, hookResultMangle((ok) => {
|
hooks.aCallFirst('authenticate', ctx, hookResultMangle((ok) => {
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
const failure = () => {
|
||||||
|
return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => {
|
||||||
|
if (ok) return;
|
||||||
|
// No plugin handled the authentication failure. Fall back to basic authentication.
|
||||||
|
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
||||||
|
// Delay the error response for 1s to slow down brute force attacks.
|
||||||
|
setTimeout(() => {
|
||||||
|
res.status(401).send('Authentication Required');
|
||||||
|
}, 1000);
|
||||||
|
}));
|
||||||
|
};
|
||||||
// Fall back to HTTP basic auth.
|
// Fall back to HTTP basic auth.
|
||||||
if (!httpBasicAuth) return failure();
|
if (!httpBasicAuth) return failure();
|
||||||
if (!(ctx.username in settings.users)) {
|
if (!(ctx.username in settings.users)) {
|
||||||
|
@ -126,7 +125,13 @@ exports.checkAccess = (req, res, next) => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
step3Authorize = () => authorize(failure);
|
step3Authorize = () => authorize(() => {
|
||||||
|
return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => {
|
||||||
|
if (ok) return;
|
||||||
|
// No plugin handled the authorization failure.
|
||||||
|
res.status(403).send('Forbidden');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
step1PreAuthenticate();
|
step1PreAuthenticate();
|
||||||
};
|
};
|
||||||
|
|
|
@ -45,7 +45,7 @@ let started = false;
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
|
|
||||||
exports.start = async () => {
|
exports.start = async () => {
|
||||||
if (started) return;
|
if (started) return express.server;
|
||||||
started = true;
|
started = true;
|
||||||
if (stopped) throw new Error('restart not supported');
|
if (stopped) throw new Error('restart not supported');
|
||||||
|
|
||||||
|
|
71
src/package-lock.json
generated
71
src/package-lock.json
generated
|
@ -8235,6 +8235,77 @@
|
||||||
"requires": {
|
"requires": {
|
||||||
"methods": "^1.1.2",
|
"methods": "^1.1.2",
|
||||||
"superagent": "^3.8.3"
|
"superagent": "^3.8.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "2.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||||
|
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"superagent": {
|
||||||
|
"version": "3.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||||
|
"integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"component-emitter": "^1.2.0",
|
||||||
|
"cookiejar": "^2.1.0",
|
||||||
|
"debug": "^3.1.0",
|
||||||
|
"extend": "^3.0.0",
|
||||||
|
"form-data": "^2.3.1",
|
||||||
|
"formidable": "^1.2.0",
|
||||||
|
"methods": "^1.1.1",
|
||||||
|
"mime": "^1.4.1",
|
||||||
|
"qs": "^6.5.1",
|
||||||
|
"readable-stream": "^2.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
"mocha-froth": "^0.2.10",
|
"mocha-froth": "^0.2.10",
|
||||||
"nyc": "15.0.1",
|
"nyc": "15.0.1",
|
||||||
"set-cookie-parser": "^2.4.6",
|
"set-cookie-parser": "^2.4.6",
|
||||||
|
"superagent": "^3.8.3",
|
||||||
"supertest": "4.0.2",
|
"supertest": "4.0.2",
|
||||||
"wd": "1.12.1"
|
"wd": "1.12.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,15 +2,14 @@
|
||||||
* Import and Export tests for the /p/whateverPadId/import and /p/whateverPadId/export endpoints.
|
* Import and Export tests for the /p/whateverPadId/import and /p/whateverPadId/export endpoints.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert').strict;
|
||||||
|
const superagent = require(__dirname+'/../../../../src/node_modules/superagent');
|
||||||
const supertest = require(__dirname+'/../../../../src/node_modules/supertest');
|
const supertest = require(__dirname+'/../../../../src/node_modules/supertest');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const settings = require(__dirname+'/../../../../src/node/utils/Settings');
|
const settings = require(__dirname+'/../../../../src/node/utils/Settings');
|
||||||
const host = 'http://127.0.0.1:'+settings.port;
|
const host = 'http://127.0.0.1:'+settings.port;
|
||||||
const api = supertest('http://'+settings.ip+":"+settings.port);
|
const agent = supertest(`http://${settings.ip}:${settings.port}`);
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const async = require(__dirname+'/../../../../src/node_modules/async');
|
|
||||||
const request = require(__dirname+'/../../../../src/node_modules/request');
|
|
||||||
const padText = fs.readFileSync("../tests/backend/specs/api/test.txt");
|
const padText = fs.readFileSync("../tests/backend/specs/api/test.txt");
|
||||||
const etherpadDoc = fs.readFileSync("../tests/backend/specs/api/test.etherpad");
|
const etherpadDoc = fs.readFileSync("../tests/backend/specs/api/test.etherpad");
|
||||||
const wordDoc = fs.readFileSync("../tests/backend/specs/api/test.doc");
|
const wordDoc = fs.readFileSync("../tests/backend/specs/api/test.doc");
|
||||||
|
@ -23,26 +22,20 @@ var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'});
|
||||||
apiKey = apiKey.replace(/\n$/, "");
|
apiKey = apiKey.replace(/\n$/, "");
|
||||||
var apiVersion = 1;
|
var apiVersion = 1;
|
||||||
var testPadId = makeid();
|
var testPadId = makeid();
|
||||||
var lastEdited = "";
|
|
||||||
var text = generateLongText();
|
|
||||||
|
|
||||||
describe('Connectivity', function(){
|
describe('Connectivity', function(){
|
||||||
it('can connect', function(done) {
|
it('can connect', async function() {
|
||||||
api.get('/api/')
|
await agent.get('/api/')
|
||||||
.expect('Content-Type', /json/)
|
.expect(200)
|
||||||
.expect(200, done)
|
.expect('Content-Type', /json/);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('API Versioning', function(){
|
describe('API Versioning', function(){
|
||||||
it('finds the version tag', function(done) {
|
it('finds the version tag', async function() {
|
||||||
api.get('/api/')
|
await agent.get('/api/')
|
||||||
.expect(function(res){
|
.expect(200)
|
||||||
apiVersion = res.body.currentVersion;
|
.expect((res) => assert(res.body.currentVersion));
|
||||||
if (!res.body.currentVersion) throw new Error("No version set in API");
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
.expect(200, done)
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -73,289 +66,144 @@ Example Curl command for testing import URI:
|
||||||
*/
|
*/
|
||||||
|
|
||||||
describe('Imports and Exports', function(){
|
describe('Imports and Exports', function(){
|
||||||
it('creates a new Pad, imports content to it, checks that content', function(done) {
|
before(function() {
|
||||||
if(!settings.allowAnyoneToImport){
|
if (!settings.allowAnyoneToImport) {
|
||||||
console.warn("not anyone can import so not testing -- to include this test set allowAnyoneToImport to true in settings.json");
|
console.warn('not anyone can import so not testing -- ' +
|
||||||
done();
|
'to include this test set allowAnyoneToImport to true in settings.json');
|
||||||
}else{
|
this.skip();
|
||||||
api.get(endPoint('createPad')+"&padID="+testPadId)
|
|
||||||
.expect(function(res){
|
|
||||||
if(res.body.code !== 0) throw new Error("Unable to create new Pad");
|
|
||||||
|
|
||||||
var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
throw new Error("Failed to import", err);
|
|
||||||
} else {
|
|
||||||
api.get(endPoint('getText')+"&padID="+testPadId)
|
|
||||||
.expect(function(res){
|
|
||||||
if(res.body.data.text !== padText.toString()){
|
|
||||||
throw new Error("text is wrong on export");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let form = req.form();
|
it('creates a new Pad, imports content to it, checks that content', async function() {
|
||||||
|
await agent.get(endPoint('createPad') + `&padID=${testPadId}`)
|
||||||
form.append('file', padText, {
|
.expect(200)
|
||||||
filename: '/test.txt',
|
|
||||||
contentType: 'text/plain'
|
|
||||||
});
|
|
||||||
|
|
||||||
})
|
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200, done)
|
.expect((res) => assert.equal(res.body.code, 0));
|
||||||
|
await agent.post(`/p/${testPadId}/import`)
|
||||||
|
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
|
||||||
|
.expect(200);
|
||||||
|
await agent.get(endPoint('getText') + `&padID=${testPadId}`)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => assert.equal(res.body.data.text, padText.toString()));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Import/Export tests requiring AbiWord/LibreOffice', function() {
|
||||||
|
before(function() {
|
||||||
|
if ((!settings.abiword || settings.abiword.indexOf('/') === -1) &&
|
||||||
|
(!settings.soffice || settings.soffice.indexOf('/') === -1)) {
|
||||||
|
this.skip();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// For some reason word import does not work in testing..
|
// For some reason word import does not work in testing..
|
||||||
// TODO: fix support for .doc files..
|
// TODO: fix support for .doc files..
|
||||||
it('Tries to import .doc that uses soffice or abiword', function(done) {
|
it('Tries to import .doc that uses soffice or abiword', async function() {
|
||||||
if(!settings.allowAnyoneToImport) return done();
|
await agent.post(`/p/${testPadId}/import`)
|
||||||
if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done();
|
.attach('file', wordDoc, {filename: '/test.doc', contentType: 'application/msword'})
|
||||||
|
.expect(200)
|
||||||
var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) {
|
.expect(/FrameCall\('undefined', 'ok'\);/);
|
||||||
if (err) {
|
|
||||||
throw new Error("Failed to import", err);
|
|
||||||
} else {
|
|
||||||
if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){
|
|
||||||
throw new Error("Failed DOC import", testPadId);
|
|
||||||
}else{
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let form = req.form();
|
it('exports DOC', async function() {
|
||||||
form.append('file', wordDoc, {
|
await agent.get(`/p/${testPadId}/export/doc`)
|
||||||
filename: '/test.doc',
|
.buffer(true).parse(superagent.parse['application/octet-stream'])
|
||||||
contentType: 'application/msword'
|
.expect(200)
|
||||||
});
|
.expect((res) => assert(res.body.length >= 9000));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exports DOC', function(done) {
|
it('Tries to import .docx that uses soffice or abiword', async function() {
|
||||||
if(!settings.allowAnyoneToImport) return done();
|
await agent.post(`/p/${testPadId}/import`)
|
||||||
if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done();
|
.attach('file', wordXDoc, {
|
||||||
try{
|
|
||||||
request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) {
|
|
||||||
// TODO: At some point checking that the contents is correct would be suitable
|
|
||||||
if(body.length >= 9000){
|
|
||||||
done();
|
|
||||||
}else{
|
|
||||||
throw new Error("Word Document export length is not right");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}catch(e){
|
|
||||||
throw new Error(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Tries to import .docx that uses soffice or abiword', function(done) {
|
|
||||||
if(!settings.allowAnyoneToImport) return done();
|
|
||||||
if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done();
|
|
||||||
|
|
||||||
var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
throw new Error("Failed to import", err);
|
|
||||||
} else {
|
|
||||||
if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){
|
|
||||||
throw new Error("Failed DOCX import");
|
|
||||||
}else{
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let form = req.form();
|
|
||||||
form.append('file', wordXDoc, {
|
|
||||||
filename: '/test.docx',
|
filename: '/test.docx',
|
||||||
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exports DOC from imported DOCX', function(done) {
|
|
||||||
if(!settings.allowAnyoneToImport) return done();
|
|
||||||
if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done();
|
|
||||||
request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) {
|
|
||||||
// TODO: At some point checking that the contents is correct would be suitable
|
|
||||||
if(body.length >= 9100){
|
|
||||||
done();
|
|
||||||
}else{
|
|
||||||
throw new Error("Word Document export length is not right");
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
.expect(200)
|
||||||
|
.expect(/FrameCall\('undefined', 'ok'\);/);
|
||||||
it('Tries to import .pdf that uses soffice or abiword', function(done) {
|
|
||||||
if(!settings.allowAnyoneToImport) return done();
|
|
||||||
if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done();
|
|
||||||
|
|
||||||
var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
throw new Error("Failed to import", err);
|
|
||||||
} else {
|
|
||||||
if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){
|
|
||||||
throw new Error("Failed PDF import");
|
|
||||||
}else{
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let form = req.form();
|
it('exports DOC from imported DOCX', async function() {
|
||||||
form.append('file', pdfDoc, {
|
await agent.get(`/p/${testPadId}/export/doc`)
|
||||||
filename: '/test.pdf',
|
.buffer(true).parse(superagent.parse['application/octet-stream'])
|
||||||
contentType: 'application/pdf'
|
.expect(200)
|
||||||
});
|
.expect((res) => assert(res.body.length >= 9100));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exports PDF', function(done) {
|
it('Tries to import .pdf that uses soffice or abiword', async function() {
|
||||||
if(!settings.allowAnyoneToImport) return done();
|
await agent.post(`/p/${testPadId}/import`)
|
||||||
if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done();
|
.attach('file', pdfDoc, {filename: '/test.pdf', contentType: 'application/pdf'})
|
||||||
request(host + '/p/'+testPadId+'/export/pdf', function (err, res, body) {
|
.expect(200)
|
||||||
// TODO: At some point checking that the contents is correct would be suitable
|
.expect(/FrameCall\('undefined', 'ok'\);/);
|
||||||
if(body.length >= 1000){
|
|
||||||
done();
|
|
||||||
}else{
|
|
||||||
throw new Error("PDF Document export length is not right");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Tries to import .odt that uses soffice or abiword', function(done) {
|
|
||||||
if(!settings.allowAnyoneToImport) return done();
|
|
||||||
if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done();
|
|
||||||
|
|
||||||
var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
throw new Error("Failed to import", err);
|
|
||||||
} else {
|
|
||||||
if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){
|
|
||||||
throw new Error("Failed ODT import", testPadId);
|
|
||||||
}else{
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let form = req.form();
|
it('exports PDF', async function() {
|
||||||
form.append('file', odtDoc, {
|
await agent.get(`/p/${testPadId}/export/pdf`)
|
||||||
filename: '/test.odt',
|
.buffer(true).parse(superagent.parse['application/octet-stream'])
|
||||||
contentType: 'application/odt'
|
.expect(200)
|
||||||
});
|
.expect((res) => assert(res.body.length >= 1000));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exports ODT', function(done) {
|
it('Tries to import .odt that uses soffice or abiword', async function() {
|
||||||
if(!settings.allowAnyoneToImport) return done();
|
await agent.post(`/p/${testPadId}/import`)
|
||||||
if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done();
|
.attach('file', odtDoc, {filename: '/test.odt', contentType: 'application/odt'})
|
||||||
request(host + '/p/'+testPadId+'/export/odt', function (err, res, body) {
|
.expect(200)
|
||||||
// TODO: At some point checking that the contents is correct would be suitable
|
.expect(/FrameCall\('undefined', 'ok'\);/);
|
||||||
if(body.length >= 7000){
|
|
||||||
done();
|
|
||||||
}else{
|
|
||||||
throw new Error("ODT Document export length is not right");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Tries to import .etherpad', function(done) {
|
|
||||||
if(!settings.allowAnyoneToImport) return done();
|
|
||||||
|
|
||||||
var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
throw new Error("Failed to import", err);
|
|
||||||
} else {
|
|
||||||
if(res.body.indexOf("FrameCall(\'true\', \'ok\');") === -1){
|
|
||||||
throw new Error("Failed Etherpad import", err, testPadId);
|
|
||||||
}else{
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let form = req.form();
|
it('exports ODT', async function() {
|
||||||
form.append('file', etherpadDoc, {
|
await agent.get(`/p/${testPadId}/export/odt`)
|
||||||
|
.buffer(true).parse(superagent.parse['application/octet-stream'])
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => assert(res.body.length >= 7000));
|
||||||
|
});
|
||||||
|
|
||||||
|
}); // End of AbiWord/LibreOffice tests.
|
||||||
|
|
||||||
|
it('Tries to import .etherpad', async function() {
|
||||||
|
await agent.post(`/p/${testPadId}/import`)
|
||||||
|
.attach('file', etherpadDoc, {
|
||||||
filename: '/test.etherpad',
|
filename: '/test.etherpad',
|
||||||
contentType: 'application/etherpad'
|
contentType: 'application/etherpad',
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exports Etherpad', function(done) {
|
|
||||||
request(host + '/p/'+testPadId+'/export/etherpad', function (err, res, body) {
|
|
||||||
// TODO: At some point checking that the contents is correct would be suitable
|
|
||||||
if(body.indexOf("hello") !== -1){
|
|
||||||
done();
|
|
||||||
}else{
|
|
||||||
console.error("body");
|
|
||||||
throw new Error("Etherpad Document does not include hello");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('exports HTML for this Etherpad file', function(done) {
|
|
||||||
request(host + '/p/'+testPadId+'/export/html', function (err, res, body) {
|
|
||||||
|
|
||||||
// broken pre fix export -- <ul class="bullet"></li><ul class="bullet"></ul></li></ul>
|
|
||||||
var expectedHTML = '<ul class="bullet"><li><ul class="bullet"><li>hello</ul></li></ul>';
|
|
||||||
// expect body to include
|
|
||||||
if(body.indexOf(expectedHTML) !== -1){
|
|
||||||
done();
|
|
||||||
}else{
|
|
||||||
console.error(body);
|
|
||||||
throw new Error("Exported HTML nested list items is not right", body);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('tries to import Plain Text to a pad that does not exist', function(done) {
|
|
||||||
var req = request.post(host + '/p/'+testPadId+testPadId+testPadId+'/import', function (err, res, body) {
|
|
||||||
if (res.statusCode === 200) {
|
|
||||||
throw new Error("Was able to import to a pad that doesn't exist");
|
|
||||||
}else{
|
|
||||||
// Wasn't able to write to a pad that doesn't exist, this is expected behavior
|
|
||||||
api.get(endPoint('getText')+"&padID="+testPadId+testPadId+testPadId)
|
|
||||||
.expect(function(res){
|
|
||||||
if(res.body.code !== 1) throw new Error("Pad Exists");
|
|
||||||
})
|
|
||||||
.expect(200, done)
|
|
||||||
}
|
|
||||||
|
|
||||||
let form = req.form();
|
|
||||||
|
|
||||||
form.append('file', padText, {
|
|
||||||
filename: '/test.txt',
|
|
||||||
contentType: 'text/plain'
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
.expect(200)
|
||||||
|
.expect(/FrameCall\('true', 'ok'\);/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Tries to import unsupported file type', function(done) {
|
it('exports Etherpad', async function() {
|
||||||
if(settings.allowUnknownFileEnds === true){
|
await agent.get(`/p/${testPadId}/export/etherpad`)
|
||||||
console.log("allowing unknown file ends so skipping this test");
|
.buffer(true).parse(superagent.parse.text)
|
||||||
return done();
|
.expect(200)
|
||||||
}
|
.expect(/hello/);
|
||||||
|
|
||||||
var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
throw new Error("Failed to import", err);
|
|
||||||
} else {
|
|
||||||
if(res.body.indexOf("FrameCall('undefined', 'ok');") !== -1){
|
|
||||||
console.log("worked");
|
|
||||||
throw new Error("You shouldn't be able to import this file", testPadId);
|
|
||||||
}
|
|
||||||
return done();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let form = req.form();
|
it('exports HTML for this Etherpad file', async function() {
|
||||||
form.append('file', padText, {
|
await agent.get(`/p/${testPadId}/export/html`)
|
||||||
filename: '/test.xasdasdxx',
|
.expect(200)
|
||||||
contentType: 'weirdness/jobby'
|
.expect('content-type', 'text/html; charset=utf-8')
|
||||||
});
|
.expect(/<ul class="bullet"><li><ul class="bullet"><li>hello<\/ul><\/li><\/ul>/);
|
||||||
});
|
});
|
||||||
|
|
||||||
// end of tests
|
it('tries to import Plain Text to a pad that does not exist', async function() {
|
||||||
})
|
const padId = testPadId + testPadId + testPadId;
|
||||||
|
await agent.post(`/p/${padId}/import`)
|
||||||
|
.attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
|
||||||
|
.expect(405);
|
||||||
|
await agent.get(endPoint('getText') + `&padID=${padId}`)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => assert.equal(res.body.code, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Tries to import unsupported file type', async function() {
|
||||||
|
if (settings.allowUnknownFileEnds === true) {
|
||||||
|
console.log('skipping test because allowUnknownFileEnds is true');
|
||||||
|
return this.skip();
|
||||||
|
}
|
||||||
|
await agent.post(`/p/${testPadId}/import`)
|
||||||
|
.attach('file', padText, {filename: '/test.xasdasdxx', contentType: 'weirdness/jobby'})
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => assert.doesNotMatch(res.text, /FrameCall\('undefined', 'ok'\);/));
|
||||||
|
});
|
||||||
|
|
||||||
|
}); // End of tests.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -363,7 +211,7 @@ describe('Imports and Exports', function(){
|
||||||
|
|
||||||
var endPoint = function(point, version){
|
var endPoint = function(point, version){
|
||||||
version = version || apiVersion;
|
version = version || apiVersion;
|
||||||
return '/api/'+version+'/'+point+'?apikey='+apiKey;
|
return `/api/${version}/${point}?apikey=${apiKey}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeid()
|
function makeid()
|
||||||
|
@ -376,35 +224,3 @@ function makeid()
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateLongText(){
|
|
||||||
var text = "";
|
|
||||||
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
|
|
||||||
for( var i=0; i < 80000; i++ ){
|
|
||||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need this to compare arrays (listSavedRevisions test)
|
|
||||||
Array.prototype.equals = function (array) {
|
|
||||||
// if the other array is a falsy value, return
|
|
||||||
if (!array)
|
|
||||||
return false;
|
|
||||||
// compare lengths - can save a lot of time
|
|
||||||
if (this.length != array.length)
|
|
||||||
return false;
|
|
||||||
for (var i = 0, l=this.length; i < l; i++) {
|
|
||||||
// Check if we have nested arrays
|
|
||||||
if (this[i] instanceof Array && array[i] instanceof Array) {
|
|
||||||
// recurse into the nested arrays
|
|
||||||
if (!this[i].equals(array[i]))
|
|
||||||
return false;
|
|
||||||
} else if (this[i] != array[i]) {
|
|
||||||
// Warning - two different object instances will never be equal: {x:20} != {x:20}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,19 +11,19 @@ const settings = require(m('node/utils/Settings'));
|
||||||
const supertest = require(m('node_modules/supertest'));
|
const supertest = require(m('node_modules/supertest'));
|
||||||
|
|
||||||
const logger = log4js.getLogger('test');
|
const logger = log4js.getLogger('test');
|
||||||
let client;
|
let agent;
|
||||||
let baseUrl;
|
let baseUrl;
|
||||||
|
|
||||||
before(async () => {
|
before(async function() {
|
||||||
settings.port = 0;
|
settings.port = 0;
|
||||||
settings.ip = 'localhost';
|
settings.ip = 'localhost';
|
||||||
const httpServer = await server.start();
|
const httpServer = await server.start();
|
||||||
baseUrl = `http://localhost:${httpServer.address().port}`;
|
baseUrl = `http://localhost:${httpServer.address().port}`;
|
||||||
logger.debug(`HTTP server at ${baseUrl}`);
|
logger.debug(`HTTP server at ${baseUrl}`);
|
||||||
client = supertest(baseUrl);
|
agent = supertest(baseUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async function() {
|
||||||
await server.stop();
|
await server.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -107,10 +107,22 @@ const handshake = async (socket, padID) => {
|
||||||
return msg;
|
return msg;
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('socket.io access checks', () => {
|
describe('socket.io access checks', function() {
|
||||||
|
let authorize;
|
||||||
|
let authorizeHooksBackup;
|
||||||
|
const cleanUpPads = async () => {
|
||||||
|
const padIds = ['pad', 'other-pad', 'päd'];
|
||||||
|
await Promise.all(padIds.map(async (padId) => {
|
||||||
|
if (await padManager.doesPadExist(padId)) {
|
||||||
|
const pad = await padManager.getPad(padId);
|
||||||
|
await pad.remove();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
const settingsBackup = {};
|
const settingsBackup = {};
|
||||||
let socket;
|
let socket;
|
||||||
beforeEach(async () => {
|
|
||||||
|
beforeEach(async function() {
|
||||||
Object.assign(settingsBackup, settings);
|
Object.assign(settingsBackup, settings);
|
||||||
assert(socket == null);
|
assert(socket == null);
|
||||||
settings.requireAuthentication = false;
|
settings.requireAuthentication = false;
|
||||||
|
@ -119,61 +131,93 @@ describe('socket.io access checks', () => {
|
||||||
admin: {password: 'admin-password', is_admin: true},
|
admin: {password: 'admin-password', is_admin: true},
|
||||||
user: {password: 'user-password'},
|
user: {password: 'user-password'},
|
||||||
};
|
};
|
||||||
Promise.all(['pad', 'other-pad'].map(async (pad) => {
|
authorize = () => true;
|
||||||
if (await padManager.doesPadExist(pad)) (await padManager.getPad(pad)).remove();
|
authorizeHooksBackup = plugins.hooks.authorize;
|
||||||
}));
|
plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => {
|
||||||
|
if (req.session.user == null) return cb([]); // Hasn't authenticated yet.
|
||||||
|
return cb([authorize(req)]);
|
||||||
|
}}];
|
||||||
|
await cleanUpPads();
|
||||||
});
|
});
|
||||||
afterEach(async () => {
|
afterEach(async function() {
|
||||||
Object.assign(settings, settingsBackup);
|
Object.assign(settings, settingsBackup);
|
||||||
if (socket) socket.close();
|
if (socket) socket.close();
|
||||||
socket = null;
|
socket = null;
|
||||||
|
plugins.hooks.authorize = authorizeHooksBackup;
|
||||||
|
await cleanUpPads();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Normal accesses.
|
// Normal accesses.
|
||||||
it('!authn anonymous /p/pad -> 200, ok', async () => {
|
it('!authn anonymous cookie /p/pad -> 200, ok', async function() {
|
||||||
const res = await client.get('/p/pad').expect(200);
|
const res = await agent.get('/p/pad').expect(200);
|
||||||
// Should not throw.
|
// Should not throw.
|
||||||
socket = await connect(res);
|
socket = await connect(res);
|
||||||
const clientVars = await handshake(socket, 'pad');
|
const clientVars = await handshake(socket, 'pad');
|
||||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||||
});
|
});
|
||||||
it('!authn user /p/pad -> 200, ok', async () => {
|
it('!authn !cookie -> ok', async function() {
|
||||||
const res = await client.get('/p/pad').auth('user', 'user-password').expect(200);
|
// Should not throw.
|
||||||
|
socket = await connect(null);
|
||||||
|
const clientVars = await handshake(socket, 'pad');
|
||||||
|
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||||
|
});
|
||||||
|
it('!authn user /p/pad -> 200, ok', async function() {
|
||||||
|
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
|
||||||
// Should not throw.
|
// Should not throw.
|
||||||
socket = await connect(res);
|
socket = await connect(res);
|
||||||
const clientVars = await handshake(socket, 'pad');
|
const clientVars = await handshake(socket, 'pad');
|
||||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||||
});
|
});
|
||||||
it('authn user /p/pad -> 200, ok', async () => {
|
it('authn user /p/pad -> 200, ok', async function() {
|
||||||
settings.requireAuthentication = true;
|
settings.requireAuthentication = true;
|
||||||
const res = await client.get('/p/pad').auth('user', 'user-password').expect(200);
|
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
|
||||||
// Should not throw.
|
// Should not throw.
|
||||||
socket = await connect(res);
|
socket = await connect(res);
|
||||||
const clientVars = await handshake(socket, 'pad');
|
const clientVars = await handshake(socket, 'pad');
|
||||||
assert.equal(clientVars.type, 'CLIENT_VARS');
|
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||||
});
|
});
|
||||||
|
it('authz user /p/pad -> 200, ok', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = true;
|
||||||
|
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
|
||||||
|
// Should not throw.
|
||||||
|
socket = await connect(res);
|
||||||
|
const clientVars = await handshake(socket, 'pad');
|
||||||
|
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||||
|
});
|
||||||
|
it('supports pad names with characters that must be percent-encoded', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
// requireAuthorization is set to true here to guarantee that the user's padAuthorizations
|
||||||
|
// object is populated. Technically this isn't necessary because the user's padAuthorizations is
|
||||||
|
// currently populated even if requireAuthorization is false, but setting this to true ensures
|
||||||
|
// the test remains useful if the implementation ever changes.
|
||||||
|
settings.requireAuthorization = true;
|
||||||
|
const encodedPadId = encodeURIComponent('päd');
|
||||||
|
const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200);
|
||||||
|
// Should not throw.
|
||||||
|
socket = await connect(res);
|
||||||
|
const clientVars = await handshake(socket, 'päd');
|
||||||
|
assert.equal(clientVars.type, 'CLIENT_VARS');
|
||||||
|
});
|
||||||
|
|
||||||
// Abnormal access attempts.
|
// Abnormal access attempts.
|
||||||
it('authn anonymous /p/pad -> 401, error', async () => {
|
it('authn anonymous /p/pad -> 401, error', async function() {
|
||||||
settings.requireAuthentication = true;
|
settings.requireAuthentication = true;
|
||||||
const res = await client.get('/p/pad').expect(401);
|
const res = await agent.get('/p/pad').expect(401);
|
||||||
// Despite the 401, try to create the pad via a socket.io connection anyway.
|
// Despite the 401, try to create the pad via a socket.io connection anyway.
|
||||||
await assert.rejects(connect(res), {message: /authentication required/i});
|
await assert.rejects(connect(res), {message: /authentication required/i});
|
||||||
});
|
});
|
||||||
it('socket.io connection without express-session cookie -> error', async () => {
|
it('authn !cookie -> error', async function() {
|
||||||
settings.requireAuthentication = true;
|
settings.requireAuthentication = true;
|
||||||
await assert.rejects(connect(null), {message: /signed express_sid cookie is required/i});
|
await assert.rejects(connect(null), {message: /signed express_sid cookie is required/i});
|
||||||
});
|
});
|
||||||
it('authorization bypass attempt -> error', async () => {
|
it('authorization bypass attempt -> error', async function() {
|
||||||
plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => {
|
|
||||||
if (req.session.user == null) return cb([]); // Hasn't authenticated yet.
|
|
||||||
// Only allowed to access /p/pad.
|
// Only allowed to access /p/pad.
|
||||||
return cb([req.path === '/p/pad']);
|
authorize = (req) => req.path === '/p/pad';
|
||||||
}}];
|
|
||||||
settings.requireAuthentication = true;
|
settings.requireAuthentication = true;
|
||||||
settings.requireAuthorization = true;
|
settings.requireAuthorization = true;
|
||||||
// First authenticate and establish a session.
|
// First authenticate and establish a session.
|
||||||
const res = await client.get('/p/pad').auth('user', 'user-password').expect(200);
|
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
|
||||||
// Connecting should work because the user successfully authenticated.
|
// Connecting should work because the user successfully authenticated.
|
||||||
socket = await connect(res);
|
socket = await connect(res);
|
||||||
// Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
|
// Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
|
||||||
|
|
166
tests/backend/specs/webaccess.js
Normal file
166
tests/backend/specs/webaccess.js
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
function m(mod) { return __dirname + '/../../../src/' + mod; }
|
||||||
|
|
||||||
|
const assert = require('assert').strict;
|
||||||
|
const log4js = require(m('node_modules/log4js'));
|
||||||
|
const plugins = require(m('static/js/pluginfw/plugin_defs'));
|
||||||
|
const server = require(m('node/server'));
|
||||||
|
const settings = require(m('node/utils/Settings'));
|
||||||
|
const supertest = require(m('node_modules/supertest'));
|
||||||
|
|
||||||
|
let agent;
|
||||||
|
const logger = log4js.getLogger('test');
|
||||||
|
|
||||||
|
before(async function() {
|
||||||
|
settings.port = 0;
|
||||||
|
settings.ip = 'localhost';
|
||||||
|
const httpServer = await server.start();
|
||||||
|
const baseUrl = `http://localhost:${httpServer.address().port}`;
|
||||||
|
logger.debug(`HTTP server at ${baseUrl}`);
|
||||||
|
agent = supertest(baseUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async function() {
|
||||||
|
await server.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('webaccess without any plugins', function() {
|
||||||
|
const backup = {};
|
||||||
|
|
||||||
|
before(async function() {
|
||||||
|
Object.assign(backup, settings);
|
||||||
|
settings.users = {
|
||||||
|
admin: {password: 'admin-password', is_admin: true},
|
||||||
|
user: {password: 'user-password'},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async function() {
|
||||||
|
Object.assign(settings, backup);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('!authn !authz anonymous / -> 200', async function() {
|
||||||
|
settings.requireAuthentication = false;
|
||||||
|
settings.requireAuthorization = false;
|
||||||
|
await agent.get('/').expect(200);
|
||||||
|
});
|
||||||
|
it('!authn !authz anonymous /admin/ -> 401', async function() {
|
||||||
|
settings.requireAuthentication = false;
|
||||||
|
settings.requireAuthorization = false;
|
||||||
|
await agent.get('/admin/').expect(401);
|
||||||
|
});
|
||||||
|
it('authn !authz anonymous / -> 401', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = false;
|
||||||
|
await agent.get('/').expect(401);
|
||||||
|
});
|
||||||
|
it('authn !authz user / -> 200', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = false;
|
||||||
|
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||||
|
});
|
||||||
|
it('authn !authz user /admin/ -> 403', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = false;
|
||||||
|
await agent.get('/admin/').auth('user', 'user-password').expect(403);
|
||||||
|
});
|
||||||
|
it('authn !authz admin / -> 200', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = false;
|
||||||
|
await agent.get('/').auth('admin', 'admin-password').expect(200);
|
||||||
|
});
|
||||||
|
it('authn !authz admin /admin/ -> 200', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = false;
|
||||||
|
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||||
|
});
|
||||||
|
it('authn authz user / -> 403', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = true;
|
||||||
|
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||||
|
});
|
||||||
|
it('authn authz user /admin/ -> 403', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = true;
|
||||||
|
await agent.get('/admin/').auth('user', 'user-password').expect(403);
|
||||||
|
});
|
||||||
|
it('authn authz admin / -> 200', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = true;
|
||||||
|
await agent.get('/').auth('admin', 'admin-password').expect(200);
|
||||||
|
});
|
||||||
|
it('authn authz admin /admin/ -> 200', async function() {
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = true;
|
||||||
|
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('webaccess with authFailure plugin', function() {
|
||||||
|
let handle, returnUndef, status, called;
|
||||||
|
const authFailure = (hookName, context, cb) => {
|
||||||
|
assert.equal(hookName, 'authFailure');
|
||||||
|
assert(context != null);
|
||||||
|
assert(context.req != null);
|
||||||
|
assert(context.res != null);
|
||||||
|
assert(context.next != null);
|
||||||
|
assert(!called);
|
||||||
|
called = true;
|
||||||
|
if (handle) {
|
||||||
|
context.res.status(status).send('injected content');
|
||||||
|
return cb([true]);
|
||||||
|
}
|
||||||
|
if (returnUndef) return cb();
|
||||||
|
return cb([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsBackup = {};
|
||||||
|
let authFailureHooksBackup;
|
||||||
|
before(function() {
|
||||||
|
Object.assign(settingsBackup, settings);
|
||||||
|
authFailureHooksBackup = plugins.hooks.authFailure;
|
||||||
|
plugins.hooks.authFailure = [{hook_fn: authFailure}];
|
||||||
|
settings.requireAuthentication = true;
|
||||||
|
settings.requireAuthorization = true;
|
||||||
|
settings.users = {
|
||||||
|
admin: {password: 'admin-password', is_admin: true},
|
||||||
|
user: {password: 'user-password'},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
after(function() {
|
||||||
|
Object.assign(settings, settingsBackup);
|
||||||
|
plugins.hooks.authFailure = authFailureHooksBackup;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
handle = false;
|
||||||
|
returnUndef = false;
|
||||||
|
status = 200;
|
||||||
|
called = false;
|
||||||
|
});
|
||||||
|
afterEach(function() {
|
||||||
|
assert(called);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('authn fail, hook handles -> 200', async function() {
|
||||||
|
handle = true;
|
||||||
|
await agent.get('/').expect(200, /injected content/);
|
||||||
|
});
|
||||||
|
it('authn fail, hook defers (undefined) -> 401', async function() {
|
||||||
|
returnUndef = true;
|
||||||
|
await agent.get('/').expect(401);
|
||||||
|
});
|
||||||
|
it('authn fail, hook defers (empty list) -> 401', async function() {
|
||||||
|
await agent.get('/').expect(401);
|
||||||
|
});
|
||||||
|
it('authz fail, hook handles -> 200', async function() {
|
||||||
|
handle = true;
|
||||||
|
await agent.get('/').auth('user', 'user-password').expect(200, /injected content/);
|
||||||
|
});
|
||||||
|
it('authz fail, hook defers (undefined) -> 403', async function() {
|
||||||
|
returnUndef = true;
|
||||||
|
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||||
|
});
|
||||||
|
it('authz fail, hook defers (empty list) -> 403', async function() {
|
||||||
|
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue