1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package proxy
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"time"
"github.com/armanlife/firefox-vpn-client/core/internal/logx"
)
type RuntimeStatus struct {
UpdatedAt string `json:"updated_at"`
PID int `json:"pid"`
ProxyPassExpiresAt string `json:"proxy_pass_expires_at"`
SecondsUntilExpiry int64 `json:"seconds_until_expiry"`
Accepting bool `json:"accepting"`
Reason string `json:"reason"`
// ActiveAccount names the account supplying the ProxyPass in
// multi-account mode.
ActiveAccount string `json:"active_account,omitempty"`
// LastError is a stable failure code of the last renewal attempt
// (e.g. reauth_required, region_unavailable); empty after success.
LastError string `json:"last_error,omitempty"`
NextAttemptAt string `json:"next_attempt_at,omitempty"`
// Quota fields are omitted when Guardian sent no valid quota headers.
QuotaUnlimited bool `json:"quota_unlimited,omitempty"`
QuotaLimitBytes *int64 `json:"quota_limit_bytes,omitempty"`
QuotaRemainingBytes *int64 `json:"quota_remaining_bytes,omitempty"`
QuotaResetAt string `json:"quota_reset_at,omitempty"`
}
func (c *Controller) CurrentStatus(reason string) RuntimeStatus {
c.mu.Lock()
defer c.mu.Unlock()
status := RuntimeStatus{
UpdatedAt: time.Now().Format(time.RFC3339),
PID: os.Getpid(),
Reason: reason,
}
if c.current != nil {
status.ProxyPassExpiresAt = c.current.expiresAt.Format(time.RFC3339)
status.SecondsUntilExpiry = int64(time.Until(c.current.expiresAt).Round(time.Second) / time.Second)
status.Accepting = c.current.accepting
}
status.LastError = c.lastError
if c.accounts != nil {
status.ActiveAccount = c.accounts.ActiveName()
}
if !c.nextAttemptAt.IsZero() {
status.NextAttemptAt = c.nextAttemptAt.Format(time.RFC3339)
}
if usage := c.usage; usage != nil {
if usage.Unlimited {
status.QuotaUnlimited = true
} else {
limit, remaining := usage.Limit, usage.Remaining
status.QuotaLimitBytes = &limit
status.QuotaRemainingBytes = &remaining
if !usage.Reset.IsZero() {
status.QuotaResetAt = usage.Reset.Format(time.RFC3339)
}
}
}
return status
}
func (c *Controller) writeStatus(reason string) {
if c.statusFile == "" {
return
}
status := c.CurrentStatus(reason)
if err := c.writeStatusFile(status); err != nil {
logx.Warn("writing status file failed path=%s err=%s", c.statusFile, logx.Err(err))
}
}
func (c *Controller) writeStatusFile(status RuntimeStatus) error {
return writeRuntimeStatus(c.statusFile, status)
}
// Status-file reasons written without a controller.
const (
// StatusSleeping: the proxy listens but holds no upstream session or
// pass (not connected yet, or asleep after proxy.idle_sleep).
StatusSleeping = "sleeping"
// StatusClosed: the proxy stopped.
StatusClosed = "closed"
)
// WriteIdleStatus writes a status file for a proxy without an upstream
// session: no pass, not accepting, with reason (StatusSleeping or
// StatusClosed).
func WriteIdleStatus(path, reason string) error {
return writeRuntimeStatus(path, RuntimeStatus{
UpdatedAt: time.Now().Format(time.RFC3339),
PID: os.Getpid(),
Reason: reason,
})
}
// writeRuntimeStatus replaces the status file at path with status; an
// empty path writes nothing.
func writeRuntimeStatus(path string, status RuntimeStatus) error {
if path == "" {
return nil
}
data, err := json.MarshalIndent(status, "", " ")
if err != nil {
return err
}
data = append(data, '\n')
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp")
if err != nil {
return err
}
tmpName := tmp.Name()
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
_ = os.Remove(tmpName)
return err
}
if err := tmp.Close(); err != nil {
_ = os.Remove(tmpName)
return err
}
if err := os.Rename(tmpName, path); err != nil {
if removeErr := os.Remove(path); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
_ = os.Remove(tmpName)
return err
}
if retryErr := os.Rename(tmpName, path); retryErr != nil {
_ = os.Remove(tmpName)
return retryErr
}
}
return nil
}