Files
maingate/core/api.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)
}
}