// Файл: internal/api/http.go // Назначение: HTTP RESTful API для взаимодействия с СУБД через curl. // Поддерживает CRUD операции, управление индексами, ACL и ограничениями. // Реализован с минимальными блокировками, использует wait-free структуры. package api import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "futriis/internal/acl" "futriis/internal/cluster" "futriis/internal/log" "futriis/internal/storage" ) type HTTPServer struct { store *storage.Storage coordinator *cluster.RaftCoordinator aclManager *acl.ACLManager logger *log.Logger server *http.Server port int } type APIResponse struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } // NewHTTPServer создаёт новый HTTP сервер func NewHTTPServer(port int, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger) *HTTPServer { s := &HTTPServer{ store: store, coordinator: coord, aclManager: aclMgr, logger: logger, port: port, } mux := http.NewServeMux() // Middleware для аутентификации mux.HandleFunc("/api/auth/login", s.handleLogin) mux.HandleFunc("/api/auth/logout", s.handleLogout) // CRUD операции mux.HandleFunc("/api/db/", s.handleDatabaseRequest) // Индексы mux.HandleFunc("/api/index/", s.handleIndexRequest) // ACL mux.HandleFunc("/api/acl/", s.handleACLRequest) // Constraints mux.HandleFunc("/api/constraint/", s.handleConstraintRequest) // Cluster mux.HandleFunc("/api/cluster/", s.handleClusterRequest) s.server = &http.Server{ Addr: fmt.Sprintf(":%d", port), Handler: mux, } return s } // Start запускает HTTP сервер func (s *HTTPServer) Start() error { s.logger.Info("Starting HTTP API server on port " + strconv.Itoa(s.port)) return s.server.ListenAndServe() } // Stop останавливает HTTP сервер func (s *HTTPServer) Stop() error { return s.server.Close() } // handleLogin обрабатывает аутентификацию func (s *HTTPServer) handleLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) return } var creds struct { Username string `json:"username"` Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&creds); err != nil { s.sendError(w, "Invalid request body", http.StatusBadRequest) return } sessionID, err := s.aclManager.Authenticate(creds.Username, creds.Password) if err != nil { s.sendError(w, err.Error(), http.StatusUnauthorized) return } s.sendSuccess(w, map[string]string{"session_id": sessionID}) } // handleLogout обрабатывает выход func (s *HTTPServer) handleLogout(w http.ResponseWriter, r *http.Request) { sessionID := r.Header.Get("X-Session-ID") if sessionID != "" { s.aclManager.Logout(sessionID) } s.sendSuccess(w, map[string]string{"status": "logged out"}) } // handleDatabaseRequest обрабатывает запросы к БД func (s *HTTPServer) handleDatabaseRequest(w http.ResponseWriter, r *http.Request) { // URL: /api/db/{database}/{collection}/{document_id} path := strings.TrimPrefix(r.URL.Path, "/api/db/") parts := strings.Split(path, "/") if len(parts) < 2 { s.sendError(w, "Invalid path. Use /api/db/{database}/{collection}[/{id}]", http.StatusBadRequest) return } database := parts[0] collection := parts[1] docID := "" if len(parts) > 2 { docID = parts[2] } // Проверка аутентификации sessionID := r.Header.Get("X-Session-ID") if sessionID == "" { s.sendError(w, "Authentication required", http.StatusUnauthorized) return } switch r.Method { case http.MethodGet: s.handleGetDocument(w, r, sessionID, database, collection, docID) case http.MethodPost: s.handleInsertDocument(w, r, sessionID, database, collection) case http.MethodPut: s.handleUpdateDocument(w, r, sessionID, database, collection, docID) case http.MethodDelete: s.handleDeleteDocument(w, r, sessionID, database, collection, docID) default: s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) } } // handleGetDocument обрабатывает GET запросы func (s *HTTPServer) handleGetDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) { // Проверка прав if !s.aclManager.CheckPermission(sessionID, database, collection, "read") { s.sendError(w, "Access denied", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } // Поиск по индексу или ID query := r.URL.Query() if indexName := query.Get("index"); indexName != "" { indexValue := query.Get("value") docs, err := coll.FindByIndex(indexName, indexValue) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } s.sendSuccess(w, docs) return } if docID == "" { // Возвращаем все документы docs := coll.GetAllDocuments() s.sendSuccess(w, docs) return } doc, err := coll.Find(docID) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } s.sendSuccess(w, doc) } // handleInsertDocument обрабатывает POST запросы func (s *HTTPServer) handleInsertDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection string) { if !s.aclManager.CheckPermission(sessionID, database, collection, "write") { s.sendError(w, "Access denied", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { // Создаём БД если не существует if err := s.store.CreateDatabase(database); err != nil { s.sendError(w, err.Error(), http.StatusInternalServerError) return } db, _ = s.store.GetDatabase(database) } coll, err := db.GetCollection(collection) if err != nil { if err := db.CreateCollection(collection); err != nil { s.sendError(w, err.Error(), http.StatusInternalServerError) return } coll, _ = db.GetCollection(collection) } var doc map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&doc); err != nil { s.sendError(w, "Invalid JSON", http.StatusBadRequest) return } if err := coll.InsertFromMap(doc); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } s.sendSuccess(w, map[string]string{"status": "inserted"}) } // handleUpdateDocument обрабатывает PUT запросы func (s *HTTPServer) handleUpdateDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) { if docID == "" { s.sendError(w, "Document ID required", http.StatusBadRequest) return } if !s.aclManager.CheckPermission(sessionID, database, collection, "write") { s.sendError(w, "Access denied", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } var updates map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { s.sendError(w, "Invalid JSON", http.StatusBadRequest) return } if err := coll.Update(docID, updates); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } s.sendSuccess(w, map[string]string{"status": "updated"}) } // handleDeleteDocument обрабатывает DELETE запросы func (s *HTTPServer) handleDeleteDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) { if docID == "" { s.sendError(w, "Document ID required", http.StatusBadRequest) return } if !s.aclManager.CheckPermission(sessionID, database, collection, "delete") { s.sendError(w, "Access denied", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } if err := coll.Delete(docID); err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } s.sendSuccess(w, map[string]string{"status": "deleted"}) } // handleIndexRequest обрабатывает запросы к индексам func (s *HTTPServer) handleIndexRequest(w http.ResponseWriter, r *http.Request) { // URL: /api/index/{database}/{collection}/{action} path := strings.TrimPrefix(r.URL.Path, "/api/index/") parts := strings.Split(path, "/") if len(parts) < 3 { s.sendError(w, "Invalid path. Use /api/index/{database}/{collection}/{action}", http.StatusBadRequest) return } database := parts[0] collection := parts[1] action := parts[2] sessionID := r.Header.Get("X-Session-ID") if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") { s.sendError(w, "Admin access required", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } switch action { case "list": indexes := coll.GetIndexes() s.sendSuccess(w, indexes) case "create": var req struct { Name string `json:"name"` Fields []string `json:"fields"` Unique bool `json:"unique"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendError(w, "Invalid request body", http.StatusBadRequest) return } if err := coll.CreateIndex(req.Name, req.Fields, req.Unique); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } s.sendSuccess(w, map[string]string{"status": "index created"}) case "drop": indexName := parts[3] if err := coll.DropIndex(indexName); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } s.sendSuccess(w, map[string]string{"status": "index dropped"}) default: s.sendError(w, "Unknown action", http.StatusBadRequest) } } // handleACLRequest обрабатывает запросы ACL func (s *HTTPServer) handleACLRequest(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/acl/") parts := strings.Split(path, "/") if len(parts) < 1 { s.sendError(w, "Invalid path", http.StatusBadRequest) return } sessionID := r.Header.Get("X-Session-ID") if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") { s.sendError(w, "Admin access required", http.StatusForbidden) return } action := parts[0] switch action { case "users": users := s.aclManager.ListUsers() s.sendSuccess(w, users) case "user": if len(parts) < 2 { s.sendError(w, "Username required", http.StatusBadRequest) return } username := parts[1] switch r.Method { case http.MethodPost: var req struct { Password string `json:"password"` Roles []string `json:"roles"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendError(w, "Invalid request body", http.StatusBadRequest) return } if err := s.aclManager.CreateUser(username, req.Password, req.Roles); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } s.sendSuccess(w, map[string]string{"status": "user created"}) default: s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) } case "roles": roles := s.aclManager.ListRoles() s.sendSuccess(w, roles) case "grant": if len(parts) < 3 { s.sendError(w, "Role and permission required", http.StatusBadRequest) return } roleName := parts[1] permission := parts[2] if err := s.aclManager.GrantPermission(roleName, permission); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } s.sendSuccess(w, map[string]string{"status": "permission granted"}) default: s.sendError(w, "Unknown action", http.StatusBadRequest) } } // handleConstraintRequest обрабатывает запросы к ограничениям func (s *HTTPServer) handleConstraintRequest(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/constraint/") parts := strings.Split(path, "/") if len(parts) < 3 { s.sendError(w, "Invalid path. Use /api/constraint/{database}/{collection}/{action}", http.StatusBadRequest) return } database := parts[0] collection := parts[1] action := parts[2] sessionID := r.Header.Get("X-Session-ID") if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") { s.sendError(w, "Admin access required", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } switch action { case "required": if len(parts) < 4 { s.sendError(w, "Field name required", http.StatusBadRequest) return } field := parts[3] coll.AddRequiredField(field) s.sendSuccess(w, map[string]string{"status": "required field added"}) case "unique": if len(parts) < 4 { s.sendError(w, "Field name required", http.StatusBadRequest) return } field := parts[3] coll.AddUniqueConstraint(field) s.sendSuccess(w, map[string]string{"status": "unique constraint added"}) case "min": if len(parts) < 5 { s.sendError(w, "Field name and value required", http.StatusBadRequest) return } field := parts[3] minVal, _ := strconv.ParseFloat(parts[4], 64) coll.AddMinConstraint(field, minVal) s.sendSuccess(w, map[string]string{"status": "min constraint added"}) case "max": if len(parts) < 5 { s.sendError(w, "Field name and value required", http.StatusBadRequest) return } field := parts[3] maxVal, _ := strconv.ParseFloat(parts[4], 64) coll.AddMaxConstraint(field, maxVal) s.sendSuccess(w, map[string]string{"status": "max constraint added"}) default: s.sendError(w, "Unknown action", http.StatusBadRequest) } } // handleClusterRequest обрабатывает запросы к кластеру func (s *HTTPServer) handleClusterRequest(w http.ResponseWriter, r *http.Request) { sessionID := r.Header.Get("X-Session-ID") if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") { s.sendError(w, "Admin access required", http.StatusForbidden) return } if s.coordinator == nil { s.sendError(w, "Cluster not available", http.StatusServiceUnavailable) return } status := s.coordinator.GetClusterStatus() s.sendSuccess(w, status) } // sendSuccess отправляет успешный ответ func (s *HTTPServer) sendSuccess(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(APIResponse{ Success: true, Data: data, }) } // sendError отправляет ответ с ошибкой func (s *HTTPServer) sendError(w http.ResponseWriter, errMsg string, statusCode int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(APIResponse{ Success: false, Error: errMsg, }) }