From a067b0b2e84edeb551a5f9962b1799664d21f118 Mon Sep 17 00:00:00 2001 From: Asim Date: Thu, 30 Jun 2016 20:21:57 +0100 Subject: [PATCH] Add http server which implements go-micro.Server --- README.md | 65 ++++++++++++++++++++++ extractor.go | 114 ++++++++++++++++++++++++++++++++++++++ handler.go | 27 +++++++++ http.go | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++ http_test.go | 89 ++++++++++++++++++++++++++++++ options.go | 43 +++++++++++++++ subscriber.go | 28 ++++++++++ 7 files changed, 514 insertions(+) create mode 100644 README.md create mode 100644 extractor.go create mode 100644 handler.go create mode 100644 http.go create mode 100644 http_test.go create mode 100644 options.go create mode 100644 subscriber.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..4464ada --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# HTTP Server + +The HTTP Server is a go-micro.Server. It's a partial implementation which strips out codecs, transports, etc but enables you +to create a HTTP Server that could potentially be used for REST based API services. + +## Usage + +```go +import ( + "net/http" + + "github.com/micro/go-micro/server" + httpServer "github.com/micro/go-plugins/server/http" +) + +func main() { + srv := httpServer.NewServer( + server.Name("helloworld"), + ) + + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`hello world`)) + }) + + hd := srv.NewHandler(mux) + + srv.Handle(hd) + srv.Start() + srv.Register() +} +``` + +Or as part of a service + +```go +import ( + "net/http" + + "github.com/micro/go-micro" + "github.com/micro/go-micro/server" + httpServer "github.com/micro/go-plugins/server/http" +) + +func main() { + srv := httpServer.NewServer( + server.Name("helloworld"), + ) + + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`hello world`)) + }) + + hd := srv.NewHandler(mux) + + srv.Handle(hd) + + service := micro.NewService( + micro.Server(srv), + ) + service.Init() + service.Run() +} +``` diff --git a/extractor.go b/extractor.go new file mode 100644 index 0000000..8701b6b --- /dev/null +++ b/extractor.go @@ -0,0 +1,114 @@ +package http + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/micro/go-micro/registry" + "github.com/micro/go-micro/server" +) + +var ( + privateBlocks []*net.IPNet +) + +func init() { + for _, b := range []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"} { + if _, block, err := net.ParseCIDR(b); err == nil { + privateBlocks = append(privateBlocks, block) + } + } +} + +func extractAddress(addr string) (string, error) { + if len(addr) > 0 && (addr != "0.0.0.0" && addr != "[::]") { + return addr, nil + } + + addrs, err := net.InterfaceAddrs() + if err != nil { + return "", fmt.Errorf("Failed to get interface addresses! Err: %v", err) + } + + var ipAddr []byte + + for _, rawAddr := range addrs { + var ip net.IP + switch addr := rawAddr.(type) { + case *net.IPAddr: + ip = addr.IP + case *net.IPNet: + ip = addr.IP + default: + continue + } + + if ip.To4() == nil { + continue + } + + if !isPrivateIP(ip.String()) { + continue + } + + ipAddr = ip + break + } + + if ipAddr == nil { + return "", fmt.Errorf("No private IP address found, and explicit IP not provided") + } + + return net.IP(ipAddr).String(), nil +} + +func isPrivateIP(ipAddr string) bool { + ip := net.ParseIP(ipAddr) + for _, priv := range privateBlocks { + if priv.Contains(ip) { + return true + } + } + return false +} + +func serviceDef(opts server.Options) *registry.Service { + var advt, host string + var port int + + if len(opts.Advertise) > 0 { + advt = opts.Advertise + } else { + advt = opts.Address + } + + parts := strings.Split(advt, ":") + if len(parts) > 1 { + host = strings.Join(parts[:len(parts)-1], ":") + port, _ = strconv.Atoi(parts[len(parts)-1]) + } else { + host = parts[0] + } + + addr, err := extractAddress(host) + if err != nil { + addr = host + } + + node := ®istry.Node{ + Id: opts.Name + "-" + opts.Id, + Address: addr, + Port: port, + Metadata: opts.Metadata, + } + + node.Metadata["server"] = "http" + + return ®istry.Service{ + Name: opts.Name, + Version: opts.Version, + Nodes: []*registry.Node{node}, + } +} diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..4d971af --- /dev/null +++ b/handler.go @@ -0,0 +1,27 @@ +package http + +import ( + "github.com/micro/go-micro/registry" + "github.com/micro/go-micro/server" +) + +type httpHandler struct { + opts server.HandlerOptions + hd interface{} +} + +func (h *httpHandler) Name() string { + return "handler" +} + +func (h *httpHandler) Handler() interface{} { + return h.hd +} + +func (h *httpHandler) Endpoints() []*registry.Endpoint { + return []*registry.Endpoint{} +} + +func (h *httpHandler) Options() server.HandlerOptions { + return h.opts +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..0d030ab --- /dev/null +++ b/http.go @@ -0,0 +1,148 @@ +// Package http implements a go-micro.Server +package http + +import ( + "errors" + "net" + "net/http" + "sync" + + "github.com/micro/go-micro/registry" + "github.com/micro/go-micro/server" +) + +type httpServer struct { + sync.Mutex + opts server.Options + hd server.Handler + exit chan chan error +} + +func (h *httpServer) Options() server.Options { + h.Lock() + opts := h.opts + h.Unlock() + return opts +} + +func (h *httpServer) Init(opts ...server.Option) error { + h.Lock() + for _, o := range opts { + o(&h.opts) + } + h.Unlock() + return nil +} + +func (h *httpServer) Handle(handler server.Handler) error { + if _, ok := handler.Handler().(http.Handler); !ok { + return errors.New("Handle requires http.Handler") + } + h.Lock() + h.hd = handler + h.Unlock() + return nil +} + +func (h *httpServer) NewHandler(handler interface{}, opts ...server.HandlerOption) server.Handler { + var options server.HandlerOptions + for _, o := range opts { + o(&options) + } + + return &httpHandler{ + opts: options, + hd: handler, + } +} + +func (h *httpServer) NewSubscriber(topic string, handler interface{}, opts ...server.SubscriberOption) server.Subscriber { + var options server.SubscriberOptions + for _, o := range opts { + o(&options) + } + + return &httpSubscriber{ + opts: options, + topic: topic, + hd: handler, + } +} + +func (h *httpServer) Subscribe(s server.Subscriber) error { + return errors.New("subscribe is not supported") +} + +func (h *httpServer) Register() error { + h.Lock() + opts := h.opts + h.Unlock() + + service := serviceDef(opts) + + rOpts := []registry.RegisterOption{ + registry.RegisterTTL(opts.RegisterTTL), + } + + return opts.Registry.Register(service, rOpts...) +} + +func (h *httpServer) Deregister() error { + h.Lock() + opts := h.opts + h.Unlock() + + service := serviceDef(opts) + return opts.Registry.Deregister(service) +} + +func (h *httpServer) Start() error { + h.Lock() + opts := h.opts + hd := h.hd + h.Unlock() + + ln, err := net.Listen("tcp", opts.Address) + if err != nil { + return err + } + + h.Lock() + h.opts.Address = ln.Addr().String() + h.Unlock() + + handler, ok := hd.Handler().(http.Handler) + if !ok { + return errors.New("Server required http.Handler") + } + + go http.Serve(ln, handler) + + go func() { + ch := <-h.exit + ch <- ln.Close() + }() + + return nil +} + +func (h *httpServer) Stop() error { + ch := make(chan error) + h.exit <- ch + return <-ch +} + +func (h *httpServer) String() string { + return "http" +} + +func newServer(opts ...server.Option) server.Server { + return &httpServer{ + opts: newOptions(opts...), + exit: make(chan chan error), + } +} + +func NewServer(opts ...server.Option) server.Server { + return newServer(opts...) +} diff --git a/http_test.go b/http_test.go new file mode 100644 index 0000000..8ca213c --- /dev/null +++ b/http_test.go @@ -0,0 +1,89 @@ +package http + +import ( + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/micro/go-micro/registry" + "github.com/micro/go-micro/registry/mock" + "github.com/micro/go-micro/server" +) + +func TestHTTPServer(t *testing.T) { + reg := mock.NewRegistry() + + // create server + srv := NewServer(server.Registry(reg)) + + // create server mux + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`hello world`)) + }) + + // create handler + hd := srv.NewHandler(mux) + + // register handler + if err := srv.Handle(hd); err != nil { + t.Fatal(err) + } + + // start server + if err := srv.Start(); err != nil { + t.Fatal(err) + } + + // register server + if err := srv.Register(); err != nil { + t.Fatal(err) + } + + // lookup server + service, err := reg.GetService(server.DefaultName) + if err != nil { + t.Fatal(err) + } + + if len(service) != 1 { + t.Fatalf("Expected 1 service got %d: %+v", len(service), service) + } + + if len(service[0].Nodes) != 1 { + t.Fatalf("Expected 1 node got %d: %+v", len(service[0].Nodes), service[0].Nodes) + } + + // make request + rsp, err := http.Get(fmt.Sprintf("http://%s:%d", service[0].Nodes[0].Address, service[0].Nodes[0].Port)) + if err != nil { + t.Fatal(err) + } + defer rsp.Body.Close() + + b, err := ioutil.ReadAll(rsp.Body) + if err != nil { + t.Fatal(err) + } + + if s := string(b); s != "hello world" { + t.Fatalf("Expected response %s, got %s", "hello world", s) + } + + // deregister server + if err := srv.Deregister(); err != nil { + t.Fatal(err) + } + + // try get service + service, err = reg.GetService(server.DefaultName) + if err == nil { + t.Fatal("Expected %v got %+v", registry.ErrNotFound, service) + } + + // stop server + if err := srv.Stop(); err != nil { + t.Fatal(err) + } +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..4557e3b --- /dev/null +++ b/options.go @@ -0,0 +1,43 @@ +package http + +import ( + "github.com/micro/go-micro/codec" + "github.com/micro/go-micro/registry" + "github.com/micro/go-micro/server" + + "golang.org/x/net/context" +) + +func newOptions(opt ...server.Option) server.Options { + opts := server.Options{ + Codecs: make(map[string]codec.NewCodec), + Metadata: map[string]string{}, + Context: context.Background(), + } + + for _, o := range opt { + o(&opts) + } + + if opts.Registry == nil { + opts.Registry = registry.DefaultRegistry + } + + if len(opts.Address) == 0 { + opts.Address = server.DefaultAddress + } + + if len(opts.Name) == 0 { + opts.Name = server.DefaultName + } + + if len(opts.Id) == 0 { + opts.Id = server.DefaultId + } + + if len(opts.Version) == 0 { + opts.Version = server.DefaultVersion + } + + return opts +} diff --git a/subscriber.go b/subscriber.go new file mode 100644 index 0000000..300084b --- /dev/null +++ b/subscriber.go @@ -0,0 +1,28 @@ +package http + +import ( + "github.com/micro/go-micro/registry" + "github.com/micro/go-micro/server" +) + +type httpSubscriber struct { + opts server.SubscriberOptions + topic string + hd interface{} +} + +func (h *httpSubscriber) Topic() string { + return h.topic +} + +func (h *httpSubscriber) Subscriber() interface{} { + return h.hd +} + +func (h *httpSubscriber) Endpoints() []*registry.Endpoint { + return []*registry.Endpoint{} +} + +func (h *httpSubscriber) Options() server.SubscriberOptions { + return h.opts +}