Files
maingate/core/service.go

758 lines
21 KiB
Go

package core
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"path"
"strings"
"sync/atomic"
"time"
"unsafe"
common "repositories.action2quare.com/ayo/gocommon"
"repositories.action2quare.com/ayo/gocommon/logger"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
)
type blockinfo struct {
Start primitive.DateTime `bson:"start" json:"start"`
End primitive.DateTime `bson:"_ts"`
Reason string `bson:"reason" json:"reason"`
}
type whitelistMemberTag = string
const (
whitelistMemberTag_Default = whitelistMemberTag("")
whitelistMemberTag_QA = whitelistMemberTag("#qa")
)
type whitelistmember struct {
Service string `bson:"service" json:"service"`
Email string `bson:"email" json:"email"`
Platform string `bson:"platform" json:"platform"`
Desc string `bson:"desc" json:"desc"`
Tag string `bson:"tag" json:"tag"`
Expired primitive.DateTime `bson:"_ts,omitempty" json:"_ts,omitempty"`
}
type whitelist struct {
emailptr unsafe.Pointer
working int32
}
type usertokeninfo struct {
platform string
userid string
token string //refreshtoken
secret string
brinfo string
accesstoken string // microsoft only
accesstoken_expire_time int64 // microsoft only
}
func (wl *whitelist) init(total []whitelistmember) {
all := make(map[string]*whitelistmember)
for _, member := range total {
all[whitelistKey(member.Email)] = &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
atomic.StorePointer(to, unsafe.Pointer(&next))
}
func removeFromUnsafePointer(from *unsafe.Pointer, email string) {
ptr := atomic.LoadPointer(from)
src := (*map[string]*whitelistmember)(ptr)
next := make(map[string]*whitelistmember)
for k, v := range *src {
next[k] = v
}
delete(next, whitelistKey(email))
atomic.StorePointer(from, unsafe.Pointer(&next))
}
func (wl *whitelist) add(m *whitelistmember) {
// 테스트
m.Tag = whitelistMemberTag_QA
addToUnsafePointer(&wl.emailptr, m)
}
func (wl *whitelist) remove(email string) {
removeFromUnsafePointer(&wl.emailptr, email)
}
func (wl *whitelist) isMember(email string, platform string) bool {
if atomic.LoadInt32(&wl.working) == 0 {
return true
}
ptr := atomic.LoadPointer(&wl.emailptr)
src := *(*map[string]*whitelistmember)(ptr)
if member, exists := src[whitelistKey(email)]; exists {
return member.Platform == platform
}
return false
}
func (wl *whitelist) hasTag(email string, platform string, tag whitelistMemberTag) bool {
ptr := atomic.LoadPointer(&wl.emailptr)
src := *(*map[string]*whitelistmember)(ptr)
if member, exists := src[whitelistKey(email)]; exists {
return strings.Contains(member.Tag, tag)
}
return false
}
type DivisionStateName string
const (
DivisionState_Closed = DivisionStateName("closed")
DivisionState_Maintenance = DivisionStateName("maintenance")
DivisionState_RestrictedOpen = DivisionStateName("restricted")
DivisionState_FullOpen = DivisionStateName("open")
)
type Maintenance struct {
Notice string `bson:"notice" json:"notice"`
StartTimeUTC int64 `bson:"start_unixtime_utc" json:"start_unixtime_utc"`
link string
}
type DivisionForUser struct {
Priority int `bson:"priority" json:"priority"`
State DivisionStateName `bson:"state" json:"state"`
Maintenance *Maintenance `bson:"maintenance,omitempty" json:"maintenance,omitempty"`
}
type Division struct {
DivisionForUser `bson:",inline" json:",inline"`
Url string `bson:"url" json:"url"`
}
type ServiceDescriptionSummary struct {
Id primitive.ObjectID `bson:"_id" json:"_id"`
ServiceName string `bson:"service" json:"service"`
ServiceCode string `bson:"code" json:"code"`
UseWhitelist bool `bson:"use_whitelist" json:"use_whitelist"`
Closed bool `bson:"closed" json:"closed"`
}
type serviceDescription struct {
ServiceDescriptionSummary `bson:",inline" json:",inline"`
Divisions map[string]*Division `bson:"divisions" json:"divisions"`
ServerApiTokens []primitive.ObjectID `bson:"api_tokens" json:"api_tokens"`
ApiUsers map[string][]string `bson:"api_users" json:"api_users"`
auths *common.AuthCollection
wl whitelist
mongoClient common.MongoClient
sessionTTL time.Duration
closed int32
serviceCodeBytes []byte
getUserBrowserInfo func(r *http.Request) (string, error)
getUserTokenWithCheck func(platform string, userid string, brinfo string) (usertokeninfo, error)
updateUserinfo func(info usertokeninfo) (bool, string, string)
getProviderInfo func(platform string, uid string) (string, string, error)
apiUsers unsafe.Pointer
divisionsForUsersSerialized unsafe.Pointer
divisionsSerialized unsafe.Pointer
serviceSerialized unsafe.Pointer
serviceSummarySerialized unsafe.Pointer
}
func (sh *serviceDescription) readProfile(authtype string, id string, binfo string) (email string, err error) {
defer func() {
s := recover()
if s != nil {
logger.Error("readProfile failed :", authtype, id, s)
if errt, ok := s.(error); ok {
err = errt
} else {
err = errors.New(fmt.Sprint(s))
}
}
}()
userinfo, err := sh.getUserTokenWithCheck(authtype, id, binfo)
if err != nil {
return "", err
}
if userinfo.token == "" {
return "", errors.New("refreshtoken token not found")
}
//-- 토큰으로 모두 확인이 끝났으면 갱신한다.
ok, _, email := sh.updateUserinfo(userinfo)
if !ok {
return "", errors.New("updateUserinfo failed")
}
return email, nil
}
func (sh *serviceDescription) prepare(mg *Maingate) error {
divs := sh.Divisions
if len(sh.ServiceCode) == 0 {
sh.ServiceCode = hex.EncodeToString(sh.Id[6:])
}
divsForUsers := make(map[string]*DivisionForUser)
for dn, div := range divs {
if div.State == DivisionState_Closed {
continue
}
divsForUsers[dn] = &div.DivisionForUser
if len(div.State) == 0 {
div.State = DivisionState_FullOpen
}
if div.State != DivisionState_FullOpen {
if div.Maintenance == nil {
div.Maintenance = &Maintenance{}
}
if len(div.Maintenance.link) == 0 {
if len(div.Maintenance.Notice) == 0 {
div.Maintenance.link = "https://www.action2quare.com"
} else if strings.HasPrefix(div.Maintenance.Notice, "http") {
div.Maintenance.link = div.Maintenance.Notice
} else {
hasher := md5.New()
hasher.Write([]byte(sh.ServiceName))
subfolder := hex.EncodeToString(hasher.Sum(nil))[:8]
div.Maintenance.link = path.Join("static", subfolder, div.Maintenance.Notice)
}
}
} else {
div.Maintenance = nil
}
}
divmarshaled, _ := json.Marshal(divs)
devstr := string(divmarshaled)
sh.divisionsSerialized = unsafe.Pointer(&devstr)
divmarshaled2, _ := json.Marshal(divsForUsers)
devstr2 := string(divmarshaled2)
sh.divisionsForUsersSerialized = unsafe.Pointer(&devstr2)
sh.mongoClient = mg.mongoClient
sh.auths = mg.auths
sh.sessionTTL = time.Duration(mg.SessionTTL * int64(time.Second))
sh.wl = whitelist{}
sh.serviceCodeBytes, _ = hex.DecodeString(sh.ServiceCode)
sh.getUserBrowserInfo = mg.GetUserBrowserInfo
sh.getUserTokenWithCheck = mg.getUserTokenWithCheck
sh.updateUserinfo = mg.updateUserinfo
sh.getProviderInfo = mg.getProviderInfo
if sh.Closed {
sh.closed = 1
} else {
sh.closed = 0
}
var whites []whitelistmember
if err := mg.mongoClient.FindAllAs(CollectionWhitelist, bson.M{
"$or": []bson.M{{"service": sh.ServiceName}, {"service": sh.ServiceCode}},
}, &whites, options.Find().SetReturnKey(false)); err != nil {
return err
}
sh.wl.init(whites)
if sh.UseWhitelist {
sh.wl.working = 1
} else {
sh.wl.working = 0
}
if len(sh.ApiUsers) == 0 {
sh.ApiUsers = map[string][]string{
"service": {},
"whitelist": {},
"account": {},
"maintenance": {},
}
}
parsedUsers := make(map[string]map[string]bool)
for cat, users := range sh.ApiUsers {
catusers := make(map[string]bool)
for _, user := range users {
catusers[user] = true
}
parsedUsers[cat] = catusers
}
sh.apiUsers = unsafe.Pointer(&parsedUsers)
for _, keyid := range sh.ServerApiTokens {
mg.apiTokenToService.add(keyid.Hex(), sh.ServiceCode)
}
bt, _ := json.Marshal(sh)
atomic.StorePointer(&sh.serviceSerialized, unsafe.Pointer(&bt))
btsum, _ := json.Marshal(sh.ServiceDescriptionSummary)
atomic.StorePointer(&sh.serviceSummarySerialized, unsafe.Pointer(&btsum))
logger.Println("service is ready :", sh.ServiceName, sh.ServiceCode, sh.UseWhitelist, sh.ApiUsers, string(divmarshaled))
return nil
}
func (sh *serviceDescription) link(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
if r.Method != "GET" {
w.WriteHeader(http.StatusBadRequest)
return
}
queryvals := r.URL.Query()
//oldToken := queryvals.Get("otoken")
oldType := queryvals.Get("otype")
oldId := queryvals.Get("oid")
sk := queryvals.Get("sk")
//newToken := queryvals.Get("ntoken")
newType := queryvals.Get("ntype")
newId := queryvals.Get("nid")
oldAuth := sh.auths.Find(sk)
if oldAuth == nil {
// 잘못된 세션
logger.Println("link failed. session key is not valid :", sk)
w.WriteHeader(http.StatusBadRequest)
return
}
// fmt.Println("=================")
// fmt.Println(oldType)
// fmt.Println(oldId)
// fmt.Println("=================")
// fmt.Println(newType)
// fmt.Println(newId)
// fmt.Println("=================")
// fmt.Println(oldAuth.Platform)
// fmt.Println(oldAuth.Uid)
// fmt.Println("=================")
//if oldAuth.Token != oldToken || oldAuth.Uid != oldId || oldAuth.Platform != oldType {
if oldAuth.Uid != oldId || oldAuth.Platform != oldType {
logger.Println("link failed. session key is not correct :", *oldAuth, queryvals)
w.WriteHeader(http.StatusBadRequest)
return
}
bfinfo, err := sh.getUserBrowserInfo(r)
if err != nil {
logger.Error("getUserBrowserInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
_, err = sh.readProfile(oldType, oldId, bfinfo)
if err != nil {
logger.Error("readProfile(old) failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
email, err := sh.readProfile(newType, newId, bfinfo)
if err != nil {
logger.Error("readProfile(new) failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// if len(email) == 0 {
// logger.Println("link failed. email is missing :", r.URL.Query())
// w.WriteHeader(http.StatusBadRequest)
// return
// }
if !sh.wl.isMember(email, newType) {
logger.Println("link failed. not whitelist member :", r.URL.Query(), email)
w.WriteHeader(http.StatusBadRequest)
return
}
newType, newId, err = sh.getProviderInfo(newType, newId)
if err != nil {
logger.Error("getProviderInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
}
createtime := primitive.NewDateTimeFromTime(time.Now().UTC())
link, err := sh.mongoClient.FindOneAndUpdate(CollectionLink, bson.M{
"platform": newType,
"uid": newId,
}, bson.M{
"$setOnInsert": bson.M{
"create": createtime,
"email": email,
},
}, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"_id": 1}))
if err != nil {
logger.Error("link failed. FindOneAndUpdate link err:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
_, newid, err := sh.mongoClient.Update(common.CollectionName(sh.ServiceName), bson.M{
"_id": link["_id"].(primitive.ObjectID),
}, bson.M{
"$setOnInsert": bson.M{
"accid": oldAuth.Accid,
"create": createtime,
},
}, options.Update().SetUpsert(true))
if err != nil {
logger.Error("link failed. Update ServiceName err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// newid가 있어야 한다. 그래야 기존 서비스 계정이 없는 상태이다.
if newid == nil {
// 이미 계정이 있네?
logger.Println("link failed. already have service account :", r.URL.Query())
w.WriteHeader(http.StatusBadRequest)
return
}
logger.Println("link success :", r.URL.Query())
}
func (sh *serviceDescription) isValidAPIUser(category string, email string) bool {
ptr := atomic.LoadPointer(&sh.apiUsers)
catusers := *(*map[string]map[string]bool)(ptr)
if category == "*" {
for _, users := range catusers {
if _, ok := users[email]; ok {
return true
}
}
logger.Println("isValidAPIUser failed. email is not allowed :", category, email, catusers)
return false
}
if users, ok := catusers[category]; ok {
if _, ok := users[email]; ok {
return true
}
logger.Println("isValidAPIUser failed. email is not allowed :", category, email, users)
return false
}
logger.Println("isValidAPIUser failed. category is missing :", category)
return false
}
func (sh *serviceDescription) authorize(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
if r.Method != "GET" {
w.WriteHeader(http.StatusBadRequest)
return
}
queryvals := r.URL.Query()
authtype := queryvals.Get("type")
uid := queryvals.Get("id")
//accesstoken := queryvals.Get("token") //-- 이거 이제 받지마라
session := queryvals.Get("sk")
//email, err := sh.readProfile(authtype, uid, accesstoken)
bfinfo, err := sh.getUserBrowserInfo(r)
if err != nil {
logger.Error("getUserBrowserInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
email, err := sh.readProfile(authtype, uid, bfinfo)
if err != nil {
logger.Error("readProfile failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
if !sh.wl.isMember(email, authtype) {
logger.Println("auth failed. not whitelist member :", sh.ServiceCode, authtype, uid, email)
w.WriteHeader(http.StatusBadRequest)
return
}
logger.Println("auth success :", authtype, uid, email, session)
newType, newId, err := sh.getProviderInfo(authtype, uid)
if err != nil {
logger.Error("getProviderInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
}
if authtype != newType || uid != newId {
authtype = newType
uid = newId
logger.Println("auth success ( redirect ) :", authtype, uid, email, session)
}
//if len(session) == 0 && len(email) > 0 {
if len(session) == 0 {
// platform + id -> account id
createtime := primitive.NewDateTimeFromTime(time.Now().UTC())
link, err := sh.mongoClient.FindOneAndUpdate(CollectionLink, bson.M{
"platform": authtype,
"uid": uid,
}, bson.M{
"$setOnInsert": bson.M{
"create": createtime,
"email": email,
},
}, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"_id": 1}))
if err != nil {
logger.Error("authorize failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
linkid := link["_id"].(primitive.ObjectID)
newaccid := primitive.NewObjectID()
for i := 0; i < len(sh.serviceCodeBytes); i++ {
newaccid[i] ^= sh.serviceCodeBytes[i]
}
account, err := sh.mongoClient.FindOneAndUpdate(common.CollectionName(sh.ServiceName), bson.M{
"_id": linkid,
}, bson.M{
"$setOnInsert": bson.M{
"accid": newaccid,
"create": createtime,
},
}, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"accid": 1, "create": 1}))
if err != nil {
logger.Error("authorize failed. Update sh.ServiceName err:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
accid := account["accid"].(primitive.ObjectID)
oldcreate := account["create"].(primitive.DateTime)
newaccount := oldcreate == createtime
var bi blockinfo
if err := sh.mongoClient.FindOneAs(CollectionBlock, bson.M{
"code": sh.ServiceCode,
"accid": accid,
}, &bi); err != nil {
logger.Error("authorize failed. find blockinfo in CollectionBlock err:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if !bi.Start.Time().IsZero() {
now := time.Now().UTC()
if bi.Start.Time().Before(now) && bi.End.Time().After(now) {
// block됐네?
// status는 정상이고 reason을 넘겨주자
json.NewEncoder(w).Encode(map[string]any{
"blocked": bi,
})
return
}
}
newsession := primitive.NewObjectID()
expired := primitive.NewDateTimeFromTime(time.Now().UTC().Add(sh.sessionTTL))
newauth := common.Authinfo{
Accid: accid,
ServiceCode: sh.ServiceCode,
Platform: authtype,
Uid: uid,
Email: email,
Sk: newsession,
Expired: expired,
//RefreshToken: queryvals.Get("rt"),
}
_, _, err = sh.mongoClient.UpsertOne(CollectionAuth, bson.M{"_id": newauth.Accid}, &newauth)
if err != nil {
logger.Error("authorize failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
output := map[string]any{
"sk": newsession.Hex(),
"expirein": sh.sessionTTL.Seconds(),
"newAccount": newaccount,
"accid": newauth.Accid.Hex(),
}
bt, _ := json.Marshal(output)
w.Write(bt)
} else if len(session) > 0 {
sessionobj, _ := primitive.ObjectIDFromHex(session)
if !sessionobj.IsZero() {
updated, _, err := sh.mongoClient.Update(CollectionAuth,
bson.M{
"sk": sessionobj,
},
bson.M{
"$currentDate": bson.M{
"_ts": bson.M{"$type": "date"},
},
}, options.Update().SetUpsert(false))
if err != nil {
logger.Error("update auth collection failed")
logger.Error(err)
return
}
if !updated {
// 세션이 없네?
logger.Println("authorize failed. session not exists in database :", session)
w.WriteHeader(http.StatusUnauthorized)
return
}
output := map[string]any{
"sk": session,
"expirein": sh.sessionTTL.Seconds(),
}
bt, _ := json.Marshal(output)
w.Write(bt)
} else {
logger.Println("authorize failed. sk is not valid hex :", session)
w.WriteHeader(http.StatusBadRequest)
return
}
} else {
logger.Println("authorize failed. id empty :", queryvals)
}
}
func (sh *serviceDescription) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
if atomic.LoadInt32(&sh.closed) != 0 {
w.WriteHeader(http.StatusNotFound)
return
}
if strings.HasSuffix(r.URL.Path, "/auth") {
sh.authorize(w, r)
} else if strings.HasSuffix(r.URL.Path, "/link") {
sh.link(w, r)
} else {
// TODO : 세션키와 authtoken을 헤더로 받아서 accid 조회
queryvals := r.URL.Query()
sk := queryvals.Get("sk")
//if len(token) == 0 || len(sk) == 0 {
if len(sk) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
// TODO : 각 서버에 있는 자산? 캐릭터 정보를 보여줘야 하나. 뭘 보여줄지는 프로젝트에 문의
// 일단 서버 종류만 내려보내자
// 세션키가 있는지 확인
if _, ok := sh.auths.IsValid(sk, ""); !ok {
logger.Println("sessionkey is not valid :", sk)
w.WriteHeader(http.StatusUnauthorized)
return
}
if divname := queryvals.Get("div"); len(divname) > 0 {
divname = strings.Trim(divname, `"`)
// 점검중인지 아닌지 확인
// 점검중이어도 입장이 가능한 인원이 있다.
div := sh.Divisions[divname]
if div != nil {
switch div.State {
case DivisionState_FullOpen:
w.Write([]byte(fmt.Sprintf(`{"service":"%s"}`, div.Url)))
case DivisionState_RestrictedOpen:
// 점검중인데 일부 권한을 갖고 있는 유저만 들어갈 수 있는 상태
cell := sh.auths.QuerySession(sk, "")
if cell == nil {
logger.Println("sessionkey is not valid :", sk)
w.WriteHeader(http.StatusBadRequest)
return
}
if sh.wl.hasTag(cell.ToAuthinfo().Email, cell.ToAuthinfo().Platform, whitelistMemberTag_QA) {
// qa 권한이면 입장 가능
w.Write([]byte(fmt.Sprintf(`{"service":"%s"}`, div.Url)))
} else if div.Maintenance != nil {
// 권한이 없으므로 공지
w.Write([]byte(fmt.Sprintf(`{"notice":"%s"}`, div.Maintenance.link)))
} else {
logger.Println("div.Maintenance is nil :", divname)
}
case DivisionState_Maintenance:
// 점검중. 아무도 못들어감
if div.Maintenance != nil {
w.Write([]byte(fmt.Sprintf(`{"notice":"%s"}`, div.Maintenance.link)))
} else {
logger.Println("div.Maintenance is nil :", divname)
}
}
}
} else {
divstrptr := atomic.LoadPointer(&sh.divisionsForUsersSerialized)
divstr := *(*string)(divstrptr)
w.Write([]byte(divstr))
}
}
}