~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charmstore.v5-unstable/internal/router/util.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2014 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package router // import "gopkg.in/juju/charmstore.v5-unstable/internal/router"
 
5
 
 
6
import (
 
7
        "encoding/json"
 
8
        "fmt"
 
9
        "io/ioutil"
 
10
        "mime"
 
11
        "net/http"
 
12
        "strings"
 
13
 
 
14
        "github.com/juju/httprequest"
 
15
        "github.com/juju/loggo"
 
16
        "gopkg.in/errgo.v1"
 
17
        "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
 
18
        "gopkg.in/macaroon-bakery.v1/httpbakery"
 
19
)
 
20
 
 
21
var logger = loggo.GetLogger("charmstore.internal.router")
 
22
 
 
23
// WriteError can be used to write an error response.
 
24
var WriteError = errorToResp.WriteError
 
25
 
 
26
// JSONHandler represents a handler that returns a JSON value.
 
27
// The provided header can be used to set response headers.
 
28
type JSONHandler func(http.Header, *http.Request) (interface{}, error)
 
29
 
 
30
// ErrorHandler represents a handler that can return an error.
 
31
type ErrorHandler func(http.ResponseWriter, *http.Request) error
 
32
 
 
33
// HandleJSON converts from a JSONHandler function to an http.Handler.
 
34
func HandleJSON(h JSONHandler) http.Handler {
 
35
        // We can't use errorToResp.HandleJSON directly because
 
36
        // we still use old-style handlers in charmstore, so we
 
37
        // insert shim functions to do the conversion.
 
38
        handleJSON := errorToResp.HandleJSON(
 
39
                func(p httprequest.Params) (interface{}, error) {
 
40
                        return h(p.Response.Header(), p.Request)
 
41
                },
 
42
        )
 
43
        return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 
44
                handleJSON(w, req, nil)
 
45
        })
 
46
}
 
47
 
 
48
// HandleJSON converts from a ErrorHandler function to an http.Handler.
 
49
func HandleErrors(h ErrorHandler) http.Handler {
 
50
        // We can't use errorToResp.HandleErrors directly because
 
51
        // we still use old-style handlers in charmstore, so we
 
52
        // insert shim functions to do the conversion.
 
53
        handleErrors := errorToResp.HandleErrors(
 
54
                func(p httprequest.Params) error {
 
55
                        return h(p.Response, p.Request)
 
56
                },
 
57
        )
 
58
        return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 
59
                handleErrors(w, req, nil)
 
60
        })
 
61
}
 
62
 
 
63
var errorToResp httprequest.ErrorMapper = func(err error) (int, interface{}) {
 
64
        status, body := errorToResp1(err)
 
65
        logger.Infof("error response %d; %s", status, errgo.Details(err))
 
66
        return status, body
 
67
}
 
68
 
 
69
func errorToResp1(err error) (int, interface{}) {
 
70
        // Allow bakery errors to be returned as the bakery would
 
71
        // like them, so that httpbakery.Client.Do will work.
 
72
        if err, ok := errgo.Cause(err).(*httpbakery.Error); ok {
 
73
                return httpbakery.ErrorToResponse(err)
 
74
        }
 
75
        errorBody := errorResponseBody(err)
 
76
        status := http.StatusInternalServerError
 
77
        switch errorBody.Code {
 
78
        case params.ErrNotFound, params.ErrMetadataNotFound:
 
79
                status = http.StatusNotFound
 
80
        case params.ErrBadRequest, params.ErrInvalidEntity:
 
81
                status = http.StatusBadRequest
 
82
        case params.ErrForbidden, params.ErrEntityIdNotAllowed:
 
83
                status = http.StatusForbidden
 
84
        case params.ErrUnauthorized:
 
85
                status = http.StatusUnauthorized
 
86
        case params.ErrMethodNotAllowed:
 
87
                // TODO(rog) from RFC 2616, section 4.7: An Allow header
 
88
                // field MUST be present in a 405 (Method Not Allowed)
 
89
                // response.
 
90
                // Perhaps we should not ever return StatusMethodNotAllowed.
 
91
                status = http.StatusMethodNotAllowed
 
92
        case params.ErrServiceUnavailable:
 
93
                status = http.StatusServiceUnavailable
 
94
        }
 
95
        return status, errorBody
 
96
}
 
97
 
 
98
// errorResponse returns an appropriate error
 
99
// response for the provided error.
 
100
func errorResponseBody(err error) *params.Error {
 
101
 
 
102
        errResp := &params.Error{
 
103
                Message: err.Error(),
 
104
        }
 
105
        cause := errgo.Cause(err)
 
106
        if coder, ok := cause.(errorCoder); ok {
 
107
                errResp.Code = coder.ErrorCode()
 
108
        }
 
109
        if infoer, ok := cause.(errorInfoer); ok {
 
110
                errResp.Info = infoer.ErrorInfo()
 
111
        }
 
112
        return errResp
 
113
}
 
114
 
 
115
type errorInfoer interface {
 
116
        ErrorInfo() map[string]*params.Error
 
117
}
 
118
 
 
119
type errorCoder interface {
 
120
        ErrorCode() params.ErrorCode
 
121
}
 
122
 
 
123
// multiError holds multiple errors.
 
124
type multiError map[string]error
 
125
 
 
126
func (err multiError) Error() string {
 
127
        return fmt.Sprintf("multiple (%d) errors", len(err))
 
128
}
 
129
 
 
130
func (err multiError) ErrorCode() params.ErrorCode {
 
131
        return params.ErrMultipleErrors
 
132
}
 
133
 
 
134
func (err multiError) ErrorInfo() map[string]*params.Error {
 
135
        m := make(map[string]*params.Error)
 
136
        for key, err := range err {
 
137
                m[key] = errorResponseBody(err)
 
138
        }
 
139
        return m
 
140
}
 
141
 
 
142
// NotFoundHandler is like http.NotFoundHandler except it
 
143
// returns a JSON error response.
 
144
func NotFoundHandler() http.Handler {
 
145
        return HandleErrors(func(w http.ResponseWriter, req *http.Request) error {
 
146
                return errgo.WithCausef(nil, params.ErrNotFound, params.ErrNotFound.Error())
 
147
        })
 
148
}
 
149
 
 
150
func NewServeMux() *ServeMux {
 
151
        return &ServeMux{http.NewServeMux()}
 
152
}
 
153
 
 
154
// ServeMux is like http.ServeMux but returns
 
155
// JSON errors when pages are not found.
 
156
type ServeMux struct {
 
157
        *http.ServeMux
 
158
}
 
159
 
 
160
func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 
161
        if req.RequestURI == "*" {
 
162
                mux.ServeMux.ServeHTTP(w, req)
 
163
                return
 
164
        }
 
165
        h, pattern := mux.Handler(req)
 
166
        if pattern == "" {
 
167
                WriteError(w, errgo.WithCausef(nil, params.ErrNotFound, "no handler for %q", req.URL.Path))
 
168
                return
 
169
        }
 
170
        h.ServeHTTP(w, req)
 
171
}
 
172
 
 
173
// RelativeURLPath returns a relative URL path that is lexically
 
174
// equivalent to targpath when interpreted by url.URL.ResolveReference.
 
175
// On success, the returned path will always be non-empty and relative
 
176
// to basePath, even if basePath and targPath share no elements.
 
177
//
 
178
// An error is returned if basePath or targPath are not absolute paths.
 
179
func RelativeURLPath(basePath, targPath string) (string, error) {
 
180
        if !strings.HasPrefix(basePath, "/") {
 
181
                return "", errgo.Newf("non-absolute base URL")
 
182
        }
 
183
        if !strings.HasPrefix(targPath, "/") {
 
184
                return "", errgo.Newf("non-absolute target URL")
 
185
        }
 
186
        baseParts := strings.Split(basePath, "/")
 
187
        targParts := strings.Split(targPath, "/")
 
188
 
 
189
        // For the purposes of dotdot, the last element of
 
190
        // the paths are irrelevant. We save the last part
 
191
        // of the target path for later.
 
192
        lastElem := targParts[len(targParts)-1]
 
193
        baseParts = baseParts[0 : len(baseParts)-1]
 
194
        targParts = targParts[0 : len(targParts)-1]
 
195
 
 
196
        // Find the common prefix between the two paths:
 
197
        var i int
 
198
        for ; i < len(baseParts); i++ {
 
199
                if i >= len(targParts) || baseParts[i] != targParts[i] {
 
200
                        break
 
201
                }
 
202
        }
 
203
        dotdotCount := len(baseParts) - i
 
204
        targOnly := targParts[i:]
 
205
        result := make([]string, 0, dotdotCount+len(targOnly)+1)
 
206
        for i := 0; i < dotdotCount; i++ {
 
207
                result = append(result, "..")
 
208
        }
 
209
        result = append(result, targOnly...)
 
210
        result = append(result, lastElem)
 
211
        final := strings.Join(result, "/")
 
212
        if final == "" {
 
213
                // If the final result is empty, the last element must
 
214
                // have been empty, so the target was slash terminated
 
215
                // and there were no previous elements, so "."
 
216
                // is appropriate.
 
217
                final = "."
 
218
        }
 
219
        return final, nil
 
220
}
 
221
 
 
222
// TODO(mhilton) This is not an ideal place for UnmarshalJSONResponse,
 
223
// maybe it should be in httprequest somewhere?
 
224
 
 
225
// UnmarshalJSONResponse unmarshals resp.Body into v. If errorF is not
 
226
// nil and resp.StatusCode indicates an error has occured (>= 400) then
 
227
// the result of calling errorF with resp is returned.
 
228
func UnmarshalJSONResponse(resp *http.Response, v interface{}, errorF func(*http.Response) error) error {
 
229
        if errorF != nil && resp.StatusCode >= http.StatusBadRequest {
 
230
                return errgo.Mask(errorF(resp), errgo.Any)
 
231
        }
 
232
        mt, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
 
233
        if err != nil {
 
234
                return errgo.Notef(err, "cannot parse content type")
 
235
        }
 
236
        if mt != "application/json" {
 
237
                return errgo.Newf("unexpected content type %q", mt)
 
238
        }
 
239
        body, err := ioutil.ReadAll(resp.Body)
 
240
        if err != nil {
 
241
                return errgo.Notef(err, "cannot read response body")
 
242
        }
 
243
        if err := json.Unmarshal(body, v); err != nil {
 
244
                return errgo.Notef(err, "cannot unmarshal response")
 
245
        }
 
246
        return nil
 
247
}