package core import ( "context" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "sync/atomic" "time" "unsafe" "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 `bson:"start" json:"start"` End primitive.DateTime `bson:"_ts"` Reason string `bson:"reason" json:"reason"` } type whitelistmember struct { Email string `bson:"email" json:"email"` Platform string `bson:"platform" json:"platform"` Desc string `bson:"desc" json:"desc"` Expired primitive.DateTime `bson:"_ts,omitempty" json:"_ts,omitempty"` } type whitelist struct { emailptr unsafe.Pointer } 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) { all := make(map[string]*whitelistmember) for _, member := range total { all[whitelistKey(member.Email, member.Platform)] = &member } atomic.StorePointer(&wl.emailptr, unsafe.Pointer(&all)) } 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.Platform)] = m atomic.StorePointer(to, unsafe.Pointer(&next)) } func removeFromUnsafePointer(from *unsafe.Pointer, email string, platform 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, platform)) atomic.StorePointer(from, unsafe.Pointer(&next)) } func (wl *whitelist) add(m *whitelistmember) { addToUnsafePointer(&wl.emailptr, m) } func (wl *whitelist) remove(email string, platform string) { removeFromUnsafePointer(&wl.emailptr, email, platform) } func (wl *whitelist) isMember(email string, platform string) bool { ptr := atomic.LoadPointer(&wl.emailptr) src := *(*map[string]*whitelistmember)(ptr) _, exists := src[whitelistKey(email, platform)] return exists } 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 DivisionForUser struct { Priority int `bson:"priority" json:"priority"` State DivisionStateName `bson:"state" json:"state"` Maintenance *Maintenance `bson:"maintenance,omitempty" json:"maintenance,omitempty"` } type Division struct { DivisionForUser `bson:",inline" json:",inline"` Url string `bson:"url" json:"url"` } type ServiceDescriptionSummary struct { Id primitive.ObjectID `bson:"_id" json:"_id"` ServiceCode string `bson:"code" json:"code"` } type serviceDescription struct { ServiceDescriptionSummary `bson:",inline" json:",inline"` Divisions map[string]*Division `bson:"divisions" json:"divisions"` ServerApiTokens []primitive.ObjectID `bson:"api_tokens" json:"api_tokens"` MaximumNumLinkAccount int64 VersionSplits map[string]string `bson:"version_splits" json:"version_splits"` auths *gocommon.AuthCollection wl *whitelist mongoClient gocommon.MongoClient sessionTTL time.Duration 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) divisionsForUsersSerialized []byte divisionsSerialized []byte serviceSerialized []byte serviceSummarySerialized []byte divisionsSplits map[string][]byte } func (sh *serviceDescription) isValidToken(apiToken primitive.ObjectID) bool { if *devflag { return true } if apiToken.IsZero() { return false } for _, test := range sh.ServerApiTokens { if test == apiToken { return true } } return false } 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:]) } if *noauth { sh.ServiceCode = "000000000000" } divsForUsers := make(map[string]*DivisionForUser) var namesOnly []string for dn, div := range divs { namesOnly = append(namesOnly, dn) if div.State == DivisionState_Closed { continue } divsForUsers[dn] = &div.DivisionForUser 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 { var fd FileDocumentDesc if err := mg.mongoClient.FindOneAs(CollectionFile, bson.M{ "key": div.Maintenance.Notice, }, &fd, options.FindOne().SetProjection(bson.M{"link": 1})); err != nil { logger.Println(err) return err } div.Maintenance.link = fd.Link logger.Println("div.Maintenance.link :", fd.Link) } } } else { div.Maintenance = nil } } sh.divisionsSerialized, _ = json.Marshal(divs) sh.divisionsForUsersSerialized, _ = json.Marshal(divsForUsers) if len(sh.VersionSplits) == 0 { sh.VersionSplits = map[string]string{ "": strings.Join(namesOnly, ","), } } sh.divisionsSplits = make(map[string][]byte) for ver, divnamesT := range sh.VersionSplits { divnames := strings.Split(divnamesT, ",") split := make(map[string]*DivisionForUser) for _, divname := range divnames { split[divname] = divsForUsers[divname] } splitMarshaled, _ := json.Marshal(split) sh.divisionsSplits[ver] = splitMarshaled } sh.MaximumNumLinkAccount = mg.maingateConfig.MaximumNumLinkAccount sh.mongoClient = mg.mongoClient sh.auths = mg.auths sh.sessionTTL = time.Duration(mg.SessionTTL * int64(time.Second)) sh.serviceCodeBytes, _ = hex.DecodeString(sh.ServiceCode) sh.getUserBrowserInfo = mg.GetUserBrowserInfo sh.getUserTokenWithCheck = mg.getUserTokenWithCheck sh.updateUserinfo = mg.updateUserinfo sh.getProviderInfo = mg.getProviderInfo sh.wl = &mg.wl sh.serviceSummarySerialized, _ = json.Marshal(sh.ServiceDescriptionSummary) logger.Println("service is ready :", sh.ServiceCode, string(sh.divisionsSerialized)) 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 // } 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(CollectionAccount, 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()) } // == link된 계정을 해제 한다. but, 최소 1개 계정은 연결되어 있어야 한다. func (sh *serviceDescription) unlink(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() sType := queryvals.Get("stype") sId := queryvals.Get("sid") sk := queryvals.Get("sk") authInfo := sh.auths.Find(sk) if authInfo == nil { // 잘못된 세션 logger.Println("linkinfo failed. session key is not valid :", sk) w.WriteHeader(http.StatusBadRequest) return } // fmt.Println("=================") // fmt.Println(sType) // fmt.Println(sId) // fmt.Println("=================") // fmt.Println(authInfo.Platform) // fmt.Println(authInfo.Uid) // fmt.Println("=================") if authInfo.Uid != sId || authInfo.Platform != sType { logger.Println("unlink failed. session key is not correct :", *authInfo, queryvals) w.WriteHeader(http.StatusBadRequest) return } numRecord, err := sh.mongoClient.Collection(CollectionAccount).CountDocuments(context.Background(), bson.M{ "accid": authInfo.Accid, }, options.Count().SetLimit(2)) if err != nil { logger.Error("unlink failed, fail to count accounts :", err) w.WriteHeader(http.StatusBadRequest) } if numRecord <= 1 { logger.Println("unlink failed. At least one link must be maintained. :", r.URL.Query()) w.WriteHeader(http.StatusBadRequest) return } sType, sId, err = sh.getProviderInfo(sType, sId) if err != nil { logger.Error("getProviderInfo failed :", err) w.WriteHeader(http.StatusBadRequest) } link, err := sh.mongoClient.FindOne(CollectionLink, bson.M{ "platform": sType, "uid": sId, }, options.FindOne().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.FindOneAndDelete(CollectionAccount, bson.M{ "_id": link["_id"].(primitive.ObjectID), }, options.FindOneAndDelete().SetProjection(bson.M{"_id": 1})) if err != nil { logger.Error("unlink failed. Delete ServiceName err :", err) w.WriteHeader(http.StatusBadRequest) return } // newid가 있어야 한다. 그래야 기존 서비스 계정이 없는 상태이다. if newid == nil { // 이미 계정이 있네? logger.Println("unlink failed. service account not found:", r.URL.Query()) w.WriteHeader(http.StatusBadRequest) return } logger.Println("unlink success :", r.URL.Query()) } // == 연결된 계정 정보(숫자) 전달하는 API func (sh *serviceDescription) linkinfo(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() sType := queryvals.Get("stype") sId := queryvals.Get("sid") sk := queryvals.Get("sk") authInfo := sh.auths.Find(sk) if authInfo == nil { // 잘못된 세션 logger.Println("linkinfo failed. session key is not valid :", sk) w.WriteHeader(http.StatusBadRequest) return } // fmt.Println("=================") // fmt.Println(sType) // fmt.Println(sId) // fmt.Println("=================") // fmt.Println(authInfo.Platform) // fmt.Println(authInfo.Uid) // fmt.Println("=================") //if oldAuth.Token != oldToken || oldAuth.Uid != oldId || oldAuth.Platform != oldType { if authInfo.Uid != sId || authInfo.Platform != sType { logger.Println("linkinfo failed. session key is not correct :", *authInfo, queryvals) w.WriteHeader(http.StatusBadRequest) return } numRecord, err := sh.mongoClient.Collection(CollectionAccount).CountDocuments(context.Background(), bson.M{ "accid": authInfo.Accid, }, options.Count().SetLimit(sh.MaximumNumLinkAccount)) if err != nil { logger.Error("linkinfo failed. CountDocuments err :", err) w.WriteHeader(http.StatusBadRequest) return } logger.Println("linkinfo :", numRecord) w.Write([]byte(fmt.Sprintf(`{"num_linked_account":"%d"}`, numRecord))) } 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") var email string if !*noauth { if len(authtype) > 0 { //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 } 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) } } else { email = fmt.Sprintf("%s@guest.flag", uid) } } else { email = fmt.Sprintf("%s@noauth.flag", uid) } //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(CollectionAccount, 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 := gocommon.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) findVersionSplit(version string) []byte { if len(version) > 0 { for k, v := range sh.divisionsSplits { if strings.HasPrefix(version, k) { if version == k || version[len(k)] == '.' { return v } } } } return sh.divisionsSplits[""] } 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 strings.HasSuffix(r.URL.Path, "/auth") { sh.authorize(w, r) } else if strings.HasSuffix(r.URL.Path, "/link") { sh.link(w, r) } else if strings.HasSuffix(r.URL.Path, "/unlink") { sh.unlink(w, r) } else if strings.HasSuffix(r.URL.Path, "/linkinfo") { sh.linkinfo(w, r) } else if strings.HasSuffix(r.URL.Path, "/divs") { // 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.StatusUnauthorized) return } version := queryvals.Get("version") w.Write(sh.findVersionSplit(version)) } else if strings.HasSuffix(r.URL.Path, "/addr") { 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.StatusUnauthorized) return } divname := queryvals.Get("div") 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: // 점검중이면 whitelist만 입장 가능 cell := sh.auths.QuerySession(sk, "") if cell == nil { logger.Println("sessionkey is not valid :", sk) w.WriteHeader(http.StatusBadRequest) return } if sh.wl.isMember(cell.ToAuthinfo().Email, cell.ToAuthinfo().Platform) { // 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 { logger.Println("div is not found :", divname) w.WriteHeader(http.StatusBadRequest) } } else { logger.Println("??? :", r.URL.Path) w.WriteHeader(http.StatusBadRequest) } }