mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 14:50:55 +03:00
external auth: add example HTTP server to use as authentication hook
The server authenticate against an LDAP server.
This commit is contained in:
146
examples/ldapauthserver/httpd/auth.go
Normal file
146
examples/ldapauthserver/httpd/auth.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
unixcrypt "github.com/nathanaelle/password/v2"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
authenticationHeader = "WWW-Authenticate"
|
||||
authenticationRealm = "LDAP Auth Server"
|
||||
unauthResponse = "Unauthorized"
|
||||
)
|
||||
|
||||
var (
|
||||
md5CryptPwdPrefixes = []string{"$1$", "$apr1$"}
|
||||
bcryptPwdPrefixes = []string{"$2a$", "$2$", "$2x$", "$2y$", "$2b$"}
|
||||
)
|
||||
|
||||
type httpAuthProvider interface {
|
||||
getHashedPassword(username string) (string, bool)
|
||||
isEnabled() bool
|
||||
}
|
||||
|
||||
type basicAuthProvider struct {
|
||||
Path string
|
||||
Info os.FileInfo
|
||||
Users map[string]string
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func newBasicAuthProvider(authUserFile string) (httpAuthProvider, error) {
|
||||
basicAuthProvider := basicAuthProvider{
|
||||
Path: authUserFile,
|
||||
Info: nil,
|
||||
Users: make(map[string]string),
|
||||
lock: new(sync.RWMutex),
|
||||
}
|
||||
return &basicAuthProvider, basicAuthProvider.loadUsers()
|
||||
}
|
||||
|
||||
func (p *basicAuthProvider) isEnabled() bool {
|
||||
return len(p.Path) > 0
|
||||
}
|
||||
|
||||
func (p *basicAuthProvider) isReloadNeeded(info os.FileInfo) bool {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
return p.Info == nil || p.Info.ModTime() != info.ModTime() || p.Info.Size() != info.Size()
|
||||
}
|
||||
|
||||
func (p *basicAuthProvider) loadUsers() error {
|
||||
if !p.isEnabled() {
|
||||
return nil
|
||||
}
|
||||
info, err := os.Stat(p.Path)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to stat basic auth users file: %v", err)
|
||||
return err
|
||||
}
|
||||
if p.isReloadNeeded(info) {
|
||||
r, err := os.Open(p.Path)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to open basic auth users file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
reader := csv.NewReader(r)
|
||||
reader.Comma = ':'
|
||||
reader.Comment = '#'
|
||||
reader.TrimLeadingSpace = true
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to parse basic auth users file: %v", err)
|
||||
return err
|
||||
}
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
p.Users = make(map[string]string)
|
||||
for _, record := range records {
|
||||
if len(record) == 2 {
|
||||
p.Users[record[0]] = record[1]
|
||||
}
|
||||
}
|
||||
logger.Debug(logSender, "", "number of users loaded for httpd basic auth: %v", len(p.Users))
|
||||
p.Info = info
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *basicAuthProvider) getHashedPassword(username string) (string, bool) {
|
||||
err := p.loadUsers()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
pwd, ok := p.Users[username]
|
||||
return pwd, ok
|
||||
}
|
||||
|
||||
func checkAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !validateCredentials(r) {
|
||||
w.Header().Set(authenticationHeader, fmt.Sprintf("Basic realm=\"%v\"", authenticationRealm))
|
||||
sendAPIResponse(w, r, errors.New(unauthResponse), "", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func validateCredentials(r *http.Request) bool {
|
||||
if !httpAuth.isEnabled() {
|
||||
return true
|
||||
}
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if hashedPwd, ok := httpAuth.getHashedPassword(username); ok {
|
||||
if utils.IsStringPrefixInSlice(hashedPwd, bcryptPwdPrefixes) {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
if utils.IsStringPrefixInSlice(hashedPwd, md5CryptPwdPrefixes) {
|
||||
crypter, ok := unixcrypt.MD5.CrypterFound(hashedPwd)
|
||||
if !ok {
|
||||
err := errors.New("cannot found matching MD5 crypter")
|
||||
logger.Debug(logSender, "", "error comparing password with MD5 crypt hash: %v", err)
|
||||
return false
|
||||
}
|
||||
return crypter.Verify([]byte(password))
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
148
examples/ldapauthserver/httpd/httpd.go
Normal file
148
examples/ldapauthserver/httpd/httpd.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/config"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/utils"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
const (
|
||||
logSender = "httpd"
|
||||
versionPath = "/api/v1/version"
|
||||
checkAuthPath = "/api/v1/check_auth"
|
||||
maxRequestSize = 1 << 18 // 256KB
|
||||
)
|
||||
|
||||
var (
|
||||
ldapConfig config.LDAPConfig
|
||||
httpAuth httpAuthProvider
|
||||
certMgr *certManager
|
||||
rootCAs *x509.CertPool
|
||||
)
|
||||
|
||||
// StartHTTPServer initializes and starts the HTTP Server
|
||||
func StartHTTPServer(configDir string, httpConfig config.HTTPDConfig) error {
|
||||
var err error
|
||||
authUserFile := getConfigPath(httpConfig.AuthUserFile, configDir)
|
||||
httpAuth, err = newBasicAuthProvider(authUserFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.RequestID)
|
||||
router.Use(middleware.RealIP)
|
||||
router.Use(logger.NewStructuredLogger(logger.GetLogger()))
|
||||
router.Use(middleware.Recoverer)
|
||||
|
||||
router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
||||
}))
|
||||
|
||||
router.MethodNotAllowed(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sendAPIResponse(w, r, nil, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}))
|
||||
|
||||
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, utils.GetAppVersion())
|
||||
})
|
||||
|
||||
router.Group(func(router chi.Router) {
|
||||
router.Use(checkAuth)
|
||||
|
||||
router.Post(checkAuthPath, checkSFTPGoUserAuth)
|
||||
})
|
||||
|
||||
ldapConfig = config.GetLDAPConfig()
|
||||
loadCACerts(configDir)
|
||||
|
||||
certificateFile := getConfigPath(httpConfig.CertificateFile, configDir)
|
||||
certificateKeyFile := getConfigPath(httpConfig.CertificateKeyFile, configDir)
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", httpConfig.BindAddress, httpConfig.BindPort),
|
||||
Handler: router,
|
||||
ReadTimeout: 70 * time.Second,
|
||||
WriteTimeout: 70 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxHeaderBytes: 1 << 16, // 64KB
|
||||
}
|
||||
if len(certificateFile) > 0 && len(certificateKeyFile) > 0 {
|
||||
certMgr, err = newCertManager(certificateFile, certificateKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := &tls.Config{
|
||||
GetCertificate: certMgr.GetCertificateFunc(),
|
||||
}
|
||||
httpServer.TLSConfig = config
|
||||
return httpServer.ListenAndServeTLS("", "")
|
||||
}
|
||||
return httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
|
||||
var errorString string
|
||||
if err != nil {
|
||||
errorString = err.Error()
|
||||
}
|
||||
resp := apiResponse{
|
||||
Error: errorString,
|
||||
Message: message,
|
||||
HTTPStatus: code,
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), render.StatusCtxKey, code)
|
||||
render.JSON(w, r.WithContext(ctx), resp)
|
||||
}
|
||||
|
||||
func loadCACerts(configDir string) error {
|
||||
var err error
|
||||
rootCAs, err = x509.SystemCertPool()
|
||||
if err != nil {
|
||||
rootCAs = x509.NewCertPool()
|
||||
}
|
||||
for _, ca := range ldapConfig.CACertificates {
|
||||
caPath := getConfigPath(ca, configDir)
|
||||
certs, err := ioutil.ReadFile(caPath)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "error loading ca cert %#v: %v", caPath, err)
|
||||
return err
|
||||
}
|
||||
if !rootCAs.AppendCertsFromPEM(certs) {
|
||||
logger.Warn(logSender, "", "unable to add ca cert %#v", caPath)
|
||||
} else {
|
||||
logger.Debug(logSender, "", "ca cert %#v added to the trusted certificates", caPath)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadTLSCertificate reloads the TLS certificate and key from the configured paths
|
||||
func ReloadTLSCertificate() {
|
||||
if certMgr != nil {
|
||||
certMgr.loadCertificate()
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigPath(name, configDir string) string {
|
||||
if !utils.IsFileInputValid(name) {
|
||||
return ""
|
||||
}
|
||||
if len(name) > 0 && !filepath.IsAbs(name) {
|
||||
return filepath.Join(configDir, name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
143
examples/ldapauthserver/httpd/ldapauth.go
Normal file
143
examples/ldapauthserver/httpd/ldapauth.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func getSFTPGoUser(entry *ldap.Entry, username string) (SFTPGoUser, error) {
|
||||
var err error
|
||||
var user SFTPGoUser
|
||||
uid := ldapConfig.DefaultUID
|
||||
gid := ldapConfig.DefaultGID
|
||||
status := 1
|
||||
|
||||
if !ldapConfig.ForceDefaultUID {
|
||||
uid, err = strconv.Atoi(entry.GetAttributeValue(ldapConfig.GetUIDNumber()))
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
|
||||
if !ldapConfig.ForceDefaultGID {
|
||||
uid, err = strconv.Atoi(entry.GetAttributeValue(ldapConfig.GetGIDNumber()))
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
|
||||
sftpgoUser := SFTPGoUser{
|
||||
Username: username,
|
||||
HomeDir: entry.GetAttributeValue(ldapConfig.GetHomeDirectory()),
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Status: status,
|
||||
}
|
||||
sftpgoUser.Permissions = make(map[string][]string)
|
||||
sftpgoUser.Permissions["/"] = []string{"*"}
|
||||
return sftpgoUser, nil
|
||||
}
|
||||
|
||||
func checkSFTPGoUserAuth(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
var authReq externalAuthRequest
|
||||
err := render.DecodeJSON(r.Body, &authReq)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "error decoding auth request: %v", err)
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
l, err := ldap.DialURL(ldapConfig.BindURL, ldap.DialWithTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: ldapConfig.InsecureSkipVerify,
|
||||
RootCAs: rootCAs,
|
||||
}))
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "error connecting to the LDAP server: %v", err)
|
||||
sendAPIResponse(w, r, err, "Error connecting to the LDAP server", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
err = l.Bind(ldapConfig.BindUsername, ldapConfig.BindPassword)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "error binding to the LDAP server: %v", err)
|
||||
sendAPIResponse(w, r, err, "Error binding to the LDAP server", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
ldapConfig.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
strings.Replace(ldapConfig.SearchFilter, "%s", authReq.Username, 1),
|
||||
ldapConfig.SearchBaseAttrs,
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := l.Search(searchRequest)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "error searching LDAP user %#v: %v", authReq.Username, err)
|
||||
sendAPIResponse(w, r, err, "Error searching LDAP user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(sr.Entries) != 1 {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "expected one user, found: %v", len(sr.Entries))
|
||||
sendAPIResponse(w, r, nil, fmt.Sprintf("Expected one user, found: %v", len(sr.Entries)), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if len(authReq.PublicKey) > 0 {
|
||||
userKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(authReq.PublicKey))
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "invalid public key for user %#v: %v", authReq.Username, err)
|
||||
sendAPIResponse(w, r, err, "Invalid public key", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
authOk := false
|
||||
for _, k := range sr.Entries[0].GetAttributeValues(ldapConfig.GetPublicKey()) {
|
||||
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
|
||||
// we skip an invalid public key stored inside the LDAP server
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(key.Marshal(), userKey.Marshal()) {
|
||||
authOk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authOk {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "public key authentication failed for user: %#v", authReq.Username)
|
||||
sendAPIResponse(w, r, nil, "public key authentication failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// bind to the LDAP server with the user dn and the given password to check the password
|
||||
userdn := sr.Entries[0].DN
|
||||
err = l.Bind(userdn, authReq.Password)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "password authentication failed for user: %#v", authReq.Username)
|
||||
sendAPIResponse(w, r, nil, "password authentication failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := getSFTPGoUser(sr.Entries[0], authReq.Username)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, middleware.GetReqID(r.Context()), "get user from LDAP entry failed for username %#v: %v",
|
||||
authReq.Username, err)
|
||||
sendAPIResponse(w, r, err, "mapping LDAP user failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, r, user)
|
||||
}
|
||||
109
examples/ldapauthserver/httpd/models.go
Normal file
109
examples/ldapauthserver/httpd/models.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package httpd
|
||||
|
||||
type apiResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
HTTPStatus int `json:"status"`
|
||||
}
|
||||
|
||||
type externalAuthRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
// SFTPGoExtensionsFilter defines filters based on file extensions
|
||||
type SFTPGoExtensionsFilter struct {
|
||||
Path string `json:"path"`
|
||||
AllowedExtensions []string `json:"allowed_extensions,omitempty"`
|
||||
DeniedExtensions []string `json:"denied_extensions,omitempty"`
|
||||
}
|
||||
|
||||
// SFTPGoUserFilters defines additional restrictions for an SFTPGo user
|
||||
type SFTPGoUserFilters struct {
|
||||
AllowedIP []string `json:"allowed_ip,omitempty"`
|
||||
DeniedIP []string `json:"denied_ip,omitempty"`
|
||||
DeniedLoginMethods []string `json:"denied_login_methods,omitempty"`
|
||||
FileExtensions []SFTPGoExtensionsFilter `json:"file_extensions,omitempty"`
|
||||
}
|
||||
|
||||
// S3FsConfig defines the configuration for S3 based filesystem
|
||||
type S3FsConfig struct {
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
KeyPrefix string `json:"key_prefix,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
StorageClass string `json:"storage_class,omitempty"`
|
||||
UploadPartSize int64 `json:"upload_part_size,omitempty"`
|
||||
UploadConcurrency int `json:"upload_concurrency,omitempty"`
|
||||
}
|
||||
|
||||
// GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
|
||||
type GCSFsConfig struct {
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
KeyPrefix string `json:"key_prefix,omitempty"`
|
||||
Credentials string `json:"credentials,omitempty"`
|
||||
AutomaticCredentials int `json:"automatic_credentials,omitempty"`
|
||||
StorageClass string `json:"storage_class,omitempty"`
|
||||
}
|
||||
|
||||
// SFTPGoFilesystem defines cloud storage filesystem details
|
||||
type SFTPGoFilesystem struct {
|
||||
// 0 local filesystem, 1 Amazon S3 compatible, 2 Google Cloud Storage
|
||||
Provider int `json:"provider"`
|
||||
S3Config S3FsConfig `json:"s3config,omitempty"`
|
||||
GCSConfig GCSFsConfig `json:"gcsconfig,omitempty"`
|
||||
}
|
||||
|
||||
type virtualFolder struct {
|
||||
VirtualPath string `json:"virtual_path"`
|
||||
MappedPath string `json:"mapped_path"`
|
||||
}
|
||||
|
||||
// SFTPGoUser defines an SFTPGo user
|
||||
type SFTPGoUser struct {
|
||||
// Database unique identifier
|
||||
ID int64 `json:"id"`
|
||||
// 1 enabled, 0 disabled (login is not allowed)
|
||||
Status int `json:"status"`
|
||||
// Username
|
||||
Username string `json:"username"`
|
||||
// Account expiration date as unix timestamp in milliseconds. An expired account cannot login.
|
||||
// 0 means no expiration
|
||||
ExpirationDate int64 `json:"expiration_date"`
|
||||
Password string `json:"password,omitempty"`
|
||||
PublicKeys []string `json:"public_keys,omitempty"`
|
||||
HomeDir string `json:"home_dir"`
|
||||
// Mapping between virtual paths and filesystem paths outside the home directory. Supported for local filesystem only
|
||||
VirtualFolders []virtualFolder `json:"virtual_folders,omitempty"`
|
||||
// If sftpgo runs as root system user then the created files and directories will be assigned to this system UID
|
||||
UID int `json:"uid"`
|
||||
// If sftpgo runs as root system user then the created files and directories will be assigned to this system GID
|
||||
GID int `json:"gid"`
|
||||
// Maximum concurrent sessions. 0 means unlimited
|
||||
MaxSessions int `json:"max_sessions"`
|
||||
// Maximum size allowed as bytes. 0 means unlimited
|
||||
QuotaSize int64 `json:"quota_size"`
|
||||
// Maximum number of files allowed. 0 means unlimited
|
||||
QuotaFiles int `json:"quota_files"`
|
||||
// List of the granted permissions
|
||||
Permissions map[string][]string `json:"permissions"`
|
||||
// Used quota as bytes
|
||||
UsedQuotaSize int64 `json:"used_quota_size"`
|
||||
// Used quota as number of files
|
||||
UsedQuotaFiles int `json:"used_quota_files"`
|
||||
// Last quota update as unix timestamp in milliseconds
|
||||
LastQuotaUpdate int64 `json:"last_quota_update"`
|
||||
// Maximum upload bandwidth as KB/s, 0 means unlimited
|
||||
UploadBandwidth int64 `json:"upload_bandwidth"`
|
||||
// Maximum download bandwidth as KB/s, 0 means unlimited
|
||||
DownloadBandwidth int64 `json:"download_bandwidth"`
|
||||
// Last login as unix timestamp in milliseconds
|
||||
LastLogin int64 `json:"last_login"`
|
||||
// Additional restrictions
|
||||
Filters SFTPGoUserFilters `json:"filters"`
|
||||
// Filesystem configuration details
|
||||
FsConfig SFTPGoFilesystem `json:"filesystem"`
|
||||
}
|
||||
50
examples/ldapauthserver/httpd/tlsutils.go
Normal file
50
examples/ldapauthserver/httpd/tlsutils.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package httpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
|
||||
"github.com/drakkan/sftpgo/ldapauthserver/logger"
|
||||
)
|
||||
|
||||
type certManager struct {
|
||||
cert *tls.Certificate
|
||||
certPath string
|
||||
keyPath string
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *certManager) loadCertificate() error {
|
||||
newCert, err := tls.LoadX509KeyPair(m.certPath, m.keyPath)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "unable to load https certificate: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Debug(logSender, "", "https certificate successfully loaded")
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
m.cert = &newCert
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *certManager) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
return m.cert, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newCertManager(certificateFile, certificateKeyFile string) (*certManager, error) {
|
||||
manager := &certManager{
|
||||
cert: nil,
|
||||
certPath: certificateFile,
|
||||
keyPath: certificateKeyFile,
|
||||
lock: new(sync.RWMutex),
|
||||
}
|
||||
err := manager.loadCertificate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return manager, nil
|
||||
}
|
||||
Reference in New Issue
Block a user