Files
maingate/core/service.go

766 lines
20 KiB
Go

package core
import (
"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
End primitive.DateTime `bson:"_ts"`
Reason string
}
type whitelistAuthType = string
const (
whitelistAuthType_Default = whitelistAuthType("")
whitelistAuthType_QA = whitelistAuthType("qa")
)
type whitelistmember struct {
Service string
Email string
Platform string
Desc string
Auth []whitelistAuthType
Expired primitive.DateTime `bson:"_ts,omitempty" json:"_ts,omitempty"`
}
type whitelist struct {
emailptr unsafe.Pointer
qaptr unsafe.Pointer
working int32
}
type usertokeninfo struct {
platform string
userid string
token string //refreshtoken
secret string
brinfo string
accesstoken string // microsoft only
accesstoken_expire_time int64 // microsoft only
}
func (wl *whitelist) init(total []whitelistmember) {
auths := make(map[string]map[string]*whitelistmember)
for _, member := range total {
all := auths[""]
if all == nil {
all = make(map[string]*whitelistmember)
auths[""] = all
}
all[whitelistKey(member.Email)] = &member
for _, auth := range member.Auth {
spec := auths[auth]
if spec == nil {
spec = make(map[string]*whitelistmember)
auths[auth] = spec
}
spec[whitelistKey(member.Email)] = &member
}
}
all := auths[whitelistAuthType_Default]
atomic.StorePointer(&wl.emailptr, unsafe.Pointer(&all))
qa := auths[whitelistAuthType_QA]
atomic.StorePointer(&wl.qaptr, unsafe.Pointer(&qa))
}
func addToUnsafePointer(to *unsafe.Pointer, m *whitelistmember) {
ptr := atomic.LoadPointer(to)
src := (*map[string]*whitelistmember)(ptr)
next := map[string]*whitelistmember{}
for k, v := range *src {
next[k] = v
}
next[whitelistKey(m.Email)] = m
atomic.StorePointer(to, unsafe.Pointer(&next))
}
func removeFromUnsafePointer(from *unsafe.Pointer, email string) {
ptr := atomic.LoadPointer(from)
src := (*map[string]*whitelistmember)(ptr)
next := make(map[string]*whitelistmember)
for k, v := range *src {
next[k] = v
}
delete(next, whitelistKey(email))
atomic.StorePointer(from, unsafe.Pointer(&next))
}
func (wl *whitelist) add(m *whitelistmember) {
addToUnsafePointer(&wl.emailptr, m)
for _, auth := range m.Auth {
if auth == whitelistAuthType_QA {
addToUnsafePointer(&wl.qaptr, m)
}
}
}
func (wl *whitelist) remove(email string) {
removeFromUnsafePointer(&wl.emailptr, email)
removeFromUnsafePointer(&wl.qaptr, email)
}
func (wl *whitelist) isMember(email string, platform string) bool {
if atomic.LoadInt32(&wl.working) == 0 {
return true
}
ptr := atomic.LoadPointer(&wl.emailptr)
src := *(*map[string]*whitelistmember)(ptr)
if member, exists := src[whitelistKey(email)]; exists {
return member.Platform == platform
}
return false
}
func (wl *whitelist) hasAuth(email string, platform string, auth whitelistAuthType) bool {
if auth == whitelistAuthType_QA {
ptr := atomic.LoadPointer(&wl.qaptr)
src := *(*map[string]*whitelistmember)(ptr)
if member, exists := src[whitelistKey(email)]; exists {
return member.Platform == platform
}
}
return false
}
type divisionStateName string
const (
DivisionState_Closed = divisionStateName("closed")
DivisionState_Maintenance = divisionStateName("maintenance")
DivisionState_RestrictedOpen = divisionStateName("restricted")
DivisionState_FullOpen = divisionStateName("open")
)
type maintenance struct {
Notice string `bson:"notice"`
StartTimeUTC int64 `bson:"start_unixtime_utc" json:"start_unixtime_utc"`
link string
}
type division struct {
Url string // 요것은 클라이언트 빌드하고 나서 json:"-"으로 변경하자. 클라이언트에 직접 내려보내지 않음
Priority int
State divisionStateName
Maintenance *maintenance `bson:",omitempty" json:",omitempty"`
}
type serviceDescription struct {
// sync.Mutex
Id primitive.ObjectID `bson:"_id"`
ServiceName string `bson:"service"`
Divisions map[string]*division `bson:"divisions"`
ServiceCode string `bson:"code"`
UseWhitelist bool `bson:"use_whitelist"`
Closed bool `bson:"closed"`
ServerApiTokens []primitive.ObjectID `bson:"api_tokens"`
ApiUsers map[string][]string `bson:"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
divisionsSerialized unsafe.Pointer
serviceSerialized 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:])
}
var closed []string
for dn, div := range divs {
if div.State == DivisionState_Closed {
closed = append(closed, dn)
continue
}
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 {
div.Maintenance.link = path.Join("static", sh.ServiceCode, div.Maintenance.Notice)
}
}
} else {
div.Maintenance = nil
}
}
for _, dn := range closed {
delete(divs, dn)
}
divmarshaled, _ := json.Marshal(divs)
devstr := string(divmarshaled)
sh.divisionsSerialized = unsafe.Pointer(&devstr)
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))
logger.Println("service is ready :", sh.ServiceName, sh.ServiceCode, sh.UseWhitelist, sh.ApiUsers, string(divmarshaled))
return nil
}
func (sh *serviceDescription) link(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
if r.Method != "GET" {
w.WriteHeader(http.StatusBadRequest)
return
}
queryvals := r.URL.Query()
//oldToken := queryvals.Get("otoken")
oldType := queryvals.Get("otype")
oldId := queryvals.Get("oid")
sk := queryvals.Get("sk")
//newToken := queryvals.Get("ntoken")
newType := queryvals.Get("ntype")
newId := queryvals.Get("nid")
oldAuth := sh.auths.Find(sk)
if oldAuth == nil {
// 잘못된 세션
logger.Println("link failed. session key is not valid :", sk)
w.WriteHeader(http.StatusBadRequest)
return
}
// fmt.Println("=================")
// fmt.Println(oldType)
// fmt.Println(oldId)
// fmt.Println("=================")
// fmt.Println(newType)
// fmt.Println(newId)
// fmt.Println("=================")
// fmt.Println(oldAuth.Platform)
// fmt.Println(oldAuth.Uid)
// fmt.Println("=================")
//if oldAuth.Token != oldToken || oldAuth.Uid != oldId || oldAuth.Platform != oldType {
if oldAuth.Uid != oldId || oldAuth.Platform != oldType {
logger.Println("link failed. session key is not correct :", *oldAuth, queryvals)
w.WriteHeader(http.StatusBadRequest)
return
}
bfinfo, err := sh.getUserBrowserInfo(r)
if err != nil {
logger.Error("getUserBrowserInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
_, err = sh.readProfile(oldType, oldId, bfinfo)
if err != nil {
logger.Error("readProfile(old) failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
email, err := sh.readProfile(newType, newId, bfinfo)
if err != nil {
logger.Error("readProfile(new) failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// if len(email) == 0 {
// logger.Println("link failed. email is missing :", r.URL.Query())
// w.WriteHeader(http.StatusBadRequest)
// return
// }
if !sh.wl.isMember(email, newType) {
logger.Println("link failed. not whitelist member :", r.URL.Query(), email)
w.WriteHeader(http.StatusBadRequest)
return
}
newType, newId, err = sh.getProviderInfo(newType, newId)
if err != nil {
logger.Error("getProviderInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
}
createtime := primitive.NewDateTimeFromTime(time.Now().UTC())
link, err := sh.mongoClient.FindOneAndUpdate(CollectionLink, bson.M{
"platform": newType,
"uid": newId,
}, bson.M{
"$setOnInsert": bson.M{
"create": createtime,
"email": email,
},
}, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"_id": 1}))
if err != nil {
logger.Error("link failed. FindOneAndUpdate link err:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
_, newid, err := sh.mongoClient.Update(common.CollectionName(sh.ServiceName), bson.M{
"_id": link["_id"].(primitive.ObjectID),
}, bson.M{
"$setOnInsert": bson.M{
"accid": oldAuth.Accid,
"create": createtime,
},
}, options.Update().SetUpsert(true))
if err != nil {
logger.Error("link failed. Update ServiceName err :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// newid가 있어야 한다. 그래야 기존 서비스 계정이 없는 상태이다.
if newid == nil {
// 이미 계정이 있네?
logger.Println("link failed. already have service account :", r.URL.Query())
w.WriteHeader(http.StatusBadRequest)
return
}
logger.Println("link success :", r.URL.Query())
}
func (sh *serviceDescription) isValidAPIUser(category string, email string) bool {
ptr := atomic.LoadPointer(&sh.apiUsers)
catusers := *(*map[string]map[string]bool)(ptr)
if category == "*" {
for _, users := range catusers {
if _, ok := users[email]; ok {
return true
}
}
logger.Println("isValidAPIUser failed. email is not allowed :", category, email, catusers)
return false
}
if users, ok := catusers[category]; ok {
if _, ok := users[email]; ok {
return true
}
logger.Println("isValidAPIUser failed. email is not allowed :", category, email, users)
return false
}
logger.Println("isValidAPIUser failed. category is missing :", category)
return false
}
func (sh *serviceDescription) authorize(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
if r.Method != "GET" {
w.WriteHeader(http.StatusBadRequest)
return
}
queryvals := r.URL.Query()
authtype := queryvals.Get("type")
uid := queryvals.Get("id")
//accesstoken := queryvals.Get("token") //-- 이거 이제 받지마라
session := queryvals.Get("sk")
//email, err := sh.readProfile(authtype, uid, accesstoken)
bfinfo, err := sh.getUserBrowserInfo(r)
if err != nil {
logger.Error("getUserBrowserInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
email, err := sh.readProfile(authtype, uid, bfinfo)
if err != nil {
logger.Error("readProfile failed :", err)
w.WriteHeader(http.StatusBadRequest)
return
}
if !sh.wl.isMember(email, authtype) {
logger.Println("auth failed. not whitelist member :", sh.ServiceCode, authtype, uid, email)
w.WriteHeader(http.StatusBadRequest)
return
}
logger.Println("auth success :", authtype, uid, email, session)
newType, newId, err := sh.getProviderInfo(authtype, uid)
if err != nil {
logger.Error("getProviderInfo failed :", err)
w.WriteHeader(http.StatusBadRequest)
}
if authtype != newType || uid != newId {
authtype = newType
uid = newId
logger.Println("auth success ( redirect ) :", authtype, uid, email, session)
}
//if len(session) == 0 && len(email) > 0 {
if len(session) == 0 {
// platform + id -> account id
createtime := primitive.NewDateTimeFromTime(time.Now().UTC())
link, err := sh.mongoClient.FindOneAndUpdate(CollectionLink, bson.M{
"platform": authtype,
"uid": uid,
}, bson.M{
"$setOnInsert": bson.M{
"create": createtime,
"email": email,
},
}, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"_id": 1}))
if err != nil {
logger.Error("authorize failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
linkid := link["_id"].(primitive.ObjectID)
newaccid := primitive.NewObjectID()
for i := 0; i < len(sh.serviceCodeBytes); i++ {
newaccid[i] ^= sh.serviceCodeBytes[i]
}
account, err := sh.mongoClient.FindOneAndUpdate(common.CollectionName(sh.ServiceName), bson.M{
"_id": linkid,
}, bson.M{
"$setOnInsert": bson.M{
"accid": newaccid,
"create": createtime,
},
}, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(true).SetProjection(bson.M{"accid": 1, "create": 1}))
if err != nil {
logger.Error("authorize failed. Update sh.ServiceName err:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
accid := account["accid"].(primitive.ObjectID)
oldcreate := account["create"].(primitive.DateTime)
newaccount := oldcreate == createtime
var bi blockinfo
if err := sh.mongoClient.FindOneAs(CollectionBlock, bson.M{
"code": sh.ServiceCode,
"accid": accid,
}, &bi); err != nil {
logger.Error("authorize failed. find blockinfo in CollectionBlock err:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if !bi.Start.Time().IsZero() {
now := time.Now().UTC()
if bi.Start.Time().Before(now) && bi.End.Time().After(now) {
// block됐네?
// status는 정상이고 reason을 넘겨주자
json.NewEncoder(w).Encode(map[string]any{
"blocked": bi,
})
return
}
}
newsession := primitive.NewObjectID()
expired := primitive.NewDateTimeFromTime(time.Now().UTC().Add(sh.sessionTTL))
newauth := common.Authinfo{
Accid: accid,
ServiceCode: sh.ServiceCode,
Platform: authtype,
Uid: uid,
Email: email,
Sk: newsession,
Expired: expired,
//RefreshToken: queryvals.Get("rt"),
}
_, _, err = sh.mongoClient.UpsertOne(CollectionAuth, bson.M{"_id": newauth.Accid}, &newauth)
if err != nil {
logger.Error("authorize failed :", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
output := map[string]any{
"sk": newsession.Hex(),
"expirein": sh.sessionTTL.Seconds(),
"newAccount": newaccount,
"accid": newauth.Accid.Hex(),
}
bt, _ := json.Marshal(output)
w.Write(bt)
} else if len(session) > 0 {
sessionobj, _ := primitive.ObjectIDFromHex(session)
if !sessionobj.IsZero() {
updated, _, err := sh.mongoClient.Update(CollectionAuth,
bson.M{
"sk": sessionobj,
},
bson.M{
"$currentDate": bson.M{
"_ts": bson.M{"$type": "date"},
},
}, options.Update().SetUpsert(false))
if err != nil {
logger.Error("update auth collection failed")
logger.Error(err)
return
}
if !updated {
// 세션이 없네?
logger.Println("authorize failed. session not exists in database :", session)
w.WriteHeader(http.StatusUnauthorized)
return
}
output := map[string]any{
"sk": session,
"expirein": sh.sessionTTL.Seconds(),
}
bt, _ := json.Marshal(output)
w.Write(bt)
} else {
logger.Println("authorize failed. sk is not valid hex :", session)
w.WriteHeader(http.StatusBadRequest)
return
}
} else {
logger.Println("authorize failed. id empty :", queryvals)
}
}
func (sh *serviceDescription) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
s := recover()
if s != nil {
logger.Error(s)
}
}()
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
if atomic.LoadInt32(&sh.closed) != 0 {
w.WriteHeader(http.StatusNotFound)
return
}
if strings.HasSuffix(r.URL.Path, "/auth") {
sh.authorize(w, r)
} else if strings.HasSuffix(r.URL.Path, "/link") {
sh.link(w, r)
} else {
// TODO : 세션키와 authtoken을 헤더로 받아서 accid 조회
queryvals := r.URL.Query()
sk := queryvals.Get("sk")
//if len(token) == 0 || len(sk) == 0 {
if len(sk) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
// TODO : 각 서버에 있는 자산? 캐릭터 정보를 보여줘야 하나. 뭘 보여줄지는 프로젝트에 문의
// 일단 서버 종류만 내려보내자
// 세션키가 있는지 확인
if _, ok := sh.auths.IsValid(sk, ""); !ok {
logger.Println("sessionkey is not valid :", sk)
w.WriteHeader(http.StatusBadRequest)
return
}
if divname := queryvals.Get("div"); len(divname) > 0 {
divname = strings.Trim(divname, `"`)
// 점검중인지 아닌지 확인
// 점검중이어도 입장이 가능한 인원이 있다.
div := sh.Divisions[divname]
if div != nil {
switch div.State {
case DivisionState_FullOpen:
w.Write([]byte(fmt.Sprintf(`{"service":"%s"}`, div.Url)))
case DivisionState_RestrictedOpen:
// 점검중인데 일부 권한을 갖고 있는 유저만 들어갈 수 있는 상태
cell := sh.auths.QuerySession(sk, "")
if cell == nil {
logger.Println("sessionkey is not valid :", sk)
w.WriteHeader(http.StatusBadRequest)
return
}
if sh.wl.hasAuth(cell.ToAuthinfo().Email, cell.ToAuthinfo().Platform, whitelistAuthType_QA) {
// qa 권한이면 입장 가능
w.Write([]byte(fmt.Sprintf(`{"service":"%s"}`, div.Url)))
} else if div.Maintenance != nil {
// 권한이 없으므로 공지
w.Write([]byte(fmt.Sprintf(`{"notice":"%s"}`, div.Maintenance.link)))
} else {
logger.Println("div.Maintenance is nil :", divname)
}
case DivisionState_Maintenance:
// 점검중. 아무도 못들어감
if div.Maintenance != nil {
w.Write([]byte(fmt.Sprintf(`{"notice":"%s"}`, div.Maintenance.link)))
} else {
logger.Println("div.Maintenance is nil :", divname)
}
}
}
} else {
divstrptr := atomic.LoadPointer(&sh.divisionsSerialized)
divstr := *(*string)(divstrptr)
w.Write([]byte(divstr))
}
}
}