-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
227 lines (190 loc) · 6.58 KB
/
main.go
File metadata and controls
227 lines (190 loc) · 6.58 KB
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
package main
import (
"context"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"tailscale.com/tsnet"
"github.com/sierrasoftworks/tailon/pkg/api"
"github.com/sierrasoftworks/tailon/pkg/apps"
"github.com/sierrasoftworks/tailon/pkg/config"
"github.com/sierrasoftworks/tailon/pkg/ui"
"github.com/sierrasoftworks/tailon/pkg/userctx"
)
var (
configFile string
verbose bool
)
var Version = "dev"
var rootCmd = &cobra.Command{
Use: "tailon",
Short: "A web service for managing applications over Tailscale",
Long: `tailon is a web service that allows you to manage and monitor applications
over your Tailscale network. It provides APIs to start, stop, and stream logs
from configured applications.`,
Run: runServer,
Version: Version,
}
func init() {
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "config.yaml", "config file")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose logging")
}
func main() {
if err := rootCmd.Execute(); err != nil {
logrus.WithError(err).Fatal("Failed to execute command")
}
}
func runServer(cmd *cobra.Command, args []string) {
// Configure logging
if verbose {
logrus.SetLevel(logrus.DebugLevel)
}
logrus.SetFormatter(&logrus.TextFormatter{})
// Load configuration
cfg, err := config.Load(configFile)
if err != nil {
logrus.WithError(err).Fatal("Failed to load configuration")
}
logrus.WithField("config", configFile).Info("Configuration loaded")
// Validate that at least one server is enabled
if !cfg.Tailscale.Enabled && cfg.Listen == "" {
logrus.Fatal("At least one server must be enabled. Please either:\n" +
" - Enable Tailscale integration by setting 'tailscale.enabled: true' in your config, or\n" +
" - Configure a local HTTP server by setting 'listen: \"localhost:8080\"' (or similar) in your config")
}
// Security warning for non-localhost bindings
if cfg.Listen != "" && !isLocalhostBinding(cfg.Listen) {
logrus.Warn("WARNING: Your 'listen' address is not bound to localhost. " +
"This will allow ANYONE with network access to your machine to control your applications. " +
"For security, consider using 'localhost:PORT' or '127.0.0.1:PORT' instead, " +
"or rely on Tailscale integration for secure remote access.")
}
ctx, cancelRequests := context.WithCancel(userctx.WithDefaultRole(context.Background(), cfg.Security.DefaultRole))
// Create application manager
appManager := apps.NewManager(cfg.Applications)
// Create servers
var apiServer *api.Server
var uiServer *ui.Server
var mainRouter *mux.Router
// Start Tailscale server if enabled
var tailscaleServer *http.Server
if cfg.Tailscale.Enabled {
// Setup Tailscale server
tsServer := &tsnet.Server{
Hostname: cfg.Tailscale.Name,
Dir: cfg.Tailscale.StateDir,
Ephemeral: cfg.Tailscale.Ephemeral,
}
// Get the LocalClient for user context
localClient, err := tsServer.LocalClient()
if err != nil {
logrus.WithError(err).Fatal("Failed to get Tailscale LocalClient")
}
// Create servers with Tailscale LocalClient for user context
apiServer = api.NewServerWithTailscale(appManager, localClient)
uiServer = ui.NewServer(appManager)
// Create main router and mount sub-routers
mainRouter = mux.NewRouter()
mainRouter.PathPrefix("/api/").Handler(apiServer.Routes())
mainRouter.PathPrefix("/docs/").Handler(uiServer.Routes())
mainRouter.PathPrefix("/").Handler(uiServer.Routes())
// Create HTTP server for Tailscale
tailscaleServer = &http.Server{
Handler: mainRouter,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
BaseContext: func(net.Listener) context.Context {
return ctx
},
}
// Start Tailscale server
go func() {
var listener net.Listener
var err error
// Try to listen on HTTPS port first (Tailscale will auto-configure HTTPS if available)
listener, err = tsServer.Listen("tcp", ":443")
if err != nil {
// Fallback to HTTP
listener, err = tsServer.Listen("tcp", ":80")
if err != nil {
logrus.WithError(err).Fatal("Failed to create Tailscale listener")
}
logrus.Info("Starting HTTP server on Tailscale network at :80")
} else {
logrus.Info("Starting HTTPS server on Tailscale network at :443")
}
if err := tailscaleServer.Serve(listener); err != nil && err != http.ErrServerClosed {
logrus.WithError(err).Fatal("Tailscale server failed")
}
}()
} else {
logrus.Info("Tailscale integration disabled")
// Create servers without Tailscale (anonymous users only)
apiServer = api.NewServer(appManager)
uiServer = ui.NewServer(appManager)
// Create main router and mount sub-routers
mainRouter = mux.NewRouter()
mainRouter.PathPrefix("/api/").Handler(apiServer.Routes())
mainRouter.PathPrefix("/docs/").Handler(uiServer.Routes())
mainRouter.PathPrefix("/").Handler(uiServer.Routes())
}
// Also listen on local interface if configured
var localServer *http.Server
if cfg.Listen != "" {
localServer = &http.Server{
Addr: cfg.Listen,
Handler: mainRouter,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
BaseContext: func(net.Listener) context.Context {
return ctx
},
}
go func() {
logrus.WithField("addr", cfg.Listen).Info("Starting local HTTP server")
if err := localServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logrus.WithError(err).Fatal("Local HTTP server failed")
}
}()
} else {
logrus.Info("Local HTTP server disabled")
}
// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
logrus.Info("Shutting down server...")
// Graceful shutdown
ctx, cancelShutdown := context.WithTimeout(context.Background(), 15*time.Second)
defer cancelShutdown()
// Shutdown both servers if they exist
if tailscaleServer != nil {
if err := tailscaleServer.Shutdown(ctx); err != nil {
logrus.WithError(err).Error("Tailscale server shutdown failed")
}
}
if localServer != nil {
if err := localServer.Shutdown(ctx); err != nil {
logrus.WithError(err).Error("Local server shutdown failed")
}
}
cancelRequests()
logrus.Info("Server stopped")
}
// isLocalhostBinding checks if the given address is bound to localhost or 127.0.0.1
func isLocalhostBinding(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
// If we can't parse it, assume it's not safe
return false
}
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}