kerberos.go 3.8 KB

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