Home Download Docs Code Community
     1	/*
     2	Copyright 2011 The Perkeep Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    15	*/
    16	
    17	// Package webserver implements a superset wrapper of http.Server.
    18	//
    19	// Among other things, it can throttle its connections, inherit its
    20	// listening socket from a file descriptor in the environment, and
    21	// log all activity.
    22	package webserver // import "perkeep.org/pkg/webserver"
    23	
    24	import (
    25		"context"
    26		"crypto/rand"
    27		"crypto/tls"
    28		"fmt"
    29		"log"
    30		"net"
    31		"net/http"
    32		"os"
    33		"path/filepath"
    34		"strconv"
    35		"strings"
    36		"sync"
    37		"time"
    38	
    39		"go4.org/net/throttle"
    40		"go4.org/wkfs"
    41		"golang.org/x/net/http2"
    42		"perkeep.org/pkg/webserver/listen"
    43		"tailscale.com/tsnet"
    44	)
    45	
    46	const alpnProto = "acme-tls/1" // from golang.org/x/crypto/acme.ALPNProto
    47	
    48	type Server struct {
    49		mux       *http.ServeMux
    50		listener  net.Listener
    51		listenURL string // optional forced value for ListenURL, if set (used by Tailscale)
    52		verbose   bool   // log HTTP requests and response codes
    53	
    54		Logger *log.Logger // or nil.
    55	
    56		// H2Server is the HTTP/2 server config.
    57		H2Server http2.Server
    58	
    59		// enableTLS sets the Server up for listening to HTTPS connections.
    60		enableTLS bool
    61		// tlsCertFile (tlsKeyFile) is the path to the HTTPS certificate (key) file.
    62		tlsCertFile, tlsKeyFile string
    63		// certManager is set as GetCertificate in the tls.Config of the listener. But tlsCertFile takes precedence.
    64		certManager func(*tls.ClientHelloInfo) (*tls.Certificate, error)
    65	
    66		// tsnetServer is non-nil when running in Tailscale tsnet mode.
    67		tsnetServer *tsnet.Server
    68	
    69		mu   sync.Mutex
    70		reqs int64
    71	}
    72	
    73	func New() *Server {
    74		verbose, _ := strconv.ParseBool(os.Getenv("CAMLI_HTTP_DEBUG"))
    75		return &Server{
    76			mux:     http.NewServeMux(),
    77			verbose: verbose,
    78		}
    79	}
    80	
    81	func (s *Server) printf(format string, v ...interface{}) {
    82		if s.Logger != nil {
    83			s.Logger.Printf(format, v...)
    84			return
    85		}
    86		log.Printf(format, v...)
    87	}
    88	
    89	func (s *Server) fatalf(format string, v ...interface{}) {
    90		if s.Logger != nil {
    91			s.Logger.Fatalf(format, v...)
    92			return
    93		}
    94		log.Fatalf(format, v...)
    95	}
    96	
    97	// TLSSetup specifies how the server gets its TLS certificate.
    98	type TLSSetup struct {
    99		// Certfile is the path to the TLS certificate file. It takes precedence over CertManager.
   100		CertFile string
   101		// KeyFile is the path to the TLS key file.
   102		KeyFile string
   103		// CertManager is the tls.GetCertificate of the tls Config. But CertFile takes precedence.
   104		CertManager func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error)
   105	}
   106	
   107	func (s *Server) SetTLS(setup TLSSetup) {
   108		s.enableTLS = true
   109		s.certManager = setup.CertManager
   110		s.tlsCertFile = setup.CertFile
   111		s.tlsKeyFile = setup.KeyFile
   112	}
   113	
   114	// ListenURL returns the base URL of the server, including its scheme and
   115	// authority, but without a trailing slash or any path.
   116	func (s *Server) ListenURL() string {
   117		if s.listenURL != "" {
   118			return s.listenURL
   119		}
   120		if s.listener == nil {
   121			return ""
   122		}
   123		taddr, ok := s.listener.Addr().(*net.TCPAddr)
   124		if !ok {
   125			return ""
   126		}
   127		scheme := "http"
   128		if s.enableTLS {
   129			scheme = "https"
   130		}
   131		if taddr.IP.IsUnspecified() {
   132			return fmt.Sprintf("%s://localhost:%d", scheme, taddr.Port)
   133		}
   134		return fmt.Sprintf("%s://%s", scheme, s.listener.Addr())
   135	}
   136	
   137	func (s *Server) HandleFunc(pattern string, fn func(http.ResponseWriter, *http.Request)) {
   138		s.mux.HandleFunc(pattern, fn)
   139	}
   140	
   141	func (s *Server) Handle(pattern string, handler http.Handler) {
   142		s.mux.Handle(pattern, handler)
   143	}
   144	
   145	func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
   146		var n int64
   147		if s.verbose {
   148			s.mu.Lock()
   149			s.reqs++
   150			n = s.reqs
   151			s.mu.Unlock()
   152			s.printf("Request #%d: %s %s (from %s) ...", n, req.Method, req.RequestURI, req.RemoteAddr)
   153			rw = &trackResponseWriter{ResponseWriter: rw}
   154		}
   155		s.mux.ServeHTTP(rw, req)
   156		if s.verbose {
   157			tw := rw.(*trackResponseWriter)
   158			s.printf("Request #%d: %s %s = code %d, %d bytes", n, req.Method, req.RequestURI, tw.code, tw.resSize)
   159		}
   160	}
   161	
   162	type trackResponseWriter struct {
   163		http.ResponseWriter
   164		code    int
   165		resSize int64
   166	}
   167	
   168	func (tw *trackResponseWriter) WriteHeader(code int) {
   169		tw.code = code
   170		tw.ResponseWriter.WriteHeader(code)
   171	}
   172	
   173	func (tw *trackResponseWriter) Write(p []byte) (int, error) {
   174		if tw.code == 0 {
   175			tw.code = 200
   176		}
   177		tw.resSize += int64(len(p))
   178		return tw.ResponseWriter.Write(p)
   179	}
   180	
   181	// Listen starts listening on the given host:port addr.
   182	//
   183	// If the "host" part is "tailscale", it goes into Tailscale tsnet mode, and the
   184	// "port" is instead an optional state directory path or a bare name for the
   185	// instance name.
   186	func (s *Server) Listen(addr string) error {
   187		if s.listener != nil {
   188			return nil
   189		}
   190	
   191		if addr == "" {
   192			return fmt.Errorf("<host>:<port> needs to be provided to start listening")
   193		}
   194	
   195		preColon, _, _ := strings.Cut(addr, ":")
   196		isTailscale := preColon == "tailscale"
   197	
   198		var err error
   199		if isTailscale {
   200			s.listener, err = s.listenTailscale(addr, s.enableTLS)
   201		} else {
   202			s.listener, err = listen.Listen(addr)
   203		}
   204		if err != nil {
   205			return fmt.Errorf("Failed to listen on %s: %v", addr, err)
   206		}
   207		base := s.ListenURL()
   208		s.printf("Starting to listen on %s\n", base)
   209	
   210		if s.enableTLS {
   211			if s.tsnetServer != nil {
   212				lc, err := s.tsnetServer.LocalClient()
   213				if err != nil {
   214					return err
   215				}
   216				s.SetTLS(TLSSetup{
   217					CertManager: lc.GetCertificate,
   218				})
   219			}
   220			doEnableTLS := func() error {
   221				config := &tls.Config{
   222					Rand:       rand.Reader,
   223					Time:       time.Now,
   224					NextProtos: []string{http2.NextProtoTLS, "http/1.1"},
   225					MinVersion: tls.VersionTLS12,
   226				}
   227				if s.tlsCertFile == "" && s.certManager != nil {
   228					config.GetCertificate = s.certManager
   229					// TODO(mpl): see if we can instead use
   230					// https://godoc.org/golang.org/x/crypto/acme/autocert#Manager.TLSConfig
   231					config.NextProtos = append(config.NextProtos, alpnProto)
   232					s.listener = tls.NewListener(s.listener, config)
   233					return nil
   234				}
   235	
   236				config.Certificates = make([]tls.Certificate, 1)
   237				config.Certificates[0], err = loadX509KeyPair(s.tlsCertFile, s.tlsKeyFile)
   238				if err != nil {
   239					return fmt.Errorf("Failed to load TLS cert: %v", err)
   240				}
   241				s.listener = tls.NewListener(s.listener, config)
   242				return nil
   243			}
   244			if err := doEnableTLS(); err != nil {
   245				return err
   246			}
   247		}
   248	
   249		return nil
   250	}
   251	
   252	func (s *Server) listenTailscale(addr string, withTLS bool) (net.Listener, error) {
   253		preColon, postColon, _ := strings.Cut(addr, ":")
   254		if preColon != "tailscale" {
   255			panic("caller error")
   256		}
   257		var dir string
   258		name := "perkeep"
   259		if postColon != "" {
   260			// Make sure they didn't think it was a port number.
   261			if _, err := strconv.Atoi(postColon); err == nil {
   262				return nil, fmt.Errorf("invalid %q Tailscale listen address; the part after the colon should be a name or directory, not a port number", addr)
   263			}
   264			if strings.Contains(postColon, string(os.PathSeparator)) {
   265				dir = postColon
   266			} else {
   267				name = postColon
   268			}
   269		}
   270		if dir == "" {
   271			confDir, err := os.UserConfigDir()
   272			if err != nil {
   273				return nil, fmt.Errorf("failed to find user config dir: %v", err)
   274			}
   275			dir = filepath.Join(confDir, "tsnet-"+name)
   276		}
   277		if fi, err := os.Stat(dir); os.IsNotExist(err) {
   278			if err := os.MkdirAll(dir, 0700); err != nil {
   279				return nil, fmt.Errorf("error creating Tailscale state directory: %w", err)
   280			}
   281		} else if err != nil {
   282			return nil, fmt.Errorf("error checking Tailscale state directory: %w", err)
   283		} else if !fi.IsDir() {
   284			return nil, fmt.Errorf("Tailscale state directory %q (from listen arg %q) is not a directory", dir, addr)
   285		}
   286		ts := &tsnet.Server{
   287			Dir:      dir, // or empty for automatic
   288			Hostname: name,
   289		}
   290		s.printf("Tailscale tsnet starting for name %q in directory %q ...", name, dir)
   291		if err := ts.Start(); err != nil {
   292			return nil, err
   293		}
   294		s.printf("Tailscale started; waiting Up...")
   295		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
   296		defer cancel()
   297		st, err := ts.Up(ctx)
   298		if err != nil {
   299			return nil, err
   300		}
   301		dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
   302		s.printf("Tailscale up; state=%v, self=%v (%q)", st.BackendState, dnsName, st.Self.TailscaleIPs)
   303		if withTLS {
   304			if len(st.CertDomains) == 0 {
   305				return nil, fmt.Errorf("HTTPS is not enabled for Tailnet %q", st.CurrentTailnet.Name)
   306			}
   307		}
   308		s.tsnetServer = ts
   309		if withTLS {
   310			s.listenURL = "https://" + dnsName
   311			return ts.Listen("tcp", ":443")
   312		}
   313		s.listenURL = "http://" + dnsName
   314		return ts.Listen("tcp", ":80")
   315	}
   316	
   317	func (s *Server) throttleListener() net.Listener {
   318		kBps, _ := strconv.Atoi(os.Getenv("DEV_THROTTLE_KBPS"))
   319		ms, _ := strconv.Atoi(os.Getenv("DEV_THROTTLE_LATENCY_MS"))
   320		if kBps == 0 && ms == 0 {
   321			return s.listener
   322		}
   323		rate := throttle.Rate{
   324			KBps:    kBps,
   325			Latency: time.Duration(ms) * time.Millisecond,
   326		}
   327		return &throttle.Listener{
   328			Listener: s.listener,
   329			Down:     rate,
   330			Up:       rate, // TODO: separate rates?
   331		}
   332	}
   333	
   334	func (s *Server) Serve() {
   335		if err := s.Listen(""); err != nil {
   336			s.fatalf("Listen error: %v", err)
   337		}
   338		go runTestHarnessIntegration(s.listener)
   339	
   340		srv := &http.Server{
   341			Handler: s,
   342		}
   343		// TODO: allow configuring src.ErrorLog (and plumb through to
   344		// Google Cloud Logging when run on GCE, eventually)
   345	
   346		// Setup the NPN NextProto map for HTTP/2 support:
   347		http2.ConfigureServer(srv, &s.H2Server)
   348	
   349		err := srv.Serve(s.throttleListener())
   350		if err != nil {
   351			s.printf("Error in http server: %v\n", err)
   352			os.Exit(1)
   353		}
   354	}
   355	
   356	// Signals the test harness that we've started listening.
   357	// Writes back the address that we randomly selected.
   358	func runTestHarnessIntegration(listener net.Listener) {
   359		addr := os.Getenv("CAMLI_SET_BASE_URL_AND_SEND_ADDR_TO")
   360		if addr == "" {
   361			return
   362		}
   363		c, err := net.Dial("tcp", addr)
   364		if err == nil {
   365			fmt.Fprintf(c, "%s\n", listener.Addr())
   366			c.Close()
   367		}
   368	}
   369	
   370	// loadX509KeyPair is a copy of tls.LoadX509KeyPair but using wkfs.
   371	func loadX509KeyPair(certFile, keyFile string) (cert tls.Certificate, err error) {
   372		certPEMBlock, err := wkfs.ReadFile(certFile)
   373		if err != nil {
   374			return
   375		}
   376		keyPEMBlock, err := wkfs.ReadFile(keyFile)
   377		if err != nil {
   378			return
   379		}
   380		return tls.X509KeyPair(certPEMBlock, keyPEMBlock)
   381	}
Website layout inspired by memcached.
Content by the authors.