gui_auth.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. // Copyright (C) 2014 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at http://mozilla.org/MPL/2.0/.
  6. package main
  7. import (
  8. "bytes"
  9. "encoding/base64"
  10. "net/http"
  11. "strings"
  12. "time"
  13. "github.com/syncthing/syncthing/lib/config"
  14. "github.com/syncthing/syncthing/lib/events"
  15. "github.com/syncthing/syncthing/lib/rand"
  16. "github.com/syncthing/syncthing/lib/sync"
  17. "golang.org/x/crypto/bcrypt"
  18. )
  19. var (
  20. sessions = make(map[string]bool)
  21. sessionsMut = sync.NewMutex()
  22. )
  23. func emitLoginAttempt(success bool, username string) {
  24. events.Default.Log(events.LoginAttempt, map[string]interface{}{
  25. "success": success,
  26. "username": username,
  27. })
  28. }
  29. func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguration, next http.Handler) http.Handler {
  30. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  31. if cfg.IsValidAPIKey(r.Header.Get("X-API-Key")) {
  32. next.ServeHTTP(w, r)
  33. return
  34. }
  35. cookie, err := r.Cookie(cookieName)
  36. if err == nil && cookie != nil {
  37. sessionsMut.Lock()
  38. _, ok := sessions[cookie.Value]
  39. sessionsMut.Unlock()
  40. if ok {
  41. next.ServeHTTP(w, r)
  42. return
  43. }
  44. }
  45. httpl.Debugln("Sessionless HTTP request with authentication; this is expensive.")
  46. error := func() {
  47. time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
  48. w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
  49. http.Error(w, "Not Authorized", http.StatusUnauthorized)
  50. }
  51. hdr := r.Header.Get("Authorization")
  52. if !strings.HasPrefix(hdr, "Basic ") {
  53. error()
  54. return
  55. }
  56. hdr = hdr[6:]
  57. bs, err := base64.StdEncoding.DecodeString(hdr)
  58. if err != nil {
  59. error()
  60. return
  61. }
  62. fields := bytes.SplitN(bs, []byte(":"), 2)
  63. if len(fields) != 2 {
  64. error()
  65. return
  66. }
  67. // Check if the username is correct, assuming it was sent as UTF-8
  68. username := string(fields[0])
  69. if username == cfg.User {
  70. goto usernameOK
  71. }
  72. // ... check it again, converting it from assumed ISO-8859-1 to UTF-8
  73. username = string(iso88591ToUTF8(fields[0]))
  74. if username == cfg.User {
  75. goto usernameOK
  76. }
  77. // Neither of the possible interpretations match the configured username
  78. emitLoginAttempt(false, username)
  79. error()
  80. return
  81. usernameOK:
  82. // Check password as given (assumes UTF-8 encoding)
  83. password := fields[1]
  84. if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), password); err == nil {
  85. goto passwordOK
  86. }
  87. // ... check it again, converting it from assumed ISO-8859-1 to UTF-8
  88. password = iso88591ToUTF8(password)
  89. if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), password); err == nil {
  90. goto passwordOK
  91. }
  92. // Neither of the attempts to verify the password checked out
  93. emitLoginAttempt(false, username)
  94. error()
  95. return
  96. passwordOK:
  97. sessionid := rand.String(32)
  98. sessionsMut.Lock()
  99. sessions[sessionid] = true
  100. sessionsMut.Unlock()
  101. http.SetCookie(w, &http.Cookie{
  102. Name: cookieName,
  103. Value: sessionid,
  104. MaxAge: 0,
  105. })
  106. emitLoginAttempt(true, username)
  107. next.ServeHTTP(w, r)
  108. })
  109. }
  110. // Convert an ISO-8859-1 encoded byte string to UTF-8. Works by the
  111. // principle that ISO-8859-1 bytes are equivalent to unicode code points,
  112. // that a rune slice is a list of code points, and that stringifying a slice
  113. // of runes generates UTF-8 in Go.
  114. func iso88591ToUTF8(s []byte) []byte {
  115. runes := make([]rune, len(s))
  116. for i := range s {
  117. runes[i] = rune(s[i])
  118. }
  119. return []byte(string(runes))
  120. }