package core import ( "context" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" "time" "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/logger" "repositories.action2quare.com/ayo/gocommon/session" "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" json:"_ts"` Reason string `bson:"reason" json:"reason"` Accid primitive.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` } type blockinfoWithStringId struct { Reason string `bson:"reason" json:"reason"` StrId string `bson:"id" json:"id"` StartUnix int64 `bson:"start_unix" json:"start_unix"` EndUnix int64 `bson:"end_unix" json:"end_unix"` } type whitelistmember struct { Id primitive.ObjectID `bson:"_id" json:"_id"` Email string `bson:"email" json:"email"` Platform string `bson:"platform" json:"platform"` Desc string `bson:"desc" json:"desc"` ExpiredAt primitive.DateTime `bson:"_ts,omitempty" json:"_ts,omitempty"` } func (wh *whitelistmember) Key() string { if strings.HasPrefix(wh.Email, "*@") { // 도메인 전체 허용 return wh.Email[2:] } return wh.Email } func (wh *whitelistmember) Expired() bool { // 얘는 Expired가 있기만 하면 제거된 상태 return wh.ExpiredAt != 0 } func (bi *blockinfo) Key() primitive.ObjectID { return bi.Accid } func (bi *blockinfo) Expired() bool { return bi.End.Time().Unix() < time.Now().UTC().Unix() } type usertokeninfo struct { platform string userid string token string //refreshtoken secret string brinfo string accesstoken string // microsoft only accesstoken_expire_time int64 // microsoft only } 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"` sessionProvider session.Provider wl memberContainerPtr[string, *whitelistmember] bl memberContainerPtr[primitive.ObjectID, *blockinfo] 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) divisionsSerialized []byte serviceSerialized []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 len(userinfo.token) == 0 { 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) defaultDivNames := make(map[string]bool) for dn, div := range divs { defaultDivNames[dn] = true 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.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] // 스플릿 된 버전은 default에서 제거해야 한다. delete(defaultDivNames, divname) } splitMarshaled, _ := json.Marshal(split) sh.divisionsSplits[ver] = splitMarshaled } defaultsDivs := make(map[string]*DivisionForUser) for divname := range defaultDivNames { defaultsDivs[divname] = divsForUsers[divname] } defaultMarshaled, _ := json.Marshal(defaultsDivs) sh.divisionsSplits["default"] = defaultMarshaled sh.MaximumNumLinkAccount = mg.maingateConfig.MaximumNumLinkAccount sh.mongoClient = mg.mongoClient sh.sessionProvider = mg.sessionProvider 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.bl = mg.bl sh.serviceSerialized, _ = json.Marshal(sh) logger.Println("service is ready :", sh.ServiceCode, string(sh.serviceSerialized)) 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, err := sh.sessionProvider.Query(sk) if err != nil { logger.Println("sessionProvider.Query return err :", err) w.WriteHeader(http.StatusInternalServerError) 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.Account, "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, err := sh.sessionProvider.Query(sk) if err != nil { logger.Println("sessionProvider.Query return err :", err) w.WriteHeader(http.StatusInternalServerError) 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.Account, }, 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, err := sh.sessionProvider.Query(sk) if err != nil { logger.Println("sessionProvider.Query return err :", err) w.WriteHeader(http.StatusInternalServerError) 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.Account, }, 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") if sk := queryvals.Get("sk"); len(sk) > 0 { success, err := sh.sessionProvider.Touch(sk) if err != nil { logger.Error("authorize failed. sessionProvider.Touch err:", err) w.WriteHeader(http.StatusInternalServerError) return } // !success일 때 빈 body를 보내면 클라이언트는 로그아웃 된다. if success { output := map[string]any{ "sk": sk, "expirein": sh.sessionTTL.Seconds(), } bt, _ := json.Marshal(output) w.Write(bt) } return } 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 } } else { email = fmt.Sprintf("%s@guest.flag", uid) } } else { email = fmt.Sprintf("%s@noauth.flag", uid) } // 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 sh.bl.contains(accid, &bi) { // 블럭된 계정. 블락 정보를 알려준다. w.Header().Add("MG-ACCOUNTBLOCK-START", strconv.FormatInt(bi.Start.Time().Unix(), 10)) w.Header().Add("MG-ACCOUNTBLOCK-END", strconv.FormatInt(bi.End.Time().Unix(), 10)) w.Header().Add("MG-ACCOUNTBLOCK-REASON", bi.Reason) w.WriteHeader(http.StatusUnauthorized) return } sk, err := sh.sessionProvider.New(&session.Authorization{ Account: accid, Platform: authtype, Uid: uid, Email: email, }) if err != nil { logger.Error("authorize failed. sessionProvider.New err:", err) w.WriteHeader(http.StatusInternalServerError) return } output := map[string]any{ "sk": sk, "expirein": sh.sessionTTL.Seconds(), "newAccount": newaccount, "accid": accid.Hex(), } if *noauth { output["noauth"] = true } bt, _ := json.Marshal(output) w.Write(bt) } 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["default"] } 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 : 각 서버에 있는 자산? 캐릭터 정보를 보여줘야 하나. 뭘 보여줄지는 프로젝트에 문의 // 일단 서버 종류만 내려보내자 // 세션키가 있는지 확인 authInfo, err := sh.sessionProvider.Query(sk) if err != nil { logger.Println("sessionProvider.Query return err :", err) w.WriteHeader(http.StatusInternalServerError) return } if authInfo == nil { 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 : 각 서버에 있는 자산? 캐릭터 정보를 보여줘야 하나. 뭘 보여줄지는 프로젝트에 문의 // 일단 서버 종류만 내려보내자 // 세션키가 있는지 확인 authInfo, err := sh.sessionProvider.Query(sk) if err != nil { logger.Println("sessionProvider.Query return err :", err) w.WriteHeader(http.StatusInternalServerError) return } if authInfo == nil { 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만 입장 가능 authInfo, err := sh.sessionProvider.Query(sk) if err != nil { logger.Println("sessionProvider.Query return err :", err) w.WriteHeader(http.StatusInternalServerError) return } wm := &whitelistmember{Email: authInfo.Email, Platform: authInfo.Platform} if sh.wl.contains(wm.Key(), nil) { // 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) } }