1// Package httplib provides common functionality for http servers 2// 3// Deprecated: httplib has been replaced with lib/http 4package httplib 5 6import ( 7 "context" 8 "crypto/tls" 9 "crypto/x509" 10 "encoding/base64" 11 "fmt" 12 "html/template" 13 "io/ioutil" 14 "log" 15 "net" 16 "net/http" 17 "strings" 18 "time" 19 20 auth "github.com/abbot/go-http-auth" 21 "github.com/pkg/errors" 22 "github.com/rclone/rclone/cmd/serve/http/data" 23 "github.com/rclone/rclone/fs" 24) 25 26// Globals 27var () 28 29// Help contains text describing the http server to add to the command 30// help. 31var Help = ` 32### Server options 33 34Use --addr to specify which IP address and port the server should 35listen on, e.g. --addr 1.2.3.4:8000 or --addr :8080 to listen to all 36IPs. By default it only listens on localhost. You can use port 37:0 to let the OS choose an available port. 38 39If you set --addr to listen on a public or LAN accessible IP address 40then using Authentication is advised - see the next section for info. 41 42--server-read-timeout and --server-write-timeout can be used to 43control the timeouts on the server. Note that this is the total time 44for a transfer. 45 46--max-header-bytes controls the maximum number of bytes the server will 47accept in the HTTP header. 48 49--baseurl controls the URL prefix that rclone serves from. By default 50rclone will serve from the root. If you used --baseurl "/rclone" then 51rclone would serve from a URL starting with "/rclone/". This is 52useful if you wish to proxy rclone serve. Rclone automatically 53inserts leading and trailing "/" on --baseurl, so --baseurl "rclone", 54--baseurl "/rclone" and --baseurl "/rclone/" are all treated 55identically. 56 57--template allows a user to specify a custom markup template for http 58and webdav serve functions. The server exports the following markup 59to be used within the template to server pages: 60 61| Parameter | Description | 62| :---------- | :---------- | 63| .Name | The full path of a file/directory. | 64| .Title | Directory listing of .Name | 65| .Sort | The current sort used. This is changeable via ?sort= parameter | 66| | Sort Options: namedirfirst,name,size,time (default namedirfirst) | 67| .Order | The current ordering used. This is changeable via ?order= parameter | 68| | Order Options: asc,desc (default asc) | 69| .Query | Currently unused. | 70| .Breadcrumb | Allows for creating a relative navigation | 71|-- .Link | The relative to the root link of the Text. | 72|-- .Text | The Name of the directory. | 73| .Entries | Information about a specific file/directory. | 74|-- .URL | The 'url' of an entry. | 75|-- .Leaf | Currently same as 'URL' but intended to be 'just' the name. | 76|-- .IsDir | Boolean for if an entry is a directory or not. | 77|-- .Size | Size in Bytes of the entry. | 78|-- .ModTime | The UTC timestamp of an entry. | 79 80#### Authentication 81 82By default this will serve files without needing a login. 83 84You can either use an htpasswd file which can take lots of users, or 85set a single username and password with the --user and --pass flags. 86 87Use --htpasswd /path/to/htpasswd to provide an htpasswd file. This is 88in standard apache format and supports MD5, SHA1 and BCrypt for basic 89authentication. Bcrypt is recommended. 90 91To create an htpasswd file: 92 93 touch htpasswd 94 htpasswd -B htpasswd user 95 htpasswd -B htpasswd anotherUser 96 97The password file can be updated while rclone is running. 98 99Use --realm to set the authentication realm. 100 101#### SSL/TLS 102 103By default this will serve over http. If you want you can serve over 104https. You will need to supply the --cert and --key flags. If you 105wish to do client side certificate validation then you will need to 106supply --client-ca also. 107 108--cert should be either a PEM encoded certificate or a concatenation 109of that with the CA certificate. --key should be the PEM encoded 110private key and --client-ca should be the PEM encoded client 111certificate authority certificate. 112` 113 114// Options contains options for the http Server 115type Options struct { 116 ListenAddr string // Port to listen on 117 BaseURL string // prefix to strip from URLs 118 ServerReadTimeout time.Duration // Timeout for server reading data 119 ServerWriteTimeout time.Duration // Timeout for server writing data 120 MaxHeaderBytes int // Maximum size of request header 121 SslCert string // SSL PEM key (concatenation of certificate and CA certificate) 122 SslKey string // SSL PEM Private key 123 ClientCA string // Client certificate authority to verify clients with 124 HtPasswd string // htpasswd file - if not provided no authentication is done 125 Realm string // realm for authentication 126 BasicUser string // single username for basic auth if not using Htpasswd 127 BasicPass string // password for BasicUser 128 Auth AuthFn `json:"-"` // custom Auth (not set by command line flags) 129 Template string // User specified template 130} 131 132// AuthFn if used will be used to authenticate user, pass. If an error 133// is returned then the user is not authenticated. 134// 135// If a non nil value is returned then it is added to the context under the key 136type AuthFn func(user, pass string) (value interface{}, err error) 137 138// DefaultOpt is the default values used for Options 139var DefaultOpt = Options{ 140 ListenAddr: "localhost:8080", 141 Realm: "rclone", 142 ServerReadTimeout: 1 * time.Hour, 143 ServerWriteTimeout: 1 * time.Hour, 144 MaxHeaderBytes: 4096, 145} 146 147// Server contains info about the running http server 148type Server struct { 149 Opt Options 150 handler http.Handler // original handler 151 listener net.Listener 152 waitChan chan struct{} // for waiting on the listener to close 153 httpServer *http.Server 154 basicPassHashed string 155 useSSL bool // if server is configured for SSL/TLS 156 usingAuth bool // set if authentication is configured 157 HTMLTemplate *template.Template // HTML template for web interface 158} 159 160type contextUserType struct{} 161 162// ContextUserKey is a simple context key for storing the username of the request 163var ContextUserKey = &contextUserType{} 164 165type contextAuthType struct{} 166 167// ContextAuthKey is a simple context key for storing info returned by AuthFn 168var ContextAuthKey = &contextAuthType{} 169 170// singleUserProvider provides the encrypted password for a single user 171func (s *Server) singleUserProvider(user, realm string) string { 172 if user == s.Opt.BasicUser { 173 return s.basicPassHashed 174 } 175 return "" 176} 177 178// parseAuthorization parses the Authorization header into user, pass 179// it returns a boolean as to whether the parse was successful 180func parseAuthorization(r *http.Request) (user, pass string, ok bool) { 181 authHeader := r.Header.Get("Authorization") 182 if authHeader != "" { 183 s := strings.SplitN(authHeader, " ", 2) 184 if len(s) == 2 && s[0] == "Basic" { 185 b, err := base64.StdEncoding.DecodeString(s[1]) 186 if err == nil { 187 parts := strings.SplitN(string(b), ":", 2) 188 user = parts[0] 189 if len(parts) > 1 { 190 pass = parts[1] 191 ok = true 192 } 193 } 194 } 195 } 196 return 197} 198 199// NewServer creates an http server. The opt can be nil in which case 200// the default options will be used. 201func NewServer(handler http.Handler, opt *Options) *Server { 202 s := &Server{ 203 handler: handler, 204 } 205 206 // Make a copy of the options 207 if opt != nil { 208 s.Opt = *opt 209 } else { 210 s.Opt = DefaultOpt 211 } 212 213 // Use htpasswd if required on everything 214 if s.Opt.HtPasswd != "" || s.Opt.BasicUser != "" || s.Opt.Auth != nil { 215 var authenticator *auth.BasicAuth 216 if s.Opt.Auth == nil { 217 var secretProvider auth.SecretProvider 218 if s.Opt.HtPasswd != "" { 219 fs.Infof(nil, "Using %q as htpasswd storage", s.Opt.HtPasswd) 220 secretProvider = auth.HtpasswdFileProvider(s.Opt.HtPasswd) 221 } else { 222 fs.Infof(nil, "Using --user %s --pass XXXX as authenticated user", s.Opt.BasicUser) 223 s.basicPassHashed = string(auth.MD5Crypt([]byte(s.Opt.BasicPass), []byte("dlPL2MqE"), []byte("$1$"))) 224 secretProvider = s.singleUserProvider 225 } 226 authenticator = auth.NewBasicAuthenticator(s.Opt.Realm, secretProvider) 227 } 228 oldHandler := handler 229 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 230 // No auth wanted for OPTIONS method 231 if r.Method == "OPTIONS" { 232 oldHandler.ServeHTTP(w, r) 233 return 234 } 235 unauthorized := func() { 236 w.Header().Set("Content-Type", "text/plain") 237 w.Header().Set("WWW-Authenticate", `Basic realm="`+s.Opt.Realm+`"`) 238 http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 239 } 240 user, pass, authValid := parseAuthorization(r) 241 if !authValid { 242 unauthorized() 243 return 244 } 245 if s.Opt.Auth == nil { 246 if username := authenticator.CheckAuth(r); username == "" { 247 fs.Infof(r.URL.Path, "%s: Unauthorized request from %s", r.RemoteAddr, user) 248 unauthorized() 249 return 250 } 251 } else { 252 // Custom Auth 253 value, err := s.Opt.Auth(user, pass) 254 if err != nil { 255 fs.Infof(r.URL.Path, "%s: Auth failed from %s: %v", r.RemoteAddr, user, err) 256 unauthorized() 257 return 258 } 259 if value != nil { 260 r = r.WithContext(context.WithValue(r.Context(), ContextAuthKey, value)) 261 } 262 } 263 r = r.WithContext(context.WithValue(r.Context(), ContextUserKey, user)) 264 oldHandler.ServeHTTP(w, r) 265 }) 266 s.usingAuth = true 267 } 268 269 s.useSSL = s.Opt.SslKey != "" 270 if (s.Opt.SslCert != "") != s.useSSL { 271 log.Fatalf("Need both -cert and -key to use SSL") 272 } 273 274 // If a Base URL is set then serve from there 275 s.Opt.BaseURL = strings.Trim(s.Opt.BaseURL, "/") 276 if s.Opt.BaseURL != "" { 277 s.Opt.BaseURL = "/" + s.Opt.BaseURL 278 } 279 280 // FIXME make a transport? 281 s.httpServer = &http.Server{ 282 Addr: s.Opt.ListenAddr, 283 Handler: handler, 284 ReadTimeout: s.Opt.ServerReadTimeout, 285 WriteTimeout: s.Opt.ServerWriteTimeout, 286 MaxHeaderBytes: s.Opt.MaxHeaderBytes, 287 ReadHeaderTimeout: 10 * time.Second, // time to send the headers 288 IdleTimeout: 60 * time.Second, // time to keep idle connections open 289 TLSConfig: &tls.Config{ 290 MinVersion: tls.VersionTLS10, // disable SSL v3.0 and earlier 291 }, 292 } 293 294 if s.Opt.ClientCA != "" { 295 if !s.useSSL { 296 log.Fatalf("Can't use --client-ca without --cert and --key") 297 } 298 certpool := x509.NewCertPool() 299 pem, err := ioutil.ReadFile(s.Opt.ClientCA) 300 if err != nil { 301 log.Fatalf("Failed to read client certificate authority: %v", err) 302 } 303 if !certpool.AppendCertsFromPEM(pem) { 304 log.Fatalf("Can't parse client certificate authority") 305 } 306 s.httpServer.TLSConfig.ClientCAs = certpool 307 s.httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert 308 } 309 310 htmlTemplate, templateErr := data.GetTemplate(s.Opt.Template) 311 if templateErr != nil { 312 log.Fatalf(templateErr.Error()) 313 } 314 s.HTMLTemplate = htmlTemplate 315 316 return s 317} 318 319// Serve runs the server - returns an error only if 320// the listener was not started; does not block, so 321// use s.Wait() to block on the listener indefinitely. 322func (s *Server) Serve() error { 323 ln, err := net.Listen("tcp", s.httpServer.Addr) 324 if err != nil { 325 return errors.Wrapf(err, "start server failed") 326 } 327 s.listener = ln 328 s.waitChan = make(chan struct{}) 329 go func() { 330 var err error 331 if s.useSSL { 332 // hacky hack to get this to work with old Go versions, which 333 // don't have ServeTLS on http.Server; see PR #2194. 334 type tlsServer interface { 335 ServeTLS(ln net.Listener, cert, key string) error 336 } 337 srvIface := interface{}(s.httpServer) 338 if tlsSrv, ok := srvIface.(tlsServer); ok { 339 // yay -- we get easy TLS support with HTTP/2 340 err = tlsSrv.ServeTLS(s.listener, s.Opt.SslCert, s.Opt.SslKey) 341 } else { 342 // oh well -- we can still do TLS but might not have HTTP/2 343 tlsConfig := new(tls.Config) 344 tlsConfig.Certificates = make([]tls.Certificate, 1) 345 tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(s.Opt.SslCert, s.Opt.SslKey) 346 if err != nil { 347 log.Printf("Error loading key pair: %v", err) 348 } 349 tlsLn := tls.NewListener(s.listener, tlsConfig) 350 err = s.httpServer.Serve(tlsLn) 351 } 352 } else { 353 err = s.httpServer.Serve(s.listener) 354 } 355 if err != nil { 356 log.Printf("Error on serving HTTP server: %v", err) 357 } 358 }() 359 return nil 360} 361 362// Wait blocks while the listener is open. 363func (s *Server) Wait() { 364 <-s.waitChan 365} 366 367// Close shuts the running server down 368func (s *Server) Close() { 369 err := s.httpServer.Close() 370 if err != nil { 371 log.Printf("Error on closing HTTP server: %v", err) 372 return 373 } 374 close(s.waitChan) 375} 376 377// URL returns the serving address of this server 378func (s *Server) URL() string { 379 proto := "http" 380 if s.useSSL { 381 proto = "https" 382 } 383 addr := s.Opt.ListenAddr 384 // prefer actual listener address if using ":port" or "addr:0" 385 useActualAddress := addr == "" || addr[0] == ':' || addr[len(addr)-1] == ':' || strings.HasSuffix(addr, ":0") 386 if s.listener != nil && useActualAddress { 387 // use actual listener address; required if using 0-port 388 // (i.e. port assigned by operating system) 389 addr = s.listener.Addr().String() 390 } 391 return fmt.Sprintf("%s://%s%s/", proto, addr, s.Opt.BaseURL) 392} 393 394// UsingAuth returns true if authentication is required 395func (s *Server) UsingAuth() bool { 396 return s.usingAuth 397} 398 399// Path returns the current path with the Prefix stripped 400// 401// If it returns false, then the path was invalid and the handler 402// should exit as the error response has already been sent 403func (s *Server) Path(w http.ResponseWriter, r *http.Request) (Path string, ok bool) { 404 Path = r.URL.Path 405 if s.Opt.BaseURL == "" { 406 return Path, true 407 } 408 if !strings.HasPrefix(Path, s.Opt.BaseURL+"/") { 409 // Send a redirect if the BaseURL was requested without a / 410 if Path == s.Opt.BaseURL { 411 http.Redirect(w, r, s.Opt.BaseURL+"/", http.StatusPermanentRedirect) 412 return Path, false 413 } 414 http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 415 return Path, false 416 } 417 Path = Path[len(s.Opt.BaseURL):] 418 return Path, true 419} 420