├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------