REST API: add a method to get the status of the services

added a status page to the built-in web admin
This commit is contained in:
Nicola Murino 2020-12-08 11:18:34 +01:00
parent 6977a4a18b
commit 50982229e1
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
22 changed files with 424 additions and 71 deletions

View file

@ -152,6 +152,13 @@ type UserActions struct {
Hook string `json:"hook" mapstructure:"hook"`
}
// ProviderStatus defines the provider status
type ProviderStatus struct {
Driver string `json:"driver"`
IsActive bool `json:"is_active"`
Error string `json:"error"`
}
// Config provider configuration
type Config struct {
// Driver name, must be one of the SupportedProviders
@ -775,8 +782,18 @@ func ParseDumpData(data []byte) (BackupData, error) {
}
// GetProviderStatus returns an error if the provider is not available
func GetProviderStatus() error {
return provider.checkAvailability()
func GetProviderStatus() ProviderStatus {
err := provider.checkAvailability()
status := ProviderStatus{
Driver: config.Driver,
}
if err == nil {
status.IsActive = true
} else {
status.IsActive = false
status.Error = err.Error()
}
return status
}
// Close releases all provider resources.

View file

@ -2,6 +2,7 @@
package ftpd
import (
"fmt"
"path/filepath"
ftpserver "github.com/fclairamb/ftpserverlib"
@ -26,6 +27,14 @@ type PortRange struct {
End int `json:"end" mapstructure:"end"`
}
// ServiceStatus defines the service status
type ServiceStatus struct {
IsActive bool `json:"is_active"`
Address string `json:"address"`
PassivePortRange PortRange `json:"passive_port_range"`
FTPES string `json:"ftpes"`
}
// Configuration defines the configuration for the ftp server
type Configuration struct {
// The port used for serving FTP requests
@ -61,6 +70,19 @@ func (c *Configuration) Initialize(configDir string) error {
if err != nil {
return err
}
server.status = ServiceStatus{
IsActive: true,
Address: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort),
PassivePortRange: c.PassivePortRange,
FTPES: "Disabled",
}
if c.CertificateFile != "" && c.CertificateKeyFile != "" {
if c.TLSMode == 1 {
server.status.FTPES = "Required"
} else {
server.status.FTPES = "Enabled"
}
}
ftpServer := ftpserver.NewFtpServer(server)
return ftpServer.ListenAndServe()
}
@ -73,6 +95,14 @@ func ReloadTLSCertificate() error {
return nil
}
// GetStatus returns the server status
func GetStatus() ServiceStatus {
if server == nil {
return ServiceStatus{}
}
return server.status
}
func getConfigPath(name, configDir string) string {
if !utils.IsFileInputValid(name) {
return ""

View file

@ -155,6 +155,12 @@ func TestMain(m *testing.M) {
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
status := ftpd.GetStatus()
if status.IsActive {
logger.ErrorToConsole("ftpd is already active")
os.Exit(1)
}
go func() {
logger.Debug(logSender, "", "initializing FTP server with config %+v", ftpdConf)
if err := ftpdConf.Initialize(configDir); err != nil {
@ -185,6 +191,18 @@ func TestMain(m *testing.M) {
os.Exit(exitCode)
}
func TestInitialization(t *testing.T) {
ftpdConf := config.GetFTPDConfig()
ftpdConf.BindPort = 2121
ftpdConf.CertificateFile = filepath.Join(os.TempDir(), "test_ftpd.crt")
ftpdConf.CertificateKeyFile = filepath.Join(os.TempDir(), "test_ftpd.key")
ftpdConf.TLSMode = 1
err := ftpdConf.Initialize(configDir)
assert.Error(t, err)
status := ftpd.GetStatus()
assert.True(t, status.IsActive)
}
func TestBasicFTPHandling(t *testing.T) {
u := getTestUser()
u.QuotaSize = 6553600

View file

@ -24,6 +24,7 @@ type Server struct {
certMgr *common.CertManager
initialMsg string
statusBanner string
status ServiceStatus
}
// NewServer returns a new FTP server driver
@ -37,7 +38,7 @@ func NewServer(config *Configuration, configDir string) (*Server, error) {
}
certificateFile := getConfigPath(config.CertificateFile, configDir)
certificateKeyFile := getConfigPath(config.CertificateKeyFile, configDir)
if len(certificateFile) > 0 && len(certificateKeyFile) > 0 {
if certificateFile != "" && certificateKeyFile != "" {
server.certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, logSender)
if err != nil {
return server, err

View file

@ -428,17 +428,17 @@ func GetVersion(expectedStatusCode int) (version.Info, []byte, error) {
return appVersion, body, err
}
// GetProviderStatus returns provider status
func GetProviderStatus(expectedStatusCode int) (map[string]interface{}, []byte, error) {
var response map[string]interface{}
// GetStatus returns the server status
func GetStatus(expectedStatusCode int) (ServicesStatus, []byte, error) {
var response ServicesStatus
var body []byte
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(providerStatusPath), nil, "")
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(serverStatusPath), nil, "")
if err != nil {
return response, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if err == nil && (expectedStatusCode == http.StatusOK || expectedStatusCode == http.StatusInternalServerError) {
if err == nil && (expectedStatusCode == http.StatusOK) {
err = render.DecodeJSON(resp.Body, &response)
} else {
body, _ = getResponseBody(resp)

View file

@ -16,8 +16,12 @@ import (
"github.com/go-chi/chi"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/ftpd"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/webdavd"
)
const (
@ -29,7 +33,7 @@ const (
userPath = "/api/v1/user"
versionPath = "/api/v1/version"
folderPath = "/api/v1/folder"
providerStatusPath = "/api/v1/providerstatus"
serverStatusPath = "/api/v1/status"
dumpDataPath = "/api/v1/dumpdata"
loadDataPath = "/api/v1/loaddata"
updateUsedQuotaPath = "/api/v1/quota_update"
@ -42,6 +46,7 @@ const (
webConnectionsPath = "/web/connections"
webFoldersPath = "/web/folders"
webFolderPath = "/web/folder"
webStatusPath = "/web/status"
webStaticFilesPath = "/static"
// MaxRestoreSize defines the max size for the loaddata input file
MaxRestoreSize = 10485760 // 10 MB
@ -55,6 +60,14 @@ var (
certMgr *common.CertManager
)
// ServicesStatus keep the state of the running services
type ServicesStatus struct {
SSH sftpd.ServiceStatus `json:"ssh"`
FTP ftpd.ServiceStatus `json:"ftp"`
WebDAV webdavd.ServiceStatus `json:"webdav"`
DataProvider dataprovider.ProviderStatus `json:"data_provider"`
}
// Conf httpd daemon configuration
type Conf struct {
// The port used for serving HTTP requests. 0 disable the HTTP server. Default: 8080
@ -155,3 +168,13 @@ func getConfigPath(name, configDir string) string {
}
return name
}
func getServicesStatus() ServicesStatus {
status := ServicesStatus{
SSH: sftpd.GetStatus(),
FTP: ftpd.GetStatus(),
WebDAV: webdavd.GetStatus(),
DataProvider: dataprovider.GetProviderStatus(),
}
return status
}

View file

@ -45,6 +45,7 @@ const (
userPath = "/api/v1/user"
folderPath = "/api/v1/folder"
activeConnectionsPath = "/api/v1/connection"
serverStatusPath = "/api/v1/status"
quotaScanPath = "/api/v1/quota_scan"
quotaScanVFolderPath = "/api/v1/folder_quota_scan"
updateUsedQuotaPath = "/api/v1/quota_update"
@ -58,6 +59,7 @@ const (
webFoldersPath = "/web/folders"
webFolderPath = "/web/folder"
webConnectionsPath = "/web/connections"
webStatusPath = "/web/status"
configDir = ".."
httpsCert = `-----BEGIN CERTIFICATE-----
MIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw
@ -1763,10 +1765,10 @@ func TestGetVersion(t *testing.T) {
assert.Error(t, err, "get version request must succeed, we requested to check a wrong status code")
}
func TestGetProviderStatus(t *testing.T) {
_, _, err := httpd.GetProviderStatus(http.StatusOK)
func TestGetStatus(t *testing.T) {
_, _, err := httpd.GetStatus(http.StatusOK)
assert.NoError(t, err)
_, _, err = httpd.GetProviderStatus(http.StatusBadRequest)
_, _, err = httpd.GetStatus(http.StatusBadRequest)
assert.Error(t, err, "get provider status request must succeed, we requested to check a wrong status code")
}
@ -1905,8 +1907,10 @@ func TestProviderErrors(t *testing.T) {
assert.NoError(t, err)
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: "apath"}, http.StatusInternalServerError)
assert.NoError(t, err)
_, _, err = httpd.GetProviderStatus(http.StatusInternalServerError)
assert.NoError(t, err)
status, _, err := httpd.GetStatus(http.StatusOK)
if assert.NoError(t, err) {
assert.False(t, status.DataProvider.IsActive)
}
_, _, err = httpd.Dumpdata("backup.json", "", http.StatusInternalServerError)
assert.NoError(t, err)
_, _, err = httpd.GetFolders(0, 0, "", http.StatusInternalServerError)
@ -2779,6 +2783,12 @@ func TestGetConnectionsMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestGetStatusMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, serverStatusPath, nil)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestDeleteActiveConnectionMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodDelete, activeConnectionsPath+"/connectionID", nil)
rr := executeRequest(req)
@ -3713,6 +3723,12 @@ func TestGetWebConnectionsMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestGetWebStatusMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, webStatusPath, nil)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestStaticFilesMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/static/favicon.ico", nil)
rr := executeRequest(req)

View file

@ -549,7 +549,7 @@ func TestApiCallToNotListeningServer(t *testing.T) {
assert.Error(t, err)
_, _, err = GetVersion(http.StatusOK)
assert.Error(t, err)
_, _, err = GetProviderStatus(http.StatusOK)
_, _, err = GetStatus(http.StatusOK)
assert.Error(t, err)
_, _, err = Dumpdata("backup.json", "0", http.StatusOK)
assert.Error(t, err)

View file

@ -9,7 +9,6 @@ import (
"github.com/go-chi/render"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/metrics"
"github.com/drakkan/sftpgo/version"
@ -66,13 +65,8 @@ func initializeRouter(staticFilesPath string, enableProfiler, enableWebAdmin boo
render.JSON(w, r, version.Get())
})
router.Get(providerStatusPath, func(w http.ResponseWriter, r *http.Request) {
err := dataprovider.GetProviderStatus()
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
} else {
sendAPIResponse(w, r, err, "Alive", http.StatusOK)
}
router.Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, getServicesStatus())
})
router.Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
@ -106,6 +100,7 @@ func initializeRouter(staticFilesPath string, enableProfiler, enableWebAdmin boo
router.Get(webFoldersPath, handleWebGetFolders)
router.Get(webFolderPath, handleWebAddFolderGet)
router.Post(webFolderPath, handleWebAddFolderPost)
router.Get(webStatusPath, handleWebGetStatus)
}
})

View file

@ -1,8 +1,8 @@
openapi: 3.0.3
info:
title: SFTPGo
description: 'SFTPGo REST API'
version: 2.2.0
description: SFTPGo REST API
version: 2.2.1
servers:
- url: /api/v1
@ -29,7 +29,7 @@ paths:
/version:
get:
tags:
- version
- maintenance
summary: Get version details
operationId: get_version
responses:
@ -47,29 +47,6 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/providerstatus:
get:
tags:
- providerstatus
summary: Get data provider status
operationId: get_provider_status
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
message: "Alive"
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/connection:
get:
tags:
@ -667,12 +644,35 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/status:
get:
tags:
- maintenance
summary: Retrieve the status of the active services
operationId: get_status
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ServicesStatus'
400:
$ref: '#/components/responses/BadRequest'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/dumpdata:
get:
tags:
- maintenance
summary: Backup SFTPGo data serializing them as JSON
description: The backup is saved to a local file to avoid to expose users hashed passwords over the network. The output of dumpdata can be used as input for loaddata
summary: Backup SFTPGo data as data provider independent JSON
description: The backup is saved to a local file to avoid to expose sensitive data over the network. The output of dumpdata can be used as input for loaddata
operationId: dumpdata
parameters:
- in: query
@ -1339,6 +1339,83 @@ components:
type: integer
format: int64
description: scan start time as unix timestamp in milliseconds
SSHHostKey:
type: object
properties:
path:
type: string
fingerprint:
type: string
BaseServiceStatus:
type: object
properties:
is_active:
type: boolean
address:
type: string
description: TCP address the server listen on in the form "host:port"
SSHServiceStatus:
allOf:
- $ref: '#/components/schemas/BaseServiceStatus'
- type: object
properties:
host_keys:
type: array
items:
$ref: '#/components/schemas/SSHHostKey'
ssh_commands:
type: string
description: accepted SSH commands comma separated
FTPPassivePortRange:
type: object
properties:
start:
type: integer
end:
type: integer
FTPServiceStatus:
allOf:
- $ref: '#/components/schemas/BaseServiceStatus'
- type: object
properties:
passive_port_range:
$ref: '#/components/schemas/FTPPassivePortRange'
ftpes:
type: string
enum:
- Disabled
- Enabled
- Required
WebDAVServiceStatus:
allOf:
- $ref: '#/components/schemas/BaseServiceStatus'
- type: object
properties:
protocol:
type: string
enum:
- HTTP
- HTTPS
DataProviderStatus:
type: object
properties:
is_active:
type: boolean
driver:
type: string
error:
type: string
ServicesStatus:
type: object
properties:
ssh:
$ref: '#/components/schemas/SSHServiceStatus'
ftp:
$ref: '#/components/schemas/FTPServiceStatus'
webdav:
$ref: '#/components/schemas/WebDAVServiceStatus'
data_provider:
$ref: '#/components/schemas/DataProviderStatus'
ApiResponse:
type: object
properties:

View file

@ -30,8 +30,10 @@ const (
templateFolders = "folders.html"
templateFolder = "folder.html"
templateMessage = "message.html"
templateStatus = "status.html"
pageUsersTitle = "Users"
pageConnectionsTitle = "Connections"
pageStatusTitle = "Status"
pageFoldersTitle = "Folders"
page400Title = "Bad request"
page404Title = "Not found"
@ -60,9 +62,11 @@ type basePage struct {
FolderURL string
APIFoldersURL string
APIFolderQuotaScanURL string
StatusURL string
UsersTitle string
ConnectionsTitle string
FoldersTitle string
StatusTitle string
Version string
}
@ -81,6 +85,11 @@ type connectionsPage struct {
Connections []common.ConnectionStatus
}
type statusPage struct {
basePage
Status ServicesStatus
}
type userPage struct {
basePage
User dataprovider.User
@ -131,12 +140,17 @@ func loadTemplates(templatesPath string) {
filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateFolder),
}
statusPath := []string{
filepath.Join(templatesPath, templateBase),
filepath.Join(templatesPath, templateStatus),
}
usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...))
userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...))
connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...))
messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...))
foldersTmpl := utils.LoadTemplate(template.ParseFiles(foldersPath...))
folderTmpl := utils.LoadTemplate(template.ParseFiles(folderPath...))
statusTmpl := utils.LoadTemplate(template.ParseFiles(statusPath...))
templates[templateUsers] = usersTmpl
templates[templateUser] = userTmpl
@ -144,6 +158,7 @@ func loadTemplates(templatesPath string) {
templates[templateMessage] = messageTmpl
templates[templateFolders] = foldersTmpl
templates[templateFolder] = folderTmpl
templates[templateStatus] = statusTmpl
}
func getBasePageData(title, currentURL string) basePage {
@ -160,9 +175,11 @@ func getBasePageData(title, currentURL string) basePage {
APIFoldersURL: folderPath,
APIFolderQuotaScanURL: quotaScanVFolderPath,
ConnectionsURL: webConnectionsPath,
StatusURL: webStatusPath,
UsersTitle: pageUsersTitle,
ConnectionsTitle: pageConnectionsTitle,
FoldersTitle: pageFoldersTitle,
StatusTitle: pageStatusTitle,
Version: version.GetAsString(),
}
}
@ -725,6 +742,14 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
}
}
func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
data := statusPage{
basePage: getBasePageData(pageStatusTitle, webStatusPath),
Status: getServicesStatus(),
}
renderTemplate(w, templateStatus, data)
}
func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
connectionStats := common.Connections.GetStats()
data := connectionsPage{

View file

@ -166,6 +166,7 @@ func (c *Configuration) Initialize(configDir string) error {
}
if err := c.checkAndLoadHostKeys(configDir, serverConfig); err != nil {
serviceStatus.HostKeys = nil
return err
}
@ -180,7 +181,8 @@ func (c *Configuration) Initialize(configDir string) error {
c.configureLoginBanner(serverConfig, configDir)
c.checkSSHCommands()
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort))
addr := fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort)
listener, err := net.Listen("tcp", addr)
if err != nil {
logger.Warn(logSender, "", "error starting listener on address %s:%d: %v", c.BindAddress, c.BindPort, err)
return err
@ -191,6 +193,9 @@ func (c *Configuration) Initialize(configDir string) error {
return err
}
logger.Info(logSender, "", "server listener registered address: %v", listener.Addr().String())
serviceStatus.Address = addr
serviceStatus.IsActive = true
serviceStatus.SSHCommands = strings.Join(c.EnabledSSHCommands, ", ")
for {
var conn net.Conn
@ -563,8 +568,8 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
if err := c.checkHostKeyAutoGeneration(configDir); err != nil {
return err
}
for _, k := range c.HostKeys {
hostKey := k
serviceStatus.HostKeys = nil
for _, hostKey := range c.HostKeys {
if !utils.IsFileInputValid(hostKey) {
logger.Warn(logSender, "", "unable to load invalid host key %#v", hostKey)
logger.WarnToConsole("unable to load invalid host key %#v", hostKey)
@ -584,8 +589,13 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
if err != nil {
return err
}
k := HostKey{
Path: hostKey,
Fingerprint: ssh.FingerprintSHA256(private.PublicKey()),
}
serviceStatus.HostKeys = append(serviceStatus.HostKeys, k)
logger.Info(logSender, "", "Host key %#v loaded, type %#v, fingerprint %#v", hostKey,
private.PublicKey().Type(), ssh.FingerprintSHA256(private.PublicKey()))
private.PublicKey().Type(), k.Fingerprint)
// Add private key to the server configuration.
serverConfig.AddHostKey(private)

View file

@ -18,6 +18,7 @@ var (
defaultSSHCommands = []string{"md5sum", "sha1sum", "cd", "pwd", "scp"}
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
systemCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"}
serviceStatus ServiceStatus
)
type sshSubsystemExitStatus struct {
@ -28,6 +29,25 @@ type sshSubsystemExecMsg struct {
Command string
}
// HostKey defines the details for a used host key
type HostKey struct {
Path string `json:"path"`
Fingerprint string `json:"fingerprint"`
}
// ServiceStatus defines the service status
type ServiceStatus struct {
IsActive bool `json:"is_active"`
Address string `json:"address"`
SSHCommands string `json:"ssh_commands"`
HostKeys []HostKey `json:"host_keys"`
}
// GetStatus returns the server status
func GetStatus() ServiceStatus {
return serviceStatus
}
// GetDefaultSSHCommands returns the SSH commands enabled as default
func GetDefaultSSHCommands() []string {
result := make([]string, len(defaultSSHCommands))

View file

@ -43,6 +43,7 @@ import (
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/vfs"
)
@ -367,6 +368,8 @@ func TestBasicSFTPHandling(t *testing.T) {
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
status := sftpd.GetStatus()
assert.True(t, status.IsActive)
}
func TestOpenReadWrite(t *testing.T) {

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/></svg>

After

Width:  |  Height:  |  Size: 659 B

View file

@ -71,6 +71,12 @@
<span>{{.ConnectionsTitle}}</span></a>
</li>
<li class="nav-item {{if eq .CurrentURL .StatusURL}}active{{end}}">
<a class="nav-link" href="{{.StatusURL}}">
<i class="fas fa-info-circle"></i>
<span>{{.StatusTitle}}</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">

View file

@ -46,7 +46,7 @@
</div>
</div>
{{else}}
<div class="card mb-4 border-left-success">
<div class="card mb-4 border-left-info">
<div class="card-body">No user connected</div>
</div>
{{end}}

73
templates/status.html Normal file
View file

@ -0,0 +1,73 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
<div class="card mb-4 {{ if .Status.SSH.IsActive}}border-left-success{{else}}border-left-info{{end}}">
<div class="card-body">
<h5 class="card-title">SFTP/SSH server</h5>
<p class="card-text">
Status: {{ if .Status.SSH.IsActive}}"Started"{{else}}"Stopped"{{end}}
{{if .Status.SSH.IsActive}}
<br>
Address: "{{.Status.SSH.Address}}"
<br>
Accepted commands: "{{.Status.SSH.SSHCommands}}"
<br>
{{range .Status.SSH.HostKeys}}
<br>
Host Key: "{{.Path}}"
<br>
Fingerprint: "{{.Fingerprint}}"
<br>
{{end}}
{{end}}
</p>
</div>
</div>
<div class="card mb-4 {{ if .Status.FTP.IsActive}}border-left-success{{else}}border-left-info{{end}}">
<div class="card-body">
<h5 class="card-title">FTP server</h5>
<p class="card-text">
Status: {{ if .Status.FTP.IsActive}}"Started"{{else}}"Stopped"{{end}}
{{if .Status.FTP.IsActive}}
<br>
Address: "{{.Status.FTP.Address}}"
<br>
Passive port range: "{{.Status.FTP.PassivePortRange.Start}}-{{.Status.FTP.PassivePortRange.End}}"
<br>
TLS: "{{.Status.FTP.FTPES}}"
{{end}}
</p>
</div>
</div>
<div class="card mb-4 {{ if .Status.WebDAV.IsActive}}border-left-success{{else}}border-left-info{{end}}">
<div class="card-body">
<h5 class="card-title">WebDAV server</h5>
<p class="card-text">
Status: {{ if .Status.WebDAV.IsActive}}"Started"{{else}}"Stopped"{{end}}
{{if .Status.WebDAV.IsActive}}
<br>
Address: "{{.Status.WebDAV.Address}}"
<br>
Protocol: "{{.Status.WebDAV.Protocol}}"
{{end}}
</p>
</div>
</div>
<div class="card mb-4 {{ if .Status.DataProvider.IsActive}}border-left-success{{else}}border-left-warning{{end}}">
<div class="card-body">
<h5 class="card-title">Data provider</h5>
<p class="card-text">
Status: {{ if .Status.DataProvider.IsActive}}"OK"{{else}}"{{.Status.DataProvider.Error}}"{{end}}
<br>
Driver: "{{.Status.DataProvider.Driver}}"
</p>
</div>
</div>
{{end}}

View file

@ -22,7 +22,6 @@ import (
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/vfs"
)
@ -665,7 +664,9 @@ func TestBasicUsersCache(t *testing.T) {
}
u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny}
user, _, err := httpd.AddUser(u, http.StatusOK)
err := dataprovider.AddUser(u)
assert.NoError(t, err)
user, err := dataprovider.UserExists(u.Username)
assert.NoError(t, err)
c := &Configuration{
@ -728,7 +729,7 @@ func TestBasicUsersCache(t *testing.T) {
assert.False(t, cachedUser.IsExpired())
}
// cache is invalidated after a user modification
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
err = dataprovider.UpdateUser(user)
assert.NoError(t, err)
_, ok = dataprovider.GetCachedWebDAVUser(username)
assert.False(t, ok)
@ -739,7 +740,7 @@ func TestBasicUsersCache(t *testing.T) {
_, ok = dataprovider.GetCachedWebDAVUser(username)
assert.True(t, ok)
// cache is invalidated after user deletion
_, err = httpd.RemoveUser(user, http.StatusOK)
err = dataprovider.DeleteUser(user)
assert.NoError(t, err)
_, ok = dataprovider.GetCachedWebDAVUser(username)
assert.False(t, ok)
@ -757,19 +758,27 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
u.Password = password + "1"
u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny}
user1, _, err := httpd.AddUser(u, http.StatusOK)
err := dataprovider.AddUser(u)
assert.NoError(t, err)
user1, err := dataprovider.UserExists(u.Username)
assert.NoError(t, err)
u.Username = username + "2"
u.Password = password + "2"
user2, _, err := httpd.AddUser(u, http.StatusOK)
err = dataprovider.AddUser(u)
assert.NoError(t, err)
user2, err := dataprovider.UserExists(u.Username)
assert.NoError(t, err)
u.Username = username + "3"
u.Password = password + "3"
user3, _, err := httpd.AddUser(u, http.StatusOK)
err = dataprovider.AddUser(u)
assert.NoError(t, err)
user3, err := dataprovider.UserExists(u.Username)
assert.NoError(t, err)
u.Username = username + "4"
u.Password = password + "4"
user4, _, err := httpd.AddUser(u, http.StatusOK)
err = dataprovider.AddUser(u)
assert.NoError(t, err)
user4, err := dataprovider.UserExists(u.Username)
assert.NoError(t, err)
c := &Configuration{
@ -878,7 +887,7 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
assert.True(t, ok)
// now remove user1 after an update
user1, _, err = httpd.UpdateUser(user1, http.StatusOK, "")
err = dataprovider.UpdateUser(user1)
assert.NoError(t, err)
_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)
assert.False(t, ok)
@ -905,13 +914,13 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
assert.True(t, ok)
_, err = httpd.RemoveUser(user1, http.StatusOK)
err = dataprovider.DeleteUser(user1)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user2, http.StatusOK)
err = dataprovider.DeleteUser(user2)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user3, http.StatusOK)
err = dataprovider.DeleteUser(user3)
assert.NoError(t, err)
_, err = httpd.RemoveUser(user4, http.StatusOK)
err = dataprovider.DeleteUser(user4)
assert.NoError(t, err)
}

View file

@ -33,6 +33,7 @@ var (
type webDavServer struct {
config *Configuration
certMgr *common.CertManager
status ServiceStatus
}
func newServer(config *Configuration, configDir string) (*webDavServer, error) {
@ -53,8 +54,12 @@ func newServer(config *Configuration, configDir string) (*webDavServer, error) {
}
func (s *webDavServer) listenAndServe() error {
addr := fmt.Sprintf("%s:%d", s.config.BindAddress, s.config.BindPort)
s.status.IsActive = true
s.status.Address = addr
s.status.Protocol = "HTTP"
httpServer := &http.Server{
Addr: fmt.Sprintf("%s:%d", s.config.BindAddress, s.config.BindPort),
Addr: addr,
Handler: server,
ReadHeaderTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
@ -75,6 +80,7 @@ func (s *webDavServer) listenAndServe() error {
httpServer.Handler = server
}
if s.certMgr != nil {
s.status.Protocol = "HTTPS"
httpServer.TLSConfig = &tls.Config{
GetCertificate: s.certMgr.GetCertificateFunc(),
MinVersion: tls.VersionTLS12,

View file

@ -23,6 +23,13 @@ var (
server *webDavServer
)
// ServiceStatus defines the service status
type ServiceStatus struct {
IsActive bool `json:"is_active"`
Address string `json:"address"`
Protocol string `json:"protocol"`
}
// Cors configuration
type Cors struct {
AllowedOrigins []string `json:"allowed_origins" mapstructure:"allowed_origins"`
@ -70,7 +77,15 @@ type Configuration struct {
Cache Cache `json:"cache" mapstructure:"cache"`
}
// Initialize configures and starts the WebDav server
// GetStatus returns the server status
func GetStatus() ServiceStatus {
if server == nil {
return ServiceStatus{}
}
return server.status
}
// Initialize configures and starts the WebDAV server
func (c *Configuration) Initialize(configDir string) error {
var err error
logger.Debug(logSender, "", "initializing WebDAV server with config %+v", *c)

View file

@ -157,6 +157,12 @@ func TestMain(m *testing.M) {
AllowCredentials: true,
}
status := webdavd.GetStatus()
if status.IsActive {
logger.ErrorToConsole("webdav server is already active")
os.Exit(1)
}
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
@ -284,6 +290,8 @@ func TestBasicHandling(t *testing.T) {
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Len(t, common.Connections.GetStats(), 0)
status := webdavd.GetStatus()
assert.True(t, status.IsActive)
}
func TestBasicHandlingCryptFs(t *testing.T) {