From d3173ed180333f3660109c7b5aea88efdb440def Mon Sep 17 00:00:00 2001 From: sp Date: Tue, 2 Mar 2021 21:19:18 +1100 Subject: [PATCH] session management simplified. using only session id , and cookie. enables CORS support. over https only. Cookie require secure. --- apiv1.go | 202 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 139 insertions(+), 63 deletions(-) diff --git a/apiv1.go b/apiv1.go index 274ce46..83e6d1e 100644 --- a/apiv1.go +++ b/apiv1.go @@ -1,15 +1,15 @@ package main import ( - "biukop/sfm/loan" + "biukop.com/sfm/loan" "database/sql" "encoding/json" + "errors" "fmt" "github.com/brianvoe/gofakeit/v6" log "github.com/sirupsen/logrus" "net/http" "net/http/httputil" - "strconv" "strings" "time" ) @@ -22,39 +22,53 @@ type apiV1HandlerMap struct { Handler func(http.ResponseWriter, *http.Request, *loan.Session) } -var apiV1Handler = []apiV1HandlerMap{ - {"POST", "login", apiV1Login}, - {"GET", "login", apiV1DumpRequest}, +var apiV1Handler = setupApiV1Handler() + +func setupApiV1Handler() []apiV1HandlerMap { + if config.Debug { + return []apiV1HandlerMap{ + {"POST", "login", apiV1Login}, + {"GET", "login", apiV1DumpRequest}, + } + } else { + return []apiV1HandlerMap{ + {"POST", "login", apiV1Login}, + {"GET", "login", apiV1EmptyResponse}, + } + } } +// //apiV1Main version 1 main entry for all REST API // func apiV1Main(w http.ResponseWriter, r *http.Request) { - logRequestDebug(httputil.DumpRequest(r, true)) + //general setup w.Header().Set("Content-Type", "application/json;charset=UTF-8") - //track browser, and take session from cookie - session := loan.Session{} - apiV1InitSessionByBrowserId(w, r, &session) + //CORS setup + setupCrossOriginResponse(&w, r) - //try session login first, if not an empty session will be created - e := apiV1InitSessionByHttpHeader(r, &session) - if e != nil { - log.Warnf("Fail to InitSession %+v", session) - apiV1Client403Error(w, r) - return + //if its options then we don't bother with other issues + if r.Method == "OPTIONS" { + apiV1EmptyResponse(w, r, nil) + return //stop processing + } + + if config.Debug { + logRequestDebug(httputil.DumpRequest(r, true)) + } + + session := apiV1InitSession(r) + if config.Debug { + log.Debugf("session : %+v", session) } - session.RenewIfExpireSoon() - session.SetRemote(r) //make sure they are using latest remote - session.Add("mid", apiV1GetMachineId(r)) //set machine id - //we have a session now, either guest or valid user //search through handler path := r.URL.Path[len(apiV1Prefix):] //strip API prefix for _, node := range apiV1Handler { - if r.Method == node.Method && path == node.Path { + if (r.Method == node.Method || node.Method == "*") && path == node.Path { node.Handler(w, r, &session) - e = session.Write() //finish this session to DB + e := session.Write() //finish this session to DB if e != nil { log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error()) } @@ -62,84 +76,119 @@ func apiV1Main(w http.ResponseWriter, r *http.Request) { } } - //Catch for all - e = session.Write() //finish this session to DB - apiV1DumpRequest(w, r, &session) + //Catch for all Uhandled Request + e := session.Write() //finish this session to DB + if e != nil { + log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error()) + } + if config.Debug { + apiV1DumpRequest(w, r, &session) + } else { + apiV1EmptyResponse(w, r, &session) + } + +} + +func apiV1InitSession(r *http.Request) (session loan.Session) { + session.MarkEmpty() + + //track browser, and take session from cookie + cookieSession, e := apiV1InitSessionByBrowserId(r) + if e == nil { + session = cookieSession + } + + //try session login first, if not an empty session will be created + headerSession, e := apiV1InitSessionByHttpHeader(r) + if e == nil { + session = headerSession + } + + if session.IsEmpty() { + session.InitGuest(time.Now().Add(loan.DefaultSessionDuration)) + } else { + session.RenewIfExpireSoon() + } + //we have a session anyway + session.Add("Biukop-Mid", apiV1GetMachineId(r)) //set machine id + session.SetRemote(r) //make sure they are using latest remote + return +} + +func setupCrossOriginResponse(w *http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + if origin == "" { + origin = "*" + } + requestedHeaders := r.Header.Get("Access-control-Request-Headers") + method := r.Header.Get("Access-Control-Request-Method") + (*w).Header().Set("Access-Control-Allow-Origin", origin) //for that specific origin + (*w).Header().Set("Access-Control-Allow-Credentials", "true") + (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, "+method) + (*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Session-Token, Biukop-Session-Expire, "+requestedHeaders) } func apiV1GetMachineId(r *http.Request) string { var mid string - inCookie, e := r.Cookie("mid") + inCookie, e := r.Cookie("Biukop-Mid") if e == nil { mid = inCookie.Value } else { - mid = strconv.Itoa(int(time.Now().Unix())) + "-" + gofakeit.UUID() + mid = gofakeit.UUID() + } + + headerMid := r.Header.Get("Biukop-Mid") + if headerMid != "" { + mid = headerMid } return mid } -func apiV1InitSessionByBrowserId(w http.ResponseWriter, r *http.Request, session *loan.Session) { - mid := apiV1GetMachineId(r) - +func apiV1InitSessionByBrowserId(r *http.Request) (session loan.Session, e error) { var sid string - inCookie, e := r.Cookie("session") + mid := apiV1GetMachineId(r) + inCookie, e := r.Cookie("Biukop-Session") if e == nil { sid = inCookie.Value if sid != "" { e = session.Read(sid) if e == nil { - if mid != session.Get("mid") { + if mid != session.Get("Biukop-Mid") { session.MarkEmpty() } } - } else { //create a new session - session.InitGuest(time.Now().Add(loan.DefaultSessionDuration)) - session.Add("mid", mid) } } + return } func apiV1AddTrackingCookie(w http.ResponseWriter, r *http.Request, session *loan.Session) { //add tracking cookie expiration := time.Now().Add(365 * 24 * time.Hour) - cookie := http.Cookie{Name: "session", Value: session.Id, Expires: expiration} - http.SetCookie(w, &cookie) - mid := apiV1GetMachineId(r) - cookie = http.Cookie{Name: "mid", Value: mid, Expires: expiration} + cookie := http.Cookie{Name: "Biukop-Mid", Value: mid, Expires: expiration, Path: "/", Secure: true, SameSite: http.SameSiteNoneMode} http.SetCookie(w, &cookie) + + if session != nil { + cookie = http.Cookie{Name: "Biukop-Session", Value: session.Id, Expires: expiration, Path: "/", Secure: true, SameSite: http.SameSiteNoneMode} + http.SetCookie(w, &cookie) + } } -func apiV1InitSessionByHttpHeader(r *http.Request, ss *loan.Session) (e error) { +func apiV1InitSessionByHttpHeader(r *http.Request) (ss loan.Session, e error) { sid := r.Header.Get("Biukop-Session") - + ss.MarkEmpty() //make sure session id is given if sid != "" { - //Try to retrieve a copy of DB session - trial := loan.Session{} - e = trial.Retrieve(r) - if e == nil { //we got existing session from DB - e = trial.ValidateRequest(r) - if e != nil { // db session does not match request - log.Warnf("failed session login %+v, %s", ss, time.Now().Format(time.RFC1123)) - ss.InitGuest(time.Now().Add(loan.DefaultSessionDuration)) - e = nil - } else { //else, we have logged this user - *ss = trial //overwrite the incoming session - } - } else if e == sql.ErrNoRows { //db does not have required session. + e = ss.Retrieve(r) + if e == sql.ErrNoRows { //db does not have required session. log.Warn("DB has no corresponding session ", sid) - } else { // retrieve has error - log.Warnf("Retrieve Session %s encountered error %s", sid, e.Error()) + } else if e != nil { // retrieve DB has error + log.Errorf("Retrieve Session %s encountered error %s", sid, e.Error()) } + } else { + e = errors.New("session not found: " + sid) } - - //if there is not session incoming - if ss.IsEmpty() { //cookie may have initialized a session - ss.InitGuest(time.Now().Add(loan.DefaultSessionDuration)) - e = nil //we try to init an empty one - } - return } @@ -152,6 +201,7 @@ func apiV1ErrorCheck(e error) { func apiV1Server500Error(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) + apiV1AddTrackingCookie(w, r, nil) //always the last one to set cookies fmt.Fprintf(w, "Server Internal Error "+time.Now().Format(time.RFC1123)) //write log @@ -160,7 +210,7 @@ func apiV1Server500Error(w http.ResponseWriter, r *http.Request) { log.Warnf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path) } -func apiV1Client403Error(w http.ResponseWriter, r *http.Request) { +func apiV1Client403Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) { w.WriteHeader(403) type struct403 struct { Error int @@ -168,6 +218,9 @@ func apiV1Client403Error(w http.ResponseWriter, r *http.Request) { } e403 := struct403{Error: 403, ErrorMsg: "Not Authorized " + time.Now().Format(time.RFC1123)} msg403, _ := json.Marshal(e403) + + //before send out + apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies fmt.Fprintln(w, string(msg403)) //write log @@ -175,3 +228,26 @@ func apiV1Client403Error(w http.ResponseWriter, r *http.Request) { dump = strings.TrimSpace(dump) log.Warnf("Not authorized http(%s) path= %s, %s", r.Method, r.URL.Path, dump) } + +func apiV1DumpRequest(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + dump := logRequestDebug(httputil.DumpRequest(r, true)) + dump = strings.TrimSpace(dump) + msg := fmt.Sprintf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path) + dumpLines := strings.Split(dump, "\r\n") + ar := apiV1ResponseBlank() + ar.Env.Msg = msg + ar.Env.Session = *ss + ar.Env.Session.Bin = []byte("masked data") //clear + ar.Env.Session.Secret = "***********" + ar.add("Body", dumpLines) + ar.add("Biukop-Mid", ss.Get("Biukop-Mid")) + b, _ := ar.toJson() + + apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies + fmt.Fprintf(w, "%s\n", b) +} + +func apiV1EmptyResponse(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies + fmt.Fprintf(w, "") +}