package web import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "code.severnaya.net/kordophone-mock/v2/model" "code.severnaya.net/kordophone-mock/v2/server" "github.com/google/uuid" "github.com/rs/zerolog/log" "golang.org/x/net/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) 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 err := m.checkAuthentication(r); err != nil { log.Error().Err(err).Msg("Status: Error checking authentication") http.Error(w, err.Error(), http.StatusUnauthorized) return } fmt.Fprintf(w, "OK") } func (m *MockHTTPServer) handleConversations(w http.ResponseWriter, r *http.Request) { 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) { 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) *time.Time { if len(s) == 0 { return nil } t, _ := time.Parse(time.RFC3339, s) return &t } 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) { // 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: time.Now(), Sender: nil, // me } // Send message m.Server.SendMessage(conversation, message) w.WriteHeader(http.StatusOK) } func (m *MockHTTPServer) handlePollUpdates(w http.ResponseWriter, r *http.Request) { // 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) { 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) handleUpdatesWebsocket(c *websocket.Conn) { // Fetch updates continuously defer c.Close() for { // Fetch updates (blocking) updates := m.Server.FetchUpdatesBlocking(-1) // Send updates to client err := websocket.JSON.Send(c, 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, } 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)) // /updates websocket this.mux.Handle("/updates", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s := websocket.Server{Handler: websocket.Handler(this.handleUpdatesWebsocket)} s.ServeHTTP(w, r) })) this.mux.Handle("/", http.HandlerFunc(this.handleNotFound)) return &this }