github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/pkg/api/handlers/utils/handler.go (about) 1 package utils 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "net/url" 9 "os" 10 "strings" 11 12 "github.com/blang/semver" 13 "github.com/gorilla/mux" 14 "github.com/pkg/errors" 15 "github.com/sirupsen/logrus" 16 ) 17 18 type ( 19 // VersionTree determines which API endpoint tree for version 20 VersionTree int 21 // VersionLevel determines which API level, current or something from the past 22 VersionLevel int 23 ) 24 25 const ( 26 // LibpodTree supports Libpod endpoints 27 LibpodTree = VersionTree(iota) 28 // CompatTree supports Libpod endpoints 29 CompatTree 30 31 // CurrentAPIVersion announces what is the current API level 32 CurrentAPIVersion = VersionLevel(iota) 33 // MinimalAPIVersion announces what is the oldest API level supported 34 MinimalAPIVersion 35 ) 36 37 var ( 38 // See https://docs.docker.com/engine/api/v1.40/ 39 // libpod compat handlers are expected to honor docker API versions 40 41 // APIVersion provides the current and minimal API versions for compat and libpod endpoint trees 42 // Note: GET|HEAD /_ping is never versioned and provides the API-Version and Libpod-API-Version headers to allow 43 // clients to shop for the Version they wish to support 44 APIVersion = map[VersionTree]map[VersionLevel]semver.Version{ 45 LibpodTree: { 46 CurrentAPIVersion: semver.MustParse("2.0.0"), 47 MinimalAPIVersion: semver.MustParse("2.0.0"), 48 }, 49 CompatTree: { 50 CurrentAPIVersion: semver.MustParse("1.40.0"), 51 MinimalAPIVersion: semver.MustParse("1.24.0"), 52 }, 53 } 54 55 // ErrVersionNotGiven returned when version not given by client 56 ErrVersionNotGiven = errors.New("version not given in URL path") 57 // ErrVersionNotSupported returned when given version is too old 58 ErrVersionNotSupported = errors.New("given version is not supported") 59 ) 60 61 // IsLibpodRequest returns true if the request related to a libpod endpoint 62 // (e.g., /v2/libpod/...). 63 func IsLibpodRequest(r *http.Request) bool { 64 split := strings.Split(r.URL.String(), "/") 65 return len(split) >= 3 && split[2] == "libpod" 66 } 67 68 // SupportedVersion validates that the version provided by client is included in the given condition 69 // https://github.com/blang/semver#ranges provides the details for writing conditions 70 // If a version is not given in URL path, ErrVersionNotGiven is returned 71 func SupportedVersion(r *http.Request, condition string) (semver.Version, error) { 72 version := semver.Version{} 73 val, ok := mux.Vars(r)["version"] 74 if !ok { 75 return version, ErrVersionNotGiven 76 } 77 safeVal, err := url.PathUnescape(val) 78 if err != nil { 79 return version, errors.Wrapf(err, "unable to unescape given API version: %q", val) 80 } 81 version, err = semver.ParseTolerant(safeVal) 82 if err != nil { 83 return version, errors.Wrapf(err, "unable to parse given API version: %q from %q", safeVal, val) 84 } 85 86 inRange, err := semver.ParseRange(condition) 87 if err != nil { 88 return version, err 89 } 90 91 if inRange(version) { 92 return version, nil 93 } 94 return version, ErrVersionNotSupported 95 } 96 97 // SupportedVersionWithDefaults validates that the version provided by client valid is supported by server 98 // minimal API version <= client path version <= maximum API version focused on the endpoint tree from URL 99 func SupportedVersionWithDefaults(r *http.Request) (semver.Version, error) { 100 tree := CompatTree 101 if IsLibpodRequest(r) { 102 tree = LibpodTree 103 } 104 105 return SupportedVersion(r, 106 fmt.Sprintf(">=%s <=%s", APIVersion[tree][MinimalAPIVersion].String(), 107 APIVersion[tree][CurrentAPIVersion].String())) 108 } 109 110 // WriteResponse encodes the given value as JSON or string and renders it for http client 111 func WriteResponse(w http.ResponseWriter, code int, value interface{}) { 112 // RFC2616 explicitly states that the following status codes "MUST NOT 113 // include a message-body": 114 switch code { 115 case http.StatusNoContent, http.StatusNotModified: // 204, 304 116 w.WriteHeader(code) 117 return 118 } 119 120 switch v := value.(type) { 121 case string: 122 w.Header().Set("Content-Type", "text/plain; charset=us-ascii") 123 w.WriteHeader(code) 124 125 if _, err := fmt.Fprintln(w, v); err != nil { 126 logrus.Errorf("unable to send string response: %q", err) 127 } 128 case *os.File: 129 w.Header().Set("Content-Type", "application/octet; charset=us-ascii") 130 w.WriteHeader(code) 131 132 if _, err := io.Copy(w, v); err != nil { 133 logrus.Errorf("unable to copy to response: %q", err) 134 } 135 case io.Reader: 136 w.Header().Set("Content-Type", "application/x-tar") 137 w.WriteHeader(code) 138 139 if _, err := io.Copy(w, v); err != nil { 140 logrus.Errorf("unable to copy to response: %q", err) 141 } 142 default: 143 WriteJSON(w, code, value) 144 } 145 } 146 147 func WriteJSON(w http.ResponseWriter, code int, value interface{}) { 148 // FIXME: we don't need to write the header in all/some circumstances. 149 w.Header().Set("Content-Type", "application/json") 150 w.WriteHeader(code) 151 152 coder := json.NewEncoder(w) 153 coder.SetEscapeHTML(true) 154 if err := coder.Encode(value); err != nil { 155 logrus.Errorf("unable to write json: %q", err) 156 } 157 } 158 159 func FilterMapToString(filters map[string][]string) (string, error) { 160 f, err := json.Marshal(filters) 161 if err != nil { 162 return "", err 163 } 164 return string(f), nil 165 } 166 167 func getVar(r *http.Request, k string) string { 168 val := mux.Vars(r)[k] 169 safeVal, err := url.PathUnescape(val) 170 if err != nil { 171 logrus.Error(errors.Wrapf(err, "failed to unescape mux key %s, value %s", k, val)) 172 return val 173 } 174 return safeVal 175 } 176 177 // GetName extracts the name from the mux 178 func GetName(r *http.Request) string { 179 return getVar(r, "name") 180 }