package core import ( "crypto/md5" "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 `bson:"start" json:"start"` End primitive.DateTime `bson:"_ts"` Reason string `bson:"reason" json:"reason"` } type whitelistAuthType = string const ( whitelistAuthType_Default = whitelistAuthType("") whitelistAuthType_QA = whitelistAuthType("qa") ) type whitelistmember struct { Service string `bson:"service" json:"service"` Email string `bson:"email" json:"email"` Platform string `bson:"platform" json:"platform"` Desc string `bson:"desc" json:"desc"` Auth []whitelistAuthType `bson:"auth" json:"auth"` 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 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"` ServiceName string `bson:"service" json:"service"` ServiceCode string `bson:"code" json:"code"` UseWhitelist bool `bson:"use_whitelist" json:"use_whitelist"` Closed bool `bson:"closed" json:"closed"` } 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"` ApiUsers map[string][]string `bson:"api_users" json:"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 divisionsForUsersSerialized unsafe.Pointer divisionsSerialized unsafe.Pointer serviceSerialized unsafe.Pointer serviceSummarySerialized 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:]) } divsForUsers := make(map[string]*DivisionForUser) for dn, div := range divs { 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 { hasher := md5.New() hasher.Write([]byte(sh.ServiceName)) subfolder := hex.EncodeToString(hasher.Sum(nil))[:8] div.Maintenance.link = path.Join("static", subfolder, div.Maintenance.Notice) } } } else { div.Maintenance = nil } } divmarshaled, _ := json.Marshal(divs) devstr := string(divmarshaled) sh.divisionsSerialized = unsafe.Pointer(&devstr) divmarshaled2, _ := json.Marshal(divsForUsers) devstr2 := string(divmarshaled2) sh.divisionsForUsersSerialized = unsafe.Pointer(&devstr2) 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) atomic.StorePointer(&sh.serviceSerialized, unsafe.Pointer(&bt)) btsum, _ := json.Marshal(sh.ServiceDescriptionSummary) atomic.StorePointer(&sh.serviceSummarySerialized, unsafe.Pointer(&btsum)) 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.divisionsForUsersSerialized) divstr := *(*string)(divstrptr) w.Write([]byte(divstr)) } } }