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  }