add SCP support

SCP is an experimental feature, we have our own SCP implementation
since we can't rely on scp system command to proper handle permissions,
quota and user's home dir restrictions. The SCP protocol is quite simple
but there is no official docs about it, so we need more testing and
feedbacks before enabling it by default.
We may not handle some borderline cases or have sneaky bugs.

This commit contains some breaking changes to the REST API.
SFTPGo API should be stable now and I hope no more breaking changes
before the first stable release.
This commit is contained in:
Nicola Murino
2019-08-24 14:41:15 +02:00
parent 2c05791624
commit e50c521c33
19 changed files with 2077 additions and 128 deletions

View File

@@ -15,7 +15,7 @@ import (
const (
logSender = "api"
activeConnectionsPath = "/api/v1/sftp_connection"
activeConnectionsPath = "/api/v1/connection"
quotaScanPath = "/api/v1/quota_scan"
userPath = "/api/v1/user"
versionPath = "/api/v1/version"

View File

@@ -33,7 +33,7 @@ const (
testPubKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"
logSender = "APITesting"
userPath = "/api/v1/user"
activeConnectionsPath = "/api/v1/sftp_connection"
activeConnectionsPath = "/api/v1/connection"
quotaScanPath = "/api/v1/quota_scan"
versionPath = "/api/v1/version"
)
@@ -405,19 +405,19 @@ func TestGetVersion(t *testing.T) {
}
}
func TestGetSFTPConnections(t *testing.T) {
_, _, err := api.GetSFTPConnections(http.StatusOK)
func TestGetConnections(t *testing.T) {
_, _, err := api.GetConnections(http.StatusOK)
if err != nil {
t.Errorf("unable to get sftp connections: %v", err)
}
_, _, err = api.GetSFTPConnections(http.StatusInternalServerError)
_, _, err = api.GetConnections(http.StatusInternalServerError)
if err == nil {
t.Errorf("get sftp connections request must succeed, we requested to check a wrong status code")
}
}
func TestCloseActiveSFTPConnection(t *testing.T) {
_, err := api.CloseSFTPConnection("non_existent_id", http.StatusNotFound)
func TestCloseActiveConnection(t *testing.T) {
_, err := api.CloseConnection("non_existent_id", http.StatusNotFound)
if err != nil {
t.Errorf("unexpected error closing non existent sftp connection: %v", err)
}
@@ -686,7 +686,7 @@ func TestGetVersionMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestGetSFTPConnectionsMock(t *testing.T) {
func TestGetConnectionsMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, activeConnectionsPath, nil)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)

View File

@@ -207,8 +207,8 @@ func StartQuotaScan(user dataprovider.User, expectedStatusCode int) ([]byte, err
return body, checkResponse(resp.StatusCode, expectedStatusCode)
}
// GetSFTPConnections returns status and stats for active SFTP connections
func GetSFTPConnections(expectedStatusCode int) ([]sftpd.ConnectionStatus, []byte, error) {
// GetConnections returns status and stats for active SFTP/SCP connections
func GetConnections(expectedStatusCode int) ([]sftpd.ConnectionStatus, []byte, error) {
var connections []sftpd.ConnectionStatus
var body []byte
resp, err := getHTTPClient().Get(buildURLRelativeToBase(activeConnectionsPath))
@@ -225,8 +225,8 @@ func GetSFTPConnections(expectedStatusCode int) ([]sftpd.ConnectionStatus, []byt
return connections, body, err
}
// CloseSFTPConnection closes an active SFTP connection identified by connectionID
func CloseSFTPConnection(connectionID string, expectedStatusCode int) ([]byte, error) {
// CloseConnection closes an active connection identified by connectionID
func CloseConnection(connectionID string, expectedStatusCode int) ([]byte, error) {
var body []byte
req, err := http.NewRequest(http.MethodDelete, buildURLRelativeToBase(activeConnectionsPath, connectionID), nil)
if err != nil {

View File

@@ -161,7 +161,7 @@ func TestApiCallsWithBadURL(t *testing.T) {
if err == nil {
t.Errorf("request with invalid URL must fail")
}
_, err = CloseSFTPConnection("non_existent_id", http.StatusNotFound)
_, err = CloseConnection("non_existent_id", http.StatusNotFound)
if err == nil {
t.Errorf("request with invalid URL must fail")
}
@@ -200,11 +200,11 @@ func TestApiCallToNotListeningServer(t *testing.T) {
if err == nil {
t.Errorf("request to an inactive URL must fail")
}
_, _, err = GetSFTPConnections(http.StatusOK)
_, _, err = GetConnections(http.StatusOK)
if err == nil {
t.Errorf("request to an inactive URL must fail")
}
_, err = CloseSFTPConnection("non_existent_id", http.StatusNotFound)
_, err = CloseConnection("non_existent_id", http.StatusNotFound)
if err == nil {
t.Errorf("request to an inactive URL must fail")
}
@@ -215,13 +215,13 @@ func TestApiCallToNotListeningServer(t *testing.T) {
SetBaseURL(oldBaseURL)
}
func TestCloseSFTPConnectionHandler(t *testing.T) {
func TestCloseConnectionHandler(t *testing.T) {
req, _ := http.NewRequest(http.MethodDelete, activeConnectionsPath+"/connectionID", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("connectionID", "")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
rr := httptest.NewRecorder()
handleCloseSFTPConnection(rr, req)
handleCloseConnection(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("Expected response code 400. Got %d", rr.Code)
}

View File

@@ -40,7 +40,7 @@ func initializeRouter() {
})
router.Delete(activeConnectionsPath+"/{connectionID}", func(w http.ResponseWriter, r *http.Request) {
handleCloseSFTPConnection(w, r)
handleCloseConnection(w, r)
})
router.Get(quotaScanPath, func(w http.ResponseWriter, r *http.Request) {
@@ -72,7 +72,7 @@ func initializeRouter() {
})
}
func handleCloseSFTPConnection(w http.ResponseWriter, r *http.Request) {
func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
connectionID := chi.URLParam(r, "connectionID")
if connectionID == "" {
sendAPIResponse(w, r, nil, "connectionID is mandatory", http.StatusBadRequest)

View File

@@ -22,12 +22,12 @@ paths:
type: array
items:
$ref : '#/components/schemas/VersionInfo'
/sftp_connection:
/connection:
get:
tags:
- connections
summary: Get the active sftp users and info about their uploads/downloads
operationId: get_sftp_connections
summary: Get the active users and info about their uploads/downloads
operationId: get_connections
responses:
200:
description: successful operation
@@ -37,12 +37,12 @@ paths:
type: array
items:
$ref : '#/components/schemas/ConnectionStatus'
/sftp_connection/{connectionID}:
/connection/{connectionID}:
delete:
tags:
- connections
summary: Terminate an active SFTP connection
operationId: close_sftp_connection
summary: Terminate an active connection
operationId: close_connection
parameters:
- name: connectionID
in: path
@@ -183,7 +183,7 @@ paths:
get:
tags:
- users
summary: Returns an array with one or more SFTP users
summary: Returns an array with one or more users
description: For security reasons password and public key are empty in the response
operationId: get_users
parameters:
@@ -261,7 +261,7 @@ paths:
post:
tags:
- users
summary: Adds a new SFTP user
summary: Adds a new SFTP/SCP user
operationId: add_user
requestBody:
required: true
@@ -562,15 +562,15 @@ components:
max_sessions:
type: integer
format: int32
description: limit the sessions that an sftp user can open. 0 means unlimited
description: limit the sessions that an user can open. 0 means unlimited
quota_size:
type: integer
format: int64
description: quota as size. 0 menas unlimited. Please note that quota is updated if files are added/removed via sftp otherwise a quota scan is needed
description: quota as size. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed
quota_files:
type: integer
format: int32
description: quota as number of files. 0 menas unlimited. Please note that quota is updated if files are added/removed via sftp otherwise a quota scan is needed
description: quota as number of files. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed
permissions:
type: array
items:
@@ -594,7 +594,7 @@ components:
type: integer
format: int32
description: Maximum download bandwidth as KB/s, 0 means unlimited
SFTPTransfer:
Transfer:
type: object
properties:
operation_type:
@@ -604,7 +604,7 @@ components:
- download
path:
type: string
description: SFTP file path for the upload/download
description: SFTP/SCP file path for the upload/download
start_time:
type: integer
format: int64
@@ -625,13 +625,13 @@ components:
description: connected username
connection_id:
type: string
description: unique sftp connection identifier
description: unique connection identifier
client_version:
type: string
description: SFTP client version
description: SFTP/SCP client version
remote_address:
type: string
description: Remote address for the connected SFTP client
description: Remote address for the connected SFTP/SCP client
connection_time:
type: integer
format: int64
@@ -640,10 +640,15 @@ components:
type: integer
format: int64
description: last client activity as unix timestamp in milliseconds
protocol:
type: string
enum:
- SFTP
- SCP
active_transfers:
type: array
items:
$ref : '#/components/schemas/SFTPTransfer'
$ref : '#/components/schemas/Transfer'
QuotaScan:
type: object
properties: