| package main | package main | ||||
| import ( | import ( | ||||
| "biukop/sfm/loan" | |||||
| "biukop.com/sfm/loan" | |||||
| "database/sql" | "database/sql" | ||||
| "encoding/json" | "encoding/json" | ||||
| "errors" | |||||
| "fmt" | "fmt" | ||||
| "github.com/brianvoe/gofakeit/v6" | "github.com/brianvoe/gofakeit/v6" | ||||
| log "github.com/sirupsen/logrus" | log "github.com/sirupsen/logrus" | ||||
| "net/http" | "net/http" | ||||
| "net/http/httputil" | "net/http/httputil" | ||||
| "strconv" | |||||
| "strings" | "strings" | ||||
| "time" | "time" | ||||
| ) | ) | ||||
| Handler func(http.ResponseWriter, *http.Request, *loan.Session) | 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 | //apiV1Main version 1 main entry for all REST API | ||||
| // | // | ||||
| func apiV1Main(w http.ResponseWriter, r *http.Request) { | 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") | 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 | //search through handler | ||||
| path := r.URL.Path[len(apiV1Prefix):] //strip API prefix | path := r.URL.Path[len(apiV1Prefix):] //strip API prefix | ||||
| for _, node := range apiV1Handler { | 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) | node.Handler(w, r, &session) | ||||
| e = session.Write() //finish this session to DB | |||||
| e := session.Write() //finish this session to DB | |||||
| if e != nil { | if e != nil { | ||||
| log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error()) | log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error()) | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| //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 { | func apiV1GetMachineId(r *http.Request) string { | ||||
| var mid string | var mid string | ||||
| inCookie, e := r.Cookie("mid") | |||||
| inCookie, e := r.Cookie("Biukop-Mid") | |||||
| if e == nil { | if e == nil { | ||||
| mid = inCookie.Value | mid = inCookie.Value | ||||
| } else { | } 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 | 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 | var sid string | ||||
| inCookie, e := r.Cookie("session") | |||||
| mid := apiV1GetMachineId(r) | |||||
| inCookie, e := r.Cookie("Biukop-Session") | |||||
| if e == nil { | if e == nil { | ||||
| sid = inCookie.Value | sid = inCookie.Value | ||||
| if sid != "" { | if sid != "" { | ||||
| e = session.Read(sid) | e = session.Read(sid) | ||||
| if e == nil { | if e == nil { | ||||
| if mid != session.Get("mid") { | |||||
| if mid != session.Get("Biukop-Mid") { | |||||
| session.MarkEmpty() | 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) { | func apiV1AddTrackingCookie(w http.ResponseWriter, r *http.Request, session *loan.Session) { | ||||
| //add tracking cookie | //add tracking cookie | ||||
| expiration := time.Now().Add(365 * 24 * time.Hour) | 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) | 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) | 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") | sid := r.Header.Get("Biukop-Session") | ||||
| ss.MarkEmpty() | |||||
| //make sure session id is given | //make sure session id is given | ||||
| if sid != "" { | 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) | 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 | return | ||||
| } | } | ||||
| func apiV1Server500Error(w http.ResponseWriter, r *http.Request) { | func apiV1Server500Error(w http.ResponseWriter, r *http.Request) { | ||||
| w.WriteHeader(500) | 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)) | fmt.Fprintf(w, "Server Internal Error "+time.Now().Format(time.RFC1123)) | ||||
| //write log | //write log | ||||
| log.Warnf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path) | 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) | w.WriteHeader(403) | ||||
| type struct403 struct { | type struct403 struct { | ||||
| Error int | Error int | ||||
| } | } | ||||
| e403 := struct403{Error: 403, ErrorMsg: "Not Authorized " + time.Now().Format(time.RFC1123)} | e403 := struct403{Error: 403, ErrorMsg: "Not Authorized " + time.Now().Format(time.RFC1123)} | ||||
| msg403, _ := json.Marshal(e403) | msg403, _ := json.Marshal(e403) | ||||
| //before send out | |||||
| apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies | |||||
| fmt.Fprintln(w, string(msg403)) | fmt.Fprintln(w, string(msg403)) | ||||
| //write log | //write log | ||||
| dump = strings.TrimSpace(dump) | dump = strings.TrimSpace(dump) | ||||
| log.Warnf("Not authorized http(%s) path= %s, %s", r.Method, r.URL.Path, 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, "") | |||||
| } |