| @@ -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, "") | |||
| } | |||