kerberos.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. package kerberos
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "log/slog"
  9. "net/http"
  10. "golang.org/x/sync/errgroup"
  11. "github.com/mk6i/open-oscar-server/config"
  12. "github.com/mk6i/open-oscar-server/wire"
  13. )
  14. type AuthService interface {
  15. KerberosLogin(ctx context.Context, inBody wire.SNAC_0x050C_0x0002_KerberosLoginRequest, advertisedHost string) (wire.SNACMessage, error)
  16. }
  17. func NewKerberosServer(listeners []config.Listener, logger *slog.Logger, authService AuthService) *Server {
  18. servers := make([]*http.Server, 0, len(listeners))
  19. for _, l := range listeners {
  20. if l.KerberosListenAddress == "" {
  21. continue
  22. }
  23. mux := http.NewServeMux()
  24. mux.HandleFunc("POST /", func(writer http.ResponseWriter, request *http.Request) {
  25. postHandler(writer, request, authService, logger, l.BOSAdvertisedHostSSL)
  26. })
  27. servers = append(servers, &http.Server{
  28. Addr: l.KerberosListenAddress,
  29. Handler: mux,
  30. })
  31. }
  32. return &Server{
  33. servers: servers,
  34. logger: logger,
  35. }
  36. }
  37. // Server hosts an HTTP endpoint capable of handling AIM-style Kerberos
  38. // authentication. The messages are structured as SNACs transmitted over HTTP.
  39. type Server struct {
  40. servers []*http.Server
  41. logger *slog.Logger
  42. }
  43. func (s *Server) ListenAndServe() error {
  44. if len(s.servers) == 0 {
  45. s.logger.Debug("no kerberos listeners defined")
  46. return nil
  47. }
  48. ctx, cancel := context.WithCancel(context.Background())
  49. defer cancel()
  50. g, _ := errgroup.WithContext(ctx)
  51. for _, server := range s.servers {
  52. g.Go(func() error {
  53. s.logger.Info("starting server", "addr", server.Addr)
  54. if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
  55. cancel()
  56. return fmt.Errorf("unable to start kerberos server: %w", err)
  57. }
  58. return nil
  59. })
  60. }
  61. return g.Wait()
  62. }
  63. func (s *Server) Shutdown(ctx context.Context) error {
  64. if len(s.servers) > 0 {
  65. for _, srv := range s.servers {
  66. _ = srv.Shutdown(ctx)
  67. }
  68. s.logger.Info("shutdown complete")
  69. }
  70. return nil
  71. }
  72. // postHandler handles AIM-style Kerberos authentication for AIM 6.0+.
  73. func postHandler(w http.ResponseWriter, r *http.Request, authService AuthService, logger *slog.Logger, listenAddress string) {
  74. b, err := io.ReadAll(r.Body)
  75. if err != nil {
  76. http.Error(w, "unable to read HTTP body", http.StatusBadRequest)
  77. return
  78. }
  79. reader := bytes.NewReader(b)
  80. var header wire.SNACFrame
  81. if err := wire.UnmarshalBE(&header, reader); err != nil {
  82. http.Error(w, "unable to read kerberos login SNAC header", http.StatusBadRequest)
  83. return
  84. }
  85. if header.FoodGroup != wire.Kerberos || header.SubGroup != wire.KerberosLoginRequest {
  86. http.Error(w, "unexpected SNAC type", http.StatusBadRequest)
  87. return
  88. }
  89. var body wire.SNAC_0x050C_0x0002_KerberosLoginRequest
  90. if err := wire.UnmarshalBE(&body, reader); err != nil {
  91. http.Error(w, "unable to read kerberos login SNAC body", http.StatusBadRequest)
  92. return
  93. }
  94. response, err := authService.KerberosLogin(r.Context(), body, listenAddress)
  95. if err != nil {
  96. logger.Error("authService.KerberosLogin", "err", err.Error())
  97. http.Error(w, "internal server error", http.StatusInternalServerError)
  98. return
  99. }
  100. logger = logger.With("ip", r.RemoteAddr)
  101. switch v := response.Body.(type) {
  102. case wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse:
  103. logger.InfoContext(r.Context(), "successful kerberos login", "screen_name", v.ClientPrincipal, "redirect_to", listenAddress)
  104. case wire.SNAC_0x050C_0x0004_KerberosLoginErrResponse:
  105. logger.InfoContext(r.Context(), "failed kerberos login", "screen_name", v.ScreenName)
  106. }
  107. w.Header().Set("Content-Type", "application/x-snac")
  108. if err := wire.MarshalBE(response, w); err != nil {
  109. logger.Error("unable to marshal SNAC response", "err", err.Error())
  110. http.Error(w, "internal server error", http.StatusInternalServerError)
  111. return
  112. }
  113. }