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", apiV1ChartAmountOfLoans}, {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly}, {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans}, {"GET", "chart/top-broker", apiV1ChartTopBroker}, //{"POST", "grid/loan/full-loan-overview", apiV1GridLoanFullOverview}, {"POST", "grid/loan/full-loan-overview", apiV1GridListLoanOverview}, {"GET", "chart/reward-vs-income-monthly", apiV1ChartRewardVsIncomeMonthly}, {"GET", "loan/", apiV1LoanSingleGet}, {"DELETE", "loan/", apiV1LoanSingleDelete}, {"GET", "loan-by-client/", apiV1LoanByClient}, {"GET", "people/", apiV1PeopleGet}, {"POST", "people/", apiV1PeoplePost}, {"PUT", "people/", apiV1PeoplePut}, {"DELETE", "people/", apiV1PeopleDelete}, {"GET", "people-extra/", apiV1PeopleExtraGet}, {"POST", "user/", apiV1UserPost}, {"PUT", "user/", apiV1UserPut}, {"DELETE", "user/", apiV1UserDelete}, {"POST", "user-enable/", apiV1UserEnable}, {"GET", "user-ex/", apiV1UserExGet}, {"POST", "user-ex/", apiV1UserExPost}, {"GET", "user-ex-list/", apiV1UserExList}, {"GET", "broker/", apiV1BrokerGet}, {"POST", "broker/", apiV1BrokerPost}, {"PUT", "broker/", apiV1BrokerPut}, {"DELETE", "broker/", apiV1BrokerDelete}, {"POST", "change-pass/", apiV1ChangePass}, {"POST", "loan/basic/", apiV1LoanSinglePostBasic}, {"GET", "avatar/", apiV1Avatar}, {"POST", "avatar/", apiV1AvatarPost}, {"POST", "reward/", apiV1RewardPost}, {"DELETE", "reward/", apiV1RewardDelete}, {"GET", "people-list/", apiV1PeopleList}, {"GET", "broker-list/", apiV1BrokerList}, {"POST", "sync-people/", apiV1SyncPeople}, {"POST", "payIn/", apiV1PayInPost}, {"DELETE", "payIn/", apiV1PayInDelete}, {"POST", "pay-in-list/", apiV1PayInList}, {"POST", "pay-in-filtered-list/", apiV1FilteredPayInList}, {"GET", "user-reward/", apiV1UserReward}, {"GET", "login-available/", apiV1LoginAvailable}, {"POST", "lender-upload/", apiV1UploadsPost}, {"GET", "upload-analysis/", apiV1UploadAnalysis}, {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis}, {"GET", "upload-as-image/", apiV1UploadAsImage}, {"PUT", "upload-as-image/", apiV1UploadCreateImage}, {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail}, {"GET", "upload-as-pdf/", apiV1UploadAsPDF}, {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF}, {"GET", "upload-original/", apiV1UploadOriginalFileGet}, {"GET", "upload/", apiV1UploadMetaGet}, {"DELETE", "upload/", apiV1UploadDelete}, {"POST", "upload-meta-list/", apiV1UploadMetaList}, {"GET", "lender-list/", apiV1LenderList}, {"GET", "payout-ex/", apiV1PayOutExGet}, {"POST", "reward-ex-list/", apiV1RewardExListPost}, {"POST", "payout/", apiV1PayOutPost}, {"POST", "payout-prepared/", apiV1PayOutPrepared}, {"POST", "payout-unprepared/", apiV1PayOutUnprepared}, {"POST", "payout-approved/", apiV1PayOutApproved}, {"POST", "payout-unapproved/", apiV1PayOutUnapproved}, {"POST", "payout-paid/", apiV1PayOutPaid}, {"POST", "payout-unpaid/", apiV1PayOutUnpaid}, {"GET", "login", apiV1DumpRequest}, } } else { //production return []apiV1HandlerMap{ {"POST", "login", apiV1Login}, {"*", "logout", apiV1Logout}, {"GET", "chart/type-of-loans", apiV1ChartTypeOfLoans}, {"GET", "chart/amount-of-loans", apiV1ChartAmountOfLoans}, {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly}, {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans}, {"GET", "chart/top-broker", apiV1ChartTopBroker}, //{"POST", "grid/loan/full-loan-overview", apiV1GridLoanFullOverview}, {"POST", "grid/loan/full-loan-overview", apiV1GridListLoanOverview}, {"GET", "chart/reward-vs-income-monthly", apiV1ChartRewardVsIncomeMonthly}, {"GET", "loan/", apiV1LoanSingleGet}, {"DELETE", "loan/", apiV1LoanSingleDelete}, {"GET", "loan-by-client/", apiV1LoanByClient}, {"GET", "people/", apiV1PeopleGet}, {"POST", "people/", apiV1PeoplePost}, {"PUT", "people/", apiV1PeoplePut}, {"DELETE", "people/", apiV1PeopleDelete}, {"GET", "people-extra/", apiV1PeopleExtraGet}, {"POST", "user/", apiV1UserPost}, {"PUT", "user/", apiV1UserPut}, {"DELETE", "user/", apiV1UserDelete}, {"POST", "user-enable/", apiV1UserEnable}, {"GET", "user-ex/", apiV1UserExGet}, {"POST", "user-ex/", apiV1UserExPost}, {"GET", "user-ex-list/", apiV1UserExList}, {"GET", "broker/", apiV1BrokerGet}, {"POST", "broker/", apiV1BrokerPost}, {"PUT", "broker/", apiV1BrokerPut}, {"DELETE", "broker/", apiV1BrokerDelete}, {"POST", "change-pass/", apiV1ChangePass}, {"POST", "loan/basic/", apiV1LoanSinglePostBasic}, {"GET", "avatar/", apiV1Avatar}, {"POST", "avatar/", apiV1AvatarPost}, {"POST", "reward/", apiV1RewardPost}, {"DELETE", "reward/", apiV1RewardDelete}, {"GET", "people-list", apiV1PeopleList}, {"GET", "broker-list/", apiV1BrokerList}, {"POST", "sync-people/", apiV1SyncPeople}, {"POST", "payIn/", apiV1PayInPost}, {"DELETE", "payIn/", apiV1PayInDelete}, {"POST", "pay-in-list/", apiV1PayInList}, {"POST", "pay-in-filtered-list/", apiV1FilteredPayInList}, {"GET", "user-reward/", apiV1UserReward}, {"GET", "login-available/", apiV1LoginAvailable}, {"POST", "lender-upload/", apiV1UploadsPost}, {"GET", "upload-analysis/", apiV1UploadAnalysis}, {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis}, {"GET", "upload-as-image/", apiV1UploadAsImage}, {"PUT", "upload-as-image/", apiV1UploadCreateImage}, {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail}, {"GET", "upload-as-pdf/", apiV1UploadAsPDF}, {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF}, {"GET", "upload-original/", apiV1UploadOriginalFileGet}, {"GET", "upload-meta/", apiV1UploadMetaGet}, {"DELETE", "upload/", apiV1UploadDelete}, {"POST", "upload-meta-list/", apiV1UploadMetaList}, {"GET", "lender-list/", apiV1LenderList}, {"GET", "payout-ex/", apiV1PayOutExGet}, {"POST", "reward-ex-list/", apiV1RewardExListPost}, {"POST", "payout/", apiV1PayOutPost}, {"POST", "payout-prepared/", apiV1PayOutPrepared}, {"POST", "payout-unprepared/", apiV1PayOutUnprepared}, {"POST", "payout-approved/", apiV1PayOutApproved}, {"POST", "payout-unapproved/", apiV1PayOutUnapproved}, {"POST", "payout-paid/", apiV1PayOutPaid}, {"POST", "payout-unpaid/", apiV1PayOutUnpaid}, {"GET", "login", apiV1EmptyResponse}, } } } // //apiV1Main version 1 main entry for all REST API // func apiV1Main(w http.ResponseWriter, r *http.Request) { //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 := loan.Session{} session.MarkEmpty() bypassSession := apiV1NoNeedSession(r) if !bypassSession { // no point to create session for preflight session = apiV1InitSession(r) if config.Debug { log.Debugf("session : %+v", session) } } //search through handler path := r.URL.Path[len(apiV1Prefix):] //strip API prefix handled := false for _, node := range apiV1Handler { if (strings.ToUpper(r.Method) == node.Method || node.Method == "*") && strings.HasPrefix(path, node.Path) { handled = true node.Handler(w, r, &session) break //stop search handler further } } if !bypassSession { // no point to write session for preflight 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()) } } //Catch for all UnHandled Request if !handled { if config.Debug { apiV1DumpRequest(w, r, &session) } else { apiV1EmptyResponse(w, r, &session) } } } func apiV1NoNeedSession(r *http.Request) bool { if r.Method == "OPTIONS" { return true } path := r.URL.Path[len(apiV1Prefix):] //strip API prefix if r.Method == "GET" { if strings.HasPrefix(path, "avatar/") || strings.HasPrefix(path, "upload-original/") || strings.HasPrefix(path, "upload-as-image/") || strings.HasPrefix(path, "upload-as-analysis/") || strings.HasPrefix(path, "upload-as-thumbnail/") || strings.HasPrefix(path, "upload-as-pdf/") { return true } } return false } func apiV1InitSession(r *http.Request) (session loan.Session) { session.MarkEmpty() //try session login first, if not an empty session will be created headerSession, e := apiV1InitSessionByHttpHeader(r) if e == nil { session = headerSession } else { // if session from header failed, we try cookie session // track browser, and take session from cookie // cookie has disadvantage that multiple tab will get overwritten cookieSession, e := apiV1InitSessionByCookie(r) if e == nil { session = cookieSession } } 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.Add("Biukop-Socket", apiV1GetSocketId(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", removeDupHeaderOptions("POST, GET, OPTIONS, PUT, DELETE, "+method)) (*w).Header().Set("Access-Control-Allow-Headers", removeDupHeaderOptions("Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Socket , "+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 apiV1GetSocketId(r *http.Request) string { socketId := r.Header.Get("Biukop-Socket") return socketId } func apiV1GetCORSHeaders(r *http.Request) (ret string) { requestedHeaders := r.Header.Get("Access-control-Request-Headers") ret = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Session-Token, Biukop-Session-Expire," + requestedHeaders return removeDupHeaderOptions(ret) } func removeDupHeaderOptions(inStr string) (out string) { headers := map[string]struct{}{} strings.ReplaceAll(inStr, " ", "") // remove space headerArray := strings.Split(inStr, ",") // split for _, v := range headerArray { headers[v] = struct{}{} // same key will overwrite each other } out = "" for k, _ := range headers { if out != "" { out += ", " } out += k } return } func apiV1GetMachineIdFromSession(ss *loan.Session) string { return ss.GetStr("Biukop-Mid") } func apiV1InitSessionByCookie(r *http.Request) (session loan.Session, e error) { var sid string session.MarkEmpty() 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) { if strings.ToUpper(r.Method) == "OPTION" { return } w.Header().Add("Access-Control-Expose-Headers", apiV1GetCORSHeaders(r)) apiV1AddTrackingSession(w, r, session) apiV1AddTrackingMachineId(w, r, session) } func apiV1AddTrackingSession(w http.ResponseWriter, r *http.Request, session *loan.Session) { sessionId := "" if session == nil { log.Warn("non-exist session, empty Id sent", session) // w.Header().Add("Biukop-Session", "") } else { if session.Id == "" { log.Warn("empty session, empty Id sent", session) } else { w.Header().Add("Biukop-Session", session.Id) sessionId = session.Id } } cookie := http.Cookie{ Name: "Biukop-Session", Value: sessionId, // may be "" Expires: time.Now().Add(365 * 24 * time.Hour), Path: "/", Secure: true, SameSite: http.SameSiteNoneMode} http.SetCookie(w, &cookie) } func apiV1AddTrackingMachineId(w http.ResponseWriter, r *http.Request, session *loan.Session) { mid := apiV1GetMachineId(r) expiration := time.Now().Add(365 * 24 * time.Hour) w.Header().Add("Biukop-Mid", mid) cookie := http.Cookie{ Name: "Biukop-Mid", Value: mid, Expires: expiration, Path: "/", Secure: true, SameSite: http.SameSiteNoneMode} http.SetCookie(w, &cookie) } func apiV1InitSessionByHttpHeader(r *http.Request) (ss loan.Session, e error) { ss.MarkEmpty() sid := apiV1GetSessionIdFromRequest(r) //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 } func apiV1GetSessionIdFromRequest(r *http.Request) string { sid := "" inCookie, e := r.Cookie("Biukop-Session") if e == nil { sid = inCookie.Value } headerSid := r.Header.Get("Biukop-Session") if headerSid != "" { sid = headerSid } return sid } func apiV1Server500Error(w http.ResponseWriter, r *http.Request) { //general setup w.Header().Set("Content-Type", "application/json;charset=UTF-8") 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) { //general setup w.Header().Set("Content-Type", "application/json;charset=UTF-8") 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) { //general setup w.Header().Set("Content-Type", "application/json;charset=UTF-8") 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) { //general setup w.Header().Set("Content-Type", "application/json;charset=UTF-8") apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies fmt.Fprintf(w, "") }