diff options
author | Mike Crute <mike@crute.us> | 2023-07-29 12:15:13 -0700 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2023-07-29 12:15:13 -0700 |
commit | 4e995f9e6c3adc43a361b6fa9b976d25378f1594 (patch) | |
tree | 862642149583fa4ad662edfe0b31a7d65b8e302e /cmd/web/server.go | |
parent | fea07831eadd35532055ec16fc43b0cde56a54b1 (diff) | |
download | websocket_proxy-4e995f9e6c3adc43a361b6fa9b976d25378f1594.tar.bz2 websocket_proxy-4e995f9e6c3adc43a361b6fa9b976d25378f1594.tar.xz websocket_proxy-4e995f9e6c3adc43a361b6fa9b976d25378f1594.zip |
Initial import of rewrite
Diffstat (limited to 'cmd/web/server.go')
-rw-r--r-- | cmd/web/server.go | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/cmd/web/server.go b/cmd/web/server.go new file mode 100644 index 0000000..6eb585a --- /dev/null +++ b/cmd/web/server.go | |||
@@ -0,0 +1,257 @@ | |||
1 | package web | ||
2 | |||
3 | import ( | ||
4 | "context" | ||
5 | "fmt" | ||
6 | "io/fs" | ||
7 | "log" | ||
8 | "os" | ||
9 | "strings" | ||
10 | "text/template" | ||
11 | "time" | ||
12 | |||
13 | "code.crute.us/mcrute/ssh-proxy/app" | ||
14 | "code.crute.us/mcrute/ssh-proxy/app/controllers" | ||
15 | "code.crute.us/mcrute/ssh-proxy/app/middleware" | ||
16 | "code.crute.us/mcrute/ssh-proxy/app/models" | ||
17 | "code.crute.us/mcrute/ssh-proxy/db" | ||
18 | |||
19 | "code.crute.us/mcrute/golib/cli" | ||
20 | "code.crute.us/mcrute/golib/clients/autocert/v2" | ||
21 | "code.crute.us/mcrute/golib/clients/netbox/v3" | ||
22 | "code.crute.us/mcrute/golib/db/mongodb/v2" | ||
23 | glecho "code.crute.us/mcrute/golib/echo" | ||
24 | glcontroller "code.crute.us/mcrute/golib/echo/controller" | ||
25 | glmiddleware "code.crute.us/mcrute/golib/echo/middleware" | ||
26 | "code.crute.us/mcrute/golib/echo/session" | ||
27 | "code.crute.us/mcrute/golib/secrets" | ||
28 | |||
29 | "github.com/go-webauthn/webauthn/webauthn" | ||
30 | "github.com/gorilla/websocket" | ||
31 | "github.com/labstack/echo/v4" | ||
32 | "github.com/spf13/cobra" | ||
33 | ) | ||
34 | |||
35 | func Register(root *cobra.Command, embeddedTemplates fs.FS, appVersion string) { | ||
36 | webCmd := &cobra.Command{ | ||
37 | Use: "web [options]", | ||
38 | Short: "Run web server", | ||
39 | Run: func(c *cobra.Command, args []string) { | ||
40 | cfg := app.Config{} | ||
41 | cli.MustGetConfig(c, &cfg) | ||
42 | webMain(cfg, embeddedTemplates, appVersion) | ||
43 | }, | ||
44 | } | ||
45 | cli.AddFlags(webCmd, &app.Config{}, app.DefaultConfig, "web") | ||
46 | root.AddCommand(webCmd) | ||
47 | } | ||
48 | |||
49 | func PopulateTemplateContext(c echo.Context) (interface{}, error) { | ||
50 | // May not be set if we're being called from something other than | ||
51 | // the generic template controller, which can happen in the order | ||
52 | // redirect controller. | ||
53 | cp, _ := c.Get("CanonicalPath").(string) | ||
54 | |||
55 | return &app.PageContext{ | ||
56 | PageName: strings.SplitN(cp, ".", 2)[0], | ||
57 | Year: time.Now().Year(), | ||
58 | RenderTime: time.Now().Format(time.RFC1123), | ||
59 | Flags: glcontroller.NewFeatureFlags(), | ||
60 | CSRFToken: glmiddleware.GetCSRFToken(c), | ||
61 | Context: glcontroller.NewPageContext(), | ||
62 | }, nil | ||
63 | } | ||
64 | |||
65 | func webMain(cfg app.Config, embeddedTemplates fs.FS, appVersion string) { | ||
66 | ctx, cancel := context.WithCancel(context.Background()) | ||
67 | defer cancel() | ||
68 | |||
69 | gt := &glcontroller.GenericTemplateHandler{Render: PopulateTemplateContext} | ||
70 | |||
71 | s, err := glecho.NewEchoWrapper(ctx, cfg.Debug) | ||
72 | if err != nil { | ||
73 | log.Fatalf("Error building echo: %s", err) | ||
74 | } | ||
75 | |||
76 | vc, err := glecho.MakeVaultSecretsClient(ctx) | ||
77 | if err != nil { | ||
78 | log.Fatalf("Error making vault client %s", err) | ||
79 | } | ||
80 | |||
81 | if err = s.Configure(glecho.EchoConfig{ | ||
82 | ApplicationName: "app-server", | ||
83 | ApplicationVersion: appVersion, | ||
84 | BindAddresses: cfg.Bind, | ||
85 | DiskTemplates: os.DirFS("templates/"), | ||
86 | EmbeddedTemplates: embeddedTemplates, | ||
87 | RedirectToWWW: false, | ||
88 | TrustedProxyIPRanges: cfg.TrustedIPRanges, | ||
89 | ContentSecurityPolicy: &glmiddleware.ContentSecurityPolicyConfig{ | ||
90 | DefaultSrc: []glmiddleware.CSPDirective{ | ||
91 | glmiddleware.CSPSelf, | ||
92 | glmiddleware.CSPUnsafeInline, | ||
93 | }, | ||
94 | }, | ||
95 | TemplateFunctions: template.FuncMap{ | ||
96 | "cacheBustUrl": gt.TmplMakeCacheBustUrl, | ||
97 | }, | ||
98 | Autocert: autocert.MustNewAutocertWrapper(ctx, autocert.AutocertConfig{ | ||
99 | ApiKey: secrets.MustGetApiKey(vc, ctx, cfg.DNSApiKeyVaultPath).Key, | ||
100 | Hosts: cfg.Hostnames, | ||
101 | Email: cfg.AutocertEmail, | ||
102 | CertHost: cfg.AutocertHost, | ||
103 | }), | ||
104 | NetboxClient: &netbox.BasicNetboxClient{ | ||
105 | Endpoint: cfg.NetboxHost, | ||
106 | ApiKey: secrets.MustGetApiKey(vc, ctx, cfg.NetboxApiKeyVaultPath).Key, | ||
107 | }, | ||
108 | }); err != nil { | ||
109 | log.Fatalf("Error configuring echo: %s", err) | ||
110 | } | ||
111 | |||
112 | glecho.AttachSecretsClient(vc, cancel, s.Runner(), s.Logger) | ||
113 | |||
114 | mongo, err := mongodb.Connect(ctx, cfg.MongoDbUri, vc) | ||
115 | if err != nil { | ||
116 | log.Fatalf("Error connecting to mongodb: %s", err) | ||
117 | } | ||
118 | |||
119 | cookieKey := secrets.MustGetRSAKey(vc, ctx, cfg.CookieKeyPath) | ||
120 | pk, err := cookieKey.RSAPrivateKey() | ||
121 | if err != nil { | ||
122 | log.Fatalf("Error fetching cookie key from vault: %s", err) | ||
123 | } | ||
124 | |||
125 | ss, err := session.NewCookieStore[*app.Session](pk, app.NewSession) | ||
126 | if err != nil { | ||
127 | log.Fatalf("Error creating session store: %s", err) | ||
128 | } | ||
129 | |||
130 | userStore := &db.MongoDbBasicStore[*models.User]{ | ||
131 | Db: mongo, | ||
132 | CollectionName: "users", | ||
133 | } | ||
134 | |||
135 | oauthClientStore := &db.MongoDbBasicStore[*models.OauthClient]{ | ||
136 | Db: mongo, | ||
137 | CollectionName: "oauth_clients", | ||
138 | } | ||
139 | |||
140 | authSessionStore := &models.AuthSessionStoreMongodb{ | ||
141 | MongoDbBasicStore: &db.MongoDbBasicStore[*models.AuthSession]{ | ||
142 | Db: mongo, | ||
143 | CollectionName: "oauth_sessions", | ||
144 | }, | ||
145 | } | ||
146 | |||
147 | wauthn, err := webauthn.New(&webauthn.Config{ | ||
148 | RPDisplayName: cfg.OauthRPName, | ||
149 | RPID: cfg.Hostnames[0], | ||
150 | RPOrigins: []string{ | ||
151 | fmt.Sprintf("https://%s:8070", cfg.Hostnames[0]), // TODO: Expose port in echo server for use here | ||
152 | }, | ||
153 | }) | ||
154 | if err != nil { | ||
155 | log.Fatalf("Error constructing webauthn: %s", err) | ||
156 | } | ||
157 | |||
158 | lc := &controllers.LoginController[*app.Session]{ | ||
159 | Logger: s.Logger, | ||
160 | Sessions: ss, | ||
161 | Users: userStore, | ||
162 | AuthSessions: authSessionStore, | ||
163 | Webauthn: wauthn, | ||
164 | SessionExpiration: cfg.OauthSessionTimeout, | ||
165 | } | ||
166 | |||
167 | rc := &controllers.RegisterController[*app.Session]{ | ||
168 | Logger: s.Logger, | ||
169 | Sessions: ss, | ||
170 | Users: userStore, | ||
171 | AuthSessions: authSessionStore, | ||
172 | Webauthn: wauthn, | ||
173 | } | ||
174 | |||
175 | o2dc := &controllers.OAuth2DeviceController[*app.Session]{ | ||
176 | Logger: s.Logger, | ||
177 | AuthSessions: authSessionStore, | ||
178 | OauthClients: oauthClientStore, | ||
179 | Hostname: fmt.Sprintf("https://%s:8070", cfg.Hostnames[0]), // TODO | ||
180 | PollSeconds: cfg.OauthDevicePollSecs, | ||
181 | SessionExpiration: cfg.OauthSessionTimeout, | ||
182 | } | ||
183 | |||
184 | ph := &controllers.ProxyHandler{ | ||
185 | Logger: s.Logger, | ||
186 | Users: userStore, | ||
187 | Upgrader: websocket.Upgrader{ | ||
188 | ReadBufferSize: 1024, | ||
189 | WriteBufferSize: 1024, | ||
190 | }, | ||
191 | } | ||
192 | |||
193 | caAuthMw := &middleware.TokenAuthMiddleware{ | ||
194 | Logger: s.Logger, | ||
195 | RequiredScope: "ca:issue", | ||
196 | AuthSessions: authSessionStore, | ||
197 | } | ||
198 | |||
199 | proxyAuthMw := &middleware.TokenAuthMiddleware{ | ||
200 | Logger: s.Logger, | ||
201 | RequiredScope: "ssh:proxy", | ||
202 | AuthSessions: authSessionStore, | ||
203 | } | ||
204 | |||
205 | var caSecret controllers.CASecret | ||
206 | if _, err := vc.Secret(ctx, cfg.SSHCAKeyPath, &caSecret); err != nil { | ||
207 | log.Fatalf("Error fetching SSH CA secret from Vault: %s", err) | ||
208 | } | ||
209 | |||
210 | ca, err := controllers.NewCAHandler(controllers.CAHandlerConfig{ | ||
211 | Logger: s.Logger, | ||
212 | Users: userStore, | ||
213 | Expiration: cfg.SSHCertificateExpiration, | ||
214 | Secret: caSecret, | ||
215 | }) | ||
216 | if err != nil { | ||
217 | log.Fatalf("Error building CA controller: %s", err) | ||
218 | } | ||
219 | |||
220 | s.Use(session.Middleware(ss)) | ||
221 | |||
222 | csm := glmiddleware.CSRFProtect(ss) | ||
223 | |||
224 | s.GET("/login", gt.Handle, csm) | ||
225 | s.GET("/register", gt.Handle, csm) | ||
226 | |||
227 | ag := s.Group("/auth") | ||
228 | { | ||
229 | ag.POST("/device", o2dc.HandleStart) | ||
230 | ag.POST("/token", o2dc.HandleToken) | ||
231 | |||
232 | lg := ag.Group("/login") | ||
233 | lg.Use(csm) | ||
234 | { | ||
235 | lg.GET("/:username", lc.HandleStart) | ||
236 | lg.POST("/:username", lc.HandleFinish) | ||
237 | } | ||
238 | |||
239 | rg := ag.Group("/register") | ||
240 | rg.Use(csm) | ||
241 | { | ||
242 | rg.GET("/:username", rc.HandleStart) | ||
243 | rg.POST("/:username", rc.HandleFinish) | ||
244 | } | ||
245 | } | ||
246 | |||
247 | s.POST("/ca/issue", ca.HandleIssue, caAuthMw.Middleware) | ||
248 | |||
249 | pg := s.Group("/proxy-to") | ||
250 | pg.Use(proxyAuthMw.Middleware) | ||
251 | { | ||
252 | pg.GET("/:host", ph.Handle) | ||
253 | pg.GET("/:host/:port", ph.Handle) | ||
254 | } | ||
255 | |||
256 | s.RunForever(!cfg.DisableBackgroundJobs) | ||
257 | } | ||