corehttp.go 3.97 KB
Newer Older
1 2
/*
Package corehttp provides utilities for the webui, gateways, and other
tavit ohanian's avatar
tavit ohanian committed
3
high-level HTTP interfaces to DMS3.
4
*/
5 6 7
package corehttp

import (
8
	"context"
9
	"fmt"
10
	"net"
11
	"net/http"
12
	"time"
13

Jakub Sztandera's avatar
Jakub Sztandera committed
14 15
	"github.com/jbenet/goprocess"
	periodicproc "github.com/jbenet/goprocess/periodic"
tavit ohanian's avatar
tavit ohanian committed
16 17 18 19
	core "gitlab.dms3.io/dms3/go-dms3/core"
	logging "gitlab.dms3.io/dms3/go-log"
	ma "gitlab.dms3.io/mf/go-multiaddr"
	manet "gitlab.dms3.io/mf/go-multiaddr/net"
20 21
)

Jeromy's avatar
Jeromy committed
22
var log = logging.Logger("core/server")
23

24 25 26 27
// shutdownTimeout is the timeout after which we'll stop waiting for hung
// commands to return on shutdown.
const shutdownTimeout = 30 * time.Second

28 29 30 31
// ServeOption registers any HTTP handlers it provides on the given mux.
// It returns the mux to expose to future options, which may be a new mux if it
// is interested in mediating requests to future options, or the same mux
// initially passed in if not.
tavit ohanian's avatar
tavit ohanian committed
32
type ServeOption func(*core.Dms3Node, net.Listener, *http.ServeMux) (*http.ServeMux, error)
33

34 35
// makeHandler turns a list of ServeOptions into a http.Handler that implements
// all of the given options, in order.
tavit ohanian's avatar
tavit ohanian committed
36
func makeHandler(n *core.Dms3Node, l net.Listener, options ...ServeOption) (http.Handler, error) {
37 38 39 40
	topMux := http.NewServeMux()
	mux := topMux
	for _, option := range options {
		var err error
41
		mux, err = option(n, l, mux)
42 43 44 45
		if err != nil {
			return nil, err
		}
	}
46 47 48 49 50 51 52 53 54 55 56
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// ServeMux does not support requests with CONNECT method,
		// so we need to handle them separately
		// https://golang.org/src/net/http/request.go#L111
		if r.Method == http.MethodConnect {
			w.WriteHeader(http.StatusOK)
			return
		}
		topMux.ServeHTTP(w, r)
	})
	return handler, nil
57 58
}

59 60 61 62 63 64
// ListenAndServe runs an HTTP server listening at |listeningMultiAddr| with
// the given serve options. The address must be provided in multiaddr format.
//
// TODO intelligently parse address strings in other formats so long as they
// unambiguously map to a valid multiaddr. e.g. for convenience, ":8080" should
// map to "/ip4/0.0.0.0/tcp/8080".
tavit ohanian's avatar
tavit ohanian committed
65
func ListenAndServe(n *core.Dms3Node, listeningMultiAddr string, options ...ServeOption) error {
66 67 68 69
	addr, err := ma.NewMultiaddr(listeningMultiAddr)
	if err != nil {
		return err
	}
70 71

	list, err := manet.Listen(addr)
72 73
	if err != nil {
		return err
74
	}
75

Dimitris Apostolou's avatar
Dimitris Apostolou committed
76
	// we might have listened to /tcp/0 - let's see what we are listing on
77 78 79
	addr = list.Multiaddr()
	fmt.Printf("API server listening on %s\n", addr)

Steven Allen's avatar
Steven Allen committed
80
	return Serve(n, manet.NetListener(list), options...)
81 82
}

83 84
// Serve accepts incoming HTTP connections on the listener and pass them
// to ServeOption handlers.
tavit ohanian's avatar
tavit ohanian committed
85
func Serve(node *core.Dms3Node, lis net.Listener, options ...ServeOption) error {
86 87 88
	// make sure we close this no matter what.
	defer lis.Close()

89
	handler, err := makeHandler(node, lis, options...)
90 91 92 93
	if err != nil {
		return err
	}

94
	addr, err := manet.FromNetAddr(lis.Addr())
95 96 97
	if err != nil {
		return err
	}
98

99
	select {
100
	case <-node.Process.Closing():
101 102 103 104
		return fmt.Errorf("failed to start server, process closing")
	default:
	}

105 106 107 108 109
	server := &http.Server{
		Handler: handler,
	}

	var serverError error
110
	serverProc := node.Process.Go(func(p goprocess.Process) {
111
		serverError = server.Serve(lis)
112
	})
113 114 115

	// wait for server to exit.
	select {
116
	case <-serverProc.Closed():
117
	// if node being closed before server exits, close server
118
	case <-node.Process.Closing():
119
		log.Infof("server at %s terminating...", addr)
120

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
		warnProc := periodicproc.Tick(5*time.Second, func(_ goprocess.Process) {
			log.Infof("waiting for server at %s to terminate...", addr)
		})

		// This timeout shouldn't be necessary if all of our commands
		// are obeying their contexts but we should have *some* timeout.
		ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
		defer cancel()
		err := server.Shutdown(ctx)

		// Should have already closed but we still need to wait for it
		// to set the error.
		<-serverProc.Closed()
		serverError = err

		warnProc.Close()
137 138 139 140 141
	}

	log.Infof("server at %s terminated", addr)
	return serverError
}