package core import ( "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "path" "strings" "sync/atomic" "time" "unsafe" common "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/logger" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo/options" ) type blockinfo struct { Start primitive.DateTime End primitive.DateTime `bson:"_ts"` Reason string } type whitelistAuthType = string const ( whitelistAuthType_Default = whitelistAuthType("") whitelistAuthType_QA = whitelistAuthType("qa") ) type whitelistmember struct { Service string Email string Platform string Desc string Auth []whitelistAuthType Expired primitive.DateTime `bson:"_ts,omitempty" json:"_ts,omitempty"` } type whitelist struct { emailptr unsafe.Pointer qaptr unsafe.Pointer working int32 } type usertokeninfo struct { platform string userid string token string //refreshtoken secret string brinfo string accesstoken string // microsoft only accesstoken_expire_time int64 // microsoft only } func (wl *whitelist) init(total []whitelistmember) { auths := make(map[string]map[string]*whitelistmember) for _, member := range total { all := auths[""] if all == nil { all = make(map[string]*whitelistmember) auths[""] = all } all[whitelistKey(member.Email)] = &member for _, auth := range member.Auth { spec := auths[auth] if spec == nil { spec = make(map[string]*whitelistmember) auths[auth] = spec } spec[whitelistKey(member.Email)] = &member } } all := auths[whitelistAuthType_Default] atomic.StorePointer(&wl.emailptr, unsafe.Pointer(&all)) qa := auths[whitelistAuthType_QA] atomic.StorePointer(&wl.qaptr, unsafe.Pointer(&qa)) } func addToUnsafePointer(to *unsafe.Pointer, m *whitelistmember) { ptr := atomic.LoadPointer(to) src := (*map[string]*whitelistmember)(ptr) next := map[string]*whitelistmember{} for k, v := range *src { next[k] = v } next[whitelistKey(m.Email)] = m atomic.StorePointer(to, unsafe.Pointer(&next)) } func removeFromUnsafePointer(from *unsafe.Pointer, email string) { ptr := atomic.LoadPointer(from) src := (*map[string]*whitelistmember)(ptr) next := make(map[string]*whitelistmember) for k, v := range *src { next[k] = v } delete(next, whitelistKey(email)) atomic.StorePointer(from, unsafe.Pointer(&next)) } func (wl *whitelist) add(m *whitelistmember) { addToUnsafePointer(&wl.emailptr, m) for _, auth := range m.Auth { if auth == whitelistAuthType_QA { addToUnsafePointer(&wl.qaptr, m) } } } func (wl *whitelist) remove(email string) { removeFromUnsafePointer(&wl.emailptr, email) removeFromUnsafePointer(&wl.qaptr, email) } func (wl *whitelist) isMember(email string, platform string) bool { if atomic.LoadInt32(&wl.working) == 0 { return true } ptr := atomic.LoadPointer(&wl.emailptr) src := *(*map[string]*whitelistmember)(ptr) if member, exists := src[whitelistKey(email)]; exists { return member.Platform == platform } return false } func (wl *whitelist) hasAuth(email string, platform string, auth whitelistAuthType) bool { if auth == whitelistAuthType_QA { ptr := atomic.LoadPointer(&wl.qaptr) src := *(*map[string]*whitelistmember)(ptr) if member, exists := src[whitelistKey(email)]; exists { return member.Platform == platform } } return false } type DivisionStateName string const ( DivisionState_Closed = DivisionStateName("closed") DivisionState_Maintenance = DivisionStateName("maintenance") DivisionState_RestrictedOpen = DivisionStateName("restricted") DivisionState_FullOpen = DivisionStateName("open") ) type Maintenance struct { Notice string `bson:"notice" json:"notice"` StartTimeUTC int64 `bson:"start_unixtime_utc" json:"start_unixtime_utc"` link string } type Division struct { Url string // 요것은 클라이언트 빌드하고 나서 json:"-"으로 변경하자. 클라이언트에 직접 내려보내지 않음 Priority int State DivisionStateName Maintenance *Maintenance `bson:",omitempty" json:",omitempty"` } type ServiceDescriptionSummary struct { Id primitive.ObjectID `bson:"_id"` ServiceName string `bson:"service"` ServiceCode string `bson:"code"` UseWhitelist bool `bson:"use_whitelist"` Closed bool `bson:"closed"` } type serviceDescription struct { ServiceDescriptionSummary `bson:",inline"` Divisions map[string]*Division `bson:"divisions"` ServerApiTokens []primitive.ObjectID `bson:"api_tokens"` ApiUsers map[string][]string `bson:"api_users"` auths *common.AuthCollection wl whitelist mongoClient common.MongoClient sessionTTL time.Duration closed int32 serviceCodeBytes []byte getUserBrowserInfo func(r *http.Request) (string, error) getUserTokenWithCheck func(platform string, userid string, brinfo string) (usertokeninfo, error) updateUserinfo func(info usertokeninfo) (bool, string, string) getProviderInfo func(platform string, uid string) (string, string, error) apiUsers unsafe.Pointer divisionsSerialized unsafe.Pointer serviceSerialized unsafe.Pointer } func (sh *serviceDescription) readProfile(authtype string, id string, binfo string) (email string, err error) { defer func() { s := recover() if s != nil { logger.Error("readProfile failed :", authtype, id, s) if errt, ok := s.(error); ok { err = errt } else { err = errors.New(fmt.Sprint(s)) } } }() userinfo, err := sh.getUserTokenWithCheck(authtype, id, binfo) if err != nil { return "", err } if userinfo.token == "" { return "", errors.New("refreshtoken token not found") } //-- 토큰으로 모두 확인이 끝났으면 갱신한다. ok, _, email := sh.updateUserinfo(userinfo) if !ok { return "", errors.New("updateUserinfo failed") } return email, nil } func (sh *serviceDescription) prepare(mg *Maingate) error { divs := sh.Divisions if len(sh.ServiceCode) == 0 { sh.ServiceCode = hex.EncodeToString(sh.Id[6:]) } var closed []string for dn, div := range divs { if div.State == DivisionState_Closed { closed = append(closed, dn) continue } if len(div.State) == 0 { div.State = DivisionState_FullOpen } if div.State != DivisionState_FullOpen { if div.Maintenance == nil { div.Maintenance = &Maintenance{} } if len(div.Maintenance.link) == 0 { if len(div.Maintenance.Notice) == 0 { div.Maintenance.link = "https://www.action2quare.com" } else if strings.HasPrefix(div.Maintenance.Notice, "http") { div.Maintenance.link = div.Maintenance.Notice } else { div.Maintenance.link = path.Join("static", sh.ServiceCode, div.Maintenance.Notice) } } } else { div.Maintenance = nil } } for _, dn := range closed { delete(divs, dn) } divmarshaled, _ := json.Marshal(divs) devstr := string(divmarshaled) sh.divisionsSerialized = unsafe.Pointer(&devstr) sh.mongoClient = mg.mongoClient sh.auths = mg.auths sh.sessionTTL = time.Duration(mg.SessionTTL * int64(time.Second)) sh.wl = whitelist{} sh.serviceCodeBytes, _ = hex.DecodeString(sh.ServiceCode) sh.getUserBrowserInfo = mg.GetUserBrowserInfo sh.getUserTokenWithCheck = mg.getUserTokenWithCheck sh.updateUserinfo = mg.updateUserinfo sh.getProviderInfo = mg.getProviderInfo if sh.Closed { sh.closed = 1 } else { sh.closed = 0 } var whites []whitelistmember if err := mg.mongoClient.FindAllAs(CollectionWhitelist, bson.M{ "$or": []bson.M{{"service": sh.ServiceName}, {"service": sh.ServiceCode}}, }, &whites, options.Find().SetReturnKey(false)); err != nil { return err } sh.wl.init(whites) if sh.UseWhitelist { sh.wl.working = 1 } else { sh.wl.working = 0 } if len(sh.ApiUsers) == 0 { sh.ApiUsers = map[string][]string{ "service": {}, "whitelist": {}, "account": {}, "maintenance": {}, } } parsedUsers := make(map[string]map[string]bool) for cat, users := range sh.ApiUsers { catusers := make(map[string]bool) for _, user := range users { catusers[user] = true } parsedUsers[cat] = catusers } sh.apiUsers = unsafe.Pointer(&parsedUsers) for _, keyid := range sh.ServerApiTokens { mg.apiTokenToService.add(keyid.Hex(), sh.ServiceCode) } bt, _ := json.Marshal(sh.ServiceDescriptionSummary) atomic.StorePointer(&sh.serviceSerialized, unsafe.Pointer(&bt)) logger.Println("service is ready :", sh.ServiceName, sh.ServiceCode, sh.UseWhitelist, sh.ApiUsers, string(divmarshaled)) return nil } func (sh *serviceDescription) link(w http.ResponseWriter, r *http.Request) { defer func() { s := recover() if s != nil { logger.Error(s) } }() if r.Method != "GET" { w.WriteHeader(http.StatusBadRequest) return } queryvals := r.URL.Query() //oldToken := queryvals.Get("otoken") oldType := queryvals.Get("otype") oldId := queryvals.Get("oid") sk := queryvals.Get("sk") //newToken := queryvals.Get("ntoken") newType := queryvals.Get("ntype") newId := queryvals.Get("nid") oldAuth := sh.auths.Find(sk) if oldAuth == nil { // 잘못된 세션 logger.Println("link failed. session key is not valid :", sk) w.WriteHeader(http.StatusBadRequest) return } // fmt.Println("=================") // fmt.Println(oldType) // fmt.Println(oldId) // fmt.Println("=================") // fmt.Println(newType) // fmt.Println(newId) // fmt.Println("=================") // fmt.Println(oldAuth.Platform) // fmt.Println(oldAuth.Uid) // fmt.Println("=================") //if oldAuth.Token != oldToken || oldAuth.Uid != oldId || oldAuth.Platform != oldType { if oldAuth.Uid != oldId || oldAuth.Platform != oldType { logger.Println("link failed. session key is not correct :", *oldAuth, queryvals) w.WriteHeader(http.StatusBadRequest) return } bfinfo, err := sh.getUserBrowserInfo(r) if err != nil { logger.Error("getUserBrowserInfo failed :", err) w.WriteHeader(http.StatusBadRequest) return } _, err = sh.readProfile(oldType, oldId, bfinfo) if err != nil { logger.Error("readProfile(old) failed :", err) w.WriteHeader(http.StatusBadRequest) return } email, err := sh.readProfile(newType, newId, bfinfo) if err != nil { logger.Error("readProfile(new) failed :", err) w.WriteHeader(http.StatusBadRequest) return } // if len(email) == 0 { // logger.Println("link failed. email is missing :", r.URL.Query()) // w.WriteHeader(http.StatusBadRequest) // return // } if !sh.wl.isMember(email, newType) { logger.Println("link failed. not whitelist member :", r.URL.Query(), email) w.WriteHeader(http.StatusBadRequest) return } newType, newId, err = sh.getProviderInfo(newType, newId) if err != nil { logger.Error("getProviderInfo failed :", err) w.WriteHeader(http.StatusBadRequest) } createtime := primitive.NewDateTimeFromTime(time.Now().UTC()) link, err := sh.mongoClient.FindOneAndUpdate(CollectionLink, bson.M{ "platform": newType, "uid": newId, }, bson.M{ "$setOnInsert": bson.M{ "create": createtime, "email": email, }, }, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"_id": 1})) if err != nil { logger.Error("link failed. FindOneAndUpdate link err:", err) w.WriteHeader(http.StatusInternalServerError) return } _, newid, err := sh.mongoClient.Update(common.CollectionName(sh.ServiceName), bson.M{ "_id": link["_id"].(primitive.ObjectID), }, bson.M{ "$setOnInsert": bson.M{ "accid": oldAuth.Accid, "create": createtime, }, }, options.Update().SetUpsert(true)) if err != nil { logger.Error("link failed. Update ServiceName err :", err) w.WriteHeader(http.StatusBadRequest) return } // newid가 있어야 한다. 그래야 기존 서비스 계정이 없는 상태이다. if newid == nil { // 이미 계정이 있네? logger.Println("link failed. already have service account :", r.URL.Query()) w.WriteHeader(http.StatusBadRequest) return } logger.Println("link success :", r.URL.Query()) } func (sh *serviceDescription) isValidAPIUser(category string, email string) bool { ptr := atomic.LoadPointer(&sh.apiUsers) catusers := *(*map[string]map[string]bool)(ptr) if category == "*" { for _, users := range catusers { if _, ok := users[email]; ok { return true } } logger.Println("isValidAPIUser failed. email is not allowed :", category, email, catusers) return false } if users, ok := catusers[category]; ok { if _, ok := users[email]; ok { return true } logger.Println("isValidAPIUser failed. email is not allowed :", category, email, users) return false } logger.Println("isValidAPIUser failed. category is missing :", category) return false } func (sh *serviceDescription) authorize(w http.ResponseWriter, r *http.Request) { defer func() { s := recover() if s != nil { logger.Error(s) } }() if r.Method != "GET" { w.WriteHeader(http.StatusBadRequest) return } queryvals := r.URL.Query() authtype := queryvals.Get("type") uid := queryvals.Get("id") //accesstoken := queryvals.Get("token") //-- 이거 이제 받지마라 session := queryvals.Get("sk") //email, err := sh.readProfile(authtype, uid, accesstoken) bfinfo, err := sh.getUserBrowserInfo(r) if err != nil { logger.Error("getUserBrowserInfo failed :", err) w.WriteHeader(http.StatusBadRequest) return } email, err := sh.readProfile(authtype, uid, bfinfo) if err != nil { logger.Error("readProfile failed :", err) w.WriteHeader(http.StatusBadRequest) return } if !sh.wl.isMember(email, authtype) { logger.Println("auth failed. not whitelist member :", sh.ServiceCode, authtype, uid, email) w.WriteHeader(http.StatusBadRequest) return } logger.Println("auth success :", authtype, uid, email, session) newType, newId, err := sh.getProviderInfo(authtype, uid) if err != nil { logger.Error("getProviderInfo failed :", err) w.WriteHeader(http.StatusBadRequest) } if authtype != newType || uid != newId { authtype = newType uid = newId logger.Println("auth success ( redirect ) :", authtype, uid, email, session) } //if len(session) == 0 && len(email) > 0 { if len(session) == 0 { // platform + id -> account id createtime := primitive.NewDateTimeFromTime(time.Now().UTC()) link, err := sh.mongoClient.FindOneAndUpdate(CollectionLink, bson.M{ "platform": authtype, "uid": uid, }, bson.M{ "$setOnInsert": bson.M{ "create": createtime, "email": email, }, }, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"_id": 1})) if err != nil { logger.Error("authorize failed :", err) w.WriteHeader(http.StatusInternalServerError) return } linkid := link["_id"].(primitive.ObjectID) newaccid := primitive.NewObjectID() for i := 0; i < len(sh.serviceCodeBytes); i++ { newaccid[i] ^= sh.serviceCodeBytes[i] } account, err := sh.mongoClient.FindOneAndUpdate(common.CollectionName(sh.ServiceName), bson.M{ "_id": linkid, }, bson.M{ "$setOnInsert": bson.M{ "accid": newaccid, "create": createtime, }, }, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"accid": 1, "create": 1})) if err != nil { logger.Error("authorize failed. Update sh.ServiceName err:", err) w.WriteHeader(http.StatusInternalServerError) return } accid := account["accid"].(primitive.ObjectID) oldcreate := account["create"].(primitive.DateTime) newaccount := oldcreate == createtime var bi blockinfo if err := sh.mongoClient.FindOneAs(CollectionBlock, bson.M{ "code": sh.ServiceCode, "accid": accid, }, &bi); err != nil { logger.Error("authorize failed. find blockinfo in CollectionBlock err:", err) w.WriteHeader(http.StatusInternalServerError) return } if !bi.Start.Time().IsZero() { now := time.Now().UTC() if bi.Start.Time().Before(now) && bi.End.Time().After(now) { // block됐네? // status는 정상이고 reason을 넘겨주자 json.NewEncoder(w).Encode(map[string]any{ "blocked": bi, }) return } } newsession := primitive.NewObjectID() expired := primitive.NewDateTimeFromTime(time.Now().UTC().Add(sh.sessionTTL)) newauth := common.Authinfo{ Accid: accid, ServiceCode: sh.ServiceCode, Platform: authtype, Uid: uid, Email: email, Sk: newsession, Expired: expired, //RefreshToken: queryvals.Get("rt"), } _, _, err = sh.mongoClient.UpsertOne(CollectionAuth, bson.M{"_id": newauth.Accid}, &newauth) if err != nil { logger.Error("authorize failed :", err) w.WriteHeader(http.StatusInternalServerError) return } output := map[string]any{ "sk": newsession.Hex(), "expirein": sh.sessionTTL.Seconds(), "newAccount": newaccount, "accid": newauth.Accid.Hex(), } bt, _ := json.Marshal(output) w.Write(bt) } else if len(session) > 0 { sessionobj, _ := primitive.ObjectIDFromHex(session) if !sessionobj.IsZero() { updated, _, err := sh.mongoClient.Update(CollectionAuth, bson.M{ "sk": sessionobj, }, bson.M{ "$currentDate": bson.M{ "_ts": bson.M{"$type": "date"}, }, }, options.Update().SetUpsert(false)) if err != nil { logger.Error("update auth collection failed") logger.Error(err) return } if !updated { // 세션이 없네? logger.Println("authorize failed. session not exists in database :", session) w.WriteHeader(http.StatusUnauthorized) return } output := map[string]any{ "sk": session, "expirein": sh.sessionTTL.Seconds(), } bt, _ := json.Marshal(output) w.Write(bt) } else { logger.Println("authorize failed. sk is not valid hex :", session) w.WriteHeader(http.StatusBadRequest) return } } else { logger.Println("authorize failed. id empty :", queryvals) } } func (sh *serviceDescription) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer func() { s := recover() if s != nil { logger.Error(s) } }() defer func() { io.Copy(io.Discard, r.Body) r.Body.Close() }() if atomic.LoadInt32(&sh.closed) != 0 { w.WriteHeader(http.StatusNotFound) return } if strings.HasSuffix(r.URL.Path, "/auth") { sh.authorize(w, r) } else if strings.HasSuffix(r.URL.Path, "/link") { sh.link(w, r) } else { // TODO : 세션키와 authtoken을 헤더로 받아서 accid 조회 queryvals := r.URL.Query() sk := queryvals.Get("sk") //if len(token) == 0 || len(sk) == 0 { if len(sk) == 0 { w.WriteHeader(http.StatusBadRequest) return } // TODO : 각 서버에 있는 자산? 캐릭터 정보를 보여줘야 하나. 뭘 보여줄지는 프로젝트에 문의 // 일단 서버 종류만 내려보내자 // 세션키가 있는지 확인 if _, ok := sh.auths.IsValid(sk, ""); !ok { logger.Println("sessionkey is not valid :", sk) w.WriteHeader(http.StatusBadRequest) return } if divname := queryvals.Get("div"); len(divname) > 0 { divname = strings.Trim(divname, `"`) // 점검중인지 아닌지 확인 // 점검중이어도 입장이 가능한 인원이 있다. div := sh.Divisions[divname] if div != nil { switch div.State { case DivisionState_FullOpen: w.Write([]byte(fmt.Sprintf(`{"service":"%s"}`, div.Url))) case DivisionState_RestrictedOpen: // 점검중인데 일부 권한을 갖고 있는 유저만 들어갈 수 있는 상태 cell := sh.auths.QuerySession(sk, "") if cell == nil { logger.Println("sessionkey is not valid :", sk) w.WriteHeader(http.StatusBadRequest) return } if sh.wl.hasAuth(cell.ToAuthinfo().Email, cell.ToAuthinfo().Platform, whitelistAuthType_QA) { // qa 권한이면 입장 가능 w.Write([]byte(fmt.Sprintf(`{"service":"%s"}`, div.Url))) } else if div.Maintenance != nil { // 권한이 없으므로 공지 w.Write([]byte(fmt.Sprintf(`{"notice":"%s"}`, div.Maintenance.link))) } else { logger.Println("div.Maintenance is nil :", divname) } case DivisionState_Maintenance: // 점검중. 아무도 못들어감 if div.Maintenance != nil { w.Write([]byte(fmt.Sprintf(`{"notice":"%s"}`, div.Maintenance.link))) } else { logger.Println("div.Maintenance is nil :", divname) } } } } else { divstrptr := atomic.LoadPointer(&sh.divisionsSerialized) divstr := *(*string)(divstrptr) w.Write([]byte(divstr)) } } }