package core import ( "bytes" "crypto/md5" "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 { if r.Method == "GET" { hasAuth := caller.isGlobalAdmin() var email string if !*noauth { v, ok := caller.userinfo["email"] if !ok { return nil } email = v.(string) _, hasAuth = caller.admins[email] } servicename := r.FormValue("service") sh := caller.mg.services.get(servicename) if sh == nil { w.WriteHeader(http.StatusBadRequest) return nil } if !hasAuth { if hasAuth = sh.isValidAPIUser("maintenance", email); !hasAuth { hasAuth = sh.isValidAPIUser("service", email) } } if !hasAuth { w.WriteHeader(http.StatusBadRequest) return nil } var files []fileDocumentDesc err := caller.mg.mongoClient.FindAllAs(CollectionFile, bson.M{ "service": servicename, }, &files, options.Find().SetProjection(bson.M{ "contents": 0, })) if err != nil { return err } if len(files) > 0 { enc := json.NewEncoder(w) return enc.Encode(files) } } else if r.Method == "DELETE" { servicename := r.FormValue("service") key := r.FormValue("key") if len(servicename) == 0 || len(key) == 0 { w.WriteHeader(http.StatusBadRequest) return nil } _, err := caller.mg.mongoClient.Delete(CollectionFile, bson.M{ "service": servicename, "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 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) maintenanceAPI(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 { w.WriteHeader(http.StatusBadRequest) return nil } if valid, admin := caller.isValidUser(name, "maintenance"); valid || admin { w.Header().Add("MG-X-SERVICE-EDITABLE", name) } serptr := atomic.LoadPointer(&mg.services.get(name).divisionsSerialized) w.Write(*(*[]byte)(serptr)) } else { caller.writeAccessableServices(w) } } else if r.Method == "POST" { servicename := r.FormValue("name") 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{ "service": servicename, }, 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 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, "/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) } }