diff --git a/api/restapi/restapi.go b/api/restapi/restapi.go index 5fbf53403..234baa43c 100644 --- a/api/restapi/restapi.go +++ b/api/restapi/restapi.go @@ -56,6 +56,11 @@ type RESTAPI struct { wg sync.WaitGroup } +type Config struct { + TLS *tls.Config + BasicAuthCreds map[string]string +} + type route struct { Name string Method string @@ -70,33 +75,26 @@ type peerAddBody struct { // NewRESTAPI creates a new REST API component. It receives // the multiaddress on which the API listens. func NewRESTAPI(apiMAddr ma.Multiaddr) (*RESTAPI, error) { - n, addr, err := manet.DialArgs(apiMAddr) - if err != nil { - return nil, err - } - l, err := net.Listen(n, addr) - if err != nil { - return nil, err - } - return newRESTAPI(apiMAddr, l) + return NewRESTAPIWithConfig(apiMAddr, &Config{}) } -// NewTlsRESTAPI creates a new REST API component that uses TLS for security -// (authentication, encryption). It receives the multiaddress on which the API -// listens, as well as paths to certificate and private key files -func NewTLSRESTAPI(apiMAddr ma.Multiaddr, tlsCfg *tls.Config) (*RESTAPI, error) { +// NewRESTAPI creates a new REST API component. It receives +// the multiaddress on which the API listens. +func NewRESTAPIWithConfig(apiMAddr ma.Multiaddr, cfg *Config) (*RESTAPI, error) { n, addr, err := manet.DialArgs(apiMAddr) if err != nil { return nil, err } - l, err := tls.Listen(n, addr, tlsCfg) + var l net.Listener + if cfg.TLS != nil { + l, err = tls.Listen(n, addr, cfg.TLS) + } else { + l, err = net.Listen(n, addr) + } if err != nil { return nil, err } - return newRESTAPI(apiMAddr, l) -} -func newRESTAPI(apiMAddr ma.Multiaddr, l net.Listener) (*RESTAPI, error) { router := mux.NewRouter().StrictSlash(true) s := &http.Server{ ReadTimeout: RESTAPIServerReadTimeout, @@ -116,18 +114,46 @@ func newRESTAPI(apiMAddr ma.Multiaddr, l net.Listener) (*RESTAPI, error) { server: s, rpcReady: make(chan struct{}, 1), } + api.addRoutes(router, cfg.BasicAuthCreds) + api.run() + return api, nil +} + +func (api *RESTAPI) addRoutes(router *mux.Router, basicAuthCreds map[string]string) { for _, route := range api.routes() { + if basicAuthCreds != nil { + route.HandlerFunc = basicAuth(route.HandlerFunc, basicAuthCreds) + } router. Methods(route.Method). Path(route.Pattern). Name(route.Name). Handler(route.HandlerFunc) } - api.router = router - api.run() - return api, nil +} + +func basicAuth(h http.HandlerFunc, credentials map[string]string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + username, password, ok := r.BasicAuth() + if !ok { + http.Error(w, "Not authorized", 401) + return + } + authorized := false + for u, p := range credentials { + if u == username && p == password { + authorized = true + } + } + if !authorized { + http.Error(w, "Not authorized", 401) + return + } + h.ServeHTTP(w, r) + } } func (rest *RESTAPI) routes() []route { diff --git a/config.go b/config.go index 1f1a473f0..e3d09fbda 100644 --- a/config.go +++ b/config.go @@ -74,6 +74,12 @@ type Config struct { // SSLCertFile. SSLKeyFile string + // BasicAuthCredentials is a map of username, password pairs used to authorize + // access to the REST API. It is *highly recommended* that you use this in + // conjunction with SSL, as the username/password sent by the client are not + // encrypted when using HTTP without SSL. + BasicAuthCredentials map[string]string + // Listen parameters for the IPFS Proxy. Used by the IPFS // connector component. IPFSProxyAddr ma.Multiaddr @@ -158,6 +164,12 @@ type JSONConfig struct { // SSLCertFile. SSLKeyFile string `json:"ssl_key_file,omitempty"` + // BasicAuthCredentials is a map of username, password pairs used to authorize + // access to the REST API. It is *highly recommended* that you use this in + // conjunction with SSL, as the username/password sent by the client are not + // encrypted when using HTTP without SSL. + BasicAuthCredentials map[string]string `json:"basic_auth_credentials"` + // Listen address for the IPFS Proxy, which forwards requests to // an IPFS daemon. IPFSProxyListenMultiaddress string `json:"ipfs_proxy_listen_multiaddress"` @@ -234,6 +246,7 @@ func (cfg *Config) ToJSONConfig() (j *JSONConfig, err error) { APIListenMultiaddress: cfg.APIAddr.String(), SSLCertFile: cfg.SSLCertFile, SSLKeyFile: cfg.SSLKeyFile, + BasicAuthCredentials: cfg.BasicAuthCredentials, IPFSProxyListenMultiaddress: cfg.IPFSProxyAddr.String(), IPFSNodeMultiaddress: cfg.IPFSNodeAddr.String(), ConsensusDataFolder: cfg.ConsensusDataFolder, @@ -347,6 +360,7 @@ func (jcfg *JSONConfig) ToConfig() (c *Config, err error) { APIAddr: apiAddr, SSLCertFile: jcfg.SSLCertFile, SSLKeyFile: jcfg.SSLKeyFile, + BasicAuthCredentials: jcfg.BasicAuthCredentials, IPFSProxyAddr: ipfsProxyAddr, IPFSNodeAddr: ipfsNodeAddr, ConsensusDataFolder: jcfg.ConsensusDataFolder, diff --git a/ipfs-cluster-service/main.go b/ipfs-cluster-service/main.go index d2a79a5fe..53c4e684e 100644 --- a/ipfs-cluster-service/main.go +++ b/ipfs-cluster-service/main.go @@ -276,13 +276,13 @@ func run(c *cli.Context) error { } var api *restapi.RESTAPI + var tlsCfg *tls.Config if len(cfg.SSLCertFile) != 0 || len(cfg.SSLKeyFile) != 0 { - tlsCfg, err := newTLSConfig(cfg.SSLCertFile, cfg.SSLKeyFile) + tlsCfg, err = newTLSConfig(cfg.SSLCertFile, cfg.SSLKeyFile) checkErr("creating TLS config: ", err) - api, err = restapi.NewTLSRESTAPI(cfg.APIAddr, tlsCfg) - } else { - api, err = restapi.NewRESTAPI(cfg.APIAddr) } + apiConfig := &restapi.Config{TLS: tlsCfg, BasicAuthCreds: cfg.BasicAuthCredentials} + api, err = restapi.NewRESTAPIWithConfig(cfg.APIAddr, apiConfig) checkErr("creating REST API component", err) proxy, err := ipfshttp.NewConnector(