123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- // Package web contains the web-related features: handlers and middleware
- package web
- import (
- "context"
- "encoding/json"
- "fmt"
- "log"
- "net"
- "net/http"
- "net/url"
- "os"
- "os/signal"
- "regexp"
- "strconv"
- "time"
- "github.com/gorilla/mux"
- "code.osinet.fr/fgm/lbc/domain"
- "code.osinet.fr/fgm/lbc/internal"
- )
- type (
- ConfigKeyType string
- Config struct {
- internal.Config
- Addr string
- Base string // Mount point for service
- WriteTimeout time.Duration
- }
- Server struct {
- BaseContext context.Context
- *Config
- *Stats
- *log.Logger
- }
- )
- func (c Config) String() string {
- return fmt.Sprintf("Addr: %s, Base URL: %s, Write Timeout: %v",
- c.Addr, c.Base, c.WriteTimeout)
- }
- const (
- ConfigKey ConfigKeyType = "config"
- // CT is the normalized representation of the content-type header.
- CT = "Content-Type"
- // JSON is the normalized representation of the JSON content-type per RFC4627 §6.
- JSON = "application/json; charset=utf-8"
- PathFB = "/{int1:[0-9]+}/{int2:[0-9]+}/{limit:[0-9]+}/{str1}/{str2}"
- )
- var (
- utf8 = regexp.MustCompile("utf-8")
- acceptJSON = regexp.MustCompile(`(application/json|\*/\*)`)
- )
- func argsFromRequest(r *http.Request) (int1, int2, limit int, str1, str2 string, status int) {
- var err error
- v := mux.Vars(r)
- status = http.StatusAccepted
- for k, p := range map[string]*int{"int1": &int1, "int2": &int2, "limit": &limit} {
- sn := v[k] // Cannot fail: route wouldn't match
- *p, err = strconv.Atoi(sn)
- if err != nil || *p < 2 {
- return 0, 0, 0, "", "", http.StatusNotFound
- }
- }
- for k, p := range map[string]*string{"str1": &str1, "str2": &str2} {
- // Cannot fail: route wouldn't match
- *p = v[k]
- }
- return
- }
- func isJSONAccepted(r *http.Request) bool {
- // Only return to clients accepting UTF-8 or not specifying a charset
- charset := r.Header.Get("Accept-Charset")
- if charset != "" && !utf8.MatchString(charset) {
- return false
- }
- // Only return to clients accepting JSON, anything, or not specifying Accept.
- accept := r.Header.Get("Accept")
- return accept == "" || acceptJSON.MatchString(accept)
- }
- func MakeFizzBuzz(logger *log.Logger, isVerbose bool) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- // Can we serve client at all?
- if !isJSONAccepted(r) {
- if isVerbose {
- logger.Println("request from client unable to accept JSON")
- }
- http.Error(w, "cannot offer an accepted content type: can only serve UTF-8 JSON", http.StatusNotAcceptable)
- return
- }
- int1, int2, limit, str1, str2, status := argsFromRequest(r)
- if status >= http.StatusBadRequest {
- logger.Printf("Invalid URL requested: %s", r.URL) // Dev-targeted actual error
- http.NotFound(w, r) // No human-targeted details to limit enumeration attacks
- return
- }
- if isVerbose {
- logger.Printf("request: %s", r.URL.Path)
- }
- fbs, err := domain.FizzBuzz(int1, int2, limit, str1, str2)
- if err != nil {
- logger.Println(err) // Dev-targeted actual error
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) // Human-targeted error to user agent
- return
- }
- w.Header().Set(CT, JSON)
- enc := json.NewEncoder(w)
- enc.SetEscapeHTML(true) // Prevent XSS on str1/str2
- enc.SetIndent("", "\t") // Human-readable JSON
- err = enc.Encode(fbs)
- if err != nil {
- logger.Println(err) // Dev-targeted actual error
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) // human-targeted error to user agent
- return
- }
- }
- }
- // SetupRouting configures the server route on the specified base path.
- func SetupRouting(base string, logger *log.Logger, stats *Stats, isVerbose bool) (http.Handler, error) {
- restBase, err := url.JoinPath("/", base) // Perform base validation, including fixing "" as "/".
- if err != nil {
- return nil, fmt.Errorf("failed building the base path: %w", err)
- }
- sm := http.NewServeMux()
- router := mux.NewRouter()
- sub := router.PathPrefix(restBase).
- Name("base").
- Subrouter()
- sub.Handle(PathFB, stats.Middleware()(MakeFizzBuzz(logger, isVerbose))).
- Name("fizzbuzz")
- sub.Handle("/stats", stats.Handler())
- sm.Handle("/", sub)
- return sm, nil
- }
- // Serve starts the HTTP listening loop.
- func (s *Server) Serve(serverClosed chan domain.Empty) error {
- // Provide access to the globally injected services in handlers.
- baseContext := func(listener net.Listener) context.Context {
- return s.BaseContext
- }
- r, err := SetupRouting(s.Base, s.Logger, s.Stats, s.Verbose)
- if err != nil {
- s.Logger.Fatalf("failed setting up routing: %v", err)
- }
- hs := http.Server{
- Addr: s.Addr,
- BaseContext: baseContext,
- ErrorLog: s.Logger,
- Handler: r,
- IdleTimeout: 100 * time.Millisecond, // Non-zero to improve benchmarks able to reuse connections.
- MaxHeaderBytes: 8192, // Cf. Apache as industry-standard practice.
- ReadHeaderTimeout: 10 * time.Millisecond, // 8kB@1Mbps = 8 msec
- ReadTimeout: 10 * time.Millisecond, // This server has no method expecting a body, so keep this small
- TLSConfig: nil,
- WriteTimeout: s.WriteTimeout, // 1Mbps*100msec = 100kB, which should be sufficient for this API
- }
- // Setup clean shutdown on signal.
- go func() {
- sigs := make(chan os.Signal, 1)
- signal.Notify(sigs, os.Interrupt)
- <-sigs
- if err := hs.Shutdown(context.Background()); err != nil {
- s.Logger.Printf("web server shutdown: %v", err)
- }
- close(serverClosed)
- }()
- if s.Verbose {
- s.Logger.Printf("listening with config %s", s.Config)
- }
- go func() {
- err = hs.ListenAndServe()
- switch err {
- case http.ErrServerClosed:
- if s.Verbose {
- s.Logger.Printf("http server shut down cleanly")
- }
- err = nil
- default:
- s.Logger.Printf("http server unexpected error: %v", err)
- }
- }()
- <-serverClosed
- return err
- }
|