From 39e1b925e5135a04fcfaa5d4df2ccdaec779f77f Mon Sep 17 00:00:00 2001 From: mountain Date: Thu, 17 Aug 2023 12:35:48 +0900 Subject: [PATCH 01/14] =?UTF-8?q?divisionsSerialized=EA=B0=80=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=20=EB=8D=AE=EC=96=B4=EC=94=8C=EC=96=B4=EC=A7=84=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/service.go | 1 - 1 file changed, 1 deletion(-) diff --git a/core/service.go b/core/service.go index c3218f4..be9ed06 100644 --- a/core/service.go +++ b/core/service.go @@ -282,7 +282,6 @@ func (sh *serviceDescription) prepare(mg *Maingate) error { sh.getProviderInfo = mg.getProviderInfo sh.wl = &mg.wl - sh.divisionsSerialized, _ = json.Marshal(sh) sh.serviceSummarySerialized, _ = json.Marshal(sh.ServiceDescriptionSummary) logger.Println("service is ready :", sh.ServiceCode, string(sh.divisionsSerialized)) From a8df7d54bd8be8dc9e6cae861b7d7e4b2ca01d7f Mon Sep 17 00:00:00 2001 From: mountain Date: Fri, 18 Aug 2023 13:56:34 +0900 Subject: [PATCH 02/14] =?UTF-8?q?version=20split=20=EB=94=94=ED=8F=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/service.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/service.go b/core/service.go index be9ed06..5e32a08 100644 --- a/core/service.go +++ b/core/service.go @@ -186,7 +186,7 @@ func (sh *serviceDescription) readProfile(authtype string, id string, binfo stri if err != nil { return "", err } - if userinfo.token == "" { + if len(userinfo.token) == 0 { return "", errors.New("refreshtoken token not found") } @@ -256,7 +256,7 @@ func (sh *serviceDescription) prepare(mg *Maingate) error { if len(sh.VersionSplits) == 0 { sh.VersionSplits = map[string]string{ - "": strings.Join(namesOnly, ","), + "default": strings.Join(namesOnly, ","), } } @@ -758,7 +758,7 @@ func (sh *serviceDescription) findVersionSplit(version string) []byte { } } } - return sh.divisionsSplits[""] + return sh.divisionsSplits["default"] } func (sh *serviceDescription) serveHTTP(w http.ResponseWriter, r *http.Request) { From e8832f329a41c8d77d7f6f835a7531775fd44250 Mon Sep 17 00:00:00 2001 From: mountain Date: Mon, 21 Aug 2023 11:01:21 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=EC=95=88=EC=93=B0=EB=8A=94=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/api.go | 10 +++------- core/maingate.go | 15 ++------------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/core/api.go b/core/api.go index b07bb99..ac9de2e 100644 --- a/core/api.go +++ b/core/api.go @@ -27,7 +27,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,9 +109,8 @@ 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)) + hasher.Write(caller.mg.service().serviceCodeBytes) subfolder := hex.EncodeToString(hasher.Sum(nil))[:8] infile, header, err := r.FormFile("file") @@ -151,12 +149,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 { diff --git a/core/maingate.go b/core/maingate.go index 2066cd8..613ad12 100644 --- a/core/maingate.go +++ b/core/maingate.go @@ -318,18 +318,6 @@ func (mg *Maingate) prepare(context context.Context) (err error) { 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 - } - if err = mg.mongoClient.MakeIndices(CollectionAccount, map[string]bson.D{ "accid": {{Key: "accid", Value: 1}}, }); err != nil { @@ -337,11 +325,12 @@ func (mg *Maingate) prepare(context context.Context) (err error) { } 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 } + // Delete대신 _ts로 expire시킴. pipeline에 삭제 알려주기 위함 if err = mg.mongoClient.MakeExpireIndex(CollectionWhitelist, 10); err != nil { return err } From 455011fd990a541742875e43777cf82f1ece35b9 Mon Sep 17 00:00:00 2001 From: mountain Date: Tue, 22 Aug 2023 10:16:09 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EC=A0=9C=EC=9E=AC?= =?UTF-8?q?=20api=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/api.go | 73 +++++++++++------ core/maingate.go | 23 +++--- core/member_container.go | 169 +++++++++++++++++++++++++++++++++++++++ core/service.go | 116 +++++++++------------------ core/watch.go | 96 ---------------------- 5 files changed, 267 insertions(+), 210 deletions(-) create mode 100644 core/member_container.go diff --git a/core/api.go b/core/api.go index ac9de2e..da14872 100644 --- a/core/api.go +++ b/core/api.go @@ -165,44 +165,67 @@ 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) + var bi blockinfo + if err := json.Unmarshal(body, &bi); err != nil { + return err + } - all, err := mg.mongoClient.All(CollectionWhitelist) + _, _, err := mg.mongoClient.Update(CollectionBlock, bson.M{ + "_id": primitive.NewObjectID(), + }, 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 } - 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) + _, _, 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(), @@ -432,6 +455,8 @@ 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) } if err != nil { diff --git a/core/maingate.go b/core/maingate.go index 613ad12..88dd1f5 100644 --- a/core/maingate.go +++ b/core/maingate.go @@ -169,7 +169,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 @@ -409,27 +410,25 @@ func (mg *Maingate) prepare(context context.Context) (err error) { } } - var whites []whitelistmember + var whites []*whitelistmember if err := mg.mongoClient.AllAs(CollectionWhitelist, &whites, options.Find().SetReturnKey(false)); err != nil { return err } mg.wl.init(whites) + var blocks []*blockinfo + if err := mg.mongoClient.AllAs(CollectionBlock, &blocks, options.Find().SetReturnKey(false)); err != nil { + return 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/service.go b/core/service.go index 5e32a08..c4b3543 100644 --- a/core/service.go +++ b/core/service.go @@ -8,10 +8,9 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" - "sync/atomic" "time" - "unsafe" "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/logger" @@ -22,20 +21,38 @@ import ( ) type blockinfo struct { + Accid primitive.ObjectID `bson:"_id" json:"_id"` 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"` + 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().Before(time.Now().UTC()) } type usertokeninfo struct { @@ -48,54 +65,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 ( @@ -135,7 +104,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 @@ -281,7 +251,8 @@ 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) logger.Println("service is ready :", sh.ServiceCode, string(sh.divisionsSerialized)) @@ -657,28 +628,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{ @@ -839,7 +798,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() From 08cb989975c8932af000c938c644fcf4a12ec1e9 Mon Sep 17 00:00:00 2001 From: mountain Date: Tue, 22 Aug 2023 11:10:59 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20json=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/api.go | 2 +- core/service.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/api.go b/core/api.go index da14872..dab0709 100644 --- a/core/api.go +++ b/core/api.go @@ -276,7 +276,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 diff --git a/core/service.go b/core/service.go index c4b3543..9b6d450 100644 --- a/core/service.go +++ b/core/service.go @@ -28,6 +28,7 @@ type blockinfo struct { } 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"` @@ -254,8 +255,9 @@ func (sh *serviceDescription) prepare(mg *Maingate) error { 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 } From bafb67dabc4b8bd9b94111af3bf934f64661a95d Mon Sep 17 00:00:00 2001 From: mountain Date: Tue, 22 Aug 2023 17:51:41 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=EC=8A=A4=ED=8C=80=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20noauth=20=EC=B6=94=EA=B0=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/platformsteam.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From e3afb58634337eddc6ab5b46a35bf82f595b5fcc Mon Sep 17 00:00:00 2001 From: mountain Date: Tue, 22 Aug 2023 18:06:14 +0900 Subject: [PATCH 07/14] =?UTF-8?q?file=20=EA=B2=BD=EB=A1=9C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20servicecode=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/api.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/core/api.go b/core/api.go index dab0709..e2c0b95 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" @@ -109,10 +107,6 @@ var seq = uint32(0) func (caller apiCaller) uploadAPI(w http.ResponseWriter, r *http.Request) error { if r.Method == "PUT" { - hasher := md5.New() - hasher.Write(caller.mg.service().serviceCodeBytes) - subfolder := hex.EncodeToString(hasher.Sum(nil))[:8] - infile, header, err := r.FormFile("file") if err != nil { w.WriteHeader(http.StatusBadRequest) @@ -128,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{ From 197ee7127b49f04cb4f517e8b6b1ba530e6f7f6c Mon Sep 17 00:00:00 2001 From: mountain Date: Tue, 22 Aug 2023 19:53:30 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=EC=84=9C=EB=B2=84=20noauth=20=ED=94=8C?= =?UTF-8?q?=EB=9E=98=EA=B7=B8=EB=A5=BC=20=EC=95=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/service.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/service.go b/core/service.go index 9b6d450..0cc7c90 100644 --- a/core/service.go +++ b/core/service.go @@ -666,6 +666,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 { From 184675a9b740402745d9c0d3a6d6b3d85189de74 Mon Sep 17 00:00:00 2001 From: mountain Date: Wed, 23 Aug 2023 17:48:47 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EC=A0=9C=EC=9E=AC?= =?UTF-8?q?=20=EB=8F=99=EC=9E=91=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/api.go | 22 ++++++++++++++++++---- core/api_test.go | 39 +++++++++++++++++++++++++++++++++++++++ core/maingate.go | 10 +++++----- core/service.go | 13 ++++++++++--- go.mod | 2 +- go.sum | 2 ++ 6 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 core/api_test.go diff --git a/core/api.go b/core/api.go index e2c0b95..90f5f1b 100644 --- a/core/api.go +++ b/core/api.go @@ -165,13 +165,27 @@ func (caller apiCaller) blockAPI(w http.ResponseWriter, r *http.Request) error { enc.Encode(mg.bl.all()) } else if r.Method == "PUT" { body, _ := io.ReadAll(r.Body) - var bi blockinfo - if err := json.Unmarshal(body, &bi); err != nil { + + var bipl blockinfoWithStringId + if err := json.Unmarshal(body, &bipl); err != nil { return err } - _, _, err := mg.mongoClient.Update(CollectionBlock, bson.M{ - "_id": primitive.NewObjectID(), + accid, err := primitive.ObjectIDFromHex(bipl.StrId) + if err != nil { + return err + } + + 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)) 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 88dd1f5..1f22c67 100644 --- a/core/maingate.go +++ b/core/maingate.go @@ -340,10 +340,10 @@ func (mg *Maingate) prepare(context context.Context) (err error) { return 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 { + if err = mg.mongoClient.DropIndex(CollectionBlock, "codeaccid"); err != nil { + return err + } } if err = mg.mongoClient.MakeExpireIndex(CollectionBlock, int32(3)); err != nil { @@ -417,7 +417,7 @@ func (mg *Maingate) prepare(context context.Context) (err error) { mg.wl.init(whites) var blocks []*blockinfo - if err := mg.mongoClient.AllAs(CollectionBlock, &blocks, options.Find().SetReturnKey(false)); err != nil { + if err := mg.mongoClient.AllAs(CollectionBlock, &blocks); err != nil { return err } mg.bl.init(blocks) diff --git a/core/service.go b/core/service.go index 0cc7c90..e13c8bd 100644 --- a/core/service.go +++ b/core/service.go @@ -21,10 +21,17 @@ import ( ) type blockinfo struct { - Accid primitive.ObjectID `bson:"_id" json:"_id"` 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 { @@ -53,7 +60,7 @@ func (bi *blockinfo) Key() primitive.ObjectID { } func (bi *blockinfo) Expired() bool { - return bi.End.Time().Before(time.Now().UTC()) + return bi.End.Time().Unix() < time.Now().UTC().Unix() } type usertokeninfo struct { diff --git a/go.mod b/go.mod index 42a4fa0..9c42c8f 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-20230823084014-c34045e215fc ) require ( diff --git a/go.sum b/go.sum index 660e2c0..f59b0e6 100644 --- a/go.sum +++ b/go.sum @@ -329,3 +329,5 @@ 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= From a7a20aebcf3ce23722337f4be19381d95cca2413 Mon Sep 17 00:00:00 2001 From: mountain Date: Wed, 23 Aug 2023 22:46:07 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 9c42c8f..19a46d3 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-20230823084014-c34045e215fc + repositories.action2quare.com/ayo/gocommon v0.0.0-20230823134414-400c7f644333 ) require ( diff --git a/go.sum b/go.sum index f59b0e6..11d4856 100644 --- a/go.sum +++ b/go.sum @@ -331,3 +331,5 @@ repositories.action2quare.com/ayo/gocommon v0.0.0-20230801051747-b501160efc3b h1 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= From d9be04541b3a2eb7b104501c3128e1139684a5d8 Mon Sep 17 00:00:00 2001 From: mountain Date: Fri, 25 Aug 2023 11:39:17 +0900 Subject: [PATCH 11/14] =?UTF-8?q?prepare=20=EB=A1=9C=EA=B7=B8=20=EC=9E=90?= =?UTF-8?q?=EC=84=B8=ED=9E=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/maingate.go | 53 +++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/core/maingate.go b/core/maingate.go index 1f22c67..4883098 100644 --- a/core/maingate.go +++ b/core/maingate.go @@ -13,6 +13,7 @@ import ( "net" "net/http" "os" + "runtime/debug" "strings" "sync/atomic" "time" @@ -207,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 } @@ -287,95 +287,98 @@ 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 err + return makeErrorWithStack(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 + 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{ "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 *devflag { - if err = mg.mongoClient.DropIndex(CollectionBlock, "codeaccid"); err != nil { - return err - } + // 에러 체크하지 말것 + 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))) @@ -387,7 +390,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 { @@ -402,23 +405,23 @@ 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 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 err + return makeErrorWithStack(err) } mg.bl.init(blocks) From f5304fae80e6b0baab7fe9e6bf471e92fd53529f Mon Sep 17 00:00:00 2001 From: mountain Date: Fri, 25 Aug 2023 12:31:32 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20api=EB=A5=BC=20maing?= =?UTF-8?q?ate=20=EB=A1=9C=20=EC=98=AE=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/api.go | 34 +++++ core/api_coupon.go | 372 +++++++++++++++++++++++++++++++++++++++++++++ core/maingate.go | 6 + go.mod | 2 +- go.sum | 2 + 5 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 core/api_coupon.go diff --git a/core/api.go b/core/api.go index 90f5f1b..6dc17aa 100644 --- a/core/api.go +++ b/core/api.go @@ -342,6 +342,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 { @@ -464,6 +496,8 @@ func (mg *Maingate) api(w http.ResponseWriter, r *http.Request) { 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..9a7e396 --- /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{"_id": 0, "effect": 1, "name": 1, "reason": 1, "total": 1, "remains": 0, "used": 0})); 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/maingate.go b/core/maingate.go index 4883098..460a6d3 100644 --- a/core/maingate.go +++ b/core/maingate.go @@ -305,6 +305,12 @@ func (mg *Maingate) prepare(context context.Context) (err error) { 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 { diff --git a/go.mod b/go.mod index 19a46d3..cc5ddb5 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-20230823134414-400c7f644333 + repositories.action2quare.com/ayo/gocommon v0.0.0-20230825015501-e4527aa5b3ff ) require ( diff --git a/go.sum b/go.sum index 11d4856..ad51ef5 100644 --- a/go.sum +++ b/go.sum @@ -333,3 +333,5 @@ repositories.action2quare.com/ayo/gocommon v0.0.0-20230823084014-c34045e215fc h1 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= From edb3e073291b75066e4826ae6c47fd481e814270 Mon Sep 17 00:00:00 2001 From: mountain Date: Mon, 28 Aug 2023 13:53:59 +0900 Subject: [PATCH 13/14] =?UTF-8?q?parsemuiltipartform=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20.=20=EC=97=90=EB=9F=AC=EB=8A=94=20=EB=AC=B4=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/api.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/api.go b/core/api.go index 6dc17aa..6aeda99 100644 --- a/core/api.go +++ b/core/api.go @@ -414,6 +414,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 { From 1ba32aa4c92ad7f0ddb47189d11bef4d5f132a18 Mon Sep 17 00:00:00 2001 From: mountain Date: Tue, 29 Aug 2023 11:06:14 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/api_coupon.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/api_coupon.go b/core/api_coupon.go index 9a7e396..8e788f6 100644 --- a/core/api_coupon.go +++ b/core/api_coupon.go @@ -231,7 +231,7 @@ func queryCoupon(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *htt var coupon couponDoc 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, "remains": 0, "used": 0})); err != nil { + }, &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