// Package http provides a micro to http proxy package http import ( "bytes" "context" "io" "net/http" "net/url" "path/filepath" "strings" "go-micro.dev/v4" "go-micro.dev/v4/errors" "go-micro.dev/v4/server" ) // Router will proxy rpc requests as http POST requests. It is a server.Router type Router struct { // Converts RPC Foo.Bar to /foo/bar Resolver *Resolver // The http backend to call Backend string // first request first bool // rpc ep / http ep mapping eps map[string]string } // Resolver resolves rpc to http. It explicity maps Foo.Bar to /foo/bar type Resolver struct{} var ( // The default backend DefaultBackend = "http://localhost:9090" // The default router DefaultRouter = &Router{} ) // Foo.Bar becomes /foo/bar func (r *Resolver) Resolve(ep string) string { // replace . with / ep = strings.Replace(ep, ".", "/", -1) // lowercase the whole thing ep = strings.ToLower(ep) // prefix with "/" return filepath.Join("/", ep) } // set the nil things func (p *Router) setup() { if p.Resolver == nil { p.Resolver = new(Resolver) } if p.Backend == "" { p.Backend = DefaultBackend } if p.eps == nil { p.eps = map[string]string{} } } // Endpoint returns the http endpoint for an rpc endpoint. // Endpoint("Foo.Bar") returns http://localhost:9090/foo/bar func (p *Router) Endpoint(rpcEp string) (string, error) { p.setup() // get http endpoint ep, ok := p.eps[rpcEp] if !ok { // get default ep = p.Resolver.Resolve(rpcEp) } // already full qualified URL if strings.HasPrefix(ep, "http://") || strings.HasPrefix(ep, "https://") { return ep, nil } // parse into url // full path to call u, err := url.Parse(p.Backend) if err != nil { return "", err } // set path u.Path = filepath.Join(u.Path, ep) // set scheme if len(u.Scheme) == 0 { u.Scheme = "http" } // set host if len(u.Host) == 0 { u.Host = "localhost" } // create ep return u.String(), nil } // RegisterEndpoint registers a http endpoint against an RPC endpoint. // It converts relative paths into backend:endpoint. Anything prefixed // with http:// or https:// will be left as is. // RegisterEndpoint("Foo.Bar", "/foo/bar") // RegisterEndpoint("Greeter.Hello", "/helloworld") // RegisterEndpoint("Greeter.Hello", "http://localhost:8080/") func (p *Router) RegisterEndpoint(rpcEp, httpEp string) error { p.setup() // create ep p.eps[rpcEp] = httpEp return nil } func (p *Router) ProcessMessage(ctx context.Context, msg server.Message) error { return nil } // ServeRequest honours the server.Router interface func (p *Router) ServeRequest(ctx context.Context, req server.Request, rsp server.Response) error { // rudimentary post based streaming for { // get data body, err := req.Read() if err == io.EOF { return nil } if err != nil { return err } var rpcEp string // get rpc endpoint if p.first { p.first = false rpcEp = req.Endpoint() } else { hdr := req.Header() rpcEp = hdr["X-Micro-Endpoint"] } // get http endpoint ep, err := p.Endpoint(rpcEp) if err != nil { return errors.NotFound(req.Service(), err.Error()) } // no stream support currently // TODO: lookup host hreq, err := http.NewRequest("POST", ep, bytes.NewReader(body)) if err != nil { return errors.InternalServerError(req.Service(), err.Error()) } // get the header hdr := req.Header() // set the headers for k, v := range hdr { hreq.Header.Set(k, v) } // make the call hrsp, err := http.DefaultClient.Do(hreq) if err != nil { return errors.InternalServerError(req.Service(), err.Error()) } // read body b, err := io.ReadAll(hrsp.Body) hrsp.Body.Close() if err != nil { return errors.InternalServerError(req.Service(), err.Error()) } // set response headers hdr = map[string]string{} for k, _ := range hrsp.Header { hdr[k] = hrsp.Header.Get(k) } // write the header rsp.WriteHeader(hdr) // write the body err = rsp.Write(b) if err == io.EOF { return nil } if err != nil { return errors.InternalServerError(req.Service(), err.Error()) } } return nil } // NewSingleHostRouter returns a router which sends requests a single http backend // // It is used by setting it in a new micro service to act as a proxy for a http backend. // // Usage: // // Create a new router to the http backend // // r := NewSingleHostRouter("http://localhost:10001") // // // Add additional routes // r.RegisterEndpoint("Hello.World", "/helloworld") // // // Create your new service // service := micro.NewService( // micro.Name("greeter"), // // Set the router // http.WithRouter(r), // ) // // // Run the service // service.Run() func NewSingleHostRouter(url string) *Router { return &Router{ Resolver: new(Resolver), Backend: url, eps: map[string]string{}, } } // NewService returns a new http proxy. It acts as a micro service and proxies to a http backend. // Routes are dynamically set e.g Foo.Bar routes to /foo/bar. The default backend is http://localhost:9090. // Optionally specify the backend endpoint url or the router. Also choose to register specific endpoints. // // Usage: // // service := NewService( // micro.Name("greeter"), // // Sets the default http endpoint // http.WithBackend("http://localhost:10001"), // ) // // Set fixed backend endpoints // // // register an endpoint // http.RegisterEndpoint("Hello.World", "/helloworld") // // service := NewService( // micro.Name("greeter"), // // Set the http endpoint // http.WithBackend("http://localhost:10001"), // ) func NewService(opts ...micro.Option) micro.Service { // prepend router to opts opts = append([]micro.Option{ WithRouter(DefaultRouter), }, opts...) // create the new service return micro.NewService(opts...) } // RegisterEndpoint registers a http endpoint against an RPC endpoint // RegisterEndpoint("Foo.Bar", "/foo/bar") // RegisterEndpoint("Greeter.Hello", "/helloworld") // RegisterEndpoint("Greeter.Hello", "http://localhost:8080/") func RegisterEndpoint(rpcEp string, httpEp string) error { return DefaultRouter.RegisterEndpoint(rpcEp, httpEp) }