package core import ( "encoding/json" "errors" "flag" "fmt" "io" "net/http" "sort" "strings" "sync/atomic" "time" "unsafe" "repositories.action2quare.com/ayo/go-ayo/common" "repositories.action2quare.com/ayo/go-ayo/logger" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo/options" ) 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() 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) 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} 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) } if err != nil { logger.Error(err) } }