Преглед изворни кода

session management simplified. using only session id , and cookie. enables CORS support. over https only. Cookie require secure.

master
sp пре 4 година
родитељ
комит
d3173ed180
1 измењених фајлова са 139 додато и 63 уклоњено
  1. +139
    -63
      apiv1.go

+ 139
- 63
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, "")
}

Loading…
Откажи
Сачувај