├── VERSION ├── fennel_logo.png ├── demouser.htpasswd ├── .gitignore ├── Makefile ├── .travis.yml ├── fenneld ├── handler │ ├── app.go │ ├── xmlhelper.go │ ├── wellknown.go │ ├── principal │ │ ├── general.go │ │ ├── report.go │ │ └── propfind.go │ ├── addressbook │ │ ├── general.go │ │ ├── report.go │ │ └── propfind.go │ ├── responder.go │ └── calendar │ │ ├── main.go │ │ ├── report.go │ │ └── propfind.go └── auth │ ├── db.go │ ├── courier.go │ ├── md5crypt.go │ ├── base.go │ └── htpasswd.go ├── go.mod ├── fennelcore ├── tablewriter.go ├── db │ ├── model │ │ ├── group.go │ │ ├── user.go │ │ ├── usergroup.go │ │ ├── permission.go │ │ ├── addressbook.go │ │ ├── vcard.go │ │ ├── ics.go │ │ └── cal.go │ └── tablemodule │ │ ├── cal.go │ │ ├── vcard.go │ │ ├── addressbook.go │ │ ├── user.go │ │ └── ics.go ├── db.go ├── config.go ├── log.go └── ics.go ├── cmd ├── fennelcli │ └── fennelcli.go └── fenneld │ └── fenneld.go ├── fennelcli └── cmd │ ├── root.go │ ├── cal.go │ ├── card.go │ └── user.go └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.1-beta -------------------------------------------------------------------------------- /fennel_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swordlordcodingcrew/fennel/HEAD/fennel_logo.png -------------------------------------------------------------------------------- /demouser.htpasswd: -------------------------------------------------------------------------------- 1 | demo:$apr1$1jd3WSjk$TJ10Xzt82mLEIVTv6Wyye0 2 | fennel:$apr1$DLyhjDw/$ZztH7IzjdM6wkIkx1EMKa0 3 | fennellocal:$apr1$IXFkZ.2B$e1UG.ecCrx7265DEJYe0k1 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin/ 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Files generated by running the program 16 | fennel.db 17 | # Configuration file 18 | fennel.json 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: mod 2 | 3 | default: fennelcli fenneld 4 | 5 | test: mod 6 | go test ./... 7 | 8 | vet: mod 9 | go vet ./... 10 | 11 | fennelcli: mod 12 | go build -o bin/ cmd/fennelcli/fennelcli.go 13 | 14 | fenneld: mod 15 | go build -o bin/ cmd/fenneld/fenneld.go 16 | 17 | mod: 18 | go mod download 19 | 20 | clean: 21 | rm -rf bin/* 22 | 23 | install: 24 | cp bin/* /usr/local/bin 25 | 26 | uninstall: 27 | rm /usr/local/bin/fenneld /usr/local/bin/fennelcli 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.16" 5 | 6 | deploy: 7 | provider: releases 8 | file: 9 | - bin/fenneld-v$VERSION 10 | - bin/fennelcli-v$VERSION 11 | api_key: 12 | secure: NS2Fw2BHbNyWcFxc+tNcMMeNj1BBHYCp5iyx45biOWSepWNl3ZJkJGoGtpRUXNIjkNjjb2fRkz6yCmXdsHXEWMNZu4CpbXR1DZXswXUjtemk0DgzyHMaPWZsIV5eMfcEfzkhLqTbQsYVXeYjo9J1N65CXtHbB0GbTcDm0iMo+5EGXiRT150x4q8Mj4HywShW0arRntH+TdJidKmLpqig/sgp+4tH0rf9ld5XPatwPtDdLlNGYCuii3tiogN+hcd99JYLt10wHi10BXV4i/QRwnCQ/Qw0xctXv+JwCaqrq4NAVXQ+p+n0QTBUTLIsddN8GXT1igMjglNtHQvuiQGnJJga2hBxSQoYfLfw5EHquiKCvS/7IGKaTNODqMHNJxihOYjKgyVjXL7xOi+x3+9enPOWnRargNbztD7XWe+kZjOW9Ab8ea9O31PKWVhPwszwSbDxXxX+o/Cvy5Hs42dIaCykRDUwSCiqphJtHhyxOWOVRW54E3ekvgwRfpXXDJF1H0/SdZr3InUFkLpa/gnv1UC2E+e3yeUa3QKyoYJzb5YdWBlYYmu/YYN5LJXjUmTGwIJQULbZHeTlFigskI1kEmupYpqFnrxY1NBov4rAyS/Vt5wcGTEnPqMf/fgjLCbigCROxuxiFtsplF1K/1TdkXlqgev/9Wzm9zx1ucz3pPA= 13 | skip_cleanup: true 14 | on: 15 | tags: true 16 | -------------------------------------------------------------------------------- /fenneld/handler/app.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import "net/http" 33 | 34 | func OnRoot(w http.ResponseWriter, req *http.Request) { 35 | 36 | RespondWithRedirect(w, req, "/p/") 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/swordlordcodingcrew/fennel 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Jeffail/gabs v1.4.0 // indirect 7 | github.com/apache/thrift v0.12.0 // indirect 8 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect 9 | github.com/beevik/etree v1.1.0 // indirect 10 | github.com/coreos/etcd v3.3.10+incompatible // indirect 11 | github.com/coreos/go-etcd v2.0.0+incompatible // indirect 12 | github.com/fsnotify/fsnotify v1.5.1 // indirect 13 | github.com/gorilla/mux v1.8.0 // indirect 14 | github.com/olekukonko/tablewriter v0.0.5 // indirect 15 | github.com/openzipkin/zipkin-go v0.1.6 // indirect 16 | github.com/pkg/errors v0.9.1 // indirect 17 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 // indirect 18 | github.com/sirupsen/logrus v1.8.1 // indirect 19 | github.com/spf13/cobra v1.2.1 // indirect 20 | github.com/spf13/viper v1.8.1 // indirect 21 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect 22 | github.com/urfave/negroni v1.0.0 // indirect 23 | github.com/vjeantet/jodaTime v1.0.0 // indirect 24 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect 25 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect 26 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 27 | gorm.io/driver/mysql v1.1.3 // indirect 28 | gorm.io/driver/postgres v1.2.2 // indirect 29 | gorm.io/driver/sqlite v1.2.4 // indirect 30 | gorm.io/gorm v1.22.3 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /fenneld/handler/xmlhelper.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "github.com/beevik/etree" 34 | ) 35 | 36 | func AddURLElement(node *etree.Element, url string) { 37 | 38 | ///p/ 39 | cup := node.CreateElement("href") 40 | cup.Space = "d" 41 | cup.SetText(url) 42 | } 43 | -------------------------------------------------------------------------------- /fennelcore/tablewriter.go: -------------------------------------------------------------------------------- 1 | package fennelcore 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "github.com/olekukonko/tablewriter" 34 | "os" 35 | ) 36 | 37 | func WriteTable(header []string, data [][]string) { 38 | 39 | table := tablewriter.NewWriter(os.Stdout) 40 | table.SetHeader(header) 41 | table.SetAutoMergeCells(true) 42 | table.SetRowLine(true) 43 | table.AppendBulk(data) 44 | table.Render() 45 | } 46 | -------------------------------------------------------------------------------- /fenneld/auth/db.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "errors" 35 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 36 | ) 37 | 38 | func ValidateDB(uid string, pwd string) (error, string) { 39 | 40 | // TODO handle permissions 41 | isValidated, _ := tablemodule.ValidateUserInDB(uid, pwd) 42 | 43 | if !isValidated { 44 | 45 | return errors.New("Login Failed"), "" 46 | 47 | } else { 48 | 49 | return nil, "" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /fennelcore/db/model/group.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "gorm.io/gorm" 35 | "time" 36 | ) 37 | 38 | type Group struct { 39 | Pkey string `gorm:"primary_key"` 40 | Description string 41 | CrtDat time.Time `sql:"DEFAULT:current_timestamp"` 42 | UpdDat time.Time `sql:"DEFAULT:current_timestamp"` 43 | } 44 | 45 | func (m *Group) BeforeCreate(scope *gorm.DB) (err error) { 46 | m.CrtDat = time.Now() 47 | return nil 48 | } 49 | 50 | 51 | func (m *Group) BeforeSave(scope *gorm.DB) (err error) { 52 | m.UpdDat = time.Now() 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /fennelcore/db/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "gorm.io/gorm" 35 | "time" 36 | ) 37 | 38 | type User struct { 39 | Name string `gorm:"primary_key",sql:"NOT NULL"` 40 | Password string `sql:"NOT NULL"` 41 | Comment string 42 | CrtDat time.Time `sql:"DEFAULT:current_timestamp"` 43 | UpdDat time.Time `sql:"DEFAULT:current_timestamp"` 44 | } 45 | 46 | func (m *User) BeforeCreate(tx *gorm.DB) (err error) { 47 | m.CrtDat = time.Now() 48 | return nil 49 | } 50 | 51 | func (m *User) BeforeSave(scope *gorm.DB) (err error) { 52 | m.UpdDat = time.Now() 53 | return nil 54 | } -------------------------------------------------------------------------------- /fennelcore/db/model/usergroup.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "gorm.io/gorm" 35 | "time" 36 | ) 37 | 38 | type UserGroup struct { 39 | UserId string `gorm:"primary_key"` 40 | GroupId string `gorm:"primary_key"` 41 | Description string 42 | CrtDat time.Time `sql:"DEFAULT:current_timestamp"` 43 | UpdDat time.Time `sql:"DEFAULT:current_timestamp"` 44 | } 45 | 46 | func (m *UserGroup) BeforeCreate(tx *gorm.DB) (err error) { 47 | m.CrtDat = time.Now() 48 | return nil 49 | } 50 | 51 | func (m *UserGroup) BeforeSave(scope *gorm.DB) (err error) { 52 | m.UpdDat = time.Now() 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /fennelcore/db/model/permission.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "gorm.io/gorm" 35 | "time" 36 | ) 37 | 38 | type Permission struct { 39 | Pkey string `gorm:"primary_key"` 40 | GroupId string `sql:"NOT NULL"` 41 | Description string 42 | Permission string 43 | CrtDat time.Time `sql:"DEFAULT:current_timestamp"` 44 | UpdDat time.Time `sql:"DEFAULT:current_timestamp"` 45 | } 46 | 47 | func (m *Permission) BeforeCreate(tx *gorm.DB) (err error) { 48 | m.CrtDat = time.Now() 49 | return nil 50 | } 51 | 52 | func (m *Permission) BeforeSave(scope *gorm.DB) (err error) { 53 | m.UpdDat = time.Now() 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /fennelcore/db/model/addressbook.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "gorm.io/gorm" 35 | "time" 36 | ) 37 | 38 | type ADB struct { 39 | Pkey string `gorm:"primary_key"` 40 | Owner string `sql:"NOT NULL"` 41 | Name string `sql:"NOT NULL"` 42 | Synctoken int `sql:"NOT NULL; DEFAULT:0"` 43 | CrtDat time.Time `sql:"DEFAULT:current_timestamp"` 44 | UpdDat time.Time `sql:"DEFAULT:current_timestamp"` 45 | } 46 | 47 | func (u *ADB) BeforeCreate(tx *gorm.DB) (err error) { 48 | u.CrtDat = time.Now() 49 | return nil 50 | } 51 | 52 | func (m *ADB) BeforeSave(tx *gorm.DB) (err error) { 53 | m.UpdDat = time.Now() 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /cmd/fennelcli/fennelcli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "fmt" 34 | "github.com/swordlordcodingcrew/fennel/fennelcli/cmd" 35 | "github.com/swordlordcodingcrew/fennel/fennelcore" 36 | "os" 37 | ) 38 | 39 | func main() { 40 | 41 | // Initialise env and params 42 | fennelcore.InitConfig() 43 | fennelcore.InitLog() 44 | 45 | // Initialise database 46 | // if there is an error, this function will quit the app 47 | fennelcore.InitDatabase() 48 | defer fennelcore.CloseDB() 49 | 50 | // initialise the command structure 51 | if err := cmd.RootCmd.Execute(); err != nil { 52 | fmt.Println(err) 53 | 54 | // yes, we deferred closing of the db, but that only works when ending with dignity 55 | fennelcore.CloseDB() 56 | os.Exit(1) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /fennelcore/db/model/vcard.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "gorm.io/gorm" 35 | "time" 36 | ) 37 | 38 | type VCARD struct { 39 | Pkey string `gorm:"primary_key"` 40 | Owner string `sql:"NOT NULL"` 41 | AddressbookId string `sql:"NOT NULL"` 42 | Content string `sql:"NOT NULL"` 43 | IsGroup bool `sql:"NOT NULL;DEFAULT:false"` 44 | CrtDat time.Time `sql:"DEFAULT:current_timestamp"` 45 | UpdDat time.Time `sql:"DEFAULT:current_timestamp"` 46 | } 47 | 48 | func (m *VCARD) BeforeCreate(tx *gorm.DB) (err error) { 49 | m.CrtDat = time.Now() 50 | return nil 51 | } 52 | 53 | func (m *VCARD) BeforeSave(scope *gorm.DB) (err error) { 54 | m.UpdDat = time.Now() 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /fenneld/handler/wellknown.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "github.com/gorilla/mux" 35 | "net/http" 36 | ) 37 | 38 | func OnWellKnown(w http.ResponseWriter, req *http.Request) { 39 | 40 | vars := mux.Vars(req) 41 | sParam := vars["param"] 42 | 43 | // there are different redirect targets depending on the .well-known url 44 | // the user wants 45 | // Never seen most of these, though 46 | switch sParam { 47 | 48 | case "caldav": 49 | RespondWithRedirect(w, req, "/cal/") 50 | case "carddav": 51 | RespondWithRedirect(w, req, "/card/") 52 | default: 53 | RespondWithRedirect(w, req, "/p/") 54 | } 55 | } 56 | 57 | func OnWellKnownNoParam(w http.ResponseWriter, req *http.Request) { 58 | 59 | RespondWithRedirect(w, req, "/p/") 60 | } 61 | -------------------------------------------------------------------------------- /fennelcore/db/model/ics.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "gorm.io/gorm" 35 | "time" 36 | ) 37 | 38 | type ICS struct { 39 | Pkey string `gorm:"primary_key"` 40 | CalendarId string `sql:"NOT NULL"` 41 | StartDate time.Time `sql:"NOT NULL; DEFAULT:current_timestamp"` 42 | EndDate time.Time `sql:"NOT NULL; DEFAULT:current_timestamp"` 43 | Content string `sql:"type:blob"` 44 | CrtDat time.Time `sql:"DEFAULT:current_timestamp"` 45 | UpdDat time.Time `sql:"DEFAULT:current_timestamp"` 46 | } 47 | 48 | func (m *ICS) BeforeCreate(tx *gorm.DB) (err error) { 49 | m.CrtDat = time.Now() 50 | return nil 51 | } 52 | 53 | func (m *ICS) BeforeSave(scope *gorm.DB) (err error) { 54 | m.UpdDat = time.Now() 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /fenneld/handler/principal/general.go: -------------------------------------------------------------------------------- 1 | package principal 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 34 | "net/http" 35 | ) 36 | 37 | // TODO: handle as expected, this is a cheap workaround 38 | func Proppatch(w http.ResponseWriter, req *http.Request) { 39 | 40 | dRet, propstat := handler.GetMultistatusDoc(req.RequestURI) 41 | 42 | // create new element to store response in 43 | prop := propstat.CreateElement("prop") 44 | prop.Space = "d" 45 | 46 | davd := prop.CreateElement("default-alarm-vevent-date") 47 | davd.Space = "cal" 48 | 49 | // add status 50 | status := propstat.CreateElement("status") 51 | status.Space = "d" 52 | status.SetText("HTTP/1.1 403 Forbidden") 53 | 54 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 55 | } 56 | 57 | func Options(w http.ResponseWriter, req *http.Request) { 58 | 59 | handler.RespondWithStandardOptions(w, http.StatusOK, "") 60 | } 61 | -------------------------------------------------------------------------------- /fennelcore/db/model/cal.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "gorm.io/gorm" 35 | "time" 36 | ) 37 | 38 | type CAL struct { 39 | Pkey string `gorm:"primary_key"` 40 | Owner string `sql:"NOT NULL"` 41 | Timezone string `sql:"NOT NULL"` 42 | Order int `sql:"NOT NULL"` 43 | FreeBusySet string `sql:"NOT NULL"` 44 | SupportedCalComponent string `sql:"NOT NULL"` 45 | Colour string `sql:"NOT NULL"` 46 | Displayname string `sql:"NOT NULL"` 47 | Synctoken int `sql:"NOT NULL; DEFAULT:0"` 48 | CrtDat time.Time `sql:"DEFAULT:current_timestamp"` 49 | UpdDat time.Time `sql:"DEFAULT:current_timestamp"` 50 | } 51 | 52 | func (m *CAL) BeforeCreate(tx *gorm.DB) (err error) { 53 | m.CrtDat = time.Now() 54 | return nil 55 | } 56 | 57 | 58 | func (u *CAL) BeforeSave(tx *gorm.DB) (err error) { 59 | u.UpdDat = time.Now() 60 | return nil 61 | } 62 | 63 | -------------------------------------------------------------------------------- /fenneld/auth/courier.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "errors" 35 | ) 36 | 37 | func ValidateCourier(uid string, pwd string) (error, string) { 38 | 39 | return errors.New("Method unknown"), "" 40 | 41 | } 42 | 43 | /* 44 | function checkCourier(username, password, callback) 45 | { 46 | log.debug("Authenticating user with courier method."); 47 | 48 | var socketPath = config.auth_method_courier_socket; 49 | log.debug("Using socket: " + socketPath); 50 | 51 | var client = net.createConnection({path: socketPath}); 52 | 53 | client.on("connect", function() { 54 | //console.log('connect'); 55 | var payload = 'service\nlogin\n' + username + '\n' + password; 56 | client.write('AUTH ' + payload.length + '\n' + payload); 57 | }); 58 | 59 | var response = ""; 60 | 61 | client.on("data", function(data) { 62 | //console.log('data: ' + data); 63 | response += data.toString(); 64 | }); 65 | 66 | client.on('end', function() { 67 | var result = response.indexOf('FAIL', 0); 68 | callback(result < 0); 69 | }); 70 | } 71 | */ 72 | -------------------------------------------------------------------------------- /fennelcli/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "github.com/spf13/cobra" 34 | ) 35 | 36 | // var cfgFile string // see init() for details 37 | 38 | // RootCmd represents the base command when called without any subcommands 39 | var RootCmd = &cobra.Command{ 40 | Use: "fennelcli", 41 | Short: "fennelcli is the cli helper tool for Fennel calendar and carddav server", 42 | Long: `fennelcli is the cli helper tool for Fennel calendar and carddav server 43 | 44 | With this tool you manage users and data in general. See help for more details.`, 45 | } 46 | 47 | func init() { 48 | // following lines just for reference. 49 | 50 | // Here you will define your flags and configuration settings. 51 | // Cobra supports persistent flags, which, if defined here, 52 | // will be global for your application. 53 | // RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.gohjasmincli.yaml)") 54 | 55 | // Cobra also supports local flags, which will only run 56 | // when this action is called directly. 57 | // RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 58 | } 59 | -------------------------------------------------------------------------------- /fennelcore/db.go: -------------------------------------------------------------------------------- 1 | package fennelcore 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "log" 34 | 35 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 36 | "gorm.io/driver/mysql" 37 | "gorm.io/driver/postgres" 38 | "gorm.io/driver/sqlite" 39 | "gorm.io/gorm" 40 | "gorm.io/gorm/schema" 41 | ) 42 | 43 | var db gorm.DB 44 | 45 | // 46 | func InitDatabase() { 47 | dialect := GetStringFromConfig("db.dialect") 48 | args := GetStringFromConfig("db.args") 49 | var dialector gorm.Dialector 50 | switch dialect { 51 | case "sqlite": 52 | dialector = sqlite.Open(args) 53 | case "postgres": 54 | dialector = postgres.Open(args) 55 | case "mysql": 56 | dialector = mysql.Open(args) 57 | default: 58 | log.Fatalf("Unsupported database dialect %v", dialect) 59 | } 60 | 61 | database, err := gorm.Open(dialector, &gorm.Config{ 62 | NamingStrategy: schema.NamingStrategy{ 63 | TablePrefix: "fnl_", // Avoid naming collisions with reserved tables like 'user' 64 | SingularTable: true, 65 | }, 66 | }) 67 | if err != nil { 68 | log.Fatalf("failed to connect database, %s", err) 69 | panic("failed to connect database") 70 | } 71 | 72 | db = *database 73 | db.AutoMigrate(&model.User{}) 74 | db.AutoMigrate(&model.Group{}) 75 | db.AutoMigrate(&model.UserGroup{}) 76 | db.AutoMigrate(&model.Permission{}) 77 | db.AutoMigrate(&model.CAL{}) 78 | db.AutoMigrate(&model.ADB{}) 79 | db.AutoMigrate(&model.ICS{}) 80 | db.AutoMigrate(&model.VCARD{}) 81 | } 82 | 83 | // 84 | func CloseDB() { 85 | // db.Close() 86 | } 87 | 88 | // 89 | func GetDB() *gorm.DB { 90 | return &db 91 | } 92 | -------------------------------------------------------------------------------- /fenneld/auth/md5crypt.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | /* 4 | This file is developed under Apache 2.0 license, and can be used for open and proprietary projects. 5 | 6 | Copyright 2012-2013 Lev Shamardin 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file or any other 9 | part of this project except in compliance with the License. You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software distributed under the License is 14 | distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and limitations under the License. 16 | */ 17 | 18 | import ( 19 | "crypto/md5" 20 | "strings" 21 | ) 22 | 23 | const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 24 | 25 | var md5_crypt_swaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11} 26 | 27 | type MD5Entry struct { 28 | Magic, Salt, Hash []byte 29 | } 30 | 31 | func NewMD5Entry(e string) *MD5Entry { 32 | parts := strings.SplitN(e, "$", 4) 33 | if len(parts) != 4 { 34 | return nil 35 | } 36 | return &MD5Entry{ 37 | Magic: []byte("$" + parts[1] + "$"), 38 | Salt: []byte(parts[2]), 39 | Hash: []byte(parts[3]), 40 | } 41 | } 42 | 43 | /* 44 | MD5 password crypt implementation 45 | */ 46 | func MD5Crypt(password, salt, magic []byte) []byte { 47 | d := md5.New() 48 | 49 | d.Write(password) 50 | d.Write(magic) 51 | d.Write(salt) 52 | 53 | d2 := md5.New() 54 | d2.Write(password) 55 | d2.Write(salt) 56 | d2.Write(password) 57 | 58 | for i, mixin := 0, d2.Sum(nil); i < len(password); i++ { 59 | d.Write([]byte{mixin[i%16]}) 60 | } 61 | 62 | for i := len(password); i != 0; i >>= 1 { 63 | if i&1 == 0 { 64 | d.Write([]byte{password[0]}) 65 | } else { 66 | d.Write([]byte{0}) 67 | } 68 | } 69 | 70 | final := d.Sum(nil) 71 | 72 | for i := 0; i < 1000; i++ { 73 | d2 := md5.New() 74 | if i&1 == 0 { 75 | d2.Write(final) 76 | } else { 77 | d2.Write(password) 78 | } 79 | 80 | if i%3 != 0 { 81 | d2.Write(salt) 82 | } 83 | 84 | if i%7 != 0 { 85 | d2.Write(password) 86 | } 87 | 88 | if i&1 == 0 { 89 | d2.Write(password) 90 | } else { 91 | d2.Write(final) 92 | } 93 | final = d2.Sum(nil) 94 | } 95 | 96 | result := make([]byte, 0, 22) 97 | v := uint(0) 98 | bits := uint(0) 99 | for _, i := range md5_crypt_swaps { 100 | v |= (uint(final[i]) << bits) 101 | for bits = bits + 8; bits > 6; bits -= 6 { 102 | result = append(result, itoa64[v&0x3f]) 103 | v >>= 6 104 | } 105 | } 106 | result = append(result, itoa64[v&0x3f]) 107 | 108 | return append(append(append(magic, salt...), '$'), result...) 109 | } 110 | -------------------------------------------------------------------------------- /fennelcore/config.go: -------------------------------------------------------------------------------- 1 | package fennelcore 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "bytes" 34 | "flag" 35 | "log" 36 | 37 | "github.com/spf13/viper" 38 | ) 39 | 40 | var defaultConfig = ` 41 | { 42 | "log": { 43 | "level": "debug" 44 | }, 45 | "www": { 46 | "host": "127.0.0.1", 47 | "port": "8888" 48 | }, 49 | "auth": { 50 | "module": "htpasswd", 51 | "file": "demouser.htpasswd" 52 | }, 53 | "folder": { 54 | "templates": "templates" 55 | }, 56 | "db": { 57 | "dialect": "sqlite", 58 | "args": "fennel.db", 59 | "logmode": "true" 60 | } 61 | } 62 | ` 63 | 64 | func InitConfig() { 65 | configOverride := flag.String("config", "", "Configuration file path (Optional, will read from standard config locations otherwise)") 66 | flag.Parse() 67 | if *configOverride != "" { 68 | viper.SetConfigFile(*configOverride) 69 | } else { 70 | viper.SetConfigName("fennel") 71 | viper.AddConfigPath(".") 72 | viper.AddConfigPath("$HOME/.config/fennel") 73 | viper.AddConfigPath("$HOME/.config") 74 | viper.AddConfigPath("/etc/fennel") 75 | viper.AddConfigPath("/etc") 76 | } 77 | 78 | // Find and read the config file 79 | if err := viper.ReadInConfig(); err != nil { 80 | log.Printf("Error reading config %v\n Falling back to default %v", err, defaultConfig) 81 | viper.SetConfigType("json") 82 | err = viper.ReadConfig(bytes.NewBuffer([]byte(defaultConfig))) 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | } 87 | 88 | // Confirm which config file is used 89 | log.Printf("config read from: %s\n", viper.ConfigFileUsed()) 90 | } 91 | 92 | func GetBoolFromConfig(key string) bool { 93 | return viper.GetBool(key) 94 | } 95 | 96 | func GetStringFromConfig(key string) string { 97 | return viper.GetString(key) 98 | } 99 | 100 | func GetLogLevel() string { 101 | loglevel := viper.GetString("log.level") 102 | if loglevel == "" { 103 | return "warn" 104 | } else { 105 | 106 | return loglevel 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /fennelcore/log.go: -------------------------------------------------------------------------------- 1 | package fennelcore 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | log "github.com/sirupsen/logrus" 34 | ) 35 | 36 | func InitLog() { 37 | 38 | level, err := log.ParseLevel(GetLogLevel()) 39 | if err != nil { 40 | 41 | log.SetLevel(log.WarnLevel) 42 | 43 | } else { 44 | 45 | log.SetLevel(level) 46 | } 47 | } 48 | 49 | func LogTrace(msg string, fields log.Fields) { 50 | 51 | if fields == nil { 52 | 53 | log.Trace(msg) 54 | 55 | } else { 56 | log.WithFields(fields).Trace(msg) 57 | } 58 | } 59 | 60 | func LogDebug(msg string, fields log.Fields) { 61 | 62 | if fields == nil { 63 | 64 | log.Debug(msg) 65 | 66 | } else { 67 | log.WithFields(fields).Debug(msg) 68 | } 69 | } 70 | 71 | func LogDebugFmt(err string, a ...interface{}) { 72 | 73 | log.Debugf(err, a...) 74 | } 75 | 76 | func LogInfo(msg string, fields log.Fields) { 77 | 78 | if fields == nil { 79 | 80 | log.Info(msg) 81 | 82 | } else { 83 | log.WithFields(fields).Info(msg) 84 | } 85 | } 86 | 87 | func LogInfoFmt(err string, a ...interface{}) { 88 | 89 | log.Infof(err, a...) 90 | } 91 | 92 | func LogWarn(msg string, fields log.Fields) { 93 | 94 | if fields == nil { 95 | 96 | log.Warn(msg) 97 | 98 | } else { 99 | log.WithFields(fields).Warn(msg) 100 | } 101 | } 102 | 103 | func LogError(msg string, fields log.Fields) { 104 | 105 | if fields == nil { 106 | 107 | log.Error(msg) 108 | 109 | } else { 110 | log.WithFields(fields).Error(msg) 111 | } 112 | } 113 | 114 | func LogErrorFmt(err string, a ...interface{}) { 115 | 116 | log.Errorf(err, a...) 117 | } 118 | 119 | func LogFatal(msg string, fields log.Fields) { 120 | 121 | if fields == nil { 122 | 123 | log.Fatal(msg) 124 | 125 | } else { 126 | log.WithFields(fields).Fatal(msg) 127 | } 128 | } 129 | 130 | func LogPanic(msg string, fields log.Fields) { 131 | 132 | if fields == nil { 133 | 134 | log.Panic(msg) 135 | 136 | } else { 137 | log.WithFields(fields).Panic(msg) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /fennelcli/cmd/cal.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "fmt" 35 | "github.com/spf13/cobra" 36 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 37 | ) 38 | 39 | // calCmd represents the domain command 40 | var calCmd = &cobra.Command{ 41 | Use: "cal", 42 | Short: "Add, change and manage calendars.", 43 | Long: `Add, change and manage calendars. Requires a subcommand.`, 44 | RunE: nil, 45 | } 46 | 47 | var calListCmd = &cobra.Command{ 48 | Use: "list", 49 | Short: "List all calendars.", 50 | Long: `List all calendars.`, 51 | RunE: ListCal, 52 | } 53 | 54 | var calAddCmd = &cobra.Command{ 55 | Use: "add [username] [calendar]", 56 | Short: "Add new calendar for given user.", 57 | Long: `Add new user to this instance of Wombag.`, 58 | Args: cobra.ExactArgs(2), 59 | RunE: AddCal, 60 | } 61 | 62 | var calDeleteCmd = &cobra.Command{ 63 | Use: "delete [calendar]", 64 | Short: "Deletes a calendar for given user.", 65 | Long: `Deletes a calendar for given user.`, 66 | Args: cobra.ExactArgs(1), 67 | RunE: DeleteCal, 68 | } 69 | 70 | func ListCal(cmd *cobra.Command, args []string) error { 71 | 72 | tablemodule.ListCal() 73 | 74 | return nil 75 | } 76 | 77 | func AddCal(cmd *cobra.Command, args []string) error { 78 | 79 | if len(args) < 2 { 80 | return fmt.Errorf("command 'add' needs a user name and a password") 81 | } 82 | 83 | // TODO 84 | //tablemodule.AddCal(args[0], args[1]) 85 | 86 | return nil 87 | } 88 | 89 | func DeleteCal(cmd *cobra.Command, args []string) error { 90 | 91 | if len(args) < 1 { 92 | return fmt.Errorf("command 'delete' needs a user identification") 93 | } 94 | 95 | tablemodule.DeleteCal(args[0]) 96 | 97 | return nil 98 | } 99 | 100 | func init() { 101 | 102 | // TODO reactivate once its running 103 | //RootCmd.AddCommand(calCmd) 104 | 105 | calCmd.AddCommand(calListCmd) 106 | calCmd.AddCommand(calAddCmd) 107 | calCmd.AddCommand(calDeleteCmd) 108 | 109 | // Here you will define your flags and configuration settings. 110 | 111 | // Cobra supports Persistent Flags which will work for this command 112 | // and all subcommands, e.g.: 113 | // domainCmd.PersistentFlags().String("foo", "", "A help for foo") 114 | 115 | // Cobra supports local flags which will only run when this command 116 | // is called directly, e.g.: 117 | // domainCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 118 | } 119 | -------------------------------------------------------------------------------- /fennelcli/cmd/card.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "fmt" 35 | "github.com/spf13/cobra" 36 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 37 | ) 38 | 39 | // calCmd represents the domain command 40 | var cardCmd = &cobra.Command{ 41 | Use: "card", 42 | Short: "Add, change and manage address books.", 43 | Long: `Add, change and manage address books. Requires a subcommand.`, 44 | RunE: nil, 45 | } 46 | 47 | var cardListCmd = &cobra.Command{ 48 | Use: "list", 49 | Short: "List all address books.", 50 | Long: `List all address books.`, 51 | RunE: ListCal, 52 | } 53 | 54 | var cardAddCmd = &cobra.Command{ 55 | Use: "add [username] [calendar]", 56 | Short: "Add new calendar for given user.", 57 | Long: `Add new user to this instance of Wombag.`, 58 | Args: cobra.ExactArgs(2), 59 | RunE: AddCal, 60 | } 61 | 62 | var cardDeleteCmd = &cobra.Command{ 63 | Use: "delete [calendar]", 64 | Short: "Deletes a calendar for given user.", 65 | Long: `Deletes a calendar for given user.`, 66 | Args: cobra.ExactArgs(1), 67 | RunE: DeleteCal, 68 | } 69 | 70 | func ListCard(cmd *cobra.Command, args []string) error { 71 | 72 | tablemodule.ListCal() 73 | 74 | return nil 75 | } 76 | 77 | func AddCard(cmd *cobra.Command, args []string) error { 78 | 79 | if len(args) < 2 { 80 | return fmt.Errorf("command 'add' needs a user name and a password") 81 | } 82 | 83 | // TODO 84 | //tablemodule.AddCal(args[0], args[1]) 85 | 86 | return nil 87 | } 88 | 89 | func DeleteCard(cmd *cobra.Command, args []string) error { 90 | 91 | if len(args) < 1 { 92 | return fmt.Errorf("command 'delete' needs a user identification") 93 | } 94 | 95 | tablemodule.DeleteVCard(args[0]) 96 | 97 | return nil 98 | } 99 | 100 | func init() { 101 | 102 | // TODO reactivate once its running 103 | //RootCmd.AddCommand(calCmd) 104 | 105 | cardCmd.AddCommand(cardListCmd) 106 | cardCmd.AddCommand(cardAddCmd) 107 | cardCmd.AddCommand(cardDeleteCmd) 108 | 109 | // Here you will define your flags and configuration settings. 110 | 111 | // Cobra supports Persistent Flags which will work for this command 112 | // and all subcommands, e.g.: 113 | // domainCmd.PersistentFlags().String("foo", "", "A help for foo") 114 | 115 | // Cobra supports local flags which will only run when this command 116 | // is called directly, e.g.: 117 | // domainCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 118 | } 119 | -------------------------------------------------------------------------------- /fenneld/auth/base.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "github.com/pkg/errors" 7 | "github.com/swordlordcodingcrew/fennel/fennelcore" 8 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 9 | "github.com/urfave/negroni" 10 | "net/http" 11 | "strings" 12 | ) 13 | 14 | /*----------------------------------------------------------------------------- 15 | ** 16 | ** - Fennel - 17 | ** 18 | ** your lightweight CalDAV and CardDAV server 19 | ** 20 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 21 | ** and contributing authors 22 | ** 23 | ** This program is free software; you can redistribute it and/or modify it 24 | ** under the terms of the GNU Affero General Public License as published by the 25 | ** Free Software Foundation, either version 3 of the License, or (at your option) 26 | ** any later version. 27 | ** 28 | ** This program is distributed in the hope that it will be useful, but WITHOUT 29 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 30 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 31 | ** for more details. 32 | ** 33 | ** You should have received a copy of the GNU Affero General Public License 34 | ** along with this program. If not, see . 35 | ** 36 | **----------------------------------------------------------------------------- 37 | ** 38 | ** Original Authors: 39 | ** LordEidi@swordlord.com 40 | ** LordCelery@swordlord.com 41 | ** 42 | -----------------------------------------------------------------------------*/ 43 | 44 | func NewFennelAuthentication() negroni.HandlerFunc { 45 | 46 | return func(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) { 47 | 48 | // do not manage service detection 49 | if strings.HasPrefix(req.RequestURI, "/.well-known") { 50 | next(w, req) 51 | return 52 | } 53 | 54 | authHeader := req.Header.Get("Authorization") 55 | 56 | // not authenticated 57 | if len(authHeader) == 0 { 58 | 59 | handler.RespondWithUnauthenticated(w) 60 | return 61 | } 62 | 63 | err, uid, pwd := parseAuthHeader(authHeader) 64 | if err != nil { 65 | 66 | handler.RespondWithUnauthenticated(w) 67 | return 68 | } 69 | 70 | err, roles := ValidateUser(uid, pwd) 71 | if err != nil { 72 | 73 | handler.RespondWithUnauthenticated(w) 74 | return 75 | } 76 | 77 | println(roles) 78 | 79 | // env/context var when authenticated 80 | ctx := req.Context() 81 | ctx = context.WithValue(ctx, "auth_user", uid) 82 | 83 | next(w, req.WithContext(ctx)) 84 | } 85 | } 86 | 87 | func ValidateUser(uid string, pwd string) (error, string) { 88 | 89 | authModule := fennelcore.GetStringFromConfig("auth.module") 90 | 91 | switch authModule { 92 | 93 | case "htpasswd": 94 | return ValidateUserHTPasswd(uid, pwd) 95 | case "ldap": 96 | case "db": 97 | return ValidateDB(uid, pwd) 98 | case "courier": 99 | return ValidateCourier(uid, pwd) 100 | default: 101 | return errors.New("Authentication Module unknown, can't authenticate"), "" 102 | } 103 | 104 | return errors.New("Authentication Module unknown, can't authenticate"), "" 105 | } 106 | 107 | func parseAuthHeader(header string) (error, string, string) { 108 | 109 | aHeader := strings.SplitN(header, " ", 2) 110 | 111 | if len(aHeader) != 2 || aHeader[0] != "Basic" { 112 | return errors.New("There is no Basic Header, or some other Header problem"), "", "" 113 | } 114 | 115 | sDecoded, err := base64.StdEncoding.DecodeString(aHeader[1]) 116 | if err != nil { 117 | return errors.New("Can't decrypt BASE64"), "", "" 118 | } 119 | 120 | aUidPwd := strings.SplitN(string(sDecoded), ":", 2) 121 | if len(aUidPwd) != 2 { 122 | return errors.New("Can't split to username:password"), "", "" 123 | } 124 | 125 | return nil, aUidPwd[0], aUidPwd[1] 126 | } 127 | -------------------------------------------------------------------------------- /fenneld/handler/addressbook/general.go: -------------------------------------------------------------------------------- 1 | package addressbook 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "fmt" 35 | "github.com/gorilla/mux" 36 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 37 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 38 | "io/ioutil" 39 | "log" 40 | "net/http" 41 | "strings" 42 | ) 43 | 44 | func Proppatch(w http.ResponseWriter, req *http.Request) { 45 | 46 | handler.RespondWithMessage(w, http.StatusOK, "Not implemented yet") 47 | } 48 | 49 | func Options(w http.ResponseWriter, req *http.Request) { 50 | 51 | handler.RespondWithStandardOptions(w, http.StatusOK, "") 52 | } 53 | 54 | func Put(w http.ResponseWriter, req *http.Request) { 55 | 56 | vars := mux.Vars(req) 57 | sAB := vars["addressbook"] 58 | sCard := vars["card"] 59 | 60 | sUser, ok := req.Context().Value("auth_user").(string) 61 | if !ok { 62 | 63 | fmt.Println("err: could not find auth_user in context") 64 | handler.RespondWithMessage(w, http.StatusPreconditionFailed, "No user supplied") 65 | return 66 | } 67 | 68 | bodyBuffer, _ := ioutil.ReadAll(req.Body) 69 | 70 | isGroup := strings.Contains(string(bodyBuffer), "X-ADDRESSBOOKSERVER-KIND:group") 71 | 72 | vcard, err := tablemodule.AddVCard(sCard, sUser, sAB, isGroup, string(bodyBuffer)) 73 | if err != nil { 74 | 75 | fmt.Println("err: could not add vcard " + sCard) 76 | handler.RespondWithMessage(w, http.StatusPreconditionFailed, err.Error()) 77 | return 78 | } 79 | 80 | handler.RespondWithMessage(w, http.StatusCreated, "VCARD added: "+vcard.Pkey) 81 | } 82 | 83 | func Get(w http.ResponseWriter, req *http.Request) { 84 | 85 | vars := mux.Vars(req) 86 | sCard := vars["card"] 87 | 88 | vcard, err := tablemodule.GetVCard(sCard) 89 | 90 | if err != nil { 91 | 92 | fmt.Println("err: could not find vcard " + sCard) 93 | 94 | handler.RespondWithMessage(w, http.StatusInternalServerError, err.Error()) 95 | return 96 | } 97 | 98 | handler.RespondWithVCARD(w, http.StatusOK, vcard.Content) 99 | } 100 | 101 | func Delete(w http.ResponseWriter, req *http.Request) { 102 | 103 | vars := mux.Vars(req) 104 | sCard := vars["card"] 105 | 106 | err := tablemodule.DeleteVCard(sCard) 107 | 108 | if err != nil { 109 | log.Printf("Error with deleting VCard %q: %s\n", sCard) 110 | 111 | handler.RespondWithMessage(w, http.StatusInternalServerError, err.Error()) 112 | 113 | return 114 | } 115 | 116 | handler.RespondWithMessage(w, http.StatusOK, "Deleted") 117 | } 118 | 119 | func Move(w http.ResponseWriter, req *http.Request) { 120 | 121 | handler.RespondWithMessage(w, http.StatusOK, "Not implemented yet") 122 | } 123 | -------------------------------------------------------------------------------- /fenneld/auth/htpasswd.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "bytes" 35 | "crypto/sha1" 36 | "crypto/subtle" 37 | "encoding/base64" 38 | "encoding/csv" 39 | "errors" 40 | "github.com/swordlordcodingcrew/fennel/fennelcore" 41 | "os" 42 | "strings" 43 | ) 44 | 45 | type usermap struct { 46 | Users map[string]string // The map of htpasswd User key value pairs 47 | IsInitialised bool 48 | } 49 | 50 | var um usermap // A reference to the singleton 51 | 52 | func LoadHTPasswd(fromFile string) error { 53 | 54 | r, err := os.Open(fromFile) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | defer r.Close() 60 | 61 | csv_reader := csv.NewReader(r) 62 | csv_reader.Comma = ':' 63 | csv_reader.Comment = '#' 64 | csv_reader.TrimLeadingSpace = true 65 | 66 | records, err := csv_reader.ReadAll() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | // Create a straps object 72 | um = usermap{ 73 | Users: make(map[string]string), 74 | IsInitialised: false, 75 | } 76 | 77 | for _, record := range records { 78 | 79 | um.Users[record[0]] = record[1] 80 | //println(record) 81 | } 82 | 83 | um.IsInitialised = true 84 | 85 | return nil 86 | } 87 | 88 | func ValidateUserHTPasswd(uid string, pwd string) (error, string) { 89 | 90 | // lazy initialisation 91 | if !um.IsInitialised { 92 | 93 | htpasswd := fennelcore.GetStringFromConfig("auth.file") 94 | err := LoadHTPasswd(htpasswd) 95 | if err != nil { 96 | return err, "" 97 | } 98 | } 99 | 100 | pwdHash := um.Users[uid] 101 | 102 | // check password 103 | if strings.HasPrefix(pwdHash, "{SHA}") { 104 | 105 | d := sha1.New() 106 | d.Write([]byte(pwd)) 107 | if subtle.ConstantTimeCompare([]byte(pwdHash)[5:], []byte(base64.StdEncoding.EncodeToString(d.Sum(nil)))) != 1 { 108 | return errors.New("Password not correct"), "" 109 | } 110 | } else if strings.HasPrefix(pwdHash, "$apr1$") { 111 | 112 | err := compareMD5HashAndPassword([]byte(pwdHash), []byte(pwd)) 113 | if err != nil { 114 | return err, "" 115 | } else { 116 | return nil, "" 117 | } 118 | } else { 119 | 120 | } 121 | 122 | return nil, "" 123 | } 124 | 125 | func compareMD5HashAndPassword(hashedPassword, password []byte) error { 126 | parts := bytes.SplitN(hashedPassword, []byte("$"), 4) 127 | if len(parts) != 4 { 128 | return errors.New("Password not correct") 129 | } 130 | magic := []byte("$" + string(parts[1]) + "$") 131 | salt := parts[2] 132 | 133 | if subtle.ConstantTimeCompare(hashedPassword, MD5Crypt(password, salt, magic)) != 1 { 134 | return errors.New("Password not correct") 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /fennelcore/ics.go: -------------------------------------------------------------------------------- 1 | package fennelcore 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "github.com/Jeffail/gabs" 35 | "regexp" 36 | "strings" 37 | ) 38 | 39 | var ( 40 | rEOL *regexp.Regexp 41 | ) 42 | 43 | func GetRegexEndOfLine() *regexp.Regexp { 44 | 45 | // lazy init and global member to not re-compile constantly... 46 | if rEOL == nil { 47 | 48 | r, err := regexp.Compile("[\r\n|\n\r|\n|\r]") 49 | if err != nil { 50 | 51 | panic(err) 52 | } 53 | 54 | rEOL = r 55 | } 56 | 57 | return rEOL 58 | } 59 | 60 | func ParseICS(file string) *gabs.Container { 61 | 62 | json := generateJSON(file) 63 | 64 | println(json) 65 | 66 | jsonParsed, err := gabs.ParseJSON([]byte(json)) 67 | 68 | if err != nil { 69 | println(err) 70 | return nil 71 | } 72 | 73 | return jsonParsed 74 | } 75 | 76 | func generateJSON(file string) string { 77 | 78 | var result = "" 79 | 80 | r := GetRegexEndOfLine() 81 | linesUnfiltered := r.Split(file, -1) 82 | 83 | lines := linesUnfiltered[:0] 84 | 85 | // clean up lines 86 | for _, line := range linesUnfiltered { 87 | 88 | // remove empty lines 89 | if len(line) == 0 { 90 | continue 91 | } 92 | 93 | // Unfold the lines, if no : at any position, assume it is folded with previous 94 | if !strings.Contains(line, ":") { 95 | 96 | lines[len(lines)-1] = lines[len(lines)-1] + line 97 | } 98 | 99 | // line seems to be ok, add it to cleaned list 100 | lines = append(lines, line) 101 | } 102 | 103 | for _, line := range lines { 104 | 105 | if strings.HasPrefix(line, "BEGIN:") { 106 | 107 | if strings.HasSuffix(line, ".") { 108 | result += "\"" + line[6:len(line)-1] + "\": {" 109 | } else { 110 | result += "\"" + line[6:] + "\": {" 111 | } 112 | } else if strings.HasPrefix(line, "END:") { 113 | 114 | // TODO: terrible hack, fixme 115 | if strings.HasSuffix(result, ",") { 116 | result = result[0 : len(result)-1] 117 | } 118 | 119 | result += "}," 120 | } else { 121 | 122 | arrKeyVal := strings.Split(line, ":") 123 | key := arrKeyVal[0] 124 | val := arrKeyVal[1] 125 | 126 | if strings.HasSuffix(val, ".") { 127 | // todo: the key.split is a terrible hack as well, we loose some information like that 128 | // example: DTEND;TZID=Europe/Zurich:20161210T010000Z. -> tzid will be lost 129 | // result += "\"" + key.split(";")[0] + "\":\"" + val.substr(0, val.length -1) + "\","; 130 | 131 | result += "\"" + strings.Split(key, ";")[0] + "\":\"" + val[0:len(val)-1] + "\"," 132 | } else { 133 | // todo, see above 134 | result += "\"" + key + "\":\"" + val + "\"," 135 | } 136 | } 137 | } 138 | 139 | // TODO: terrible hack, fixme 140 | if strings.HasSuffix(result, ",") { 141 | result = result[0 : len(result)-1] 142 | } 143 | 144 | result = "{" + result + "}" 145 | 146 | return result 147 | } 148 | -------------------------------------------------------------------------------- /fennelcore/db/tablemodule/cal.go: -------------------------------------------------------------------------------- 1 | package tablemodule 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "fmt" 34 | fcdb "github.com/swordlordcodingcrew/fennel/fennelcore" 35 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 36 | "log" 37 | ) 38 | 39 | func ListCal() { 40 | 41 | db := fcdb.GetDB() 42 | 43 | var rows []*model.CAL 44 | 45 | db.Find(&rows) 46 | 47 | var cal [][]string 48 | 49 | for _, rec := range rows { 50 | 51 | cal = append(cal, []string{rec.Pkey, rec.CrtDat.Format("2006-01-02 15:04:05"), rec.UpdDat.Format("2006-01-02 15:04:05")}) 52 | } 53 | 54 | fcdb.WriteTable([]string{"Id", "CrtDat", "UpdDat"}, cal) 55 | } 56 | 57 | func AddCal(user string, calId string, displayname string, colour string, freebusyset string, order int, supportedCalComponent string, synctoken int, timezone string) (model.CAL, error) { 58 | 59 | db := fcdb.GetDB() 60 | 61 | cal := model.CAL{Pkey: calId} 62 | 63 | cal.Owner = user 64 | cal.Displayname = displayname 65 | cal.Colour = colour 66 | cal.FreeBusySet = freebusyset 67 | cal.Order = order 68 | cal.SupportedCalComponent = supportedCalComponent 69 | cal.Synctoken = synctoken 70 | cal.Timezone = timezone 71 | 72 | retDB := db.Create(&cal) 73 | 74 | if retDB.Error != nil { 75 | log.Printf("Error with CAL %q: %s\n", calId, retDB.Error) 76 | return model.CAL{}, retDB.Error 77 | } 78 | 79 | fmt.Printf("CAL %s for user %s added.\n", calId, user) 80 | 81 | return cal, nil 82 | } 83 | 84 | func GetCal(calId string) (model.CAL, error) { 85 | 86 | db := fcdb.GetDB() 87 | 88 | var cal model.CAL 89 | retDB := db.First(&cal, "pkey = ?", calId) 90 | 91 | // retDB := db.Model(&model.CAL{}).Where("pkey=?", calId) 92 | //retDB := db.Where("Pkey = ?", calId).First(&model.CAL{}) 93 | if retDB.Error != nil { 94 | log.Printf("Error with loading CAL %q: %s\n", calId, retDB.Error) 95 | return model.CAL{}, retDB.Error 96 | } 97 | 98 | return cal, nil 99 | } 100 | 101 | /* 102 | func UpdateAddressbook(name string, password string) error { 103 | 104 | db := fcdb.GetDB() 105 | 106 | pwd, err := hashPassword(password) 107 | if err != nil { 108 | log.Printf("Error with hashing password %q: %s\n", password, err ) 109 | return err 110 | } 111 | 112 | retDB := db.Model(&model.ADB{}).Where("Id=?", name).Update("Token", pwd) 113 | 114 | if retDB.Error != nil { 115 | log.Printf("Error with Device %q: %s\n", name, retDB.Error) 116 | return retDB.Error 117 | } 118 | 119 | fmt.Printf("Device %s updated.\n", name) 120 | 121 | return nil 122 | } */ 123 | 124 | func DeleteCal(name string) { 125 | 126 | db := fcdb.GetDB() 127 | 128 | cal := &model.CAL{} 129 | 130 | retDB := db.Where("id = ?", name).First(&cal) 131 | 132 | if retDB.Error != nil { 133 | log.Printf("Error with Device %q: %s\n", name, retDB.Error) 134 | log.Fatal(retDB.Error) 135 | return 136 | } 137 | 138 | if retDB.RowsAffected <= 0 { 139 | log.Printf("Device not found: %s\n", name) 140 | log.Fatal("Device not found: " + name + "\n") 141 | return 142 | } 143 | 144 | log.Printf("Deleting Calendar: %s", &cal.Pkey) 145 | 146 | db.Delete(&cal) 147 | 148 | fmt.Printf("Calendar %s deleted.\n", name) 149 | } 150 | -------------------------------------------------------------------------------- /fenneld/handler/principal/report.go: -------------------------------------------------------------------------------- 1 | package principal 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "fmt" 34 | "github.com/beevik/etree" 35 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 36 | "net/http" 37 | ) 38 | 39 | func Report(w http.ResponseWriter, req *http.Request) { 40 | 41 | doc := etree.NewDocument() 42 | size, err := doc.ReadFrom(req.Body) 43 | if err != nil || size == 0 { 44 | 45 | fmt.Printf("Error reading XML Body. Error: %s Size: %v", err, size) 46 | 47 | handler.RespondWithMessage(w, http.StatusInternalServerError, "") 48 | return 49 | } 50 | 51 | root := doc.Root() 52 | name := root.Tag 53 | 54 | /* 55 | 56 | 57 | */ 58 | 59 | switch name { 60 | 61 | case "principal-search-property-set": 62 | handleSearchPropertySet(w, req.RequestURI, root) 63 | 64 | default: 65 | if name != "text" { 66 | fmt.Println("Principal-Report: not handled: " + name) 67 | } 68 | } 69 | } 70 | 71 | func handleSearchPropertySet(w http.ResponseWriter, url string, root *etree.Element) { 72 | 73 | /* 74 | 75 | 76 | 77 | 78 | 79 | 80 | Display name 81 | 82 | 83 | 84 | 85 | 86 | Email address 87 | 88 | 89 | 90 | 91 | 92 | Calendar address 93 | 94 | 95 | */ 96 | 97 | dRet := etree.NewDocument() 98 | dRet.Indent(2) 99 | dRet.CreateProcInst("xml", `version="1.0" encoding="utf-8"`) 100 | 101 | psps := dRet.CreateElement("principal-search-property-set") 102 | psps.Space = "d" 103 | 104 | psps.CreateAttr("xmlns:d", "DAV:") 105 | psps.CreateAttr("xmlns:d", "DAV:") 106 | psps.CreateAttr("xmlns:s", "http://github.com/swordlordcodingcrew/fennel/ns") 107 | psps.CreateAttr("xmlns:cal", "urn:ietf:params:xml:ns:caldav") 108 | psps.CreateAttr("xmlns:cs", "http://calendarserver.org/ns/") 109 | psps.CreateAttr("xmlns:card", "urn:ietf:params:xml:ns:carddav") 110 | 111 | addPrincipalSearchProperty(psps, "displayname", "d", "Display Name") 112 | addPrincipalSearchProperty(psps, "email-address", "s", "Email address") 113 | addPrincipalSearchProperty(psps, "calendar-user-address-set", "cal", "Calendar address") 114 | 115 | handler.SendETreeDocument(w, http.StatusOK, dRet) 116 | } 117 | 118 | func addPrincipalSearchProperty(psps *etree.Element, prop string, ns string, desc string) { 119 | 120 | /* 121 | 122 | 123 | 124 | 125 | Calendar address 126 | 127 | */ 128 | 129 | psp := psps.CreateElement("principal-search-property") 130 | psp.Space = "d" 131 | 132 | p := psp.CreateElement("prop") 133 | p.Space = "d" 134 | 135 | el := p.CreateElement(prop) 136 | el.Space = ns 137 | 138 | d := psp.CreateElement("description") 139 | d.Space = "d" 140 | d.CreateAttr("xml:lang", "en") 141 | d.SetText(desc) 142 | } 143 | -------------------------------------------------------------------------------- /fennelcore/db/tablemodule/vcard.go: -------------------------------------------------------------------------------- 1 | package tablemodule 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "fmt" 34 | fcdb "github.com/swordlordcodingcrew/fennel/fennelcore" 35 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 36 | "log" 37 | ) 38 | 39 | func ListVCardsPerAddressbook(addressbook string) { 40 | 41 | var vcard [][]string 42 | 43 | err, rows := FindVcardsByAddressbook(addressbook) 44 | if err != nil { 45 | 46 | log.Printf("Error with VCARD in Addressbook %q: %s\n", addressbook, err) 47 | return 48 | } 49 | 50 | for _, rec := range rows { 51 | 52 | vcard = append(vcard, []string{rec.Pkey, rec.CrtDat.Format("2006-01-02 15:04:05"), rec.UpdDat.Format("2006-01-02 15:04:05")}) 53 | } 54 | 55 | fcdb.WriteTable([]string{"Id", "CrtDat", "UpdDat"}, vcard) 56 | } 57 | 58 | func AddVCard(vcardId string, owner string, addressbookId string, isGroup bool, content string) (model.VCARD, error) { 59 | 60 | db := fcdb.GetDB() 61 | 62 | vcard := model.VCARD{Pkey: vcardId} 63 | 64 | vcard.AddressbookId = addressbookId 65 | vcard.Owner = owner 66 | vcard.Content = content 67 | vcard.IsGroup = isGroup 68 | 69 | retDB := db.Create(&vcard) 70 | 71 | if retDB.Error != nil { 72 | log.Printf("Error with VCARD %q: %s\n", vcardId, retDB.Error) 73 | return model.VCARD{}, retDB.Error 74 | } 75 | 76 | fmt.Printf("VCARD %s for owner %s added.\n", vcardId, owner) 77 | 78 | return vcard, nil 79 | } 80 | 81 | func UpdateVCard(name string, password string) error { 82 | 83 | return nil 84 | } 85 | 86 | func GetVCard(vcardId string) (model.VCARD, error) { 87 | 88 | db := fcdb.GetDB() 89 | 90 | var vcard model.VCARD 91 | retDB := db.First(&vcard, "pkey = ?", vcardId) 92 | 93 | if retDB.Error != nil { 94 | log.Printf("Error with loading VCARD %q: %s\n", vcardId, retDB.Error) 95 | return model.VCARD{}, retDB.Error 96 | } 97 | 98 | return vcard, nil 99 | } 100 | 101 | func FindVcardsByAddressbook(addressbookID string) (error, []*model.VCARD) { 102 | 103 | var vcard model.VCARD 104 | 105 | db := fcdb.GetDB() 106 | db = db.Model(vcard).Where(model.VCARD{AddressbookId: addressbookID}) 107 | 108 | var rows []*model.VCARD 109 | 110 | retDB := db.Find(&rows) 111 | 112 | if retDB.Error != nil { 113 | log.Printf("Error with loading VCARD %s\n", retDB.Error) 114 | return retDB.Error, rows 115 | } 116 | 117 | return nil, rows 118 | } 119 | 120 | func FindVCardsFromAddressbook(adbID string, vcardIDs []string) (error, []*model.VCARD) { 121 | 122 | var vcard model.VCARD 123 | 124 | db := fcdb.GetDB() 125 | db = db.Model(vcard) 126 | 127 | db = db.Where("pkey in (?)", vcardIDs).Where("addressbook_id = ?", adbID) 128 | 129 | var rows []*model.VCARD 130 | 131 | retDB := db.Find(&rows) 132 | 133 | if retDB.Error != nil { 134 | log.Printf("Error with VCARD from Addressbook %s: %s\n", adbID, retDB.Error) 135 | return retDB.Error, rows 136 | } 137 | 138 | return nil, rows 139 | } 140 | 141 | func DeleteVCard(vcardId string) error { 142 | 143 | db := fcdb.GetDB() 144 | 145 | vcard := &model.VCARD{} 146 | 147 | retDB := db.Where("pkey = ?", vcardId).First(&vcard) 148 | 149 | if retDB.Error != nil { 150 | log.Printf("Error with VCARD %q: %s\n", vcardId, retDB.Error) 151 | log.Fatal(retDB.Error) 152 | return retDB.Error 153 | } 154 | 155 | if retDB.RowsAffected <= 0 { 156 | log.Printf("VCARD not found: %s\n", vcardId) 157 | log.Fatal("VCARD not found: " + vcardId + "\n") 158 | return retDB.Error 159 | } 160 | 161 | log.Printf("Deleting VCARD: %s", &vcard.Pkey) 162 | 163 | ret := db.Delete(&vcard) 164 | 165 | return ret.Error 166 | } 167 | -------------------------------------------------------------------------------- /fennelcore/db/tablemodule/addressbook.go: -------------------------------------------------------------------------------- 1 | package tablemodule 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "fmt" 34 | fcdb "github.com/swordlordcodingcrew/fennel/fennelcore" 35 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 36 | "log" 37 | ) 38 | 39 | func ListAddressbook() { 40 | 41 | db := fcdb.GetDB() 42 | 43 | var rows []*model.ADB 44 | 45 | db.Find(&rows) 46 | 47 | var adb [][]string 48 | 49 | for _, rec := range rows { 50 | 51 | adb = append(adb, []string{rec.Pkey, rec.CrtDat.Format("2006-01-02 15:04:05"), rec.UpdDat.Format("2006-01-02 15:04:05")}) 52 | } 53 | 54 | //wombag.WriteTable([]string{"Id", "CrtDat", "UpdDat"}, adb) 55 | } 56 | 57 | func AddAddressbook(name string, password string, user string) (model.ADB, error) { 58 | 59 | db := fcdb.GetDB() 60 | 61 | _, err := hashPassword(password) 62 | if err != nil { 63 | log.Printf("Error with hashing password %q: %s\n", password, err) 64 | return model.ADB{}, err 65 | } 66 | 67 | adb := model.ADB{Pkey: name} 68 | retDB := db.Create(&adb) 69 | 70 | if retDB.Error != nil { 71 | log.Printf("Error with Device %q: %s\n", name, retDB.Error) 72 | log.Fatal(retDB.Error) 73 | return model.ADB{}, retDB.Error 74 | } 75 | 76 | fmt.Printf("Device %s for user %s added.\n", name, user) 77 | 78 | return adb, nil 79 | } 80 | 81 | func GetAddressbooksFromUser(user string) (error, []*model.ADB) { 82 | 83 | var adb model.ADB 84 | 85 | db := fcdb.GetDB() 86 | db = db.Model(adb) 87 | 88 | db = db.Where("owner = ?", user) 89 | 90 | var rows []*model.ADB 91 | 92 | retDB := db.Find(&rows) 93 | 94 | if retDB.Error != nil { 95 | log.Printf("Error with ADB from User %q: %s\n", user, retDB.Error) 96 | return retDB.Error, rows 97 | } 98 | 99 | return nil, rows 100 | } 101 | 102 | func GetAddressbookByName(name string) (error, *model.ADB) { 103 | 104 | var adb model.ADB 105 | 106 | db := fcdb.GetDB() 107 | db = db.Model(adb) 108 | 109 | db = db.Where("name = ?", name) 110 | 111 | var row *model.ADB 112 | 113 | retDB := db.First(&row) 114 | 115 | if retDB.Error != nil { 116 | log.Printf("Error with ADB by name %q: %s\n", name, retDB.Error) 117 | return retDB.Error, row 118 | } 119 | 120 | return nil, row 121 | } 122 | 123 | func GetOrCreateAddressbookByName(name string, owner string) (error, *model.ADB) { 124 | 125 | var adb model.ADB 126 | 127 | db := fcdb.GetDB() 128 | db = db.Model(adb) 129 | 130 | db = db.Where(model.ADB{Name: name}).Attrs(model.ADB{Owner: owner}) 131 | 132 | var row *model.ADB 133 | 134 | retDB := db.FirstOrCreate(&row) 135 | 136 | if retDB.Error != nil { 137 | log.Printf("Error with ADB by name %q: %s\n", name, retDB.Error) 138 | return retDB.Error, row 139 | } 140 | 141 | return nil, row 142 | } 143 | 144 | func UpdateAddressbook(name string, password string) error { 145 | 146 | db := fcdb.GetDB() 147 | 148 | pwd, err := hashPassword(password) 149 | if err != nil { 150 | log.Printf("Error with hashing password %q: %s\n", password, err) 151 | return err 152 | } 153 | 154 | retDB := db.Model(&model.ADB{}).Where("Id=?", name).Update("Token", pwd) 155 | 156 | if retDB.Error != nil { 157 | log.Printf("Error with Device %q: %s\n", name, retDB.Error) 158 | return retDB.Error 159 | } 160 | 161 | fmt.Printf("Device %s updated.\n", name) 162 | 163 | return nil 164 | } 165 | 166 | func DeleteAddressbook(name string) { 167 | 168 | db := fcdb.GetDB() 169 | 170 | rec := &model.ADB{} 171 | 172 | retDB := db.Where("id = ?", name).First(&rec) 173 | 174 | if retDB.Error != nil { 175 | log.Printf("Error with Device %q: %s\n", name, retDB.Error) 176 | log.Fatal(retDB.Error) 177 | return 178 | } 179 | 180 | if retDB.RowsAffected <= 0 { 181 | log.Printf("Device not found: %s\n", name) 182 | log.Fatal("Device not found: " + name + "\n") 183 | return 184 | } 185 | 186 | log.Printf("Deleting Device: %s", &rec.Pkey) 187 | 188 | db.Delete(&rec) 189 | 190 | fmt.Printf("Device %s deleted.\n", name) 191 | } 192 | -------------------------------------------------------------------------------- /fennelcli/cmd/user.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "fmt" 35 | "github.com/spf13/cobra" 36 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 37 | ) 38 | 39 | // userCmd represents the domain command 40 | var userCmd = &cobra.Command{ 41 | Use: "user", 42 | Short: "Add, change and manage your users.", 43 | Long: `Add, change and manage your users. Requires a subcommand.`, 44 | RunE: nil, 45 | } 46 | 47 | var userListCmd = &cobra.Command{ 48 | Use: "list", 49 | Short: "List all users.", 50 | Long: `List all users.`, 51 | RunE: ListUser, 52 | } 53 | 54 | var userAddCmd = &cobra.Command{ 55 | Use: "add [username] [password]", 56 | Short: "Add new user to this instance of Fennel.", 57 | Long: `Add new user to this instance of Fennel.`, 58 | Args: cobra.ExactArgs(2), 59 | RunE: AddUser, 60 | } 61 | 62 | var userUpdateCmd = &cobra.Command{ 63 | Use: "update [userid] [password] [comment]", 64 | Short: "Update the password and comment of the user.", 65 | Long: `Update the password of the user. Comment field can be left empty`, 66 | Args: cobra.MinimumNArgs(2), 67 | RunE: UpdateUser, 68 | } 69 | 70 | var userVerifyCmd = &cobra.Command{ 71 | Use: "verify [userid] [pwd]", 72 | Short: "Verifies the password of given user.", 73 | Long: `Verifies the password of the given user. Can be used to check if memorised password is correct.`, 74 | Args: cobra.ExactArgs(2), 75 | RunE: VerifyUser, 76 | } 77 | 78 | var userDeleteCmd = &cobra.Command{ 79 | Use: "delete [userid]", 80 | Short: "Deletes a user and all of her devices.", 81 | Long: `Deletes a user and all of his or her devices.`, 82 | Args: cobra.ExactArgs(1), 83 | RunE: DeleteUser, 84 | } 85 | 86 | func ListUser(cmd *cobra.Command, args []string) error { 87 | 88 | tablemodule.ListUser() 89 | 90 | return nil 91 | } 92 | 93 | func AddUser(cmd *cobra.Command, args []string) error { 94 | 95 | if len(args) < 2 { 96 | return fmt.Errorf("command 'add' needs a user name and a password") 97 | } 98 | 99 | tablemodule.AddUser(args[0], args[1]) 100 | 101 | return nil 102 | } 103 | 104 | func VerifyUser(cmd *cobra.Command, args []string) error { 105 | 106 | if len(args) < 2 { 107 | return fmt.Errorf("command 'verify' needs a user name and a password") 108 | } 109 | 110 | isValid, _ := tablemodule.ValidateUserInDB(args[0], args[1]) 111 | 112 | if isValid { 113 | 114 | fmt.Println("User was verified") 115 | } else { 116 | 117 | fmt.Println("User could not be verified") 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func UpdateUser(cmd *cobra.Command, args []string) error { 124 | 125 | argCount := len(args) 126 | 127 | if argCount < 2 { 128 | return fmt.Errorf("command 'update' needs a user identification, a new password and optionally a comment") 129 | } 130 | 131 | comment := "" 132 | 133 | if argCount > 2 { 134 | 135 | comment = args[2] 136 | } 137 | 138 | tablemodule.UpdateUser(args[0], args[1], comment) 139 | 140 | return nil 141 | } 142 | 143 | func DeleteUser(cmd *cobra.Command, args []string) error { 144 | 145 | if len(args) < 1 { 146 | return fmt.Errorf("command 'delete' needs a user identification") 147 | } 148 | 149 | tablemodule.DeleteUser(args[0]) 150 | 151 | return nil 152 | } 153 | 154 | func init() { 155 | RootCmd.AddCommand(userCmd) 156 | 157 | userCmd.AddCommand(userListCmd) 158 | userCmd.AddCommand(userAddCmd) 159 | userCmd.AddCommand(userVerifyCmd) 160 | userCmd.AddCommand(userUpdateCmd) 161 | userCmd.AddCommand(userDeleteCmd) 162 | 163 | // Here you will define your flags and configuration settings. 164 | 165 | // Cobra supports Persistent Flags which will work for this command 166 | // and all subcommands, e.g.: 167 | // domainCmd.PersistentFlags().String("foo", "", "A help for foo") 168 | 169 | // Cobra supports local flags which will only run when this command 170 | // is called directly, e.g.: 171 | // domainCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 172 | } 173 | -------------------------------------------------------------------------------- /fennelcore/db/tablemodule/user.go: -------------------------------------------------------------------------------- 1 | package tablemodule 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "fmt" 34 | fc "github.com/swordlordcodingcrew/fennel/fennelcore" 35 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 36 | "golang.org/x/crypto/bcrypt" 37 | "log" 38 | ) 39 | 40 | // TODO return permission, not true/false. when empty permission, no access... 41 | func ValidateUserInDB(name, password string) (bool, []string) { 42 | 43 | permissions := []string{} 44 | 45 | // TODO get permissions from db 46 | permissions = append(permissions, "permission_from_DB") 47 | 48 | db := fc.GetDB() 49 | 50 | user := &model.User{} 51 | 52 | retDB := db.Where("name = ?", name).First(&user) 53 | 54 | if retDB.Error != nil { 55 | log.Printf("Login of user failed %q: %s\n", name, retDB.Error) 56 | return false, permissions 57 | } 58 | 59 | if retDB.RowsAffected <= 0 { 60 | log.Printf("Login of user failed. User not found: %s\n", name) 61 | return false, permissions 62 | } 63 | 64 | // TODO fill permissions array 65 | 66 | err := checkHashedPassword(user.Password, password) 67 | if err != nil { 68 | log.Printf("Login of user failed %q: %s\n", name, err) 69 | return false, permissions 70 | } else { 71 | 72 | return true, permissions 73 | } 74 | } 75 | 76 | func ListUser() { 77 | 78 | db := fc.GetDB() 79 | 80 | var rows []*model.User 81 | 82 | db.Find(&rows) 83 | 84 | var users [][]string 85 | 86 | for _, user := range rows { 87 | 88 | users = append(users, []string{user.Name, user.Comment, user.CrtDat.Format("2006-01-02 15:04:05"), user.UpdDat.Format("2006-01-02 15:04:05")}) 89 | } 90 | 91 | fc.WriteTable([]string{"Name", "Comment", "CrtDat", "UpdDat"}, users) 92 | } 93 | 94 | func AddUser(name string, password string) (model.User, error) { 95 | 96 | db := fc.GetDB() 97 | 98 | pwd, err := hashPassword(password) 99 | if err != nil { 100 | log.Printf("Error with hashing password %q: %s\n", password, err) 101 | return model.User{}, err 102 | } 103 | 104 | user := model.User{Name: name, Password: pwd} 105 | retDB := db.Create(&user) 106 | 107 | if retDB.Error != nil { 108 | log.Printf("Error with User %q: %s\n", name, retDB.Error) 109 | log.Fatal(retDB.Error) 110 | return model.User{}, retDB.Error 111 | } 112 | 113 | fmt.Printf("User %s added.\n", name) 114 | return user, nil 115 | } 116 | 117 | func UpdateUser(name string, password string, comment string) error { 118 | 119 | db := fc.GetDB() 120 | 121 | user := &model.User{} 122 | 123 | hash, err := hashPassword(password) 124 | if err == nil { 125 | user.Password = hash 126 | } 127 | 128 | // we can set w/o checking if empty, for GORM will only update non-empty fields 129 | user.Comment = comment 130 | 131 | retDB := db.Model(&user).Where("name=?", name).Updates(&user) 132 | if retDB.Error != nil { 133 | log.Printf("Error with User %q: %s\n", name, retDB.Error) 134 | return retDB.Error 135 | } 136 | 137 | fmt.Printf("User %s updated.\n", name) 138 | 139 | return nil 140 | } 141 | 142 | func DeleteUser(name string) { 143 | 144 | db := fc.GetDB() 145 | 146 | user := &model.User{} 147 | 148 | retDB := db.Where("name = ?", name).First(&user) 149 | 150 | if retDB.Error != nil { 151 | log.Printf("Error with User %q: %s\n", name, retDB.Error) 152 | log.Fatal(retDB.Error) 153 | return 154 | } 155 | 156 | if retDB.RowsAffected <= 0 { 157 | log.Printf("User not found: %s\n", name) 158 | log.Fatal("User not found: " + name + "\n") 159 | return 160 | } 161 | 162 | db.Delete(&user) 163 | 164 | fmt.Printf("User %s deleted.\n", name) 165 | } 166 | 167 | func hashPassword(pwd string) (string, error) { 168 | 169 | password := []byte(pwd) 170 | 171 | // Hashing the password with the default cost of 10 172 | hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) 173 | if err != nil { 174 | return "", err 175 | } 176 | 177 | return string(hashedPassword), nil 178 | } 179 | 180 | func checkHashedPassword(hashedPassword string, password string) error { 181 | 182 | pwd := []byte(password) 183 | hashedPwd := []byte(hashedPassword) 184 | 185 | // Comparing the password with the hash 186 | err := bcrypt.CompareHashAndPassword(hashedPwd, pwd) 187 | 188 | // nil means it is a match 189 | return err 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Fennel 2 | ====== 3 | 4 | ![Fennel](https://raw.github.com/swordlordcodingcrew/fennel/master/fennel_logo.png) 5 | 6 | **Fennel** (c) 2014-21 by [SwordLord - the coding crew](http://www.swordlord.com/) 7 | 8 | ## Introduction ## 9 | 10 | **Fennel** is a lightweight CardDAV / CalDAV server. It is written in Go and based on the proof of concept [Fennel.js](https://github.com/LordEidi/fennel.js) (which is written in JavaScript and running on NodeJS). 11 | 12 | If you are looking for a lightweight CalDAV / CardDAV server, **Fennel** might be for you: 13 | 14 | - hassle free installation. Drop a binary, start it, that's it. 15 | - authentication is meant to be pluggable. While we concentrate on CourierAuth and .htaccess, you can add whatever can check a username and a password. 16 | - authorisation is meant to be pluggable as well. 17 | - the data storage backend is meant to be pluggable as well. While we start with SQLite3, we do use an ORM. Whatever database can be used with **Gorm** can be used as storage backend for **Fennel**. 18 | - and after all, **Fennel** is OSS and is written in Go. Whatever you do not like, you are free to replace / rewrite. Just respect the licence and give back. 19 | 20 | ## Status ## 21 | 22 | ![Build Status](https://travis-ci.org/swordlordcodingcrew/fennel.svg?branch=master) 23 | 24 | **Fennel** is beta software and should be handled as such: 25 | 26 | - The CalDAV part is still work in progress. 27 | - The CardDAV part is still work in progress. 28 | 29 | **Fennel** is tested on Calendar on iOS > v10.0 and on OSX Calendar as well as with Mozilla Lightning. If you run 30 | **Fennel** with another client your mileage may vary. 31 | 32 | What's missing: 33 | 34 | - different clients (we will somewhen test with other clients, but we did not do thoroughly yet) 35 | - 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. If you wonder how we test, have a look at the testing code in Fennel.js as well as at the [Project Spoon](https://github.com/swordlordcodingcrew/spoon) 36 | - While **Fennel**'s goal is to have an RBAC based authorisation system, **Fennel** does currently only know global permissions without groups. But we are working on it. 37 | 38 | ## Installation ## 39 | 40 | ### From source ### 41 | 42 | Dependencies: [golang](https://golang.org/dl/), [GNU Make](https://www.gnu.org/software/make/) 43 | 44 | ``` 45 | git clone https://github.com/swordlordcodingcrew/fennel 46 | cd fennel 47 | make 48 | ``` 49 | Executables fenneld and fennelcli are built into the bin/ folder. 50 | 51 | ### How to set up transport security ### 52 | 53 | 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 54 | with nginx, which is a lightweight http server and proxy. 55 | 56 | First prepare your /etc/apt/sources.list file (or just install the standard Debian package, your choice): 57 | 58 | deb http://nginx.org/packages/debian/ stretch nginx 59 | deb-src http://nginx.org/packages/debian/ stretch nginx 60 | 61 | Update apt-cache and install nginx to your system. 62 | 63 | sudo update 64 | sudo apt-get install nginx 65 | 66 | Now configure a proxy configuration so that your instance of nginx will serve / prox the content of / for the 67 | **Fennel** server. To do so, you will need a configuration along this example: 68 | 69 | server { 70 | listen 443; 71 | server_name fennel.yourdomain.tld; 72 | 73 | access_log /var/www/logs/fennel_access.log combined; 74 | error_log /var/www/logs/fennel_error.log; 75 | 76 | root /var/www/pages/; 77 | index index.html index.htm; 78 | 79 | error_page 500 502 503 504 /50x.html; 80 | location = /50x.html { 81 | root /var/www/nginx-default; 82 | } 83 | 84 | location / { 85 | proxy_pass http://127.0.0.1:8888; 86 | proxy_redirect off; 87 | proxy_set_header Host $host; 88 | proxy_set_header X-Real-IP $remote_addr; 89 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 90 | proxy_buffering off; 91 | } 92 | 93 | ssl on; 94 | ssl_certificate /etc/nginx/certs/yourdomain.tld.pem; 95 | ssl_certificate_key /etc/nginx/certs/yourdomain.tld.pem; 96 | ssl_session_timeout 5m; 97 | 98 | # modern configuration. tweak to your needs. 99 | ssl_protocols TLSv1.1 TLSv1.2; 100 | 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'; 101 | ssl_prefer_server_ciphers on; 102 | 103 | # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) 104 | add_header Strict-Transport-Security max-age=15768000; 105 | } 106 | 107 | Please check this site for updates on what TLS settings currently make sense: 108 | 109 | [https://mozilla.github.io/server-side-tls/ssl-config-generator](https://mozilla.github.io/server-side-tls/ssl-config-generator) 110 | 111 | Now run or reset your nginx and start your instance of **Fennel**. 112 | 113 | 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 114 | you want to change the options. 115 | 116 | ## Configuration ## 117 | 118 | All parameters which can be configured right now are in the file *fennel.config.js*. There are not much parameters yet, indeed. 119 | But **Fennel** is not ready production anyway. And you are welcome to help out in adding parameters and configuration 120 | options. 121 | 122 | ## Contribution ## 123 | 124 | If you happen to know how to write Go, 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. 125 | 126 | If some feature is missing, just remember that this is an Open Source Project. If you need something, think about contributing it yourself, pull requests welcome... 127 | 128 | ## Dependencies ## 129 | 130 | When compiling, have a look at the vendor folder. Binaries have no direct Dependency whatsoever. You might want to have your database backend ready though. 131 | 132 | ## License ## 133 | 134 | **Fennel** is published under the GNU Affero General Public Licence version 3. See the LICENCE file for details. 135 | -------------------------------------------------------------------------------- /cmd/fenneld/fenneld.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "net/http/pprof" 34 | 35 | "github.com/swordlordcodingcrew/fennel/fennelcore" 36 | "github.com/swordlordcodingcrew/fennel/fenneld/auth" 37 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 38 | "github.com/swordlordcodingcrew/fennel/fenneld/handler/addressbook" 39 | "github.com/swordlordcodingcrew/fennel/fenneld/handler/calendar" 40 | "github.com/swordlordcodingcrew/fennel/fenneld/handler/principal" 41 | 42 | "github.com/gorilla/mux" 43 | _ "github.com/mattn/go-sqlite3" 44 | "github.com/urfave/negroni" 45 | ) 46 | 47 | func main() { 48 | 49 | // Initialise env and params 50 | fennelcore.InitConfig() 51 | fennelcore.InitLog() 52 | 53 | // Initialise database 54 | // if there is an error, this function will quit the app 55 | fennelcore.InitDatabase() 56 | defer fennelcore.CloseDB() 57 | 58 | logLevel := fennelcore.GetLogLevel() 59 | 60 | // TODO write our own logger using logrus 61 | n := negroni.New(negroni.NewRecovery(), negroni.NewLogger(), auth.NewFennelAuthentication()) 62 | 63 | gr := mux.NewRouter().StrictSlash(false) 64 | 65 | n.UseHandler(gr) 66 | 67 | // TODO add these handlers 68 | //gr.NotFoundHandler 69 | //gr.MethodNotAllowedHandler 70 | 71 | // what to do when a user hits the root 72 | gr.HandleFunc("/", handler.OnRoot).Methods("GET") 73 | gr.HandleFunc("/", handler.OnRoot).Methods("PROPFIND") 74 | 75 | // ******************* SERVICE DISCOVERY 76 | gr.HandleFunc("/.well-known", handler.OnWellKnownNoParam).Methods("GET", "PROPFIND") 77 | gr.HandleFunc("/.well-known/", handler.OnWellKnownNoParam).Methods("GET", "PROPFIND") 78 | gr.HandleFunc("/.well-known/{param:[0-9a-zA-Z-]+}", handler.OnWellKnown).Methods("GET", "PROPFIND") 79 | gr.HandleFunc("/.well-known/{param:[0-9a-zA-Z-]+}/", handler.OnWellKnown).Methods("GET", "PROPFIND") 80 | 81 | // ******************* PRINCIPAL 82 | srP := gr.PathPrefix("/p").Subrouter() 83 | //srP.HandleFunc("", handler.onPrincipal).Methods("GET") -> should not happen? 84 | srP.HandleFunc("/", principal.Options).Methods("OPTIONS") 85 | srP.HandleFunc("/", principal.Report).Methods("REPORT") 86 | srP.HandleFunc("/", principal.Propfind).Methods("PROPFIND") 87 | srP.HandleFunc("/{user:[0-9a-zA-Z-]+}/", principal.Propfind).Methods("PROPFIND") 88 | srP.HandleFunc("/{user:[0-9a-zA-Z-]+}/", principal.Options).Methods("OPTIONS") 89 | srP.HandleFunc("", principal.Proppatch).Methods("PROPPATCH") 90 | 91 | // ******************* CALENDAR 92 | srCal := gr.PathPrefix("/cal").Subrouter() 93 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/", calendar.PropfindRoot).Methods("PROPFIND") 94 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/", calendar.Options).Methods("OPTIONS") 95 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/{calendar:[0-9a-zA-Z-]+}/", calendar.MakeCalendar).Methods("MKCALENDAR") 96 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/{calendar:[0-9a-zA-Z-]+}/{event:[0-9a-zA-Z-]+}.ics", calendar.Put).Methods("PUT") 97 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/{calendar:[0-9a-zA-Z-]+}/{event:[0-9a-zA-Z-]+}.ics", calendar.Get).Methods("GET") 98 | 99 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/", calendar.PropfindUser).Methods("PROPFIND") 100 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/inbox/", calendar.PropfindInbox).Methods("PROPFIND") 101 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/outbox/", calendar.PropfindOutbox).Methods("PROPFIND") 102 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/notifications/", calendar.PropfindNotification).Methods("PROPFIND") 103 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/{calendar:[0-9a-zA-Z-]+}/", calendar.PropfindCalendar).Methods("PROPFIND") 104 | 105 | srCal.HandleFunc("/{user:[0-9a-zA-Z-]+}/{calendar:[0-9a-zA-Z-]+}/", calendar.Report).Methods("REPORT") 106 | 107 | // ******************* ADDRESSBOOK 108 | srCard := gr.PathPrefix("/card").Subrouter() 109 | srCard.HandleFunc("/", addressbook.PropfindRoot).Methods("PROPFIND") //todo find out when this happens... 110 | srCard.HandleFunc("/{user:[0-9a-zA-Z-]+}/", addressbook.PropfindUser).Methods("PROPFIND") 111 | //srCard.HandleFunc("/{user:[0-9a-zA-Z-]+}/{addressbook:[0-9a-zA-Z-]+}/", addressbook.PropfindAddressbook).Methods("PROPFIND") 112 | srCard.HandleFunc("/{user:[0-9a-zA-Z-]+}/{addressbook:[0-9a-zA-Z-]+}/", addressbook.Options).Methods("OPTIONS") 113 | srCard.HandleFunc("/{user:[0-9a-zA-Z-]+}/{addressbook:[0-9a-zA-Z-]+}/", addressbook.Report).Methods("REPORT") 114 | srCard.HandleFunc("/{user:[0-9a-zA-Z-]+}/{addressbook:[0-9a-zA-Z-]+}/{card:[0-9a-zA-Z-]+}.vcf", addressbook.Put).Methods("PUT") 115 | 116 | // get settings 117 | host := fennelcore.GetStringFromConfig("www.host") 118 | port := fennelcore.GetStringFromConfig("www.port") 119 | 120 | // check if user wants to mount debug urls 121 | if logLevel == "debug" { 122 | 123 | // give the user the possibility to trace and profile the app 124 | srDebug := gr.PathPrefix("/debug/pprof").Subrouter() 125 | srDebug.HandleFunc("/block", pprof.Index).Methods("GET") 126 | srDebug.HandleFunc("/heap", pprof.Index).Methods("GET") 127 | srDebug.HandleFunc("/profile", pprof.Profile).Methods("GET") 128 | srDebug.HandleFunc("/symbol", pprof.Symbol).Methods("POST") 129 | srDebug.HandleFunc("/symbol", pprof.Symbol).Methods("GET") 130 | srDebug.HandleFunc("/trace", pprof.Trace).Methods("GET") 131 | 132 | // give the user some hints on what URLs she could test 133 | fennelcore.LogDebugFmt("get options : curl -X OPTIONS 'http://%s:%s/cal/demo/'", host, port) 134 | fennelcore.LogDebugFmt("get block: go tool pprof 'http://%s:%s/debug/pprof/block'", host, port) 135 | fennelcore.LogDebugFmt("get heap: go tool pprof 'http://%s:%s/debug/pprof/heap'", host, port) 136 | fennelcore.LogDebugFmt("get profile: go tool pprof 'http://%s:%s/debug/pprof/profile'", host, port) 137 | fennelcore.LogDebugFmt("post symbol: go tool pprof 'http://%s:%s/debug/pprof/symbol'", host, port) 138 | fennelcore.LogDebugFmt("get symbol: go tool pprof 'http://%s:%s/debug/pprof/symbol'", host, port) 139 | fennelcore.LogDebugFmt("get trace: go tool pprof 'http://%s:%s/debug/pprof/trace'", host, port) 140 | } 141 | 142 | fennelcore.LogInfoFmt("fenneld running on %v:%v.", host, port) 143 | 144 | // have fun with fennel 145 | n.Run(host + ":" + port) 146 | } 147 | -------------------------------------------------------------------------------- /fennelcore/db/tablemodule/ics.go: -------------------------------------------------------------------------------- 1 | package tablemodule 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2019 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "fmt" 34 | "log" 35 | "strings" 36 | "time" 37 | 38 | "github.com/Jeffail/gabs" 39 | fcdb "github.com/swordlordcodingcrew/fennel/fennelcore" 40 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 41 | "github.com/vjeantet/jodaTime" 42 | ) 43 | 44 | const DATEPARSER string = "yMd'T'Hms" 45 | const UTCMARKER string = "'Z'" 46 | 47 | func parseDatesAndFill(json *gabs.Container, ics *model.ICS) error { 48 | 49 | // if with TZID -> parse timezone, directly, otherwise... 50 | // DTSTART;TZID=Europe/Vatican:20190823T180200 51 | // DTEND;TZID=Europe/Vatican:20190823T190200 52 | 53 | // TODO respect the TZID 54 | childMap, err := json.Search("VCALENDAR", "VEVENT").ChildrenMap() 55 | if err != nil { 56 | return err 57 | } 58 | for key, child := range childMap { 59 | if strings.HasPrefix(key, "DTSTART") { 60 | 61 | format := DATEPARSER 62 | 63 | sStart := child.Data().(string) 64 | if sStart[len(sStart)-1:] == "Z" { 65 | format += UTCMARKER 66 | } 67 | 68 | start, err := jodaTime.Parse(format, sStart) 69 | if err != nil { 70 | fcdb.LogErrorFmt("Error when parsing start date '%s' from VEVENT: %s", sStart, err.Error()) 71 | return err 72 | } 73 | 74 | ics.StartDate = start 75 | 76 | } else if strings.HasPrefix(key, "DTEND") { 77 | 78 | format := DATEPARSER 79 | 80 | sEnd := child.Data().(string) 81 | if sEnd[len(sEnd)-1:] == "Z" { 82 | format += UTCMARKER 83 | } 84 | 85 | end, err := jodaTime.Parse(format, sEnd) 86 | if err != nil { 87 | fcdb.LogErrorFmt("Error when parsing end date '%s' from VEVENT: %s", sEnd, err.Error()) 88 | return err 89 | } 90 | 91 | ics.EndDate = end 92 | } 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func ListIcsPerCal(calendar string) { 99 | 100 | db := fcdb.GetDB() 101 | 102 | var rows []*model.ICS 103 | 104 | db.Find(&rows) 105 | 106 | var ics [][]string 107 | 108 | for _, rec := range rows { 109 | 110 | ics = append(ics, []string{rec.Pkey, rec.CrtDat.Format("2006-01-02 15:04:05"), rec.UpdDat.Format("2006-01-02 15:04:05")}) 111 | } 112 | 113 | fcdb.WriteTable([]string{"Id", "CrtDat", "UpdDat"}, ics) 114 | } 115 | 116 | func AddIcs(calId string, user string, calendar string, content string) (model.ICS, error) { 117 | 118 | json := fcdb.ParseICS(content) 119 | 120 | db := fcdb.GetDB() 121 | 122 | ics := model.ICS{Pkey: calId} 123 | 124 | ics.CalendarId = calendar 125 | 126 | err := parseDatesAndFill(json, &ics) 127 | if err != nil { 128 | return model.ICS{}, err 129 | } 130 | 131 | ics.Content = content 132 | 133 | retDB := db.Create(&ics) 134 | 135 | if retDB.Error != nil { 136 | log.Printf("Error with ICS %q: %s\n", calId, retDB.Error) 137 | return model.ICS{}, retDB.Error 138 | } 139 | 140 | fmt.Printf("ICS %s for user %s added.\n", calId, user) 141 | 142 | return ics, nil 143 | } 144 | 145 | func UpdateIcs(calId string, content string) (model.ICS, error) { 146 | 147 | db := fcdb.GetDB() 148 | 149 | ics, err := GetICS(calId) 150 | if err != nil { 151 | return model.ICS{}, err 152 | } 153 | 154 | // todo update changed fields only 155 | ics.Content = content 156 | ics.UpdDat = time.Now() 157 | 158 | json := fcdb.ParseICS(content) 159 | 160 | err = parseDatesAndFill(json, &ics) 161 | if err != nil { 162 | return model.ICS{}, err 163 | } 164 | 165 | retDB := db.Save(&ics) 166 | 167 | if retDB.Error != nil { 168 | log.Printf("Error with ICS %q: %s\n", calId, retDB.Error) 169 | return model.ICS{}, retDB.Error 170 | } 171 | 172 | fmt.Printf("ICS %s updated.\n", calId) 173 | 174 | return ics, nil 175 | } 176 | 177 | func GetICS(icsId string) (model.ICS, error) { 178 | 179 | db := fcdb.GetDB() 180 | 181 | var ics model.ICS 182 | retDB := db.First(&ics, "pkey = ?", icsId) 183 | 184 | if retDB.Error != nil { 185 | log.Printf("Error with loading ICS %q: %s\n", icsId, retDB.Error) 186 | return model.ICS{}, retDB.Error 187 | } 188 | 189 | return ics, nil 190 | } 191 | 192 | func FindIcsByCalendar(calID string) ([]*model.ICS, error) { 193 | 194 | var ics model.ICS 195 | 196 | db := fcdb.GetDB() 197 | db = db.Model(ics).Where("calendar_id = ?", calID) 198 | 199 | var rows []*model.ICS 200 | 201 | retDB := db.Find(&rows) 202 | 203 | if retDB.Error != nil { 204 | log.Printf("Error with loading ICS %s\n", retDB.Error) 205 | return rows, retDB.Error 206 | } 207 | 208 | return rows, nil 209 | } 210 | 211 | func FindIcsByTimeslot(calID string, start *time.Time, end *time.Time) ([]*model.ICS, error) { 212 | 213 | var ics model.ICS 214 | 215 | db := fcdb.GetDB() 216 | db = db.Model(ics) 217 | 218 | if len(calID) > 0 { 219 | 220 | db = db.Where("calendar_id = ?", calID) 221 | } 222 | 223 | if start != nil && !start.IsZero() { 224 | 225 | db = db.Where("start_date >= ?", start) 226 | } 227 | 228 | if end != nil && !end.IsZero() { 229 | 230 | db = db.Where("end_date <= ?", end) 231 | } 232 | 233 | var rows []*model.ICS 234 | 235 | retDB := db.Find(&rows) 236 | 237 | if retDB.Error != nil { 238 | log.Printf("Error with loading ICS %s\n", retDB.Error) 239 | return rows, retDB.Error 240 | } 241 | 242 | return rows, nil 243 | } 244 | 245 | func FindIcsInList(arrICS []string) ([]*model.ICS, error) { 246 | 247 | var ics model.ICS 248 | 249 | db := fcdb.GetDB() 250 | db = db.Model(ics).Where("pkey in (?)", arrICS) 251 | 252 | var rows []*model.ICS 253 | 254 | retDB := db.Find(&rows) 255 | 256 | if retDB.Error != nil { 257 | log.Printf("Error with loading ICS %s\n", retDB.Error) 258 | return rows, retDB.Error 259 | } 260 | 261 | return rows, nil 262 | } 263 | 264 | func DeleteIcs(icsId string) error { 265 | 266 | db := fcdb.GetDB() 267 | 268 | ics := &model.ICS{} 269 | 270 | retDB := db.Where("pkey = ?", icsId).First(&ics) 271 | 272 | if retDB.Error != nil { 273 | log.Printf("Error with Ics %q: %s\n", icsId, retDB.Error) 274 | log.Fatal(retDB.Error) 275 | return retDB.Error 276 | } 277 | 278 | if retDB.RowsAffected <= 0 { 279 | log.Printf("ICS not found: %s\n", icsId) 280 | log.Fatal("ICS not found: " + icsId + "\n") 281 | return retDB.Error 282 | } 283 | 284 | log.Printf("Deleting ICS: %s", &ics.Pkey) 285 | 286 | ret := db.Delete(&ics) 287 | 288 | return ret.Error 289 | } 290 | -------------------------------------------------------------------------------- /fenneld/handler/responder.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Wombag - 6 | ** 7 | ** the alternative, native backend for your Wallabag apps 8 | ** 9 | ** Copyright 2017-18 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordLightningBolt@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "fmt" 34 | "net/http" 35 | "github.com/beevik/etree" 36 | ) 37 | 38 | /* 39 | func Render(w http.ResponseWriter, status int, r render.Render){ 40 | 41 | w.WriteHeader(status) 42 | 43 | if !bodyAllowedForStatus(status) { 44 | 45 | r.WriteContentType(w) 46 | return 47 | } 48 | 49 | if err := r.Render(w); err != nil { 50 | panic(err) 51 | } 52 | } 53 | */ 54 | 55 | // bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. 56 | func bodyAllowedForStatus(status int) bool { 57 | switch { 58 | case status >= 100 && status <= 199: 59 | return false 60 | case status == 204: 61 | return false 62 | case status == 304: 63 | return false 64 | } 65 | return true 66 | } 67 | 68 | func NotImplementedYet(w http.ResponseWriter) { 69 | 70 | w.WriteHeader(http.StatusNotImplemented) 71 | w.Header().Set("Content-Type", "application/json") 72 | fmt.Fprintf(w, "This function is not implemented yet\n") 73 | } 74 | 75 | func SetAllowHeader(w http.ResponseWriter) { 76 | 77 | w.Header().Add("Allow", "OPTIONS, PROPFIND, HEAD, GET, REPORT, PROPPATCH, PUT, DELETE, POST, COPY, MOVE") 78 | } 79 | 80 | func SetDAVHeader(w http.ResponseWriter) { 81 | 82 | w.Header().Set("DAV", "1, 3, extended-mkcol, calendar-access, calendar-schedule, calendar-proxy, calendarserver-sharing, calendarserver-subscribed, addressbook, access-control, calendarserver-principal-property-search") 83 | } 84 | 85 | func SetStandardXMLHeader(w http.ResponseWriter) { 86 | 87 | w.Header().Set("Content-Type", "application/xml; charset=utf-8") 88 | w.Header().Set("Server", "Fennel") 89 | } 90 | 91 | func SetStandardHTMLHeader(w http.ResponseWriter) { 92 | 93 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 94 | w.Header().Set("Server", "Fennel") 95 | } 96 | 97 | func RespondWithRedirect(w http.ResponseWriter, req *http.Request, uri string) { 98 | 99 | http.Redirect(w, req, uri, http.StatusMovedPermanently) 100 | } 101 | 102 | func RespondWithMessage(w http.ResponseWriter, status int, message string) { 103 | 104 | w.WriteHeader(status) 105 | w.Header().Set("Content-Type", "application/json") 106 | fmt.Fprintf(w, message+"\n") 107 | } 108 | 109 | func RespondWithICS(w http.ResponseWriter, status int, ics string) { 110 | 111 | w.WriteHeader(status) 112 | w.Header().Set("Content-Type", "text/calendar") 113 | fmt.Fprintf(w, ics+"\n") 114 | } 115 | 116 | func RespondWithVCARD(w http.ResponseWriter, status int, vcard string) { 117 | 118 | w.WriteHeader(status) 119 | w.Header().Set("Content-Type", "text/vcard; charset=utf-8") 120 | fmt.Fprintf(w, vcard+"\n") 121 | } 122 | 123 | func RespondWithUnauthenticated(w http.ResponseWriter) { 124 | 125 | w.Header().Set("WWW-Authenticate", "Basic realm=\"Fennel\"") 126 | w.WriteHeader(http.StatusUnauthorized) 127 | fmt.Fprintf(w, "Not authorized\n") 128 | } 129 | 130 | func RespondWithStandardOptions(w http.ResponseWriter, status int, message string) { 131 | 132 | //log.debug("pushOptionsResponse called"); 133 | 134 | SetStandardHTMLHeader(w) 135 | SetDAVHeader(w) 136 | SetAllowHeader(w) 137 | 138 | RespondWithMessage(w, http.StatusOK, "") 139 | } 140 | 141 | func SendMultiStatus(w http.ResponseWriter, httpStatus int, dRet *etree.Document, propstat *etree.Element) { 142 | 143 | SetStandardHTMLHeader(w) 144 | SetDAVHeader(w) 145 | SetAllowHeader(w) 146 | 147 | AddStatusToPropstat(httpStatus, propstat) 148 | 149 | w.WriteHeader(http.StatusMultiStatus) 150 | dRet.WriteTo(w) 151 | } 152 | 153 | func SendETreeDocument(w http.ResponseWriter, status int, dRet *etree.Document) { 154 | 155 | SetStandardXMLHeader(w) 156 | SetDAVHeader(w) 157 | SetAllowHeader(w) 158 | 159 | w.WriteHeader(status) 160 | dRet.WriteTo(w) 161 | } 162 | 163 | func GetMultistatusDoc(sURL string) (*etree.Document, *etree.Element) { 164 | 165 | doc, ms := GetMultistatusDocWOResponseTag() 166 | 167 | response := ms.CreateElement("response") 168 | response.Space = "d" 169 | 170 | href := response.CreateElement("href") 171 | href.SetText(sURL) 172 | href.Space = "d" 173 | 174 | propstat := response.CreateElement("propstat") 175 | propstat.Space = "d" 176 | 177 | return doc, propstat 178 | } 179 | 180 | func GetMultistatusDocWOResponseTag() (*etree.Document, *etree.Element) { 181 | 182 | doc := etree.NewDocument() 183 | doc.Indent(2) 184 | doc.CreateProcInst("xml", `version="1.0" encoding="utf-8"`) 185 | 186 | ms := doc.CreateElement("multistatus") 187 | ms.Space = "d" 188 | 189 | /* 190 | 195 | 196 | /card/user/ 197 | 198 | 199 | */ 200 | ms.CreateAttr("xmlns:d", "DAV:") 201 | ms.CreateAttr("xmlns:d", "DAV:") 202 | ms.CreateAttr("xmlns:s", "http://github.com/swordlordcodingcrew/fennel/ns") 203 | ms.CreateAttr("xmlns:cal", "urn:ietf:params:xml:ns:caldav") 204 | ms.CreateAttr("xmlns:cs", "http://calendarserver.org/ns/") 205 | ms.CreateAttr("xmlns:card", "urn:ietf:params:xml:ns:carddav") 206 | 207 | return doc, ms 208 | } 209 | 210 | func AddResponseWStatusToMultistat(ms *etree.Element, uri string, httpStatus int) *etree.Element { 211 | 212 | propstat := AddResponseToMultistat(ms, uri) 213 | 214 | AddStatusToPropstat(httpStatus, propstat) 215 | 216 | return propstat 217 | } 218 | 219 | func AddResponseToMultistat(ms *etree.Element, uri string) *etree.Element { 220 | 221 | response := ms.CreateElement("response") 222 | response.Space = "d" 223 | 224 | href := response.CreateElement("href") 225 | href.SetText(uri) 226 | href.Space = "d" 227 | 228 | propstat := response.CreateElement("propstat") 229 | propstat.Space = "d" 230 | 231 | return propstat 232 | } 233 | 234 | func AddStatusToPropstat(httpStatus int, propstat *etree.Element) { 235 | 236 | status := propstat.CreateElement("status") 237 | status.Space = "d" 238 | 239 | switch httpStatus { 240 | 241 | case http.StatusNotFound: 242 | status.SetText("HTTP/1.1 404 Not Found") 243 | case http.StatusOK: 244 | status.SetText("HTTP/1.1 200 OK") 245 | default: 246 | status.SetText("HTTP/1.1 500 Internal Server Error") 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /fenneld/handler/calendar/main.go: -------------------------------------------------------------------------------- 1 | package calendar 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "encoding/xml" 35 | "fmt" 36 | "github.com/gorilla/mux" 37 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 38 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 39 | "io/ioutil" 40 | "log" 41 | "net/http" 42 | ) 43 | 44 | type Xmlmakecalendar struct { 45 | XMLName xml.Name 46 | Set Xmlset `xml:"set"` 47 | } 48 | 49 | type Xmlset struct { 50 | XMLName xml.Name 51 | Prop Xmlprop `xml:"prop"` 52 | } 53 | 54 | type Xmlprop struct { 55 | XMLName xml.Name 56 | Displayname string `xml:"displayname"` 57 | CalendarOrder int `xml:"calendar-order"` 58 | CalendarTimezone string `xml:"calendar-timezone"` 59 | CalendarColour string `xml:"calendar-color"` 60 | FreeBusySet string `xml:"calendar-free-busy-set"` 61 | } 62 | 63 | func Proppatch(w http.ResponseWriter, req *http.Request) { 64 | 65 | handler.RespondWithMessage(w, http.StatusOK, "Not implemented yet") 66 | 67 | } 68 | 69 | func Options(w http.ResponseWriter, req *http.Request) { 70 | 71 | handler.RespondWithStandardOptions(w, http.StatusOK, "") 72 | } 73 | 74 | /* 75 | Version string `xml:"version,attr"` 76 | 77 | "\n\r"; 78 | payload += "\n\r"; 79 | payload += "\n\r"; 80 | payload += "\n\r"; 81 | payload += " \n\r"; 82 | payload += "\n\r"; 83 | payload += "Three\n\r"; 84 | payload += "4\n\r"; 85 | payload += "\n\r"; 86 | payload += " \n\r"; 87 | payload += "\n\r"; 88 | payload += "BEGIN:VCALENDAR \n\r"; 89 | payload += "VERSION:2.0 \n\r"; 90 | payload += "CALSCALE:GREGORIAN \n\r"; 91 | payload += "BEGIN:VTIMEZONE \n\r"; 92 | payload += "TZID:Europe/Zurich \n\r"; 93 | payload += "BEGIN:DAYLIGHT \n\r"; 94 | payload += "TZOFFSETFROM:+0100 \n\r"; 95 | payload += "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU \n\r"; 96 | payload += "DTSTART:19810329T020000 \n\r"; 97 | payload += "TZNAME:GMT+2 \n\r"; 98 | payload += "TZOFFSETTO:+0200 \n\r"; 99 | payload += "END:DAYLIGHT \n\r"; 100 | payload += "BEGIN:STANDARD \n\r"; 101 | payload += "TZOFFSETFROM:+0200 \n\r"; 102 | payload += "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU \n\r"; 103 | payload += "DTSTART:19961027T030000 \n\r"; 104 | payload += "TZNAME:GMT+1 \n\r"; 105 | payload += "TZOFFSETTO:+0100 \n\r"; 106 | payload += "END:STANDARD \n\r"; 107 | payload += "END:VTIMEZONE \n\r"; 108 | payload += "END:VCALENDAR \n\r"; 109 | payload += "\n\r"; 110 | payload += "#FFCC00\n\r"; 112 | payload += "\n\r"; 113 | payload += "\n\r"; 114 | payload += "\n\r"; 115 | */ 116 | 117 | func MakeCalendar(w http.ResponseWriter, req *http.Request) { 118 | 119 | vars := mux.Vars(req) 120 | sCal := vars["calendar"] 121 | 122 | sUser, ok := req.Context().Value("auth_user").(string) 123 | if !ok { 124 | // something went wrong with the authenticated user (missing?) 125 | handler.RespondWithMessage(w, http.StatusBadRequest, "no user") 126 | return 127 | } 128 | 129 | decoder := xml.NewDecoder(req.Body) 130 | sentCal := Xmlmakecalendar{} 131 | err := decoder.Decode(&sentCal) 132 | if err != nil { 133 | 134 | handler.RespondWithMessage(w, http.StatusInternalServerError, err.Error()) 135 | return 136 | } 137 | 138 | // for debugging... 139 | // fmt.Println(err) 140 | // fmt.Println(sentCal) 141 | // fmt.Println(sentCal.Set.Prop.Displayname) 142 | 143 | prop := sentCal.Set.Prop 144 | 145 | // TODO: set freebusyset only to yes if tag exists ->"YES" 146 | fmt.Println(prop.FreeBusySet) 147 | 148 | // TODO: set supported cal component to "VEVENT" if tag exists 149 | 150 | cal, err := tablemodule.AddCal(sUser, sCal, prop.Displayname, prop.CalendarColour, "YES", prop.CalendarOrder, "VEVENT", 0, prop.CalendarTimezone) 151 | if err != nil { 152 | 153 | handler.RespondWithMessage(w, http.StatusPreconditionFailed, err.Error()) 154 | return 155 | } 156 | 157 | handler.RespondWithMessage(w, http.StatusCreated, "Make calendar: "+cal.Pkey+" for user: "+sUser) 158 | } 159 | 160 | func Put(w http.ResponseWriter, req *http.Request) { 161 | 162 | vars := mux.Vars(req) 163 | sCal := vars["calendar"] 164 | sEvent := vars["event"] 165 | 166 | sUser, ok := req.Context().Value("auth_user").(string) 167 | if !ok { 168 | // TODO fail when there is no user, since this can't really happen! 169 | sUser = "" 170 | } 171 | 172 | // todo: make sure calendar exists, make otherwise? 173 | 174 | bodyBuffer, _ := ioutil.ReadAll(req.Body) 175 | 176 | // make sure, that ICS does not exist or update it otherwise... 177 | _, err := tablemodule.GetICS(sEvent) 178 | if err != nil { 179 | ics, err := tablemodule.AddIcs(sEvent, sUser, sCal, string(bodyBuffer)) 180 | if err != nil { 181 | 182 | handler.RespondWithMessage(w, http.StatusPreconditionFailed, err.Error()) 183 | return 184 | } 185 | handler.RespondWithMessage(w, http.StatusCreated, "ICS added: "+ics.Pkey) 186 | } else { 187 | 188 | ics, err := tablemodule.UpdateIcs(sEvent, string(bodyBuffer)) 189 | if err != nil { 190 | 191 | } 192 | handler.RespondWithMessage(w, http.StatusCreated, "ICS updated: "+ics.Pkey) 193 | } 194 | // todo: cal increment sync token 195 | // todo: return e-tag 196 | } 197 | 198 | func Get(w http.ResponseWriter, req *http.Request) { 199 | 200 | vars := mux.Vars(req) 201 | sEvent := vars["event"] 202 | 203 | ics, err := tablemodule.GetICS(sEvent) 204 | 205 | if err != nil { 206 | 207 | fmt.Println("err: could not find ics " + sEvent) 208 | // TODO send error 209 | handler.RespondWithMessage(w, http.StatusInternalServerError, err.Error()) 210 | return 211 | } 212 | 213 | handler.RespondWithICS(w, http.StatusOK, ics.Content) 214 | } 215 | 216 | func Delete(w http.ResponseWriter, req *http.Request) { 217 | 218 | vars := mux.Vars(req) 219 | sEvent := vars["event"] 220 | 221 | err := tablemodule.DeleteIcs(sEvent) 222 | 223 | if err != nil { 224 | log.Printf("Error with deleting Ics %q: %s\n", sEvent) 225 | 226 | handler.RespondWithMessage(w, http.StatusInternalServerError, err.Error()) 227 | 228 | return 229 | } 230 | 231 | handler.RespondWithMessage(w, http.StatusOK, "Deleted") 232 | } 233 | 234 | func Move(w http.ResponseWriter, req *http.Request) { 235 | 236 | handler.RespondWithMessage(w, http.StatusOK, "Not implemented yet") 237 | 238 | } 239 | -------------------------------------------------------------------------------- /fenneld/handler/addressbook/report.go: -------------------------------------------------------------------------------- 1 | package addressbook 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "fmt" 35 | "github.com/beevik/etree" 36 | "github.com/gorilla/mux" 37 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 38 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 39 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 40 | "net/http" 41 | "strconv" 42 | "strings" 43 | ) 44 | 45 | func Report(w http.ResponseWriter, req *http.Request) { 46 | 47 | // REPORT https://dav.fruux.com/addressbooks/user/addressbookid/ 48 | 49 | /* 50 | 51 | 52 | http://sabre.io/ns/sync/18 53 | 1 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | http://sabre.io/ns/sync/18 63 | 64 | 65 | 66 | resp. 67 | 68 | 69 | 72 | 73 | 74 | /addressbooks/a3298271331/8ec6424c-ede3-4a55-8613-e760df985cac/6251687E-BF17-4B5B-AA4C-95A7D22DF020.vcf 75 | 76 | 77 | "39687715-66286691" 78 | 79 | HTTP/1.1 200 OK 80 | 81 | 82 | 83 | 84 | /addressbooks/a3298271331/8ec6424c-ede3-4a55-8613-e760df985cac/dc6789c8-4299-4360-9f2c-5781962d92aa.vcf 85 | 86 | 87 | "8767949-66286692" 88 | 89 | HTTP/1.1 200 OK 90 | 91 | 92 | http://sabre.io/ns/sync/20 93 | 94 | 95 | */ 96 | 97 | vars := mux.Vars(req) 98 | sAB := vars["addressbook"] 99 | sUser := vars["user"] 100 | 101 | // todo do we need the logged in user? 102 | /* 103 | sUser, ok := req.Context().Value("auth_user").(string) 104 | if !ok { 105 | // TODO fail when there is no user, since this can't really happen! 106 | sUser = "" 107 | } 108 | */ 109 | 110 | doc := etree.NewDocument() 111 | size, err := doc.ReadFrom(req.Body) 112 | if err != nil || size == 0 { 113 | 114 | fmt.Printf("Error reading XML Body. Error: %s Size: %v", err, size) 115 | 116 | handler.RespondWithMessage(w, http.StatusInternalServerError, "") 117 | return 118 | } 119 | 120 | root := doc.Root() 121 | name := root.Tag 122 | 123 | switch name { 124 | 125 | case "sync-collection": 126 | handleReportSyncCollection(w, req.RequestURI, root, sUser, sAB) 127 | 128 | case "addressbook-multiget": 129 | handleReportAddressbookMultiget(w, req.RequestURI, root, sUser, sAB) 130 | 131 | default: 132 | if name != "text" { 133 | fmt.Println("CARD-Report: root not handled: " + name) 134 | handler.RespondWithMessage(w, http.StatusInternalServerError, "") 135 | } 136 | } 137 | } 138 | 139 | func handleReportSyncCollection(w http.ResponseWriter, uri string, root *etree.Element, sUser string, sAB string) { 140 | 141 | dRet, ms := handler.GetMultistatusDocWOResponseTag() 142 | 143 | var syncToken = "synctoken" 144 | var syncLevel = 1 145 | 146 | elements := root.FindElements("./*") 147 | for _, e := range elements { 148 | 149 | //fmt.Println(e.Tag) 150 | name := e.Tag 151 | switch name { 152 | 153 | case "sync-token": 154 | syncToken = e.Text() 155 | 156 | case "sync-level": 157 | sl, err := strconv.Atoi(e.Text()) 158 | if err != nil { 159 | syncLevel = sl 160 | } 161 | 162 | default: 163 | if name != "text" { 164 | fmt.Println("Adressbook Report SyncCollection: not handled: " + name) 165 | } 166 | } 167 | } 168 | 169 | fmt.Printf("SyncToken: %v\n\r", syncToken) 170 | fmt.Printf("SyncLevel: %v\n\r", syncLevel) 171 | 172 | fmt.Printf("MultiStatus: %v\n\r", ms) 173 | 174 | // TODO apply filter, return etoken 175 | 176 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 177 | } 178 | 179 | func handleReportAddressbookMultiget(w http.ResponseWriter, uri string, root *etree.Element, sUser string, sAB string) { 180 | 181 | dRet, ms := handler.GetMultistatusDocWOResponseTag() 182 | 183 | var getETag = false 184 | var getAddressData = false 185 | 186 | // TODO check filter: 187 | // payload += "\n\r"; 188 | // payload += " \n\r"; 189 | // payload += " \n\r"; 190 | // payload += "\n\r"; 191 | propsQuery := root.FindElements("./prop/*") 192 | for _, e := range propsQuery { 193 | 194 | //fmt.Println(e.Tag) 195 | name := e.Tag 196 | switch name { 197 | 198 | case "getetag": 199 | getETag = true 200 | 201 | case "address-data": 202 | getAddressData = true 203 | } 204 | } 205 | 206 | // get all hrefs requested 207 | reqDocs := root.FindElements("./href/") 208 | 209 | arrVCards := make([]string, len(reqDocs)) 210 | 211 | for i, reqDoc := range reqDocs { 212 | 213 | // get the last element, which should contain the filename 214 | arrPath := strings.Split(reqDoc.Text(), "/") 215 | pathCount := len(arrPath) 216 | 217 | if pathCount < 2 { 218 | // TODO continue is suboptimal, we should add a bool to the array and send a 404 for said file. 219 | continue 220 | } 221 | 222 | filename := arrPath[pathCount-1] 223 | 224 | arrFile := strings.Split(filename, ".") 225 | if len(arrFile) < 2 { 226 | // TODO continue is suboptimal, we should add a bool to the array and send a 404 for said file. 227 | continue 228 | } 229 | 230 | arrVCards[i] = arrFile[0] 231 | } 232 | 233 | err, rows := tablemodule.FindVCardsFromAddressbook(sAB, arrVCards) 234 | if err != nil { 235 | 236 | handler.RespondWithMessage(w, http.StatusInternalServerError, "") 237 | } 238 | 239 | for _, row := range rows { 240 | 241 | // TODO probably wrong -> send some data, no detail if not required 242 | if getAddressData { 243 | handleReportVCardReply(ms, row, uri, getETag, getAddressData) 244 | } 245 | } 246 | 247 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 248 | } 249 | 250 | func handleReportVCardReply(ms *etree.Element, vcard *model.VCARD, uri string, getETag bool, getAddressData bool) { 251 | 252 | ps := handler.AddResponseToMultistat(ms, uri+vcard.Pkey+".vcf") 253 | 254 | if getETag { 255 | 256 | prop := ps.CreateElement("getetag") 257 | prop.Space = "d" 258 | // TODO fill correct text 259 | prop.SetText("E1") 260 | } 261 | 262 | if getAddressData { 263 | 264 | prop := ps.CreateElement("address-data") 265 | prop.Space = "card" 266 | // TODO correct format 267 | 268 | // content = content.replace(/&/g,'&'); 269 | // content = content.replace(/\r\n|\r|\n/g,' \r\n'); 270 | 271 | prop.SetText(vcard.Content) 272 | } 273 | 274 | handler.AddStatusToPropstat(http.StatusOK, ps) 275 | } 276 | -------------------------------------------------------------------------------- /fenneld/handler/principal/propfind.go: -------------------------------------------------------------------------------- 1 | package principal 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | import ( 33 | "fmt" 34 | "net/http" 35 | 36 | "github.com/beevik/etree" 37 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 38 | ) 39 | 40 | func Propfind(w http.ResponseWriter, req *http.Request) { 41 | 42 | sUser, ok := req.Context().Value("auth_user").(string) 43 | if !ok { 44 | // TODO fail when there is no user, since this can't really happen! 45 | sUser = "" 46 | } 47 | 48 | dRet, propstat := handler.GetMultistatusDoc(req.RequestURI) 49 | 50 | doc := etree.NewDocument() 51 | size, err := doc.ReadFrom(req.Body) 52 | if err != nil || size == 0 { 53 | 54 | fmt.Printf("Error reading XML Body. Error: %s Size: %v", err, size) 55 | 56 | handler.SendMultiStatus(w, http.StatusNotFound, dRet, propstat) 57 | return 58 | } 59 | 60 | // find query parameters and store in props 61 | // could probably be faster with compiled path... 62 | // propfindPath := etree.MustCompilePath("/propfind/prop/*") 63 | propsQuery := doc.FindElements("/propfind/prop/*") 64 | 65 | // create new element to store response in 66 | prop := propstat.CreateElement("prop") 67 | prop.Space = "d" 68 | 69 | // let helper function fill prop element with requested props 70 | fillPropfindResponse(prop, propsQuery, sUser) 71 | 72 | // add status based on query 73 | status := propstat.CreateElement("status") 74 | status.Space = "d" 75 | 76 | if len(prop.ChildElements()) > 0 { 77 | 78 | status.SetText("HTTP/1.1 200 OK") 79 | 80 | } else { 81 | 82 | status.SetText("HTTP/1.1 404 Not Found") 83 | } 84 | 85 | // send response to client 86 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 87 | } 88 | 89 | func fillPropfindResponse(node *etree.Element, props []*etree.Element, sUser string) { 90 | 91 | // TODO 92 | token := "" 93 | 94 | for _, e := range props { 95 | 96 | //fmt.Println(e.Tag) 97 | name := e.Tag 98 | switch name { 99 | 100 | case "checksum-versions": 101 | //"; 102 | 103 | case "sync-token": 104 | prop := node.CreateElement("sync-token") 105 | prop.Space = "d" 106 | prop.SetText("https://swordlord.com/ns/sync/" + token) 107 | 108 | case "supported-report-set": 109 | fillSupportedReportSet(node) 110 | 111 | case "principal-URL": 112 | ///p/" + comm.getUser().getUserName() + "/\r\n"; 113 | prop := node.CreateElement("principal-URL") 114 | prop.Space = "d" 115 | 116 | href := prop.CreateElement("href") 117 | href.Space = "d" 118 | 119 | href.SetText("/p/" + sUser + "/") 120 | 121 | case "displayname": 122 | //" + comm.getUser().getUserName() + ""; 123 | prop := node.CreateElement("displayname") 124 | prop.Space = "d" 125 | prop.SetText(sUser) 126 | 127 | case "principal-collection-set": 128 | ///p/"; 129 | prop := node.CreateElement("principal-collection-set") 130 | prop.Space = "d" 131 | 132 | href := prop.CreateElement("href") 133 | href.Space = "d" 134 | 135 | href.SetText("/p/") 136 | 137 | case "current-user-principal": 138 | ///p/" + comm.getUser().getUserName() + "/"; 139 | prop := node.CreateElement("current-user-principal") 140 | prop.Space = "d" 141 | 142 | href := prop.CreateElement("href") 143 | href.Space = "d" 144 | 145 | href.SetText("/p/" + sUser + "/") 146 | 147 | case "calendar-home-set": 148 | ///cal/" + comm.getUser().getUserName() + ""; 149 | prop := node.CreateElement("calendar-home-set") 150 | prop.Space = "cal" 151 | 152 | href := prop.CreateElement("href") 153 | href.Space = "d" 154 | 155 | href.SetText("/cal/" + sUser + "/") 156 | 157 | case "schedule-outbox-URL": 158 | ///cal/" + comm.getUser().getUserName() + "/outbox"; 159 | prop := node.CreateElement("schedule-outbox-URL") 160 | prop.Space = "cal" 161 | 162 | href := prop.CreateElement("href") 163 | href.Space = "d" 164 | href.SetText("/cal/" + sUser + "/outbox/") 165 | 166 | case "calendar-user-address-set": 167 | prop := node.CreateElement("calendar-user-address-set") 168 | prop.Space = "cal" 169 | 170 | href := prop.CreateElement("href") 171 | href.Space = "d" 172 | href.SetText("mailto:lord test at swordlord.com") 173 | 174 | href2 := prop.CreateElement("href") 175 | href2.Space = "d" 176 | href2.SetText("/p/" + sUser + "/") 177 | 178 | case "notification-URL": 179 | ///cal/" + comm.getUser().getUserName() + "/notifications/"; 180 | prop := node.CreateElement("notification-URL") 181 | prop.Space = "cs" 182 | 183 | href := prop.CreateElement("href") 184 | href.Space = "d" 185 | 186 | href.SetText("/cal/" + sUser + "/notifications/") 187 | 188 | case "getcontenttype": 189 | //"; 190 | 191 | case "addressbook-home-set": 192 | ///card/" + comm.getUser().getUserName() + "/"; 193 | prop := node.CreateElement("addressbook-home-set") 194 | prop.Space = "card" 195 | 196 | href := prop.CreateElement("href") 197 | href.Space = "d" 198 | 199 | href.SetText("/card/" + sUser + "/") 200 | 201 | case "directory-gateway": 202 | //"; 203 | 204 | case "email-address-set": 205 | //lord test at swordlord.com"; 206 | prop := node.CreateElement("email-address-set") 207 | prop.Space = "cs" 208 | 209 | ea := prop.CreateElement("email-address") 210 | ea.Space = "cs" 211 | 212 | // todo load user email from db 213 | ea.SetText("lord test at swordlord.com") 214 | 215 | case "resource-id": 216 | 217 | default: 218 | if name != "text" { 219 | fmt.Println("PRINCIPAL-PF: not handled: " + name) 220 | } 221 | } 222 | } 223 | } 224 | 225 | func fillSupportedReportSet(node *etree.Element) { 226 | 227 | /* 228 | \r\n"; 229 | \r\n"; 230 | \r\n"; 231 | \r\n"; 232 | \r\n"; 233 | \r\n"; 234 | \r\n"; 235 | \r\n"; 236 | \r\n"; 237 | \r\n"; 238 | \r\n"; 239 | \r\n"; 240 | \r\n"; 241 | \r\n"; 242 | \r\n"; 243 | \r\n"; 244 | \r\n"; 245 | */ 246 | srs := node.CreateElement("supported-report-set") 247 | srs.Space = "d" 248 | 249 | // --- 250 | sr1 := srs.CreateElement("supported-report") 251 | sr1.Space = "d" 252 | 253 | r1 := sr1.CreateElement("report") 254 | r1.Space = "d" 255 | 256 | ep := r1.CreateElement("expand-property") 257 | ep.Space = "d" 258 | 259 | // --- 260 | sr2 := srs.CreateElement("supported-report") 261 | sr2.Space = "d" 262 | 263 | r2 := sr2.CreateElement("report") 264 | r2.Space = "d" 265 | 266 | pps := r2.CreateElement("expand-property") 267 | pps.Space = "d" 268 | 269 | // --- 270 | sr3 := srs.CreateElement("supported-report") 271 | sr3.Space = "d" 272 | 273 | r3 := sr3.CreateElement("report") 274 | r3.Space = "d" 275 | 276 | psps := r3.CreateElement("principal-search-property-set") 277 | psps.Space = "d" 278 | 279 | } 280 | -------------------------------------------------------------------------------- /fenneld/handler/calendar/report.go: -------------------------------------------------------------------------------- 1 | package calendar 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "fmt" 35 | "github.com/beevik/etree" 36 | "github.com/gorilla/mux" 37 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 38 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 39 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 40 | "net/http" 41 | "strings" 42 | "time" 43 | ) 44 | 45 | func Report(w http.ResponseWriter, req *http.Request) { 46 | 47 | vars := mux.Vars(req) 48 | sCalId := vars["calendar"] 49 | 50 | doc := etree.NewDocument() 51 | size, err := doc.ReadFrom(req.Body) 52 | if err != nil || size == 0 { 53 | 54 | fmt.Printf("Error reading XML Body. Error: %s Size: %v", err, size) 55 | 56 | handler.RespondWithMessage(w, http.StatusInternalServerError, "") 57 | return 58 | } 59 | 60 | root := doc.Root() 61 | name := root.Tag 62 | 63 | switch name { 64 | 65 | case "sync-collection": 66 | handleReportSyncCollection(w, req.RequestURI, root, sCalId) 67 | 68 | case "calendar-multiget": 69 | // TODO add handleReportCalendarMultiget 70 | handleReportCalendarMultiget(w, req.RequestURI, root, sCalId) 71 | 72 | case "calendar-query": 73 | handleReportCalendarQuery(w, req.RequestURI, root, sCalId) 74 | 75 | default: 76 | if name != "text" { 77 | fmt.Println("CAL-Report: not handled: " + name) 78 | } 79 | } 80 | } 81 | 82 | func handleReportCalendarQuery(w http.ResponseWriter, uri string, nodeQuery *etree.Element, sCalId string) { 83 | 84 | dRet, ms := handler.GetMultistatusDocWOResponseTag() 85 | 86 | cal, err := tablemodule.GetCal(sCalId) 87 | if err != nil { 88 | 89 | fmt.Println(err) 90 | 91 | propstat := handler.AddResponseToMultistat(ms, uri) 92 | 93 | handler.SendMultiStatus(w, http.StatusNotFound, dRet, propstat) 94 | return 95 | } 96 | 97 | props := nodeQuery.FindElements("./prop/*") 98 | 99 | // TODO: check filter: 100 | // \n\r"; 101 | // \n\r"; 102 | // \n\r"; 103 | // \n\r"; 104 | //\n\r 105 | // 106 | // BEGIN:VEVENT. 107 | // DTSTART;TZID=Europe/Zurich:20161014T120000Z. 108 | // DTEND;TZID=Europe/Zurich:20161014T130000Z 109 | // parse when storing 110 | //timerange := nodeQuery.FindElement("./filter/comp-filter[name='VCALENDAR']/comp-filter[name='VEVENT']/time-range") 111 | var dtmStart *time.Time 112 | var dtmEnd *time.Time 113 | 114 | filter := nodeQuery.FindElement("./filter/comp-filter[@name='VCALENDAR']/comp-filter[@name='VEVENT']/time-range") 115 | 116 | // get everything if filter is not supplied 117 | if filter != nil { 118 | start := filter.SelectAttr("start") 119 | if start != nil { 120 | 121 | // TODO warning, can have either whole day or time 122 | s, err := time.Parse("20060102T150405Z", start.Value) 123 | if err != nil { 124 | 125 | fmt.Println("Could not parse time format: " + start.Value) 126 | } else { 127 | dtmStart = &s 128 | } 129 | } 130 | 131 | end := filter.SelectAttr("end") 132 | if end != nil { 133 | 134 | // TODO warning, can have either whole day or time 135 | e, err := time.Parse("20060102T150405Z", end.Value) 136 | if err != nil { 137 | 138 | fmt.Println("Could not parse time format: " + end.Value) 139 | } else { 140 | dtmEnd = &e 141 | } 142 | } 143 | } 144 | 145 | rows, err := tablemodule.FindIcsByTimeslot(sCalId, dtmStart, dtmEnd) 146 | for _, row := range rows { 147 | 148 | appendIcsAsResponseToMultistat(ms, row, props, uri, cal.SupportedCalComponent) 149 | } 150 | 151 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 152 | } 153 | 154 | func appendIcsAsResponseToMultistat(ms *etree.Element, ics *model.ICS, props []*etree.Element, uri string, supportedCalComponent string) { 155 | 156 | propstat := handler.AddResponseToMultistat(ms, uri+ics.Pkey+".ics") 157 | 158 | // values to return from: /B:calendar-query/A:prop 159 | for _, prop := range props { 160 | 161 | propName := prop.Tag 162 | switch propName { 163 | 164 | // TODO missing: 165 | // 166 | // 167 | // 168 | // 169 | 170 | case "getetag": 171 | getETag := propstat.CreateElement("getetag") 172 | getETag.Space = "d" 173 | getETag.SetText("etag") 174 | //response += "\"" + Number(date) + "\""; 175 | 176 | case "getcontenttype": 177 | getCT := propstat.CreateElement("getcontenttype") 178 | getCT.Space = "d" 179 | getCT.SetText("text/calendar; charset=utf-8; component=" + supportedCalComponent) 180 | //response += "text/calendar; charset=utf-8; component=" + cal.supported_cal_component + ""; 181 | 182 | case "calendar-data": 183 | getCD := propstat.CreateElement("calendar-data") 184 | getCD.Space = "cal" 185 | getCD.SetText(ics.Content) 186 | //response += "" + ics.content + ""; // has to be cal: since a few lines below the namespace is cal: not c: 187 | 188 | default: 189 | if propName != "text" { 190 | fmt.Println("CAL-Query: not handled: " + propName) 191 | } 192 | } 193 | 194 | } 195 | 196 | handler.AddStatusToPropstat(http.StatusOK, propstat) 197 | } 198 | 199 | func handleReportSyncCollection(w http.ResponseWriter, uri string, nodeSyncCollection *etree.Element, sCalId string) { 200 | 201 | dRet, propstat := handler.GetMultistatusDoc(uri) 202 | 203 | cal, err := tablemodule.GetCal(sCalId) 204 | if err != nil { 205 | 206 | fmt.Println(err) 207 | 208 | handler.SendMultiStatus(w, http.StatusNotFound, dRet, propstat) 209 | return 210 | } 211 | 212 | fmt.Println(cal.Pkey) 213 | 214 | rows, err := tablemodule.FindIcsByCalendar(sCalId) 215 | if err != nil { 216 | 217 | fmt.Println(err) 218 | 219 | handler.SendMultiStatus(w, http.StatusNotFound, dRet, propstat) 220 | return 221 | } 222 | 223 | for _, ics := range rows { 224 | 225 | for _, el := range nodeSyncCollection.ChildElements() { 226 | 227 | //fmt.Println(e.Tag) 228 | name := el.Tag 229 | switch name { 230 | 231 | case "sync-token": 232 | // TODO 233 | 234 | case "sync-level": 235 | // TODO 236 | 237 | case "prop": 238 | //response += handleReportCalendarProp(comm, child, cal, ics); 239 | // TODO 240 | fmt.Println("found: " + ics.Content) 241 | 242 | default: 243 | if name != "text" { 244 | fmt.Println("CAL-RSC: not handled: " + name) 245 | } 246 | } 247 | } 248 | } 249 | 250 | // TODO there is a solution which is much more elegant, find it :) 251 | ms := dRet.FindElement("/multistatus") 252 | 253 | st := ms.CreateElement("sync-token") 254 | st.Space = "d" 255 | st.SetText("https://swordlord.org/ns/sync/" + fmt.Sprint(cal.Synctoken)) 256 | 257 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 258 | } 259 | 260 | /* 261 | function handleReportCalendarProp(comm, node, cal, ics) 262 | { 263 | var response = ""; 264 | 265 | var reqUrl = comm.getURL(); 266 | reqUrl += reqUrl.match("\/$") ? "" : "/"; 267 | 268 | response += ""; 269 | response += "" + reqUrl + ics.pkey + ".ics"; 270 | response += ""; 271 | 272 | var childs = node.childNodes(); 273 | 274 | var date = Date.parse(ics.updatedAt); 275 | 276 | var len = childs.length; 277 | for (var i=0; i < len; ++i) 278 | { 279 | var child = childs[i]; 280 | var name = child.name(); 281 | switch(name) 282 | { 283 | case 'getetag': 284 | response += "\"" + Number(date) + "\""; 285 | break; 286 | 287 | case 'getcontenttype': 288 | response += "text/calendar; charset=utf-8; component=" + cal.supported_cal_component + ""; 289 | break; 290 | 291 | default: 292 | if(name != 'text') log.warn("P-R: not handled: " + name); 293 | break; 294 | } 295 | } 296 | 297 | response += ""; 298 | response += "HTTP/1.1 200 OK"; 299 | response += ""; 300 | response += ""; 301 | 302 | return response; 303 | }*/ 304 | 305 | func handleReportCalendarMultiget(w http.ResponseWriter, uri string, mg *etree.Element, sCalId string) { 306 | 307 | dRet, ms := handler.GetMultistatusDocWOResponseTag() 308 | 309 | cal, err := tablemodule.GetCal(sCalId) 310 | if err != nil { 311 | 312 | fmt.Println(err) 313 | 314 | propstat := handler.AddResponseToMultistat(ms, uri) 315 | 316 | handler.SendMultiStatus(w, http.StatusNotFound, dRet, propstat) 317 | return 318 | } 319 | 320 | // get all href into an array for lookup/query 321 | hrefs := mg.FindElements("./href") 322 | 323 | arrHref := make([]string, len(hrefs)) 324 | 325 | for i, href := range hrefs { 326 | 327 | arrHref[i] = parseHrefToIcsId(href.Text()) 328 | } 329 | 330 | // get all root/prop entities for query and return 331 | props := mg.FindElements("./prop/*") 332 | 333 | // send these together with array to format response 334 | rows, err := tablemodule.FindIcsInList(arrHref) 335 | for _, row := range rows { 336 | 337 | appendIcsAsResponseToMultistat(ms, row, props, uri, cal.SupportedCalComponent) 338 | } 339 | 340 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 341 | } 342 | 343 | func parseHrefToIcsId(href string) string { 344 | 345 | arrPath := strings.Split(href, "/") 346 | pathCount := len(arrPath) 347 | 348 | if pathCount < 2 { 349 | // TODO continue is suboptimal, we should add a bool to the array and send a 404 for said file. 350 | return "" 351 | } 352 | 353 | filename := arrPath[pathCount-1] 354 | 355 | arrFile := strings.Split(filename, ".") 356 | if len(arrFile) < 2 { 357 | // TODO continue is suboptimal, we should add a bool to the array and send a 404 for said file. 358 | return "" 359 | } 360 | 361 | return arrFile[0] 362 | } 363 | -------------------------------------------------------------------------------- /fenneld/handler/addressbook/propfind.go: -------------------------------------------------------------------------------- 1 | package addressbook 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "fmt" 35 | "net/http" 36 | "strconv" 37 | 38 | "github.com/beevik/etree" 39 | "github.com/gorilla/mux" 40 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 41 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 42 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 43 | ) 44 | 45 | // TODO check if on root, if yes, answer differently 46 | func PropfindRoot(w http.ResponseWriter, req *http.Request) { 47 | 48 | handler.RespondWithMessage(w, http.StatusNotImplemented, "Propfind on Root not implemented yet") 49 | } 50 | 51 | func PropfindUser(w http.ResponseWriter, req *http.Request) { 52 | 53 | vars := mux.Vars(req) 54 | sUser := vars["user"] 55 | 56 | // todo do we need the logged in user? 57 | /* 58 | sUser, ok := req.Context().Value("auth_user").(string) 59 | if !ok { 60 | // TODO fail when there is no user, since this can't really happen! 61 | sUser = "" 62 | } 63 | */ 64 | 65 | dRet, ms := handler.GetMultistatusDocWOResponseTag() 66 | 67 | // TODO check if user exists? 68 | 69 | doc := etree.NewDocument() 70 | size, err := doc.ReadFrom(req.Body) 71 | if err != nil || size == 0 { 72 | 73 | fmt.Printf("Error reading XML Body. Error: %s Size: %v", err, size) 74 | 75 | handler.RespondWithMessage(w, http.StatusInternalServerError, "") 76 | return 77 | } 78 | 79 | // find query parameters and store in props 80 | // could probably be faster with compiled path... 81 | // propfindPath := etree.MustCompilePath("/propfind/prop/*") 82 | propsQuery := doc.FindElements("/propfind/prop/*") 83 | 84 | // get the propstat for the root response 85 | psRoot := handler.AddResponseWStatusToMultistat(ms, "/cal/"+sUser+"/", http.StatusOK) 86 | 87 | // let helper function fill prop element with requested props from the root 88 | fillPropfindResponseOnAddressbookRoot(psRoot, sUser, propsQuery) 89 | 90 | err, rowsADB := tablemodule.GetAddressbooksFromUser(sUser) 91 | if err != nil { 92 | 93 | fmt.Printf("Error getting ADB from User: %s", err) 94 | 95 | handler.RespondWithMessage(w, http.StatusInternalServerError, "") 96 | return 97 | } 98 | 99 | // let helper function fill prop elements for every single sub element (addressbook for this user) 100 | fillPropfindResponseOnEachAddressbook(ms, sUser, rowsADB, propsQuery) 101 | 102 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 103 | } 104 | 105 | /* 106 | TODO find out if that function should ever be called 107 | func PropfindAddressbook(w http.ResponseWriter, req *http.Request) { 108 | 109 | vars := mux.Vars(req) 110 | sUser := vars["user"] 111 | sAddressbook := vars["addressbook"] 112 | 113 | // TODO: check out if we already have a record for the default addressbook 114 | // if not, lets create it, otherwise let's return its values... 115 | err, adb := tablemodule.GetOrCreateAddressbookByName(sAddressbook, sUser) 116 | if err != nil { 117 | 118 | fmt.Printf("Error getting ADB by name: %s", err) 119 | 120 | handler.RespondWithMessage(w, http.StatusInternalServerError, "Can't find addressbook") 121 | return 122 | } 123 | 124 | err, vcards := tablemodule.FindVcardsByAddressbook(adb.Pkey) 125 | if err != nil { 126 | 127 | fmt.Printf("Error getting VCARDS for ADB: %s", err) 128 | 129 | handler.RespondWithMessage(w, http.StatusInternalServerError, "Error loading VCARD") 130 | return 131 | } 132 | 133 | dRet, ms := handler.GetMultistatusDocWOResponseTag() 134 | 135 | 136 | for _, vcard := range vcards { 137 | 138 | response += returnPropfindProps(comm, childs, adb, rsVCARDS); 139 | 140 | } 141 | 142 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 143 | 144 | // -> response += returnPropfindProps(comm, childs, adb, rsVCARDS); 145 | 146 | /* 147 | response += returnPropfindRootProps(comm, childs); 148 | 149 | var defaults = { 150 | pkey: generateUUIDv4(), 151 | ownerId: username, 152 | name: 'default', 153 | synctoken: 0 154 | }; 155 | 156 | // check out if we already have a record for the default addressbook 157 | // if not, lets create it, otherwise let's return its values... 158 | ADB.findOrCreate({where: {ownerId: username, name: defaults.name}, defaults: defaults }).spread(function(adb, created) 159 | { 160 | VCARD.findAndCountAll(/* 161 | response += returnPropfindRootProps(comm, childs); 162 | 163 | var defaults = { 164 | pkey: generateUUIDv4(), 165 | ownerId: username, 166 | name: 'default', 167 | synctoken: 0 168 | }; 169 | 170 | // check out if we already have a record for the default addressbook 171 | // if not, lets create it, otherwise let's return its values... 172 | ADB.findOrCreate({where: {ownerId: username, name: defaults.name}, defaults: defaults }).spread(function(adb, created) 173 | { 174 | VCARD.findAndCountAll( 175 | { where: {addressbookId: adb.pkey}} 176 | ).then(function(rsVCARDS) 177 | { 178 | response += returnPropfindProps(comm, childs, adb, rsVCARDS); 179 | 180 | if(created) 181 | { 182 | adb.save().then(function() 183 | { 184 | log.warn('adb saved'); 185 | }); 186 | } 187 | 188 | comm.appendResBody(""); 189 | comm.appendResBody(response); 190 | comm.appendResBody(""); 191 | 192 | comm.flushResponse(); 193 | }); 194 | }); 195 | } 196 | */ 197 | 198 | func fillPropfindResponseOnAddressbookRoot(psRoot *etree.Element, sUser string, propsQuery []*etree.Element) { 199 | 200 | token := "" 201 | 202 | for _, e := range propsQuery { 203 | 204 | //fmt.Println(e.Tag) 205 | name := e.Tag 206 | switch name { 207 | 208 | case "current-user-privilege-set": 209 | fillCurrentUserPrivilegeSetRoot(psRoot, sUser) 210 | 211 | case "owner": 212 | // 213 | // /p/a3298271331/ 214 | // 215 | prop := psRoot.CreateElement("owner") 216 | prop.Space = "d" 217 | 218 | href := prop.CreateElement("href") 219 | href.Space = "d" 220 | 221 | href.SetText("/p/" + sUser + "/") 222 | 223 | case "resourcetype": 224 | // 225 | // 226 | // 227 | prop := psRoot.CreateElement("resourcetype") 228 | prop.Space = "d" 229 | 230 | col := prop.CreateElement("collection") 231 | col.Space = "d" 232 | 233 | case "supported-report-set": 234 | fillSupportedReportSetRoot(psRoot) 235 | 236 | case "sync-token": 237 | prop := psRoot.CreateElement("sync-token") 238 | prop.Space = "d" 239 | prop.SetText("https://swordlord.com/ns/sync/" + token) 240 | 241 | default: 242 | if name != "text" { 243 | fmt.Println("CARD_AddressbookRoot-PF: not handled: " + name) 244 | } 245 | } 246 | } 247 | } 248 | 249 | func fillPropfindResponseOnEachAddressbook(ms *etree.Element, sUser string, rowsADB []*model.ADB, propsQuery []*etree.Element) { 250 | 251 | for _, row := range rowsADB { 252 | 253 | psRoot := handler.AddResponseWStatusToMultistat(ms, "/cal/"+sUser+"/"+row.Pkey+"/", http.StatusOK) 254 | 255 | for _, e := range propsQuery { 256 | 257 | //fmt.Println(e.Tag) 258 | name := e.Tag 259 | switch name { 260 | 261 | case "current-user-privilege-set": 262 | fillCurrentUserPrivilegeSetADB(psRoot, sUser, row) 263 | 264 | case "displayname": //>Addressbook 265 | // Addressbook 266 | prop := psRoot.CreateElement("displayname") 267 | prop.Space = "d" 268 | 269 | prop.SetText(row.Name) 270 | 271 | case "max-resource-size": //>1048576 272 | // 1048576 273 | prop := psRoot.CreateElement("max-resource-size") 274 | prop.Space = "card" 275 | 276 | prop.SetText("1048576") 277 | 278 | case "owner": 279 | // 280 | // /p/a3298271331/ 281 | // 282 | prop := psRoot.CreateElement("owner") 283 | prop.Space = "d" 284 | 285 | href := prop.CreateElement("href") 286 | href.Space = "d" 287 | 288 | href.SetText("/p/" + sUser + "/") 289 | 290 | case "resourcetype": 291 | // 292 | // 293 | // 294 | // 295 | prop := psRoot.CreateElement("resourcetype") 296 | prop.Space = "d" 297 | 298 | col := prop.CreateElement("collection") 299 | col.Space = "d" 300 | 301 | addrb := prop.CreateElement("addressbook") 302 | addrb.Space = "card" 303 | 304 | case "supported-report-set": 305 | fillSupportedReportSetADB(psRoot) 306 | 307 | case "sync-token": 308 | prop := psRoot.CreateElement("sync-token") 309 | prop.Space = "d" 310 | prop.SetText("https://swordlord.com/ns/sync/" + strconv.Itoa(row.Synctoken)) 311 | 312 | default: 313 | if name != "text" { 314 | fmt.Println("CARD-Addressbook-PF: not handled: " + name) 315 | } 316 | } 317 | } 318 | } 319 | } 320 | 321 | func fillCurrentUserPrivilegeSetRoot(ps *etree.Element, user string) { 322 | 323 | fillCurrentUserPrivilegeStandardSet(ps) 324 | } 325 | 326 | func fillCurrentUserPrivilegeSetADB(ps *etree.Element, user string, row *model.ADB) { 327 | 328 | fillCurrentUserPrivilegeStandardSet(ps) 329 | } 330 | 331 | func fillCurrentUserPrivilegeStandardSet(ps *etree.Element) { 332 | 333 | // 334 | // 335 | // 336 | // 337 | // 338 | // 339 | // 340 | // 341 | // 342 | // 343 | // 344 | // 345 | // 346 | // 347 | // 348 | // 349 | // 350 | // 351 | // 352 | // 353 | // 354 | // 355 | // 356 | // 357 | // 358 | // 359 | // 360 | // 361 | // 362 | // 363 | // 364 | // 365 | 366 | cups := ps.CreateElement("current-user-privilege-set") 367 | cups.Space = "d" 368 | 369 | addPrivilege(cups, "d", "write") 370 | addPrivilege(cups, "d", "write-acl") 371 | addPrivilege(cups, "d", "write-properties") 372 | addPrivilege(cups, "d", "write-content") 373 | addPrivilege(cups, "d", "bind") 374 | addPrivilege(cups, "d", "unbind") 375 | addPrivilege(cups, "d", "unlock") 376 | addPrivilege(cups, "d", "read") 377 | addPrivilege(cups, "d", "read-acl") 378 | addPrivilege(cups, "d", "read-current-user-privilege-set") 379 | } 380 | 381 | func addPrivilege(cups *etree.Element, namespace string, elementName string) { 382 | 383 | p := cups.CreateElement("privilege") 384 | p.Space = "d" 385 | 386 | e := p.CreateElement(elementName) 387 | e.Space = namespace 388 | } 389 | 390 | func fillSupportedReportSetRoot(ps *etree.Element) { 391 | 392 | // 393 | // 394 | // 395 | // 396 | // 397 | // 398 | // 399 | // 400 | // 401 | // 402 | // 403 | // 404 | // 405 | // 406 | // 407 | // 408 | // 409 | srs := ps.CreateElement("supported-report-set") 410 | srs.Space = "d" 411 | 412 | addSupportedReportElement(srs, "d", "expand-property") 413 | addSupportedReportElement(srs, "d", "principal-property-search") 414 | addSupportedReportElement(srs, "d", "principal-search-property-set") 415 | } 416 | 417 | func fillSupportedReportSetADB(ps *etree.Element) { 418 | // 419 | // 420 | // 421 | // 422 | // 423 | // 424 | // 425 | // 426 | // 427 | // 428 | // 429 | // 430 | // 431 | // 432 | // 433 | // 434 | // 435 | // 436 | // 437 | // 438 | // 439 | // 440 | // 441 | // 442 | // 443 | // 444 | // 445 | // 446 | // 447 | // 448 | // 449 | // 450 | 451 | srs := ps.CreateElement("supported-report-set") 452 | srs.Space = "d" 453 | 454 | addSupportedReportElement(srs, "d", "expand-property") 455 | addSupportedReportElement(srs, "d", "principal-property-search") 456 | addSupportedReportElement(srs, "d", "principal-search-property-set") 457 | addSupportedReportElement(srs, "card", "addressbook-multiget") 458 | addSupportedReportElement(srs, "card", "addressbook-query") 459 | addSupportedReportElement(srs, "d", "sync-collection") 460 | } 461 | 462 | func addSupportedReportElement(srs *etree.Element, namespace string, elementName string) { 463 | 464 | sr := srs.CreateElement("supported-report") 465 | sr.Space = "d" 466 | 467 | r := sr.CreateElement("report") 468 | r.Space = "d" 469 | 470 | e := r.CreateElement(elementName) 471 | e.Space = namespace 472 | } 473 | -------------------------------------------------------------------------------- /fenneld/handler/calendar/propfind.go: -------------------------------------------------------------------------------- 1 | package calendar 2 | 3 | /*----------------------------------------------------------------------------- 4 | ** 5 | ** - Fennel - 6 | ** 7 | ** your lightweight CalDAV and CardDAV server 8 | ** 9 | ** Copyright 2018 by SwordLord - the coding crew - http://www.swordlord.com 10 | ** and contributing authors 11 | ** 12 | ** This program is free software; you can redistribute it and/or modify it 13 | ** under the terms of the GNU Affero General Public License as published by the 14 | ** Free Software Foundation, either version 3 of the License, or (at your option) 15 | ** any later version. 16 | ** 17 | ** This program is distributed in the hope that it will be useful, but WITHOUT 18 | ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | ** FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 20 | ** for more details. 21 | ** 22 | ** You should have received a copy of the GNU Affero General Public License 23 | ** along with this program. If not, see . 24 | ** 25 | **----------------------------------------------------------------------------- 26 | ** 27 | ** Original Authors: 28 | ** LordEidi@swordlord.com 29 | ** LordCelery@swordlord.com 30 | ** 31 | -----------------------------------------------------------------------------*/ 32 | 33 | import ( 34 | "fmt" 35 | "net/http" 36 | "strconv" 37 | 38 | "github.com/beevik/etree" 39 | "github.com/gorilla/mux" 40 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/model" 41 | "github.com/swordlordcodingcrew/fennel/fennelcore/db/tablemodule" 42 | "github.com/swordlordcodingcrew/fennel/fenneld/handler" 43 | ) 44 | 45 | // TODO check if on root, if yes, answer differently 46 | func PropfindRoot(w http.ResponseWriter, req *http.Request) { 47 | 48 | sUser, ok := req.Context().Value("auth_user").(string) 49 | if !ok { 50 | // TODO fail when there is no user, since this can't really happen! 51 | sUser = "" 52 | } 53 | 54 | dRet, propstat := handler.GetMultistatusDoc("/cal/" + sUser + "/") 55 | 56 | doc := etree.NewDocument() 57 | size, err := doc.ReadFrom(req.Body) 58 | if err != nil || size == 0 { 59 | 60 | fmt.Printf("Error reading XML Body. Error: %s Size: %v", err, size) 61 | 62 | handler.SendMultiStatus(w, http.StatusNotFound, dRet, propstat) 63 | return 64 | } 65 | 66 | // find query parameters and store in props 67 | // could probably be faster with compiled path... 68 | // propfindPath := etree.MustCompilePath("/propfind/prop/*") 69 | propsQuery := doc.FindElements("/propfind/prop/*") 70 | 71 | // create new element to store response in 72 | prop := propstat.CreateElement("prop") 73 | prop.Space = "d" 74 | 75 | // let helper function fill prop element with requested props 76 | // TODO fix empty CAL (nil preferred) 77 | fillPropfindResponse(prop, sUser, model.CAL{}, propsQuery, true) 78 | 79 | // add status based on query 80 | status := propstat.CreateElement("status") 81 | status.Space = "d" 82 | 83 | if len(prop.ChildElements()) > 0 { 84 | 85 | status.SetText("HTTP/1.1 200 OK") 86 | 87 | } else { 88 | 89 | status.SetText("HTTP/1.1 404 Not Found") 90 | } 91 | 92 | // send response to client 93 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 94 | } 95 | 96 | func PropfindUser(w http.ResponseWriter, req *http.Request) { 97 | 98 | // TODO 99 | handler.RespondWithMessage(w, http.StatusMultiStatus, "Not implemented yet") 100 | } 101 | 102 | func PropfindInbox(w http.ResponseWriter, req *http.Request) { 103 | 104 | dRet, propstat := handler.GetMultistatusDoc(req.RequestURI) 105 | 106 | handler.SendMultiStatus(w, http.StatusOK, dRet, propstat) 107 | } 108 | 109 | func PropfindOutbox(w http.ResponseWriter, req *http.Request) { 110 | 111 | // TODO 112 | handler.RespondWithMessage(w, http.StatusMultiStatus, "Not implemented yet") 113 | } 114 | 115 | func PropfindNotification(w http.ResponseWriter, req *http.Request) { 116 | 117 | dRet, propstat := handler.GetMultistatusDoc(req.RequestURI) 118 | 119 | handler.SendMultiStatus(w, http.StatusOK, dRet, propstat) 120 | } 121 | 122 | func PropfindCalendar(w http.ResponseWriter, req *http.Request) { 123 | 124 | vars := mux.Vars(req) 125 | sCal := vars["calendar"] 126 | 127 | sUser, ok := req.Context().Value("auth_user").(string) 128 | if !ok { 129 | // TODO fail when there is no user, since this can't really happen! 130 | sUser = "" 131 | } 132 | 133 | dRet, propstat := handler.GetMultistatusDoc("/cal/" + sUser + "/" + sCal + "/") 134 | 135 | cal, err := tablemodule.GetCal(sCal) 136 | if err != nil { 137 | 138 | fmt.Println(err) 139 | 140 | handler.SendMultiStatus(w, http.StatusNotFound, dRet, propstat) 141 | return 142 | } 143 | 144 | doc := etree.NewDocument() 145 | size, err := doc.ReadFrom(req.Body) 146 | if err != nil || size == 0 { 147 | 148 | fmt.Printf("Error reading XML Body. Error: %s Size: %v", err, size) 149 | 150 | handler.SendMultiStatus(w, http.StatusNotFound, dRet, propstat) 151 | return 152 | } 153 | 154 | // find query parameters and store in props 155 | // could probably be faster with compiled path... 156 | // propfindPath := etree.MustCompilePath("/propfind/prop/*") 157 | propsQuery := doc.FindElements("/propfind/prop/*") 158 | 159 | // create new element to store response in 160 | prop := propstat.CreateElement("prop") 161 | prop.Space = "d" 162 | 163 | // let helper function fill prop element with requested props 164 | fillPropfindResponse(prop, sUser, cal, propsQuery, false) 165 | 166 | // add status based on query 167 | status := propstat.CreateElement("status") 168 | status.Space = "d" 169 | 170 | if len(prop.ChildElements()) > 0 { 171 | 172 | status.SetText("HTTP/1.1 200 OK") 173 | 174 | } else { 175 | 176 | status.SetText("HTTP/1.1 404 Not Found") 177 | } 178 | 179 | // send response to client 180 | handler.SendETreeDocument(w, http.StatusMultiStatus, dRet) 181 | } 182 | 183 | func fillPropfindResponse(node *etree.Element, user string, cal model.CAL, props []*etree.Element, isRoot bool) { 184 | 185 | // TODO 186 | token := "" 187 | 188 | for _, e := range props { 189 | 190 | // fmt.Println(e.Tag) 191 | name := e.Tag 192 | switch name { 193 | 194 | //case "add-member": 195 | 196 | case "allowed-sharing-modes": 197 | // "; 198 | asm := node.CreateElement("allowed-sharing-modes") 199 | asm.Space = "cs" 200 | cbs := asm.CreateElement("can-be-shared") 201 | cbs.Space = "cs" 202 | cbp := asm.CreateElement("can-be-published") 203 | cbp.Space = "cs" 204 | 205 | //case "autoprovisioned": 206 | //case "bulk-requests": 207 | 208 | case "calendar-color": 209 | // " + cal.Colour + ""; 210 | cc := node.CreateElement("calendar-color") 211 | cc.Space = "xical" 212 | cc.CreateAttr("xmlns:xical", "http://apple.com/ns/ical/") 213 | cc.SetText(cal.Colour) 214 | 215 | //case "calendar-description": 216 | //case "calendar-free-busy-set": 217 | //response += "/"; 218 | 219 | case "calendar-order": 220 | // "" + cal.Order + ""; 221 | co := node.CreateElement("calendar-order") 222 | co.Space = "xical" 223 | co.CreateAttr("xmlns:xical", "http://apple.com/ns/ical/") 224 | co.SetText(strconv.Itoa(cal.Order)) 225 | 226 | case "calendar-timezone": 227 | var timezone = cal.Timezone 228 | // TODO check why here we had a replace 229 | //timezone = timezone.replace(/\r\n|\r|\n/g," \r\n"); 230 | //"" + timezone + ""; 231 | ct := node.CreateElement("calendar-timezone") 232 | ct.Space = "cal" 233 | ct.SetText(timezone) 234 | 235 | case "current-user-privilege-set": 236 | getCurrentUserPrivilegeSet(node) 237 | 238 | case "current-user-principal": 239 | // /p/" + username + "/ 240 | cup := node.CreateElement("current-user-principal") 241 | cup.Space = "d" 242 | handler.AddURLElement(cup, "/p/"+user+"/") 243 | 244 | //case "default-alarm-vevent-date": 245 | //case "default-alarm-vevent-datetime": 246 | 247 | case "displayname": 248 | // "" + cal.Displayname + "" 249 | ct := node.CreateElement("displayname") 250 | ct.Space = "d" 251 | ct.SetText(cal.Displayname) 252 | 253 | //case "language-code": 254 | //case "location-code": 255 | 256 | case "owner": 257 | // "/p/" + user +"/" 258 | o := node.CreateElement("owner") 259 | o.Space = "d" 260 | handler.AddURLElement(o, "/p/"+user+"/") 261 | 262 | case "principal-collection-set": 263 | //"/p/" 264 | pcs := node.CreateElement("principal-collection-set") 265 | pcs.Space = "d" 266 | handler.AddURLElement(pcs, "/p/") 267 | 268 | // TODO Check if relative URL is acceptable. if so -> OK 269 | case "pre-publish-url": 270 | //"https://127.0.0.1/cal/" + user + "/" + cal.Pkey + ""; 271 | pcs := node.CreateElement("pre-publish-url") 272 | pcs.Space = "cs" 273 | handler.AddURLElement(pcs, "/cal/"+user+"/"+cal.Pkey) 274 | 275 | //case "publish-url": 276 | //case "push-transports": 277 | //case "pushkey": 278 | //case "quota-available-bytes": 279 | //case "quota-used-bytes": 280 | //case "refreshrate": 281 | //case "resource-id": 282 | 283 | case "resourcetype": 284 | // ""; 285 | rt := node.CreateElement("resourcetype") 286 | rt.Space = "d" 287 | col := rt.CreateElement("collection") 288 | col.Space = "d" 289 | cal := rt.CreateElement("calendar") 290 | cal.Space = "cal" 291 | 292 | case "schedule-calendar-transp": 293 | // ""; 294 | sct := node.CreateElement("schedule-calendar-transp") 295 | sct.Space = "cal" 296 | o := sct.CreateElement("opaque") 297 | o.Space = "cal" 298 | 299 | //case "schedule-default-calendar-URL": 300 | //case "source": 301 | //case "subscribed-strip-alarms": 302 | //case "subscribed-strip-attachments": 303 | //case "subscribed-strip-todos": 304 | //case "supported-calendar-component-set": 305 | 306 | case "supported-calendar-component-set": 307 | // ""; 308 | scc := node.CreateElement("supported-calendar-component-set") 309 | scc.Space = "cal" 310 | c := scc.CreateElement("comp") 311 | c.Space = "cal" 312 | c.CreateAttr("name", "VEVENT") 313 | 314 | case "supported-report-set": 315 | getSupportedReportSet(node, isRoot) 316 | 317 | case "getctag": 318 | prop := node.CreateElement("getctag") 319 | prop.Space = "cs" 320 | prop.SetText("https://swordlord.com/ns/sync/" + token) 321 | 322 | //case "getetag": 323 | // no response? 324 | 325 | //case "checksum-versions": 326 | // no response? 327 | 328 | case "sync-token": 329 | prop := node.CreateElement("sync-token") 330 | prop.Space = "d" 331 | prop.SetText("https://swordlord.com/ns/sync/" + token) 332 | 333 | case "acl": 334 | getACL(node, user) 335 | 336 | case "getcontenttype": 337 | //response += "text/calendar;charset=utf-8"; 338 | gct := node.CreateElement("getcontenttype") 339 | gct.Space = "d" 340 | gct.SetText("text/calendar;charset=utf-8") 341 | 342 | default: 343 | if name != "text" { 344 | fmt.Println("CAL-PF: not handled: " + name) 345 | } 346 | } 347 | } 348 | } 349 | 350 | func getCurrentUserPrivilegeSet(node *etree.Element) { 351 | 352 | /* 353 | response += ""; 354 | response += ""; 355 | response += ""; 356 | response += ""; 357 | response += ""; 358 | response += ""; 359 | response += ""; 360 | response += ""; 361 | response += ""; 362 | response += ""; 363 | response += ""; 364 | response += ""; 365 | response += ""; 366 | */ 367 | 368 | cups := node.CreateElement("current-user-privilege-set") 369 | cups.Space = "d" 370 | 371 | addPrivilegeToPrivilegeSet(cups, "cal", "read-free-busy") 372 | 373 | addPrivilegeToPrivilegeSet(cups, "d", "write") 374 | addPrivilegeToPrivilegeSet(cups, "d", "write-acl") 375 | addPrivilegeToPrivilegeSet(cups, "d", "write-content") 376 | addPrivilegeToPrivilegeSet(cups, "d", "write-properties") 377 | addPrivilegeToPrivilegeSet(cups, "d", "bind") 378 | addPrivilegeToPrivilegeSet(cups, "d", "unbind") 379 | addPrivilegeToPrivilegeSet(cups, "d", "unlock") 380 | addPrivilegeToPrivilegeSet(cups, "d", "read") 381 | addPrivilegeToPrivilegeSet(cups, "d", "read-acl") 382 | addPrivilegeToPrivilegeSet(cups, "d", "read-current-user-privilege-set") 383 | } 384 | 385 | func addPrivilegeToPrivilegeSet(cups *etree.Element, namespace string, privilege string) { 386 | 387 | p := cups.CreateElement("privilege") 388 | p.Space = "d" 389 | p.CreateAttr("xmlns:d", "DAV") 390 | 391 | e := p.CreateElement(privilege) 392 | e.Space = namespace 393 | } 394 | 395 | func getSupportedReportSet(node *etree.Element, isRoot bool) { 396 | 397 | /* 398 | response += ""; 399 | 400 | if(!isRoot) 401 | { 402 | response += ""; 403 | response += ""; 404 | response += ""; 405 | } 406 | 407 | response += ""; 408 | response += ""; 409 | response += ""; 410 | response += ""; 411 | response += ""; 412 | */ 413 | srs := node.CreateElement("supported-report-set") 414 | srs.Space = "d" 415 | 416 | if isRoot { 417 | 418 | addSupportedReport(srs, "calendar-multiget") 419 | addSupportedReport(srs, "calendar-query") 420 | addSupportedReport(srs, "free-busy-query") 421 | } 422 | 423 | addSupportedReport(srs, "sync-collection") 424 | addSupportedReport(srs, "expand-property") 425 | addSupportedReport(srs, "principal-property-search") 426 | addSupportedReport(srs, "principal-search-property-set") 427 | } 428 | 429 | func addSupportedReport(srs *etree.Element, report string) { 430 | 431 | sr := srs.CreateElement("supported-report") 432 | sr.Space = "d" 433 | 434 | r := sr.CreateElement("report") 435 | r.Space = "d" 436 | 437 | e := r.CreateElement(report) 438 | e.Space = "d" 439 | } 440 | 441 | func getACL(node *etree.Element, user string) { 442 | 443 | /* 444 | response += ""; 445 | response += " "; 446 | response += " /p/" + username + ""; 447 | response += " "; 448 | response += " "; 449 | response += " "; 450 | 451 | response += " "; 452 | response += " /p/" + username + ""; 453 | response += " "; 454 | response += " "; 455 | response += " "; 456 | 457 | response += " "; 458 | response += " /p/" + username + "/calendar-proxy-write/"; 459 | response += " "; 460 | response += " "; 461 | response += " "; 462 | 463 | response += " "; 464 | response += " /p/" + username + "/calendar-proxy-write/"; 465 | response += " "; 466 | response += " "; 467 | response += " "; 468 | 469 | response += " "; 470 | response += " /p/" + username + "/calendar-proxy-read/"; 471 | response += " "; 472 | response += " "; 473 | response += " "; 474 | 475 | response += " "; 476 | response += " "; 477 | response += " "; 478 | response += " "; 479 | response += " "; 480 | 481 | response += " "; 482 | response += " /p/system/admins/"; 483 | response += " "; 484 | response += " "; 485 | response += " "; 486 | */ 487 | acl := node.CreateElement("acl") 488 | acl.Space = "d" 489 | 490 | addACEwURL(acl, "/p/"+user, "read") 491 | addACEwURL(acl, "/p/"+user, "write") 492 | 493 | addACEwURL(acl, "/p/"+user+"/calendar-proxy-write/", "read") 494 | addACEwURL(acl, "/p/"+user+"/calendar-proxy-write/", "write") 495 | 496 | addACEwURL(acl, "/p/"+user+"/calendar-proxy-read/", "read") 497 | 498 | addACEFreeBusy(acl) 499 | 500 | addACEwURL(acl, "/p/system/admins/", "all") 501 | } 502 | 503 | func addACEwURL(acl *etree.Element, url string, privilege string) { 504 | 505 | // "; 506 | // /p/" + username + ""; 507 | // "; 508 | // "; 509 | // "; 510 | 511 | ace := acl.CreateElement("ace") 512 | ace.Space = "d" 513 | 514 | princ := ace.CreateElement("principal") 515 | princ.Space = "d" 516 | 517 | href := princ.CreateElement("href") 518 | href.Space = "d" 519 | href.SetText(url) 520 | 521 | g := ace.CreateElement("grant") 522 | g.Space = "d" 523 | 524 | priv := g.CreateElement("privilege") 525 | priv.Space = "d" 526 | 527 | rw := priv.CreateElement(privilege) 528 | rw.Space = "d" 529 | 530 | prot := ace.CreateElement("protected") 531 | prot.Space = "d" 532 | } 533 | 534 | func addACEFreeBusy(acl *etree.Element) { 535 | 536 | // "; 537 | // "; 538 | // "; 539 | // "; 540 | // "; 541 | 542 | ace := acl.CreateElement("ace") 543 | ace.Space = "d" 544 | 545 | princ := ace.CreateElement("principal") 546 | princ.Space = "d" 547 | 548 | a := princ.CreateElement("authenticated") 549 | a.Space = "d" 550 | 551 | g := ace.CreateElement("grant") 552 | g.Space = "d" 553 | 554 | priv := g.CreateElement("privilege") 555 | priv.Space = "d" 556 | 557 | rfb := priv.CreateElement("read-free-busy") 558 | rfb.Space = "cal" 559 | 560 | prot := ace.CreateElement("protected") 561 | prot.Space = "d" 562 | } 563 | --------------------------------------------------------------------------------