diff --git a/etcd.go b/etcd.go index 6bc036724..775d6a184 100644 --- a/etcd.go +++ b/etcd.go @@ -45,7 +45,6 @@ var ( cpuprofile string cors string - corsList map[string]bool ) func init() { @@ -212,20 +211,3 @@ func main() { } -// parseCorsFlag gathers up the cors whitelist and puts it into the corsList. -func parseCorsFlag() { - if cors != "" { - corsList = make(map[string]bool) - list := strings.Split(cors, ",") - for _, v := range list { - fmt.Println(v) - if v != "*" { - _, err := url.Parse(v) - if err != nil { - panic(fmt.Sprintf("bad cors url: %s", err)) - } - } - corsList[v] = true - } - } -} diff --git a/etcd_handler_v1.go b/etcd_handler_v1.go deleted file mode 100644 index 987b2e794..000000000 --- a/etcd_handler_v1.go +++ /dev/null @@ -1,250 +0,0 @@ -package main - -import ( - "encoding/json" - "net/http" - "strconv" - - etcdErr "github.com/coreos/etcd/error" - "github.com/coreos/etcd/store" - "github.com/coreos/go-raft" -) - -//------------------------------------------------------------------- -// Handlers to handle etcd-store related request via etcd url -//------------------------------------------------------------------- -// Multiplex GET/POST/DELETE request to corresponding handlers -func (e *etcdServer) MultiplexerV1(w http.ResponseWriter, req *http.Request) error { - - switch req.Method { - case "GET": - return e.GetHttpHandlerV1(w, req) - case "POST": - return e.SetHttpHandlerV1(w, req) - case "PUT": - return e.SetHttpHandlerV1(w, req) - case "DELETE": - return e.DeleteHttpHandlerV1(w, req) - default: - w.WriteHeader(http.StatusMethodNotAllowed) - return nil - } -} - -//-------------------------------------- -// State sensitive handlers -// Set/Delete will dispatch to leader -//-------------------------------------- - -// Set Command Handler -func (e *etcdServer) SetHttpHandlerV1(w http.ResponseWriter, req *http.Request) error { - key := req.URL.Path[len("/v1/keys/"):] - - debugf("[recv] POST %v/v1/keys/%s [%s]", e.url, key, req.RemoteAddr) - - req.ParseForm() - - value := req.Form.Get("value") - - if len(value) == 0 { - return etcdErr.NewError(200, "Set", store.UndefIndex, store.UndefTerm) - } - - strDuration := req.Form.Get("ttl") - - expireTime, err := durationToExpireTime(strDuration) - - if err != nil { - return etcdErr.NewError(202, "Set", store.UndefIndex, store.UndefTerm) - } - - if prevValueArr, ok := req.Form["prevValue"]; ok && len(prevValueArr) > 0 { - command := &TestAndSetCommand{ - Key: key, - Value: value, - PrevValue: prevValueArr[0], - ExpireTime: expireTime, - } - - return dispatchEtcdCommandV1(command, w, req) - - } else { - command := &CreateCommand{ - Key: key, - Value: value, - ExpireTime: expireTime, - Force: true, - } - - return dispatchEtcdCommandV1(command, w, req) - } -} - -// Delete Handler -func (e *etcdServer) DeleteHttpHandlerV1(w http.ResponseWriter, req *http.Request) error { - key := req.URL.Path[len("/v1/keys/"):] - - debugf("[recv] DELETE %v/v1/keys/%s [%s]", e.url, key, req.RemoteAddr) - - command := &DeleteCommand{ - Key: key, - } - - return dispatchEtcdCommandV1(command, w, req) -} - -//-------------------------------------- -// State non-sensitive handlers -// will not dispatch to leader -// TODO: add sensitive version for these -// command? -//-------------------------------------- - -// Get Handler -func (e *etcdServer) GetHttpHandlerV1(w http.ResponseWriter, req *http.Request) error { - key := req.URL.Path[len("/v1/keys/"):] - - r := e.raftServer - debugf("[recv] GET %s/v1/keys/%s [%s]", e.url, key, req.RemoteAddr) - - command := &GetCommand{ - Key: key, - } - - if event, err := command.Apply(r.Server); err != nil { - return err - } else { - event, _ := event.(*store.Event) - - response := eventToResponse(event) - bytes, _ := json.Marshal(response) - - w.WriteHeader(http.StatusOK) - - w.Write(bytes) - - return nil - } - -} - -// Watch handler -func (e *etcdServer) WatchHttpHandlerV1(w http.ResponseWriter, req *http.Request) error { - key := req.URL.Path[len("/v1/watch/"):] - - command := &WatchCommand{ - Key: key, - } - r := e.raftServer - if req.Method == "GET" { - debugf("[recv] GET %s/watch/%s [%s]", e.url, key, req.RemoteAddr) - command.SinceIndex = 0 - - } else if req.Method == "POST" { - // watch from a specific index - - debugf("[recv] POST %s/watch/%s [%s]", e.url, key, req.RemoteAddr) - content := req.FormValue("index") - - sinceIndex, err := strconv.ParseUint(string(content), 10, 64) - if err != nil { - return etcdErr.NewError(203, "Watch From Index", store.UndefIndex, store.UndefTerm) - } - command.SinceIndex = sinceIndex - - } else { - w.WriteHeader(http.StatusMethodNotAllowed) - return nil - } - - if event, err := command.Apply(r.Server); err != nil { - return etcdErr.NewError(500, key, store.UndefIndex, store.UndefTerm) - } else { - event, _ := event.(*store.Event) - - response := eventToResponse(event) - bytes, _ := json.Marshal(response) - - w.WriteHeader(http.StatusOK) - - w.Write(bytes) - return nil - } - -} - -// Dispatch the command to leader -func dispatchEtcdCommandV1(c Command, w http.ResponseWriter, req *http.Request) error { - return dispatchV1(c, w, req, nameToEtcdURL) -} - -func dispatchV1(c Command, w http.ResponseWriter, req *http.Request, toURL func(name string) (string, bool)) error { - r := e.raftServer - if r.State() == raft.Leader { - if event, err := r.Do(c); err != nil { - return err - } else { - if event == nil { - return etcdErr.NewError(300, "Empty result from raft", store.UndefIndex, store.UndefTerm) - } - - event, _ := event.(*store.Event) - - response := eventToResponse(event) - bytes, _ := json.Marshal(response) - - w.WriteHeader(http.StatusOK) - w.Write(bytes) - return nil - - } - - } else { - leader := r.Leader() - // current no leader - if leader == "" { - return etcdErr.NewError(300, "", store.UndefIndex, store.UndefTerm) - } - url, _ := toURL(leader) - - redirect(url, w, req) - - return nil - } -} - -func eventToResponse(event *store.Event) interface{} { - if !event.Dir { - response := &store.Response{ - Action: event.Action, - Key: event.Key, - Value: event.Value, - PrevValue: event.PrevValue, - Index: event.Index, - TTL: event.TTL, - Expiration: event.Expiration, - } - - if response.Action == store.Create || response.Action == store.Update { - response.Action = "set" - if response.PrevValue == "" { - response.NewKey = true - } - } - - return response - } else { - responses := make([]*store.Response, len(event.KVPairs)) - - for i, kv := range event.KVPairs { - responses[i] = &store.Response{ - Action: event.Action, - Key: kv.Key, - Value: kv.Value, - Dir: kv.Dir, - Index: event.Index, - } - } - return responses - } -} diff --git a/etcd_handlers.go b/etcd_handlers.go index 487c1cef5..0d294de7d 100644 --- a/etcd_handlers.go +++ b/etcd_handlers.go @@ -18,11 +18,11 @@ import ( func NewEtcdMuxer() *http.ServeMux { // external commands - etcdMux := http.NewServeMux() - etcdMux.Handle("/"+version+"/keys/", errorHandler(e.Multiplexer)) - etcdMux.Handle("/"+version+"/leader", errorHandler(e.LeaderHttpHandler)) - etcdMux.Handle("/"+version+"/machines", errorHandler(e.MachinesHttpHandler)) - etcdMux.Handle("/"+version+"/stats/", errorHandler(e.StatsHttpHandler)) + router := mux.NewRouter() + etcdMux.Handle("/v2/keys/", errorHandler(e.Multiplexer)) + etcdMux.Handle("/v2/leader", errorHandler(e.LeaderHttpHandler)) + etcdMux.Handle("/v2/machines", errorHandler(e.MachinesHttpHandler)) + etcdMux.Handle("/v2/stats/", errorHandler(e.StatsHttpHandler)) etcdMux.Handle("/version", errorHandler(e.VersionHttpHandler)) etcdMux.HandleFunc("/test/", TestHttpHandler) diff --git a/etcd_server.go b/etcd_server.go deleted file mode 100644 index 657c4f967..000000000 --- a/etcd_server.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "net/http" -) - -type etcdServer struct { - http.Server - raftServer *raftServer - name string - url string - tlsConf *TLSConfig - tlsInfo *TLSInfo -} - -var e *etcdServer - -func newEtcdServer(name string, urlStr string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo, raftServer *raftServer) *etcdServer { - e = &etcdServer{ - Server: http.Server{ - TLSConfig: &tlsConf.Server, - Addr: listenHost, - }, - name: name, - url: urlStr, - tlsConf: tlsConf, - tlsInfo: tlsInfo, - raftServer: raftServer, - } - e.Handler = NewEtcdMuxer() - return e -} - -// Start to listen and response etcd client command -func (e *etcdServer) ListenAndServe() { - - infof("etcd server [name %s, listen on %s, advertised url %s]", e.name, e.Server.Addr, e.url) - - if e.tlsConf.Scheme == "http" { - fatal(e.Server.ListenAndServe()) - } else { - fatal(e.Server.ListenAndServeTLS(e.tlsInfo.CertFile, e.tlsInfo.KeyFile)) - } -} diff --git a/server/server.go b/server/server.go new file mode 100644 index 000000000..8deb2fc24 --- /dev/null +++ b/server/server.go @@ -0,0 +1,98 @@ +package server + +import ( + "github.com/gorilla/mux" + "net/http" +) + +// The Server provides an HTTP interface to the underlying data store. +type Server struct { + http.Server + raftServer *raftServer + name string + url string + tlsConf *TLSConfig + tlsInfo *TLSInfo + corsOrigins map[string]bool +} + +// Creates a new Server. +func New(name string, urlStr string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo, raftServer *raftServer) *Server { + s := &etcdServer{ + Server: http.Server{ + Handler: mux.NewRouter(), + TLSConfig: &tlsConf.Server, + Addr: listenHost, + }, + name: name, + url: urlStr, + tlsConf: tlsConf, + tlsInfo: tlsInfo, + raftServer: raftServer, + } + + // TODO: Move to main.go. + // Install the routes for each version of the API. + // v1.Install(s) + // v2.Install(s) + + return s +} + +// Adds a server handler to the router. +func (s *Server) HandleFunc(path string, f func(http.ResponseWriter, *http.Request, *server.Server) error) *mux.Route { + r := s.Handler.(*mux.Router) + + // Wrap the standard HandleFunc interface to pass in the server reference. + return r.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) { + // Write CORS header. + if s.OriginAllowed("*") { + w.Header().Add("Access-Control-Allow-Origin", "*") + } else if s.OriginAllowed(r.Header.Get("Origin")) { + w.Header().Add("Access-Control-Allow-Origin", r.Header.Get("Origin")) + } + + // Execute handler function and return error if necessary. + if err := f(w, req, s); err != nil { + if etcdErr, ok := err.(*etcdErr.Error); ok { + debug("Return error: ", (*etcdErr).Error()) + etcdErr.Write(w) + } else { + http.Error(w, e.Error(), http.StatusInternalServerError) + } + } + }) +} + +// Start to listen and response etcd client command +func (s *Server) ListenAndServe() { + infof("etcd server [name %s, listen on %s, advertised url %s]", s.name, s.Server.Addr, s.url) + + if s.tlsConf.Scheme == "http" { + fatal(s.Server.ListenAndServe()) + } else { + fatal(s.Server.ListenAndServeTLS(s.tlsInfo.CertFile, s.tlsInfo.KeyFile)) + } +} + +// Sets a comma-delimited list of origins that are allowed. +func (s *Server) AllowOrigins(origins string) error { + // Construct a lookup of all origins. + m := make(map[string]bool) + for _, v := range strings.Split(cors, ",") { + if v != "*" { + if _, err := url.Parse(v); err != nil { + return fmt.Errorf("Invalid CORS origin: %s", err) + } + } + m[v] = true + } + s.origins = m + + return nil +} + +// Determines whether the server will allow a given CORS origin. +func (s *Server) OriginAllowed(origin string) { + return s.origins["*"] || s.origins[origin] +} diff --git a/server/v1/delete_key_handler.go b/server/v1/delete_key_handler.go new file mode 100644 index 000000000..d2d450b6a --- /dev/null +++ b/server/v1/delete_key_handler.go @@ -0,0 +1,13 @@ +package v1 + +func deleteKeyHandler(w http.ResponseWriter, req *http.Request, e *etcdServer) error { + key := req.URL.Path[len("/v1/keys/"):] + + debugf("[recv] DELETE %v/v1/keys/%s [%s]", e.url, key, req.RemoteAddr) + + command := &DeleteCommand{ + Key: key, + } + + return dispatchEtcdCommandV1(command, w, req) +} diff --git a/server/v1/dispatch.go b/server/v1/dispatch.go new file mode 100644 index 000000000..e1aa02922 --- /dev/null +++ b/server/v1/dispatch.go @@ -0,0 +1,42 @@ +package v1 + +// Dispatch the command to leader. +func dispatchCommand(c Command, w http.ResponseWriter, req *http.Request) error { + return dispatch(c, w, req, nameToEtcdURL) +} + +// Dispatches a command to a given URL. +func dispatch(c Command, w http.ResponseWriter, req *http.Request, toURL func(name string) (string, bool)) error { + r := e.raftServer + if r.State() == raft.Leader { + if event, err := r.Do(c); err != nil { + return err + } else { + if event == nil { + return etcdErr.NewError(300, "Empty result from raft", store.UndefIndex, store.UndefTerm) + } + + event, _ := event.(*store.Event) + + response := eventToResponse(event) + bytes, _ := json.Marshal(response) + + w.WriteHeader(http.StatusOK) + w.Write(bytes) + return nil + + } + + } else { + leader := r.Leader() + // current no leader + if leader == "" { + return etcdErr.NewError(300, "", store.UndefIndex, store.UndefTerm) + } + url, _ := toURL(leader) + + redirect(url, w, req) + + return nil + } +} diff --git a/server/v1/get_key_handler.go b/server/v1/get_key_handler.go new file mode 100644 index 000000000..391a47a7a --- /dev/null +++ b/server/v1/get_key_handler.go @@ -0,0 +1,31 @@ +package v1 + +import ( + "encoding/json" + "github.com/coreos/etcd/store" + "net/http" +) + +// Retrieves the value for a given key. +func getKeyHandler(w http.ResponseWriter, req *http.Request, e *etcdServer) error { + vars := mux.Vars(req) + key := "/" + vars["key"] + + debugf("[recv] GET %s/v1/keys/%s [%s]", e.url, key, req.RemoteAddr) + + // Execute the command. + command := &GetCommand{Key: key} + event, err := command.Apply(e.raftServer.Server) + if err != nil { + return err + } + + // Convert event to a response and write to client. + event, _ := event.(*store.Event) + response := eventToResponse(event) + b, _ := json.Marshal(response) + w.WriteHeader(http.StatusOK) + w.Write(b) + + return nil +} diff --git a/server/v1/install.go b/server/v1/install.go new file mode 100644 index 000000000..18b664cfe --- /dev/null +++ b/server/v1/install.go @@ -0,0 +1,15 @@ +package v1 + +import ( + "github.com/coreos/etcd/server" + "github.com/gorilla/mux" +) + +// Installs the routes for version 1 of the API on to a server. +func Install(s *server.Server) { + s.HandleFunc("/v1/keys/{key:.*}", getKeyHandler).Methods("GET") + s.HandleFunc("/v1/keys/{key:.*}", setKeyHandler).Methods("POST", "PUT") + s.HandleFunc("/v1/keys/{key:.*}", deleteKeyHandler).Methods("DELETE") + + s.HandleFunc("/v1/watch/{key:.*}", watchKeyHandler).Methods("GET", "POST") +} diff --git a/server/v1/set_key_handler.go b/server/v1/set_key_handler.go new file mode 100644 index 000000000..26f6db014 --- /dev/null +++ b/server/v1/set_key_handler.go @@ -0,0 +1,50 @@ +package v1 + +import ( + "encoding/json" + "github.com/coreos/etcd/store" + "net/http" +) + +// Sets the value for a given key. +func setKeyHandler(w http.ResponseWriter, req *http.Request, e *etcdServer) error { + vars := mux.Vars(req) + key := "/" + vars["key"] + + debugf("[recv] POST %v/v1/keys/%s [%s]", e.url, key, req.RemoteAddr) + + req.ParseForm() + + // Parse non-blank value. + value := req.Form.Get("value") + if len(value) == 0 { + return error.NewError(200, "Set", store.UndefIndex, store.UndefTerm) + } + + // Convert time-to-live to an expiration time. + expireTime, err := durationToExpireTime(req.Form.Get("ttl")) + if err != nil { + return etcdErr.NewError(202, "Set", store.UndefIndex, store.UndefTerm) + } + + // If the "prevValue" is specified then test-and-set. Otherwise create a new key. + var c command.Command + if prevValueArr, ok := req.Form["prevValue"]; ok && len(prevValueArr) > 0 { + c = &TestAndSetCommand{ + Key: key, + Value: value, + PrevValue: prevValueArr[0], + ExpireTime: expireTime, + } + + } else { + c = &CreateCommand{ + Key: key, + Value: value, + ExpireTime: expireTime, + Force: true, + } + } + + return dispatchEtcdCommand(command, w, req) +} diff --git a/server/v1/util.go b/server/v1/util.go new file mode 100644 index 000000000..8bfc22f23 --- /dev/null +++ b/server/v1/util.go @@ -0,0 +1,38 @@ +package v1 + +// Converts an event object into a response object. +func eventToResponse(event *store.Event) interface{} { + if !event.Dir { + response := &store.Response{ + Action: event.Action, + Key: event.Key, + Value: event.Value, + PrevValue: event.PrevValue, + Index: event.Index, + TTL: event.TTL, + Expiration: event.Expiration, + } + + if response.Action == store.Create || response.Action == store.Update { + response.Action = "set" + if response.PrevValue == "" { + response.NewKey = true + } + } + + return response + } else { + responses := make([]*store.Response, len(event.KVPairs)) + + for i, kv := range event.KVPairs { + responses[i] = &store.Response{ + Action: event.Action, + Key: kv.Key, + Value: kv.Value, + Dir: kv.Dir, + Index: event.Index, + } + } + return responses + } +} diff --git a/server/v1/watch_key_handler.go b/server/v1/watch_key_handler.go new file mode 100644 index 000000000..0b32314aa --- /dev/null +++ b/server/v1/watch_key_handler.go @@ -0,0 +1,39 @@ +package v1 + +import ( + "encoding/json" + "github.com/coreos/etcd/store" + "net/http" +) + +// Watches a given key prefix for changes. +func watchKeyHandler(w http.ResponseWriter, req *http.Request, e *etcdServer) error { + vars := mux.Vars(req) + key := "/" + vars["key"] + + debugf("[recv] %s %s/watch/%s [%s]", req.Method, e.url, key, req.RemoteAddr) + + // Create a command to watch from a given index (default 0). + command := &WatchCommand{Key: key} + if req.Method == "POST" { + sinceIndex, err := strconv.ParseUint(string(req.FormValue("index")), 10, 64) + if err != nil { + return etcdErr.NewError(203, "Watch From Index", store.UndefIndex, store.UndefTerm) + } + command.SinceIndex = sinceIndex + } + + // Apply the command and write the response. + event, err := command.Apply(e.raftServer.Server) + if err != nil { + return etcdErr.NewError(500, key, store.UndefIndex, store.UndefTerm) + } + + event, _ := event.(*store.Event) + response := eventToResponse(event) + b, _ := json.Marshal(response) + w.WriteHeader(http.StatusOK) + w.Write(b) + + return nil +}