123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- package webdav
- /*
- WebDAV File Server
- author: tobychui
- This module handles file sharing via WebDAV protocol.
- In theory, this should be compatible with Windows 10 and possibily
- replacing the need for samba
- */
- import (
- "encoding/json"
- "log"
- "net/http"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "sync"
- "time"
- "imuslab.com/arozos/mod/filesystem"
- "imuslab.com/arozos/mod/filesystem/hidden"
- "imuslab.com/arozos/mod/filesystem/metadata"
- "imuslab.com/arozos/mod/network/webdav"
- "imuslab.com/arozos/mod/user"
- "imuslab.com/arozos/mod/utils"
- )
- type Server struct {
- hostname string //The hostname of this devices
- userHandler *user.UserHandler //The central userHandler
- filesystems sync.Map //The syncmap for storing opened file server
- prefix string //The prefix to strip away from filepath
- tlsMode bool //Bypass tls windows mode if enabled
- Enabled bool //If the server is enabled. Set this to false for disable this service
- //Windows related authentication using Web interface
- readOnlyFileSystemHandler *webdav.Handler
- windowsClientNotLoggedIn sync.Map //Map to store not logged in windows WebDAV Client
- windowsClientLoggedIn sync.Map //Map to store logged in Windows WebDAV Client
- }
- type WindowClientInfo struct {
- Agent string
- LastConnectionTimestamp int64
- UUID string
- Username string
- ClientIP string
- }
- //NewServer create a new WebDAV server object required by arozos
- func NewServer(hostname string, prefix string, tmpdir string, tlsMode bool, userHandler *user.UserHandler) *Server {
- //Generate a default handler
- os.MkdirAll(filepath.Join(tmpdir, "webdav"), 0777)
- rofs := &webdav.Handler{
- Prefix: prefix,
- FileSystem: webdav.Dir(filepath.Join(tmpdir, "webdav")),
- LockSystem: webdav.NewMemLS(),
- }
- return &Server{
- hostname: hostname,
- userHandler: userHandler,
- filesystems: sync.Map{},
- prefix: prefix,
- tlsMode: tlsMode,
- Enabled: true,
- readOnlyFileSystemHandler: rofs,
- }
- }
- func (s *Server) HandleClearAllPending(w http.ResponseWriter, r *http.Request) {
- //Clear all pending client requests
- keys := []string{}
- s.windowsClientNotLoggedIn.Range(func(key, value interface{}) bool {
- keys = append(keys, key.(string))
- return true
- })
- //Clear all pending requests
- for _, key := range keys {
- s.windowsClientNotLoggedIn.Delete(key)
- }
- sendOK(w)
- }
- //Handle allow and remove permission of a windows WebDAV Client
- func (s *Server) HandlePermissionEdit(w http.ResponseWriter, r *http.Request) {
- opr, err := utils.Mv(r, "opr", true)
- if err != nil {
- sendErrorResponse(w, "Invalid operations")
- return
- }
- uuid, err := utils.Mv(r, "uuid", true)
- if err != nil {
- sendErrorResponse(w, "Invalid uuid")
- return
- }
- userinfo, err := s.userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- sendErrorResponse(w, "User not logged in")
- return
- }
- if opr == "set" {
- //Set the given uuid into the user permission folder
- value, ok := s.windowsClientNotLoggedIn.Load(uuid)
- if !ok {
- sendErrorResponse(w, "Client registry not exists!")
- return
- }
- //Add the value into the logged in list with this username
- ConnectionObject := value.(*WindowClientInfo)
- ConnectionObject.Username = userinfo.Username
- s.windowsClientLoggedIn.Store(uuid, ConnectionObject)
- //Remove the value from the not logged in list
- s.windowsClientNotLoggedIn.Delete(uuid)
- sendOK(w)
- } else if opr == "remove" {
- value, ok := s.windowsClientLoggedIn.Load(uuid)
- if !ok {
- sendErrorResponse(w, "Client registry not exists!")
- return
- }
- //Move the object back to the not logged in one and remove username
- ConnectionObject := value.(*WindowClientInfo)
- ConnectionObject.Username = ""
- s.windowsClientNotLoggedIn.Store(uuid, ConnectionObject)
- //Remove the object from logged in list
- s.windowsClientLoggedIn.Delete(uuid)
- sendOK(w)
- } else {
- sendErrorResponse(w, "Unsupported operation")
- return
- }
- }
- func (s *Server) HandleConnectionList(w http.ResponseWriter, r *http.Request) {
- target, _ := utils.Mv(r, "target", false)
- results := []*WindowClientInfo{}
- if target == "" {
- //List not logged in clients
- s.windowsClientNotLoggedIn.Range(func(key, value interface{}) bool {
- targetWindowClientInfo := value.(*WindowClientInfo)
- results = append(results, targetWindowClientInfo)
- return true
- })
- } else if target == "loggedin" {
- userinfo, err := s.userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- sendErrorResponse(w, "User not logged in")
- return
- }
- userIsAdmin := userinfo.IsAdmin()
- //List logged in clients
- s.windowsClientLoggedIn.Range(func(key, value interface{}) bool {
- targetWindowClientInfo := value.(*WindowClientInfo)
- if userIsAdmin {
- //Allow access to all user's permission
- results = append(results, targetWindowClientInfo)
- } else {
- //Check if username match before append
- if targetWindowClientInfo.Username == userinfo.Username {
- results = append(results, targetWindowClientInfo)
- }
- }
- return true
- })
- }
- //Sort the results
- sort.Slice(results, func(i, j int) bool {
- return results[i].LastConnectionTimestamp > results[j].LastConnectionTimestamp
- })
- js, _ := json.Marshal(results)
- sendJSONResponse(w, string(js))
- }
- func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) {
- //log.Println(r.Header)
- //log.Println("Request Method: ", r.Method)
- //Check if this is enabled
- if !s.Enabled {
- http.NotFound(w, r)
- return
- }
- if r.URL.Path == "/webdav" {
- //No vRoot defined. Reject connection
- http.NotFound(w, r)
- return
- }
- reqInfo := strings.Split(r.URL.RequestURI()[1:], "/")
- reqRoot := "user"
- if len(reqInfo) > 1 {
- reqRoot = reqInfo[1]
- }
- if strings.TrimSpace(reqRoot) == "" {
- //No vroot defined.
- http.NotFound(w, r)
- return
- }
- //Windows File Explorer. Handle with special case
- if r.Header["User-Agent"] != nil && strings.Contains(r.Header["User-Agent"][0], "Microsoft-WebDAV-MiniRedir") && r.TLS == nil {
- log.Println("Windows File Explorer Connection. Routing using alternative handler")
- s.HandleWindowClientAccess(w, r, reqRoot)
- return
- }
- username, password, ok := r.BasicAuth()
- if !ok {
- //User not logged in.
- //log.Println("Not logged in!")
- w.Header().Set("WWW-Authenticate", `Basic realm="Login with your `+s.hostname+` account"`)
- w.WriteHeader(http.StatusUnauthorized)
- return
- }
- //validate username and password
- authAgent := s.userHandler.GetAuthAgent()
- //Validate request origin
- allowAccess, err := authAgent.ValidateLoginRequest(w, r)
- if !allowAccess {
- log.Println("Someone from " + r.RemoteAddr + " try to log into " + username + " WebDAV endpoint but got rejected: " + err.Error())
- http.Error(w, err.Error(), http.StatusUnauthorized)
- return
- }
- passwordValid, rejectionReason := authAgent.ValidateUsernameAndPasswordWithReason(username, password)
- if !passwordValid {
- authAgent.Logger.LogAuthByRequestInfo(username, r.RemoteAddr, time.Now().Unix(), false, "webdav")
- log.Println("Someone from " + r.RemoteAddr + " try to log into " + username + " WebDAV endpoint but got rejected: " + rejectionReason)
- http.Error(w, rejectionReason, http.StatusUnauthorized)
- return
- }
- //Resolve the vroot to realpath
- userinfo, err := s.userHandler.GetUserInfoFromUsername(username)
- if err != nil {
- log.Println(err.Error())
- http.Error(w, "Invalid username or password", http.StatusUnauthorized)
- return
- }
- fsh, err := userinfo.GetFileSystemHandlerFromVirtualPath(reqRoot + ":/")
- if err != nil {
- log.Println("[WebDAV] Failed to load File System Handler from request root: ", reqRoot+":/", err.Error())
- http.Error(w, "Invalid ", http.StatusInternalServerError)
- return
- }
- //Try to resolve the realpath of the vroot
- /*
- realRoot, err := userinfo.VirtualPathToRealPath(reqRoot + ":/")
- if err != nil {
- log.Println(err.Error())
- http.Error(w, "Invalid ", http.StatusUnauthorized)
- return
- }
- */
- //Ok. Check if the file server of this root already exists
- fs := s.getFsFromRealRoot(fsh, userinfo.Username, filepath.ToSlash(filepath.Join(s.prefix, reqRoot)))
- //Serve the content
- fs.ServeHTTP(w, r)
- }
- /*
- Serve ReadOnly WebDAV Server
- This section exists because Windows WebDAV Services require a
- success connection in order to store the cookie. If nothing is served,
- File Explorer will not cache the cookie in its cache
- */
- func (s *Server) serveReadOnlyWebDav(w http.ResponseWriter, r *http.Request) {
- if r.Method == "PUT" || r.Method == "POST" || r.Method == "MKCOL" ||
- r.Method == "DELETE" || r.Method == "COPY" || r.Method == "MOVE" {
- //Not allowed
- w.WriteHeader(http.StatusForbidden)
- } else {
- r.URL.Path = "/webdav/"
- s.readOnlyFileSystemHandler.ServeHTTP(w, r)
- }
- }
- func (s *Server) getFsFromRealRoot(fsh *filesystem.FileSystemHandler, username string, prefix string) *webdav.Handler {
- //Create a webdav adapter from the fsh
- fshadapter := NewFshWebDAVAdapter(fsh, username)
- fs := &webdav.Handler{
- Prefix: prefix,
- FileSystem: fshadapter,
- LockSystem: webdav.NewMemLS(),
- }
- //Create event listener for the path request
- fs.RequestEventListener = func(path string) {
- //Generate thumbnail in the background if listed
- vpath, _ := fsh.FileSystemAbstraction.RealPathToVirtualPath(path, username)
- go func() {
- isHidden, _ := hidden.IsHidden(vpath, false)
- if !isHidden {
- metadata.NewRenderHandler().BuildCacheForFolder(fsh, vpath, username)
- }
- }()
- }
- return fs
- }
|