615 lines
14 KiB
Go
615 lines
14 KiB
Go
package core
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"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 fileDocumentDesc struct {
|
|
Service string
|
|
Key string
|
|
Src string
|
|
Link string
|
|
Desc string
|
|
Extract bool
|
|
Timestamp int64
|
|
Contents []byte `json:",omitempty"`
|
|
}
|
|
|
|
func (fd *fileDocumentDesc) save() error {
|
|
// 새 파일 올라옴
|
|
if len(fd.Contents) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var destFile string
|
|
if fd.Extract {
|
|
os.MkdirAll(fd.Link, os.ModePerm)
|
|
destFile = path.Join(fd.Link, fd.Src)
|
|
} else {
|
|
os.MkdirAll(path.Dir(fd.Link), os.ModePerm)
|
|
destFile = fd.Link
|
|
}
|
|
|
|
f, err := os.Create(destFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = io.Copy(f, bytes.NewBuffer(fd.Contents))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if fd.Extract {
|
|
switch path.Ext(destFile) {
|
|
case ".zip":
|
|
err = common.Unzip(destFile)
|
|
case ".tar":
|
|
err = common.Untar(destFile)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (caller apiCaller) isGlobalAdmin() bool {
|
|
if *noauth {
|
|
return true
|
|
}
|
|
|
|
email, ok := caller.userinfo["email"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if _, ok := caller.admins[email.(string)]; ok {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (caller apiCaller) writeAccessableServices(w http.ResponseWriter) {
|
|
services, editable := caller.getAccessableServices()
|
|
for _, r := range editable {
|
|
w.Header().Add("MG-X-SERVICE-EDITABLE", r)
|
|
}
|
|
|
|
w.Write([]byte("{"))
|
|
start := true
|
|
for _, v := range services {
|
|
if !start {
|
|
w.Write([]byte(","))
|
|
}
|
|
w.Write([]byte(fmt.Sprintf(`"%s":`, v.ServiceName)))
|
|
serptr := atomic.LoadPointer(&v.serviceSerialized)
|
|
w.Write(*(*[]byte)(serptr))
|
|
start = false
|
|
}
|
|
w.Write([]byte("}"))
|
|
}
|
|
|
|
func (caller apiCaller) getAccessableServices() ([]*serviceDescription, []string) {
|
|
allservices := caller.mg.services.all()
|
|
|
|
admin := caller.isGlobalAdmin()
|
|
var email string
|
|
if !*noauth {
|
|
v, ok := caller.userinfo["email"]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
email = v.(string)
|
|
_, admin = caller.admins[email]
|
|
}
|
|
|
|
var output []*serviceDescription
|
|
var editable []string
|
|
for _, desc := range allservices {
|
|
if admin {
|
|
output = append(output, desc)
|
|
editable = append(editable, desc.ServiceName)
|
|
} else if desc.isValidAPIUser("*", email) {
|
|
output = append(output, desc)
|
|
if desc.isValidAPIUser("service", email) {
|
|
editable = append(editable, desc.ServiceName)
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(output, func(i, j int) bool {
|
|
return output[i].ServiceName < output[j].ServiceName
|
|
})
|
|
|
|
return output, editable
|
|
}
|
|
|
|
func (caller apiCaller) isValidUser(service any, category string) (valid bool, admin bool) {
|
|
if *noauth {
|
|
return true, true
|
|
}
|
|
|
|
v, ok := caller.userinfo["email"]
|
|
if !ok {
|
|
logger.Println("isVaidUser failed. email is missing :", caller.userinfo)
|
|
return false, false
|
|
}
|
|
|
|
email := v.(string)
|
|
if _, ok := caller.admins[email]; ok {
|
|
return true, true
|
|
}
|
|
|
|
svcdesc := caller.mg.services.get(service)
|
|
if svcdesc == nil {
|
|
logger.Println("isVaidUser failed. service is missing :", service)
|
|
return false, false
|
|
}
|
|
|
|
return svcdesc.isValidAPIUser(category, email), false
|
|
}
|
|
|
|
func (caller apiCaller) filesAPI(w http.ResponseWriter, r *http.Request) error {
|
|
serviceid := r.FormValue("service")
|
|
if len(serviceid) == 0 {
|
|
serviceid = "000000000000"
|
|
}
|
|
|
|
var files []fileDocumentDesc
|
|
err := caller.mg.mongoClient.FindAllAs(CollectionFile, bson.M{
|
|
"service": serviceid,
|
|
}, &files, options.Find().SetProjection(bson.M{
|
|
"contents": 0,
|
|
}))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
enc := json.NewEncoder(w)
|
|
return enc.Encode(files)
|
|
}
|
|
|
|
var seq = uint32(0)
|
|
|
|
func (caller apiCaller) uploadAPI(w http.ResponseWriter, r *http.Request) error {
|
|
serviceid := r.FormValue("service")
|
|
if len(serviceid) == 0 {
|
|
serviceid = "000000000000"
|
|
}
|
|
|
|
infile, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return err
|
|
}
|
|
defer infile.Close()
|
|
|
|
desc := r.FormValue("desc")
|
|
contents, _ := io.ReadAll(infile)
|
|
extractstr := r.FormValue("extract")
|
|
extract, _ := strconv.ParseBool(extractstr)
|
|
|
|
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[:])
|
|
|
|
var link string
|
|
if extract {
|
|
link = path.Join("static", serviceid, rf)
|
|
} else {
|
|
link = path.Join("static", serviceid, rf, header.Filename)
|
|
}
|
|
|
|
newdoc := fileDocumentDesc{
|
|
Contents: contents,
|
|
Src: header.Filename,
|
|
Timestamp: time.Now().UTC().Unix(),
|
|
Extract: extract,
|
|
Link: link,
|
|
Desc: desc,
|
|
Key: rf,
|
|
Service: serviceid,
|
|
}
|
|
_, _, err = caller.mg.mongoClient.UpsertOne(CollectionFile, bson.M{
|
|
"service": serviceid,
|
|
"key": rf,
|
|
}, newdoc)
|
|
|
|
if err == nil {
|
|
newdoc.Contents = nil
|
|
enc := json.NewEncoder(w)
|
|
enc.Encode(newdoc)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (caller apiCaller) whitelistAPI(w http.ResponseWriter, r *http.Request) error {
|
|
mg := caller.mg
|
|
queryvals := r.URL.Query()
|
|
if r.Method == "GET" {
|
|
service := queryvals.Get("service")
|
|
|
|
if valid, _ := caller.isValidUser(service, "whitelist"); !valid {
|
|
logger.Println("whitelistAPI failed. not vaild user :", r.Method, caller.userinfo)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
if len(service) > 0 {
|
|
|
|
all, err := mg.mongoClient.FindAll(CollectionWhitelist, bson.M{
|
|
"service": service,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(all) > 0 {
|
|
allraw, _ := json.Marshal(all)
|
|
w.Write(allraw)
|
|
}
|
|
} else {
|
|
logger.Println("service param is missing")
|
|
}
|
|
} else if r.Method == "PUT" {
|
|
body, _ := io.ReadAll(r.Body)
|
|
var member whitelistmember
|
|
if err := json.Unmarshal(body, &member); err != nil {
|
|
return err
|
|
}
|
|
if valid, _ := caller.isValidUser(member.Service, "whitelist"); !valid {
|
|
logger.Println("whitelistAPI failed. not vaild user :", r.Method, caller.userinfo)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
member.Expired = 0
|
|
|
|
_, _, err := mg.mongoClient.Update(CollectionWhitelist, bson.M{
|
|
"_id": primitive.NewObjectID(),
|
|
}, bson.M{
|
|
"$set": &member,
|
|
}, options.Update().SetUpsert(true))
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if r.Method == "DELETE" {
|
|
id := queryvals.Get("id")
|
|
if len(id) == 0 {
|
|
return errors.New("id param is missing")
|
|
}
|
|
idobj, err := primitive.ObjectIDFromHex(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, err = mg.mongoClient.Update(CollectionWhitelist, bson.M{
|
|
"_id": idobj,
|
|
}, bson.M{
|
|
"$currentDate": bson.M{
|
|
"_ts": bson.M{"$type": "date"},
|
|
},
|
|
}, options.Update().SetUpsert(false))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (caller apiCaller) serviceAPI(w http.ResponseWriter, r *http.Request) error {
|
|
mg := caller.mg
|
|
queryvals := r.URL.Query()
|
|
if r.Method == "GET" {
|
|
name := queryvals.Get("name")
|
|
if len(name) > 0 {
|
|
if valid, _ := caller.isValidUser(name, "*"); !valid {
|
|
logger.Println("serviceAPI failed. not vaild user :", r.Method, caller.userinfo)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
if valid, admin := caller.isValidUser(name, "service"); valid || admin {
|
|
w.Header().Add("MG-X-SERVICE-EDITABLE", name)
|
|
}
|
|
|
|
serptr := atomic.LoadPointer(&mg.services.get(name).serviceSerialized)
|
|
w.Write(*(*[]byte)(serptr))
|
|
} else {
|
|
caller.writeAccessableServices(w)
|
|
}
|
|
} else if r.Method == "POST" {
|
|
body, _ := io.ReadAll(r.Body)
|
|
var service serviceDescription
|
|
if err := json.Unmarshal(body, &service); err != nil {
|
|
return err
|
|
}
|
|
|
|
if service.Id.IsZero() {
|
|
if caller.isGlobalAdmin() {
|
|
service.Id = primitive.NewObjectID()
|
|
} else {
|
|
logger.Println("serviceAPI failed. not vaild user :", r.Method, caller.userinfo)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
} else if valid, _ := caller.isValidUser(service.Id, "service"); !valid {
|
|
logger.Println("serviceAPI failed. not vaild user :", r.Method, caller.userinfo)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
filter := bson.M{"_id": service.Id}
|
|
if len(service.ServiceCode) == 0 {
|
|
service.ServiceCode = hex.EncodeToString(service.Id[6:])
|
|
}
|
|
|
|
success, _, err := mg.mongoClient.Update(CollectionService, filter, bson.M{
|
|
"$set": &service,
|
|
}, options.Update().SetUpsert(true))
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !success {
|
|
logger.Println("serviceAPI failed. not vaild user :", caller.userinfo)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (caller apiCaller) accountAPI(w http.ResponseWriter, r *http.Request) error {
|
|
mg := caller.mg
|
|
queryvals := r.URL.Query()
|
|
if r.Method == "GET" {
|
|
service := queryvals.Get("service")
|
|
if len(service) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if valid, _ := caller.isValidUser(service, "account"); !valid {
|
|
logger.Println("accountAPI failed. not vaild user :", r.Method, caller.userinfo)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
var accdoc primitive.M
|
|
if v := queryvals.Get("accid"); len(v) == 0 {
|
|
email := queryvals.Get("email")
|
|
platform := queryvals.Get("platform")
|
|
if len(email) == 0 || len(platform) == 0 {
|
|
return nil
|
|
}
|
|
|
|
found, err := mg.mongoClient.FindOne(CollectionLink, bson.M{
|
|
"email": email,
|
|
"platform": platform,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if found == nil {
|
|
return nil
|
|
}
|
|
if idobj, ok := found["_id"]; ok {
|
|
svcdoc, err := mg.mongoClient.FindOne(common.CollectionName(service), bson.M{
|
|
"_id": idobj,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if svcdoc != nil {
|
|
found["accid"] = svcdoc["accid"]
|
|
}
|
|
|
|
accdoc = found
|
|
}
|
|
} else {
|
|
accid, err := primitive.ObjectIDFromHex(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
svcdoc, err := mg.mongoClient.FindOne(common.CollectionName(service), bson.M{
|
|
"accid": accid,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
found, err := mg.mongoClient.FindOne(CollectionLink, bson.M{
|
|
"_id": svcdoc["_id"],
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if found != nil {
|
|
found["accid"] = accid
|
|
}
|
|
accdoc = found
|
|
}
|
|
|
|
if accdoc != nil {
|
|
accdoc["code"] = service
|
|
delete(accdoc, "uid")
|
|
delete(accdoc, "_id")
|
|
|
|
var bi blockinfo
|
|
if err := mg.mongoClient.FindOneAs(CollectionBlock, bson.M{
|
|
"code": service,
|
|
"accid": accdoc["accid"],
|
|
}, &bi); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !bi.Start.Time().IsZero() && bi.End.Time().After(time.Now().UTC()) {
|
|
accdoc["blocked"] = bi
|
|
}
|
|
return json.NewEncoder(w).Encode(accdoc)
|
|
}
|
|
} else if r.Method == "POST" {
|
|
var account struct {
|
|
Code string
|
|
Accid string
|
|
Blocked blockinfo
|
|
}
|
|
|
|
body, _ := io.ReadAll(r.Body)
|
|
if err := json.Unmarshal(body, &account); err != nil {
|
|
return err
|
|
}
|
|
|
|
accid, _ := primitive.ObjectIDFromHex(account.Accid)
|
|
if !account.Blocked.Start.Time().IsZero() && account.Blocked.Start.Time().After(time.Now().UTC()) {
|
|
if _, _, err := mg.mongoClient.Update(CollectionBlock, bson.M{
|
|
"code": account.Code,
|
|
"accid": accid,
|
|
}, bson.M{
|
|
"$set": account.Blocked,
|
|
}, options.Update().SetUpsert(true)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var errApiTokenMissing = errors.New("mg-x-api-token is missing")
|
|
|
|
func (caller apiCaller) configAPI(w http.ResponseWriter, r *http.Request) error {
|
|
mg := caller.mg
|
|
apitoken := r.Header.Get("MG-X-API-TOKEN")
|
|
if len(apitoken) == 0 {
|
|
return errApiTokenMissing
|
|
}
|
|
|
|
if _, exists := mg.apiTokenToService.get(apitoken); !exists {
|
|
return fmt.Errorf("mg-x-api-token is not valid : %s", apitoken)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var noauth = flag.Bool("noauth", false, "")
|
|
|
|
type apiCaller struct {
|
|
userinfo map[string]any
|
|
admins map[string]bool
|
|
mg *Maingate
|
|
}
|
|
|
|
func (mg *Maingate) api(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()
|
|
}()
|
|
|
|
var userinfo map[string]any
|
|
|
|
if !*noauth {
|
|
authheader := r.Header.Get("Authorization")
|
|
if len(authheader) == 0 {
|
|
logger.Println("Authorization header is not valid :", authheader)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
req, _ := http.NewRequest("GET", "https://graph.microsoft.com/oidc/userinfo", nil)
|
|
req.Header.Add("Authorization", authheader)
|
|
client := &http.Client{}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
logger.Println("graph microsoft api call failed :", err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
if err = json.Unmarshal(raw, &userinfo); err != nil {
|
|
return
|
|
}
|
|
|
|
if _, expired := userinfo["error"]; expired {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
|
|
ptr := atomic.LoadPointer(&mg.admins)
|
|
adminsptr := (*globalAdmins)(ptr)
|
|
|
|
if adminsptr.modtime != common.ConfigModTime() {
|
|
var config globalAdmins
|
|
if err := common.LoadConfig(&config); err == nil {
|
|
config.parse()
|
|
adminsptr = &config
|
|
atomic.StorePointer(&mg.admins, unsafe.Pointer(adminsptr))
|
|
}
|
|
}
|
|
|
|
logger.Println("api call :", r.URL.Path, r.Method, r.URL.Query(), userinfo)
|
|
caller := apiCaller{
|
|
userinfo: userinfo,
|
|
admins: adminsptr.emails,
|
|
mg: mg,
|
|
}
|
|
|
|
var err error
|
|
if strings.HasSuffix(r.URL.Path, "/service") {
|
|
err = caller.serviceAPI(w, r)
|
|
} else if strings.HasSuffix(r.URL.Path, "/whitelist") {
|
|
err = caller.whitelistAPI(w, r)
|
|
} else if strings.HasSuffix(r.URL.Path, "/config") {
|
|
err = caller.configAPI(w, r)
|
|
} else if strings.HasSuffix(r.URL.Path, "/account") {
|
|
err = caller.accountAPI(w, r)
|
|
} else if strings.HasSuffix(r.URL.Path, "/upload") {
|
|
err = caller.uploadAPI(w, r)
|
|
} else if strings.HasSuffix(r.URL.Path, "/files") {
|
|
err = caller.filesAPI(w, r)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Error(err)
|
|
}
|
|
}
|