From 6d5bdff3942ce5e030b2cbd1510f418de25a1a53 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 3 Jun 2013 21:39:00 -0400 Subject: [PATCH 1/5] Add flag to enable cross domain requests in Api Add the -api-enable-cors flag when running docker in daemon mode to allow CORS requests to be made to the Remote Api. The default value is false for this flag to not allow cross origin request to be made. Also added a handler for OPTIONS requests the standard for cross domain requests is to initially make an OPTIONS request to the api. --- api.go | 13 +++++++++++++ api_test.go | 26 ++++++++++++++++++++++++++ docker/docker.go | 7 ++++--- docs/sources/api/docker_remote_api.rst | 8 ++++++++ server.go | 8 +++++--- 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/api.go b/api.go index 7666b79a5f..978cd296a5 100644 --- a/api.go +++ b/api.go @@ -703,6 +703,11 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ return nil } +func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") +} + func ListenAndServe(addr string, srv *Server, logging bool) error { r := mux.NewRouter() log.Printf("Listening for HTTP on %s\n", addr) @@ -773,12 +778,20 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { w.WriteHeader(http.StatusNotFound) return } + if srv.enableCors { + writeCorsHeaders(w, r) + } if err := localFct(srv, version, w, r, mux.Vars(r)); err != nil { httpError(w, err) } } r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) r.Path(localRoute).Methods(localMethod).HandlerFunc(f) + r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if srv.enableCors { + writeCorsHeaders(w, r) + } + }) } } return http.ListenAndServe(addr, r) diff --git a/api_test.go b/api_test.go index 9121167e10..e464e51108 100644 --- a/api_test.go +++ b/api_test.go @@ -1239,6 +1239,32 @@ func TestDeleteContainers(t *testing.T) { } } +func TestGetEnabledCors(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime, enableCors: true} + + r := httptest.NewRecorder() + + if err := getVersion(srv, API_VERSION, r, nil, nil); err != nil { + t.Fatal(err) + } + + allowOrigin := r.Header().Get("Access-Control-Allow-Origin") + allowHeaders := r.Header().Get("Access-Control-Allow-Headers") + + if allowOrigin != "*" { + t.Errorf("Expected header Access-Control-Allow-Origin to be \"*\", %s found.", allowOrigin) + } + if allowHeaders != "Origin, X-Requested-With, Content-Type, Accept" { + t.Errorf("Expected header Access-Control-Allow-Headers to be \"Origin, X-Requested-With, Content-Type, Accept\", %s found.", allowHeaders) + } +} + func TestDeleteImages(t *testing.T) { //FIXME: Implement this test t.Log("Test not implemented") diff --git a/docker/docker.go b/docker/docker.go index 7b8aa7f858..dd804f81c0 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -33,6 +33,7 @@ func main() { bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge") pidfile := flag.String("p", "/var/run/docker.pid", "File containing process PID") flHost := flag.String("H", fmt.Sprintf("%s:%d", host, port), "Host:port to bind/connect to") + flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.") flag.Parse() if *bridgeName != "" { docker.NetworkBridgeIface = *bridgeName @@ -65,7 +66,7 @@ func main() { flag.Usage() return } - if err := daemon(*pidfile, host, port, *flAutoRestart); err != nil { + if err := daemon(*pidfile, host, port, *flAutoRestart, *flEnableCors); err != nil { log.Fatal(err) os.Exit(-1) } @@ -104,7 +105,7 @@ func removePidFile(pidfile string) { } } -func daemon(pidfile, addr string, port int, autoRestart bool) error { +func daemon(pidfile, addr string, port int, autoRestart, enableCors bool) error { if addr != "127.0.0.1" { log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") } @@ -122,7 +123,7 @@ func daemon(pidfile, addr string, port int, autoRestart bool) error { os.Exit(0) }() - server, err := docker.NewServer(autoRestart) + server, err := docker.NewServer(autoRestart, enableCors) if err != nil { return err } diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index dca4599c55..e59b93d621 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -1056,3 +1056,11 @@ Here are the steps of 'docker run' : In this first version of the API, some of the endpoints, like /attach, /pull or /push uses hijacking to transport stdin, stdout and stderr on the same socket. This might change in the future. + + +3.3 CORS Requests +----------------- + +To enable cross origin requests to the remote api add the flag "-api-enable-cors" when running docker in daemon mode. + + docker -d -H="192.168.1.9:4243" -api-enable-cors diff --git a/server.go b/server.go index 08cb37a72e..d86ffe0f44 100644 --- a/server.go +++ b/server.go @@ -870,7 +870,7 @@ func (srv *Server) ImageInspect(name string) (*Image, error) { return nil, fmt.Errorf("No such image: %s", name) } -func NewServer(autoRestart bool) (*Server, error) { +func NewServer(autoRestart, enableCors bool) (*Server, error) { if runtime.GOARCH != "amd64" { log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH) } @@ -879,12 +879,14 @@ func NewServer(autoRestart bool) (*Server, error) { return nil, err } srv := &Server{ - runtime: runtime, + runtime: runtime, + enableCors: enableCors, } runtime.srv = srv return srv, nil } type Server struct { - runtime *Runtime + runtime *Runtime + enableCors bool } From 393e873d25093f579d1a293bc473007b04f3c239 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Sun, 9 Jun 2013 17:17:35 -0900 Subject: [PATCH 2/5] Add Access-Control-Allow-Methods header Add the Access-Control-Allow-Methods header so that DELETE operations are allowed. Also move the write CORS headers method before docker writes a 404 not found so that the client receives the correct response and not an invalid CORS request. --- api.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api.go b/api.go index 978cd296a5..3831f326f2 100644 --- a/api.go +++ b/api.go @@ -706,6 +706,7 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") + w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS") } func ListenAndServe(addr string, srv *Server, logging bool) error { @@ -774,13 +775,13 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { if err != nil { version = API_VERSION } + if srv.enableCors { + writeCorsHeaders(w, r) + } if version == 0 || version > API_VERSION { w.WriteHeader(http.StatusNotFound) return } - if srv.enableCors { - writeCorsHeaders(w, r) - } if err := localFct(srv, version, w, r, mux.Vars(r)); err != nil { httpError(w, err) } From 0a28628c02d486512dc7e62eb54ccfd27eaa0a27 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 10 Jun 2013 13:02:40 -0900 Subject: [PATCH 3/5] Add Cors and OPTIONS route unit tests Move creating the router and populating the routes to a separate function outside of ListenAndServe to allow unit tests to make assertions on the configured routes and handler funcs. --- api.go | 23 ++++++++++++++++------- api_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/api.go b/api.go index 3831f326f2..cc00849a60 100644 --- a/api.go +++ b/api.go @@ -709,9 +709,8 @@ func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS") } -func ListenAndServe(addr string, srv *Server, logging bool) error { +func createRouter(srv *Server, logging bool) (*mux.Router, error) { r := mux.NewRouter() - log.Printf("Listening for HTTP on %s\n", addr) m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{ "GET": { @@ -788,12 +787,22 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { } r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) r.Path(localRoute).Methods(localMethod).HandlerFunc(f) - r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if srv.enableCors { - writeCorsHeaders(w, r) - } - }) } } + r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if srv.enableCors { + writeCorsHeaders(w, r) + } + }) + return r, nil +} + +func ListenAndServe(addr string, srv *Server, logging bool) error { + log.Printf("Listening for HTTP on %s\n", addr) + + r, err := createRouter(srv, logging) + if err != nil { + return err + } return http.ListenAndServe(addr, r) } diff --git a/api_test.go b/api_test.go index e464e51108..748bcf812c 100644 --- a/api_test.go +++ b/api_test.go @@ -1239,6 +1239,32 @@ func TestDeleteContainers(t *testing.T) { } } +func TestOptionsRoute(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime, enableCors: true} + + r := httptest.NewRecorder() + router, err := createRouter(srv, false) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("OPTIONS", "/", nil) + if err != nil { + t.Fatal(err) + } + + router.ServeHTTP(r, req) + if r.Code != 200 { + t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) + } +} + func TestGetEnabledCors(t *testing.T) { runtime, err := newTestRuntime() if err != nil { @@ -1250,12 +1276,24 @@ func TestGetEnabledCors(t *testing.T) { r := httptest.NewRecorder() - if err := getVersion(srv, API_VERSION, r, nil, nil); err != nil { + router, err := createRouter(srv, false) + if err != nil { t.Fatal(err) } + req, err := http.NewRequest("GET", "/version", nil) + if err != nil { + t.Fatal(err) + } + + router.ServeHTTP(r, req) + if r.Code != 200 { + t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) + } + allowOrigin := r.Header().Get("Access-Control-Allow-Origin") allowHeaders := r.Header().Get("Access-Control-Allow-Headers") + allowMethods := r.Header().Get("Access-Control-Allow-Methods") if allowOrigin != "*" { t.Errorf("Expected header Access-Control-Allow-Origin to be \"*\", %s found.", allowOrigin) @@ -1263,6 +1301,9 @@ func TestGetEnabledCors(t *testing.T) { if allowHeaders != "Origin, X-Requested-With, Content-Type, Accept" { t.Errorf("Expected header Access-Control-Allow-Headers to be \"Origin, X-Requested-With, Content-Type, Accept\", %s found.", allowHeaders) } + if allowMethods != "GET, POST, DELETE, PUT, OPTIONS" { + t.Errorf("Expected hearder Access-Control-Allow-Methods to be \"GET, POST, DELETE, PUT, OPTIONS\", %s found.", allowMethods) + } } func TestDeleteImages(t *testing.T) { From ac599d652846f6456366b8028b2c38da0565d8b1 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 10 Jun 2013 14:44:10 -0900 Subject: [PATCH 4/5] Add explicit status response to OPTIONS handler Write the http.StatusOK header in the OPTIONS handler and update the unit tests to refer to the response code using the const from the http package. --- api.go | 1 + api_test.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api.go b/api.go index cc00849a60..681b719c1a 100644 --- a/api.go +++ b/api.go @@ -793,6 +793,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { if srv.enableCors { writeCorsHeaders(w, r) } + w.WriteHeader(http.StatusOK) }) return r, nil } diff --git a/api_test.go b/api_test.go index 748bcf812c..62da94113a 100644 --- a/api_test.go +++ b/api_test.go @@ -1260,7 +1260,7 @@ func TestOptionsRoute(t *testing.T) { } router.ServeHTTP(r, req) - if r.Code != 200 { + if r.Code != http.StatusOK { t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) } } @@ -1287,7 +1287,7 @@ func TestGetEnabledCors(t *testing.T) { } router.ServeHTTP(r, req) - if r.Code != 200 { + if r.Code != http.StatusOK { t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) } From dd53c457d75a49e6e140c6d71642b237f3ee9056 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 10 Jun 2013 16:10:40 -0900 Subject: [PATCH 5/5] Add OPTIONS to route map Move the OPTIONS method registration into the existing route map. Also add support for empty paths in the map. --- api.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/api.go b/api.go index 681b719c1a..29ff76171b 100644 --- a/api.go +++ b/api.go @@ -703,6 +703,10 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ return nil } +func optionsHandler(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + w.WriteHeader(http.StatusOK) + return nil +} func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") @@ -750,6 +754,9 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { "/containers/{name:.*}": deleteContainers, "/images/{name:.*}": deleteImages, }, + "OPTIONS": { + "": optionsHandler, + }, } for method, routes := range m { @@ -785,16 +792,15 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { httpError(w, err) } } - r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) - r.Path(localRoute).Methods(localMethod).HandlerFunc(f) + + if localRoute == "" { + r.Methods(localMethod).HandlerFunc(f) + } else { + r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) + r.Path(localRoute).Methods(localMethod).HandlerFunc(f) + } } } - r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if srv.enableCors { - writeCorsHeaders(w, r) - } - w.WriteHeader(http.StatusOK) - }) return r, nil }