github.com/anycable/anycable-go@v1.5.1/broadcast/http.go (about)

     1  package broadcast
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"log/slog"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/anycable/anycable-go/server"
    13  	"github.com/anycable/anycable-go/utils"
    14  	"github.com/joomcode/errorx"
    15  )
    16  
    17  const (
    18  	defaultHTTPPath    = "/_broadcast"
    19  	broadcastKeyPhrase = "broadcast-cable"
    20  )
    21  
    22  // HTTPConfig contains HTTP pubsub adapter configuration
    23  type HTTPConfig struct {
    24  	// Port to listen on
    25  	Port int
    26  	// Path for HTTP broadast
    27  	Path string
    28  	// Secret token to authorize requests
    29  	Secret string
    30  	// SecretBase is a secret used to generate a token if none provided
    31  	SecretBase string
    32  	// AddCORSHeaders enables adding CORS headers (so you can perform broadcast requests from the browser)
    33  	// (We mostly need it for Stackblitz)
    34  	AddCORSHeaders bool
    35  	// CORSHosts contains a list of hostnames for CORS (comma-separated)
    36  	CORSHosts string
    37  }
    38  
    39  // NewHTTPConfig builds a new config for HTTP pub/sub
    40  func NewHTTPConfig() HTTPConfig {
    41  	return HTTPConfig{
    42  		Path: defaultHTTPPath,
    43  	}
    44  }
    45  
    46  func (c *HTTPConfig) IsSecured() bool {
    47  	return c.Secret != "" || c.SecretBase != ""
    48  }
    49  
    50  // HTTPBroadcaster represents HTTP broadcaster
    51  type HTTPBroadcaster struct {
    52  	port         int
    53  	path         string
    54  	conf         *HTTPConfig
    55  	authHeader   string
    56  	enableCORS   bool
    57  	allowedHosts []string
    58  	server       *server.HTTPServer
    59  	node         Handler
    60  	log          *slog.Logger
    61  }
    62  
    63  var _ Broadcaster = (*HTTPBroadcaster)(nil)
    64  
    65  // NewHTTPBroadcaster builds a new HTTPSubscriber struct
    66  func NewHTTPBroadcaster(node Handler, config *HTTPConfig, l *slog.Logger) *HTTPBroadcaster {
    67  	return &HTTPBroadcaster{
    68  		node: node,
    69  		log:  l.With("context", "broadcast").With("provider", "http"),
    70  		port: config.Port,
    71  		path: config.Path,
    72  		conf: config,
    73  	}
    74  }
    75  
    76  func (HTTPBroadcaster) IsFanout() bool {
    77  	return false
    78  }
    79  
    80  // Prepare configures the broadcaster to make it ready to accept requests
    81  // (i.e., calculates the authentication token, etc.)
    82  func (s *HTTPBroadcaster) Prepare() error {
    83  	authHeader := ""
    84  
    85  	if s.conf.Secret == "" && s.conf.SecretBase != "" {
    86  		secret, err := utils.NewMessageVerifier(s.conf.SecretBase).Sign([]byte(broadcastKeyPhrase))
    87  
    88  		if err != nil {
    89  			err = errorx.Decorate(err, "failed to auto-generate authentication key for HTTP broadcaster")
    90  			return err
    91  		}
    92  
    93  		s.log.Info("auto-generated authorization secret from the application secret")
    94  		s.conf.Secret = string(secret)
    95  	}
    96  
    97  	if s.conf.Secret != "" {
    98  		authHeader = fmt.Sprintf("Bearer %s", s.conf.Secret)
    99  	}
   100  
   101  	s.authHeader = authHeader
   102  
   103  	if s.conf.AddCORSHeaders {
   104  		s.enableCORS = true
   105  		if s.conf.CORSHosts != "" {
   106  			s.allowedHosts = strings.Split(s.conf.CORSHosts, ",")
   107  		} else {
   108  			s.allowedHosts = []string{}
   109  		}
   110  	}
   111  
   112  	return nil
   113  }
   114  
   115  // Start creates an HTTP server or attaches a handler to the existing one
   116  func (s *HTTPBroadcaster) Start(done chan (error)) error {
   117  	server, err := server.ForPort(strconv.Itoa(s.port))
   118  
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	err = s.Prepare()
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	s.server = server
   129  	s.server.SetupHandler(s.path, http.HandlerFunc(s.Handler))
   130  
   131  	var verifiedVia string
   132  
   133  	if s.authHeader != "" {
   134  		verifiedVia = "authorization required"
   135  	} else {
   136  		verifiedVia = "no authorization"
   137  	}
   138  
   139  	if s.enableCORS {
   140  		verifiedVia += ", CORS enabled"
   141  	}
   142  
   143  	s.log.Info(fmt.Sprintf("Accept broadcast requests at %s%s (%s)", s.server.Address(), s.path, verifiedVia))
   144  
   145  	go func() {
   146  		if err := s.server.StartAndAnnounce("broadcasting HTTP server"); err != nil {
   147  			if !s.server.Stopped() {
   148  				done <- fmt.Errorf("broadcasting HTTP server at %s stopped: %v", s.server.Address(), err)
   149  			}
   150  		}
   151  	}()
   152  
   153  	return nil
   154  }
   155  
   156  // Shutdown stops the HTTP server
   157  func (s *HTTPBroadcaster) Shutdown(ctx context.Context) error {
   158  	if s.server != nil {
   159  		s.server.Shutdown(ctx) //nolint:errcheck
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  // Handler processes HTTP requests
   166  func (s *HTTPBroadcaster) Handler(w http.ResponseWriter, r *http.Request) {
   167  	if s.enableCORS {
   168  		// Write CORS headers
   169  		server.WriteCORSHeaders(w, r, s.allowedHosts)
   170  
   171  		// Respond to preflight requests
   172  		if r.Method == http.MethodOptions {
   173  			w.WriteHeader(http.StatusOK)
   174  			return
   175  		}
   176  	}
   177  
   178  	if r.Method != "POST" {
   179  		s.log.Debug("invalid request method", "method", r.Method)
   180  		w.WriteHeader(422)
   181  		return
   182  	}
   183  
   184  	if s.authHeader != "" {
   185  		if r.Header.Get("Authorization") != s.authHeader {
   186  			w.WriteHeader(401)
   187  			return
   188  		}
   189  	}
   190  
   191  	body, err := io.ReadAll(r.Body)
   192  
   193  	if err != nil {
   194  		s.log.Error("failed to read request body")
   195  		w.WriteHeader(422)
   196  		return
   197  	}
   198  
   199  	s.node.HandleBroadcast(body)
   200  
   201  	w.WriteHeader(201)
   202  }