| // Package api implements an HTTP-based API and server for CFSSL. | |
| package api | |
| import ( | |
| "encoding/json" | |
| "io/ioutil" | |
| "net/http" | |
| "github.com/cloudflare/cfssl/errors" | |
| "github.com/cloudflare/cfssl/log" | |
| ) | |
| // Handler is an interface providing a generic mechanism for handling HTTP requests. | |
| type Handler interface { | |
| Handle(w http.ResponseWriter, r *http.Request) error | |
| } | |
| // HTTPHandler is a wrapper that encapsulates Handler interface as http.Handler. | |
| // HTTPHandler also enforces that the Handler only responds to requests with registered HTTP methods. | |
| type HTTPHandler struct { | |
| Handler // CFSSL handler | |
| Methods []string // The associated HTTP methods | |
| } | |
| // HandlerFunc is similar to the http.HandlerFunc type; it serves as | |
| // an adapter allowing the use of ordinary functions as Handlers. If | |
| // f is a function with the appropriate signature, HandlerFunc(f) is a | |
| // Handler object that calls f. | |
| type HandlerFunc func(http.ResponseWriter, *http.Request) error | |
| // Handle calls f(w, r) | |
| func (f HandlerFunc) Handle(w http.ResponseWriter, r *http.Request) error { | |
| w.Header().Set("Content-Type", "application/json") | |
| return f(w, r) | |
| } | |
| // HandleError is the centralised error handling and reporting. | |
| func HandleError(w http.ResponseWriter, err error) (code int) { | |
| if err == nil { | |
| return http.StatusOK | |
| } | |
| msg := err.Error() | |
| httpCode := http.StatusInternalServerError | |
| // If it is recognized as HttpError emitted from cfssl, | |
| // we rewrite the status code accordingly. If it is a | |
| // cfssl error, set the http status to StatusBadRequest | |
| switch err := err.(type) { | |
| case *errors.HTTPError: | |
| httpCode = err.StatusCode | |
| code = err.StatusCode | |
| case *errors.Error: | |
| httpCode = http.StatusBadRequest | |
| code = err.ErrorCode | |
| msg = err.Message | |
| } | |
| response := NewErrorResponse(msg, code) | |
| jsonMessage, err := json.Marshal(response) | |
| if err != nil { | |
| log.Errorf("Failed to marshal JSON: %v", err) | |
| } else { | |
| msg = string(jsonMessage) | |
| } | |
| http.Error(w, msg, httpCode) | |
| return code | |
| } | |
| // ServeHTTP encapsulates the call to underlying Handler to handle the request | |
| // and return the response with proper HTTP status code | |
| func (h HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| var err error | |
| var match bool | |
| // Throw 405 when requested with an unsupported verb. | |
| for _, m := range h.Methods { | |
| if m == r.Method { | |
| match = true | |
| } | |
| } | |
| if match { | |
| err = h.Handle(w, r) | |
| } else { | |
| err = errors.NewMethodNotAllowed(r.Method) | |
| } | |
| status := HandleError(w, err) | |
| log.Infof("%s - \"%s %s\" %d", r.RemoteAddr, r.Method, r.URL, status) | |
| } | |
| // readRequestBlob takes a JSON-blob-encoded response body in the form | |
| // map[string]string and returns it, the list of keywords presented, | |
| // and any error that occurred. | |
| func readRequestBlob(r *http.Request) (map[string]string, error) { | |
| var blob map[string]string | |
| body, err := ioutil.ReadAll(r.Body) | |
| if err != nil { | |
| return nil, err | |
| } | |
| r.Body.Close() | |
| err = json.Unmarshal(body, &blob) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return blob, nil | |
| } | |
| // ProcessRequestOneOf reads a JSON blob for the request and makes | |
| // sure it contains one of a set of keywords. For example, a request | |
| // might have the ('foo' && 'bar') keys, OR it might have the 'baz' | |
| // key. In either case, we want to accept the request; however, if | |
| // none of these sets shows up, the request is a bad request, and it | |
| // should be returned. | |
| func ProcessRequestOneOf(r *http.Request, keywordSets [][]string) (map[string]string, []string, error) { | |
| blob, err := readRequestBlob(r) | |
| if err != nil { | |
| return nil, nil, err | |
| } | |
| var matched []string | |
| for _, set := range keywordSets { | |
| if matchKeywords(blob, set) { | |
| if matched != nil { | |
| return nil, nil, errors.NewBadRequestString("mismatched parameters") | |
| } | |
| matched = set | |
| } | |
| } | |
| if matched == nil { | |
| return nil, nil, errors.NewBadRequestString("no valid parameter sets found") | |
| } | |
| return blob, matched, nil | |
| } | |
| // ProcessRequestFirstMatchOf reads a JSON blob for the request and returns | |
| // the first match of a set of keywords. For example, a request | |
| // might have one of the following combinations: (foo=1, bar=2), (foo=1), and (bar=2) | |
| // By giving a specific ordering of those combinations, we could decide how to accept | |
| // the request. | |
| func ProcessRequestFirstMatchOf(r *http.Request, keywordSets [][]string) (map[string]string, []string, error) { | |
| blob, err := readRequestBlob(r) | |
| if err != nil { | |
| return nil, nil, err | |
| } | |
| for _, set := range keywordSets { | |
| if matchKeywords(blob, set) { | |
| return blob, set, nil | |
| } | |
| } | |
| return nil, nil, errors.NewBadRequestString("no valid parameter sets found") | |
| } | |
| func matchKeywords(blob map[string]string, keywords []string) bool { | |
| for _, keyword := range keywords { | |
| if _, ok := blob[keyword]; !ok { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| // ResponseMessage implements the standard for response errors and | |
| // messages. A message has a code and a string message. | |
| type ResponseMessage struct { | |
| Code int `json:"code"` | |
| Message string `json:"message"` | |
| } | |
| // Response implements the CloudFlare standard for API | |
| // responses. | |
| type Response struct { | |
| Success bool `json:"success"` | |
| Result interface{} `json:"result"` | |
| Errors []ResponseMessage `json:"errors"` | |
| Messages []ResponseMessage `json:"messages"` | |
| } | |
| // NewSuccessResponse is a shortcut for creating new successul API | |
| // responses. | |
| func NewSuccessResponse(result interface{}) Response { | |
| return Response{ | |
| Success: true, | |
| Result: result, | |
| Errors: []ResponseMessage{}, | |
| Messages: []ResponseMessage{}, | |
| } | |
| } | |
| // NewSuccessResponseWithMessage is a shortcut for creating new successul API | |
| // responses that includes a message. | |
| func NewSuccessResponseWithMessage(result interface{}, message string, code int) Response { | |
| return Response{ | |
| Success: true, | |
| Result: result, | |
| Errors: []ResponseMessage{}, | |
| Messages: []ResponseMessage{{code, message}}, | |
| } | |
| } | |
| // NewErrorResponse is a shortcut for creating an error response for a | |
| // single error. | |
| func NewErrorResponse(message string, code int) Response { | |
| return Response{ | |
| Success: false, | |
| Result: nil, | |
| Errors: []ResponseMessage{{code, message}}, | |
| Messages: []ResponseMessage{}, | |
| } | |
| } | |
| // SendResponse builds a response from the result, sets the JSON | |
| // header, and writes to the http.ResponseWriter. | |
| func SendResponse(w http.ResponseWriter, result interface{}) error { | |
| response := NewSuccessResponse(result) | |
| w.Header().Set("Content-Type", "application/json") | |
| enc := json.NewEncoder(w) | |
| err := enc.Encode(response) | |
| return err | |
| } | |
| // SendResponseWithMessage builds a response from the result and the | |
| // provided message, sets the JSON header, and writes to the | |
| // http.ResponseWriter. | |
| func SendResponseWithMessage(w http.ResponseWriter, result interface{}, message string, code int) error { | |
| response := NewSuccessResponseWithMessage(result, message, code) | |
| w.Header().Set("Content-Type", "application/json") | |
| enc := json.NewEncoder(w) | |
| err := enc.Encode(response) | |
| return err | |
| } |