Skip to content

Commit ca5b50c

Browse files
committed
feat: Implement start of external workspace proxies
1 parent eb66cc9 commit ca5b50c

File tree

1 file changed

+161
-0
lines changed

1 file changed

+161
-0
lines changed

enterprise/externalproxy/proxy.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package externalproxy
2+
3+
import (
4+
"net/http"
5+
"net/url"
6+
"regexp"
7+
"time"
8+
9+
"github.com/coder/coder/buildinfo"
10+
11+
"github.com/prometheus/client_golang/prometheus"
12+
13+
"github.com/coder/coder/coderd/tracing"
14+
"go.opentelemetry.io/otel/trace"
15+
16+
"github.com/go-chi/chi/v5"
17+
18+
"github.com/coder/coder/coderd/wsconncache"
19+
20+
"github.com/coder/coder/coderd/httpmw"
21+
22+
"cdr.dev/slog"
23+
"github.com/coder/coder/coderd/workspaceapps"
24+
)
25+
26+
type Options struct {
27+
Logger slog.Logger
28+
29+
// PrimaryAccessURL is the URL of the primary coderd instance.
30+
// This also serves as the DashboardURL.
31+
PrimaryAccessURL *url.URL
32+
// AccessURL is the URL of the WorkspaceProxy. This is the url to communicate
33+
// with this server.
34+
AccessURL *url.URL
35+
36+
// TODO: @emyrk We use these two fields in many places with this comment.
37+
// Maybe we should make some shared options struct?
38+
// AppHostname should be the wildcard hostname to use for workspace
39+
// applications INCLUDING the asterisk, (optional) suffix and leading dot.
40+
// It will use the same scheme and port number as the access URL.
41+
// E.g. "*.apps.coder.com" or "*-apps.coder.com".
42+
AppHostname string
43+
// AppHostnameRegex contains the regex version of options.AppHostname as
44+
// generated by httpapi.CompileHostnamePattern(). It MUST be set if
45+
// options.AppHostname is set.
46+
AppHostnameRegex *regexp.Regexp
47+
48+
RealIPConfig *httpmw.RealIPConfig
49+
// TODO: @emyrk this key needs to be provided via a file or something?
50+
// Maybe we should curl it from the primary over some secure connection?
51+
AppSecurityKey workspaceapps.SecurityKey
52+
53+
Tracing trace.TracerProvider
54+
PrometheusRegistry *prometheus.Registry
55+
56+
APIRateLimit int
57+
SecureAuthCookie bool
58+
}
59+
60+
// Server is an external workspace proxy server. This server can communicate
61+
// directly with a workspace. It requires a primary coderd to establish a said
62+
// connection.
63+
type Server struct {
64+
PrimaryAccessURL *url.URL
65+
AppServer *workspaceapps.Server
66+
67+
// Logging/Metrics
68+
Logger slog.Logger
69+
TracerProvider trace.TracerProvider
70+
PrometheusRegistry *prometheus.Registry
71+
72+
Handler chi.Router
73+
74+
// TODO: Missing:
75+
// - derpserver
76+
77+
Options *Options
78+
}
79+
80+
func New(opts *Options) *Server {
81+
if opts.PrometheusRegistry == nil {
82+
opts.PrometheusRegistry = prometheus.NewRegistry()
83+
}
84+
85+
r := chi.NewRouter()
86+
s := &Server{
87+
Options: opts,
88+
PrimaryAccessURL: opts.PrimaryAccessURL,
89+
AppServer: &workspaceapps.Server{
90+
Logger: opts.Logger.Named("workspaceapps"),
91+
DashboardURL: opts.PrimaryAccessURL,
92+
AccessURL: opts.AccessURL,
93+
Hostname: opts.AppHostname,
94+
HostnameRegex: opts.AppHostnameRegex,
95+
// TODO: @emyrk We should reduce the options passed in here.
96+
DeploymentValues: nil,
97+
RealIPConfig: opts.RealIPConfig,
98+
// TODO: @emyrk we need to implement this for external token providers.
99+
SignedTokenProvider: nil,
100+
// TODO: @emyrk we need to implement a dialer
101+
WorkspaceConnCache: wsconncache.New(nil, 0),
102+
AppSecurityKey: opts.AppSecurityKey,
103+
},
104+
Logger: opts.Logger.Named("workspace-proxy"),
105+
TracerProvider: opts.Tracing,
106+
PrometheusRegistry: opts.PrometheusRegistry,
107+
Handler: r,
108+
}
109+
110+
// Routes
111+
apiRateLimiter := httpmw.RateLimit(opts.APIRateLimit, time.Minute)
112+
// Persistant middlewares to all routes
113+
r.Use(
114+
// TODO: @emyrk Should we standardize these in some other package?
115+
httpmw.Recover(s.Logger),
116+
tracing.StatusWriterMiddleware,
117+
tracing.Middleware(s.TracerProvider),
118+
httpmw.AttachRequestID,
119+
httpmw.ExtractRealIP(s.Options.RealIPConfig),
120+
httpmw.Logger(s.Logger),
121+
httpmw.Prometheus(s.PrometheusRegistry),
122+
123+
// SubdomainAppMW is a middleware that handles all requests to the
124+
// subdomain based workspace apps.
125+
s.AppServer.SubdomainAppMW(apiRateLimiter),
126+
// Build-Version is helpful for debugging.
127+
func(next http.Handler) http.Handler {
128+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129+
w.Header().Add("X-Coder-Build-Version", buildinfo.Version())
130+
next.ServeHTTP(w, r)
131+
})
132+
},
133+
// This header stops a browser from trying to MIME-sniff the content type and
134+
// forces it to stick with the declared content-type. This is the only valid
135+
// value for this header.
136+
// See: https://github.com/coder/security/issues/12
137+
func(next http.Handler) http.Handler {
138+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
139+
w.Header().Add("X-Content-Type-Options", "nosniff")
140+
next.ServeHTTP(w, r)
141+
})
142+
},
143+
// TODO: @emyrk we might not need this? But good to have if it does
144+
// not break anything.
145+
httpmw.CSRF(s.Options.SecureAuthCookie),
146+
)
147+
148+
// Attach workspace apps routes.
149+
r.Group(func(r chi.Router) {
150+
r.Use(apiRateLimiter)
151+
s.AppServer.Attach(r)
152+
})
153+
154+
// TODO: @emyrk Buildinfo and healthz routes.
155+
156+
return s
157+
}
158+
159+
func (s *Server) Close() error {
160+
return s.AppServer.Close()
161+
}

0 commit comments

Comments
 (0)