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 }