-
Notifications
You must be signed in to change notification settings - Fork 64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support custom user principal and home set paths #62
Support custom user principal and home set paths #62
Conversation
With this, I have successfully created a single server on which Evolution finds both a calendar as well as an address book. I also tested that using e.g. a simple I also managed to create a setup with dynamic user principal paths, so you could have e.g. |
server.go
Outdated
|
||
// ServeUserPrincipal replies to discovery requests. It returns false if the | ||
// request wasn't handled. | ||
func ServeUserPrincipal(w http.ResponseWriter, r *http.Request, backend UserPrincipalBackend) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should just take a struct instead of an interface here. For instance:
func ServeUserPrincipal(w http.ResponseWriter, r *http.Request, options *ServeUserPrincipalOptions) bool
type ServeUserPrincipalOptions struct {
Path string
HomeSets map[string]HomeSet
}
Should be very cheap to compute for the backend I think?
Not sure about the struct name yet -- if it's used by other functions and not just ServeUserPrincipal
we should pick a more general name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could be used elsewhere, though it wouldn't have to if it's inconvenient. I was actually at some point considering attaching such a struct to the context. Maybe even use a middleware to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, we also need the interface to pass on to the internal.Handler
, which needs it (in general, not so much for this function), so I guess we might as well stick to it? I might also be misunderstanding what you are aiming for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess what I'm trying to get to is that UserPrincipalBackend
is a bit overkill for the purposes of ServeUserPrincipal
. We don't actually need the complexity of an interface with multiple functions here, a simple ServeUserPrincipalOptions
struct with the info in it would be enough.
That said, it's true that CardDAV and CalDAV backends also need to access this info, and embedding UserPrincipalBackend
in their backends is a simple way to require that. If we use a struct instead of an interface, we'd have to add
UserPrincipal(context.Context) *ServeUserPrincipalOptions
to the CardDAV and CalDAV backend interfaces. At which point the name ServeUserPrincipalOptions
is maybe not a good name, maybe something else like UserPrincipalOptions
or UserPrincipalInfo
would be more fitting.
Just thinking out loud here, not sure what the best solution is, and I'm probably thinking too much as I often do. :P
5ef61b2
to
bba482a
Compare
server.go
Outdated
@@ -234,3 +237,128 @@ func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (cr | |||
} | |||
return created, err | |||
} | |||
|
|||
type HomeSet interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not super happy about this interface, because it's pretty easy to implement it externally outside of go-webdav, and implement it wrong by missing the fact that it needs to be XML-marshalable. To alleviate this, I've been thinking about making it impossible for external users to implement it. Usually this is done by adding an unexported func in the interface, e.g.
type HomeSet interface {
homeSet()
}
that way only the current package can implement it. But this wouldn't work here because the type is shared between our own packages, and the carddav
package won't be able to implement the type.
Then I thought about a bunch of different solutions, like moving it to internal/
and doing more tricks to ensure only us can implement it. But it's pretty ugly and not worth the trouble.
tl;dr I think it's fine and we just need to document the type here. Explain what it represents, explain that it needs to marshal to XML to describe the home set element, etc.
server.go
Outdated
@@ -234,3 +237,128 @@ func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (cr | |||
} | |||
return created, err | |||
} | |||
|
|||
type HomeSet interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This interface looks like it won't be easily re-usable in the DAV client. It would be nicer to find a good name to reflect this.
caldav/server.go
Outdated
@@ -24,7 +25,8 @@ type Backend interface { | |||
// Handler handles CalDAV HTTP requests. It can be used to create a CalDAV | |||
// server. | |||
type Handler struct { | |||
Backend Backend | |||
Backend Backend | |||
UserPrincipalBackend webdav.UserPrincipalBackend |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could just embed webdav.UserPrincipalBackend
into Backend
to avoid dealing with multiple backends.
server.go
Outdated
// RelativeResourcePath returns the path of a resource without the prefix of | ||
// the home set that the resource is in. If the resource is not in any known | ||
// home set, the original request path is returned. | ||
func RelativeResourcePath(r *http.Request, backend UserPrincipalBackend) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move RelativeResourcePath
and IsResourceRequest
to internal/
? Or do we have good reasons to expose them?
4afef1c
to
9790163
Compare
Ok, I think I addressed all of your comments. I also shuffled the interfaces a bit (user principal backend is only responsible for user principal URL, carddav/caldav backend are responsible for home set path). I think the implementation is reasonably clean, given the complexity of the matter. The application implementation would also be pretty straightforward. If you give a thumbs up I am also happy to add some more documentation. |
9790163
to
2f3e249
Compare
server.go
Outdated
// ServeUserPrincipal replies to discovery requests. It returns false if the | ||
// request wasn't handled. | ||
func ServeUserPrincipal(w http.ResponseWriter, r *http.Request, options ServeUserPrincipalOptions) bool { | ||
if r.URL.Path == "/.well-known/carddav" || r.URL.Path == "/.well-known/caldav" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. I'm not a fan of hardcoding carddav and caldav here. Can we leave that to the appropriate packages?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please open an issue with more details. I don't really understand what you're talking about here.
carddav/server.go
Outdated
} | ||
if !strings.HasPrefix(r.URL.Path, homeSetPath) { | ||
return nil, &internal.HTTPError{Code: http.StatusMethodNotAllowed} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure it's a good idea to add this logic. Servers may want to allow users to fetch/edit calendars which aren't under the home set. Additionally, strings.HasPrefix
is not appropriate here -- the check passes with the path /abcdef
and the home set /abc
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so a few things 🙂
Servers may want to allow users to fetch/edit calendars which aren't under the home set
I agree that this won't work like this, but as things are right now, this is outright impossible anyways (due to the home set being /
). I think part of why this PR turned a bit tedious might be that I cannot quite seem to gauge your expectations. I initially had the impression you'd actually prefer to only do the bare minimum required to get the two servers working side-by-side, which would have just been allowing a home set prefix. I guess it is on me that I brought the user principal URL into this.
However, with this comment, I get the impression that now we're talking about laying the foundations for all kinds of things that have been previously impossible, such as accessing other people's calendars. This is fine, I just need to understand where we want to go.
Through #70 , I also realized that the backends should behave more like a HTTP backend (as opposed to e.g. map lookup or such). As such, my suggestion would be:
- we remove that check entirely
- as discussed in carddav: return 404 if contact not found #70, we expose some appropriate internal errors (not found, access denied, etc)
- we document that the backend can have absolutely zero expectations w/ regards to the path it is asked about and has to check it itself and return the appropriate errors
WDYT?
PS: on a side note, I am quite disappointed that Go doesn't have a proper path.HasPrefix()
, which is why I initially wrote those custom functions. But the check really would have never been good enough anyways, and the backend will have to perform them. So just removing it from here really makes sense...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, with this comment, I get the impression that now we're talking about laying the foundations for all kinds of things that have been previously impossible, such as accessing other people's calendars. This is fine, I just need to understand where we want to go.
From my PoV, I'm just trying not to add any more assumptions which we will regret later on. The HasPrefix
checks are new here, and I don't think it's something we want to have long-term.
Through #70 , I also realized that the backends should behave more like a HTTP backend (as opposed to e.g. map lookup or such).
Right. It's mostly about being able to return more detailed errors than "something failed". Just like os.Open
errors can be introspected for instance.
WDYT?
LGTM!
I am quite disappointed that Go doesn't have a proper path.HasPrefix(), which is why I initially wrote those custom functions.
Yeah, I agree this is disappointing, as this is a common thing to do and it can be error-prone. Something like this can be used in backends:
func pathHasPrefix(p, prefix string) bool {
p = path.Clean(p)
prefix = path.Clean(prefix)
return p == prefix || strings.HasPrefix(p, prefix + "/")
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the checks as discussed and also addressed your other comments.
carddav/server.go
Outdated
@@ -330,6 +374,18 @@ func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth i | |||
return internal.NewMultistatus(resps...), nil | |||
} | |||
|
|||
func (b *backend) propfindCommon(propfind *internal.Propfind, path, principalPath, homeSetPath string) (*internal.Response, error) { | |||
props := map[xml.Name]internal.PropfindFunc{ | |||
addressBookHomeSetName: func(*internal.RawXMLValue) (interface{}, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This only needs to be defined on the user principal.
2f3e249
to
4927d7a
Compare
server.go
Outdated
// ServeUserPrincipal replies to discovery requests. It returns false if the | ||
// request wasn't handled. | ||
func ServeUserPrincipal(w http.ResponseWriter, r *http.Request, options ServeUserPrincipalOptions) bool { | ||
if r.Method != "PROPFIND" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. If r.URL.Path != b.options.UserPrincipalPath
we shouldn't handle the request, right? Should we return false here in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(In which case, maybe we can just let the caller perform the path check and drop UserPrincipalPath
?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the idea was that this handles everything up to, including the user principal URL. I've observed at least Evolution sending PROPSTAT to /
during discovery, which then returns the user principal URL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's also why the .well-know handling was in there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm I'd rather just handle the principal stuff in here (as the function name suggests).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I rewrote that function based on #74 (feel free to add yourself as co-author 🙂 ). I didn't take everything from there to not blow this up any more, I hope that's fine. Happy to add more bits and pieces afterwards.
caldav/server.go
Outdated
var resps []internal.Response | ||
if r.URL.Path == "/" { | ||
|
||
resp, err := b.propfindCommon(propfind, r.URL.Path, principalPath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. This is a bit weird. It produces multiple <response>
elements for the same href potentially.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sorry, I think I refactored that once too often. The composability is a bit weird there. Are you okay with how it is now?
I think in the long run, if we add more basic WebDAV properties, it might make sense to pass around pre-filled maps. But I tried that, and it would require a map copy in some places, so it's a bit ugly also. For now, it seemed best to just follow the current pattern but add the additional property everywhere...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you okay with how it is now?
Yeah sounds fine, let's keep it simple. Though, CardDAV seems fixed but not CalDAV.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, that got lost in rebase-land 😕 fixed now
fc42929
to
56f724d
Compare
Currently, the user principal path and the home set path are both hardcoded to "/", for both CalDAV and CardDAV. This poses a challenge if one wishes to run a CardDAV and CalDAV server in the same server. This commit introduces the concept of a UserPrincipalBackend. This backend must provide the path of the current user's principal URL from the given request context. The CalDAV and CardDAV backends are extended to also function as UserPrincipalBackend. In addition, they are required to supply the path of the respective home set (`calendar-home-set` and `addressbook-home-set`). The CardDAV and CalDAV servers act accordingly. The individual servers will continue to work as before (including the option of keeping everything at "/"). If one wishes to run CardDAV and CalDAV in parallel, the new `webdav.ServeUserPrincipal()` can be used as a convenience function to serve a common user principal URL for both servers. The input for this function can be easily computed by the application by getting the home set paths from the backends and using `caldav.NewCalendarHomeSet()` and `carddav.NewAddressbookHomeSet()` to create the home sets. Note that the storage backend will have to know about these paths as well. For any non-trivial use case, a storage backend should probably have access to the same UserPrincipalBackend. That is, however, an implementation detail and doesn't have to be reflected in the interfaces.
56f724d
to
6b29ae3
Compare
For what it's worth, you can see how the usage of all this could look like in its current state here: https://git.sr.ht/~sircmpwn/tokidoki/tree/master/item/cmd/tokidoki/main.go Note that I am using more stuff that I wrote that I have not yet created PRs for, as it would conflict with this one, you can find that here: https://github.com/bitfehler/go-webdav/commits/bitfehler/more-caldav I am constantly rebasing that branch on the latest developments here, so should be easy to merge the rest once we arrive at a solution. |
if err := internal.DecodeXMLRequest(r, &propfind); err != nil { | ||
return err | ||
} | ||
props := map[xml.Name]internal.PropfindFunc{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're missing a few principal props marked in the RFC as "MUST support", but we were missing them prior to this PR too. Let's keep that in mind for the future.
Thanks! |
Currently, the user principal path and the home set path are both
hardcoded to "/", for both CalDAV and CardDAV. This poses a challenge if
one wishes to run a CardDAV and CalDAV server in the same server.
This commit introduces the concept of a UserPrincipalBackend. This
backend must provide the locations for the user principal as well as the
home sets. The CardDAV and CalDAV servers act accordingly.
The individual servers will continue to work as before (including the
option of keeping everything at "/"). If one wishes to run CardDAV and
CalDAV in parallel, the new
webdav.ServeUserPrincipal()
can be used,which will use the UserPrincipalBackend interface to serve a common user
principal for both servers (if so desired, can be used for a single one
also).
Note that the storage backend will have to know about these paths as
well. For any non-trivial use case, a storage backend should probably
have access to the same UserPrincipalBackend. That is, however, an
implementation detail and doesn't have to be reflected in the
interfaces.