registry.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. // Copyright 2025 The frp Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package registry
  15. import (
  16. "fmt"
  17. "sync"
  18. "time"
  19. )
  20. // ClientInfo captures metadata about a connected frpc instance.
  21. type ClientInfo struct {
  22. Key string
  23. User string
  24. ClientID string
  25. RunID string
  26. Hostname string
  27. IP string
  28. FirstConnectedAt time.Time
  29. LastConnectedAt time.Time
  30. DisconnectedAt time.Time
  31. Online bool
  32. }
  33. // ClientRegistry keeps track of active clients keyed by "{user}.{clientID}" (or runID if clientID is empty).
  34. // Entries without an explicit clientID are removed on disconnect to avoid stale offline records.
  35. type ClientRegistry struct {
  36. mu sync.RWMutex
  37. clients map[string]*ClientInfo
  38. runIndex map[string]string
  39. }
  40. func NewClientRegistry() *ClientRegistry {
  41. return &ClientRegistry{
  42. clients: make(map[string]*ClientInfo),
  43. runIndex: make(map[string]string),
  44. }
  45. }
  46. // Register stores/updates metadata for a client and returns the registry key plus whether it conflicts with an online client.
  47. func (cr *ClientRegistry) Register(user, clientID, runID, hostname, remoteAddr string) (key string, conflict bool) {
  48. if runID == "" {
  49. return "", false
  50. }
  51. effectiveID := clientID
  52. if effectiveID == "" {
  53. effectiveID = runID
  54. }
  55. key = cr.composeClientKey(user, effectiveID)
  56. enforceUnique := clientID != ""
  57. now := time.Now()
  58. cr.mu.Lock()
  59. defer cr.mu.Unlock()
  60. info, exists := cr.clients[key]
  61. if enforceUnique && exists && info.Online && info.RunID != "" && info.RunID != runID {
  62. return key, true
  63. }
  64. if !exists {
  65. info = &ClientInfo{
  66. Key: key,
  67. User: user,
  68. ClientID: clientID,
  69. FirstConnectedAt: now,
  70. }
  71. cr.clients[key] = info
  72. } else if info.RunID != "" {
  73. delete(cr.runIndex, info.RunID)
  74. }
  75. info.RunID = runID
  76. info.Hostname = hostname
  77. info.IP = remoteAddr
  78. if info.FirstConnectedAt.IsZero() {
  79. info.FirstConnectedAt = now
  80. }
  81. info.LastConnectedAt = now
  82. info.DisconnectedAt = time.Time{}
  83. info.Online = true
  84. cr.runIndex[runID] = key
  85. return key, false
  86. }
  87. // MarkOfflineByRunID marks the client as offline when the corresponding control disconnects.
  88. func (cr *ClientRegistry) MarkOfflineByRunID(runID string) {
  89. cr.mu.Lock()
  90. defer cr.mu.Unlock()
  91. key, ok := cr.runIndex[runID]
  92. if !ok {
  93. return
  94. }
  95. if info, ok := cr.clients[key]; ok && info.RunID == runID {
  96. if info.ClientID == "" {
  97. delete(cr.clients, key)
  98. } else {
  99. info.RunID = ""
  100. info.Online = false
  101. now := time.Now()
  102. info.DisconnectedAt = now
  103. }
  104. }
  105. delete(cr.runIndex, runID)
  106. }
  107. // List returns a snapshot of all known clients.
  108. func (cr *ClientRegistry) List() []ClientInfo {
  109. cr.mu.RLock()
  110. defer cr.mu.RUnlock()
  111. result := make([]ClientInfo, 0, len(cr.clients))
  112. for _, info := range cr.clients {
  113. result = append(result, *info)
  114. }
  115. return result
  116. }
  117. // GetByKey retrieves a client by its composite key ({user}.{clientID} or runID fallback).
  118. func (cr *ClientRegistry) GetByKey(key string) (ClientInfo, bool) {
  119. cr.mu.RLock()
  120. defer cr.mu.RUnlock()
  121. info, ok := cr.clients[key]
  122. if !ok {
  123. return ClientInfo{}, false
  124. }
  125. return *info, true
  126. }
  127. func (cr *ClientRegistry) composeClientKey(user, id string) string {
  128. switch {
  129. case user == "":
  130. return id
  131. case id == "":
  132. return user
  133. default:
  134. return fmt.Sprintf("%s.%s", user, id)
  135. }
  136. }