You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

254 lines
6.9KB

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