mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
239 lines
6.4 KiB
Go
239 lines
6.4 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package service
|
|
|
|
import (
|
|
"Wavelet/core/contracts"
|
|
"Wavelet/pkg/logger"
|
|
"Wavelet/plugins/domain/admin/errs"
|
|
"Wavelet/plugins/domain/admin/model"
|
|
"Wavelet/plugins/domain/admin/repository"
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
analyticsDays = 7
|
|
|
|
denyingRobotsFile = "User-Agent: *\nDisallow: /\n"
|
|
allowingRobotsFile = "User-Agent: *\nAllow: /\n"
|
|
)
|
|
|
|
// RecentSystemLogs reads a page of the process log ring buffer.
|
|
func RecentSystemLogs(cursor, limit int) model.LogsResponse {
|
|
entries, hasMore := logger.GlobalRingBuffer.Query(cursor, limit)
|
|
|
|
resp := model.LogsResponse{
|
|
Lines: entries,
|
|
HasMore: hasMore,
|
|
}
|
|
if len(entries) > 0 {
|
|
resp.NextCursor = entries[0].Index
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// RobotsTxtBody resolves the robots.txt payload from the indexing setting.
|
|
func RobotsTxtBody(ctx context.Context) string {
|
|
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeySearchEngineIndexingEnabled)
|
|
if err == nil && enabled {
|
|
return allowingRobotsFile
|
|
}
|
|
return denyingRobotsFile
|
|
}
|
|
|
|
// IsAllowedLogOrigin reports whether a WebSocket handshake origin may subscribe to logs.
|
|
func IsAllowedLogOrigin(ctx context.Context, origin, host string) bool {
|
|
if origin == "" {
|
|
return true
|
|
}
|
|
|
|
// 1. 同源检查 (Same-origin check)
|
|
u, err := url.Parse(origin)
|
|
if err == nil && strings.EqualFold(u.Host, host) {
|
|
return true
|
|
}
|
|
|
|
// 2. 检查配置的允许跨域 Origin (Check allowed origins in system config)
|
|
sc, cfgErr := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress)
|
|
if cfgErr != nil || sc.Value == "" {
|
|
return false
|
|
}
|
|
originToCheck := strings.TrimRight(strings.TrimSpace(origin), "/")
|
|
for _, allowed := range strings.Split(sc.Value, ",") {
|
|
allowed = strings.TrimRight(strings.TrimSpace(allowed), "/")
|
|
if allowed != "" && strings.EqualFold(allowed, originToCheck) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// AccessLogs queries the analytical access log store and decorates rows with user names.
|
|
func AccessLogs(ctx context.Context, q model.AccessLogQuery) (model.AccessLogsResponse, error) {
|
|
rc := GetRiskControlService()
|
|
if rc == nil {
|
|
return model.AccessLogsResponse{}, errs.ErrLogStoreUnavailable
|
|
}
|
|
|
|
filter, err := buildAccessLogFilter(ctx, q)
|
|
if err != nil {
|
|
return model.AccessLogsResponse{}, err
|
|
}
|
|
if filter.UserIDs != nil && len(filter.UserIDs) == 0 {
|
|
return model.AccessLogsResponse{Total: 0, List: []model.AccessLogItem{}}, nil
|
|
}
|
|
|
|
logs, total, err := rc.QueryAccessLogs(ctx, filter, q.Page, q.PageSize)
|
|
if err != nil {
|
|
return model.AccessLogsResponse{}, err
|
|
}
|
|
if total == 0 {
|
|
return model.AccessLogsResponse{Total: 0, List: []model.AccessLogItem{}}, nil
|
|
}
|
|
|
|
list := make([]model.AccessLogItem, len(logs))
|
|
for i, logItem := range logs {
|
|
list[i] = model.AccessLogItem{
|
|
ID: logItem.ID,
|
|
UserID: logItem.UserID,
|
|
Path: logItem.Path,
|
|
Method: logItem.Method,
|
|
IP: logItem.IP,
|
|
UserAgent: logItem.UserAgent,
|
|
Status: logItem.Status,
|
|
Latency: logItem.Latency,
|
|
CreatedAt: logItem.CreatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|
|
enrichAccessLogsWithUsers(ctx, list)
|
|
|
|
return model.AccessLogsResponse{Total: total, List: list}, nil
|
|
}
|
|
|
|
// AccessLogAnalytics aggregates the daily trend of the access log store.
|
|
func AccessLogAnalytics(ctx context.Context) (model.LogsAnalyticsResponse, error) {
|
|
rc := GetRiskControlService()
|
|
if rc == nil {
|
|
return model.LogsAnalyticsResponse{}, errs.ErrLogStoreUnavailable
|
|
}
|
|
|
|
stats, err := rc.QueryAccessLogStats(ctx, analyticsDays)
|
|
if err != nil {
|
|
return model.LogsAnalyticsResponse{}, fmt.Errorf("%s%w", errs.ErrQueryAccessTrendFailed, err)
|
|
}
|
|
|
|
trendList := make([]model.TrendItem, len(stats))
|
|
for i, st := range stats {
|
|
trendList[i] = model.TrendItem{
|
|
Date: st.Date,
|
|
Count: st.PV,
|
|
}
|
|
}
|
|
|
|
return model.LogsAnalyticsResponse{
|
|
Trend: trendList,
|
|
Browsers: []model.BrowserItem{},
|
|
TopUsers: []model.TopUserItem{},
|
|
}, nil
|
|
}
|
|
|
|
// findUserIDsByUsername resolves the user id filter behind a username search term.
|
|
func findUserIDsByUsername(ctx context.Context, username string) ([]uint64, error) {
|
|
if userSvc := GetUserService(ctx); userSvc != nil {
|
|
users, _, err := userSvc.ListUsers(ctx, 1, userQueryMaxLimit, username)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(errs.ErrQueryUserFailed, err)
|
|
}
|
|
ids := make([]uint64, 0, len(users))
|
|
for _, u := range users {
|
|
ids = append(ids, u.ID)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
ids, err := repository.SearchUserIDsByUsername(ctx, username)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(errs.ErrQueryUserFailed, err)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
const userQueryMaxLimit = 100
|
|
|
|
func buildAccessLogFilter(ctx context.Context, q model.AccessLogQuery) (contracts.AccessLogFilterDTO, error) {
|
|
filter := contracts.AccessLogFilterDTO{}
|
|
|
|
if q.Username != "" {
|
|
userIDs, err := findUserIDsByUsername(ctx, q.Username)
|
|
if err != nil {
|
|
return filter, err
|
|
}
|
|
filter.UserIDs = userIDs
|
|
}
|
|
|
|
if q.Path != "" {
|
|
filter.Path = q.Path
|
|
}
|
|
|
|
if q.StartTime != "" {
|
|
if t, err := parseAccessLogTime(q.StartTime); err == nil {
|
|
filter.StartTime = &t
|
|
}
|
|
}
|
|
|
|
if q.EndTime != "" {
|
|
if t, err := parseAccessLogTime(q.EndTime); err == nil {
|
|
filter.EndTime = &t
|
|
}
|
|
}
|
|
|
|
return filter, nil
|
|
}
|
|
|
|
func parseAccessLogTime(value string) (time.Time, error) {
|
|
if t, err := time.Parse(time.RFC3339, value); err == nil {
|
|
return t, nil
|
|
}
|
|
return time.Parse("2006-01-02 15:04:05", value)
|
|
}
|
|
|
|
// enrichAccessLogsWithUsers attaches usernames and nicknames to access log rows.
|
|
func enrichAccessLogsWithUsers(ctx context.Context, list []model.AccessLogItem) {
|
|
if len(list) == 0 {
|
|
return
|
|
}
|
|
|
|
userIDs := make([]uint64, 0, len(list))
|
|
seen := make(map[uint64]struct{}, len(list))
|
|
for _, item := range list {
|
|
if _, ok := seen[item.UserID]; ok {
|
|
continue
|
|
}
|
|
seen[item.UserID] = struct{}{}
|
|
userIDs = append(userIDs, item.UserID)
|
|
}
|
|
|
|
userMap := make(map[uint64]repository.UserDisplayName, len(userIDs))
|
|
if userSvc := GetUserService(ctx); userSvc != nil {
|
|
for _, uid := range userIDs {
|
|
if u, err := userSvc.GetUserByID(ctx, uid); err == nil && u != nil {
|
|
userMap[uid] = repository.UserDisplayName{Username: u.Username, Nickname: u.Nickname}
|
|
}
|
|
}
|
|
} else if names, err := repository.LoadUserDisplayNames(ctx, userIDs); err == nil {
|
|
userMap = names
|
|
}
|
|
|
|
for i := range list {
|
|
if info, ok := userMap[list[i].UserID]; ok {
|
|
list[i].Username = info.Username
|
|
list[i].Nickname = info.Nickname
|
|
}
|
|
}
|
|
}
|