github.com/divyam234/rclone@v1.64.1/cmd/serve/http/http.go (about) 1 // Package http provides common functionality for http servers 2 package http 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "log" 10 "net/http" 11 "os" 12 "path" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/go-chi/chi/v5/middleware" 18 "github.com/divyam234/rclone/cmd" 19 "github.com/divyam234/rclone/cmd/serve/proxy" 20 "github.com/divyam234/rclone/cmd/serve/proxy/proxyflags" 21 "github.com/divyam234/rclone/fs" 22 "github.com/divyam234/rclone/fs/accounting" 23 libhttp "github.com/divyam234/rclone/lib/http" 24 "github.com/divyam234/rclone/lib/http/serve" 25 "github.com/divyam234/rclone/vfs" 26 "github.com/divyam234/rclone/vfs/vfsflags" 27 "github.com/spf13/cobra" 28 ) 29 30 // Options required for http server 31 type Options struct { 32 Auth libhttp.AuthConfig 33 HTTP libhttp.Config 34 Template libhttp.TemplateConfig 35 } 36 37 // DefaultOpt is the default values used for Options 38 var DefaultOpt = Options{ 39 Auth: libhttp.DefaultAuthCfg(), 40 HTTP: libhttp.DefaultCfg(), 41 Template: libhttp.DefaultTemplateCfg(), 42 } 43 44 // Opt is options set by command line flags 45 var Opt = DefaultOpt 46 47 // flagPrefix is the prefix used to uniquely identify command line flags. 48 // It is intentionally empty for this package. 49 const flagPrefix = "" 50 51 func init() { 52 flagSet := Command.Flags() 53 libhttp.AddAuthFlagsPrefix(flagSet, flagPrefix, &Opt.Auth) 54 libhttp.AddHTTPFlagsPrefix(flagSet, flagPrefix, &Opt.HTTP) 55 libhttp.AddTemplateFlagsPrefix(flagSet, flagPrefix, &Opt.Template) 56 vfsflags.AddFlags(flagSet) 57 proxyflags.AddFlags(flagSet) 58 } 59 60 // Command definition for cobra 61 var Command = &cobra.Command{ 62 Use: "http remote:path", 63 Short: `Serve the remote over HTTP.`, 64 Long: `Run a basic web server to serve a remote over HTTP. 65 This can be viewed in a web browser or you can make a remote of type 66 http read from it. 67 68 You can use the filter flags (e.g. ` + "`--include`, `--exclude`" + `) to control what 69 is served. 70 71 The server will log errors. Use ` + "`-v`" + ` to see access logs. 72 73 ` + "`--bwlimit`" + ` will be respected for file transfers. Use ` + "`--stats`" + ` to 74 control the stats printing. 75 ` + libhttp.Help(flagPrefix) + libhttp.TemplateHelp(flagPrefix) + libhttp.AuthHelp(flagPrefix) + vfs.Help + proxy.Help, 76 Annotations: map[string]string{ 77 "versionIntroduced": "v1.39", 78 "groups": "Filter", 79 }, 80 Run: func(command *cobra.Command, args []string) { 81 var f fs.Fs 82 if proxyflags.Opt.AuthProxy == "" { 83 cmd.CheckArgs(1, 1, command, args) 84 f = cmd.NewFsSrc(args) 85 } else { 86 cmd.CheckArgs(0, 0, command, args) 87 } 88 89 cmd.Run(false, true, command, func() error { 90 s, err := run(context.Background(), f, Opt) 91 if err != nil { 92 log.Fatal(err) 93 } 94 95 s.server.Wait() 96 return nil 97 }) 98 }, 99 } 100 101 // HTTP contains everything to run the server 102 type HTTP struct { 103 f fs.Fs 104 _vfs *vfs.VFS // don't use directly, use getVFS 105 server *libhttp.Server 106 opt Options 107 proxy *proxy.Proxy 108 ctx context.Context // for global config 109 } 110 111 // Gets the VFS in use for this request 112 func (s *HTTP) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) { 113 if s._vfs != nil { 114 return s._vfs, nil 115 } 116 value := libhttp.CtxGetAuth(ctx) 117 if value == nil { 118 return nil, errors.New("no VFS found in context") 119 } 120 VFS, ok := value.(*vfs.VFS) 121 if !ok { 122 return nil, fmt.Errorf("context value is not VFS: %#v", value) 123 } 124 return VFS, nil 125 } 126 127 // auth does proxy authorization 128 func (s *HTTP) auth(user, pass string) (value interface{}, err error) { 129 VFS, _, err := s.proxy.Call(user, pass, false) 130 if err != nil { 131 return nil, err 132 } 133 return VFS, err 134 } 135 136 func run(ctx context.Context, f fs.Fs, opt Options) (s *HTTP, err error) { 137 s = &HTTP{ 138 f: f, 139 ctx: ctx, 140 opt: opt, 141 } 142 143 if proxyflags.Opt.AuthProxy != "" { 144 s.proxy = proxy.New(ctx, &proxyflags.Opt) 145 // override auth 146 s.opt.Auth.CustomAuthFn = s.auth 147 } else { 148 s._vfs = vfs.New(f, &vfsflags.Opt) 149 } 150 151 s.server, err = libhttp.NewServer(ctx, 152 libhttp.WithConfig(s.opt.HTTP), 153 libhttp.WithAuth(s.opt.Auth), 154 libhttp.WithTemplate(s.opt.Template), 155 ) 156 if err != nil { 157 return nil, fmt.Errorf("failed to init server: %w", err) 158 } 159 160 router := s.server.Router() 161 router.Use( 162 middleware.SetHeader("Accept-Ranges", "bytes"), 163 middleware.SetHeader("Server", "rclone/"+fs.Version), 164 ) 165 router.Get("/*", s.handler) 166 router.Head("/*", s.handler) 167 168 s.server.Serve() 169 170 return s, nil 171 } 172 173 // handler reads incoming requests and dispatches them 174 func (s *HTTP) handler(w http.ResponseWriter, r *http.Request) { 175 isDir := strings.HasSuffix(r.URL.Path, "/") 176 remote := strings.Trim(r.URL.Path, "/") 177 if isDir { 178 s.serveDir(w, r, remote) 179 } else { 180 s.serveFile(w, r, remote) 181 } 182 } 183 184 // serveDir serves a directory index at dirRemote 185 func (s *HTTP) serveDir(w http.ResponseWriter, r *http.Request, dirRemote string) { 186 VFS, err := s.getVFS(r.Context()) 187 if err != nil { 188 http.Error(w, "Root directory not found", http.StatusNotFound) 189 fs.Errorf(nil, "Failed to serve directory: %v", err) 190 return 191 } 192 // List the directory 193 node, err := VFS.Stat(dirRemote) 194 if err == vfs.ENOENT { 195 http.Error(w, "Directory not found", http.StatusNotFound) 196 return 197 } else if err != nil { 198 serve.Error(dirRemote, w, "Failed to list directory", err) 199 return 200 } 201 if !node.IsDir() { 202 http.Error(w, "Not a directory", http.StatusNotFound) 203 return 204 } 205 dir := node.(*vfs.Dir) 206 dirEntries, err := dir.ReadDirAll() 207 if err != nil { 208 serve.Error(dirRemote, w, "Failed to list directory", err) 209 return 210 } 211 212 // Make the entries for display 213 directory := serve.NewDirectory(dirRemote, s.server.HTMLTemplate()) 214 for _, node := range dirEntries { 215 if vfsflags.Opt.NoModTime { 216 directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{}) 217 } else { 218 directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), node.ModTime().UTC()) 219 } 220 } 221 222 sortParm := r.URL.Query().Get("sort") 223 orderParm := r.URL.Query().Get("order") 224 directory.ProcessQueryParams(sortParm, orderParm) 225 226 // Set the Last-Modified header to the timestamp 227 w.Header().Set("Last-Modified", dir.ModTime().UTC().Format(http.TimeFormat)) 228 229 directory.Serve(w, r) 230 } 231 232 // serveFile serves a file object at remote 233 func (s *HTTP) serveFile(w http.ResponseWriter, r *http.Request, remote string) { 234 VFS, err := s.getVFS(r.Context()) 235 if err != nil { 236 http.Error(w, "File not found", http.StatusNotFound) 237 fs.Errorf(nil, "Failed to serve file: %v", err) 238 return 239 } 240 241 node, err := VFS.Stat(remote) 242 if err == vfs.ENOENT { 243 fs.Infof(remote, "%s: File not found", r.RemoteAddr) 244 http.Error(w, "File not found", http.StatusNotFound) 245 return 246 } else if err != nil { 247 serve.Error(remote, w, "Failed to find file", err) 248 return 249 } 250 if !node.IsFile() { 251 http.Error(w, "Not a file", http.StatusNotFound) 252 return 253 } 254 entry := node.DirEntry() 255 if entry == nil { 256 http.Error(w, "Can't open file being written", http.StatusNotFound) 257 return 258 } 259 obj := entry.(fs.Object) 260 file := node.(*vfs.File) 261 262 // Set content length if we know how long the object is 263 knownSize := obj.Size() >= 0 264 if knownSize { 265 w.Header().Set("Content-Length", strconv.FormatInt(node.Size(), 10)) 266 } 267 268 // Set content type 269 mimeType := fs.MimeType(r.Context(), obj) 270 if mimeType == "application/octet-stream" && path.Ext(remote) == "" { 271 // Leave header blank so http server guesses 272 } else { 273 w.Header().Set("Content-Type", mimeType) 274 } 275 276 // Set the Last-Modified header to the timestamp 277 w.Header().Set("Last-Modified", file.ModTime().UTC().Format(http.TimeFormat)) 278 279 // If HEAD no need to read the object since we have set the headers 280 if r.Method == "HEAD" { 281 return 282 } 283 284 // open the object 285 in, err := file.Open(os.O_RDONLY) 286 if err != nil { 287 serve.Error(remote, w, "Failed to open file", err) 288 return 289 } 290 defer func() { 291 err := in.Close() 292 if err != nil { 293 fs.Errorf(remote, "Failed to close file: %v", err) 294 } 295 }() 296 297 // Account the transfer 298 tr := accounting.Stats(r.Context()).NewTransfer(obj) 299 defer tr.Done(r.Context(), nil) 300 // FIXME in = fs.NewAccount(in, obj).WithBuffer() // account the transfer 301 302 // Serve the file 303 if knownSize { 304 http.ServeContent(w, r, remote, node.ModTime(), in) 305 } else { 306 // http.ServeContent can't serve unknown length files 307 if rangeRequest := r.Header.Get("Range"); rangeRequest != "" { 308 http.Error(w, "Can't use Range: on files of unknown length", http.StatusRequestedRangeNotSatisfiable) 309 return 310 } 311 n, err := io.Copy(w, in) 312 if err != nil { 313 fs.Errorf(obj, "Didn't finish writing GET request (wrote %d/unknown bytes): %v", n, err) 314 return 315 } 316 } 317 318 }