diff --git a/core/api.go b/core/api.go index 3e67589..5039810 100644 --- a/core/api.go +++ b/core/api.go @@ -2,9 +2,7 @@ package core import ( "bytes" - "crypto/md5" "encoding/binary" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -27,7 +25,6 @@ import ( ) type FileDocumentDesc struct { - Service string `bson:"service" json:"service"` Key string `bson:"key" json:"key"` Src string `bson:"src" json:"src"` Link string `bson:"link" json:"link"` @@ -110,11 +107,6 @@ var seq = uint32(0) func (caller apiCaller) uploadAPI(w http.ResponseWriter, r *http.Request) error { if r.Method == "PUT" { - servicename := r.FormValue("service") - hasher := md5.New() - hasher.Write([]byte(servicename)) - subfolder := hex.EncodeToString(hasher.Sum(nil))[:8] - infile, header, err := r.FormFile("file") if err != nil { w.WriteHeader(http.StatusBadRequest) @@ -130,17 +122,16 @@ func (caller apiCaller) uploadAPI(w http.ResponseWriter, r *http.Request) error var b [5]byte binary.BigEndian.PutUint32(b[0:4], uint32(time.Now().Unix())) b[4] = byte(atomic.AddUint32(&seq, 1) % 255) - rf := hex.EncodeToString(b[1:]) - newidstr := subfolder + rf - newidbt, _ := hex.DecodeString(newidstr) - newidobj := primitive.NewObjectID() - copy(newidobj[:], newidbt[:8]) + newidobj := primitive.NewObjectID() + copy(newidobj[:], b[1:]) + + rf := newidobj.Hex() var link string if extract { - link = path.Join("static", subfolder, rf) + link = path.Join("static", rf) } else { - link = path.Join("static", subfolder, rf, header.Filename) + link = path.Join("static", rf, header.Filename) } newdoc := FileDocumentDesc{ @@ -151,12 +142,10 @@ func (caller apiCaller) uploadAPI(w http.ResponseWriter, r *http.Request) error Link: link, Desc: desc, Key: rf, - Service: servicename, } _, _, err = caller.mg.mongoClient.UpsertOne(CollectionFile, bson.M{ - "_id": newidobj, - "service": servicename, - "key": rf, + "_id": newidobj, + "key": rf, }, newdoc) if err == nil { @@ -169,44 +158,81 @@ func (caller apiCaller) uploadAPI(w http.ResponseWriter, r *http.Request) error return nil } -func (caller apiCaller) whitelistAPI(w http.ResponseWriter, r *http.Request) error { +func (caller apiCaller) blockAPI(w http.ResponseWriter, r *http.Request) error { mg := caller.mg if r.Method == "GET" { - // if !caller.isAdminOrValidToken() { - // logger.Println("whitelistAPI failed. not vaild user :", r.Method, caller.userinfo) - // w.WriteHeader(http.StatusUnauthorized) - // return nil - // } + enc := json.NewEncoder(w) + enc.Encode(mg.bl.all()) + } else if r.Method == "PUT" { + body, _ := io.ReadAll(r.Body) - all, err := mg.mongoClient.All(CollectionWhitelist) + var bipl blockinfoWithStringId + if err := json.Unmarshal(body, &bipl); err != nil { + return err + } + + accid, err := primitive.ObjectIDFromHex(bipl.StrId) if err != nil { return err } - if len(all) > 0 { - var notexp []primitive.M - for _, v := range all { - if _, exp := v["_ts"]; !exp { - notexp = append(notexp, v) - } - } - allraw, _ := json.Marshal(notexp) - w.Write(allraw) + bi := blockinfo{ + Start: primitive.NewDateTimeFromTime(time.Unix(bipl.StartUnix, 0)), + End: primitive.NewDateTimeFromTime(time.Unix(bipl.EndUnix, 0)), + Reason: bipl.Reason, } + + logger.Println("bi :", accid, bi) + + _, _, err = mg.mongoClient.Update(CollectionBlock, bson.M{ + "_id": accid, + }, bson.M{ + "$set": &bi, + }, options.Update().SetUpsert(true)) + + if err != nil { + return err + } + } else if r.Method == "DELETE" { + id := r.URL.Query().Get("id") + + if len(id) == 0 { + return errors.New("id param is missing") + } + idobj, err := primitive.ObjectIDFromHex(id) + if err != nil { + return err + } + + _, _, err = mg.mongoClient.Update(CollectionBlock, bson.M{ + "_id": idobj, + }, bson.M{ + "$currentDate": bson.M{ + "_ts": bson.M{"$type": "date"}, + }, + }, options.Update().SetUpsert(false)) + + if err != nil { + return err + } + + mg.mongoClient.Delete(CollectionAuth, bson.M{"_id": idobj}) + } + return nil +} + +func (caller apiCaller) whitelistAPI(w http.ResponseWriter, r *http.Request) error { + mg := caller.mg + if r.Method == "GET" { + enc := json.NewEncoder(w) + enc.Encode(mg.wl.all()) } else if r.Method == "PUT" { body, _ := io.ReadAll(r.Body) var member whitelistmember if err := json.Unmarshal(body, &member); err != nil { return err } - - // if !caller.isAdminOrValidToken() { - // logger.Println("whitelistAPI failed. not vaild user :", r.Method, caller.userinfo) - // w.WriteHeader(http.StatusUnauthorized) - // return nil - // } - - member.Expired = 0 + member.ExpiredAt = 0 _, _, err := mg.mongoClient.Update(CollectionWhitelist, bson.M{ "_id": primitive.NewObjectID(), @@ -260,7 +286,7 @@ func (caller apiCaller) serviceAPI(w http.ResponseWriter, r *http.Request) error atomic.StorePointer(&mg.serviceptr, unsafe.Pointer(&newService)) } - w.Write(mg.service().divisionsSerialized) + w.Write(mg.service().serviceSerialized) } else if r.Method == "POST" { body, _ := io.ReadAll(r.Body) var service serviceDescription @@ -319,6 +345,38 @@ func (caller apiCaller) maintenanceAPI(w http.ResponseWriter, r *http.Request) e return nil } +func (caller apiCaller) couponAPI(w http.ResponseWriter, r *http.Request) error { + switch r.Method { + case "PUT": + // 쿠폰 생성 + logger.Println("begin generateCoupons") + generateCoupons(caller.mg.mongoClient, w, r) + + case "POST": + // TODO : 쿠폰 사용 + // 쿠폰 사용 표시 해주고 내용을 응답 + logger.Println("begin useCoupon") + useCoupon(caller.mg.mongoClient, w, r) + + case "GET": + // 쿠폰 조회 + if r.Form.Has("code") { + // 쿠폰 코드 조회 + logger.Println("begin queryCoupon") + queryCoupon(caller.mg.mongoClient, w, r) + } else if r.Form.Has("name") { + // 쿠폰 코드 다운 + logger.Println("begin downloadCoupons") + downloadCoupons(caller.mg.mongoClient, w, r) + } else { + // 쿠폰 이름 목록 + logger.Println("begin listAllCouponNames") + listAllCouponNames(caller.mg.mongoClient, w, r) + } + } + return nil +} + var errApiTokenMissing = errors.New("mg-x-api-token is missing") func (caller apiCaller) configAPI(w http.ResponseWriter, r *http.Request) error { @@ -359,6 +417,8 @@ func (mg *Maingate) api(w http.ResponseWriter, r *http.Request) { r.Body.Close() }() + r.ParseMultipartForm(32 << 20) + var userinfo map[string]any if !*devflag { @@ -439,6 +499,10 @@ func (mg *Maingate) api(w http.ResponseWriter, r *http.Request) { err = caller.maintenanceAPI(w, r) } else if strings.HasSuffix(r.URL.Path, "/files") { err = caller.filesAPI(w, r) + } else if strings.HasSuffix(r.URL.Path, "/block") { + err = caller.blockAPI(w, r) + } else if strings.HasSuffix(r.URL.Path, "/coupon") { + err = caller.couponAPI(w, r) } if err != nil { diff --git a/core/api_coupon.go b/core/api_coupon.go new file mode 100644 index 0000000..8e788f6 --- /dev/null +++ b/core/api_coupon.go @@ -0,0 +1,372 @@ +package core + +import ( + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "math/rand" + "net/http" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "repositories.action2quare.com/ayo/gocommon" + coupon "repositories.action2quare.com/ayo/gocommon/coupon" + "repositories.action2quare.com/ayo/gocommon/logger" +) + +const ( + CollectionCoupon = gocommon.CollectionName("coupon") + CollectionCouponUse = gocommon.CollectionName("coupon_use") +) + +type couponDoc struct { + Name string `json:"name" bson:"name"` + Effect string `json:"effect" bson:"effect"` + Desc string `json:"desc" bson:"desc"` + Total int64 `json:"total" bson:"total"` + Remains []string `json:"remains,omitempty" bson:"remains,omitempty"` + Used []string `json:"used,omitempty" bson:"used,omitempty"` +} + +func makeCouponKey(roundnum uint32, uid []byte) string { + left := binary.BigEndian.Uint16(uid[0:2]) + right := binary.BigEndian.Uint16(uid[2:4]) + multi := uint32(left) * uint32(right) + xor := roundnum ^ multi + + final := make([]byte, 8) + binary.LittleEndian.PutUint32(final, xor) + copy(final[4:], uid) + return fmt.Sprintf("%s-%s-%s-%s", hex.EncodeToString(final[0:2]), hex.EncodeToString(final[2:4]), hex.EncodeToString(final[4:6]), hex.EncodeToString(final[6:8])) +} + +func makeCouponCodes(name string, count int) (string, map[string]string) { + checkunique := make(map[string]bool) + keys := make(map[string]string) + uid := make([]byte, 4) + + roundHash, roundnum := coupon.MakeCouponRoundHash(name) + seed := time.Now().UnixNano() + + for len(keys) < count { + rand.Seed(seed) + rand.Read(uid) + + code := makeCouponKey(roundnum, uid) + + if _, ok := checkunique[code]; !ok { + checkunique[code] = true + keys[hex.EncodeToString(uid)] = code + } + seed = int64(binary.BigEndian.Uint32(uid)) + } + return roundHash, keys +} + +func generateCoupons(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) { + name, _ := gocommon.ReadStringFormValue(r.Form, "name") + effect, _ := gocommon.ReadStringFormValue(r.Form, "effect") + count, _ := gocommon.ReadIntegerFormValue(r.Form, "count") + desc, _ := gocommon.ReadStringFormValue(r.Form, "desc") + + if count == 0 { + logger.Println("[generateCoupons] count == 0") + w.WriteHeader(http.StatusBadRequest) + return + } + + roundHash, _ := coupon.MakeCouponRoundHash(name) + roundObj, _ := primitive.ObjectIDFromHex(roundHash + roundHash + roundHash) + + if count < 0 { + // 무한 쿠폰이므로 그냥 문서 생성해 주고 끝 + if _, _, err := mongoClient.Update(CollectionCoupon, bson.M{ + "_id": roundObj, + }, bson.M{ + "$set": &couponDoc{ + Name: name, + Effect: effect, + Desc: desc, + Total: -1, + }, + }, options.Update().SetUpsert(true)); err != nil { + logger.Println("[generateCoupons] Update failed :", err) + w.WriteHeader(http.StatusInternalServerError) + } + return + } + + // effect가 비어있으면 기존의 roundName에 갯수를 추가해 준다 + // effect가 비어있지 않으면 roundName이 겹쳐서는 안된다. + coupondoc, err := mongoClient.FindOne(CollectionCoupon, bson.M{"_id": roundObj}) + if err != nil { + logger.Println("[generateCoupons] FindOne failed :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + lastKeys := make(map[string]bool) + if coupondoc != nil { + if r, ok := coupondoc["remains"]; ok { + remains := r.(primitive.A) + for _, uid := range remains { + lastKeys[uid.(string)] = true + } + } + } + + issuedKeys := make(map[string]string) + for len(issuedKeys) < int(count) { + _, vs := makeCouponCodes(name, int(count)-len(issuedKeys)) + for k, v := range vs { + if _, ok := lastKeys[k]; !ok { + // 기존 키와 중복되지 않는 것만 + issuedKeys[k] = v + } + } + } + + var coupons []string + var uids []string + for uid, code := range issuedKeys { + uids = append(uids, uid) + coupons = append(coupons, code) + } + + if coupondoc != nil { + _, _, err = mongoClient.Update(CollectionCoupon, bson.M{ + "_id": roundObj, + }, bson.M{ + "$push": bson.M{"remains": bson.M{"$each": uids}}, + "$inc": bson.M{"total": count}, + }, options.Update().SetUpsert(true)) + } else { + _, _, err = mongoClient.Update(CollectionCoupon, bson.M{ + "_id": roundObj, + }, bson.M{ + "$push": bson.M{"remains": bson.M{"$each": uids}}, + "$set": couponDoc{ + Name: name, + Effect: effect, + Desc: desc, + Total: count, + }, + }, options.Update().SetUpsert(true)) + } + + if err != nil { + logger.Println("[generateCoupons] Update failed :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + enc := json.NewEncoder(w) + enc.Encode(coupons) +} + +func downloadCoupons(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) { + name, _ := gocommon.ReadStringFormValue(r.Form, "name") + if len(name) == 0 { + logger.Println("[downloadCoupons] name is empty") + w.WriteHeader(http.StatusBadRequest) + return + } + + round, _ := coupon.MakeCouponRoundHash(name) + + roundObj, err := primitive.ObjectIDFromHex(round + round + round) + if err != nil { + // 유효하지 않은 형식의 code + logger.Println("[downloadCoupons] ObjectIDFromHex failed :", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + var coupon couponDoc + if err := mongoClient.FindOneAs(CollectionCoupon, bson.M{ + "_id": roundObj, + }, &coupon, options.FindOne().SetProjection(bson.M{"_id": 0, "remains": 1})); err != nil { + logger.Println("[downloadCoupons] FindOne failed :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + roundnum := binary.BigEndian.Uint32(roundObj[:]) + var coupons []string + for _, uid := range coupon.Remains { + coupons = append(coupons, makeCouponKey(roundnum, []byte(uid))) + } + + enc := json.NewEncoder(w) + enc.Encode(coupons) +} + +func queryCoupon(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) { + code, _ := gocommon.ReadStringFormValue(r.Form, "code") + if len(code) == 0 { + logger.Println("[queryCoupon] code is empty") + w.WriteHeader(http.StatusBadRequest) + return + } + + round, _ := coupon.DisolveCouponCode(code) + if len(round) == 0 { + // 유효하지 않은 형식의 code + // 쿠폰 이름일 수 있으므로 round hash를 계산한다. + round, _ = coupon.MakeCouponRoundHash(code) + } + + roundObj, err := primitive.ObjectIDFromHex(round + round + round) + if err != nil { + // 유효하지 않은 형식의 code + logger.Println("[queryCoupon] ObjectIDFromHex failed :", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + var coupon couponDoc + if err := mongoClient.FindOneAs(CollectionCoupon, bson.M{ + "_id": roundObj, + }, &coupon, options.FindOne().SetProjection(bson.M{"effect": 1, "name": 1, "reason": 1, "total": 1, "desc": 1}).SetReturnKey(false)); err != nil { + logger.Println("[queryCoupon] FindOneAs failed :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + enc := json.NewEncoder(w) + enc.Encode(coupon) +} + +func listAllCouponNames(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) { + all, err := mongoClient.FindAll(CollectionCoupon, bson.M{}, options.Find().SetProjection(bson.M{"name": 1})) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + var names []string + for _, doc := range all { + names = append(names, doc["name"].(string)) + } + + enc := json.NewEncoder(w) + enc.Encode(names) +} + +func useCoupon(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) { + acc, ok := gocommon.ReadObjectIDFormValue(r.Form, "accid") + if !ok || acc.IsZero() { + w.WriteHeader(http.StatusBadRequest) + return + } + code, _ := gocommon.ReadStringFormValue(r.Form, "code") + code = strings.TrimSpace(code) + if len(code) == 0 { + w.WriteHeader(http.StatusBadRequest) + return + } + + round, key := coupon.DisolveCouponCode(code) + if len(round) == 0 { + // couponId가 쿠폰 이름일 수도 있다. 무한 쿠폰 + round, _ = coupon.MakeCouponRoundHash(code) + } + + // 1. 내가 이 라운드의 쿠폰을 쓴 적이 있나 + already, err := mongoClient.Exists(CollectionCouponUse, bson.M{ + "_id": acc, + "rounds": round, + }) + if err != nil { + logger.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if already { + // 이미 이 라운드의 쿠폰을 사용한 적이 있다. + w.WriteHeader(http.StatusConflict) + return + } + + var coupon couponDoc + roundObj, _ := primitive.ObjectIDFromHex(round + round + round) + if len(key) == 0 { + // 무한 쿠폰일 수 있으므로 존재하는지 확인 + if err := mongoClient.FindOneAs(CollectionCoupon, bson.M{ + "_id": roundObj, + }, &coupon, options.FindOne().SetProjection(bson.M{"_id": 0, "effect": 1, "name": 1, "reason": 1, "total": 1})); err != nil { + logger.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if coupon.Total > 0 { + // 무한 쿠폰 아니네? + w.WriteHeader(http.StatusBadRequest) + return + } + } else { + // 2. 쿠폰을 하나 꺼냄 + matched, _, err := mongoClient.Update(CollectionCoupon, bson.M{ + "_id": roundObj, + }, bson.M{ + "$pull": bson.M{"remains": key}, + }) + if err != nil { + logger.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if !matched { + // 쿠폰이 없다. + w.WriteHeader(http.StatusBadRequest) + return + } + + // 3. round의 효과 읽기 + if err := mongoClient.FindOneAndUpdateAs(CollectionCoupon, bson.M{ + "_id": roundObj, + }, bson.M{ + "$push": bson.M{"used": key}, + }, &coupon, options.FindOneAndUpdate().SetProjection(bson.M{"effect": 1})); err != nil { + logger.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + if len(coupon.Effect) == 0 { + // 쿠폰이 없네? + w.WriteHeader(http.StatusBadRequest) + return + } + + // 4. 쿠폰은 사용한 것으로 표시 + // 이제 이 아래에서 실패하면 이 쿠폰은 못쓴다. + updated, _, err := mongoClient.Update(CollectionCouponUse, bson.M{ + "_id": acc, + }, bson.M{ + "$push": bson.M{"rounds": round}, + "$set": bson.M{round + ".id": code}, + "$currentDate": bson.M{round + ".ts": true}, + }, options.Update().SetUpsert(true)) + + if err != nil { + logger.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if !updated { + logger.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write([]byte(coupon.Effect)) +} diff --git a/core/api_test.go b/core/api_test.go new file mode 100644 index 0000000..a0f883b --- /dev/null +++ b/core/api_test.go @@ -0,0 +1,39 @@ +package core + +import ( + "context" + "fmt" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "repositories.action2quare.com/ayo/gocommon" +) + +func TestMakeLocalUniqueId(t *testing.T) { + ts := int64(1690815600) + start := primitive.NewDateTimeFromTime(time.Unix(ts, 0)) + ts = int64(1693493999) + end := primitive.NewDateTimeFromTime(time.Unix(ts, 0)) + + fmt.Println(start.Time().Format(time.RFC3339)) + fmt.Println(end.Time().Format(time.RFC3339)) + + mongoClient, err := gocommon.NewMongoClient(context.Background(), "mongodb://121.134.91.160:27018/?replicaSet=rs0&retrywrites=true", "mountain-maingate") + if err != nil { + t.Error(err) + } + + bi := blockinfo{ + Start: start, + End: end, + Reason: "test", + } + mongoClient.Update(CollectionBlock, bson.M{ + "_id": primitive.NewObjectID(), + }, bson.M{ + "$set": &bi, + }, options.Update().SetUpsert(true)) +} diff --git a/core/maingate.go b/core/maingate.go index 19c7f01..bb0f0e9 100644 --- a/core/maingate.go +++ b/core/maingate.go @@ -13,6 +13,7 @@ import ( "net" "net/http" "os" + "runtime/debug" "strings" "sync/atomic" "time" @@ -169,7 +170,8 @@ type Maingate struct { //services servicelist serviceptr unsafe.Pointer admins unsafe.Pointer - wl whitelist + wl memberContainerPtr[string, *whitelistmember] + bl memberContainerPtr[primitive.ObjectID, *blockinfo] tokenEndpoints map[string]string authorizationEndpoints map[string]string @@ -206,7 +208,6 @@ func New(ctx context.Context) (*Maingate, error) { err := mg.prepare(ctx) if err != nil { - logger.Error("mg.prepare() failed :", err) return nil, err } @@ -286,106 +287,104 @@ func (mg *Maingate) discoverOpenIdConfiguration(name string, url string) error { } +func makeErrorWithStack(err error) error { + return fmt.Errorf("%s\n%s", err.Error(), string(debug.Stack())) +} + func (mg *Maingate) prepare(context context.Context) (err error) { if err := mg.discoverOpenIdConfiguration(AuthPlatformMicrosoft, "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"); err != nil { - return err + return makeErrorWithStack(err) } if err := mg.discoverOpenIdConfiguration("google", "https://accounts.google.com/.well-known/openid-configuration"); err != nil { - return err + return makeErrorWithStack(err) } // redis에서 env를 가져온 후에 mg.mongoClient, err = gocommon.NewMongoClient(context, mg.Mongo, "maingate") if err != nil { + return makeErrorWithStack(err) + } + + if err = mg.mongoClient.MakeUniqueIndices(CollectionCouponUse, map[string]bson.D{ + "idrounds": {{Key: "_id", Value: 1}, {Key: "rounds", Value: 1}}, + }); err != nil { return err } if err = mg.mongoClient.MakeUniqueIndices(CollectionAuth, map[string]bson.D{ "skonly": {{Key: "sk", Value: 1}}, }); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeUniqueIndices(CollectionLink, map[string]bson.D{ "platformuid": {{Key: "platform", Value: 1}, {Key: "uid", Value: 1}}, }); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeUniqueIndices(CollectionLink, map[string]bson.D{ "emailplatform": {{Key: "email", Value: 1}, {Key: "platform", Value: 1}}, }); err != nil { - return err - } - - if err = mg.mongoClient.MakeIndices(CollectionWhitelist, map[string]bson.D{ - "service": {{Key: "service", Value: 1}}, - }); err != nil { - return err - } - - if err = mg.mongoClient.MakeIndices(CollectionFile, map[string]bson.D{ - "service": {{Key: "service", Value: 1}}, - }); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeIndices(CollectionAccount, map[string]bson.D{ "accid": {{Key: "accid", Value: 1}}, }); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeUniqueIndices(CollectionFile, map[string]bson.D{ - "sk": {{Key: "service", Value: 1}, {Key: "key", Value: 1}}, + "keyonly": {{Key: "key", Value: 1}}, }); err != nil { - return err + return makeErrorWithStack(err) } + // Delete대신 _ts로 expire시킴. pipeline에 삭제 알려주기 위함 if err = mg.mongoClient.MakeExpireIndex(CollectionWhitelist, 10); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeExpireIndex(CollectionAuth, int32(mg.SessionTTL+300)); err != nil { - return err + return makeErrorWithStack(err) } - if err = mg.mongoClient.MakeUniqueIndices(CollectionBlock, map[string]bson.D{ - "codeaccid": {{Key: "code", Value: 1}, {Key: "accid", Value: 1}}, - }); err != nil { - return err + if *devflag { + // 에러 체크하지 말것 + mg.mongoClient.DropIndex(CollectionBlock, "codeaccid") } if err = mg.mongoClient.MakeExpireIndex(CollectionBlock, int32(3)); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeUniqueIndices(CollectionPlatformLoginToken, map[string]bson.D{ "platformauthtoken": {{Key: "platform", Value: 1}, {Key: "key", Value: 1}}, }); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeExpireIndex(CollectionPlatformLoginToken, int32(mg.SessionTTL+300)); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeUniqueIndices(CollectionUserToken, map[string]bson.D{ "platformusertoken": {{Key: "platform", Value: 1}, {Key: "userid", Value: 1}}, }); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeUniqueIndices(CollectionGamepotUserInfo, map[string]bson.D{ "gamepotuserid": {{Key: "gamepotuserid", Value: 1}}, }); err != nil { - return err + return makeErrorWithStack(err) } if err = mg.mongoClient.MakeUniqueIndices(CollectionFirebaseUserInfo, map[string]bson.D{ "firebaseuserid": {{Key: "firebaseuserid", Value: 1}}, }); err != nil { - return err + return makeErrorWithStack(err) } mg.auths = makeAuthCollection(mg.mongoClient, time.Duration(mg.SessionTTL*int64(time.Second))) @@ -397,7 +396,7 @@ func (mg *Maingate) prepare(context context.Context) (err error) { if err = mg.mongoClient.FindAllAs(CollectionFile, nil, &preall, options.Find().SetProjection(bson.M{ "link": 1, })); err != nil { - return err + return makeErrorWithStack(err) } for _, pre := range preall { @@ -412,35 +411,33 @@ func (mg *Maingate) prepare(context context.Context) (err error) { "_id": pre.Id, }, &fulldoc) if err != nil { - return err + return makeErrorWithStack(err) } err = fulldoc.Save() if err != nil { - return err + return makeErrorWithStack(err) } } - var whites []whitelistmember + var whites []*whitelistmember if err := mg.mongoClient.AllAs(CollectionWhitelist, &whites, options.Find().SetReturnKey(false)); err != nil { - return err + return makeErrorWithStack(err) } mg.wl.init(whites) + var blocks []*blockinfo + if err := mg.mongoClient.AllAs(CollectionBlock, &blocks); err != nil { + return makeErrorWithStack(err) + } + mg.bl.init(blocks) + go watchAuthCollection(context, mg.auths, mg.mongoClient) - go mg.watchWhitelistCollection(context) + go mg.wl.watchCollection(context, CollectionWhitelist, mg.mongoClient) + go mg.bl.watchCollection(context, CollectionBlock, mg.mongoClient) return nil } -func whitelistKey(email string, platform string) string { - if strings.HasPrefix(email, "*@") { - // 도메인 전체 허용 - return email[2:] - } - - return email -} - func (mg *Maingate) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux, prefix string) error { var allServices []*serviceDescription if err := mg.mongoClient.AllAs(CollectionService, &allServices, options.Find().SetReturnKey(false)); err != nil { diff --git a/core/member_container.go b/core/member_container.go new file mode 100644 index 0000000..2c4eaa4 --- /dev/null +++ b/core/member_container.go @@ -0,0 +1,169 @@ +package core + +import ( + "context" + "sync/atomic" + "time" + "unsafe" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "repositories.action2quare.com/ayo/gocommon" + "repositories.action2quare.com/ayo/gocommon/logger" +) + +type memberContraints[K comparable] interface { + Key() K + Expired() bool +} + +type memberContainerPtr[K comparable, T memberContraints[K]] struct { + ptr unsafe.Pointer +} + +func (p *memberContainerPtr[K, T]) init(ms []T) { + next := map[K]T{} + for _, m := range ms { + next[m.Key()] = m + } + atomic.StorePointer(&p.ptr, unsafe.Pointer(&next)) +} + +func (p *memberContainerPtr[K, T]) add(m T) { + ptr := atomic.LoadPointer(&p.ptr) + src := (*map[K]T)(ptr) + + next := map[K]T{} + for k, v := range *src { + next[k] = v + } + next[m.Key()] = m + atomic.StorePointer(&p.ptr, unsafe.Pointer(&next)) +} + +func (p *memberContainerPtr[K, T]) remove(key K) { + ptr := atomic.LoadPointer(&p.ptr) + src := (*map[K]T)(ptr) + + next := map[K]T{} + for k, v := range *src { + next[k] = v + } + delete(next, key) + atomic.StorePointer(&p.ptr, unsafe.Pointer(&next)) +} + +type memberPipelineDocument[K comparable, T memberContraints[K]] struct { + OperationType string `bson:"operationType"` + DocumentKey struct { + Id primitive.ObjectID `bson:"_id"` + } `bson:"documentKey"` + Member T `bson:"fullDocument"` +} + +func (p *memberContainerPtr[K, T]) all() []T { + ptr := atomic.LoadPointer(&p.ptr) + src := (*map[K]T)(ptr) + + out := make([]T, 0, len(*src)) + for _, m := range *src { + if m.Expired() { + continue + } + out = append(out, m) + } + return out +} + +func (p *memberContainerPtr[K, T]) contains(key K, out *T) bool { + ptr := atomic.LoadPointer(&p.ptr) + src := (*map[K]T)(ptr) + + found, exists := (*src)[key] + if exists { + if found.Expired() { + p.remove(key) + return false + } + if out != nil { + out = &found + } + return true + } + return false +} + +func (p *memberContainerPtr[K, T]) watchCollection(parentctx context.Context, coll gocommon.CollectionName, mc gocommon.MongoClient) { + defer func() { + s := recover() + if s != nil { + logger.Error(s) + } + }() + + matchStage := bson.D{ + { + Key: "$match", Value: bson.D{ + {Key: "operationType", Value: bson.D{ + {Key: "$in", Value: bson.A{ + "update", + "insert", + }}, + }}, + }, + }} + projectStage := bson.D{ + { + Key: "$project", Value: bson.D{ + {Key: "documentKey", Value: 1}, + {Key: "fullDocument", Value: 1}, + }, + }, + } + + var stream *mongo.ChangeStream + var err error + var ctx context.Context + + for { + if stream == nil { + stream, err = mc.Watch(coll, mongo.Pipeline{matchStage, projectStage}) + if err != nil { + logger.Error("watchCollection watch failed :", err) + time.Sleep(time.Minute) + continue + } + ctx = context.TODO() + } + + changed := stream.TryNext(ctx) + if ctx.Err() != nil { + logger.Error("watchCollection stream.TryNext failed. process should be restarted! :", ctx.Err().Error()) + break + } + + if changed { + var data memberPipelineDocument[K, T] + if err := stream.Decode(&data); err == nil { + p.add(data.Member) + } else { + logger.Error("watchCollection stream.Decode failed :", err) + } + } else if stream.Err() != nil || stream.ID() == 0 { + select { + case <-ctx.Done(): + logger.Println("watchCollection is done") + stream.Close(ctx) + return + + case <-time.After(time.Second): + logger.Error("watchCollection stream error :", stream.Err()) + stream.Close(ctx) + stream = nil + } + } else { + time.Sleep(time.Second) + } + } +} diff --git a/core/platformsteam.go b/core/platformsteam.go index b52f25e..8766bdf 100644 --- a/core/platformsteam.go +++ b/core/platformsteam.go @@ -39,10 +39,11 @@ func (mg *Maingate) platform_steamsdk_authorize(w http.ResponseWriter, r *http.R return } - err = authenticateSteamUser(mg.SteamPublisherAuthKey, mg.SteamAppId, authinfo.UserSteamId, authinfo.UserAuthToken) + if !*noauth { + err = authenticateSteamUser(mg.SteamPublisherAuthKey, mg.SteamAppId, authinfo.UserSteamId, authinfo.UserAuthToken) + } if err == nil { - acceestoken_expire_time := time.Date(2999, 1, int(time.January), 0, 0, 0, 0, time.UTC).Unix() var info usertokeninfo diff --git a/core/service.go b/core/service.go index 082062d..fd8f090 100644 --- a/core/service.go +++ b/core/service.go @@ -7,10 +7,9 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" - "sync/atomic" "time" - "unsafe" "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/logger" @@ -22,19 +21,45 @@ import ( type blockinfo struct { Start primitive.DateTime `bson:"start" json:"start"` - End primitive.DateTime `bson:"_ts"` + 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 { - 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"` + 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"` } -type whitelist struct { - emailptr unsafe.Pointer +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 { @@ -47,54 +72,6 @@ type usertokeninfo struct { 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 ( @@ -134,7 +111,8 @@ type serviceDescription struct { VersionSplits map[string]string `bson:"version_splits" json:"version_splits"` auths *gocommon.AuthCollection - wl *whitelist + wl memberContainerPtr[string, *whitelistmember] + bl memberContainerPtr[primitive.ObjectID, *blockinfo] mongoClient gocommon.MongoClient sessionTTL time.Duration @@ -280,10 +258,12 @@ func (sh *serviceDescription) prepare(mg *Maingate) error { sh.updateUserinfo = mg.updateUserinfo sh.getProviderInfo = mg.getProviderInfo - sh.wl = &mg.wl + sh.wl = mg.wl + sh.bl = mg.bl sh.serviceSummarySerialized, _ = json.Marshal(sh.ServiceDescriptionSummary) + sh.serviceSerialized, _ = json.Marshal(sh) - logger.Println("service is ready :", sh.ServiceCode, string(sh.divisionsSerialized)) + logger.Println("service is ready :", sh.ServiceCode, string(sh.serviceSerialized)) return nil } @@ -721,28 +701,16 @@ func (sh *serviceDescription) authorize(w http.ResponseWriter, r *http.Request) 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) + 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 } - 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{ @@ -769,6 +737,9 @@ func (sh *serviceDescription) authorize(w http.ResponseWriter, r *http.Request) "newAccount": newaccount, "accid": newauth.Accid.Hex(), } + if *noauth { + output["noauth"] = true + } bt, _ := json.Marshal(output) w.Write(bt) } else if len(session) > 0 { @@ -971,7 +942,8 @@ func (sh *serviceDescription) serveHTTP(w http.ResponseWriter, r *http.Request) w.WriteHeader(http.StatusBadRequest) return } - if sh.wl.isMember(cell.ToAuthinfo().Email, cell.ToAuthinfo().Platform) { + wm := &whitelistmember{Email: cell.ToAuthinfo().Email, Platform: cell.ToAuthinfo().Platform} + if sh.wl.contains(wm.Key(), nil) { // qa 권한이면 입장 가능 w.Write([]byte(fmt.Sprintf(`{"service":"%s"}`, div.Url))) } else if div.Maintenance != nil { diff --git a/core/watch.go b/core/watch.go index 04e3b8c..ba190f6 100644 --- a/core/watch.go +++ b/core/watch.go @@ -43,102 +43,6 @@ type filePipelineDocument struct { File *FileDocumentDesc `bson:"fullDocument"` } -type whilelistPipelineDocument struct { - OperationType string `bson:"operationType"` - DocumentKey struct { - Id primitive.ObjectID `bson:"_id"` - } `bson:"documentKey"` - Member *whitelistmember `bson:"fullDocument"` -} - -func (mg *Maingate) watchWhitelistCollection(parentctx context.Context) { - defer func() { - s := recover() - if s != nil { - logger.Error(s) - } - }() - - matchStage := bson.D{ - { - Key: "$match", Value: bson.D{ - {Key: "operationType", Value: bson.D{ - {Key: "$in", Value: bson.A{ - "update", - "insert", - }}, - }}, - }, - }} - projectStage := bson.D{ - { - Key: "$project", Value: bson.D{ - {Key: "documentKey", Value: 1}, - {Key: "operationType", Value: 1}, - {Key: "fullDocument", Value: 1}, - }, - }, - } - - var stream *mongo.ChangeStream - var err error - var ctx context.Context - - for { - if stream == nil { - stream, err = mg.mongoClient.Watch(CollectionWhitelist, mongo.Pipeline{matchStage, projectStage}) - if err != nil { - logger.Error("watchWhitelistCollection watch failed :", err) - time.Sleep(time.Minute) - continue - } - ctx = context.TODO() - } - - changed := stream.TryNext(ctx) - if ctx.Err() != nil { - logger.Error("watchWhitelistCollection stream.TryNext failed. process should be restarted! :", ctx.Err().Error()) - break - } - - if changed { - var data whilelistPipelineDocument - if err := stream.Decode(&data); err == nil { - ot := data.OperationType - switch ot { - case "insert": - // 새 화이트리스트 멤버 - mg.service().wl.add(data.Member) - case "update": - if data.Member.Expired != 0 { - logger.Println("whitelist member is removed :", *data.Member) - mg.service().wl.remove(data.Member.Email, data.Member.Platform) - } else { - logger.Println("whitelist member is updated :", *data.Member) - mg.service().wl.add(data.Member) - } - } - } else { - logger.Error("watchWhitelistCollection stream.Decode failed :", err) - } - } else if stream.Err() != nil || stream.ID() == 0 { - select { - case <-ctx.Done(): - logger.Println("watchWhitelistCollection is done") - stream.Close(ctx) - return - - case <-time.After(time.Second): - logger.Error("watchWhitelistCollection stream error :", stream.Err()) - stream.Close(ctx) - stream = nil - } - } else { - time.Sleep(time.Second) - } - } -} - func (mg *Maingate) watchFileCollection(parentctx context.Context, serveMux *http.ServeMux, prefix string) { defer func() { s := recover() diff --git a/go.mod b/go.mod index 5a9c0de..960c17b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible go.mongodb.org/mongo-driver v1.11.7 google.golang.org/api v0.128.0 - repositories.action2quare.com/ayo/gocommon v0.0.0-20230801051747-b501160efc3b + repositories.action2quare.com/ayo/gocommon v0.0.0-20230825015501-e4527aa5b3ff ) require ( diff --git a/go.sum b/go.sum index 26cff37..83782df 100644 --- a/go.sum +++ b/go.sum @@ -292,3 +292,9 @@ repositories.action2quare.com/ayo/gocommon v0.0.0-20230710085810-8173216e9574 h1 repositories.action2quare.com/ayo/gocommon v0.0.0-20230710085810-8173216e9574/go.mod h1:rn6NA28Mej+qgLNx/Bu2wsdGyIycmacqlNP6gUXX2a0= repositories.action2quare.com/ayo/gocommon v0.0.0-20230801051747-b501160efc3b h1:yV1cBeu0GFxkDD6TDxzKv/rM3OMtyt1JXpeqDF5IO3Y= repositories.action2quare.com/ayo/gocommon v0.0.0-20230801051747-b501160efc3b/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230823084014-c34045e215fc h1:/nFKyjpcfMCdC7vrEZ7+IQOA5RoMmcBUHNRl40JN3ys= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230823084014-c34045e215fc/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230823134414-400c7f644333 h1:3QWHeK6eX1yhaeN/Lu88N4B2ORb/PdBkXUS+HzFOWgU= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230823134414-400c7f644333/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230825015501-e4527aa5b3ff h1:nTOqgPSfm0EANR1SFAi+Zi/KErAAlstVcEWWOnyDT5g= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230825015501-e4527aa5b3ff/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw=