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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//go:generate go run ../../tools/winres
package main
import (
"errors"
"flag"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"golang.org/x/term"
"github.com/armanlife/firefox-vpn-client/core/internal/desktop"
"github.com/armanlife/firefox-vpn-client/core/internal/logx"
"github.com/armanlife/firefox-vpn-client/core/internal/service"
"github.com/armanlife/firefox-vpn-client/core/internal/update"
"github.com/armanlife/firefox-vpn-client/core/internal/version"
)
func main() {
flag.Usage = func() {
out := flag.CommandLine.Output()
fmt.Fprintln(out, "Usage: firefox-vpn-core [-data-dir DIR] [-plain] [-verbose]")
fmt.Fprintln(out, " firefox-vpn-core -tray [-data-dir DIR]")
fmt.Fprintln(out, " firefox-vpn-core -app [-data-dir DIR] [-verbose] (started by the desktop app)")
fmt.Fprintln(out, " firefox-vpn-core -update -data-dir DIR (a copy started by the app's core, to install an update)")
fmt.Fprintln(out, " firefox-vpn-core -add-account | -list-accounts | -set-dashboard-login [-data-dir DIR]")
fmt.Fprintln(out, " firefox-vpn-core -setup-server USERNAME [-data-dir DIR] (install.sh, new Linux servers)")
fmt.Fprintln(out)
fmt.Fprintln(out, "Without a command, runs the proxy service and its web dashboard (default")
fmt.Fprintln(out, "http://127.0.0.1:10820) with a status screen in the terminal (-plain prints the")
fmt.Fprintln(out, "log instead). Settings live in user-data/config.json next to the executable;")
fmt.Fprintln(out, "routing (routing.enabled, off by default) sends the sites and networks of")
fmt.Fprintln(out, "routing.region (default ir) directly and everything else through Mozilla.")
fmt.Fprintln(out)
fmt.Fprintln(out, "-app runs the service for the desktop app: no tray icon or status screen, the")
fmt.Fprintln(out, "log in the log file only. It stops on POST /api/app/exit, Ctrl+C, SIGTERM, or")
fmt.Fprintln(out, "when its standard input (a pipe the app keeps open) ends. Exit status: 0 after")
fmt.Fprintln(out, "a stop, 1 on an error, 2 on bad arguments, 3 when another copy already serves")
fmt.Fprintln(out, "the data folder, 4 when another program holds the dashboard address. On a")
fmt.Fprintln(out, "failure the first line on the standard error is \"app-error: <code> <detail>\".")
fmt.Fprintln(out)
flag.PrintDefaults()
}
dataDir := flag.String("data-dir", "", "Data folder (default: the desktop app's folder when the core sits beside firefox-vpn(.exe), else the \"user-data\" folder next to the executable)")
addAccount := flag.Bool("add-account", false, "Sign in on the terminal and add the account, then exit")
listAccounts := flag.Bool("list-accounts", false, "List the accounts with status and quota, then exit")
status := flag.Bool("status", false, "Print the running service's proxy state, account, and data left in a few lines, then exit")
setLogin := flag.Bool("set-dashboard-login", false, "Set or remove the dashboard username and password on the terminal, then exit")
setupServer := flag.String("setup-server", "", "Open the dashboard and the proxy to the network (the dashboard login for USERNAME, \"-\" for a random one, and a random proxy login, each with a random password), print the logins, then exit (install.sh runs it on a new server)")
tray := flag.Bool("tray", false, "Show the tray icon; runs the service in-process unless one is already running")
app := flag.Bool("app", false, "Run the service for the desktop app (no tray icon or status screen); stops when standard input ends")
applyUpdate := flag.Bool("update", false, "Install the desktop app's downloaded update (the app's core starts a copy of itself with it; the log goes to the log file)")
showVersion := flag.Bool("version", false, "Print the version and exit")
plain := flag.Bool("plain", false, "Print the log instead of the status screen")
verbose := flag.Bool("verbose", false, "Log CONNECT destinations and debug lines regardless of the config")
checkQUIC := flag.Bool("check-quic", false, "Try QUIC (HTTP/3) handshakes to Mozilla's proxy servers and to well-known sites, print the results, then exit; tells whether this network lets HTTP/3 through")
flag.Parse()
if *showVersion {
fmt.Println(version.Current)
return
}
if *checkQUIC {
if !runQUICCheck(os.Stdout) {
os.Exit(1)
}
return
}
if flag.NArg() > 0 {
fmt.Fprintf(os.Stderr, "unexpected arguments: %s\n", strings.Join(flag.Args(), " "))
flag.Usage()
os.Exit(2)
}
if err := runCommand(commandOptions{
dataDir: *dataDir,
addAccount: *addAccount,
listAccounts: *listAccounts,
status: *status,
setLogin: *setLogin,
setupServer: *setupServer,
tray: *tray,
app: *app,
update: *applyUpdate,
plain: *plain,
verbose: *verbose,
}); err != nil {
logx.Error("%s", logx.Err(err))
if *app {
// The log goes to the file only in this mode; the app reads
// the reason from the standard error.
os.Exit(desktop.ReportAppError(os.Stderr, err))
}
os.Exit(1)
}
}
// commandOptions are the parsed command-line options.
type commandOptions struct {
dataDir string
addAccount bool
listAccounts bool
status bool
setLogin bool
setupServer string
tray bool
app bool
update bool
plain bool
verbose bool
}
// runCommand runs the requested one-shot command, or the service when no
// command is given.
func runCommand(opts commandOptions) error {
commands := 0
for _, set := range []bool{opts.addAccount, opts.listAccounts, opts.status, opts.setLogin, opts.setupServer != "", opts.tray, opts.app, opts.update} {
if set {
commands++
}
}
if commands > 1 {
return errors.New("use only one of -add-account, -list-accounts, -status, -set-dashboard-login, -setup-server, -tray, -app, -update")
}
if opts.app && opts.plain {
return errors.New("-plain applies to the terminal; -app logs to the log file only")
}
paths, err := service.ResolveDataPaths(opts.dataDir)
if err != nil {
return err
}
switch {
case opts.addAccount:
return runAddAccount(paths)
case opts.status:
return runStatus(paths, os.Stdout)
case opts.listAccounts:
return runListAccounts(paths)
case opts.setLogin:
return runSetDashboardLogin(paths)
case opts.setupServer != "":
return runSetupServer(paths, opts.setupServer)
case opts.update:
// A copy of the core, started apart by the app's core: no console,
// and nobody reads its output.
logx.EnableFile(paths.LogDir, false)
return update.Apply(paths.Dir)
case opts.app:
if term.IsTerminal(int(os.Stdin.Fd())) {
// Typed in a terminal: the app starts it with a pipe.
fmt.Fprintln(os.Stderr, appInTerminalHint)
}
// Standard output and error are pipes the app may never read; a
// full pipe would block logging, so the log goes to the file only.
logx.EnableFile(paths.LogDir, false)
if desktop.StartedWithOwnConsole() {
// Windows opened a console window for this process (a GUI
// parent); the app is the window.
desktop.DetachConsole()
}
return desktop.RunApp(paths, opts.verbose)
case opts.tray:
if desktop.StartedWithOwnConsole() {
// Started from Explorer (a shortcut): the tray icon replaces
// the console window.
desktop.DetachConsole()
logx.EnableFile(paths.LogDir, false)
} else {
logx.EnableFile(paths.LogDir, true)
}
return desktop.RunTray(paths, opts.verbose)
}
if !opts.plain {
// An interactive terminal (including a double-click on Windows)
// gets the status screen; anything else gets the plain log.
if shown, err := desktop.RunConsole(paths, opts.verbose); shown {
return err
}
}
return runConsole(paths, opts.verbose)
}
// appInTerminalHint tells someone who typed -app what that mode is for: it
// waits for the desktop app to turn the proxy on.
const appInTerminalHint = `firefox-vpn-core: -app is the desktop app's mode: the proxy stays off until the
app or the panel's power button turns it on, the log goes to user-data/logs
only, and closing this terminal stops it. In a terminal or as a service, run
firefox-vpn-core without -app: the proxy is on from the start (-plain prints
the log).`
// runConsole runs the service in the terminal until Ctrl+C or SIGTERM.
func runConsole(paths service.DataPaths, verbose bool) error {
logx.EnableFile(paths.LogDir, true)
d, err := service.NewDaemon(paths, verbose)
if err != nil {
return err
}
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(signals)
go func() {
if _, ok := <-signals; ok {
logx.Info("stopping")
d.Shutdown()
}
}()
return d.Run()
}