github.com/furusax0621/goa-v1@v1.4.3/service.go (about) 1 package goa 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "log" 8 "net" 9 "net/http" 10 "net/url" 11 "os" 12 "path/filepath" 13 "sort" 14 "strings" 15 "sync" 16 17 "github.com/dimfeld/httptreemux" 18 ) 19 20 type ( 21 // Service is the data structure supporting goa services. 22 // It provides methods for configuring a service and running it. 23 // At the basic level a service consists of a set of controllers, each implementing a given 24 // resource actions. goagen generates global functions - one per resource - that make it 25 // possible to mount the corresponding controller onto a service. A service contains the 26 // middleware, not found handler, encoders and muxes shared by all its controllers. 27 Service struct { 28 // Name of service used for logging, tracing etc. 29 Name string 30 // Mux is the service request mux 31 Mux ServeMux 32 // Server is the service HTTP server. 33 Server *http.Server 34 // Context is the root context from which all request contexts are derived. 35 // Set values in the root context prior to starting the server to make these values 36 // available to all request handlers. 37 Context context.Context 38 // Request body decoder 39 Decoder *HTTPDecoder 40 // Response body encoder 41 Encoder *HTTPEncoder 42 43 middleware []Middleware // Middleware chain 44 cancel context.CancelFunc // Service context cancel signal trigger 45 } 46 47 // Controller defines the common fields and behavior of generated controllers. 48 Controller struct { 49 // Controller resource name 50 Name string 51 // Service that exposes the controller 52 Service *Service 53 // Controller root context 54 Context context.Context 55 // MaxRequestBodyLength is the maximum length read from request bodies. 56 // Set to 0 to remove the limit altogether. Defaults to 1GB. 57 MaxRequestBodyLength int64 58 // FileSystem is used in FileHandler to open files. By default it returns 59 // http.Dir but you can override it with another one that implements http.FileSystem. 60 // For example using github.com/elazarl/go-bindata-assetfs is like below. 61 // 62 // ctrl.FileSystem = func(dir string) http.FileSystem { 63 // return &assetfs.AssetFS{ 64 // Asset: Asset, 65 // AssetDir: AssetDir, 66 // AssetInfo: AssetInfo, 67 // Prefix: dir, 68 // } 69 // } 70 FileSystem func(string) http.FileSystem 71 72 middleware []Middleware // Controller specific middleware if any 73 } 74 75 // FileServer is the interface implemented by controllers that can serve static files. 76 FileServer interface { 77 // FileHandler returns a handler that serves files under the given request path. 78 FileHandler(path, filename string) Handler 79 } 80 81 // Handler defines the request handler signatures. 82 Handler func(context.Context, http.ResponseWriter, *http.Request) error 83 84 // Unmarshaler defines the request payload unmarshaler signatures. 85 Unmarshaler func(context.Context, *Service, *http.Request) error 86 87 // DecodeFunc is the function that initialize the unmarshaled payload from the request body. 88 DecodeFunc func(context.Context, io.ReadCloser, interface{}) error 89 ) 90 91 // New instantiates a service with the given name. 92 func New(name string) *Service { 93 var ( 94 stdlog = log.New(os.Stderr, "", log.LstdFlags) 95 ctx = WithLogger(context.Background(), NewLogger(stdlog)) 96 cctx, cancel = context.WithCancel(ctx) 97 mux = NewMux() 98 service = &Service{ 99 Name: name, 100 Context: cctx, 101 Mux: mux, 102 Server: &http.Server{ 103 Handler: mux, 104 }, 105 Decoder: NewHTTPDecoder(), 106 Encoder: NewHTTPEncoder(), 107 108 cancel: cancel, 109 } 110 notFoundHandler Handler 111 methodNotAllowedHandler Handler 112 ) 113 114 // Setup default NotFound handler 115 mux.HandleNotFound(func(rw http.ResponseWriter, req *http.Request, params url.Values) { 116 if resp := ContextResponse(ctx); resp != nil && resp.Written() { 117 return 118 } 119 // Use closure to do lazy computation of middleware chain so all middlewares are 120 // registered. 121 if notFoundHandler == nil { 122 notFoundHandler = func(_ context.Context, _ http.ResponseWriter, req *http.Request) error { 123 return ErrNotFound(req.URL.Path) 124 } 125 chain := service.middleware 126 ml := len(chain) 127 for i := range chain { 128 notFoundHandler = chain[ml-i-1](notFoundHandler) 129 } 130 } 131 ctx := NewContext(service.Context, rw, req, params) 132 err := notFoundHandler(ctx, ContextResponse(ctx), req) 133 if !ContextResponse(ctx).Written() { 134 service.Send(ctx, 404, err) 135 } 136 }) 137 138 // Setup default MethodNotAllowed handler 139 mux.HandleMethodNotAllowed(func(rw http.ResponseWriter, req *http.Request, params url.Values, methods map[string]httptreemux.HandlerFunc) { 140 if resp := ContextResponse(ctx); resp != nil && resp.Written() { 141 return 142 } 143 // Use closure to do lazy computation of middleware chain so all middlewares are 144 // registered. 145 if methodNotAllowedHandler == nil { 146 methodNotAllowedHandler = func(_ context.Context, rw http.ResponseWriter, req *http.Request) error { 147 allowedMethods := make([]string, len(methods)) 148 i := 0 149 for k := range methods { 150 allowedMethods[i] = k 151 i++ 152 } 153 rw.Header().Set("Allow", strings.Join(allowedMethods, ", ")) 154 return MethodNotAllowedError(req.Method, allowedMethods) 155 } 156 chain := service.middleware 157 ml := len(chain) 158 for i := range chain { 159 methodNotAllowedHandler = chain[ml-i-1](methodNotAllowedHandler) 160 } 161 } 162 ctx := NewContext(service.Context, rw, req, params) 163 err := methodNotAllowedHandler(ctx, ContextResponse(ctx), req) 164 if !ContextResponse(ctx).Written() { 165 service.Send(ctx, 405, err) 166 } 167 }) 168 169 return service 170 } 171 172 // CancelAll sends a cancel signals to all request handlers via the context. 173 // See https://golang.org/pkg/context/ for details on how to handle the signal. 174 func (service *Service) CancelAll() { 175 service.cancel() 176 } 177 178 // Use adds a middleware to the service wide middleware chain. 179 // goa comes with a set of commonly used middleware, see the middleware package. 180 // Controller specific middleware should be mounted using the Controller struct Use method instead. 181 func (service *Service) Use(m Middleware) { 182 service.middleware = append(service.middleware, m) 183 } 184 185 // WithLogger sets the logger used internally by the service and by Log. 186 func (service *Service) WithLogger(logger LogAdapter) { 187 service.Context = WithLogger(service.Context, logger) 188 } 189 190 // LogInfo logs the message and values at odd indeces using the keys at even indeces of the keyvals slice. 191 func (service *Service) LogInfo(msg string, keyvals ...interface{}) { 192 LogInfo(service.Context, msg, keyvals...) 193 } 194 195 // LogError logs the error and values at odd indeces using the keys at even indeces of the keyvals slice. 196 func (service *Service) LogError(msg string, keyvals ...interface{}) { 197 LogError(service.Context, msg, keyvals...) 198 } 199 200 // ListenAndServe starts a HTTP server and sets up a listener on the given host/port. 201 func (service *Service) ListenAndServe(addr string) error { 202 service.LogInfo("listen", "transport", "http", "addr", addr) 203 service.Server.Addr = addr 204 return service.Server.ListenAndServe() 205 } 206 207 // ListenAndServeTLS starts a HTTPS server and sets up a listener on the given host/port. 208 func (service *Service) ListenAndServeTLS(addr, certFile, keyFile string) error { 209 service.LogInfo("listen", "transport", "https", "addr", addr) 210 service.Server.Addr = addr 211 return service.Server.ListenAndServeTLS(certFile, keyFile) 212 } 213 214 // Serve accepts incoming HTTP connections on the listener l, invoking the service mux handler for each. 215 func (service *Service) Serve(l net.Listener) error { 216 return service.Server.Serve(l) 217 } 218 219 // NewController returns a controller for the given resource. This method is mainly intended for 220 // use by the generated code. User code shouldn't have to call it directly. 221 func (service *Service) NewController(name string) *Controller { 222 return &Controller{ 223 Name: name, 224 Service: service, 225 Context: context.WithValue(service.Context, ctrlKey, name), 226 MaxRequestBodyLength: 1073741824, // 1 GB 227 FileSystem: func(dir string) http.FileSystem { 228 return http.Dir(dir) 229 }, 230 } 231 } 232 233 // Send serializes the given body matching the request Accept header against the service 234 // encoders. It uses the default service encoder if no match is found. 235 func (service *Service) Send(ctx context.Context, code int, body interface{}) error { 236 r := ContextResponse(ctx) 237 if r == nil { 238 return fmt.Errorf("no response data in context") 239 } 240 r.WriteHeader(code) 241 return service.EncodeResponse(ctx, body) 242 } 243 244 // ServeFiles create a "FileServer" controller and calls ServerFiles on it. 245 func (service *Service) ServeFiles(path, filename string) error { 246 ctrl := service.NewController("FileServer") 247 return ctrl.ServeFiles(path, filename) 248 } 249 250 // DecodeRequest uses the HTTP decoder to unmarshal the request body into the provided value based 251 // on the request Content-Type header. 252 func (service *Service) DecodeRequest(req *http.Request, v interface{}) error { 253 body, contentType := req.Body, req.Header.Get("Content-Type") 254 defer body.Close() 255 256 if err := service.Decoder.Decode(v, body, contentType); err != nil { 257 return fmt.Errorf("failed to decode request body with content type %#v: %s", contentType, err) 258 } 259 260 return nil 261 } 262 263 // EncodeResponse uses the HTTP encoder to marshal and write the response body based on the request 264 // Accept header. 265 func (service *Service) EncodeResponse(ctx context.Context, v interface{}) error { 266 accept := ContextRequest(ctx).Header.Get("Accept") 267 return service.Encoder.Encode(v, ContextResponse(ctx), accept) 268 } 269 270 // ServeFiles replies to the request with the contents of the named file or directory. See 271 // FileHandler for details. 272 func (ctrl *Controller) ServeFiles(path, filename string) error { 273 if strings.Contains(path, ":") { 274 return fmt.Errorf("path may only include wildcards that match the entire end of the URL (e.g. *filepath)") 275 } 276 LogInfo(ctrl.Context, "mount file", "name", filename, "route", fmt.Sprintf("GET %s", path)) 277 handler := func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { 278 if !ContextResponse(ctx).Written() { 279 return ctrl.FileHandler(path, filename)(ctx, rw, req) 280 } 281 return nil 282 } 283 ctrl.Service.Mux.Handle("GET", path, ctrl.MuxHandler("serve", handler, nil)) 284 return nil 285 } 286 287 // Use adds a middleware to the controller. 288 // Service-wide middleware should be added via the Service Use method instead. 289 func (ctrl *Controller) Use(m Middleware) { 290 ctrl.middleware = append(ctrl.middleware, m) 291 } 292 293 // MuxHandler wraps a request handler into a MuxHandler. The MuxHandler initializes the request 294 // context by loading the request state, invokes the handler and in case of error invokes the 295 // controller (if there is one) or Service error handler. 296 // This function is intended for the controller generated code. User code should not need to call 297 // it directly. 298 func (ctrl *Controller) MuxHandler(name string, hdlr Handler, unm Unmarshaler) MuxHandler { 299 // Use closure to enable late computation of handlers to ensure all middleware has been 300 // registered. 301 var handler Handler 302 var initHandler sync.Once 303 304 return func(rw http.ResponseWriter, req *http.Request, params url.Values) { 305 // Build handler middleware chains on first invocation 306 initHandler.Do(func() { 307 handler = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { 308 if !ContextResponse(ctx).Written() { 309 return hdlr(ctx, rw, req) 310 } 311 return nil 312 } 313 mwLen := len(ctrl.Service.middleware) 314 chain := append(ctrl.Service.middleware[:mwLen:mwLen], ctrl.middleware...) 315 ml := len(chain) 316 for i := range chain { 317 handler = chain[ml-i-1](handler) 318 } 319 }) 320 321 // Build context 322 ctx := NewContext(WithAction(ctrl.Context, name), rw, req, params) 323 324 // Protect against request bodies with unreasonable length 325 if ctrl.MaxRequestBodyLength > 0 { 326 req.Body = http.MaxBytesReader(rw, req.Body, ctrl.MaxRequestBodyLength) 327 } 328 329 // Load body if any 330 if req.ContentLength > 0 && unm != nil { 331 if err := unm(ctx, ctrl.Service, req); err != nil { 332 if err.Error() == "http: request body too large" { 333 msg := fmt.Sprintf("request body length exceeds %d bytes", ctrl.MaxRequestBodyLength) 334 err = ErrRequestBodyTooLarge(msg) 335 } else { 336 err = ErrBadRequest(err) 337 } 338 ctx = WithError(ctx, err) 339 } 340 } 341 342 // Invoke handler 343 if err := handler(ctx, ContextResponse(ctx), req); err != nil { 344 LogError(ctx, "uncaught error", "err", err) 345 respBody := fmt.Sprintf("Internal error: %s", err) // Sprintf catches panics 346 ctrl.Service.Send(ctx, 500, respBody) 347 } 348 } 349 } 350 351 // FileHandler returns a handler that serves files under the given filename for the given route path. 352 // The logic for what to do when the filename points to a file vs. a directory is the same as the 353 // standard http package ServeFile function. The path may end with a wildcard that matches the rest 354 // of the URL (e.g. *filepath). If it does the matching path is appended to filename to form the 355 // full file path, so: 356 // 357 // c.FileHandler("/index.html", "/www/data/index.html") 358 // 359 // Returns the content of the file "/www/data/index.html" when requests are sent to "/index.html" 360 // and: 361 // 362 // c.FileHandler("/assets/*filepath", "/www/data/assets") 363 // 364 // returns the content of the file "/www/data/assets/x/y/z" when requests are sent to 365 // "/assets/x/y/z". 366 func (ctrl *Controller) FileHandler(path, filename string) Handler { 367 var wc string 368 if idx := strings.LastIndex(path, "/*"); idx > -1 && idx < len(path)-1 { 369 wc = path[idx+2:] 370 if strings.Contains(wc, "/") { 371 wc = "" 372 } 373 } 374 return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { 375 // prevent path traversal 376 if attemptsPathTraversal(req.URL.Path, path) { 377 return ErrNotFound(req.URL.Path) 378 } 379 fname := filename 380 if len(wc) > 0 { 381 if m, ok := ContextRequest(ctx).Params[wc]; ok { 382 fname = filepath.Join(filename, m[0]) 383 } 384 } 385 LogInfo(ctx, "serve file", "name", fname, "route", req.URL.Path) 386 dir, name := filepath.Split(fname) 387 fs := ctrl.FileSystem(dir) 388 f, err := fs.Open(name) 389 if err != nil { 390 return ErrInvalidFile(err) 391 } 392 defer f.Close() 393 d, err := f.Stat() 394 if err != nil { 395 return ErrInvalidFile(err) 396 } 397 // use contents of index.html for directory, if present 398 if d.IsDir() { 399 index := strings.TrimSuffix(name, "/") + "/index.html" 400 ff, err := fs.Open(index) 401 if err == nil { 402 defer ff.Close() 403 dd, err := ff.Stat() 404 if err == nil { 405 name = index 406 d = dd 407 f = ff 408 } 409 } 410 } 411 412 // serveContent will check modification time 413 // Still a directory? (we didn't find an index.html file) 414 if d.IsDir() { 415 return dirList(rw, f) 416 } 417 http.ServeContent(rw, req, d.Name(), d.ModTime(), f) 418 return nil 419 } 420 } 421 422 func attemptsPathTraversal(req string, path string) bool { 423 if !strings.Contains(req, "..") { 424 return false 425 } 426 427 currentPathIdx := 0 428 if idx := strings.LastIndex(path, "/*"); idx > -1 && idx < len(path)-1 { 429 req = req[idx+1:] 430 } 431 for _, runeValue := range strings.FieldsFunc(req, isSlashRune) { 432 if runeValue == ".." { 433 currentPathIdx-- 434 if currentPathIdx < 0 { 435 return true 436 } 437 } else { 438 currentPathIdx++ 439 } 440 } 441 return false 442 } 443 444 func isSlashRune(r rune) bool { 445 return os.IsPathSeparator(uint8(r)) 446 } 447 448 var replacer = strings.NewReplacer( 449 "&", "&", 450 "<", "<", 451 ">", ">", 452 // """ is shorter than """. 453 `"`, """, 454 // "'" is shorter than "'" and apos was not in HTML until HTML5. 455 "'", "'", 456 ) 457 458 func dirList(w http.ResponseWriter, f http.File) error { 459 dirs, err := f.Readdir(-1) 460 if err != nil { 461 return err 462 } 463 sort.Sort(byName(dirs)) 464 465 w.Header().Set("Content-Type", "text/html; charset=utf-8") 466 fmt.Fprintf(w, "<pre>\n") 467 for _, d := range dirs { 468 name := d.Name() 469 if d.IsDir() { 470 name += "/" 471 } 472 // name may contain '?' or '#', which must be escaped to remain 473 // part of the URL path, and not indicate the start of a query 474 // string or fragment. 475 url := url.URL{Path: name} 476 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), replacer.Replace(name)) 477 } 478 fmt.Fprintf(w, "</pre>\n") 479 return nil 480 } 481 482 type byName []os.FileInfo 483 484 func (s byName) Len() int { return len(s) } 485 func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() } 486 func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }