mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-28 12:02:23 +00:00
Merge 9e5e4de2bcdaa1877606a471217374119a54f3db into b3455fa99a5e8d07133d5140017ec7c49f032a07
This commit is contained in:
commit
73f77e7776
@ -160,6 +160,7 @@ type serveEnv struct {
|
||||
http uint // HTTP port
|
||||
tcp uint // TCP port
|
||||
tlsTerminatedTCP uint // a TLS terminated TCP port
|
||||
promoteHTTPS bool // promote HTTP to HTTPS
|
||||
subcmd serveMode // subcommand
|
||||
yes bool // update without prompt
|
||||
|
||||
|
@ -124,6 +124,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
|
||||
if subcmd == serve {
|
||||
fs.UintVar(&e.http, "http", 0, "Expose an HTTP server at the specified port")
|
||||
fs.BoolVar(&e.promoteHTTPS, "promote-https", false, "Promote HTTP to HTTPS (default false)")
|
||||
}
|
||||
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
||||
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
||||
@ -224,6 +225,11 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
|
||||
if srvType == serveTypeHTTP && e.promoteHTTPS {
|
||||
fmt.Fprintf(e.stderr(), "error: --promote-https is only valid for HTTPS\n\n")
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting serve config: %w", err)
|
||||
@ -296,7 +302,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
|
||||
return err
|
||||
}
|
||||
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel)
|
||||
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel, e.promoteHTTPS)
|
||||
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
||||
}
|
||||
if err != nil {
|
||||
@ -366,7 +372,7 @@ func serveFromPortHandler(tcp *ipn.TCPPortHandler) serveType {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool) error {
|
||||
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool, promoteHTTPS bool) error {
|
||||
// update serve config based on the type
|
||||
switch srvType {
|
||||
case serveTypeHTTPS, serveTypeHTTP:
|
||||
@ -391,6 +397,8 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName st
|
||||
// update the serve config based on if funnel is enabled
|
||||
e.applyFunnel(sc, dnsName, srvPort, allowFunnel)
|
||||
|
||||
sc.SetRedirectToHTTPS(dnsName, srvPort, promoteHTTPS)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -232,9 +232,10 @@ func (src *HTTPHandler) Clone() *HTTPHandler {
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _HTTPHandlerCloneNeedsRegeneration = HTTPHandler(struct {
|
||||
Path string
|
||||
Proxy string
|
||||
Text string
|
||||
Path string
|
||||
Proxy string
|
||||
Text string
|
||||
Redirect string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of WebServerConfig.
|
||||
|
@ -456,15 +456,17 @@ func (v *HTTPHandlerView) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v HTTPHandlerView) Path() string { return v.ж.Path }
|
||||
func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy }
|
||||
func (v HTTPHandlerView) Text() string { return v.ж.Text }
|
||||
func (v HTTPHandlerView) Path() string { return v.ж.Path }
|
||||
func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy }
|
||||
func (v HTTPHandlerView) Text() string { return v.ж.Text }
|
||||
func (v HTTPHandlerView) Redirect() string { return v.ж.Redirect }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _HTTPHandlerViewNeedsRegeneration = HTTPHandler(struct {
|
||||
Path string
|
||||
Proxy string
|
||||
Text string
|
||||
Path string
|
||||
Proxy string
|
||||
Text string
|
||||
Redirect string
|
||||
}{})
|
||||
|
||||
// View returns a read-only view of WebServerConfig.
|
||||
|
@ -374,9 +374,9 @@ type LocalBackend struct {
|
||||
webClient webClient
|
||||
webClientListeners map[netip.AddrPort]*localListener // listeners for local web client traffic
|
||||
|
||||
serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic
|
||||
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy
|
||||
|
||||
serveListeners map[netip.AddrPort]*localListener // listeners for local serve traffic
|
||||
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *reverseProxy
|
||||
serveRedirectHandlers sync.Map // string (HTTPHandler.Redirect) => http.Handler
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
statusLock sync.Mutex
|
||||
@ -6531,6 +6531,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
||||
}
|
||||
|
||||
b.setServeProxyHandlersLocked()
|
||||
b.setServeRedirectHandlersLocked()
|
||||
|
||||
// don't listen on netmap addresses if we're in userspace mode
|
||||
if !b.sys.IsNetstack() {
|
||||
@ -6614,6 +6615,51 @@ func (b *LocalBackend) setServeProxyHandlersLocked() {
|
||||
})
|
||||
}
|
||||
|
||||
// setServeRedirectHandlersLocked ensures there is an http redirect handler for
|
||||
// each redirect specified in serveConfig. It expects serveConfig to be valid
|
||||
// and up-to-date, so should be called after reloadServeConfigLocked.
|
||||
func (b *LocalBackend) setServeRedirectHandlersLocked() {
|
||||
if !b.serveConfig.Valid() {
|
||||
return
|
||||
}
|
||||
var redirects map[string]bool
|
||||
b.serveConfig.RangeOverWebs(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) {
|
||||
conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) {
|
||||
redirect := h.Redirect()
|
||||
if redirect == "" {
|
||||
// Only create redirect handlers for servers with a redirect target.
|
||||
return true
|
||||
}
|
||||
|
||||
mak.Set(&redirects, redirect, true)
|
||||
if _, ok := b.serveRedirectHandlers.Load(redirect); ok {
|
||||
return true
|
||||
}
|
||||
|
||||
b.logf("serve: creating a new redirect handler for %s", redirect)
|
||||
rh, err := b.redirectHandlerForRedirect(redirect, 301)
|
||||
if err != nil {
|
||||
b.logf("[unexpected] could not create redirect handler for %v: %s", redirect, err)
|
||||
return true
|
||||
}
|
||||
b.serveRedirectHandlers.Store(redirect, rh)
|
||||
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
// Clean up redirect handlers that are no longer present in configuration.
|
||||
b.serveRedirectHandlers.Range(func(key, value any) bool {
|
||||
redirect := key.(string)
|
||||
if !redirects[redirect] {
|
||||
b.logf("serve: closing idle connections to %s", redirect)
|
||||
b.serveRedirectHandlers.Delete(redirect)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// operatorUserName returns the current pref's OperatorUser's name, or the
|
||||
// empty string if none.
|
||||
func (b *LocalBackend) operatorUserName() string {
|
||||
|
@ -804,6 +804,21 @@ func (rp *reverseProxy) shouldProxyViaH2C(r *http.Request) bool {
|
||||
return r.ProtoMajor == 2 && strings.HasPrefix(rp.backend, "http://") && isGRPCContentType(contentType)
|
||||
}
|
||||
|
||||
// redirectHandlerForRedirect creates a new HTTP redirect handler for a particular url.
|
||||
// `targetURL` is a HTTPHandler.Redirect string (url).
|
||||
func (b *LocalBackend) redirectHandlerForRedirect(targetURL string, code int) (http.Handler, error) {
|
||||
u, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
u.Path = r.URL.Path
|
||||
u.RawPath = r.URL.RawPath
|
||||
http.Redirect(w, r, u.String(), code)
|
||||
}), nil
|
||||
}
|
||||
|
||||
// isGRPC accepts an HTTP request's content type header value and determines
|
||||
// whether this is gRPC content. grpc-go considers a value that equals
|
||||
// application/grpc or has a prefix of application/grpc+ or application/grpc; a
|
||||
@ -901,6 +916,15 @@ func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if v := h.Redirect(); v != "" {
|
||||
h, ok := b.serveRedirectHandlers.Load(v)
|
||||
if !ok {
|
||||
http.Error(w, "unknown redirect destination", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.(http.Handler).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "empty handler", 500)
|
||||
}
|
||||
|
@ -658,6 +658,63 @@ func TestServeHTTPProxyPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeHTTPRedirect(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
requestPath string
|
||||
wantPath string
|
||||
}{
|
||||
{
|
||||
name: "http / -> https /",
|
||||
requestPath: "/",
|
||||
wantPath: "/",
|
||||
},
|
||||
{
|
||||
name: "http /foo -> https /foo",
|
||||
requestPath: "/foo",
|
||||
wantPath: "/foo",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
conf := &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"example.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Redirect: "https://example.ts.net"},
|
||||
}},
|
||||
"example.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Text: "foo bar"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
if err := b.SetServeConfig(conf, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := &http.Request{
|
||||
URL: &url.URL{Path: tt.requestPath},
|
||||
TLS: &tls.ConnectionState{ServerName: "example.ts.net"},
|
||||
}
|
||||
req = req.WithContext(serveHTTPContextKey.WithValue(req.Context(),
|
||||
&serveHTTPContext{
|
||||
DestPort: 80,
|
||||
SrcAddr: netip.MustParseAddrPort("1.2.3.4:1234"), // random src
|
||||
}))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
b.serveWebHandler(w, req)
|
||||
|
||||
// Verify what path was requested
|
||||
p := w.Result().Header.Get("Location")
|
||||
wantLoc := fmt.Sprintf("https://example.ts.net%s", tt.wantPath)
|
||||
if p != wantLoc {
|
||||
t.Errorf("wanted request path %s got %s", wantLoc, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeHTTPProxyHeaders(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
|
||||
|
39
ipn/serve.go
39
ipn/serve.go
@ -160,6 +160,8 @@ type HTTPHandler struct {
|
||||
|
||||
Text string `json:",omitempty"` // plaintext to serve (primarily for testing)
|
||||
|
||||
Redirect string `json:",omitempty"` // redirect requests to this URL
|
||||
|
||||
// TODO(bradfitz): bool to not enumerate directories? TTL on mapping for
|
||||
// temporary ones? Error codes? Redirects?
|
||||
}
|
||||
@ -344,6 +346,43 @@ func (sc *ServeConfig) SetFunnel(host string, port uint16, setOn bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetRedirectToHTTPS configures a TCPPortHandler and HTTPHandler to redirect all
|
||||
// traffic from port 80 to HTTPS, using the provided `host` and `port` for the
|
||||
// redirect URL. If setOn is false, it removes any active redirect handlers on
|
||||
// port 80.
|
||||
func (sc *ServeConfig) SetRedirectToHTTPS(host string, port uint16, setOn bool) {
|
||||
if sc == nil {
|
||||
sc = new(ServeConfig)
|
||||
}
|
||||
|
||||
hp := HostPort(net.JoinHostPort(host, "80"))
|
||||
if setOn {
|
||||
mak.Set(&sc.TCP, 80, &TCPPortHandler{HTTP: true})
|
||||
var url string
|
||||
if port == 443 {
|
||||
url = fmt.Sprintf("https://%s", host)
|
||||
} else {
|
||||
url = fmt.Sprintf("https://%s:%d", host, port)
|
||||
}
|
||||
handler := &HTTPHandler{Redirect: url}
|
||||
if _, ok := sc.Web[hp]; !ok {
|
||||
mak.Set(&sc.Web, hp, new(WebServerConfig))
|
||||
}
|
||||
// Overwrite any existing handlers as we're handling all HTTP traffic.
|
||||
sc.Web[hp].Handlers = map[string]*HTTPHandler{"/": handler}
|
||||
} else {
|
||||
// If we're running with HTTP to HTTPS promotion, we need to remove any
|
||||
// existing Redirect handlers.
|
||||
if tcph, exists := sc.TCP[80]; exists && tcph.HTTP {
|
||||
if wh, exists := sc.Web[hp]; exists {
|
||||
if wh.Handlers["/"].Redirect != "" {
|
||||
delete(wh.Handlers, "/")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveWebHandler deletes the web handlers at all of the given mount points
|
||||
// for the provided host and port in the serve config. If cleanupFunnel is
|
||||
// true, this also removes the funnel value for this port if no handlers remain.
|
||||
|
Loading…
x
Reference in New Issue
Block a user