package main import ( "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" "strings" "time" ) const apiV1Prefix = "/api/v1/" const apiV1WebSocket = apiV1Prefix + "ws" type apiV1HandlerMap struct { Method string Path string //regex Handler func(http.ResponseWriter, *http.Request, *loan.Session) } var apiV1Handler = setupApiV1Handler() func setupApiV1Handler() []apiV1HandlerMap { if config.Debug { //debug only return []apiV1HandlerMap{ {"POST", "login", apiV1Login}, {"*", "logout", apiV1Logout}, {"GET", "chart/type-of-loans", apiV1ChartTypeOfLoans}, {"GET", "chart/amount-of-loans", apiV1ChartTypeOfLoans}, {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly}, {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans}, {"GET", "chart/top-broker", apiV1ChartTopBroker}, {"POST", "grid/loan/full-loan-overview", apiV1GridLoanFullOverview}, {"GET", "loan/", apiV1LoanSingleGet}, {"POST", "loan/basic/", apiV1LoanSinglePostBasic}, {"GET", "avatar/", apiV1Avatar}, {"GET", "login", apiV1DumpRequest}, } } else { //production return []apiV1HandlerMap{ {"POST", "login", apiV1Login}, {"*", "logout", apiV1Logout}, {"GET", "chart/type-of-loans", apiV1ChartTypeOfLoans}, {"GET", "chart/amount-of-loans", apiV1ChartTypeOfLoans}, {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly}, {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans}, {"GET", "chart/top-broker", apiV1ChartTopBroker}, {"POST", "grid/loan/full-loan-overview", apiV1GridLoanFullOverview}, {"GET", "loan/", apiV1LoanSingleGet}, {"POST", "loan/basic/", apiV1LoanSinglePostBasic}, {"GET", "avatar/", apiV1Avatar}, {"GET", "login", apiV1EmptyResponse}, } } } // //apiV1Main version 1 main entry for all REST API // func apiV1Main(w http.ResponseWriter, r *http.Request) { //general setup w.Header().Set("Content-Type", "application/json;charset=UTF-8") //CORS setup setupCrossOriginResponse(&w, r) //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) } //search through handler path := r.URL.Path[len(apiV1Prefix):] //strip API prefix for _, node := range apiV1Handler { //log.Println(node, path, strings.HasPrefix(path, node.Path)) if (r.Method == node.Method || node.Method == "*") && strings.HasPrefix(path, node.Path) { node.Handler(w, r, &session) 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()) } return } } //Catch for all UnHandled 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("Biukop-Mid") if e == nil { mid = inCookie.Value } else { mid = gofakeit.UUID() } headerMid := r.Header.Get("Biukop-Mid") if headerMid != "" { mid = headerMid } return mid } func apiV1InitSessionByBrowserId(r *http.Request) (session loan.Session, e error) { var sid string 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("Biukop-Mid") { session.MarkEmpty() } } } } return } func apiV1AddTrackingCookie(w http.ResponseWriter, r *http.Request, session *loan.Session) { //set session header too. w.Header().Add("Access-Control-Expose-Headers", "Biukop-Session") if session == nil { w.Header().Add("Biukop-Session", "") } else { w.Header().Add("Biukop-Session", session.Id) } //add tracking cookie expiration := time.Now().Add(365 * 24 * time.Hour) mid := apiV1GetMachineId(r) 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) { sid := r.Header.Get("Biukop-Session") ss.MarkEmpty() //make sure session id is given if sid != "" { e = ss.Retrieve(r) if e == sql.ErrNoRows { //db does not have required session. log.Warn("DB has no corresponding session ", sid) } 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) } return return } func apiV1ErrorCheck(e error) { if nil != e { panic(e.Error()) //TODO: detailed error check, truck all caller } } 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 dump := logRequestDebug(httputil.DumpRequest(r, true)) dump = strings.TrimSpace(dump) log.Warnf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path) } func apiV1Client403Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) { w.WriteHeader(403) type struct403 struct { Error int ErrorMsg string } 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 dump := logRequestDebug(httputil.DumpRequest(r, true)) dump = strings.TrimSpace(dump) log.Warnf("Not authorized http(%s) path= %s, %s", r.Method, r.URL.Path, dump) } func apiV1Client404Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) { w.WriteHeader(404) type struct404 struct { Error int ErrorMsg string } e404 := struct404{Error: 404, ErrorMsg: "Not Found " + time.Now().Format(time.RFC1123)} msg404, _ := json.Marshal(e404) //before send out apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies fmt.Fprintln(w, string(msg404)) //write log dump := logRequestDebug(httputil.DumpRequest(r, true)) dump = strings.TrimSpace(dump) log.Warnf("Not found 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, "") }