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" "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 { 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 = gocommon.Unzip(destFile) case ".tar": err = gocommon.Untar(destFile) } } return err } func (caller apiCaller) filesAPI(w http.ResponseWriter, r *http.Request) error { if r.Method == "GET" { 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 } _, 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" { hasher := md5.New() hasher.Write(caller.mg.service().serviceCodeBytes) 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, } _, _, err = caller.mg.mongoClient.UpsertOne(CollectionFile, bson.M{ "_id": newidobj, "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 if r.Method == "GET" { // if !caller.isAdminOrValidToken() { // logger.Println("whitelistAPI failed. not vaild user :", r.Method, caller.userinfo) // w.WriteHeader(http.StatusUnauthorized) // return nil // } all, err := mg.mongoClient.All(CollectionWhitelist) 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 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 := r.URL.Query().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)) } w.Write(mg.service().divisionsSerialized) } else if r.Method == "POST" { body, _ := io.ReadAll(r.Body) var service serviceDescription if err := json.Unmarshal(body, &service); err != nil { return err } if len(service.ServerApiTokens) == 0 { service.ServerApiTokens = []primitive.ObjectID{ primitive.NewObjectIDFromTimestamp(time.Now().Add(-time.Hour * 24 * 30 * 465)), } } 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" { w.Write(mg.service().divisionsSerialized) } 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 } var errApiTokenMissing = errors.New("mg-x-api-token is missing") func (caller apiCaller) configAPI(w http.ResponseWriter, r *http.Request) error { mg := caller.mg if !*devflag { 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 !*devflag { 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 != gocommon.ConfigModTime() { var config globalAdmins if err := gocommon.LoadConfig(&config); err == nil { config.parse() adminsptr = &config atomic.StorePointer(&mg.admins, unsafe.Pointer(adminsptr)) } } var apiTokenObj primitive.ObjectID if !*devflag { apiToken := r.Header.Get("MG-X-API-TOKEN") 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, "/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) } }