diff --git a/backend/internal/http/geoip/geoip.go b/backend/internal/http/geoip/geoip.go index 96086b643..398c79808 100644 --- a/backend/internal/http/geoip/geoip.go +++ b/backend/internal/http/geoip/geoip.go @@ -4,6 +4,7 @@ import ( "github.com/oschwald/maxminddb-golang" "log" "net" + "strings" ) type geoIPRecord struct { @@ -24,6 +25,24 @@ type GeoRecord struct { City string } +func (r *GeoRecord) Pack() string { + return r.Country + "|" + r.State + "|" + r.City +} + +func UnpackGeoRecord(pkg string) *GeoRecord { + parts := strings.Split(pkg, "|") + if len(parts) != 3 { + return &GeoRecord{ + Country: pkg, + } + } + return &GeoRecord{ + Country: parts[0], + State: parts[1], + City: parts[2], + } +} + type GeoParser interface { Parse(ip net.IP) *GeoRecord } diff --git a/backend/internal/http/router/handlers-ios.go b/backend/internal/http/router/handlers-ios.go deleted file mode 100644 index 1bcc2708d..000000000 --- a/backend/internal/http/router/handlers-ios.go +++ /dev/null @@ -1,177 +0,0 @@ -package router - -import ( - "encoding/json" - "errors" - "log" - "math/rand" - "net/http" - "openreplay/backend/internal/http/ios" - "openreplay/backend/internal/http/util" - "openreplay/backend/internal/http/uuid" - "openreplay/backend/pkg/objectstorage" - "strconv" - "time" - - "openreplay/backend/pkg/db/postgres" - . "openreplay/backend/pkg/messages" - "openreplay/backend/pkg/token" -) - -func (e *Router) startSessionHandlerIOS(w http.ResponseWriter, r *http.Request) { - startTime := time.Now() - req := &StartIOSSessionRequest{} - - if r.Body == nil { - ResponseWithError(w, http.StatusBadRequest, errors.New("request body is empty"), startTime, r.URL.Path, 0) - return - } - body := http.MaxBytesReader(w, r.Body, e.cfg.JsonSizeLimit) - defer body.Close() - - if err := json.NewDecoder(body).Decode(req); err != nil { - ResponseWithError(w, http.StatusBadRequest, err, startTime, r.URL.Path, 0) - return - } - - if req.ProjectKey == nil { - ResponseWithError(w, http.StatusForbidden, errors.New("ProjectKey value required"), startTime, r.URL.Path, 0) - return - } - - p, err := e.services.Database.GetProjectByKey(*req.ProjectKey) - if err != nil { - if postgres.IsNoRowsErr(err) { - ResponseWithError(w, http.StatusNotFound, errors.New("Project doesn't exist or is not active"), startTime, r.URL.Path, 0) - } else { - ResponseWithError(w, http.StatusInternalServerError, err, startTime, r.URL.Path, 0) // TODO: send error here only on staging - } - return - } - userUUID := uuid.GetUUID(req.UserUUID) - tokenData, err := e.services.Tokenizer.Parse(req.Token) - - if err != nil { // Starting the new one - dice := byte(rand.Intn(100)) // [0, 100) - if dice >= p.SampleRate { - ResponseWithError(w, http.StatusForbidden, errors.New("cancel"), startTime, r.URL.Path, 0) - return - } - - ua := e.services.UaParser.ParseFromHTTPRequest(r) - if ua == nil { - ResponseWithError(w, http.StatusForbidden, errors.New("browser not recognized"), startTime, r.URL.Path, 0) - return - } - sessionID, err := e.services.Flaker.Compose(uint64(startTime.UnixMilli())) - if err != nil { - ResponseWithError(w, http.StatusInternalServerError, err, startTime, r.URL.Path, 0) - return - } - // TODO: if EXPIRED => send message for two sessions association - expTime := startTime.Add(time.Duration(p.MaxSessionDuration) * time.Millisecond) - tokenData = &token.TokenData{sessionID, 0, expTime.UnixMilli()} - geoInfo := e.ExtractGeoData(r) - - // The difference with web is mostly here: - sessStart := &IOSSessionStart{ - Timestamp: req.Timestamp, - ProjectID: uint64(p.ProjectID), - TrackerVersion: req.TrackerVersion, - RevID: req.RevID, - UserUUID: userUUID, - UserOS: "IOS", - UserOSVersion: req.UserOSVersion, - UserDevice: ios.MapIOSDevice(req.UserDevice), - UserDeviceType: ios.GetIOSDeviceType(req.UserDevice), - UserCountry: geoInfo.Country, - } - e.services.Producer.Produce(e.cfg.TopicRawIOS, tokenData.ID, sessStart.Encode()) - } - - ResponseWithJSON(w, &StartIOSSessionResponse{ - Token: e.services.Tokenizer.Compose(*tokenData), - UserUUID: userUUID, - SessionID: strconv.FormatUint(tokenData.ID, 10), - BeaconSizeLimit: e.cfg.BeaconSizeLimit, - }, startTime, r.URL.Path, 0) -} - -func (e *Router) pushMessagesHandlerIOS(w http.ResponseWriter, r *http.Request) { - startTime := time.Now() - sessionData, err := e.services.Tokenizer.ParseFromHTTPRequest(r) - if err != nil { - ResponseWithError(w, http.StatusUnauthorized, err, startTime, r.URL.Path, 0) - return - } - e.pushMessages(w, r, sessionData.ID, e.cfg.TopicRawIOS) -} - -func (e *Router) pushLateMessagesHandlerIOS(w http.ResponseWriter, r *http.Request) { - startTime := time.Now() - sessionData, err := e.services.Tokenizer.ParseFromHTTPRequest(r) - if err != nil && err != token.EXPIRED { - ResponseWithError(w, http.StatusUnauthorized, err, startTime, r.URL.Path, 0) - return - } - // Check timestamps here? - e.pushMessages(w, r, sessionData.ID, e.cfg.TopicRawIOS) -} - -func (e *Router) imagesUploadHandlerIOS(w http.ResponseWriter, r *http.Request) { - startTime := time.Now() - log.Printf("recieved imagerequest") - - sessionData, err := e.services.Tokenizer.ParseFromHTTPRequest(r) - if err != nil { // Should accept expired token? - ResponseWithError(w, http.StatusUnauthorized, err, startTime, r.URL.Path, 0) - return - } - - if r.Body == nil { - ResponseWithError(w, http.StatusBadRequest, errors.New("request body is empty"), startTime, r.URL.Path, 0) - return - } - r.Body = http.MaxBytesReader(w, r.Body, e.cfg.FileSizeLimit) - defer r.Body.Close() - - err = r.ParseMultipartForm(1e6) // ~1Mb - if err == http.ErrNotMultipart || err == http.ErrMissingBoundary { - ResponseWithError(w, http.StatusUnsupportedMediaType, err, startTime, r.URL.Path, 0) - return - // } else if err == multipart.ErrMessageTooLarge // if non-files part exceeds 10 MB - } else if err != nil { - ResponseWithError(w, http.StatusInternalServerError, err, startTime, r.URL.Path, 0) // TODO: send error here only on staging - return - } - - if r.MultipartForm == nil { - ResponseWithError(w, http.StatusInternalServerError, errors.New("Multipart not parsed"), startTime, r.URL.Path, 0) - return - } - - if len(r.MultipartForm.Value["projectKey"]) == 0 { - ResponseWithError(w, http.StatusBadRequest, errors.New("projectKey parameter missing"), startTime, r.URL.Path, 0) // status for missing/wrong parameter? - return - } - - prefix := r.MultipartForm.Value["projectKey"][0] + "/" + strconv.FormatUint(sessionData.ID, 10) + "/" - - for _, fileHeaderList := range r.MultipartForm.File { - for _, fileHeader := range fileHeaderList { - file, err := fileHeader.Open() - if err != nil { - continue // TODO: send server error or accumulate successful files - } - key := prefix + fileHeader.Filename - log.Printf("Uploading image... %v", util.SafeString(key)) - go func() { //TODO: mime type from header - if err := e.services.Storage.Upload(file, key, "image/jpeg", objectstorage.NoCompression); err != nil { - log.Printf("Upload ios screen error. %v", err) - } - }() - } - } - - w.WriteHeader(http.StatusOK) -} diff --git a/backend/internal/http/router/handlers-web.go b/backend/internal/http/router/handlers-web.go index 6fd469f8e..e66e351d4 100644 --- a/backend/internal/http/router/handlers-web.go +++ b/backend/internal/http/router/handlers-web.go @@ -155,7 +155,7 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request) UserBrowserVersion: ua.BrowserVersion, UserDevice: ua.Device, UserDeviceType: ua.DeviceType, - UserCountry: geoInfo.Country, + UserCountry: geoInfo.Pack(), UserDeviceMemorySize: req.DeviceMemory, UserDeviceHeapSize: req.JsHeapSizeLimit, UserID: req.UserID, diff --git a/backend/internal/http/router/model.go b/backend/internal/http/router/model.go index fa6249ae7..0b3a3234e 100644 --- a/backend/internal/http/router/model.go +++ b/backend/internal/http/router/model.go @@ -31,22 +31,3 @@ type NotStartedRequest struct { TrackerVersion string `json:"trackerVersion"` DoNotTrack bool `json:"DoNotTrack"` } - -type StartIOSSessionRequest struct { - Token string `json:"token"` - ProjectKey *string `json:"projectKey"` - TrackerVersion string `json:"trackerVersion"` - RevID string `json:"revID"` - UserUUID *string `json:"userUUID"` - UserOSVersion string `json:"userOSVersion"` - UserDevice string `json:"userDevice"` - Timestamp uint64 `json:"timestamp"` -} - -type StartIOSSessionResponse struct { - Token string `json:"token"` - ImagesHashList []string `json:"imagesHashList"` - UserUUID string `json:"userUUID"` - BeaconSizeLimit int64 `json:"beaconSizeLimit"` - SessionID string `json:"sessionID"` -} diff --git a/backend/pkg/db/cache/messages-web.go b/backend/pkg/db/cache/messages-web.go index e33d203d5..08ba7f591 100644 --- a/backend/pkg/db/cache/messages-web.go +++ b/backend/pkg/db/cache/messages-web.go @@ -37,19 +37,21 @@ func (c *PGCache) HandleWebSessionStart(s *SessionStart) error { if c.Cache.HasSession(sessionID) { return fmt.Errorf("session %d already in cache", sessionID) } + geoInfo := geoip.UnpackGeoRecord(s.UserCountry) newSess := &Session{ - SessionID: sessionID, - Platform: "web", - Timestamp: s.Timestamp, - ProjectID: uint32(s.ProjectID), - TrackerVersion: s.TrackerVersion, - RevID: s.RevID, - UserUUID: s.UserUUID, - UserOS: s.UserOS, - UserOSVersion: s.UserOSVersion, - UserDevice: s.UserDevice, - UserCountry: s.UserCountry, - // web properties (TODO: unite different platform types) + SessionID: sessionID, + Platform: "web", + Timestamp: s.Timestamp, + ProjectID: uint32(s.ProjectID), + TrackerVersion: s.TrackerVersion, + RevID: s.RevID, + UserUUID: s.UserUUID, + UserOS: s.UserOS, + UserOSVersion: s.UserOSVersion, + UserDevice: s.UserDevice, + UserCountry: geoInfo.Country, + UserState: geoInfo.State, + UserCity: geoInfo.City, UserAgent: s.UserAgent, UserBrowser: s.UserBrowser, UserBrowserVersion: s.UserBrowserVersion, @@ -60,7 +62,6 @@ func (c *PGCache) HandleWebSessionStart(s *SessionStart) error { } c.Cache.SetSession(newSess) if err := c.Conn.HandleSessionStart(sessionID, newSess); err != nil { - // don't know why? c.Cache.SetSession(nil) return err } diff --git a/backend/pkg/db/postgres/messages-common.go b/backend/pkg/db/postgres/messages-common.go index eb7508168..80f17eb62 100644 --- a/backend/pkg/db/postgres/messages-common.go +++ b/backend/pkg/db/postgres/messages-common.go @@ -55,6 +55,8 @@ func (conn *Conn) HandleSessionStart(sessionID uint64, s *types.Session) error { conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USEROS", s.Platform), s.UserOS) conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USERDEVICE", s.Platform), s.UserDevice) conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USERCOUNTRY", s.Platform), s.UserCountry) + conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USERSTATE", s.Platform), s.UserState) + conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USERCITY", s.Platform), s.UserCity) conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("REVID", s.Platform), s.RevID) // s.Platform == "web" conn.insertAutocompleteValue(sessionID, s.ProjectID, "USERBROWSER", s.UserBrowser)