package core import ( "bytes" "crypto/md5" "encoding/binary" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "os" "path" "strconv" "strings" "sync/atomic" "time" "unsafe" common "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/logger" "repositories.action2quare.com/ayo/maingate/flag" "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 `bson:"service" json:"service"` Key string `bson:"key" json:"key"` Src string `bson:"src" json:"src"` Link string `bson:"link" json:"link"` Desc string `bson:"desc" json:"desc"` Extract bool `bson:"extract" json:"extract"` Timestamp int64 `bson:"timestamp" json:"timestamp"` Contents []byte `bson:"contents,omitempty" json:"contents,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) isAdmin() bool { if *flag.Noauth { return true } v, ok := caller.userinfo["email"] if !ok { logger.Println("isVaidUser failed. email is missing :", caller.userinfo) return false } email := v.(string) if _, ok := caller.globalAdmins[email]; ok { return true } return caller.mg.service().isAdmin(email) } func (caller apiCaller) isAdminOrValidToken() bool { if caller.isAdmin() { return true } return caller.mg.service().isValidToken(caller.apiToken) } func (caller apiCaller) filesAPI(w http.ResponseWriter, r *http.Request) error { if r.Method == "GET" { if !caller.isAdminOrValidToken() { w.WriteHeader(http.StatusUnauthorized) return nil } allfiles, err := caller.mg.mongoClient.All(CollectionFile, options.Find().SetProjection(bson.M{ "contents": 0, }).SetReturnKey(false)) if err != nil { return err } if len(allfiles) > 0 { enc := json.NewEncoder(w) return enc.Encode(allfiles) } } else if r.Method == "DELETE" { key := r.FormValue("key") if len(key) == 0 { w.WriteHeader(http.StatusBadRequest) return nil } if !caller.isAdminOrValidToken() { w.WriteHeader(http.StatusUnauthorized) return nil } _, err := caller.mg.mongoClient.Delete(CollectionFile, bson.M{ "key": key, }) if err != nil { return err } } return nil } 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)) subfolder := hex.EncodeToString(hasher.Sum(nil))[:8] 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[1:]) newidstr := subfolder + rf newidbt, _ := hex.DecodeString(newidstr) newidobj := primitive.NewObjectID() copy(newidobj[:], newidbt[:8]) var link string if extract { link = path.Join("static", subfolder, rf) } else { link = path.Join("static", subfolder, rf, header.Filename) } newdoc := fileDocumentDesc{ Contents: contents, Src: header.Filename, Timestamp: time.Now().UTC().Unix(), Extract: extract, Link: link, Desc: desc, Key: rf, Service: servicename, } _, _, err = caller.mg.mongoClient.UpsertOne(CollectionFile, bson.M{ "_id": newidobj, "service": servicename, "key": rf, }, newdoc) if err == nil { newdoc.Contents = nil enc := json.NewEncoder(w) enc.Encode(newdoc) } return err } return nil } 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 len(service) > 0 { if !caller.isAdminOrValidToken() { logger.Println("whitelistAPI failed. not vaild user :", r.Method, caller.userinfo) w.WriteHeader(http.StatusUnauthorized) return nil } all, err := mg.mongoClient.FindAll(CollectionWhitelist, bson.M{ "service": service, }) 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) } } 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 !caller.isAdminOrValidToken() { logger.Println("whitelistAPI failed. not vaild user :", r.Method, caller.userinfo) w.WriteHeader(http.StatusUnauthorized) 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 if r.Method == "GET" { if mg.service().Id.IsZero() { newService := serviceDescription{ ServiceDescriptionSummary: ServiceDescriptionSummary{ Id: primitive.NewObjectID(), }, } if err := newService.prepare(caller.mg); err != nil { return err } atomic.StorePointer(&mg.serviceptr, unsafe.Pointer(&newService)) } serptr := atomic.LoadPointer(&mg.service().serviceSerialized) w.Write(*(*[]byte)(serptr)) } else if r.Method == "POST" { body, _ := io.ReadAll(r.Body) var service serviceDescription if err := json.Unmarshal(body, &service); err != nil { return err } filter := bson.M{"_id": service.Id} 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) maintenanceAPI(w http.ResponseWriter, r *http.Request) error { mg := caller.mg if r.Method == "GET" { serptr := atomic.LoadPointer(&mg.service().divisionsSerialized) w.Write(*(*[]byte)(serptr)) } else if r.Method == "POST" { var divs map[string]*Division dec := json.NewDecoder(r.Body) if err := dec.Decode(&divs); err != nil { w.WriteHeader(http.StatusBadRequest) return err } _, _, err := mg.mongoClient.Update(CollectionService, bson.M{ "_Id": mg.service().Id, }, bson.M{ "$set": bson.M{"divisions": divs}, }, options.Update().SetUpsert(false)) if err != nil { w.WriteHeader(http.StatusInternalServerError) return err } } 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 !caller.isAdminOrValidToken() { logger.Println("accountAPI failed. not vaild user :", r.Method, caller.userinfo) w.WriteHeader(http.StatusUnauthorized) 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 } apitokenObj, _ := primitive.ObjectIDFromHex(apitoken) if !mg.service().isValidToken(apitokenObj) { return fmt.Errorf("mg-x-api-token is not valid : %s", apitoken) } return nil } type apiCaller struct { userinfo map[string]any globalAdmins map[string]bool mg *Maingate apiToken primitive.ObjectID } 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 !*flag.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)) } } apiToken := r.Header.Get("MG-X-API-TOKEN") var apiTokenObj primitive.ObjectID if len(apiToken) > 0 { obj, err := primitive.ObjectIDFromHex(apiToken) if err != nil { logger.Error(err) w.WriteHeader(http.StatusBadRequest) return } apiTokenObj = obj } logger.Println("api call :", r.URL.Path, r.Method, r.URL.Query(), userinfo) caller := apiCaller{ userinfo: userinfo, globalAdmins: adminsptr.emails, mg: mg, apiToken: apiTokenObj, } 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, "/maintenance") { err = caller.maintenanceAPI(w, r) } else if strings.HasSuffix(r.URL.Path, "/files") { err = caller.filesAPI(w, r) } if err != nil { logger.Error(err) } }