github.com/cortesi/devd@v0.0.0-20200427000907-c1a3bfba27d8/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/cortesi/devd/fileserver" 13 "github.com/cortesi/devd/httpctx" 14 "github.com/cortesi/devd/inject" 15 "github.com/cortesi/devd/reverseproxy" 16 "github.com/cortesi/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 }