github.com/wader/devd@v0.0.0-20221031103345-441c7e455249/route.go (about)

     1  package devd
     2  
     3  import (
     4  	"crypto/tls"
     5  	"errors"
     6  	"fmt"
     7  	"html/template"
     8  	"net/http"
     9  	"net/url"
    10  	"time"
    11  
    12  	"github.com/wader/devd/fileserver"
    13  	"github.com/wader/devd/httpctx"
    14  	"github.com/wader/devd/inject"
    15  	"github.com/wader/devd/reverseproxy"
    16  	"github.com/wader/devd/routespec"
    17  )
    18  
    19  // Endpoint is the destination of a Route - either on the filesystem or
    20  // forwarding to another URL
    21  type endpoint interface {
    22  	Handler(prefix string, templates *template.Template, ci inject.CopyInject) httpctx.Handler
    23  	String() string
    24  }
    25  
    26  // An endpoint that forwards to an upstream URL
    27  type forwardEndpoint url.URL
    28  
    29  func (ep forwardEndpoint) Handler(prefix string, templates *template.Template, ci inject.CopyInject) httpctx.Handler {
    30  	u := url.URL(ep)
    31  	rp := reverseproxy.NewSingleHostReverseProxy(&u, ci)
    32  	rp.Transport = &http.Transport{
    33  		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    34  	}
    35  	rp.FlushInterval = 200 * time.Millisecond
    36  	return httpctx.StripPrefix(prefix, rp)
    37  }
    38  
    39  func newForwardEndpoint(path string) (*forwardEndpoint, error) {
    40  	url, err := url.Parse(path)
    41  	if err != nil {
    42  		return nil, fmt.Errorf("Could not parse route URL: %s", err)
    43  	}
    44  	f := forwardEndpoint(*url)
    45  	return &f, nil
    46  }
    47  
    48  func (ep forwardEndpoint) String() string {
    49  	return "forward to " + ep.Scheme + "://" + ep.Host + ep.Path
    50  }
    51  
    52  // An enpoint that serves a filesystem location
    53  type filesystemEndpoint struct {
    54  	Root           string
    55  	notFoundRoutes []routespec.RouteSpec
    56  }
    57  
    58  func newFilesystemEndpoint(path string, notfound []string) (*filesystemEndpoint, error) {
    59  	rparts := []routespec.RouteSpec{}
    60  	for _, p := range notfound {
    61  		rp, err := routespec.ParseRouteSpec(p)
    62  		if err != nil {
    63  			return nil, err
    64  		}
    65  		if rp.IsURL {
    66  			return nil, fmt.Errorf("Not found over-ride target cannot be a URL.")
    67  		}
    68  		rparts = append(rparts, *rp)
    69  	}
    70  	return &filesystemEndpoint{path, rparts}, nil
    71  }
    72  
    73  func (ep filesystemEndpoint) Handler(prefix string, templates *template.Template, ci inject.CopyInject) httpctx.Handler {
    74  	return &fileserver.FileServer{
    75  		Version:        "devd " + Version,
    76  		Root:           http.Dir(ep.Root),
    77  		Inject:         ci,
    78  		Templates:      templates,
    79  		NotFoundRoutes: ep.notFoundRoutes,
    80  		Prefix:         prefix,
    81  	}
    82  }
    83  
    84  func (ep filesystemEndpoint) String() string {
    85  	return "reads files from " + ep.Root
    86  }
    87  
    88  // Route is a mapping from a (host, path) tuple to an endpoint.
    89  type Route struct {
    90  	Host     string
    91  	Path     string
    92  	Endpoint endpoint
    93  }
    94  
    95  // Constructs a new route from a string specifcation. Specifcations are of the
    96  // form ANCHOR=VALUE.
    97  func newRoute(s string, notfound []string) (*Route, error) {
    98  	rp, err := routespec.ParseRouteSpec(s)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	var ep endpoint
   104  
   105  	if rp.IsURL {
   106  		ep, err = newForwardEndpoint(rp.Value)
   107  	} else {
   108  		ep, err = newFilesystemEndpoint(rp.Value, notfound)
   109  	}
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	return &Route{rp.Host, rp.Path, ep}, nil
   114  }
   115  
   116  // MuxMatch produces a match clause suitable for passing to a Mux
   117  func (f Route) MuxMatch() string {
   118  	// Path is guaranteed to start with /
   119  	return f.Host + f.Path
   120  }
   121  
   122  // RouteCollection is a collection of routes
   123  type RouteCollection map[string]Route
   124  
   125  func (f *RouteCollection) String() string {
   126  	return fmt.Sprintf("%v", *f)
   127  }
   128  
   129  // Add a route to the collection
   130  func (f RouteCollection) Add(value string, notfound []string) error {
   131  	s, err := newRoute(value, notfound)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	if _, exists := f[s.MuxMatch()]; exists {
   136  		return errors.New("Route already exists.")
   137  	}
   138  	f[s.MuxMatch()] = *s
   139  	return nil
   140  }