github.com/readium/readium-lcp-server@v0.0.0-20240509124024-799e77a0bbd6/api/common_server.go (about)

     1  // Copyright 2020 Readium Foundation. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license
     3  // that can be found in the LICENSE file exposed on Github (readium) in the project repository.
     4  
     5  package api
     6  
     7  import (
     8  	"fmt"
     9  	"log"
    10  	"net/http"
    11  
    12  	auth "github.com/abbot/go-http-auth"
    13  	"github.com/gorilla/mux"
    14  	"github.com/jeffbmartinez/delay"
    15  	"github.com/rs/cors"
    16  	"github.com/urfave/negroni"
    17  
    18  	"github.com/readium/readium-lcp-server/problem"
    19  )
    20  
    21  const (
    22  	// DO NOT FORGET to update the version
    23  	Software_Version = "1.9.2"
    24  
    25  	ContentType_LCP_JSON  = "application/vnd.readium.lcp.license.v1.0+json"
    26  	ContentType_LSD_JSON  = "application/vnd.readium.license.status.v1.0+json"
    27  	ContentType_TEXT_HTML = "text/html"
    28  
    29  	ContentType_JSON = "application/json"
    30  
    31  	ContentType_FORM_URL_ENCODED = "application/x-www-form-urlencoded"
    32  )
    33  
    34  type ServerRouter struct {
    35  	R *mux.Router
    36  	N *negroni.Negroni
    37  }
    38  
    39  func CreateServerRouter(tplPath string) ServerRouter {
    40  
    41  	log.Println("Software Version " + Software_Version)
    42  
    43  	r := mux.NewRouter()
    44  
    45  	//handle 404 errors
    46  	r.NotFoundHandler = http.HandlerFunc(problem.NotFoundHandler)
    47  
    48  	// this demonstrates a panic report
    49  	r.HandleFunc("/panic", func(w http.ResponseWriter, req *http.Request) {
    50  		panic("just testing. no worries.")
    51  	})
    52  
    53  	//n := negroni.Classic() == negroni.New(negroni.NewRecovery(), negroni.NewLogger(), negroni.NewStatic(...))
    54  	n := negroni.New()
    55  
    56  	// HTTP client can emit requests with custom header:
    57  	//X-Add-Delay: 300ms
    58  	//X-Add-Delay: 2.5s
    59  	n.Use(delay.Middleware{})
    60  
    61  	// possibly useful middlewares:
    62  	// https://github.com/jeffbmartinez/delay
    63  
    64  	//https://github.com/urfave/negroni#recovery
    65  	recovery := negroni.NewRecovery()
    66  	recovery.PrintStack = true
    67  	n.Use(recovery)
    68  
    69  	//https://github.com/urfave/negroni#logger
    70  	// Nov 2023, suppression of negroni logs
    71  	//n.Use(negroni.NewLogger())
    72  
    73  	// debug: log request details
    74  	//n.Use(negroni.HandlerFunc(ExtraLogger))
    75  
    76  	if tplPath != "" {
    77  		//https://github.com/urfave/negroni#static
    78  		n.Use(negroni.NewStatic(http.Dir(tplPath)))
    79  	}
    80  
    81  	// debug: log CORS details
    82  	//n.Use(negroni.HandlerFunc(CORSHeaders))
    83  
    84  	// Does not insert CORS headers as intended, depends on Origin check in the HTTP request...we want the same headers, always.
    85  	// IMPORT "github.com/rs/cors"
    86  	// //https://github.com/rs/cors#parameters
    87  	// [cors] logs depend on the Debug option (false/true)
    88  	c := cors.New(cors.Options{
    89  		AllowedOrigins: []string{"*"},
    90  		AllowedMethods: []string{"PATCH", "HEAD", "POST", "GET", "OPTIONS", "PUT", "DELETE"},
    91  		AllowedHeaders: []string{"Range", "Content-Type", "Origin", "X-Requested-With", "Accept", "Accept-Language", "Content-Language", "Authorization"},
    92  		Debug:          false,
    93  	})
    94  	n.Use(c)
    95  
    96  	n.UseHandler(r)
    97  
    98  	sr := ServerRouter{
    99  		R: r,
   100  		N: n,
   101  	}
   102  
   103  	return sr
   104  }
   105  
   106  func ExtraLogger(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
   107  
   108  	log.Print(" << -------------------")
   109  
   110  	fmt.Printf("%s => %s (%s)\n", r.RemoteAddr, r.URL.String(), r.RequestURI)
   111  
   112  	log.Println("method: ", r.Method, " path: ", r.URL.Path, " query: ", r.URL.RawQuery)
   113  
   114  	log.Printf("REQUEST headers: %#v", r.Header)
   115  
   116  	// before
   117  	next(rw, r)
   118  	// after
   119  
   120  	contentType := rw.Header().Get("Content-Type")
   121  	if contentType == problem.ContentType_PROBLEM_JSON {
   122  		log.Print("^^^^ " + problem.ContentType_PROBLEM_JSON + " ^^^^")
   123  	}
   124  
   125  	log.Printf("RESPONSE headers: %#v", rw.Header())
   126  
   127  	log.Print(" >> -------------------")
   128  }
   129  
   130  func CORSHeaders(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
   131  
   132  	rw.Header().Add("Access-Control-Allow-Methods", "PATCH, HEAD, POST, GET, OPTIONS, PUT, DELETE")
   133  	rw.Header().Add("Access-Control-Allow-Credentials", "true")
   134  	rw.Header().Add("Access-Control-Allow-Origin", "*")
   135  	rw.Header().Add("Access-Control-Allow-Headers", "Range, Content-Type, Origin, X-Requested-With, Accept, Accept-Language, Content-Language, Authorization")
   136  
   137  	// before
   138  	next(rw, r)
   139  	// after
   140  
   141  	// noop
   142  }
   143  
   144  func CheckAuth(authenticator *auth.BasicAuth, w http.ResponseWriter, r *http.Request) bool {
   145  	var username string
   146  	if username = authenticator.CheckAuth(r); username == "" {
   147  		w.Header().Set("WWW-Authenticate", `Basic realm="`+authenticator.Realm+`"`)
   148  		problem.Error(w, r, problem.Problem{Detail: "User or password do not match!"}, http.StatusUnauthorized)
   149  		return false
   150  	}
   151  	return true
   152  }