Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

312 lignes
9.3KB

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