package web import ( "bufio" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "go.buzzert.net/kordophone-mock/model" "go.buzzert.net/kordophone-mock/server" "github.com/google/uuid" "github.com/rs/zerolog/log" "github.com/gorilla/websocket" ) type MockHTTPServerConfiguration struct { AuthEnabled bool } type MockHTTPServer struct { Server server.Server mux http.ServeMux authEnabled bool } type AuthError struct { message string } func (e *AuthError) Error() string { return e.message } func (m *MockHTTPServer) logRequest(r *http.Request, extras ...string) { log.Debug().Msgf("%s %s %s", r.Method, r.URL.Path, strings.Join(extras, " ")) } func (m *MockHTTPServer) checkAuthentication(r *http.Request) error { if !m.authEnabled { return nil } // Check for Authorization header authHeader := r.Header.Get("Authorization") if authHeader == "" { return &AuthError{"Missing Authorization header"} } // Check for "Bearer" prefix if authHeader[:7] != "Bearer " { return &AuthError{"Invalid Authorization header"} } // Check for valid token token := authHeader[7:] if !m.Server.CheckBearerToken(token) { return &AuthError{"Invalid token"} } return nil } func (m *MockHTTPServer) requireAuthentication(w http.ResponseWriter, r *http.Request) bool { if !m.authEnabled { return true } if err := m.checkAuthentication(r); err != nil { log.Error().Err(err).Msg("Error checking authentication") http.Error(w, err.Error(), http.StatusUnauthorized) return false } return true } func (m *MockHTTPServer) handleVersion(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%s", m.Server.Version()) } func (m *MockHTTPServer) handleStatus(w http.ResponseWriter, r *http.Request) { if !m.requireAuthentication(w, r) { return } fmt.Fprintf(w, "OK") } func (m *MockHTTPServer) handleConversations(w http.ResponseWriter, r *http.Request) { if !m.requireAuthentication(w, r) { return } convos := m.Server.Conversations() // Encode convos as JSON jsonData, err := json.Marshal(convos) if err != nil { log.Error().Err(err).Msg("Error marshalling conversations") http.Error(w, err.Error(), http.StatusInternalServerError) return } // Write JSON to response w.Header().Set("Content-Type", "application/json") w.Write(jsonData) } func (m *MockHTTPServer) handleMessages(w http.ResponseWriter, r *http.Request) { if !m.requireAuthentication(w, r) { return } guid := r.URL.Query().Get("guid") if len(guid) == 0 { log.Error().Msg("handleMessage: Got empty guid parameter") http.Error(w, "no guid parameter specified", http.StatusBadRequest) return } conversation, err := m.Server.ConversationForGUID(guid) if err != nil { log.Error().Err(err).Msgf("handleMessage: Error getting conversation (%s)", guid) http.Error(w, "conversation not found", http.StatusBadRequest) return } beforeDate := r.URL.Query().Get("beforeDate") beforeGUID := r.URL.Query().Get("beforeMessageGUID") afterGUID := r.URL.Query().Get("afterMessageGUID") limit := r.URL.Query().Get("limit") stringOrNil := func(s string) *string { if len(s) == 0 { return nil } return &s } dateOrNil := func(s string) *model.Date { if len(s) == 0 { return nil } t, _ := time.Parse(time.RFC3339, s) date := model.Date(t) return &date } intOrNil := func(s string) *int { if len(s) == 0 { return nil } i, _ := strconv.Atoi(s) return &i } query := server.MessagesQuery{ Limit: intOrNil(limit), BeforeDate: dateOrNil(beforeDate), BeforeGUID: stringOrNil(beforeGUID), AfterGUID: stringOrNil(afterGUID), ConversationGUID: conversation.Guid, } messages := m.Server.PerformMessageQuery(&query) jsonData, err := json.Marshal(messages) if err != nil { log.Error().Err(err).Msg("Error marshalling messages") http.Error(w, err.Error(), http.StatusInternalServerError) return } // Write JSON to response w.Header().Set("Content-Type", "application/json") w.Write(jsonData) } func (m *MockHTTPServer) handleAuthenticate(w http.ResponseWriter, r *http.Request) { // Decode request body as AuthenticationRequest var authReq AuthenticationRequest err := json.NewDecoder(r.Body).Decode(&authReq) if err != nil { log.Error().Err(err).Msg("Authenticate: Error decoding request body") http.Error(w, err.Error(), http.StatusBadRequest) return } // Authenticate token, err := m.Server.Authenticate(authReq.Username, authReq.Password) if err != nil { log.Error().Err(err).Msg("Authenticate: Error authenticating") http.Error(w, err.Error(), http.StatusUnauthorized) return } // Write response w.Header().Set("Content-Type", "application/json") // Encode token as JSON jsonData, err := json.Marshal(token) if err != nil { log.Error().Err(err).Msg("Error marshalling token") http.Error(w, err.Error(), http.StatusInternalServerError) return } // Write JSON to response w.Write(jsonData) } func (m *MockHTTPServer) handleNotFound(w http.ResponseWriter, r *http.Request) { log.Error().Msgf("Unimplemented API endpoint: %s %s", r.Method, r.URL.Path) http.NotFound(w, r) } func (m *MockHTTPServer) handleSendMessage(w http.ResponseWriter, r *http.Request) { if !m.requireAuthentication(w, r) { return } // Decode request body as SendMessageRequest var sendMessageReq SendMessageRequest err := json.NewDecoder(r.Body).Decode(&sendMessageReq) if err != nil { log.Error().Err(err).Msg("SendMessage: Error decoding request body") http.Error(w, err.Error(), http.StatusBadRequest) return } // Find conversation conversation, err := m.Server.ConversationForGUID(sendMessageReq.ConversationGUID) if err != nil { log.Error().Err(err).Msgf("SendMessage: Error finding conversation (%s)", sendMessageReq.ConversationGUID) http.Error(w, err.Error(), http.StatusBadRequest) return } // Create Message message := model.Message{ Guid: uuid.New().String(), Text: sendMessageReq.Body, Date: model.Date(time.Now()), AttachmentGUIDs: sendMessageReq.TransferGUIDs, Sender: nil, // me } // Send message m.Server.SendMessage(conversation, message) // Encode response as JSON jsonData, err := json.Marshal(message) if err != nil { log.Error().Err(err).Msg("Error marshalling response") http.Error(w, err.Error(), http.StatusInternalServerError) return } // Write JSON to response w.Header().Set("Content-Type", "application/json") w.Write(jsonData) } func (m *MockHTTPServer) handlePollUpdates(w http.ResponseWriter, r *http.Request) { if !m.requireAuthentication(w, r) { return } // TODO: This should block if we don't have updates for that seq yet. seq := -1 seqString := r.URL.Query().Get("seq") if len(seqString) > 0 { var err error seq, err = strconv.Atoi(seqString) if err != nil { log.Error().Err(err).Msg("FetchUpdates: Error parsing seq") http.Error(w, err.Error(), http.StatusBadRequest) return } } // Fetch updates (blocking) updates := m.Server.FetchUpdatesBlocking(seq) if len(updates) == 0 { // return 205 (Nothing to report) w.WriteHeader(http.StatusResetContent) log.Info().Msg("FetchUpdates: Nothing to report") } else { // Encode updates as JSON jsonData, err := json.Marshal(updates) if err != nil { log.Error().Err(err).Msg("Error marshalling updates") http.Error(w, err.Error(), http.StatusInternalServerError) return } // Write JSON to response w.Header().Set("Content-Type", "application/json") w.Write(jsonData) } } func (m *MockHTTPServer) handleMarkConversation(w http.ResponseWriter, r *http.Request) { if !m.requireAuthentication(w, r) { return } guid := r.URL.Query().Get("guid") if len(guid) == 0 { log.Error().Msg("handleMarkConversation: Got empty guid parameter") http.Error(w, "no guid parameter specified", http.StatusBadRequest) return } // Find conversation convo, err := m.Server.ConversationForGUID(guid) if err != nil { log.Error().Err(err).Msgf("handleMarkConversation: Error finding conversation (%s)", guid) http.Error(w, err.Error(), http.StatusBadRequest) return } // Mark conversation m.Server.MarkConversationAsRead(convo) // Respond 200 w.WriteHeader(http.StatusOK) } func (m *MockHTTPServer) handleFetchAttachment(w http.ResponseWriter, r *http.Request) { if !m.requireAuthentication(w, r) { return } guid := r.URL.Query().Get("guid") if guid == "" { log.Error().Msg("fetchAttachment: Missing 'guid' parameter") http.Error(w, "no guid parameter specified", http.StatusBadRequest) return } reader, err := m.Server.FetchAttachment(guid) if err != nil { log.Error().Msgf("fetchAttachment: Could not load attachment from store: %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } dw := bufio.NewWriter(w) _, err = dw.ReadFrom(reader) if err != nil { log.Error().Msgf("fetchAttachment: Error reading attachment data: %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } } func (m *MockHTTPServer) handleUploadAttachment(w http.ResponseWriter, r *http.Request) { if !m.requireAuthentication(w, r) { return } filename := r.URL.Query().Get("filename") if filename == "" { log.Error().Msgf("uploadAttachment: filename not provided") http.Error(w, "filename not provided", http.StatusBadRequest) return } guid, err := m.Server.UploadAttachment(filename, r.Body) if err != nil { log.Error().Msgf("uploadAttachment: error storing attachment: %s", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } response := UploadAttachmentResponse{ TransferGUID: *guid, } jsonData, err := json.Marshal(response) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(jsonData) } func (m *MockHTTPServer) handleUpdates(w http.ResponseWriter, r *http.Request) { if !m.requireAuthentication(w, r) { return } upgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } c, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Error().Err(err).Msg("websocket upgrade failed") return } m.handleUpdatesWebsocket(c) } func (m *MockHTTPServer) handleUpdatesWebsocket(c *websocket.Conn) { // Fetch updates continuously defer c.Close() // Start a goroutine to handle incoming messages (pings, usually) go func() { for { _, _, err := c.ReadMessage() if err != nil { log.Error().Err(err).Msg("WebSocket read error") return } } }() for { // Fetch updates (blocking) updates := m.Server.FetchUpdatesBlocking(-1) // Send updates to client err := c.WriteJSON(updates) if err != nil { log.Error().Err(err).Msg("handleUpdatesWebsocket: Error sending updates to client (probably disconnected)") return } } } func (m *MockHTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { m.logRequest(r, r.URL.Query().Encode()) m.mux.ServeHTTP(w, r) } func NewMockHTTPServer(config MockHTTPServerConfiguration) *MockHTTPServer { this := MockHTTPServer{ Server: *server.NewServer(), mux: *http.NewServeMux(), authEnabled: config.AuthEnabled, } // Redirect /api/* to /* this.mux.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, r.URL.Path, http.StatusMovedPermanently) }))) this.mux.Handle("/version", http.HandlerFunc(this.handleVersion)) this.mux.Handle("/conversations", http.HandlerFunc(this.handleConversations)) this.mux.Handle("/status", http.HandlerFunc(this.handleStatus)) this.mux.Handle("/authenticate", http.HandlerFunc(this.handleAuthenticate)) this.mux.Handle("/messages", http.HandlerFunc(this.handleMessages)) this.mux.Handle("/pollUpdates", http.HandlerFunc(this.handlePollUpdates)) this.mux.Handle("/sendMessage", http.HandlerFunc(this.handleSendMessage)) this.mux.Handle("/markConversation", http.HandlerFunc(this.handleMarkConversation)) this.mux.Handle("/attachment", http.HandlerFunc(this.handleFetchAttachment)) this.mux.Handle("/uploadAttachment", http.HandlerFunc(this.handleUploadAttachment)) this.mux.Handle("/updates", http.HandlerFunc(this.handleUpdates)) this.mux.Handle("/", http.HandlerFunc(this.handleNotFound)) return &this }