├── .npmignore ├── .gitignore ├── fennel_logo.png ├── demouser.htaccess ├── libs ├── xmlhelper.js ├── log.js ├── user.js ├── parser.js ├── requesthandler.js ├── authentication.js ├── db.js └── communication.js ├── package.json ├── utilities └── fennel_supervisor.conf ├── test ├── http │ ├── redirect.js │ └── dotwellknown.js ├── principal │ ├── options.js │ ├── report.js │ └── propfind.js ├── cal │ ├── options.js │ ├── mkcalendar.js │ ├── put.js │ ├── report.js │ └── propfind.js └── card │ ├── options.js │ ├── put.js │ └── report.js ├── config.js ├── server.js ├── README.md ├── handler ├── principal.js └── addressbook.js └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | *.sqlite 3 | utils/* 4 | .DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | *.sqlite 3 | *.flow 4 | utils/* 5 | .DS_Store -------------------------------------------------------------------------------- /fennel_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andris9/fennel/master/fennel_logo.png -------------------------------------------------------------------------------- /demouser.htaccess: -------------------------------------------------------------------------------- 1 | demo:$apr1$1jd3WSjk$TJ10Xzt82mLEIVTv6Wyye0 2 | fennel:$apr1$DLyhjDw/$ZztH7IzjdM6wkIkx1EMKa0 3 | fennellocal:$apr1$IXFkZ.2B$e1UG.ecCrx7265DEJYe0k1 4 | -------------------------------------------------------------------------------- /libs/xmlhelper.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | 11 | // Exporting. 12 | module.exports = { 13 | getXMLHead: getXMLHead 14 | }; 15 | 16 | function getXMLHead() 17 | { 18 | return "\n\r"; 19 | } -------------------------------------------------------------------------------- /libs/log.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014-16 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | 11 | var log4js = require('log4js'); 12 | var log = log4js.getLogger("fennelapp"); 13 | 14 | function initialise() 15 | { 16 | log.debug("logger loaded"); 17 | } 18 | 19 | // Exporting. 20 | module.exports = { 21 | log: log 22 | }; 23 | 24 | initialise(); 25 | -------------------------------------------------------------------------------- /libs/user.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | 11 | var log = require('../libs/log').log; 12 | 13 | // Exporting. 14 | module.exports = { 15 | user: user 16 | }; 17 | 18 | 19 | function user(username) 20 | { 21 | this.username = username; 22 | 23 | return this; 24 | } 25 | 26 | user.prototype.getUserName = function() 27 | { 28 | return this.username; 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fennel", 3 | "author": { 4 | "name": "LordEidi from SwordLord - the coding crew", 5 | "email": "", 6 | "url": "https://swordlord.com/" 7 | }, 8 | "description": "Fennel is a lightweight CardDAV / CalDAV server", 9 | "keywords": [ 10 | "caldav", 11 | "carddav", 12 | "lightweight" 13 | ], 14 | "license": "agpl-3.0", 15 | "version": "0.2.0-beta", 16 | "main": "server.js", 17 | "readmeFilename": "README.md", 18 | "bugs": { 19 | "url": "https://github.com/LordEidi/fennel/issues" 20 | }, 21 | "scripts": { 22 | "test": "tape test/**/*.js | faucet" 23 | }, 24 | "homepage": "https://github.com/LordEidi/fennel", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/LordEidi/fennel.git" 28 | }, 29 | "dependencies": { 30 | "crossroads": "^0.12.2", 31 | "htdigest": "^2.0.2", 32 | "htpasswd": "^2.1.2", 33 | "http-auth": "^2.1.9", 34 | "libxmljs": "^0.18.0", 35 | "log4js": "^0.6.9", 36 | "moment": "^2.15.1", 37 | "node-regexp": "^0.1.1", 38 | "pretty-data": "^0.40.0", 39 | "sequelize": "^3.30.4", 40 | "shiro-trie": "^0.3.12", 41 | "sqlite3": "^3.0.8", 42 | "uuid": "^2.0.3", 43 | "xml2js": "^0.4.9" 44 | }, 45 | "devDependencies": { 46 | "tape": "^4.0.1", 47 | "request": "^2.65.0", 48 | "faucet": "0.0.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /utilities/fennel_supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:fennel] 2 | command=node server.js ; the program (relative uses PATH, can take args) 3 | process_name=%(program_name)s ; process_name expr (default %(program_name)s) 4 | numprocs=1 ; number of processes copies to start (def 1) 5 | directory=/home/node/fennel ; directory to cwd to before exec (def no cwd) 6 | ;umask=022 ; umask for process (default None) 7 | ;priority=999 ; the relative start priority (default 999) 8 | ;autostart=true ; start at supervisord start (default: true) 9 | ;autorestart=true ; retstart at unexpected quit (default: true) 10 | ;startsecs=10 ; number of secs prog must stay running (def. 1) 11 | ;startretries=3 ; max # of serial start failures (default 3) 12 | ;exitcodes=0,2 ; 'expected' exit codes for process (default 0,2) 13 | ;stopsignal=QUIT ; signal used to kill process (default TERM) 14 | ;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10) 15 | user=node ; setuid to this UNIX account to run the program 16 | ;redirect_stderr=true ; redirect proc stderr to stdout (default false) 17 | ;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO 18 | ;stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) 19 | ;stdout_logfile_backups=10 ; # of stdout logfile backups (default 10) 20 | ;stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) 21 | ;stdout_events_enabled=false ; emit events on stdout writes (default false) 22 | ;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO 23 | ;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) 24 | ;stderr_logfile_backups=10 ; # of stderr logfile backups (default 10) 25 | ;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) 26 | ;stderr_events_enabled=false ; emit events on stderr writes (default false) 27 | ;environment=A=1,B=2 ; process environment additions (def no adds) 28 | ;serverurl=AUTO ; override serverurl computation (childutils) -------------------------------------------------------------------------------- /test/http/redirect.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2015 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | 36 | var config = require('../../config').config; 37 | 38 | var username = config.test_user_name; 39 | var password = config.test_user_pwd; 40 | 41 | test('Redirect to /p/ when hitting /', function (t) { 42 | 43 | t.plan(2); 44 | 45 | var options = { 46 | method: 'GET', 47 | uri: "http://" + config.ip + ":" + config.port + "/", 48 | auth: { 49 | 'user': username, 50 | 'pass': password, 51 | 'sendImmediately': true 52 | } , 53 | followRedirect: false 54 | } 55 | 56 | request(options, function (error, response, body) { 57 | 58 | if (!error) { 59 | t.equal(response.statusCode, 302, "StatusCode matches"); 60 | t.equal(response.headers.location, "/p/", "Redirection matches"); 61 | } 62 | else { 63 | t.fail(); 64 | } 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/http/dotwellknown.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2015 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | 36 | var config = require('../../config').config; 37 | 38 | var username = config.test_user_name; 39 | var password = config.test_user_pwd; 40 | 41 | test('Redirect to /p/ when hitting .well-known URL', function (t) { 42 | 43 | t.plan(2); 44 | 45 | var options = { 46 | method: 'GET', 47 | uri: "http://" + config.ip + ":" + config.port + "/.well-known", 48 | auth: { 49 | 'user': username, 50 | 'pass': password, 51 | 'sendImmediately': true 52 | } , 53 | followRedirect: false 54 | } 55 | 56 | request(options, function (error, response, body) { 57 | 58 | if (!error) { 59 | t.equal(response.statusCode, 302, "StatusCode matches"); 60 | t.equal(response.headers.location, "/p/", "Redirection matches"); 61 | } 62 | else { 63 | t.fail(); 64 | } 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/principal/options.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2015-17 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the 13 | ** Free Software Foundation, either version 3 of the License, or (at your 14 | ** option) any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License 22 | ** along with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | 36 | var config = require('../../config').config; 37 | 38 | var username = config.test_user_name; 39 | var password = config.test_user_pwd; 40 | 41 | test('Calling OPTIONS on principal', function (t) { 42 | 43 | t.plan(2); 44 | 45 | var options = { 46 | method: 'OPTIONS', 47 | uri: "http://" + config.ip + ":" + config.port + "/p/", 48 | auth: { 49 | 'user': username, 50 | 'pass': password, 51 | 'sendImmediately': true 52 | } , 53 | body: "", 54 | followRedirect: false 55 | } 56 | 57 | request(options, function (error, response, body) { 58 | 59 | if (!error) { 60 | t.equal(response.statusCode, 200, "StatusCode matches"); 61 | t.equal(response.headers.allow, "OPTIONS, PROPFIND, HEAD, GET, REPORT, PROPPATCH, PUT, DELETE, POST, COPY, MOVE", "Options match"); 62 | console.log(body); 63 | } 64 | else { 65 | t.fail(); 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/cal/options.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2017 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | 36 | var config = require('../../config').config; 37 | 38 | var username = config.test_user_name; 39 | var password = config.test_user_pwd; 40 | 41 | test('Calling OPTIONS on card', function (t) { 42 | 43 | t.plan(2); 44 | 45 | var options = { 46 | method: 'OPTIONS', 47 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/", 48 | auth: { 49 | 'user': username, 50 | 'pass': password, 51 | 'sendImmediately': true 52 | } , 53 | body: "", 54 | followRedirect: false 55 | } 56 | 57 | request(options, function (error, response, body) { 58 | 59 | if (!error) { 60 | t.equal(response.statusCode, 200, "StatusCode matches"); 61 | t.equal(response.headers.allow, "OPTIONS, PROPFIND, HEAD, GET, REPORT, PROPPATCH, PUT, DELETE, POST, COPY, MOVE", "Options match"); 62 | console.log(body); 63 | } 64 | else { 65 | t.fail(); 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/card/options.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2017 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | 36 | var config = require('../../config').config; 37 | 38 | var username = config.test_user_name; 39 | var password = config.test_user_pwd; 40 | 41 | test('Calling OPTIONS on card', function (t) { 42 | 43 | t.plan(2); 44 | 45 | var options = { 46 | method: 'OPTIONS', 47 | uri: "http://" + config.ip + ":" + config.port + "/card/" + username + "/default/", 48 | auth: { 49 | 'user': username, 50 | 'pass': password, 51 | 'sendImmediately': true 52 | } , 53 | body: "", 54 | followRedirect: false 55 | } 56 | 57 | request(options, function (error, response, body) { 58 | 59 | if (!error) { 60 | t.equal(response.statusCode, 200, "StatusCode matches"); 61 | t.equal(response.headers.allow, "OPTIONS, PROPFIND, HEAD, GET, REPORT, PROPPATCH, PUT, DELETE, POST, COPY, MOVE", "Options match"); 62 | console.log(body); 63 | } 64 | else { 65 | t.fail(); 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/principal/report.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2015 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | 36 | var config = require('../../config').config; 37 | 38 | var username = config.test_user_name; 39 | var password = config.test_user_pwd; 40 | 41 | test('Calling REPORT on principal', function (t) { 42 | 43 | t.plan(1); 44 | 45 | var options = { 46 | method: 'REPORT', 47 | uri: "http://" + config.ip + ":" + config.port + "/p/", 48 | auth: { 49 | 'user': username, 50 | 'pass': password, 51 | 'sendImmediately': true 52 | } , 53 | headers: [ 54 | { 55 | name: 'content-type', 56 | value: 'application/xml; charset=utf-8' 57 | } 58 | ], 59 | body: "", 60 | followRedirect: false 61 | } 62 | 63 | request(options, function (error, response, body) { 64 | 65 | if (!error) { 66 | t.equal(response.statusCode, 200, "StatusCode matches"); 67 | console.log(body); 68 | //t.equal(response.headers.location, "/p/", "Redirection matches"); 69 | } 70 | else { 71 | t.fail(); 72 | } 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/card/put.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2015-17 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the 13 | ** Free Software Foundation, either version 3 of the License, or (at your 14 | ** option) any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License 22 | ** along with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | 36 | var config = require('../../config').config; 37 | 38 | var username = config.test_user_name; 39 | var password = config.test_user_pwd; 40 | 41 | test('Calling PUT on cards', function (t) { 42 | 43 | t.plan(1); 44 | 45 | var payload = "BEGIN:VCARD\n\r"; 46 | payload += "VERSION:3.0\n\r"; 47 | payload += "PRODID:-//Apple Inc.//iOS 9.0.2//EN\n\r"; 48 | payload += "N:Www;Www;;;\n\r"; 49 | payload += "FN:Www Www\n\r"; 50 | payload += "ORG:company;\n\r"; 51 | payload += "REV:2015-10-16T13:00:28Z\n\r"; 52 | payload += "UID:E2D83EA7-9DA7-46F9-93EC-70F73BB1E4D1\n\r"; 53 | payload += "END:VCARD\n\r"; 54 | 55 | var options = { 56 | method: 'PUT', 57 | uri: "http://" + config.ip + ":" + config.port + "/card/" + username + "/default/7997A784-375D-4B42-8FAE-A9EAA3FB0DBF.vcf", 58 | auth: { 59 | 'user': username, 60 | 'pass': password, 61 | 'sendImmediately': true 62 | } , 63 | body: payload, 64 | followRedirect: false 65 | } 66 | 67 | request(options, function (error, response, body) { 68 | 69 | if (!error) { 70 | t.equal(response.statusCode, 201, "StatusCode matches"); 71 | } 72 | else { 73 | t.fail(error); 74 | } 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /libs/parser.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2016-17 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | 11 | var log4js = require('log4js'); 12 | var log = log4js.getLogger("parser"); 13 | 14 | function parseICS(file) 15 | { 16 | //TODO caveman parser, should be replaced with something more stable 17 | var lines = file.split(/[\r\n|\n\r|\n|\r]/g); 18 | 19 | var result = ""; 20 | 21 | // Remove empty lines 22 | for(var j = 1; j < lines.length; j++) 23 | { 24 | if (lines[j].length == 0) 25 | { 26 | lines.splice(j, 1); 27 | } 28 | } 29 | 30 | // Unfold the lines, if no : assume it is folded with previous 31 | for(var j = 1; j < lines.length; j++) 32 | { 33 | if (lines[j].indexOf(":") == -1) 34 | { 35 | lines[j-1] = lines[j-1] + lines[j]; 36 | lines.splice(j, 1); 37 | j--; // make sure to decrease the counter since we deleted one line... 38 | } 39 | } 40 | 41 | var len = lines.length; 42 | for(var i = 0; i < len; i++) 43 | { 44 | var el = lines[i]; 45 | if(el.length > 0) 46 | { 47 | if(el.substr(0, 6) === "BEGIN:") 48 | { 49 | if(el.charAt(el.length -1) === ".") 50 | { 51 | result += "\"" + el.substr(6, el.length -7) + "\": {"; 52 | } 53 | else 54 | { 55 | result += "\"" + el.substr(6) + "\": {"; 56 | } 57 | } 58 | else if (el.substr(0, 4) === "END:") 59 | { 60 | // TODO: terrible hack, fixme 61 | if(result.charAt(result.length - 1) === ",") 62 | { 63 | result = result.substr(0, result.length -1); 64 | } 65 | 66 | result += "}," 67 | } 68 | else 69 | { 70 | var arrLine = el.split(":"); 71 | var key = arrLine[0]; 72 | var val = arrLine[1]; 73 | 74 | if(val.charAt(val.length -1) === ".") 75 | { 76 | // todo: the key.split is a terrible hack as well, we loose some information like that 77 | // example: DTEND;TZID=Europe/Zurich:20161210T010000Z. -> tzid will be lost 78 | result += "\"" + key.split(";")[0] + "\":\"" + val.substr(0, val.length -1) + "\","; 79 | } 80 | else 81 | { 82 | // todo, see above 83 | result += "\"" + key.split(";")[0] + "\":\"" + val + "\","; 84 | } 85 | } 86 | } 87 | } 88 | 89 | // TODO: terrible hack, fixme 90 | if(result.charAt(result.length - 1) === ",") 91 | { 92 | result = result.substr(0, result.length -1); 93 | } 94 | 95 | result = "{" + result + "}"; 96 | 97 | //console.log(result); 98 | 99 | return JSON.parse(result); 100 | } 101 | 102 | // Exporting. 103 | module.exports = { 104 | parseICS: parseICS 105 | }; 106 | -------------------------------------------------------------------------------- /test/card/report.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2015 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | 36 | var config = require('../../config').config; 37 | 38 | var username = config.test_user_name; 39 | var password = config.test_user_pwd; 40 | 41 | test('Calling REPORT on cards', function (t) { 42 | 43 | t.plan(1); 44 | 45 | var payload = "\n\r"; 46 | payload += "\n\r"; 47 | payload += "http://sabredav.org/ns/sync/14\n\r"; 48 | payload += "1\n\r"; 49 | payload += "\n\r"; 50 | payload += "\n\r"; 51 | payload += "\n\r"; 52 | payload += "\n\r"; 53 | 54 | /* 55 | REPORT /addressbooks/a3298271331/8ec6424c-ede3-4a55-8613-e760df985cac/ HTTP/1.1 56 | 57 | var payload = "\n\r"; 58 | payload += "\n\r"; 59 | payload += "\n\r"; 60 | payload += "\n\r"; 61 | payload += "\n\r"; 62 | payload += "\n\r"; 63 | payload += "/addressbooks/a3298271331/8ec6424c-ede3-4a55-8613-e760df985cac/973d3ed8-9ef6-4724-a04a-b73748829d15.vcf 64 | payload += "\n\r"; 65 | */ 66 | 67 | var options = { 68 | method: 'REPORT', 69 | uri: "http://" + config.ip + ":" + config.port + "/card/" + username + "/default/", 70 | auth: { 71 | 'user': username, 72 | 'pass': password, 73 | 'sendImmediately': true 74 | } , 75 | body: payload, 76 | followRedirect: false 77 | } 78 | 79 | request(options, function (error, response, body) { 80 | 81 | if (!error) { 82 | t.equal(response.statusCode, 200, "StatusCode matches"); 83 | console.log(body); 84 | } 85 | else { 86 | t.fail(error); 87 | } 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014-17 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This program is free software; you can redistribute it and/or modify it 10 | ** under the terms of the GNU Affero General Public License as published by the 11 | ** Free Software Foundation, either version 3 of the License, or (at your 12 | ** option) any later version. 13 | ** 14 | ** This program is distributed in the hope that it will be useful, but WITHOUT 15 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 16 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 17 | ** more details. 18 | ** 19 | ** You should have received a copy of the GNU Affero General Public License 20 | ** along with this program. If not, see . 21 | ** 22 | **----------------------------------------------------------------------------- 23 | ** 24 | ** Original Authors: 25 | ** LordEidi@swordlord.com 26 | ** LordLightningBolt@swordlord.com 27 | ** 28 | ** $Id: 29 | ** 30 | -----------------------------------------------------------------------------*/ 31 | 32 | // Place all your configuration options here 33 | 34 | var config = 35 | { 36 | version_nr: '0.1.0', 37 | 38 | // Server specific configuration 39 | // Please use a proxy in front of Fennel to support TLS. 40 | // We suggest you use nginx as the TLS endpoint 41 | port: 8888, 42 | //port: 80, 43 | ip: '127.0.0.1', 44 | //ip: '0.0.0.0', 45 | 46 | // db specific configuration. you can use whatever sequelize supports. 47 | db_name: 'fennel', 48 | db_uid: 'uid', 49 | db_pwd: 'pwd', 50 | db_dialect: 'sqlite', 51 | db_logging: true, 52 | db_storage: 'fennel.sqlite', 53 | // db_host: 'localhost', // For myql, postgres etc. 54 | 55 | // Authentication 56 | // Authentication methods so far: courier, htaccess, ldap 57 | auth_method: 'htaccess', 58 | auth_method_courier_socket: '/var/run/courier/authdaemon/socket', 59 | auth_method_htaccess_file: 'demouser.htaccess', 60 | 61 | // ldap authentication requires the ldapjs@1.0.0 node module. Please install manually 62 | auth_method_ldap_url: 'ldap://localhost:3002', 63 | auth_method_ldap_user_base_dn: 'ou=users,dc=example', 64 | 65 | 66 | // Authorisation 67 | // Authorisation Rules: 68 | // This property takes an array of Shiro formatted strings. Users are 69 | // only permitted access to resources when said access is explicitly 70 | // allowed here. Please see http://shiro.apache.org/permissions.html 71 | // for a short introduction to Shiro Syntax. 72 | // 73 | // Fennel uses the URL + the function to check for authorisation. 74 | // /card/demo/default/card_id.vcf with method PUT will become 75 | // card:demo:default:card_id.vcf:put 76 | // 77 | // Please note that $username is not recognised by shiro-trie but 78 | // will be replaced by Fennel with the current user when loaded into 79 | // the current process. 80 | // 81 | // The current set will allow the owner to access his or her own stuff 82 | authorisation: [ 83 | 'cal:$username:*', 84 | 'card:$username:*', 85 | 'p:options,report,propfind', 86 | 'p:$username:*' 87 | ], 88 | 89 | test_user_name: 'demo', 90 | test_user_pwd: 'demo' 91 | }; 92 | 93 | // Exporting. 94 | module.exports = { 95 | config: config 96 | }; 97 | -------------------------------------------------------------------------------- /libs/requesthandler.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014-16 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | var log4js = require('log4js'); 11 | var log = log4js.getLogger("requesthandler"); 12 | 13 | var principal = require("../handler/principal"); 14 | var cal = require("../handler/calendar"); 15 | var card = require("../handler/addressbook"); 16 | 17 | function handlePrincipal(request) 18 | { 19 | // check if root url or cal or card url 20 | var method = request.getReq().method; 21 | switch(method) 22 | { 23 | case 'PROPFIND': 24 | principal.propfind(request); 25 | break; 26 | 27 | case 'PROPPATCH': 28 | principal.proppatch(request); 29 | break; 30 | 31 | case 'OPTIONS': 32 | principal.options(request); 33 | break; 34 | 35 | case 'REPORT': 36 | principal.report(request); 37 | break; 38 | 39 | default: 40 | var res = request.getRes(); 41 | log.info("Request method is unknown: " + method); 42 | res.writeHead(500); 43 | res.write(method + " is not implemented yet"); 44 | break; 45 | } 46 | } 47 | 48 | function handleCalendar(request) 49 | { 50 | var method = request.getReq().method; 51 | switch(method) 52 | { 53 | case 'PROPFIND': 54 | cal.propfind(request); 55 | break; 56 | 57 | case 'PROPPATCH': 58 | cal.proppatch(request); 59 | break; 60 | 61 | case 'OPTIONS': 62 | cal.options(request); 63 | break; 64 | 65 | case 'REPORT': 66 | cal.report(request); 67 | break; 68 | 69 | case 'MKCALENDAR': 70 | cal.makeCalendar(request); 71 | break; 72 | 73 | case 'PUT': 74 | cal.put(request); 75 | break; 76 | 77 | case 'GET': 78 | cal.get(request); 79 | break; 80 | 81 | case 'DELETE': 82 | cal.delete(request); 83 | break; 84 | 85 | case 'MOVE': 86 | cal.move(request); 87 | break; 88 | 89 | default: 90 | var res = request.getRes(); 91 | log.info("Request method is unknown: " + method); 92 | res.writeHead(500); 93 | res.write(method + " is not implemented yet"); 94 | break; 95 | } 96 | } 97 | 98 | function handleCard(request) 99 | { 100 | var method = request.getReq().method; 101 | switch(method) 102 | { 103 | case 'PROPFIND': 104 | card.propfind(request); 105 | break; 106 | 107 | case 'PROPPATCH': 108 | card.proppatch(request); 109 | break; 110 | 111 | case 'OPTIONS': 112 | card.options(request); 113 | break; 114 | 115 | case 'REPORT': 116 | card.report(request); 117 | break; 118 | 119 | case 'PUT': 120 | card.put(request); 121 | break; 122 | 123 | case 'GET': 124 | card.get(request); 125 | break; 126 | 127 | case 'DELETE': 128 | card.delete(request); 129 | break; 130 | 131 | case 'MOVE': 132 | card.move(request); 133 | break; 134 | 135 | default: 136 | var res = request.getRes(); 137 | log.info("Request method is unknown: " + method); 138 | res.writeHead(500); 139 | res.write(method + " is not implemented yet"); 140 | break; 141 | } 142 | } 143 | 144 | // Exporting. 145 | module.exports = { 146 | handlePrincipal: handlePrincipal, 147 | handleCalendar: handleCalendar, 148 | handleCard: handleCard 149 | }; 150 | -------------------------------------------------------------------------------- /test/cal/mkcalendar.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2016 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | var moment = require('moment'); 36 | 37 | var config = require('../../config').config; 38 | 39 | var username = config.test_user_name; 40 | var password = config.test_user_pwd; 41 | 42 | var payload = "\n\r"; 43 | payload += "\n\r"; 44 | payload += "\n\r"; 45 | payload += "\n\r"; 46 | payload += "\n\r"; 47 | payload += " \n\r"; 48 | payload += "\n\r"; 49 | payload += "Three\n\r"; 50 | payload += "4\n\r"; 51 | payload += "\n\r"; 52 | payload += " \n\r"; 53 | payload += "\n\r"; 54 | payload += "BEGIN:VCALENDAR \n\r"; 55 | payload += "VERSION:2.0 \n\r"; 56 | payload += "CALSCALE:GREGORIAN \n\r"; 57 | payload += "BEGIN:VTIMEZONE \n\r"; 58 | payload += "TZID:Europe/Zurich \n\r"; 59 | payload += "BEGIN:DAYLIGHT \n\r"; 60 | payload += "TZOFFSETFROM:+0100 \n\r"; 61 | payload += "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU \n\r"; 62 | payload += "DTSTART:19810329T020000 \n\r"; 63 | payload += "TZNAME:GMT+2 \n\r"; 64 | payload += "TZOFFSETTO:+0200 \n\r"; 65 | payload += "END:DAYLIGHT \n\r"; 66 | payload += "BEGIN:STANDARD \n\r"; 67 | payload += "TZOFFSETFROM:+0200 \n\r"; 68 | payload += "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU \n\r"; 69 | payload += "DTSTART:19961027T030000 \n\r"; 70 | payload += "TZNAME:GMT+1 \n\r"; 71 | payload += "TZOFFSETTO:+0100 \n\r"; 72 | payload += "END:STANDARD \n\r"; 73 | payload += "END:VTIMEZONE \n\r"; 74 | payload += "END:VCALENDAR \n\r"; 75 | payload += "\n\r"; 76 | payload += "#FFCC00\n\r"; 78 | payload += "\n\r"; 79 | payload += "\n\r"; 80 | payload += "\n\r"; 81 | 82 | test('Calling MKCALENDAR on demo user', function (t) { 83 | 84 | t.plan(1); 85 | 86 | var options = { 87 | method: 'MKCALENDAR', 88 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/2A2AF854-18A0-47CB-870A-D94CA2341BAA/", 89 | auth: { 90 | 'user': username, 91 | 'pass': password, 92 | 'sendImmediately': true 93 | } , 94 | body: payload, 95 | followRedirect: false 96 | }; 97 | 98 | request(options, function (error, response, body) { 99 | 100 | if (!error) 101 | { 102 | t.equal(response.statusCode, 201, "StatusCode matches"); 103 | //t.equal(response.headers.allow, "OPTIONS, PROPFIND, HEAD, GET, REPORT, PROPPATCH, PUT, DELETE, POST, COPY, MOVE", "Options match"); 104 | //console.log(body); 105 | } 106 | else 107 | { 108 | t.fail(error); 109 | } 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/cal/put.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2016 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | var moment = require('moment'); 36 | var uuid = require('uuid'); 37 | 38 | var config = require('../../config').config; 39 | 40 | var username = config.test_user_name; 41 | var password = config.test_user_pwd; 42 | 43 | test('Calling PUT on calendar', function (t) { 44 | 45 | t.plan(2); 46 | 47 | var now = moment(); 48 | var uuidEvent = uuid.v4(); 49 | 50 | var payload = "BEGIN:VCALENDAR\r\n"; 51 | payload += "CALSCALE:GREGORIAN.\r\n"; 52 | payload += "PRODID:-//SwordLord - the coding crew.//" + config.version_nr + "//EN.\r\n"; 53 | payload += "VERSION:2.0.\r\n"; 54 | payload += "BEGIN:VTIMEZONE.\r\n"; 55 | payload += "TZID:Europe/Zurich.\r\n"; 56 | payload += "BEGIN:DAYLIGHT.\r\n"; 57 | payload += "DTSTART:19810329T020000.\r\n"; 58 | payload += "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU.\r\n"; 59 | payload += "TZNAME:GMT+2.\r\n"; 60 | payload += "TZOFFSETFROM:+0100.\r\n"; 61 | payload += "TZOFFSETTO:+0200.\r\n"; 62 | payload += "END:DAYLIGHT.\r\n"; 63 | payload += "BEGIN:STANDARD.\r\n"; 64 | payload += "DTSTART:19961027T030000.\r\n"; 65 | payload += "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU.\r\n"; 66 | payload += "TZNAME:GMT+1.\r\n"; 67 | payload += "TZOFFSETFROM:+0200.\r\n"; 68 | payload += "TZOFFSETTO:+0100.\r\n"; 69 | payload += "END:STANDARD.\r\n"; 70 | payload += "END:VTIMEZONE.\r\n"; 71 | payload += "BEGIN:VEVENT.\r\n"; 72 | payload += "CREATED:" + now.format("YMMDD[T]HHmmSS[Z]") + ".\r\n"; 73 | payload += "LAST-MODIFIED:" + now.format("YMMDD[T]HHmmSS[Z]") + ".\r\n"; 74 | payload += "DTSTAMP:" + now.format("YMMDD[T]HHmmSS[Z]") + ".\r\n"; 75 | payload += "DTSTART;TZID=Europe/Zurich:" + now.add(1, "h").format("YMMDD[T]HH0000[Z]") + ".\r\n"; 76 | payload += "DTEND;TZID=Europe/Zurich:" + now.add(1, "h").format("YMMDD[T]HH0000[Z]") + ".\r\n"; 77 | payload += "SEQUENCE:0.\r\n"; 78 | payload += "SUMMARY:Demo Event " + now.format("x") + ".\r\n"; 79 | payload += "TRANSP:OPAQUE.\r\n"; 80 | payload += "UID:" + uuidEvent + ".\r\n"; 81 | payload += "END:VEVENT.\r\n"; 82 | payload += "END:VCALENDAR.\r\n"; 83 | 84 | var options = { 85 | method: 'PUT', 86 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/2A2AF854-18A0-47CB-870A-D94CA2341BAA/" + uuidEvent + ".icf", 87 | auth: { 88 | 'user': username, 89 | 'pass': password, 90 | 'sendImmediately': true 91 | } , 92 | body: payload, 93 | headers: { 94 | 'If-None-Match': '*' 95 | }, 96 | followRedirect: false 97 | } 98 | 99 | request(options, function (error, response, body) { 100 | 101 | if (!error) { 102 | t.equal(response.statusCode, 201, "StatusCode matches"); 103 | // Check ETAG Header 104 | 105 | // resend, should send status 412 now, existing record should not be overwritten 106 | request(options, function (error, response, body) { 107 | 108 | if (!error) { 109 | t.equal(response.statusCode, 412, "StatusCode matches"); 110 | // Check ETAG Header 111 | } 112 | else { 113 | t.fail(error); 114 | } 115 | }); 116 | } 117 | else { 118 | t.fail(error); 119 | } 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /libs/authentication.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014-16 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | 11 | var log = require('../libs/log').log; 12 | var config = require('../config').config; 13 | 14 | var fs = require('fs'); 15 | var path = require('path'); 16 | 17 | function checkLogin(basicAuth, username, password, callback) 18 | { 19 | log.debug("Login process started for user: " + username); 20 | 21 | switch(config.auth_method) 22 | { 23 | case 'courier': 24 | checkCourier(username, password, callback); 25 | break; 26 | 27 | case 'htaccess': 28 | checkHtaccess(basicAuth, username, password, callback); 29 | break; 30 | 31 | case 'ldap': 32 | checkLDAP(username, password, callback); 33 | break; 34 | 35 | default: 36 | log.info("No authentication method defined. Denying access."); 37 | callback(false); 38 | break; 39 | } 40 | } 41 | 42 | function checkHtaccess(basicAuth, username, password, callback) 43 | { 44 | log.debug("Authenticating user with htaccess method."); 45 | 46 | var fHTAccess = path.resolve('.', config.auth_method_htaccess_file); 47 | 48 | if(!fs.existsSync(fHTAccess)) 49 | { 50 | log.warn("File not found for htaccess authentication: " + fHTAccess); 51 | callback(false); 52 | return; 53 | } 54 | 55 | var strHTAccess = fs.readFileSync(fHTAccess, 'utf8'); 56 | var lines = strHTAccess.replace(/\r\n/g, "\n").split("\n"); 57 | 58 | for (var i in lines) 59 | { 60 | var line = lines[i]; 61 | //log.debug("Read line from htaccess file: " + line); 62 | if(line.length > 0) 63 | { 64 | var ret = processLine(line); 65 | if(ret.username == username) 66 | { 67 | if(basicAuth.validate(ret.passwordhash, password)) 68 | { 69 | log.info("User logged in: " + username); 70 | callback(true); 71 | return; 72 | } 73 | } 74 | } 75 | } 76 | 77 | log.warn("User could not be logged in. Wrong username or password: " + username); 78 | callback(false); 79 | } 80 | 81 | function processLine(line) 82 | { 83 | var pwdhash, lineSplit, username; 84 | lineSplit = line.split(":"); 85 | username = lineSplit.shift(); 86 | pwdhash = lineSplit.join(":"); 87 | 88 | return new htaccessLine(username, pwdhash); 89 | } 90 | 91 | function htaccessLine(user, hash) 92 | { 93 | this.username = user; 94 | this.passwordhash = hash; 95 | } 96 | 97 | function checkCourier(username, password, callback) 98 | { 99 | log.debug("Authenticating user with courier method."); 100 | 101 | var socketPath = config.auth_method_courier_socket; 102 | log.debug("Using socket: " + socketPath); 103 | 104 | var client = net.createConnection({path: socketPath}); 105 | 106 | client.on("connect", function() { 107 | //console.log('connect'); 108 | var payload = 'service\nlogin\n' + username + '\n' + password; 109 | client.write('AUTH ' + payload.length + '\n' + payload); 110 | }); 111 | 112 | var response = ""; 113 | 114 | client.on("data", function(data) { 115 | //console.log('data: ' + data); 116 | response += data.toString(); 117 | }); 118 | 119 | client.on('end', function() { 120 | var result = response.indexOf('FAIL', 0); 121 | callback(result < 0); 122 | }); 123 | } 124 | 125 | function checkLDAP(username, password, callback) 126 | { 127 | var ldapjs; 128 | 129 | log.debug('Authenticating user with ldap method.'); 130 | 131 | try { 132 | ldapjs = require('ldapjs'); 133 | } catch (e) { 134 | log.error('ldapjs@1.0.0 node module not found'); 135 | return callback(false); 136 | } 137 | 138 | var ldapClient = ldapjs.createClient({ url: config.auth_method_ldap_url }); 139 | ldapClient.on('error', function (error) { 140 | log.warn('LDAP error', error); 141 | callback(false); 142 | }); 143 | 144 | var ldapDn = 'cn=' + username + ',' + config.auth_method_ldap_user_base_dn; 145 | ldapClient.bind(ldapDn, password, function (error) { 146 | if (error) { 147 | log.warn('User could not be logged in. Wrong username or password: ' + username); 148 | callback(false); 149 | } else { 150 | log.info('User logged in: ' + username); 151 | callback(true); 152 | } 153 | }); 154 | } 155 | 156 | // Exporting. 157 | module.exports = { 158 | checkLogin: checkLogin 159 | }; -------------------------------------------------------------------------------- /libs/db.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014-16 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | 11 | var log = require('../libs/log').log; 12 | var config = require('../config').config; 13 | 14 | var Sequelize = require('sequelize'); 15 | 16 | /** 17 | * 18 | * @type {*|Promise.Sequelize|Sequelize} 19 | */ 20 | var sequelize = new Sequelize(config.db_name, config.db_uid, config.db_pwd, { 21 | host: config.db_host || 'localhost', 22 | dialect: config.db_dialect, 23 | logging: function( info ) {if(config.db_logging){log.info(info)}}, // thanks to mdarveau for the fix 24 | storage: config.db_storage 25 | }); 26 | 27 | /** 28 | * 29 | * @type {Model} 30 | * represents an event object with a start and an end date 31 | */ 32 | var ICS = sequelize.define('ICS', { 33 | pkey: { type: Sequelize.STRING, allowNull: false, unique: true, primaryKey: true}, 34 | calendarId: { type: Sequelize.STRING, allowNull: false}, 35 | startDate: { type: Sequelize.DATE, allowNull: false}, 36 | endDate: { type: Sequelize.DATE, allowNull: false}, 37 | content: { type: Sequelize.TEXT, allowNull: false} 38 | }); 39 | 40 | /** 41 | * 42 | * @type {Model} 43 | * represents a calendar object containing events (ics) 44 | */ 45 | var CAL = sequelize.define('CAL', { 46 | pkey: { type: Sequelize.STRING, allowNull: false, unique: true, primaryKey: true}, 47 | owner: { type: Sequelize.STRING, allowNull: false}, 48 | timezone: { type: Sequelize.TEXT, allowNull: false}, 49 | order: { type: Sequelize.STRING, allowNull: false}, 50 | free_busy_set: { type: Sequelize.STRING, allowNull: false}, 51 | supported_cal_component: { type: Sequelize.STRING, allowNull: false}, 52 | colour: { type: Sequelize.STRING, allowNull: false}, 53 | displayname: { type: Sequelize.STRING, allowNull: false}, 54 | synctoken: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0} 55 | }); 56 | 57 | /** 58 | * 59 | * @type {Model} 60 | * represents an address (vcard) object 61 | */ 62 | var VCARD = sequelize.define('VCARD', { 63 | pkey: { type: Sequelize.STRING, allowNull: false, unique: true, primaryKey: true}, 64 | ownerId: { type: Sequelize.STRING, allowNull: false}, 65 | addressbookId: { type: Sequelize.STRING, allowNull: false}, 66 | content: { type: Sequelize.TEXT, allowNull: false}, 67 | is_group: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false} 68 | }); 69 | 70 | /** 71 | * 72 | * @type {Model} 73 | * represents an addressbook containing addresses (vcard) 74 | */ 75 | var ADDRESSBOOK = sequelize.define('ADB', { 76 | pkey: { type: Sequelize.STRING, allowNull: false, unique: true, primaryKey: true}, 77 | ownerId: { type: Sequelize.STRING, allowNull: false}, 78 | name: { type: Sequelize.STRING, allowNull: false}, 79 | synctoken: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0} 80 | }); 81 | 82 | /** 83 | * 84 | * @type {Model} 85 | * users are grouped into usergroup. this is the binding table between users -n--n- groups 86 | */ 87 | var USER_GROUP = sequelize.define('USER_GROUP', { 88 | userId: { type: Sequelize.STRING, allowNull: false, unique: false, primaryKey: true}, 89 | groupId: { type: Sequelize.STRING, allowNull: false, unique: false, primaryKey: true}, 90 | description: { type: Sequelize.TEXT, allowNull: true} 91 | }); 92 | 93 | /** 94 | * 95 | * @type {Model} 96 | * a group of users which can have specific authorisation rules and permissions. 97 | * a group is somewhat a role in RBAC. 98 | */ 99 | var GROUP = sequelize.define('GROUP', { 100 | groupId: { type: Sequelize.STRING, allowNull: false, unique: false, primaryKey: true}, 101 | description: { type: Sequelize.TEXT, allowNull: true} 102 | }); 103 | 104 | /** 105 | * 106 | * @type {Model} 107 | * a permission for a specific group. Permission has always this structure: 108 | * 109 | * entrypoint(card vs calendar):path:to:element:action 110 | * 111 | * Users need to have respective permission to execute specific action 112 | */ 113 | var PERMISSION = sequelize.define('PERMISSION', { 114 | permissionId: { type: Sequelize.STRING, allowNull: false, unique: true, primaryKey: true}, 115 | groupId: { type: Sequelize.STRING, allowNull: false}, 116 | permission: { type: Sequelize.TEXT, allowNull: false} 117 | }); 118 | 119 | 120 | function _getPermission(user) 121 | { 122 | // get groups from user 123 | // get permissions from groups 124 | } 125 | 126 | sequelize.sync().then(function() 127 | { 128 | log.info("Database structure updated"); 129 | }).error(function(error) 130 | { 131 | log.error("Database structure update crashed: " + error); 132 | } 133 | ); 134 | 135 | // Exporting. 136 | module.exports = { 137 | ICS: ICS, 138 | CAL: CAL, 139 | VCARD: VCARD, 140 | ADB: ADDRESSBOOK, 141 | getPermission: function (user) { 142 | return _getPermission(user); 143 | }, 144 | sequelize: sequelize 145 | }; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014-16 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This program is free software; you can redistribute it and/or modify it 10 | ** under the terms of the GNU Affero General Public License as published by the Free 11 | ** Software Foundation, either version 3 of the License, or (at your option) 12 | ** any later version. 13 | ** 14 | ** This program is distributed in the hope that it will be useful, but WITHOUT 15 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 16 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 17 | ** more details. 18 | ** 19 | ** You should have received a copy of the GNU Affero General Public License along 20 | ** with this program. If not, see . 21 | ** 22 | **----------------------------------------------------------------------------- 23 | ** 24 | ** Original Authors: 25 | ** LordEidi@swordlord.com 26 | ** LordLightningBolt@swordlord.com 27 | ** 28 | ** $Id: 29 | ** 30 | -----------------------------------------------------------------------------*/ 31 | var config = require('./config').config; 32 | var authlib = require('./libs/authentication'); 33 | var http = require('http'); 34 | var url = require('url'); 35 | var log = require('./libs/log').log; 36 | var handler = require('./libs/requesthandler'); 37 | 38 | var communication = require('./libs/communication'); 39 | var httpauth = require('http-auth'); 40 | 41 | var crossroads = require('crossroads'); 42 | crossroads.ignoreState = true; 43 | 44 | var basic = httpauth.basic( 45 | { 46 | realm: "Fennel" 47 | }, function (username, password, callback) 48 | { 49 | authlib.checkLogin(basic, username, password, callback); 50 | } 51 | ); 52 | 53 | /** 54 | * Called when the URL is not matched against any known/defined pattern 55 | * @param comm 56 | * @param path 57 | */ 58 | function onBypass(comm, path) 59 | { 60 | log.info('URL unknown: ' + path); 61 | 62 | var res = comm.getRes(); 63 | 64 | res.writeHead(500); 65 | res.write(comm.url + " is not known"); 66 | res.end(); 67 | } 68 | 69 | /** 70 | * Gets called when the / URL is hit 71 | * @param comm 72 | */ 73 | function onHitRoot(comm) 74 | { 75 | log.debug("Called the root. Redirecting to /p/"); 76 | 77 | comm.getRes().writeHead(302, 78 | { 79 | 'Location': '/p/' 80 | //todo: add other headers here...? 81 | }); 82 | comm.flushResponse(); 83 | } 84 | 85 | function onHitWellKnown(comm, params) 86 | { 87 | log.debug("Called .well-known URL for " + params + ". Redirecting to /p/"); 88 | 89 | comm.getRes().writeHead(302, 90 | { 91 | 'Location': '/p/' 92 | //todo: add other headers here...? 93 | }); 94 | comm.flushResponse(); 95 | } 96 | 97 | /** 98 | * Gets called when /p is hit 99 | * @param comm 100 | * @param params 101 | */ 102 | function onHitPrincipal(comm, params) 103 | { 104 | comm.params = params; 105 | 106 | // check authorisation 107 | if(!comm.checkPermission(comm.getURL(), comm.getReq().method)) 108 | { 109 | var res = comm.getRes(); 110 | log.info("Request is denied to this user"); 111 | res.writeHead(403); 112 | res.write("Request is denied to this user"); 113 | return; 114 | } 115 | 116 | handler.handlePrincipal(comm); 117 | } 118 | 119 | /** 120 | * Gets called when /cal is hit 121 | * @param comm 122 | * @param username 123 | * @param cal 124 | * @param params 125 | */ 126 | function onHitCalendar(comm, username, cal, params) 127 | { 128 | comm.username = username; 129 | comm.cal = cal; 130 | comm.params = params; 131 | 132 | // check authorisation 133 | if(!comm.checkPermission(comm.getURL(), comm.getReq().method)) 134 | { 135 | var res = comm.getRes(); 136 | log.info("Request is denied to this user"); 137 | res.writeHead(403); 138 | res.write("Request is denied to this user"); 139 | return; 140 | } 141 | 142 | handler.handleCalendar(comm); 143 | } 144 | 145 | /** 146 | * Gets called when /card is hit 147 | * @param comm 148 | * @param username 149 | * @param card 150 | * @param params 151 | */ 152 | function onHitCard(comm, username, card, params) 153 | { 154 | comm.username = username; 155 | comm.card = card; 156 | comm.params = params; 157 | 158 | // check authorisation 159 | if(!comm.checkPermission(comm.getURL(), comm.getReq().method)) 160 | { 161 | var res = comm.getRes(); 162 | log.info("Request is denied to this user"); 163 | res.writeHead(403); 164 | res.write("Request is denied to this user"); 165 | return; 166 | } 167 | 168 | handler.handleCard(comm); 169 | } 170 | 171 | crossroads.addRoute('/p/:params*:', onHitPrincipal); 172 | crossroads.addRoute('/cal/:username:/:cal:/:params*:', onHitCalendar); 173 | crossroads.addRoute('/card/:username:/:card:/:params*:', onHitCard); 174 | crossroads.addRoute('/.well-known/:params*:', onHitWellKnown); 175 | crossroads.addRoute('/', onHitRoot); 176 | crossroads.bypassed.add(onBypass); 177 | 178 | // start the server and process requests 179 | var server = http.createServer(basic, function (req, res) 180 | { 181 | log.debug("Method: " + req.method + ", URL: " + req.url); 182 | 183 | // will contain the whole body submitted 184 | var reqBody = ""; 185 | 186 | req.on('data', function (data) 187 | { 188 | reqBody += data.toString(); 189 | }); 190 | 191 | req.on('end',function() 192 | { 193 | var comm = new communication(req, res, reqBody); 194 | 195 | var sUrl = url.parse(req.url).pathname; 196 | log.debug("Request body: " + reqBody); 197 | crossroads.parse(sUrl, [comm]); 198 | }); 199 | }); 200 | 201 | server.listen(config.port); 202 | 203 | server.on('error', function (e) 204 | { 205 | log.warn('Caught error: ' + e.message); 206 | log.debug(e.stack); 207 | }); 208 | 209 | process.on('uncaughtException', function(err) 210 | { 211 | log.warn('Caught exception: ' + err.message); 212 | log.debug(err.stack); 213 | }); 214 | 215 | // Put a friendly message on the terminal 216 | log.info("Server running at http://" + config.ip + ":" + config.port + "/"); 217 | -------------------------------------------------------------------------------- /test/principal/propfind.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2015-16 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | var xml = require("libxmljs"); 36 | 37 | var config = require('../../config').config; 38 | 39 | var username = config.test_user_name; 40 | var password = config.test_user_pwd; 41 | 42 | test('Calling PROPFIND on principal user', function (t) { 43 | 44 | t.plan(9); 45 | 46 | var payload = "\r\n"; 47 | payload += "\r\n"; 48 | payload += "\r\n"; 49 | payload += "\r\n"; 50 | payload += "\r\n"; 51 | payload += "\r\n"; 52 | payload += "\r\n"; 53 | payload += "\r\n"; 54 | payload += "\r\n"; 55 | payload += "\r\n"; 56 | payload += "\r\n"; 57 | payload += "\r\n"; 58 | payload += "\r\n"; 59 | payload += "\r\n"; 60 | 61 | var options = { 62 | method: 'PROPFIND', 63 | uri: "http://" + config.ip + ":" + config.port + "/p/" + username + "/", 64 | auth: { 65 | 'user': username, 66 | 'pass': password, 67 | 'sendImmediately': true 68 | } , 69 | body: payload, 70 | followRedirect: false 71 | } 72 | 73 | request(options, function (error, response, body) { 74 | 75 | if (!error) { 76 | t.equal(response.statusCode, 207, "StatusCode matches"); 77 | 78 | console.log(response.headers); 79 | t.equal(response.headers.dav, '1, 3, extended-mkcol, calendar-access, calendar-schedule, calendar-proxy, calendarserver-sharing, calendarserver-subscribed, addressbook, access-control, calendarserver-principal-property-search', "DAV header matches"); 80 | 81 | var xmlDoc = xml.parseXml(body); 82 | 83 | var nodeHref = xmlDoc.get('/D:multistatus/D:response/D:href', { D: 'DAV:' } ); 84 | t.doesNotEqual(nodeHref, undefined, "href node exists"); 85 | 86 | t.ok(nodeHref.text().match(/^\/p\/[a-z0-9]+\/$/g), "href has right URL"); 87 | 88 | var nodeStatusCode = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:status', { D: 'DAV:' } ); 89 | t.doesNotEqual(nodeStatusCode, undefined, "status node exists"); 90 | 91 | t.ok(nodeStatusCode.text().match(/^HTTP\/1.1 200 OK/g), "statuscode node has right text"); 92 | 93 | var nodeCalUserAddSet = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:prop/CAL:calendar-user-address-set', { D: 'DAV:', CAL: 'urn:ietf:params:xml:ns:caldav' } ); 94 | t.doesNotEqual(nodeCalUserAddSet, undefined, "calendar-user-address-set node exists"); 95 | 96 | var nodeCardABHomeSet = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:prop/CARD:addressbook-home-set', { D: 'DAV:', CARD: 'urn:ietf:params:xml:ns:carddav' } ); 97 | t.doesNotEqual(nodeCardABHomeSet, undefined, "card_addressbook_home_set node exists"); 98 | 99 | var nodeDisplayName = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:prop/D:displayname', { D: 'DAV:' } ); 100 | t.doesNotEqual(nodeDisplayName, undefined, "displayname node exists"); 101 | 102 | //var nodeEmail = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:prop/CS:email-address-set', { D: 'DAV:', CS: 'http://calendarserver.org/ns' } ); 103 | //t.doesNotEqual(nodeEmail, undefined, "email-address node exists"); 104 | 105 | //console.log(body); 106 | } 107 | else { 108 | t.fail(); 109 | } 110 | }); 111 | }); 112 | 113 | /* 114 | PROPFIND /p/_userid_/ HTTP/1.1 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | */ 130 | 131 | /* 132 | * 133 | * /PRINCIPAL/ID 134 | * 135 | query: /p/_userid_/ 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | ret 146 | 147 | 148 | /p/_userid_/ 149 | 150 | 151 | 152 | mailto:email 153 | /principals/uid/_userid_/ 154 | 155 | user name 156 | 157 | email 158 | 159 | 160 | HTTP/1.1 200 OK 161 | 162 | 163 | 164 | 165 | */ -------------------------------------------------------------------------------- /libs/communication.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014-17 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | 11 | var log = require('../libs/log').log; 12 | var config = require('../config').config; 13 | 14 | var userLib = require('../libs/user'); 15 | var url = require('url'); 16 | 17 | var pd = require('pretty-data').pd; 18 | 19 | // shiro-trie object for authorisation checks 20 | var st = require('shiro-trie'); 21 | 22 | // Exporting. 23 | /** 24 | * 25 | * @type {comm} 26 | */ 27 | module.exports = comm; 28 | 29 | /** 30 | * generates a new request object, constructor like 31 | * @param req 32 | * @param res 33 | * @param reqBody 34 | * @returns {request} 35 | */ 36 | function comm(req, res, reqBody) 37 | { 38 | // request object as well as body 39 | this.req = req; 40 | this.reqBody = reqBody; 41 | 42 | // response object as well as body we gonna write ourselfs 43 | this.res = res; 44 | this.resBody = ""; // response body 45 | 46 | // get the user name from the authentication process 47 | var header = req.headers['authorization']||''; // get the header 48 | var token = header.split(/\s+/).pop()||''; // and the encoded auth token 49 | var auth = new Buffer(token, 'base64').toString(); // convert from base64 50 | var parts = auth.split(/:/); // split on colon 51 | var username = parts[0]; 52 | 53 | this.user = new userLib.user(username); 54 | 55 | this.authority = st.new(); 56 | 57 | // get shiro-trie configuration from config file 58 | var arrAuthorisation = config.authorisation; 59 | 60 | for(var i = 0; i < arrAuthorisation.length; i++) 61 | { 62 | var el = arrAuthorisation[i]; 63 | this.authority.add(el.replace("$username", username)); 64 | } 65 | 66 | return this; 67 | } 68 | 69 | /** 70 | * pushes out the standard OPTIONS response headers and flushes the response 71 | */ 72 | comm.prototype.pushOptionsResponse = function() 73 | { 74 | log.debug("pushOptionsResponse called"); 75 | 76 | // TODO comm.setstandardheader... 77 | this.setHeader("Content-Type", "text/html"); 78 | this.setHeader("Server", "Fennel"); 79 | 80 | this.setDAVHeaders(); 81 | this.setAllowHeader(); 82 | 83 | this.setResponseCode(200); 84 | this.flushResponse(); 85 | }; 86 | 87 | 88 | /** 89 | * Sets the response code given 90 | * @param responseCode 91 | */ 92 | comm.prototype.setResponseCode = function(responseCode) 93 | { 94 | log.info("Setting response code: " + responseCode); 95 | this.res.writeHead(responseCode); 96 | }; 97 | 98 | /** 99 | * Writes out the body and sends a response.end 100 | */ 101 | comm.prototype.flushResponse = function() 102 | { 103 | // prettify XML when we have XML in the body 104 | var response = this.resBody; 105 | 106 | if(response.substr(0, 5) === " v7.0 and on OSX Calendar as well as with Mozilla Lightning. If you run 31 | **Fennel** with another client your mileage may vary. 32 | 33 | What's missing: 34 | 35 | - different clients (we will somewhen test with other clients, but we did not do thoroughly yet) 36 | - Test cases for everything. We would love to have test cases for as many scenarios and features as possible. It is a pain in the neck to test **Fennel** otherwise. 37 | - While **Fennel**'s goal is to have an RBAC based authorisation system, **Fennel** does currently only know global permissions without groups. 38 | 39 | ## Installation ## 40 | 41 | First of all, you need a Node.js installation. 42 | 43 | ### nodejs on Debian ### 44 | 45 | Make sure that you have this line in your /etc/apt/sources.list file: 46 | 47 | deb http://YOURMIRROR.debian.org/debian jessie main 48 | 49 | and then run: 50 | 51 | sudo apt-get install nodejs nodejs-legacy npm 52 | // eventually the next line as well 53 | // sudo ln -s /usr/lib/nodejs/ /usr/lib/node 54 | 55 | ### nodejs on OSX with homebrew ### 56 | 57 | brew install node 58 | brew install npm 59 | 60 | ### Installation of **Fennel** ### 61 | 62 | If you want to run **Fennel** under a specific user (node), do this: 63 | 64 | sudo adduser node 65 | su node 66 | cd 67 | mkdir fennel 68 | cd fennel 69 | 70 | Go into the directory where you want to run your copy of **Fennel** and get the latest and greatest: 71 | 72 | cd /home/node/fennel 73 | git clone https://github.com/LordEidi/fennel.git 74 | 75 | And then with the magic of npm get the required libraries 76 | 77 | npm install 78 | 79 | If everything worked according to plan, you should now have a new installation of the latest **Fennel**. 80 | 81 | ### Use supervisord to run **Fennel** as a service ### 82 | 83 | Now we want to make sure that **Fennel** runs forever. First install the required software: 84 | 85 | sudo apt-get install supervisor 86 | 87 | Then copy the file utilities/fennel_supervisor.conf into your local supervisor configuration directory. This is usually done like this: 88 | 89 | cp utilities/fennel_supervisor.conf /etc/supervisor/conf.d/fennel.conf 90 | 91 | Make sure you change the configuration to your local setup. 92 | 93 | ### How to set up transport security ### 94 | 95 | Since **Fennel** does not bring it's own crypto, you may need to install a TLS server in front of **Fennel**. You can do so 96 | with nginx, which is a lightweight http server and proxy. 97 | 98 | First prepare your /etc/apt/sources.list file (or just install the standard Debian package, your choice): 99 | 100 | deb http://nginx.org/packages/debian/ jessie nginx 101 | deb-src http://nginx.org/packages/debian/ jessie nginx 102 | 103 | Update apt-cache and install nginx to your system. 104 | 105 | sudo update 106 | sudo apt-get install nginx 107 | 108 | Now configure a proxy configuration so that your instance of nginx will serve / prox the content of / for the 109 | **Fennel** server. To do so, you will need a configuration along this example: 110 | 111 | server { 112 | listen 443; 113 | server_name carl.yourdomain.tld; 114 | 115 | access_log /var/www/logs/fennel_access.log combined; 116 | error_log /var/www/logs/fennel_error.log; 117 | 118 | root /var/www/pages/; 119 | index index.html index.htm; 120 | 121 | error_page 500 502 503 504 /50x.html; 122 | location = /50x.html { 123 | root /var/www/nginx-default; 124 | } 125 | 126 | location / { 127 | proxy_pass http://127.0.0.1:8888; 128 | proxy_redirect off; 129 | proxy_set_header Host $host; 130 | proxy_set_header X-Real-IP $remote_addr; 131 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 132 | proxy_buffering off; 133 | } 134 | 135 | ssl on; 136 | ssl_certificate /etc/nginx/certs/yourdomain.tld.pem; 137 | ssl_certificate_key /etc/nginx/certs/yourdomain.tld.pem; 138 | ssl_session_timeout 5m; 139 | 140 | # modern configuration. tweak to your needs. 141 | ssl_protocols TLSv1.1 TLSv1.2; 142 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; 143 | ssl_prefer_server_ciphers on; 144 | 145 | # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) 146 | add_header Strict-Transport-Security max-age=15768000; 147 | } 148 | 149 | Please check this site for updates on what TLS settings currently make sense: 150 | 151 | [https://mozilla.github.io/server-side-tls/ssl-config-generator](https://mozilla.github.io/server-side-tls/ssl-config-generator) 152 | 153 | Now run or reset your nginx and start your instance of **Fennel**. 154 | 155 | Thats it, your instance of **Fennel** should run now. All logs are sent to stdout for now. Have a look at */libs/log.js* if 156 | you want to change the options. 157 | 158 | ## Configuration ## 159 | 160 | All parameters which can be configured right now are in the file *config.js*. There are not much parameters yet, indeed. 161 | But **Fennel** is not ready production anyway. And you are welcome to help out in adding parameters and configuration 162 | options. 163 | 164 | ### Authentication - auth_method ### 165 | 166 | The authentication method to use to authenticate users. Supported methods so far: 167 | 168 | - Courier: use a local courier authdaemon socket. You will need to fill in this config option as well: auth_method_courier_socket 169 | - htaccess: use an htaccess file to authenticate the users. You will need to fill in this option as well: auth_method_htaccess_file 170 | - LDAP: use an ldap server to authenticate users. You will need to fill in the options auth_method_ldap_url and auth_method_ldap_user_base_dn and make sure to have ldapjs@1.0.0 installed. 171 | 172 | 173 | ### Authorisation ### 174 | 175 | **Fennel**'s authorisation strategy is based on the npm module shiro-trie. Permissions are written in an [Apache Shiro](http://shiro.apache.org/permissions.html)-like style. 176 | 177 | Permission is given on an URL and http method basis. Which means permissions can be given on a specific URL and method or with a wildcard character on multiple URLs and methods. 178 | 179 | When defining your own permissions, make sure to change the URL syntax to the shiro syntax. Which means that /my/url/ becomes my:url. See the standard configuration for details. 180 | 181 | While **Fennel**'s goal is to have an RBAC based authorisation system, **Fennel** does currently only know global permissions without groups. See also [Contribution](Contribution). 182 | 183 | ## How to run ## 184 | 185 | Point your CalDAV and CardDAV client to the root of **Fennel**. All the rest should be managed by **Fennel** via 186 | *./well-known* URLs and the chat between your client and **Fennel**. 187 | 188 | 189 | ## Test cases ## 190 | 191 | There are a few test cases to check the working of **Fennel**. Make sure to 192 | 193 | - check your configuration, especially the test user and password. 194 | - run an instance of **Fennel**. 195 | - run: npm test 196 | 197 | You can find the test cases in the test directory. All cases are made to be run against your (local) copy of **Fennel**. 198 | 199 | 200 | ## Contribution ## 201 | 202 | If you happen to know how to write JavaScript, documentation or can help out with something else, drop us a note at *contact at swordlord dot com*. As more helping hands we have, as quicker this server gets up and feature complete. 203 | 204 | If some feature is missing, just remember that this is an Open Source Project. If you need something, think about contributing it yourself... 205 | 206 | 207 | ## Dependencies ## 208 | 209 | For now, have a look at the package.json file. 210 | 211 | If you: 212 | - run the test cases, make sure that you installed the dev dependencies... 213 | - use the LDAP authentication method, make sure to install ldapjs@1.0.0 node module with "npm install ldapjs@1.0.0". 214 | 215 | ## License ## 216 | 217 | **Fennel** is published under the GNU Affero General Public Licence version 3. See the LICENCE file for details. -------------------------------------------------------------------------------- /handler/principal.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014-17 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | 11 | var xml = require("libxmljs"); 12 | var xh = require("../libs/xmlhelper"); 13 | var log = require('../libs/log').log; 14 | 15 | // Exporting. 16 | module.exports = { 17 | propfind: propfind, 18 | proppatch: proppatch, 19 | report: report, 20 | options: options 21 | }; 22 | 23 | function propfind(comm) 24 | { 25 | log.debug("principal.propfind called"); 26 | 27 | comm.setStandardHeaders(); 28 | comm.setDAVHeaders(); 29 | comm.setResponseCode(207); 30 | 31 | comm.appendResBody(xh.getXMLHead()); 32 | 33 | var body = comm.getReqBody(); 34 | var xmlDoc = xml.parseXml(body); 35 | 36 | var node = xmlDoc.get('/A:propfind/A:prop', { 37 | A: 'DAV:', 38 | B: "urn:ietf:params:xml:ns:caldav", 39 | C: 'http://calendarserver.org/ns/', 40 | D: "http://apple.com/ns/ical/", 41 | E: "http://me.com/_namespace/" 42 | }); 43 | var childs = node.childNodes(); 44 | 45 | var response = ""; 46 | 47 | var len = childs.length; 48 | for (var i=0; i < len; ++i) 49 | { 50 | var child = childs[i]; 51 | var name = child.name(); 52 | switch(name) 53 | { 54 | case 'checksum-versions': 55 | response += ""; 56 | break; 57 | 58 | case 'sync-token': 59 | response += "http://sabredav.org/ns/sync/5"; 60 | break; 61 | 62 | case 'supported-report-set': 63 | response += getSupportedReportSet(comm); 64 | break; 65 | 66 | case 'principal-URL': 67 | response += "/p/" + comm.getUser().getUserName() + "/\r\n"; 68 | break; 69 | 70 | case 'displayname': 71 | response += "" + comm.getUser().getUserName() + ""; 72 | break; 73 | 74 | case 'principal-collection-set': 75 | response += "/p/"; 76 | break; 77 | 78 | case 'current-user-principal': 79 | response += "/p/" + comm.getUser().getUserName() + "/"; 80 | break; 81 | 82 | case 'calendar-home-set': 83 | response += "/cal/" + comm.getUser().getUserName() + ""; 84 | break; 85 | 86 | case 'schedule-outbox-URL': 87 | response += "/cal/" + comm.getUser().getUserName() + "/outbox"; 88 | break; 89 | 90 | case 'calendar-user-address-set': 91 | response += getCalendarUserAddressSet(comm); 92 | break; 93 | 94 | case 'notification-URL': 95 | response += "/cal/" + comm.getUser().getUserName() + "/notifications/"; 96 | break; 97 | 98 | case 'getcontenttype': 99 | response += ""; 100 | break; 101 | 102 | case 'addressbook-home-set': 103 | response += "/card/" + comm.getUser().getUserName() + "/"; 104 | break; 105 | 106 | case 'directory-gateway': 107 | response += ""; 108 | break; 109 | case 'email-address-set': 110 | response += "lord test at swordlord.com"; 111 | break; 112 | case 'resource-id': 113 | response += ""; 114 | break; 115 | 116 | default: 117 | if(name != 'text') log.warn("P-PF: not handled: " + name); 118 | break; 119 | } 120 | } 121 | 122 | comm.appendResBody(""); 123 | comm.appendResBody("" + comm.getURL() + ""); 124 | comm.appendResBody(""); 125 | comm.appendResBody(""); 126 | comm.appendResBody(response); 127 | comm.appendResBody(""); 128 | comm.appendResBody("HTTP/1.1 200 OK"); 129 | comm.appendResBody(""); 130 | comm.appendResBody(""); 131 | comm.appendResBody(""); 132 | 133 | comm.flushResponse(); 134 | } 135 | 136 | function getCalendarUserAddressSet(comm) 137 | { 138 | var response = ""; 139 | 140 | response += " \r\n"; 141 | response += " mailto:lord test at swordlord.com\r\n"; 142 | response += " /p/" + comm.getUser().getUserName() + "/\r\n"; 143 | response += " \r\n"; 144 | 145 | return response; 146 | } 147 | 148 | function getSupportedReportSet(comm) 149 | { 150 | var response = ""; 151 | response += " \r\n"; 152 | response += " \r\n"; 153 | response += " \r\n"; 154 | response += " \r\n"; 155 | response += " \r\n"; 156 | response += " \r\n"; 157 | response += " \r\n"; 158 | response += " \r\n"; 159 | response += " \r\n"; 160 | response += " \r\n"; 161 | response += " \r\n"; 162 | response += " \r\n"; 163 | response += " \r\n"; 164 | response += " \r\n"; 165 | response += " \r\n"; 166 | response += " \r\n"; 167 | response += " \r\n"; 168 | 169 | return response; 170 | } 171 | 172 | function options(comm) 173 | { 174 | log.debug("principal.options called"); 175 | 176 | comm.pushOptionsResponse(); 177 | } 178 | 179 | function report(comm) 180 | { 181 | log.debug("principal.report called"); 182 | 183 | comm.setStandardHeaders(); 184 | 185 | var body = comm.getReqBody(); 186 | if(!body) 187 | { 188 | log.warn("principal.report called with no body"); 189 | 190 | comm.setResponseCode(500); 191 | comm.appendResBody("Internal Server Error"); 192 | comm.flushResponse(); 193 | return; 194 | } 195 | 196 | comm.setResponseCode(200); 197 | comm.appendResBody(xh.getXMLHead()); 198 | 199 | var xmlDoc = xml.parseXml(body); 200 | 201 | var node = xmlDoc.get('/A:propfind/A:prop', { 202 | A: 'DAV:', 203 | B: "urn:ietf:params:xml:ns:caldav", 204 | C: 'http://calendarserver.org/ns/', 205 | D: "http://apple.com/ns/ical/", 206 | E: "http://me.com/_namespace/" 207 | }); 208 | 209 | var response = ""; 210 | 211 | if(node != undefined) 212 | { 213 | var childs = node.childNodes(); 214 | 215 | var len = childs.length; 216 | for (var i=0; i < len; ++i) 217 | { 218 | var child = childs[i]; 219 | var name = child.name(); 220 | switch(name) 221 | { 222 | case 'principal-search-property-set': 223 | response += getPrincipalSearchPropertySet(comm); 224 | break; 225 | 226 | default: 227 | if(name != 'text') log.warn("P-R: not handled: " + name); 228 | break; 229 | } 230 | } 231 | } 232 | 233 | node = xmlDoc.get('/A:principal-search-property-set', { 234 | A: 'DAV:', 235 | B: "urn:ietf:params:xml:ns:caldav", 236 | C: 'http://calendarserver.org/ns/', 237 | D: "http://apple.com/ns/ical/", 238 | E: "http://me.com/_namespace/" 239 | }); 240 | 241 | if(node != undefined) 242 | { 243 | var name = node.name(); 244 | switch(name) 245 | { 246 | case 'principal-search-property-set': 247 | response += getPrincipalSearchPropertySet(comm); 248 | break; 249 | 250 | default: 251 | if(name != 'text') log.warn("P-R: not handled: " + name); 252 | break; 253 | } 254 | } 255 | 256 | // TODO: clean up 257 | comm.appendResBody(response); 258 | 259 | if(isReportPropertyCalendarProxyWriteFor(comm)) 260 | { 261 | replyPropertyCalendarProxyWriteFor(comm); 262 | } 263 | 264 | comm.flushResponse(); 265 | } 266 | 267 | 268 | function getPrincipalSearchPropertySet(comm) 269 | { 270 | var response = ""; 271 | response += "\r\n"; 272 | response += " \r\n"; 273 | response += " \r\n"; 274 | response += " \r\n"; 275 | response += " \r\n"; 276 | response += " Display name\r\n"; 277 | response += " \r\n"; 278 | // response += " \r\n"; 279 | // response += " \r\n"; 280 | // response += " \r\n"; 281 | // response += " \r\n"; 282 | // response += " Email address\r\n"; 283 | // response += " \r\n"; 284 | response += "\r\n"; 285 | 286 | return response; 287 | } 288 | 289 | 290 | function isReportPropertyCalendarProxyWriteFor(comm) 291 | { 292 | var body = comm.getReqBody(); 293 | var xmlDoc = xml.parseXml(body); 294 | 295 | var node = xmlDoc.get('/A:expand-property/A:property[@name=\'calendar-proxy-write-for\']', { A: 'DAV:', C: 'http://calendarserver.org/ns/'}); 296 | 297 | return typeof node != 'undefined'; 298 | } 299 | 300 | function replyPropertyCalendarProxyWriteFor(comm) 301 | { 302 | var url = comm.getURL(); 303 | comm.appendResBody("\r\n"); 304 | comm.appendResBody(""); 305 | comm.appendResBody(" " + url + ""); 306 | comm.appendResBody(" "); 307 | comm.appendResBody(" "); 308 | comm.appendResBody(" "); 309 | comm.appendResBody(" "); 310 | comm.appendResBody(" "); 311 | comm.appendResBody(" HTTP/1.1 200 OK"); 312 | comm.appendResBody(" "); 313 | comm.appendResBody(""); 314 | comm.appendResBody("\r\n"); 315 | } 316 | 317 | function proppatch(comm) 318 | { 319 | log.debug("principal.proppatch called"); 320 | 321 | comm.setStandardHeaders(comm); 322 | 323 | var url = comm.getURL(); 324 | comm.setResponseCode(200); 325 | 326 | comm.appendResBody(""); 327 | comm.appendResBody("\r\n"); 328 | comm.appendResBody(" \r\n"); 329 | comm.appendResBody(" " + url + "\r\n"); 330 | comm.appendResBody(" \r\n"); 331 | comm.appendResBody(" \r\n"); 332 | comm.appendResBody(" \r\n"); 333 | comm.appendResBody(" \r\n"); 334 | comm.appendResBody(" HTTP/1.1 403 Forbidden\r\n"); 335 | comm.appendResBody(" \r\n"); 336 | comm.appendResBody(" \r\n"); 337 | comm.appendResBody("\r\n"); 338 | 339 | comm.flushResponse(); 340 | } 341 | 342 | -------------------------------------------------------------------------------- /test/cal/report.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2016 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | var xml = require("libxmljs"); 36 | var moment = require('moment'); 37 | 38 | var config = require('../../config').config; 39 | 40 | var username = config.test_user_name; 41 | var password = config.test_user_pwd; 42 | 43 | // will be filled by a report query before use 44 | var ics_file = ""; 45 | 46 | test('Calling REPORT synctoken on calendar', function (t) { 47 | 48 | var payload = "\n\r"; 49 | 50 | payload += "\n\r"; 51 | payload += "http://www.swordlord.org/ns/sync/20\n\r"; 52 | payload += "1\n\r"; 53 | payload += "\n\r"; 54 | payload += "\n\r"; 55 | payload += "\n\r"; 56 | payload += "\n\r"; 57 | payload += "\n\r"; 58 | 59 | var options = { 60 | method: 'REPORT', 61 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/2A2AF854-18A0-47CB-870A-D94CA2341BAA", 62 | auth: { 63 | 'user': username, 64 | 'pass': password, 65 | 'sendImmediately': true 66 | } , 67 | body: payload, 68 | followRedirect: false 69 | } 70 | 71 | request(options, function (error, response, body) { 72 | 73 | if (!error) 74 | { 75 | t.plan(3); 76 | 77 | t.equal(response.statusCode, 207, "StatusCode matches"); 78 | 79 | var xmlDoc = xml.parseXml(body); 80 | 81 | var nodeSync = xmlDoc.get('/D:multistatus/D:sync-token', { D: 'DAV:' } ); 82 | 83 | t.doesNotEqual(nodeSync, undefined, "sync-token node exists"); 84 | t.equal(nodeSync.text().substr(0, 29), "http://swordlord.org/ns/sync/", "sync token has right URL"); 85 | 86 | /* 87 | QUERY 88 | 89 | 90 | http://swordlord.com/ns/sync/20 91 | 1 92 | 93 | 94 | 95 | 96 | 97 | 98 | RESPONSE 99 | 101 | http://swordlord.com/ns/sync/20 102 | 103 | 104 | * */ 105 | 106 | //console.log(body); 107 | } 108 | else 109 | { 110 | t.fail(error); 111 | } 112 | }); 113 | }); 114 | 115 | test('Calling REPORT comp-filter on calendar WITHOUT data, changeset with etags only', function (t) { 116 | 117 | var now = moment(); 118 | 119 | var payload = "\n\r"; 120 | 121 | payload += "\n\r"; 122 | payload += "\n\r"; 123 | payload += " \n\r"; 124 | payload += " \n\r"; 125 | payload += "\n\r"; 126 | payload += "\n\r"; 127 | payload += "\n\r"; 128 | payload += " \n\r"; 129 | payload += " \n\r"; 130 | payload += " \n\r"; 131 | payload += "\n\r"; 132 | payload += "\n\r"; 133 | payload += "\n\r"; 134 | 135 | var options = { 136 | method: 'REPORT', 137 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/2A2AF854-18A0-47CB-870A-D94CA2341BAA", 138 | auth: { 139 | 'user': username, 140 | 'pass': password, 141 | 'sendImmediately': true 142 | } , 143 | body: payload, 144 | followRedirect: false 145 | } 146 | 147 | request(options, function (error, response, body) { 148 | 149 | if (!error) 150 | { 151 | t.plan(3); 152 | 153 | t.equal(response.statusCode, 207, "StatusCode matches"); 154 | 155 | var xmlDoc = xml.parseXml(body); 156 | 157 | var nodeHref = xmlDoc.get('/D:multistatus/D:response/D:href', { D: 'DAV:' } ); 158 | t.doesNotEqual(nodeHref, undefined, "href node exists"); 159 | 160 | // store for a later test case 161 | ics_file = nodeHref.text(); 162 | 163 | t.ok(nodeHref.text().match(/^\/cal\/[a-z0-9]+\/2A2AF854-18A0-47CB-870A-D94CA2341BAA\/[A-Z0-9_-]+\.ics$/g), "href has right URL"); 164 | 165 | /* 166 | 167 | QUERY 168 | * 169 | 170 | 171 | 172 | -> we want the calendar data as well! 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | RESPONSE 185 | * 186 | 192 | 193 | /cal/username/calid/event.ics 194 | 195 | 196 | "etag" 197 | text/calendar; charset=utf-8; component=VEVENT 198 | 199 | HTTP/1.1 200 OK 200 | 201 | 202 | 203 | ... 204 | 205 | http://swordlord.org/ns/sync/id 206 | 207 | 208 | * */ 209 | 210 | //console.log(body); 211 | } 212 | else 213 | { 214 | t.fail(error); 215 | } 216 | }); 217 | }); 218 | 219 | // TODO: finish testcase 220 | test('Calling REPORT comp-filter on calendar WITH data, full calendar-data', function (t) { 221 | 222 | var now = moment(); 223 | 224 | var payload = "\n\r"; 225 | 226 | payload += "\n\r"; 227 | payload += "\n\r"; 228 | payload += " \n\r"; 229 | payload += " \n\r"; 230 | payload += "\n\r"; 231 | payload += "\n\r"; 232 | payload += "\n\r"; 233 | payload += " \n\r"; 234 | payload += " \n\r"; 235 | payload += " \n\r"; 236 | payload += "\n\r"; 237 | payload += "\n\r"; 238 | payload += "\n\r"; 239 | 240 | var options = { 241 | method: 'REPORT', 242 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/2A2AF854-18A0-47CB-870A-D94CA2341BAA", 243 | auth: { 244 | 'user': username, 245 | 'pass': password, 246 | 'sendImmediately': true 247 | } , 248 | body: payload, 249 | followRedirect: false 250 | } 251 | 252 | request(options, function (error, response, body) { 253 | 254 | if (!error) 255 | { 256 | t.plan(3); 257 | 258 | t.equal(response.statusCode, 207, "StatusCode matches"); 259 | 260 | var xmlDoc = xml.parseXml(body); 261 | 262 | var nodeHref = xmlDoc.get('/D:multistatus/D:response/D:href', { D: 'DAV:' } ); 263 | t.doesNotEqual(nodeHref, undefined, "href node exists"); 264 | 265 | // store for a later test case 266 | ics_file = nodeHref.text(); 267 | 268 | t.ok(nodeHref.text().match(/^\/cal\/[a-z0-9]+\/2A2AF854-18A0-47CB-870A-D94CA2341BAA\/[A-Z0-9_-]+\.ics$/g), "href has right URL"); 269 | 270 | t.comment("++ TODO: Check for full ICS data is missing"); 271 | // TODO: Check for full ICS data... 272 | /* 273 | 274 | QUERY 275 | * 276 | 277 | 278 | 279 | -> we want the calendar data as well! 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | RESPONSE 292 | * 293 | 299 | 300 | /cal/username/calid/event.ics 301 | 302 | 303 | "etag" 304 | text/calendar; charset=utf-8; component=VEVENT 305 | 306 | HTTP/1.1 200 OK 307 | 308 | 309 | 310 | ... 311 | 312 | http://swordlord.org/ns/sync/id 313 | 314 | 315 | * */ 316 | 317 | console.log(body); 318 | } 319 | else 320 | { 321 | t.fail(error); 322 | } 323 | }); 324 | }); 325 | 326 | // todo: get request for one item 327 | test('Calling GET ICS on calendar', function (t) { 328 | 329 | var options = { 330 | method: 'GET', 331 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/2A2AF854-18A0-47CB-870A-D94CA2341BAA/" + ics_file, 332 | auth: { 333 | 'user': username, 334 | 'pass': password, 335 | 'sendImmediately': true 336 | } , 337 | followRedirect: false 338 | } 339 | 340 | request(options, function (error, response, body) { 341 | 342 | if (!error) 343 | { 344 | t.plan(1); 345 | 346 | t.equal(response.statusCode, 200, "StatusCode matches"); 347 | 348 | console.log(body); 349 | } 350 | else 351 | { 352 | t.fail(error); 353 | } 354 | }); 355 | }); 356 | -------------------------------------------------------------------------------- /handler/addressbook.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2014-17 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | -----------------------------------------------------------------------------*/ 10 | 11 | var xml = require("libxmljs"); 12 | var xh = require("../libs/xmlhelper"); 13 | var log = require('../libs/log').log; 14 | var VCARD = require('../libs/db').VCARD; 15 | var ADB = require('../libs/db').ADB; 16 | 17 | // Exporting. 18 | module.exports = { 19 | propfind: propfind, 20 | proppatch: proppatch, 21 | report: report, 22 | options: options, 23 | put: put, 24 | get: gett, 25 | delete: del, 26 | move: move 27 | }; 28 | 29 | function propfind(comm) 30 | { 31 | log.debug("addressbook.propfind called"); 32 | 33 | comm.setStandardHeaders(); 34 | comm.setDAVHeaders(); 35 | 36 | comm.setResponseCode(207); 37 | comm.appendResBody(xh.getXMLHead()); 38 | 39 | var response = ""; 40 | 41 | var body = comm.getReqBody(); 42 | var xmlDoc = xml.parseXml(body); 43 | 44 | var node = xmlDoc.get('/A:propfind/A:prop', { 45 | A: 'DAV:', 46 | B: "urn:ietf:params:xml:ns:caldav", 47 | C: 'http://calendarserver.org/ns/', 48 | D: "http://me.com/_namespace/" 49 | }); 50 | var childs = node.childNodes(); 51 | 52 | var isRoot = true; 53 | 54 | // if URL element size === 4, this is a call for the root URL of a user. 55 | // TODO: 56 | if(comm.getUrlElementSize() > 4) 57 | { 58 | isRoot = false; 59 | } 60 | 61 | var username = comm.getUser().getUserName(); 62 | 63 | // respond for root and every addressbook 64 | if(isRoot === true) 65 | { 66 | response += returnPropfindRootProps(comm, childs); 67 | 68 | var defaults = { 69 | pkey: generateUUIDv4(), 70 | ownerId: username, 71 | name: 'default', 72 | synctoken: 0 73 | }; 74 | 75 | // check out if we already have a record for the default addressbook 76 | // if not, lets create it, otherwise let's return its values... 77 | ADB.findOrCreate({where: {ownerId: username, name: defaults.name}, defaults: defaults }).spread(function(adb, created) 78 | { 79 | VCARD.findAndCountAll( 80 | { where: {addressbookId: adb.pkey}} 81 | ).then(function(rsVCARDS) 82 | { 83 | response += returnPropfindProps(comm, childs, adb, rsVCARDS); 84 | 85 | if(created) 86 | { 87 | adb.save().then(function() 88 | { 89 | log.warn('adb saved'); 90 | }); 91 | } 92 | 93 | comm.appendResBody(""); 94 | comm.appendResBody(response); 95 | comm.appendResBody(""); 96 | 97 | comm.flushResponse(); 98 | }); 99 | }); 100 | } 101 | else 102 | { 103 | var adbName = comm.getPathElement(3); 104 | 105 | // check out if we already have a record for the default addressbook 106 | // if not, lets create it, otherwise let's return its values... 107 | ADB.find({ where: {ownerId: username, name: adbName} }).then(function(adb) 108 | { 109 | VCARD.findAndCountAll( 110 | { where: {addressbookId: adb.pkey}} 111 | ).then(function(rsVCARDS) 112 | { 113 | response += returnPropfindProps(comm, childs, adb, rsVCARDS); 114 | 115 | comm.appendResBody(""); 116 | comm.appendResBody(response); 117 | comm.appendResBody(""); 118 | 119 | comm.flushResponse(); 120 | }); 121 | }); 122 | } 123 | } 124 | 125 | function generateUUIDv4() 126 | { 127 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; 128 | uuid = uuid.replace(/[xy]/g, function(c) { 129 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 130 | return v.toString(16); 131 | }); 132 | 133 | return uuid; 134 | } 135 | 136 | function returnPropfindRootProps(comm, nodes, adb, rsVCARD) 137 | { 138 | var response = "" + comm.getURL() + ""; 139 | 140 | response += ""; 141 | response += ""; 142 | 143 | var responseEtag = ""; 144 | 145 | var username = comm.getUser().getUserName(); 146 | 147 | var len = nodes.length; 148 | for (var i=0; i < len; ++i) 149 | { 150 | var child = nodes[i]; 151 | var name = child.name(); 152 | switch(name) 153 | { 154 | case 'add-member': 155 | response += ""; 156 | break; 157 | case 'bulk-requests': 158 | response += ""; 159 | break; 160 | case 'current-user-privilege-set': 161 | response += getCurrentUserPrivilegeSet(); 162 | break; 163 | case 'displayname': 164 | // TODO: let the user change the value of displayname 165 | response += "Contacts"; 166 | break; 167 | case 'max-image-size': 168 | response += ""; 169 | break; 170 | case 'max-resource-size': 171 | response += ""; 172 | break; 173 | case 'me-card': 174 | response += ""; 175 | break; 176 | case 'owner': 177 | response += "/p/" + username + "/"; 178 | break; 179 | case 'push-transports': 180 | response += ""; 181 | break; 182 | case 'pushkey': 183 | response += ""; 184 | break; 185 | case 'quota-available-bytes': 186 | response += ""; 187 | break; 188 | case 'quota-used-bytes': 189 | response += ""; 190 | break; 191 | case 'resource-id': 192 | response += ""; 193 | break; 194 | case 'resourcetype': 195 | response += ""; 196 | break; 197 | case 'supported-report-set': 198 | response += getSupportedReportSet(); 199 | break; 200 | case 'sync-token': 201 | response += ""; 202 | break; 203 | case 'getctag': 204 | response += ""; 205 | break; 206 | case 'getetag': 207 | responseEtag += returnADBETag(comm, rsVCARD); 208 | break; 209 | 210 | default: 211 | if(name != 'text') log.warn("CARD-PropFind Root: not handled: " + name); 212 | break; 213 | } 214 | } 215 | 216 | response += ""; 217 | response += "HTTP/1.1 200 OK"; 218 | response += ""; 219 | response += ""; 220 | 221 | if(responseEtag.length > 0) 222 | { 223 | response += responseEtag; 224 | } 225 | 226 | return response; 227 | } 228 | 229 | function returnADBETag(comm, rsVCARD) 230 | { 231 | var response = ""; 232 | 233 | for (var j=0; j < rsVCARD.count; ++j) 234 | { 235 | var vcard = rsVCARD.rows[j]; 236 | 237 | var date = Date.parse(vcard.updatedAt); 238 | 239 | response += ""; 240 | response += "" + comm.getURL() + vcard.pkey + ".vcf"; 241 | response += ""; 242 | response += ""; 243 | response += "\"" + Number(date) + "\""; 244 | response += ""; 245 | response += "HTTP/1.1 200 OK"; 246 | response += ""; 247 | response += ""; 248 | } 249 | 250 | return response; 251 | } 252 | 253 | function returnPropfindProps(comm, nodes, adb, rsVCARD) 254 | { 255 | var username = comm.getUser().getUserName(); 256 | 257 | var response = "/card/" + username + "/" + adb.name + "/"; 258 | 259 | response += ""; 260 | response += ""; 261 | 262 | var responseEtag = ""; 263 | 264 | var len = nodes.length; 265 | for (var i=0; i < len; ++i) 266 | { 267 | var child = nodes[i]; 268 | var name = child.name(); 269 | switch(name) 270 | { 271 | case 'add-member': 272 | response += ""; 273 | break; 274 | case 'bulk-requests': 275 | response += ""; 276 | break; 277 | case 'current-user-privilege-set': 278 | response += getCurrentUserPrivilegeSet(); 279 | break; 280 | case 'displayname': 281 | // TODO: let the user change the value of displayname 282 | response += "Contacts"; 283 | break; 284 | case 'max-image-size': 285 | response += ""; 286 | break; 287 | case 'max-resource-size': 288 | response += ""; 289 | break; 290 | case 'me-card': 291 | response += ""; 292 | break; 293 | case 'owner': 294 | response += "/p/" + username + "/"; 295 | break; 296 | case 'push-transports': 297 | response += ""; 298 | break; 299 | case 'pushkey': 300 | response += ""; 301 | break; 302 | case 'quota-available-bytes': 303 | response += ""; 304 | break; 305 | case 'quota-used-bytes': 306 | response += ""; 307 | break; 308 | case 'resource-id': 309 | response += ""; 310 | break; 311 | case 'resourcetype': 312 | response += ""; 313 | break; 314 | case 'supported-report-set': 315 | response += getSupportedReportSet(); 316 | break; 317 | case 'sync-token': 318 | response += ""; 319 | break; 320 | case 'getetag': 321 | responseEtag += returnADBETag(comm, rsVCARD); 322 | break; 323 | 324 | default: 325 | if(name != 'text') log.warn("CARD-PropFind: not handled: " + name); 326 | break; 327 | } 328 | } 329 | 330 | response += ""; 331 | response += "HTTP/1.1 200 OK"; 332 | response += ""; 333 | response += ""; 334 | 335 | if(responseEtag.length > 0) 336 | { 337 | response += responseEtag; 338 | } 339 | 340 | return response; 341 | } 342 | 343 | 344 | function del(comm) 345 | { 346 | log.debug("addressbook.delete called"); 347 | 348 | comm.setHeader("Content-Type", "text/html"); 349 | comm.setHeader("Server", "Fennel"); 350 | 351 | // TODO: actually no. respond according to delete status or error while trying to delete 352 | comm.setResponseCode(204); 353 | 354 | log.debug("URLAsArray: " + comm.getURLAsArray()); 355 | log.debug("URLElementSize: " + comm.getUrlElementSize()); 356 | 357 | var isRoot = true; 358 | 359 | // // if URL element size === 4, this is a call for the root URL of a user. 360 | // // TODO: check if the current user is the user requesting the resource (ACL) 361 | if(comm.getUrlElementSize() > 4) 362 | { 363 | var lastPathElement = comm.getFilenameFromPath(false); 364 | if(comm.stringEndsWith(lastPathElement, '.vcf')) 365 | { 366 | isRoot = false; 367 | } 368 | } 369 | 370 | if(isRoot === true) 371 | { 372 | var addressbookId = comm.getPathElement(3); 373 | 374 | ADB.find({ where: {pkey: addressbookId} }).then(function(adb) 375 | { 376 | if(adb === null) 377 | { 378 | log.warn('err: could not find addressbook with ID: ' + addressbookId); 379 | } 380 | else 381 | { 382 | adb.destroy().then(function() 383 | { 384 | log.debug('addressbook deleted'); 385 | }) 386 | } 387 | 388 | comm.flushResponse(); 389 | }); 390 | } 391 | else 392 | { 393 | var vcardId = comm.getFilenameFromPath(true); 394 | 395 | VCARD.find( { where: {pkey: vcardId}}).then(function(vcard) 396 | { 397 | if(vcard === null) 398 | { 399 | log.warn('err: could not find vcard: ' + vcardId); 400 | } 401 | else 402 | { 403 | vcard.destroy().then(function() 404 | { 405 | log.debug('vcard deleted'); 406 | }) 407 | } 408 | 409 | comm.flushResponse(); 410 | }); 411 | } 412 | } 413 | 414 | function gett(comm) 415 | { 416 | log.debug("calendar.get called"); 417 | 418 | var res = comm.getRes(); 419 | res.setHeader("Content-Type", "text/vcard; charset=utf-8"); 420 | 421 | var vcardId = comm.getFilenameFromPath(true); 422 | VCARD.find({ where: {pkey: vcardId} }).then(function(vcard) 423 | { 424 | if(vcard === null) 425 | { 426 | log.warn('err: could not find vcard'); 427 | } 428 | else 429 | { 430 | var res = comm.getRes(); 431 | 432 | var content = vcard.content; 433 | //content = content.replace(/\r\n|\r|\n/g,' \r\n'); 434 | 435 | comm.appendResBody(content); 436 | } 437 | 438 | comm.flushResponse(); 439 | }); 440 | } 441 | 442 | function put(comm) 443 | { 444 | // PUT /addressbooks/a3298271331/fruux-merged/68a386ea-be30-4922-a407-890b56bf944d.vcf HTTP/1.1 445 | // X-ADDRESSBOOKSERVER-KIND:group -> is_group === true 446 | 447 | log.debug("addressbook.put called"); 448 | 449 | var vcardId = comm.getFilenameFromPath(true); 450 | 451 | var body = comm.getReqBody(); 452 | 453 | var match = body.search(/X-ADDRESSBOOKSERVER-KIND:group/); 454 | var isGroup = (match >= 0); 455 | 456 | var username = comm.getUser().getUserName(); 457 | 458 | var adbName = comm.getCardIdFromURL(); 459 | 460 | // check out if we already have a record for the default addressbook 461 | // if not, lets create it, otherwise let's return its values... 462 | ADB.find({ where: {ownerId: username, name: adbName} }).then(function(adb) 463 | { 464 | var defaults = { 465 | addressbookId: adb.pkey, 466 | content: body, 467 | ownerId: comm.getUser().getUserName(), 468 | is_group: isGroup 469 | }; 470 | 471 | // check out if we already have a record for the default addressbook 472 | // if not, lets create it, otherwise let's return its values... 473 | VCARD.findOrCreate({where: { pkey: vcardId }, defaults: defaults }).spread(function(vcard, created) 474 | { 475 | if(created) 476 | { 477 | log.debug('Created VCARD: ' + JSON.stringify(vcard, null, 4)); 478 | } 479 | else 480 | { 481 | vcard.content = comm.getReqBody(); 482 | vcard.is_group = isGroup; 483 | log.debug('Loaded VCARD: ' + JSON.stringify(vcard, null, 4)); 484 | } 485 | 486 | vcard.save().then(function() 487 | { 488 | log.info('vcard updated'); 489 | 490 | // update addressbook collection 491 | /* 492 | ADB.find({ where: {pkey: addressbookId} } ).then(function(cal) 493 | { 494 | if(cal !== null && cal !== undefined) 495 | { 496 | cal.increment('synctoken', { by: 1 }).then(function() 497 | { 498 | log.info('synctoken on cal updated'); 499 | }); 500 | } 501 | }); 502 | */ 503 | }); 504 | }); 505 | }); 506 | 507 | comm.setStandardHeaders(); 508 | 509 | var date = new Date(); 510 | comm.setHeader("ETag", Number(date)); 511 | 512 | comm.setResponseCode(201); 513 | comm.flushResponse(); 514 | } 515 | 516 | function move(comm) 517 | { 518 | 519 | log.debug("calendar.move called"); 520 | /* 521 | comm.setStandardHeaders(comm); 522 | 523 | var ics_id = comm.getFilenameFromPath(true); 524 | var calendar = comm.getLastPathElement(); 525 | 526 | var destination = ""; 527 | 528 | var req = comm.getReq(); 529 | var headers = req.headers; 530 | for(var header in headers) 531 | { 532 | if(header === "destination") 533 | { 534 | destination = req.headers[header]; 535 | } 536 | } 537 | 538 | if(destination.length > 0) 539 | { 540 | var aURL = destination.split("/"); 541 | var newCal = aURL[aURL.length - 2]; 542 | 543 | ICS.find({ where: {pkey: ics_id} }).then(function(ics) 544 | { 545 | if(ics === null) 546 | { 547 | log.warn('ics not found'); 548 | } 549 | else 550 | { 551 | ics.calendarId = newCal; 552 | ics.save().then(function() 553 | { 554 | log.warn('ics updated'); 555 | }); 556 | } 557 | }); 558 | } 559 | 560 | comm.setResponseCode(201); 561 | */ 562 | } 563 | 564 | function getSupportedReportSet() 565 | { 566 | var response = ""; 567 | 568 | response += ""; 569 | response += ""; 570 | response += ""; 571 | response += ""; 572 | response += ""; 573 | response += ""; 574 | 575 | return response; 576 | } 577 | 578 | 579 | function getCurrentUserPrivilegeSet() 580 | { 581 | var response = ""; 582 | 583 | response += ""; 584 | response += ""; 585 | response += ""; 586 | response += ""; 587 | response += ""; 588 | response += ""; 589 | response += ""; 590 | response += ""; 591 | response += ""; 592 | response += ""; 593 | response += ""; 594 | response += ""; 595 | response += ""; 596 | 597 | return response; 598 | } 599 | 600 | function options(comm) 601 | { 602 | log.debug("principal.options called"); 603 | 604 | comm.pushOptionsResponse(); 605 | } 606 | 607 | function report(comm) 608 | { 609 | /* 610 | REPORT /addressbooks/a3298271331/fruux-merged/ HTTP/1.1 611 | 612 | 613 | 614 | 615 | 616 | 617 | /addressbooks/a3298271331/fruux-merged/55b786f0-166b-4548-9bf3-1c4499fb3339.vcf 618 | /addressbooks/a3298271331/fruux-merged/68a386ea-be30-4922-a407-890b56bf944d.vcf 619 | 620 | */ 621 | 622 | log.debug("addressbook.report called"); 623 | 624 | comm.setStandardHeaders(); 625 | 626 | comm.setResponseCode(200); 627 | comm.appendResBody(xh.getXMLHead()); 628 | 629 | var body = comm.getReqBody(); 630 | var xmlDoc = xml.parseXml(body); 631 | 632 | var rootNode = xmlDoc.root(); 633 | 634 | var name = rootNode.name(); 635 | switch(name) 636 | { 637 | case 'addressbook-multiget': 638 | handleReportAdressbookMultiget(comm); 639 | break; 640 | 641 | default: 642 | if(name != 'text') log.warn("P-R: not handled: " + name); 643 | comm.flushResponse(); 644 | break; 645 | } 646 | } 647 | 648 | function handleReportAdressbookMultiget(comm) 649 | { 650 | var body = comm.getReqBody(); 651 | var xmlDoc = xml.parseXml(body); 652 | 653 | var node = xmlDoc.get('/B:addressbook-multiget', { 654 | A: 'DAV:', 655 | B: "urn:ietf:params:xml:ns:carddav", 656 | C: 'http://calendarserver.org/ns/', 657 | D: "http://apple.com/ns/ical/", 658 | E: "http://me.com/_namespace/" 659 | }); 660 | 661 | if(node != undefined) 662 | { 663 | var childs = node.childNodes(); 664 | 665 | var arrHrefs = []; 666 | 667 | var len = childs.length; 668 | for (var i=0; i < len; ++i) 669 | { 670 | var child = childs[i]; 671 | var name = child.name(); 672 | switch(name) 673 | { 674 | case 'prop': // TODO: theoretically we should first get the parameters ordered by the client, lets do so later :) 675 | break; 676 | 677 | case 'href': 678 | arrHrefs.push(parseHrefToVCARDId(child.text())); 679 | break; 680 | 681 | default: 682 | if(name != 'text') log.warn("ADB-R: not handled: " + name); 683 | break; 684 | } 685 | } 686 | 687 | handleReportHrefs(comm, arrHrefs); 688 | } 689 | else 690 | { 691 | comm.flushResponse(); 692 | } 693 | } 694 | 695 | function parseHrefToVCARDId(href) 696 | { 697 | var e = href.split("/"); 698 | var id = e[e.length - 1]; 699 | 700 | return id.substr(0, id.length - 4); 701 | } 702 | 703 | function handleReportHrefs(comm, arrVCARDIds) 704 | { 705 | VCARD.findAndCountAll( { where: {pkey: arrVCARDIds}}).then(function(result) 706 | { 707 | var response = ""; 708 | 709 | for (var i=0; i < result.count; ++i) 710 | { 711 | var vcard = result.rows[i]; 712 | 713 | var date = Date.parse(vcard.updatedAt); 714 | 715 | var content = vcard.content; 716 | content = content.replace(/&/g,'&'); 717 | content = content.replace(/\r\n|\r|\n/g,' \r\n'); 718 | 719 | response += ""; 720 | response += "" + comm.getURL() + vcard.pkey + ".vcf"; 721 | response += ""; 722 | response += "" + content + ""; 723 | response += "\"" + Number(date) + "\""; 724 | response += "HTTP/1.1 200 OK"; 725 | response += ""; 726 | } 727 | 728 | comm.appendResBody("\r\n"); 729 | comm.appendResBody(response); 730 | comm.appendResBody("\r\n"); 731 | 732 | comm.flushResponse(); 733 | }); 734 | } 735 | 736 | function proppatch(comm) 737 | { 738 | log.debug("addressbook.proppatch called"); 739 | 740 | /* 741 | comm.setStandardHeaders(); 742 | 743 | comm.setResponseCode(200); 744 | 745 | comm.appendResBody(xh.getXMLHead()); 746 | 747 | var body = comm.getReqBody(); 748 | var xmlDoc = xml.parseXml(body); 749 | 750 | var node = xmlDoc.get('/A:propertyupdate/A:set/A:prop', { 751 | A: 'DAV:', 752 | B: "urn:ietf:params:xml:ns:caldav", 753 | C: 'http://calendarserver.org/ns/', 754 | D: "http://apple.com/ns/ical/", 755 | E: "http://me.com/_namespace/" 756 | }); 757 | var childs = node.childNodes(); 758 | 759 | var isRoot = true; 760 | 761 | // if URL element size === 4, this is a call for the root URL of a user. 762 | // TODO: 763 | if(comm.getUrlElementSize() > 4) 764 | { 765 | var lastPathElement = comm.getFilenameFromPath(false); 766 | if(comm.stringEndsWith(lastPathElement, '.ics')) 767 | { 768 | isRoot = false; 769 | } 770 | } 771 | 772 | var response = ""; 773 | 774 | if(isRoot) 775 | { 776 | var calendarId = comm.getLastPathElement(false); 777 | CAL.find({ where: {pkey: calendarId} }).then(function(cal) 778 | { 779 | if(cal === null) 780 | { 781 | log.warn('Calendar not found'); 782 | 783 | var len = childs.length; 784 | for (var i=0; i < len; ++i) 785 | { 786 | var child = childs[i]; 787 | var name = child.name(); 788 | switch(name) 789 | { 790 | case 'default-alarm-vevent-date': 791 | response += ""; 792 | log.info("proppatch default-alarm-vevent-date not handled yet"); 793 | break; 794 | 795 | case 'default-alarm-vevent-datetime': 796 | response += ""; 797 | log.info("proppatch default-alarm-vevent-datetime not handled yet"); 798 | break; 799 | 800 | default: 801 | if(name != 'text') log.warn("CAL-PP: not handled: " + name); 802 | break; 803 | } 804 | } 805 | 806 | comm.appendResBody("\r\n"); 807 | comm.appendResBody(" \r\n"); 808 | comm.appendResBody(" " + comm.getURL() + "\r\n"); 809 | comm.appendResBody(" \r\n"); 810 | comm.appendResBody(" \r\n"); 811 | comm.appendResBody(response); 812 | comm.appendResBody(" \r\n"); 813 | comm.appendResBody(" HTTP/1.1 403 Forbidden\r\n"); 814 | comm.appendResBody(" \r\n"); 815 | comm.appendResBody(" \r\n"); 816 | comm.appendResBody("\r\n"); 817 | } 818 | else 819 | { 820 | var len = childs.length; 821 | for (var i=0; i < len; ++i) 822 | { 823 | var child = childs[i]; 824 | var name = child.name(); 825 | switch(name) 826 | { 827 | case 'default-alarm-vevent-date': 828 | response += ""; 829 | log.info("proppatch default-alarm-vevent-date not handled yet"); 830 | break; 831 | 832 | case 'default-alarm-vevent-datetime': 833 | response += ""; 834 | log.info("proppatch default-alarm-vevent-datetime not handled yet"); 835 | break; 836 | 837 | case 'displayname': 838 | response += ""; 839 | cal.displayname = child.text(); 840 | break; 841 | 842 | case 'calendar-timezone': 843 | response += ""; 844 | cal.timezone = child.text(); 845 | break; 846 | 847 | case 'calendar-color': 848 | response += ""; 849 | cal.colour = child.text(); 850 | break; 851 | 852 | case 'calendar-order': 853 | response += ""; 854 | cal.order = child.text(); 855 | break; 856 | 857 | default: 858 | if(name != 'text') log.warn("CAL-PP: not handled: " + name); 859 | break; 860 | } 861 | } 862 | 863 | cal.save().then(function() 864 | { 865 | log.warn('cal saved'); 866 | }); 867 | 868 | comm.appendResBody("\r\n"); 869 | comm.appendResBody(" \r\n"); 870 | comm.appendResBody(" " + comm.getURL() + "\r\n"); 871 | comm.appendResBody(" \r\n"); 872 | comm.appendResBody(" \r\n"); 873 | comm.appendResBody(response); 874 | comm.appendResBody(" \r\n"); 875 | comm.appendResBody(" HTTP/1.1 200 OK\r\n"); 876 | comm.appendResBody(" \r\n"); 877 | comm.appendResBody(" \r\n"); 878 | comm.appendResBody("\r\n"); 879 | } 880 | 881 | comm.flushResponse(); 882 | }); 883 | } 884 | */ 885 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /test/cal/propfind.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | ** 3 | ** - Fennel Card-/CalDAV - 4 | ** 5 | ** Copyright 2016 by 6 | ** SwordLord - the coding crew - http://www.swordlord.com 7 | ** and contributing authors 8 | ** 9 | ** This file is part of the test suite 10 | ** 11 | ** This program is free software; you can redistribute it and/or modify it 12 | ** under the terms of the GNU Affero General Public License as published by the Free 13 | ** Software Foundation, either version 3 of the License, or (at your option) 14 | ** any later version. 15 | ** 16 | ** This program is distributed in the hope that it will be useful, but WITHOUT 17 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 19 | ** more details. 20 | ** 21 | ** You should have received a copy of the GNU Affero General Public License along 22 | ** with this program. If not, see . 23 | ** 24 | **----------------------------------------------------------------------------- 25 | ** 26 | ** Original Authors: 27 | ** LordEidi@swordlord.com 28 | ** LordLightningBolt@swordlord.com 29 | ** 30 | ** $Id: 31 | ** 32 | -----------------------------------------------------------------------------*/ 33 | var test = require('tape'); 34 | var request = require('request'); 35 | var xml = require("libxmljs"); 36 | 37 | var config = require('../../config').config; 38 | 39 | var username = config.test_user_name; 40 | var password = config.test_user_pwd; 41 | 42 | test('Calling PROPFIND getctag and synctoken on calendar with existent ID', function (t) { 43 | 44 | t.plan(5); 45 | 46 | var payload = "\n\r"; 47 | payload += "\n\r"; 48 | payload += "\n\r"; 49 | payload += "\n\r"; 50 | payload += "\n\r"; 51 | payload += "\n\r"; 52 | payload += "\n\r"; 53 | 54 | var options = { 55 | method: 'PROPFIND', 56 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/2A2AF854-18A0-47CB-870A-D94CA2341BAA/", 57 | auth: { 58 | 'user': username, 59 | 'pass': password, 60 | 'sendImmediately': true 61 | } , 62 | body: payload, 63 | followRedirect: false 64 | } 65 | 66 | request(options, function (error, response, body) { 67 | 68 | if (!error) 69 | { 70 | t.equal(response.statusCode, 207, "StatusCode matches"); 71 | // todo: Check body for correct amount of xml tags and entities 72 | 73 | var xmlDoc = xml.parseXml(body); 74 | 75 | var nodegetctag = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:prop/CS:getctag', { D: 'DAV:', CS: 'http://calendarserver.org/ns/' } ); 76 | t.doesNotEqual(nodegetctag, undefined, "getctag node exists"); 77 | 78 | var nodeSynctoken = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:prop/D:sync-token', { D: 'DAV:' } ); 79 | t.doesNotEqual(nodeSynctoken, undefined, "synctoken node exists"); 80 | 81 | var nodeStatusCode = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:status', { D: 'DAV:' } ); 82 | t.doesNotEqual(nodeStatusCode, undefined, "status node exists"); 83 | 84 | t.ok(nodeStatusCode.text().match(/^HTTP\/1.1 200 OK/g), "statuscode node has right text"); 85 | 86 | /* 87 | * * request to the specified calendar and getctag, sync token 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | /cal/user/calid/ 100 | 101 | 102 | http://swordlord.org/ns/sync/3 103 | http://swordlord.org/ns/sync/3 104 | 105 | HTTP/1.1 200 OK 106 | 107 | 108 | 109 | 110 | 111 | * 112 | * */ 113 | 114 | 115 | //console.log(body); 116 | } 117 | else 118 | { 119 | t.fail(error); 120 | } 121 | }); 122 | }); 123 | 124 | test('Calling PROPFIND getctag and synctoken on calendar with NON existent ID', function (t) { 125 | 126 | t.plan(3); 127 | 128 | var payload = "\n\r"; 129 | payload += "\n\r"; 130 | payload += "\n\r"; 131 | payload += "\n\r"; 132 | payload += "\n\r"; 133 | payload += "\n\r"; 134 | payload += "\n\r"; 135 | 136 | var options = { 137 | method: 'PROPFIND', 138 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/DOES-NOT-EXIST/", 139 | auth: { 140 | 'user': username, 141 | 'pass': password, 142 | 'sendImmediately': true 143 | } , 144 | body: payload, 145 | followRedirect: false 146 | } 147 | 148 | request(options, function (error, response, body) { 149 | 150 | if (!error) 151 | { 152 | t.equal(response.statusCode, 207, "StatusCode matches"); 153 | 154 | var xmlDoc = xml.parseXml(body); 155 | 156 | var nodeStatusCode = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:status', { D: 'DAV:' } ); 157 | t.doesNotEqual(nodeStatusCode, undefined, "status node exists"); 158 | 159 | t.ok(nodeStatusCode.text().match(/^HTTP\/1.1 404/g), "statuscode node has right text"); 160 | 161 | /* 162 | * * request to the specified calendar and getctag, sync token 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | /cal/user/calid/ 174 | 175 | HTTP/1.1 404 NOT FOUND 176 | 177 | 178 | 179 | 180 | 181 | * 182 | * */ 183 | 184 | 185 | //console.log(body); 186 | } 187 | else 188 | { 189 | t.fail(error); 190 | } 191 | }); 192 | }); 193 | 194 | test('Calling PROPFIND getctag and synctoken on calendar inbox', function (t) { 195 | 196 | t.plan(2); 197 | 198 | var payload = "\n\r"; 199 | payload += "\n\r"; 200 | payload += "\n\r"; 201 | payload += "\n\r"; 202 | payload += "\n\r"; 203 | payload += "\n\r"; 204 | payload += "\n\r"; 205 | 206 | var options = { 207 | method: 'PROPFIND', 208 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/inbox/", 209 | auth: { 210 | 'user': username, 211 | 'pass': password, 212 | 'sendImmediately': true 213 | } , 214 | body: payload, 215 | followRedirect: false 216 | } 217 | 218 | request(options, function (error, response, body) { 219 | 220 | if (!error) 221 | { 222 | t.equal(response.statusCode, 207, "StatusCode matches"); 223 | // todo: Check body for correct amount of xml tags and entities 224 | 225 | var xmlDoc = xml.parseXml(body); 226 | 227 | var nodeHref = xmlDoc.get('/D:multistatus/D:response/D:href', { D: 'DAV:' } ); 228 | 229 | t.doesNotEqual(nodeHref, undefined, "href node exists"); 230 | /* 231 | * * request to the inbox and getctag, sync token 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | * 240 | 241 | /calendars/_userid_/notifications/ 242 | 243 | 244 | * 245 | * */ 246 | 247 | 248 | //console.log(body); 249 | } 250 | else 251 | { 252 | t.fail(error); 253 | } 254 | }); 255 | }); 256 | 257 | test('Calling PROPFIND getctag and synctoken on calendar notifications', function (t) { 258 | 259 | t.plan(2); 260 | 261 | var payload = "\n\r"; 262 | payload += "\n\r"; 263 | payload += "\n\r"; 264 | payload += "\n\r"; 265 | payload += "\n\r"; 266 | payload += "\n\r"; 267 | payload += "\n\r"; 268 | 269 | var options = { 270 | method: 'PROPFIND', 271 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/notifications/", 272 | auth: { 273 | 'user': username, 274 | 'pass': password, 275 | 'sendImmediately': true 276 | } , 277 | body: payload, 278 | followRedirect: false 279 | } 280 | 281 | request(options, function (error, response, body) { 282 | 283 | if (!error) 284 | { 285 | t.equal(response.statusCode, 207, "StatusCode matches"); 286 | // todo: Check body for correct amount of xml tags and entities 287 | 288 | var xmlDoc = xml.parseXml(body); 289 | 290 | var nodeHref = xmlDoc.get('/D:multistatus/D:response/D:href', { D: 'DAV:' } ); 291 | 292 | t.doesNotEqual(nodeHref, undefined, "href node exists"); 293 | /* 294 | * * request to the inbox and getctag, sync token 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | * 303 | 304 | /calendars/_userid_/notifications/ 305 | 306 | 307 | * 308 | * */ 309 | 310 | 311 | //console.log(body); 312 | } 313 | else 314 | { 315 | t.fail(error); 316 | } 317 | }); 318 | }); 319 | 320 | test('Calling PROPFIND getetag and notificationtype on calendar notifications', function (t) { 321 | 322 | t.plan(5); 323 | 324 | var payload = "\n\r"; 325 | payload += "\n\r"; 326 | payload += "\n\r"; 327 | payload += "\n\r"; 328 | payload += "\n\r"; 329 | payload += "\n\r"; 330 | payload += "\n\r"; 331 | 332 | var options = { 333 | method: 'PROPFIND', 334 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/notifications/", 335 | auth: { 336 | 'user': username, 337 | 'pass': password, 338 | 'sendImmediately': true 339 | } , 340 | body: payload, 341 | followRedirect: false 342 | } 343 | 344 | request(options, function (error, response, body) { 345 | 346 | if (!error) 347 | { 348 | t.equal(response.statusCode, 207, "StatusCode matches"); 349 | 350 | var xmlDoc = xml.parseXml(body); 351 | 352 | var nodeHref = xmlDoc.get('/D:multistatus/D:response/D:href', { D: 'DAV:' } ); 353 | t.doesNotEqual(nodeHref, undefined, "href node exists"); 354 | 355 | t.ok(nodeHref.text().match(/^\/cal\/[a-z0-9]+\/notifications\/$/g), "href has right URL"); 356 | 357 | var nodeStatusCode = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:status', { D: 'DAV:' } ); 358 | t.doesNotEqual(nodeStatusCode, undefined, "status node exists"); 359 | 360 | t.ok(nodeStatusCode.text().match(/^HTTP\/1.1 200 OK/g), "statuscode node has right text"); 361 | 362 | //console.log(body); 363 | } 364 | else 365 | { 366 | t.fail(error); 367 | } 368 | }); 369 | }); 370 | 371 | test('Calling PROPFIND getetag and contenttype on calendar inbox', function (t) { 372 | 373 | // and then the call for inbox and content type, calendar.js -> line 364 ff 374 | 375 | t.plan(5); 376 | 377 | var payload = "\n\r"; 378 | payload += "\n\r"; 379 | payload += "\n\r"; 380 | payload += "\n\r"; 381 | payload += "\n\r"; 382 | payload += "\n\r"; 383 | payload += "\n\r"; 384 | 385 | var options = { 386 | method: 'PROPFIND', 387 | uri: "http://" + config.ip + ":" + config.port + "/cal/" + username + "/inbox/", 388 | auth: { 389 | 'user': username, 390 | 'pass': password, 391 | 'sendImmediately': true 392 | } , 393 | body: payload, 394 | followRedirect: false 395 | } 396 | 397 | request(options, function (error, response, body) { 398 | 399 | if (!error) 400 | { 401 | t.equal(response.statusCode, 207, "StatusCode matches"); 402 | 403 | var xmlDoc = xml.parseXml(body); 404 | 405 | var nodeHref = xmlDoc.get('/D:multistatus/D:response/D:href', { D: 'DAV:' } ); 406 | t.doesNotEqual(nodeHref, undefined, "href node exists"); 407 | 408 | t.ok(nodeHref.text().match(/^\/cal\/[a-z0-9]+\/inbox\/$/g), "href has right URL"); 409 | 410 | var nodeStatusCode = xmlDoc.get('/D:multistatus/D:response/D:propstat/D:status', { D: 'DAV:' } ); 411 | t.doesNotEqual(nodeStatusCode, undefined, "status node exists"); 412 | 413 | t.ok(nodeStatusCode.text().match(/^HTTP\/1.1 200 OK/g), "statuscode node has right text"); 414 | 415 | //console.log(body); 416 | } 417 | else 418 | { 419 | t.fail(error); 420 | } 421 | }); 422 | 423 | }); 424 | 425 | 426 | /* 427 | 428 | * ************************************************************************************************************************************************************************************ 429 | * ************************************************************************************************************************************************************************************ 430 | * ************************************************************************************************************************************************************************************ 431 | * 432 | * cal/USER/CALENDAR_ID 433 | * 434 | * ************************************************************************************************************************************************************************************ 435 | 436 | query 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | ret 445 | 446 | 447 | 448 | /cal/user/calid/ 449 | 450 | 451 | http://swordlord.org/ns/sync/3 452 | http://swordlord.org/ns/sync/3 453 | 454 | HTTP/1.1 200 OK 455 | 456 | 457 | 458 | 459 | 460 | * ************************************************************************************************************************************************************************************ 461 | * ************************************************************************************************************************************************************************************ 462 | * ************************************************************************************************************************************************************************************ 463 | * 464 | * cal/USER/notifications 465 | * 466 | * ************************************************************************************************************************************************************************************ 467 | 468 | Query 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | ret 477 | 478 | 479 | /calendars/_userid_/notifications/ 480 | 481 | 482 | 483 | 484 | * ************************************************************************************************************************************************************************************ 485 | * ************************************************************************************************************************************************************************************ 486 | * ************************************************************************************************************************************************************************************ 487 | * 488 | * cal/USER/inbox 489 | * 490 | * ************************************************************************************************************************************************************************************ 491 | 492 | Query 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | ret 501 | 502 | 503 | /calendars/_userid_/inbox/ 504 | 505 | 506 | 507 | * ************************************************************************************************************************************************************************************ 508 | * ************************************************************************************************************************************************************************************ 509 | * ************************************************************************************************************************************************************************************ 510 | * 511 | * cal/USER 512 | * 513 | * ************************************************************************************************************************************************************************************ 514 | * 515 | Query 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | ret 560 | 561 | 562 | /calendars/_userid_/ 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | BEGIN:VALARM 598 | X-WR-ALARMUID:3A9B1BC5-9505-4E9A-8387-2C440930C1FD 599 | UID:3A9B1BC5-9505-4E9A-8387-2C440930C1FD 600 | TRIGGER:-PT15H 601 | ATTACH;VALUE=URI:Basso 602 | ACTION:AUDIO 603 | END:VALARM 604 | 605 | BEGIN:VALARM 606 | X-WR-ALARMUID:UUID1 607 | UID:UUID1 608 | TRIGGER;VALUE=DATE-TIME:19760401T005545Z 609 | ACTION:NONE 610 | END:VALARM 611 | 612 | 613 | /p/_userid_/ 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | HTTP/1.1 200 OK 642 | 643 | 644 | 645 | /cal/_userid_/calid/ 646 | 647 | 648 | 649 | 650 | 651 | 652 | #F64F00FF 653 | 654 | 1 655 | BEGIN:VCALENDAR 656 | VERSION:2.0 657 | PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN 658 | CALSCALE:GREGORIAN 659 | BEGIN:VTIMEZONE 660 | TZID:Europe/Zurich 661 | BEGIN:DAYLIGHT 662 | TZOFFSETFROM:+0100 663 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 664 | DTSTART:19810329T020000 665 | TZNAME:GMT+2 666 | TZOFFSETTO:+0200 667 | END:DAYLIGHT 668 | BEGIN:STANDARD 669 | TZOFFSETFROM:+0200 670 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 671 | DTSTART:19961027T030000 672 | TZNAME:GMT+1 673 | TZOFFSETTO:+0100 674 | END:STANDARD 675 | END:VTIMEZONE 676 | END:VCALENDAR 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | Tasks 714 | http://swordlord.org/ns/sync/5 715 | 716 | 717 | /p/_userid_/ 718 | 719 | 720 | https://domain/cal/_userid_/calid.ics 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | http://swordlord.org/ns/sync/5 770 | 771 | HTTP/1.1 200 OK 772 | 773 | 774 | 775 | /cal/_userid_/calid/ 776 | 777 | 778 | 779 | 780 | 781 | 782 | #44A703FF 783 | 784 | 2 785 | BEGIN:VCALENDAR 786 | VERSION:2.0 787 | PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN 788 | CALSCALE:GREGORIAN 789 | BEGIN:VTIMEZONE 790 | TZID:Europe/Zurich 791 | BEGIN:DAYLIGHT 792 | TZOFFSETFROM:+0100 793 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 794 | DTSTART:19810329T020000 795 | TZNAME:GMT+2 796 | TZOFFSETTO:+0200 797 | END:DAYLIGHT 798 | BEGIN:STANDARD 799 | TZOFFSETFROM:+0200 800 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 801 | DTSTART:19961027T030000 802 | TZNAME:GMT+1 803 | TZOFFSETTO:+0100 804 | END:STANDARD 805 | END:VTIMEZONE 806 | END:VCALENDAR 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | Calendar 844 | http://swordlord.org/ns/sync/20 845 | 846 | 847 | /principals/uid/_userid_/ 848 | 849 | 850 | https://domain/cal/_userid_/calid.ics 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | http://swordlord.org/ns/sync/20 900 | 901 | HTTP/1.1 200 OK 902 | 903 | 904 | 905 | /cal/_userid_/calid/ 906 | 907 | 908 | 909 | 910 | 911 | 912 | #0E61B9FF 913 | 914 | 3 915 | BEGIN:VCALENDAR 916 | VERSION:2.0 917 | PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN 918 | CALSCALE:GREGORIAN 919 | BEGIN:VTIMEZONE 920 | TZID:GMT 921 | BEGIN:STANDARD 922 | TZOFFSETFROM:+0000 923 | DTSTART:20010101T000000 924 | TZNAME:GMT 925 | TZOFFSETTO:+0000 926 | END:STANDARD 927 | END:VTIMEZONE 928 | END:VCALENDAR 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | neues 966 | http://swordlord.org/ns/sync/20 967 | 968 | 969 | /principals/uid/_userid_/ 970 | 971 | 972 | https://domain/cal/_userid_/calitem.ics 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | http://swordlord.org/ns/sync/20 1022 | 1023 | HTTP/1.1 200 OK 1024 | 1025 | 1026 | 1027 | /cal/_userid_/outbox/ 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | /p/_userid_/ 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | HTTP/1.1 200 OK 1073 | 1074 | 1075 | 1076 | /cal/_userid_/inbox/ 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | /p/_userid_/ 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 1134 | 1135 | 1136 | 1137 | HTTP/1.1 200 OK 1138 | 1139 | 1140 | 1141 | /cal/_userid_/notifications/ 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | 1171 | 1172 | 1173 | 1174 | 1175 | 1176 | 1177 | /p/_userid_/ 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | 1185 | 1186 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | HTTP/1.1 200 OK 1202 | 1203 | 1204 | 1205 | * 1206 | * */ --------------------------------------------------------------------------------