No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

517 líneas
15KB

  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", "chart/reward-vs-income-monthly", apiV1ChartRewardVsIncomeMonthly},
  35. {"GET", "loan/", apiV1LoanSingleGet},
  36. {"DELETE", "loan/", apiV1LoanSingleDelete},
  37. {"GET", "loan-by-client/", apiV1LoanByClient},
  38. {"GET", "people/", apiV1PeopleGet},
  39. {"POST", "people/", apiV1PeoplePost},
  40. {"PUT", "people/", apiV1PeoplePut},
  41. {"DELETE", "people/", apiV1PeopleDelete},
  42. {"GET", "people-extra/", apiV1PeopleExtraGet},
  43. {"POST", "user/", apiV1UserPost},
  44. {"PUT", "user/", apiV1UserPut},
  45. {"DELETE", "user/", apiV1UserDelete},
  46. {"POST", "user-enable/", apiV1UserEnable},
  47. {"GET", "user-ex/", apiV1UserExGet},
  48. {"GET", "broker/", apiV1BrokerGet},
  49. {"POST", "broker/", apiV1BrokerPost},
  50. {"PUT", "broker/", apiV1BrokerPut},
  51. {"DELETE", "broker/", apiV1BrokerDelete},
  52. {"POST", "change-pass/", apiV1ChangePass},
  53. {"POST", "loan/basic/", apiV1LoanSinglePostBasic},
  54. {"GET", "avatar/", apiV1Avatar},
  55. {"POST", "avatar/", apiV1AvatarPost},
  56. {"POST", "reward/", apiV1RewardPost},
  57. {"DELETE", "reward/", apiV1RewardDelete},
  58. {"GET", "people-list/", apiV1PeopleList},
  59. {"GET", "broker-list/", apiV1BrokerList},
  60. {"POST", "sync-people/", apiV1SyncPeople},
  61. {"POST", "payIn/", apiV1PayInPost},
  62. {"DELETE", "payIn/", apiV1PayInDelete},
  63. {"POST", "pay-in-list/", apiV1PayInList},
  64. {"GET", "user-reward/", apiV1UserReward},
  65. {"GET", "login-available/", apiV1LoginAvailable},
  66. {"POST", "lender-upload/", apiV1UploadsPost},
  67. {"GET", "upload-analysis/", apiV1UploadAnalysis},
  68. {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis},
  69. {"GET", "upload-as-image/", apiV1UploadAsImage},
  70. {"PUT", "upload-as-image/", apiV1UploadCreateImage},
  71. {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail},
  72. {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail},
  73. {"GET", "upload-as-pdf/", apiV1UploadAsPDF},
  74. {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF},
  75. {"GET", "upload-original/", apiV1UploadOriginalFileGet},
  76. {"GET", "upload/", apiV1UploadMetaGet},
  77. {"DELETE", "upload/", apiV1UploadDelete},
  78. {"POST", "upload-meta-list/", apiV1UploadMetaList},
  79. {"GET", "lender-list/", apiV1LenderList},
  80. {"GET", "payout-ex/", apiV1PayOutExGet},
  81. {"GET", "login", apiV1DumpRequest},
  82. }
  83. } else { //production
  84. return []apiV1HandlerMap{
  85. {"POST", "login", apiV1Login},
  86. {"*", "logout", apiV1Logout},
  87. {"GET", "chart/type-of-loans", apiV1ChartTypeOfLoans},
  88. {"GET", "chart/amount-of-loans", apiV1ChartTypeOfLoans},
  89. {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly},
  90. {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans},
  91. {"GET", "chart/top-broker", apiV1ChartTopBroker},
  92. {"POST", "grid/loan/full-loan-overview", apiV1GridLoanFullOverview},
  93. {"GET", "chart/reward-vs-income-monthly", apiV1ChartRewardVsIncomeMonthly},
  94. {"GET", "loan/", apiV1LoanSingleGet},
  95. {"DELETE", "loan/", apiV1LoanSingleDelete},
  96. {"GET", "loan-by-client/", apiV1LoanByClient},
  97. {"GET", "people/", apiV1PeopleGet},
  98. {"POST", "people/", apiV1PeoplePost},
  99. {"PUT", "people/", apiV1PeoplePut},
  100. {"DELETE", "people/", apiV1PeopleDelete},
  101. {"GET", "people-extra/", apiV1PeopleExtraGet},
  102. {"POST", "user/", apiV1UserPost},
  103. {"PUT", "user/", apiV1UserPut},
  104. {"DELETE", "user/", apiV1UserDelete},
  105. {"POST", "user-enable/", apiV1UserEnable},
  106. {"GET", "user-ex/", apiV1UserExGet},
  107. {"GET", "broker/", apiV1BrokerGet},
  108. {"POST", "broker/", apiV1BrokerPost},
  109. {"PUT", "broker/", apiV1BrokerPut},
  110. {"DELETE", "broker/", apiV1BrokerDelete},
  111. {"POST", "change-pass/", apiV1ChangePass},
  112. {"POST", "loan/basic/", apiV1LoanSinglePostBasic},
  113. {"GET", "avatar/", apiV1Avatar},
  114. {"POST", "avatar/", apiV1AvatarPost},
  115. {"POST", "reward/", apiV1RewardPost},
  116. {"DELETE", "reward/", apiV1RewardDelete},
  117. {"GET", "people-list", apiV1PeopleList},
  118. {"GET", "broker-list/", apiV1BrokerList},
  119. {"POST", "sync-people/", apiV1SyncPeople},
  120. {"POST", "payIn/", apiV1PayInPost},
  121. {"DELETE", "payIn/", apiV1PayInDelete},
  122. {"POST", "pay-in-list/", apiV1PayInList},
  123. {"GET", "user-reward/", apiV1UserReward},
  124. {"GET", "login-available/", apiV1LoginAvailable},
  125. {"POST", "lender-upload/", apiV1UploadsPost},
  126. {"GET", "upload-analysis/", apiV1UploadAnalysis},
  127. {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis},
  128. {"GET", "upload-as-image/", apiV1UploadAsImage},
  129. {"PUT", "upload-as-image/", apiV1UploadCreateImage},
  130. {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail},
  131. {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail},
  132. {"GET", "upload-as-pdf/", apiV1UploadAsPDF},
  133. {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF},
  134. {"GET", "upload-original/", apiV1UploadOriginalFileGet},
  135. {"GET", "upload-meta/", apiV1UploadMetaGet},
  136. {"DELETE", "upload/", apiV1UploadDelete},
  137. {"POST", "upload-meta-list/", apiV1UploadMetaList},
  138. {"GET", "lender-list/", apiV1LenderList},
  139. {"GET", "payout-ex/", apiV1PayOutExGet},
  140. {"GET", "login", apiV1EmptyResponse},
  141. }
  142. }
  143. }
  144. //
  145. //apiV1Main version 1 main entry for all REST API
  146. //
  147. func apiV1Main(w http.ResponseWriter, r *http.Request) {
  148. //CORS setup
  149. setupCrossOriginResponse(&w, r)
  150. //if its options then we don't bother with other issues
  151. if r.Method == "OPTIONS" {
  152. apiV1EmptyResponse(w, r, nil)
  153. return //stop processing
  154. }
  155. if config.Debug {
  156. logRequestDebug(httputil.DumpRequest(r, true))
  157. }
  158. session := loan.Session{}
  159. session.MarkEmpty()
  160. bypassSession := apiV1NoNeedSession(r)
  161. if !bypassSession { // no point to create session for preflight
  162. session = apiV1InitSession(r)
  163. if config.Debug {
  164. log.Debugf("session : %+v", session)
  165. }
  166. }
  167. //search through handler
  168. path := r.URL.Path[len(apiV1Prefix):] //strip API prefix
  169. handled := false
  170. for _, node := range apiV1Handler {
  171. if (strings.ToUpper(r.Method) == node.Method || node.Method == "*") && strings.HasPrefix(path, node.Path) {
  172. handled = true
  173. node.Handler(w, r, &session)
  174. break //stop search handler further
  175. }
  176. }
  177. if !bypassSession { // no point to write session for preflight
  178. e := session.Write() //finish this session to DB
  179. if e != nil {
  180. log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error())
  181. }
  182. }
  183. //Catch for all UnHandled Request
  184. if !handled {
  185. if config.Debug {
  186. apiV1DumpRequest(w, r, &session)
  187. } else {
  188. apiV1EmptyResponse(w, r, &session)
  189. }
  190. }
  191. }
  192. func apiV1NoNeedSession(r *http.Request) bool {
  193. if r.Method == "OPTIONS" {
  194. return true
  195. }
  196. path := r.URL.Path[len(apiV1Prefix):] //strip API prefix
  197. if r.Method == "GET" {
  198. if strings.HasPrefix(path, "avatar/") ||
  199. strings.HasPrefix(path, "upload-original/") ||
  200. strings.HasPrefix(path, "upload-as-image/") ||
  201. strings.HasPrefix(path, "upload-as-analysis/") ||
  202. strings.HasPrefix(path, "upload-as-thumbnail/") ||
  203. strings.HasPrefix(path, "upload-as-pdf/") {
  204. return true
  205. }
  206. }
  207. return false
  208. }
  209. func apiV1InitSession(r *http.Request) (session loan.Session) {
  210. session.MarkEmpty()
  211. //track browser, and take session from cookie
  212. cookieSession, e := apiV1InitSessionByCookie(r)
  213. if e == nil {
  214. session = cookieSession
  215. }
  216. //try session login first, if not an empty session will be created
  217. headerSession, e := apiV1InitSessionByHttpHeader(r)
  218. if e == nil {
  219. session = headerSession
  220. }
  221. if session.IsEmpty() {
  222. session.InitGuest(time.Now().Add(loan.DefaultSessionDuration))
  223. } else {
  224. session.RenewIfExpireSoon()
  225. }
  226. //we have a session anyway
  227. session.Add("Biukop-Mid", apiV1GetMachineId(r)) //set machine id
  228. session.SetRemote(r) //make sure they are using latest remote
  229. return
  230. }
  231. func setupCrossOriginResponse(w *http.ResponseWriter, r *http.Request) {
  232. origin := r.Header.Get("Origin")
  233. if origin == "" {
  234. origin = "*"
  235. }
  236. requestedHeaders := r.Header.Get("Access-control-Request-Headers")
  237. method := r.Header.Get("Access-Control-Request-Method")
  238. (*w).Header().Set("Access-Control-Allow-Origin", origin) //for that specific origin
  239. (*w).Header().Set("Access-Control-Allow-Credentials", "true")
  240. (*w).Header().Set("Access-Control-Allow-Methods", removeDupHeaderOptions("POST, GET, OPTIONS, PUT, DELETE, "+method))
  241. (*w).Header().Set("Access-Control-Allow-Headers", removeDupHeaderOptions("Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Session-Token, Biukop-Session-Expire, "+requestedHeaders))
  242. }
  243. func apiV1GetMachineId(r *http.Request) string {
  244. var mid string
  245. inCookie, e := r.Cookie("Biukop-Mid")
  246. if e == nil {
  247. mid = inCookie.Value
  248. } else {
  249. mid = gofakeit.UUID()
  250. }
  251. headerMid := r.Header.Get("Biukop-Mid")
  252. if headerMid != "" {
  253. mid = headerMid
  254. }
  255. return mid
  256. }
  257. func apiV1GetCORSHeaders(r *http.Request) (ret string) {
  258. requestedHeaders := r.Header.Get("Access-control-Request-Headers")
  259. ret = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Session-Token, Biukop-Session-Expire," + requestedHeaders
  260. return removeDupHeaderOptions(ret)
  261. }
  262. func removeDupHeaderOptions(inStr string) (out string) {
  263. headers := map[string]struct{}{}
  264. strings.ReplaceAll(inStr, " ", "") // remove space
  265. headerArray := strings.Split(inStr, ",") // split
  266. for _, v := range headerArray {
  267. headers[v] = struct{}{} // same key will overwrite each other
  268. }
  269. out = ""
  270. for k, _ := range headers {
  271. if out != "" {
  272. out += ", "
  273. }
  274. out += k
  275. }
  276. return
  277. }
  278. func apiV1GetMachineIdFromSession(ss *loan.Session) string {
  279. return ss.GetStr("Biukop-Mid")
  280. }
  281. func apiV1InitSessionByCookie(r *http.Request) (session loan.Session, e error) {
  282. var sid string
  283. session.MarkEmpty()
  284. mid := apiV1GetMachineId(r)
  285. inCookie, e := r.Cookie("Biukop-Session")
  286. if e == nil {
  287. sid = inCookie.Value
  288. if sid != "" {
  289. e = session.Read(sid)
  290. if e == nil {
  291. if mid != session.Get("Biukop-Mid") {
  292. session.MarkEmpty()
  293. }
  294. }
  295. }
  296. }
  297. return
  298. }
  299. func apiV1AddTrackingCookie(w http.ResponseWriter, r *http.Request, session *loan.Session) {
  300. if strings.ToUpper(r.Method) == "OPTION" {
  301. return
  302. }
  303. w.Header().Add("Access-Control-Expose-Headers", apiV1GetCORSHeaders(r))
  304. apiV1AddTrackingSession(w, r, session)
  305. apiV1AddTrackingMachineId(w, r, session)
  306. }
  307. func apiV1AddTrackingSession(w http.ResponseWriter, r *http.Request, session *loan.Session) {
  308. sessionId := ""
  309. if session == nil {
  310. log.Warn("non-exist session, empty Id sent", session)
  311. w.Header().Add("Biukop-Session", "")
  312. } else {
  313. if session.Id == "" {
  314. log.Warn("empty session, empty Id sent", session)
  315. } else {
  316. w.Header().Add("Biukop-Session", session.Id)
  317. sessionId = session.Id
  318. }
  319. }
  320. cookie := http.Cookie{
  321. Name: "Biukop-Session",
  322. Value: sessionId, // may be ""
  323. Expires: time.Now().Add(365 * 24 * time.Hour),
  324. Path: "/",
  325. Secure: true,
  326. SameSite: http.SameSiteNoneMode}
  327. http.SetCookie(w, &cookie)
  328. }
  329. func apiV1AddTrackingMachineId(w http.ResponseWriter, r *http.Request, session *loan.Session) {
  330. mid := apiV1GetMachineId(r)
  331. expiration := time.Now().Add(365 * 24 * time.Hour)
  332. w.Header().Add("Biukop-Mid", mid)
  333. cookie := http.Cookie{
  334. Name: "Biukop-Mid",
  335. Value: mid,
  336. Expires: expiration,
  337. Path: "/",
  338. Secure: true,
  339. SameSite: http.SameSiteNoneMode}
  340. http.SetCookie(w, &cookie)
  341. }
  342. func apiV1InitSessionByHttpHeader(r *http.Request) (ss loan.Session, e error) {
  343. ss.MarkEmpty()
  344. sid := apiV1GetSessionIdFromRequest(r)
  345. //make sure session id is given
  346. if sid != "" {
  347. e = ss.Retrieve(r)
  348. if e == sql.ErrNoRows { //db does not have required session.
  349. log.Warn("DB has no corresponding session ", sid)
  350. } else if e != nil { // retrieve DB has error
  351. log.Errorf("Retrieve Session %s encountered error %s", sid, e.Error())
  352. }
  353. } else {
  354. e = errors.New("session not found: " + sid)
  355. }
  356. return
  357. }
  358. func apiV1GetSessionIdFromRequest(r *http.Request) string {
  359. sid := ""
  360. inCookie, e := r.Cookie("Biukop-Session")
  361. if e == nil {
  362. sid = inCookie.Value
  363. }
  364. headerSid := r.Header.Get("Biukop-Session")
  365. if headerSid != "" {
  366. sid = headerSid
  367. }
  368. return sid
  369. }
  370. func apiV1Server500Error(w http.ResponseWriter, r *http.Request) {
  371. //general setup
  372. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  373. w.WriteHeader(500)
  374. apiV1AddTrackingCookie(w, r, nil) //always the last one to set cookies
  375. fmt.Fprintf(w, "Server Internal Error "+time.Now().Format(time.RFC1123))
  376. //write log
  377. dump := logRequestDebug(httputil.DumpRequest(r, true))
  378. dump = strings.TrimSpace(dump)
  379. log.Warnf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path)
  380. }
  381. func apiV1Client403Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  382. //general setup
  383. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  384. w.WriteHeader(403)
  385. type struct403 struct {
  386. Error int
  387. ErrorMsg string
  388. }
  389. e403 := struct403{Error: 403, ErrorMsg: "Not Authorized " + time.Now().Format(time.RFC1123)}
  390. msg403, _ := json.Marshal(e403)
  391. //before send out
  392. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  393. fmt.Fprintln(w, string(msg403))
  394. //write log
  395. dump := logRequestDebug(httputil.DumpRequest(r, true))
  396. dump = strings.TrimSpace(dump)
  397. log.Warnf("Not authorized http(%s) path= %s, %s", r.Method, r.URL.Path, dump)
  398. }
  399. func apiV1Client404Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  400. //general setup
  401. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  402. w.WriteHeader(404)
  403. type struct404 struct {
  404. Error int
  405. ErrorMsg string
  406. }
  407. e404 := struct404{Error: 404, ErrorMsg: "Not Found " + time.Now().Format(time.RFC1123)}
  408. msg404, _ := json.Marshal(e404)
  409. //before send out
  410. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  411. fmt.Fprintln(w, string(msg404))
  412. //write log
  413. dump := logRequestDebug(httputil.DumpRequest(r, true))
  414. dump = strings.TrimSpace(dump)
  415. log.Warnf("Not found http(%s) path= %s, %s", r.Method, r.URL.Path, dump)
  416. }
  417. func apiV1DumpRequest(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  418. dump := logRequestDebug(httputil.DumpRequest(r, true))
  419. dump = strings.TrimSpace(dump)
  420. msg := fmt.Sprintf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path)
  421. dumpLines := strings.Split(dump, "\r\n")
  422. ar := apiV1ResponseBlank()
  423. ar.Env.Msg = msg
  424. ar.Env.Session = *ss
  425. ar.Env.Session.Bin = []byte("masked data") //clear
  426. ar.Env.Session.Secret = "***********"
  427. ar.add("Body", dumpLines)
  428. ar.add("Biukop-Mid", ss.Get("Biukop-Mid"))
  429. b, _ := ar.toJson()
  430. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  431. fmt.Fprintf(w, "%s\n", b)
  432. }
  433. func apiV1EmptyResponse(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  434. //general setup
  435. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  436. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  437. fmt.Fprintf(w, "")
  438. }