1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
|
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package api
import (
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"net"
"net/http"
"strings"
"time"
ldap "github.com/go-ldap/ldap/v3"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/sync"
"golang.org/x/crypto/bcrypt"
)
var (
sessions = make(map[string]bool)
sessionsMut = sync.NewMutex()
)
func emitLoginAttempt(success bool, username string, evLogger events.Logger) {
evLogger.Log(events.LoginAttempt, map[string]interface{}{
"success": success,
"username": username,
})
}
func basicAuthAndSessionMiddleware(cookieName string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, next http.Handler, evLogger events.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if guiCfg.IsValidAPIKey(r.Header.Get("X-API-Key")) {
next.ServeHTTP(w, r)
return
}
cookie, err := r.Cookie(cookieName)
if err == nil && cookie != nil {
sessionsMut.Lock()
_, ok := sessions[cookie.Value]
sessionsMut.Unlock()
if ok {
next.ServeHTTP(w, r)
return
}
}
l.Debugln("Sessionless HTTP request with authentication; this is expensive.")
error := func() {
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
http.Error(w, "Not Authorized", http.StatusUnauthorized)
}
hdr := r.Header.Get("Authorization")
if !strings.HasPrefix(hdr, "Basic ") {
error()
return
}
hdr = hdr[6:]
bs, err := base64.StdEncoding.DecodeString(hdr)
if err != nil {
error()
return
}
fields := bytes.SplitN(bs, []byte(":"), 2)
if len(fields) != 2 {
error()
return
}
username := string(fields[0])
password := string(fields[1])
authOk := auth(username, password, guiCfg, ldapCfg)
if !authOk {
usernameIso := string(iso88591ToUTF8([]byte(username)))
passwordIso := string(iso88591ToUTF8([]byte(password)))
authOk = auth(usernameIso, passwordIso, guiCfg, ldapCfg)
if authOk {
username = usernameIso
}
}
if !authOk {
emitLoginAttempt(false, username, evLogger)
error()
return
}
sessionid := rand.String(32)
sessionsMut.Lock()
sessions[sessionid] = true
sessionsMut.Unlock()
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: sessionid,
MaxAge: 0,
})
emitLoginAttempt(true, username, evLogger)
next.ServeHTTP(w, r)
})
}
func auth(username string, password string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration) bool {
if guiCfg.AuthMode == config.AuthModeLDAP {
return authLDAP(username, password, ldapCfg)
} else {
return authStatic(username, password, guiCfg.User, guiCfg.Password)
}
}
func authStatic(username string, password string, configUser string, configPassword string) bool {
configPasswordBytes := []byte(configPassword)
passwordBytes := []byte(password)
return bcrypt.CompareHashAndPassword(configPasswordBytes, passwordBytes) == nil && username == configUser
}
func authLDAP(username string, password string, cfg config.LDAPConfiguration) bool {
address := cfg.Address
hostname, _, err := net.SplitHostPort(address)
if err != nil {
hostname = address
}
var connection *ldap.Conn
if cfg.Transport == config.LDAPTransportTLS {
connection, err = ldap.DialTLS("tcp", address, &tls.Config{
ServerName: hostname,
InsecureSkipVerify: cfg.InsecureSkipVerify,
})
} else {
connection, err = ldap.Dial("tcp", address)
}
if err != nil {
l.Warnln("LDAP Dial:", err)
return false
}
if cfg.Transport == config.LDAPTransportStartTLS {
err = connection.StartTLS(&tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify})
if err != nil {
l.Warnln("LDAP Start TLS:", err)
return false
}
}
defer connection.Close()
err = connection.Bind(fmt.Sprintf(cfg.BindDN, username), password)
if err != nil {
l.Warnln("LDAP Bind:", err)
return false
}
if cfg.SearchFilter == "" && cfg.SearchBaseDN == "" {
// We're done here.
return true
}
if cfg.SearchFilter == "" || cfg.SearchBaseDN == "" {
l.Warnln("LDAP configuration: both searchFilter and searchBaseDN must be set, or neither.")
return false
}
// If a search filter and search base is set we do an LDAP search for
// the user. If this matches precisely one user then we are good to go.
// The search filter uses the same %s interpolation as the bind DN.
searchString := fmt.Sprintf(cfg.SearchFilter, username)
const sizeLimit = 2 // we search for up to two users -- we only want to match one, so getting any number >1 is a failure.
const timeLimit = 60 // Search for up to a minute...
searchReq := ldap.NewSearchRequest(cfg.SearchBaseDN, ldap.ScopeWholeSubtree, ldap.DerefFindingBaseObj, sizeLimit, timeLimit, false, searchString, nil, nil)
res, err := connection.Search(searchReq)
if err != nil {
l.Warnln("LDAP Search:", err)
return false
}
if len(res.Entries) != 1 {
l.Infof("Wrong number of LDAP search results, %d != 1", len(res.Entries))
return false
}
return true
}
// Convert an ISO-8859-1 encoded byte string to UTF-8. Works by the
// principle that ISO-8859-1 bytes are equivalent to unicode code points,
// that a rune slice is a list of code points, and that stringifying a slice
// of runes generates UTF-8 in Go.
func iso88591ToUTF8(s []byte) []byte {
runes := make([]rune, len(s))
for i := range s {
runes[i] = rune(s[i])
}
return []byte(string(runes))
}
|