1//  Copyright (c) 2017 Couchbase, Inc.
2//  Licensed under the Apache License, Version 2.0 (the "License");
3//  you may not use this file except in compliance with the
4//  License. You may obtain a copy of the License at
5//    http://www.apache.org/licenses/LICENSE-2.0
6//  Unless required by applicable law or agreed to in writing,
7//  software distributed under the License is distributed on an "AS
8//  IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
9//  express or implied. See the License for the specific language
10//  governing permissions and limitations under the License.
11
12package main
13
14import (
15	"crypto/tls"
16	"crypto/x509"
17	"io/ioutil"
18	"net"
19	"net/http"
20	"strconv"
21	"strings"
22	"sync"
23	"sync/atomic"
24	"time"
25
26	"github.com/couchbase/cbauth"
27	"github.com/couchbase/cbft"
28	log "github.com/couchbase/clog"
29
30	"golang.org/x/net/netutil"
31)
32
33type httpsServer struct {
34	server   *http.Server
35	listener net.Listener
36}
37
38// List of active https servers
39var httpsServers []*httpsServer
40var httpsServersMutex sync.Mutex
41
42// AuthType used for HTTPS connections
43var authType string
44
45// Use IPv6
46var ipv6 string
47
48func setupHTTPListenersAndServ(routerInUse http.Handler, bindHTTPList []string,
49	options map[string]string) {
50	http.Handle("/", routerInUse)
51	ipv6 = options["ipv6"]
52
53	anyHostPorts := map[string]bool{}
54	// Bind to 0.0.0.0's (IPv4) or [::]'s (IPv6) first for http listening.
55	for _, bindHTTP := range bindHTTPList {
56		if strings.HasPrefix(bindHTTP, "0.0.0.0:") ||
57			strings.HasPrefix(bindHTTP, "[::]:") {
58			go mainServeHTTP("http", bindHTTP, nil, "", "")
59
60			anyHostPorts[bindHTTP] = true
61		}
62	}
63
64	for i := len(bindHTTPList) - 1; i >= 1; i-- {
65		go mainServeHTTP("http", bindHTTPList[i], anyHostPorts, "", "")
66	}
67
68	authType = options["authType"]
69
70	if authType == "cbauth" {
71		// Registering a TLS refresh callback with cbauth, which
72		// will be responsible for updating https listeners,
73		// whenever ssl certificates or the client cert auth settings
74		// are changed.
75		cbauth.RegisterTLSRefreshCallback(setupHTTPSListeners)
76	} else {
77		setupHTTPSListeners()
78	}
79
80	mainServeHTTP("http", bindHTTPList[0], anyHostPorts, "", "")
81}
82
83// Add to HTTPS Server list serially
84func addToHTTPSServerList(server *http.Server, listener net.Listener) {
85	httpsServersMutex.Lock()
86	entry := &httpsServer{
87		server:   server,
88		listener: listener,
89	}
90	httpsServers = append(httpsServers, entry)
91	httpsServersMutex.Unlock()
92}
93
94// Close all HTTPS Servers and clear HTTPS Server list
95func closeAndClearHTTPSServerList() {
96	httpsServersMutex.Lock()
97	defer httpsServersMutex.Unlock()
98
99	for _, entry := range httpsServers {
100		// Close the listener associated with the server first.
101		entry.listener.Close()
102		// Then Close the server.
103		entry.server.Close()
104	}
105	httpsServers = nil
106}
107
108func setupHTTPSListeners() error {
109	// Close any previously open https servers
110	closeAndClearHTTPSServerList()
111
112	anyHostPorts := map[string]bool{}
113
114	if flags.BindHTTPS != "" {
115		bindHTTPSList := strings.Split(flags.BindHTTPS, ",")
116
117		// Bind to 0.0.0.0's first for https listening.
118		for _, bindHTTPS := range bindHTTPSList {
119			if strings.HasPrefix(bindHTTPS, "0.0.0.0:") ||
120				strings.HasPrefix(bindHTTPS, "[::]:") {
121				go mainServeHTTP("https", bindHTTPS, nil,
122					flags.TLSCertFile, flags.TLSKeyFile)
123
124				anyHostPorts[bindHTTPS] = true
125			}
126		}
127
128		for _, bindHTTPS := range bindHTTPSList {
129			go mainServeHTTP("https", bindHTTPS, anyHostPorts,
130				flags.TLSCertFile, flags.TLSKeyFile)
131		}
132	}
133
134	return nil
135}
136
137// mainServeHTTP starts the http/https servers for cbft.
138// The proto may be "http" or "https".
139func mainServeHTTP(proto, bindHTTP string, anyHostPorts map[string]bool,
140	certFile, keyFile string) {
141	if bindHTTP[0] == ':' && proto == "http" {
142		bindHTTP = "localhost" + bindHTTP
143	}
144
145	bar := "main: ------------------------------------------------------"
146
147	if anyHostPorts != nil && len(bindHTTP) > 0 {
148		// If we've already bound to 0.0.0.0 or [::] on the same port, then
149		// skip this hostPort.
150		portIndex := strings.LastIndex(bindHTTP, ":") + 1
151		if portIndex > 0 && portIndex < len(bindHTTP) {
152			// Possibly valid port available.
153			port := bindHTTP[portIndex:]
154			if _, err := strconv.Atoi(port); err == nil {
155				// Valid port.
156				host := "0.0.0.0"
157				if net.ParseIP(bindHTTP[:portIndex-1]).To4() == nil && // Not an IPv4
158					ipv6 == "true" {
159					host = "[::]"
160				}
161
162				anyHostPort := host + ":" + port
163				if anyHostPorts[anyHostPort] {
164					if anyHostPort != bindHTTP {
165						log.Printf(bar)
166						log.Printf("init_http: web UI / REST API is available"+
167							" (via %v): %s://%s", host, proto, bindHTTP)
168						log.Printf(bar)
169					}
170					return
171				}
172			}
173		} // Else port not found.
174	}
175
176	log.Printf(bar)
177	log.Printf("init_http: web UI / REST API is available: %s://%s", proto, bindHTTP)
178	log.Printf(bar)
179
180	listener, err := net.Listen("tcp", bindHTTP)
181	if err != nil {
182		log.Fatalf("init_http: listen, err: %v", err)
183	}
184	server := &http.Server{Addr: bindHTTP,
185		Handler:      routerInUse,
186		ReadTimeout:  httpReadTimeout,
187		WriteTimeout: httpWriteTimeout}
188
189	if proto == "http" {
190		limitListener := netutil.LimitListener(listener, httpMaxConnections)
191		log.Printf("init_http: Setting up a http limit listener over %q", bindHTTP)
192		atomic.AddUint64(&cbft.TotHTTPLimitListenersOpened, 1)
193		err = server.Serve(limitListener)
194		if err != nil {
195			log.Fatalf("init_http: Serve, err: %v;\n"+
196				"  Please check that your -bindHttp(s) parameter (%q)\n"+
197				"  is correct and available.", err, bindHTTP)
198		}
199		atomic.AddUint64(&cbft.TotHTTPLimitListenersClosed, 1)
200	} else {
201		addToHTTPSServerList(server, listener)
202		// Initialize server.TLSConfig to the listener's TLS Config before calling
203		// server for HTTP/2 support.
204		// See: https://golang.org/pkg/net/http/#Server.Serve
205		config := cloneTLSConfig(server.TLSConfig)
206		if !strSliceContains(config.NextProtos, "http/1.1") {
207			config.NextProtos = append(config.NextProtos, "http/1.1")
208		}
209		if !strSliceContains(config.NextProtos, "h2") {
210			config.NextProtos = append(config.NextProtos, "h2")
211		}
212
213		config.Certificates = make([]tls.Certificate, 1)
214		config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
215		if err != nil {
216			log.Fatalf("init_http: LoadX509KeyPair, err: %v", err)
217		}
218
219		if authType == "cbauth" {
220			// Set MinTLSVersion and CipherSuites to what is provided by
221			// cbauth if authType were cbauth.
222			config.MinVersion = cbauth.MinTLSVersion()
223			config.CipherSuites = cbauth.CipherSuites()
224
225			clientAuthType, er := cbauth.GetClientCertAuthType()
226			if er != nil {
227				log.Fatalf("init_http: GetClientCertAuthType, err: %v", err)
228			}
229
230			if clientAuthType != tls.NoClientCert {
231				caCert, er := ioutil.ReadFile(certFile)
232				if er != nil {
233					log.Fatalf("init_http: ReadFile of cacert, err: %v", err)
234				}
235				caCertPool := x509.NewCertPool()
236				caCertPool.AppendCertsFromPEM(caCert)
237				config.ClientCAs = caCertPool
238				config.ClientAuth = clientAuthType
239			}
240		}
241
242		keepAliveListener := tcpKeepAliveListener{listener.(*net.TCPListener)}
243		limitListener := netutil.LimitListener(keepAliveListener, httpMaxConnections)
244		tlsListener := tls.NewListener(limitListener, config)
245		log.Printf("init_http: Setting up a https limit listener over %q", bindHTTP)
246		atomic.AddUint64(&cbft.TotHTTPSLimitListenersOpened, 1)
247		err = server.Serve(tlsListener)
248		if err != nil {
249			log.Printf("init_http: Serve, err: %v;\n"+
250				" HTTPS listeners closed, likely to be re-initialized, "+
251				" -bindHttp(s) (%q)\n", err, bindHTTP)
252		}
253		atomic.AddUint64(&cbft.TotHTTPSLimitListenersClosed, 1)
254	}
255}
256
257// Helper function to determine if the provided string is already
258// present in the provided array of strings.
259func strSliceContains(ss []string, s string) bool {
260	for _, v := range ss {
261		if v == s {
262			return true
263		}
264	}
265	return false
266}
267
268// cloneTLSConfig returns a clone of the tls.Config.
269func cloneTLSConfig(cfg *tls.Config) *tls.Config {
270	if cfg == nil {
271		return &tls.Config{}
272	}
273	return cfg.Clone()
274}
275
276// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
277// connections. It's used by ListenAndServe and ListenAndServeTLS so
278// dead TCP connections (e.g. closing laptop mid-download) eventually
279// go away.
280type tcpKeepAliveListener struct {
281	*net.TCPListener
282}
283
284func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
285	tc, err := ln.AcceptTCP()
286	if err != nil {
287		return
288	}
289	tc.SetKeepAlive(true)
290	tc.SetKeepAlivePeriod(3 * time.Minute)
291	return tc, nil
292}
293