Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

267 lines
7.7KB

  1. package main
  2. import (
  3. "biukop.com/sfm/loan"
  4. "database/sql"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "github.com/brianvoe/gofakeit/v6"
  9. log "github.com/sirupsen/logrus"
  10. "net/http"
  11. "net/http/httputil"
  12. "strings"
  13. "time"
  14. )
  15. const apiV1Prefix = "/api/v1/"
  16. const apiV1WebSocket = apiV1Prefix + "ws"
  17. type apiV1HandlerMap struct {
  18. Method string
  19. Path string //regex
  20. Handler func(http.ResponseWriter, *http.Request, *loan.Session)
  21. }
  22. var apiV1Handler = setupApiV1Handler()
  23. func setupApiV1Handler() []apiV1HandlerMap {
  24. if config.Debug { //debug only
  25. return []apiV1HandlerMap{
  26. {"POST", "login", apiV1Login},
  27. {"*", "logout", apiV1Logout},
  28. {"GET", "chart/type-of-loans", apiV1ChartTypeOfLoans},
  29. {"GET", "chart/amount-of-loans", apiV1ChartTypeOfLoans},
  30. {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly},
  31. {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans},
  32. {"GET", "chart/top-broker", apiV1ChartTopBroker},
  33. {"GET", "login", apiV1DumpRequest},
  34. }
  35. } else { //production
  36. return []apiV1HandlerMap{
  37. {"POST", "login", apiV1Login},
  38. {"*", "logout", apiV1Logout},
  39. {"GET", "chart/type-of-loans", apiV1ChartTypeOfLoans},
  40. {"GET", "chart/amount-of-loans", apiV1ChartTypeOfLoans},
  41. {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly},
  42. {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans},
  43. {"GET", "chart/top-broker", apiV1ChartTopBroker},
  44. {"GET", "login", apiV1EmptyResponse},
  45. }
  46. }
  47. }
  48. //
  49. //apiV1Main version 1 main entry for all REST API
  50. //
  51. func apiV1Main(w http.ResponseWriter, r *http.Request) {
  52. //general setup
  53. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  54. //CORS setup
  55. setupCrossOriginResponse(&w, r)
  56. //if its options then we don't bother with other issues
  57. if r.Method == "OPTIONS" {
  58. apiV1EmptyResponse(w, r, nil)
  59. return //stop processing
  60. }
  61. if config.Debug {
  62. logRequestDebug(httputil.DumpRequest(r, true))
  63. }
  64. session := apiV1InitSession(r)
  65. if config.Debug {
  66. log.Debugf("session : %+v", session)
  67. }
  68. //search through handler
  69. path := r.URL.Path[len(apiV1Prefix):] //strip API prefix
  70. for _, node := range apiV1Handler {
  71. if (r.Method == node.Method || node.Method == "*") && path == node.Path {
  72. node.Handler(w, r, &session)
  73. e := session.Write() //finish this session to DB
  74. if e != nil {
  75. log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error())
  76. }
  77. return
  78. }
  79. }
  80. //Catch for all Uhandled Request
  81. e := session.Write() //finish this session to DB
  82. if e != nil {
  83. log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error())
  84. }
  85. if config.Debug {
  86. apiV1DumpRequest(w, r, &session)
  87. } else {
  88. apiV1EmptyResponse(w, r, &session)
  89. }
  90. }
  91. func apiV1InitSession(r *http.Request) (session loan.Session) {
  92. session.MarkEmpty()
  93. //track browser, and take session from cookie
  94. cookieSession, e := apiV1InitSessionByBrowserId(r)
  95. if e == nil {
  96. session = cookieSession
  97. }
  98. //try session login first, if not an empty session will be created
  99. headerSession, e := apiV1InitSessionByHttpHeader(r)
  100. if e == nil {
  101. session = headerSession
  102. }
  103. if session.IsEmpty() {
  104. session.InitGuest(time.Now().Add(loan.DefaultSessionDuration))
  105. } else {
  106. session.RenewIfExpireSoon()
  107. }
  108. //we have a session anyway
  109. session.Add("Biukop-Mid", apiV1GetMachineId(r)) //set machine id
  110. session.SetRemote(r) //make sure they are using latest remote
  111. return
  112. }
  113. func setupCrossOriginResponse(w *http.ResponseWriter, r *http.Request) {
  114. origin := r.Header.Get("Origin")
  115. if origin == "" {
  116. origin = "*"
  117. }
  118. requestedHeaders := r.Header.Get("Access-control-Request-Headers")
  119. method := r.Header.Get("Access-Control-Request-Method")
  120. (*w).Header().Set("Access-Control-Allow-Origin", origin) //for that specific origin
  121. (*w).Header().Set("Access-Control-Allow-Credentials", "true")
  122. (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, "+method)
  123. (*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)
  124. }
  125. func apiV1GetMachineId(r *http.Request) string {
  126. var mid string
  127. inCookie, e := r.Cookie("Biukop-Mid")
  128. if e == nil {
  129. mid = inCookie.Value
  130. } else {
  131. mid = gofakeit.UUID()
  132. }
  133. headerMid := r.Header.Get("Biukop-Mid")
  134. if headerMid != "" {
  135. mid = headerMid
  136. }
  137. return mid
  138. }
  139. func apiV1InitSessionByBrowserId(r *http.Request) (session loan.Session, e error) {
  140. var sid string
  141. mid := apiV1GetMachineId(r)
  142. inCookie, e := r.Cookie("Biukop-Session")
  143. if e == nil {
  144. sid = inCookie.Value
  145. if sid != "" {
  146. e = session.Read(sid)
  147. if e == nil {
  148. if mid != session.Get("Biukop-Mid") {
  149. session.MarkEmpty()
  150. }
  151. }
  152. }
  153. }
  154. return
  155. }
  156. func apiV1AddTrackingCookie(w http.ResponseWriter, r *http.Request, session *loan.Session) {
  157. //add tracking cookie
  158. expiration := time.Now().Add(365 * 24 * time.Hour)
  159. mid := apiV1GetMachineId(r)
  160. cookie := http.Cookie{Name: "Biukop-Mid", Value: mid, Expires: expiration, Path: "/", Secure: true, SameSite: http.SameSiteNoneMode}
  161. http.SetCookie(w, &cookie)
  162. if session != nil {
  163. cookie = http.Cookie{Name: "Biukop-Session", Value: session.Id, Expires: expiration, Path: "/", Secure: true, SameSite: http.SameSiteNoneMode}
  164. http.SetCookie(w, &cookie)
  165. }
  166. }
  167. func apiV1InitSessionByHttpHeader(r *http.Request) (ss loan.Session, e error) {
  168. sid := r.Header.Get("Biukop-Session")
  169. ss.MarkEmpty()
  170. //make sure session id is given
  171. if sid != "" {
  172. e = ss.Retrieve(r)
  173. if e == sql.ErrNoRows { //db does not have required session.
  174. log.Warn("DB has no corresponding session ", sid)
  175. } else if e != nil { // retrieve DB has error
  176. log.Errorf("Retrieve Session %s encountered error %s", sid, e.Error())
  177. }
  178. } else {
  179. e = errors.New("session not found: " + sid)
  180. }
  181. return
  182. }
  183. func apiV1ErrorCheck(e error) {
  184. if nil != e {
  185. panic(e.Error()) //TODO: detailed error check, truck all caller
  186. }
  187. }
  188. func apiV1Server500Error(w http.ResponseWriter, r *http.Request) {
  189. w.WriteHeader(500)
  190. apiV1AddTrackingCookie(w, r, nil) //always the last one to set cookies
  191. fmt.Fprintf(w, "Server Internal Error "+time.Now().Format(time.RFC1123))
  192. //write log
  193. dump := logRequestDebug(httputil.DumpRequest(r, true))
  194. dump = strings.TrimSpace(dump)
  195. log.Warnf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path)
  196. }
  197. func apiV1Client403Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  198. w.WriteHeader(403)
  199. type struct403 struct {
  200. Error int
  201. ErrorMsg string
  202. }
  203. e403 := struct403{Error: 403, ErrorMsg: "Not Authorized " + time.Now().Format(time.RFC1123)}
  204. msg403, _ := json.Marshal(e403)
  205. //before send out
  206. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  207. fmt.Fprintln(w, string(msg403))
  208. //write log
  209. dump := logRequestDebug(httputil.DumpRequest(r, true))
  210. dump = strings.TrimSpace(dump)
  211. log.Warnf("Not authorized http(%s) path= %s, %s", r.Method, r.URL.Path, dump)
  212. }
  213. func apiV1DumpRequest(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  214. dump := logRequestDebug(httputil.DumpRequest(r, true))
  215. dump = strings.TrimSpace(dump)
  216. msg := fmt.Sprintf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path)
  217. dumpLines := strings.Split(dump, "\r\n")
  218. ar := apiV1ResponseBlank()
  219. ar.Env.Msg = msg
  220. ar.Env.Session = *ss
  221. ar.Env.Session.Bin = []byte("masked data") //clear
  222. ar.Env.Session.Secret = "***********"
  223. ar.add("Body", dumpLines)
  224. ar.add("Biukop-Mid", ss.Get("Biukop-Mid"))
  225. b, _ := ar.toJson()
  226. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  227. fmt.Fprintf(w, "%s\n", b)
  228. }
  229. func apiV1EmptyResponse(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  230. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  231. fmt.Fprintf(w, "")
  232. }