calendar_renew.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. package service
  2. import (
  3. "time"
  4. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  5. )
  6. // nextCalendarRenewal returns the next renewal strictly after from, at midnight
  7. // in loc; a missing day clamps to the month's last, so the 31st comes back (#6106).
  8. func nextCalendarRenewal(from time.Time, day int, loc *time.Location) time.Time {
  9. if loc == nil {
  10. loc = time.UTC
  11. }
  12. if day < 1 {
  13. day = 1
  14. }
  15. if day > 31 {
  16. day = 31
  17. }
  18. local := from.In(loc)
  19. candidate := calendarDay(local.Year(), local.Month(), day, loc)
  20. if !candidate.After(local) {
  21. year, month := local.Year(), local.Month()+1
  22. if month > time.December {
  23. year, month = year+1, time.January
  24. }
  25. candidate = calendarDay(year, month, day, loc)
  26. }
  27. return candidate
  28. }
  29. // Clamped rather than normalized: time.Date rolls 31 February into March, which
  30. // is the drift this mode exists to avoid.
  31. func calendarDay(year int, month time.Month, day int, loc *time.Location) time.Time {
  32. last := daysInMonth(year, month)
  33. if day > last {
  34. day = last
  35. }
  36. return time.Date(year, month, day, 0, 0, 0, 0, loc)
  37. }
  38. func daysInMonth(year int, month time.Month) int {
  39. return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
  40. }
  41. // Advance by calendar dates, not 168 hours; a repeated midnight must not spend
  42. // another weekly allowance on the same local date.
  43. func nextWeeklyRenewal(from time.Time, weekday int, loc *time.Location) time.Time {
  44. if loc == nil {
  45. loc = time.UTC
  46. }
  47. local := from.In(loc)
  48. days := (min(7, max(1, weekday))%7 - int(local.Weekday()) + 7) % 7
  49. if days == 0 {
  50. days = 7
  51. }
  52. date := time.Date(local.Year(), local.Month(), local.Day()+days, 0, 0, 0, 0, time.UTC)
  53. // A corrupt or unusual zone must not stall the single traffic writer.
  54. // Returning from lets the catch-up forward-progress guard fail closed.
  55. for range 8 {
  56. candidate, exists := localCalendarDateStart(date, loc)
  57. if exists && candidate.After(local) {
  58. return candidate
  59. }
  60. date = date.AddDate(0, 0, 7)
  61. }
  62. return from
  63. }
  64. // Find the first valid instant of a local date: Date can pick a repeated
  65. // midnight or normalize a nonexistent midnight into the preceding day.
  66. func localCalendarDateStart(date time.Time, loc *time.Location) (time.Time, bool) {
  67. localDate := func(at time.Time) time.Time {
  68. local := at.In(loc)
  69. return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, time.UTC)
  70. }
  71. candidate := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, loc)
  72. if localDate(candidate).Equal(date) && localDate(candidate.Add(-time.Second)).Before(date) {
  73. return candidate, true
  74. }
  75. low, high := candidate.Unix()-86400, candidate.Unix()+86400
  76. for low < high {
  77. mid := low + (high-low)/2
  78. if localDate(time.Unix(mid, 0)).Before(date) {
  79. low = mid + 1
  80. } else {
  81. high = mid
  82. }
  83. }
  84. start := time.Unix(low, 0).In(loc)
  85. return start, localDate(start).Equal(date)
  86. }
  87. func nextClientRenewal(expiry int64, reset, day, weekday int, loc *time.Location) int64 {
  88. if day > 0 {
  89. return nextCalendarRenewal(time.UnixMilli(expiry), day, loc).UnixMilli()
  90. }
  91. if weekday > 0 {
  92. return nextWeeklyRenewal(time.UnixMilli(expiry), weekday, loc).UnixMilli()
  93. }
  94. return expiry + int64(reset)*86400000
  95. }
  96. func canonicalRenewalExpiry(expiry int64, reset, day, weekday int, loc *time.Location) int64 {
  97. if day > 0 || weekday > 0 {
  98. boundary := nextClientRenewal(expiry, reset, day, weekday, loc)
  99. if expiry >= boundary-1000 && expiry < boundary {
  100. return boundary
  101. }
  102. }
  103. return expiry
  104. }
  105. func catchUpClientRenewal(traffic *xray.ClientTraffic, now int64, loc *time.Location) (int64, int) {
  106. if traffic.ResetDay <= 0 && traffic.ResetWeekday <= 0 && traffic.Reset <= 0 {
  107. return traffic.ExpiryTime, 0
  108. }
  109. expiry := canonicalRenewalExpiry(traffic.ExpiryTime, traffic.Reset, traffic.ResetDay, traffic.ResetWeekday, loc)
  110. renewals := 0
  111. for expiry < now {
  112. if traffic.ResetMax > 0 && traffic.ResetCount+renewals >= traffic.ResetMax {
  113. break
  114. }
  115. next := nextClientRenewal(expiry, traffic.Reset, traffic.ResetDay, traffic.ResetWeekday, loc)
  116. if next <= expiry {
  117. return traffic.ExpiryTime, 0
  118. }
  119. expiry = next
  120. renewals++
  121. }
  122. if renewals == 0 {
  123. return traffic.ExpiryTime, 0
  124. }
  125. return expiry, renewals
  126. }