// Copyright (c) 2020, Andreas Schneider // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of the nor the // names of its contributors may be used to endorse or promote products // derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package main import ( "context" "fmt" "log" "net/http" "strings" "github.com/go-chi/chi" "golang.org/x/crypto/bcrypt" "golang.org/x/net/webdav" ) type CmdServe struct { ListenAddress string `name:"listen-address" default:":3000" help:"Address to listen on for HTTP requests."` WebDavPath string `name:"dav-path" default:"/dav" help:"Path to use for WebDAV requests."` AdminPath string `name:"admin-path" default:"/admin" help:"Path to use for the admin interface."` } func (cmd *CmdServe) Run(app *app) error { h := &webdav.Handler{} h.Prefix = cmd.WebDavPath + "/" h.LockSystem = webdav.NewMemLS() h.FileSystem = BaseDir(app.DataDirectory) authenticatedWebdavHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { writeUnauthorized := func() { w.Header().Set("WWW-Authenticate", `Basic realm="ShareDAV"`) http.Error(w, "unauthorized", http.StatusUnauthorized) } username, password, ok := r.BasicAuth() if !ok { writeUnauthorized() return } usernameParts := strings.Split(username, "/") if len(usernameParts) != 2 { http.Error(w, "invalid username pattern", http.StatusBadRequest) return } username = usernameParts[0] loginName := usernameParts[1] share, err := app.shareStore.FindShareByLogin(username, loginName) if err == ErrShareNotFound { writeUnauthorized() return } else if err != nil { log.Printf("Cannot process login request: %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if err := bcrypt.CompareHashAndPassword([]byte(share.Password), []byte(password)); err != nil { writeUnauthorized() return } readonly := share.ReadOnly || share.Role == ShareRoleReader directoryMapping := DirectoryMapping{ DataDirName: share.UUID.String(), ReadOnly: readonly, } // Use the WebDAV handler to actually serve the request. Also enhance the context // to contain the subdirectory (of the base directory) which contains the data for // the authenticated user. h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "mapping", &directoryMapping))) }) // Setup the admin endpoint with convenience routing. (Should the user omit the trailing slash, we add // it for them.) r := chi.NewRouter() adminPathWithoutSlash := strings.TrimSuffix(cmd.AdminPath, "/") if !strings.HasSuffix(cmd.AdminPath, "/") { cmd.AdminPath = cmd.AdminPath + "/" } r.Get(adminPathWithoutSlash, func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, cmd.AdminPath, http.StatusMovedPermanently) }) r.Mount(cmd.AdminPath, newWebAdminHandler(app)) // Can't use Chi at the root, since it can't properly handle the mounted WebDAV // handler. So we register Chi as the default handler, but delegate the WebDAV // path with the default mux. m := http.NewServeMux() m.Handle("/", r) m.Handle(h.Prefix, authenticatedWebdavHandler) fmt.Printf("Listening on %s\n", cmd.ListenAddress) if err := http.ListenAndServe(cmd.ListenAddress, m); err != http.ErrServerClosed && err != nil { return fmt.Errorf("cannot listen: %w", err) } return nil }