From 4f4489d3f105ed640718a8d7a74d02c71c5d5098 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 8 Aug 2019 10:01:33 +0200 Subject: [PATCH] add version info --- README.md | 20 ++++++++++++++++---- api/api.go | 1 + api/api_test.go | 18 ++++++++++++++++++ api/api_utils.go | 18 ++++++++++++++++++ api/internal_test.go | 4 ++++ api/router.go | 5 +++++ api/schema/openapi.yaml | 25 +++++++++++++++++++++++++ cmd/root.go | 5 +++-- config/config.go | 6 +----- config/config_linux.go | 11 +++++++++++ config/config_nolinux.go | 7 +++++++ scripts/sftpgo_api_cli.py | 9 +++++++++ sftpd/sftpd.go | 2 +- sftpd/sftpd_test.go | 4 +++- utils/utils.go | 19 +++---------------- utils/version.go | 35 +++++++++++++++++++++++++++++++++++ 16 files changed, 160 insertions(+), 29 deletions(-) create mode 100644 config/config_linux.go create mode 100644 config/config_nolinux.go create mode 100644 utils/version.go diff --git a/README.md b/README.md index 2ee0c61c..d7f7290b 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,17 @@ $ go get -u github.com/drakkan/sftpgo Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`. +Version info can be embedded populating the following variables at build time: + +- `github.com/drakkan/sftpgo/utils.commit` +- `github.com/drakkan/sftpgo/utils.date` + +For example on Linux you can build using the following ldflags: + +```bash +-ldflags "-s -w -X github.com/drakkan/sftpgo/utils.commit=`git describe --tags --always --dirty` -X github.com/drakkan/sftpgo/utils.date=`date --utc +%FT%TZ`" +``` + A systemd sample [service](https://github.com/drakkan/sftpgo/tree/master/init/sftpgo.service "systemd service") can be found inside the source tree. Alternately you can use distro packages: @@ -195,7 +206,7 @@ For each account the following properties can be configured: - `username` - `password` used for password authentication. For users created using SFTPGo REST API the password will be stored using argon2id hashing algo. SFTPGo supports checking passwords stored with bcrypt too. Currently, as fallback, there is a clear text password checking but you should not store passwords as clear text and this support could be removed at any time, so please don't depend on it. -- `public_key` array of public keys. At least one public key or the password is mandatory. +- `public_keys` array of public keys. At least one public key or the password is mandatory. - `home_dir` The user cannot upload or download files outside this directory. Must be an absolute path - `uid`, `gid`. If sftpgo runs as root system user then the created files and directories will be assigned to this system uid/gid. Ignored on windows and if sftpgo runs as non root user: in this case files and directories for all SFTP users will be owned by the system user that runs sftpgo. - `max_sessions` maximum concurrent sessions. 0 means unlimited @@ -223,7 +234,7 @@ If quota tracking is enabled in `sftpgo` configuration file, then the used size REST API is designed to run on localhost or on a trusted network, if you need HTTPS or authentication you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX. -For example you can setup a reverse proxy using apache this way: +For example you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way: ``` ProxyPass /api/v1 http://127.0.0.1:8080/api/v1 @@ -236,7 +247,8 @@ and you can add authentication with something like this: AuthType Digest AuthName "Private" - AuthBasicProvider file + AuthDigestDomain "/api/v1" + AuthDigestProvider file AuthUserFile "/etc/httpd/conf/auth_digest" Require valid-user @@ -248,7 +260,7 @@ The OpenAPI 3 schema for the exposed API can be found inside the source tree: [o A sample CLI client for the REST API can be found inside the source tree [scripts](https://github.com/drakkan/sftpgo/tree/master/scripts "scripts") directory. -You can also generate your own REST client using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/) +You can also generate your own REST client, in your preferred programming language or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/) ## Logs diff --git a/api/api.go b/api/api.go index 9511ac56..4b40c320 100644 --- a/api/api.go +++ b/api/api.go @@ -18,6 +18,7 @@ const ( activeConnectionsPath = "/api/v1/sftp_connection" quotaScanPath = "/api/v1/quota_scan" userPath = "/api/v1/user" + versionPath = "/api/v1/version" ) var ( diff --git a/api/api_test.go b/api/api_test.go index 30ad443c..38d3e105 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -35,6 +35,7 @@ const ( userPath = "/api/v1/user" activeConnectionsPath = "/api/v1/sftp_connection" quotaScanPath = "/api/v1/quota_scan" + versionPath = "/api/v1/version" ) var ( @@ -393,6 +394,17 @@ func TestStartQuotaScan(t *testing.T) { } } +func TestGetVersion(t *testing.T) { + _, _, err := api.GetVersion(http.StatusOK) + if err != nil { + t.Errorf("unable to get sftp version: %v", err) + } + _, _, err = api.GetVersion(http.StatusInternalServerError) + if err == nil { + t.Errorf("get version request must succeed, we requested to check a wrong status code") + } +} + func TestGetSFTPConnections(t *testing.T) { _, _, err := api.GetSFTPConnections(http.StatusOK) if err != nil { @@ -668,6 +680,12 @@ func TestStartQuotaScanNonExistentUserMock(t *testing.T) { checkResponseCode(t, http.StatusBadRequest, rr.Code) } +func TestGetVersionMock(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, versionPath, nil) + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) +} + func TestGetSFTPConnectionsMock(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, activeConnectionsPath, nil) rr := executeRequest(req) diff --git a/api/api_utils.go b/api/api_utils.go index 9e5f961a..46c96169 100644 --- a/api/api_utils.go +++ b/api/api_utils.go @@ -242,6 +242,24 @@ func CloseSFTPConnection(connectionID string, expectedStatusCode int) ([]byte, e return body, err } +// GetVersion returns version details +func GetVersion(expectedStatusCode int) (utils.VersionInfo, []byte, error) { + var version utils.VersionInfo + var body []byte + resp, err := getHTTPClient().Get(buildURLRelativeToBase(versionPath)) + if err != nil { + return version, body, err + } + defer resp.Body.Close() + err = checkResponse(resp.StatusCode, expectedStatusCode) + if err == nil && expectedStatusCode == http.StatusOK { + err = render.DecodeJSON(resp.Body, &version) + } else { + body, _ = getResponseBody(resp) + } + return version, body, err +} + func checkResponse(actual int, expected int) error { if expected != actual { return fmt.Errorf("wrong status code: got %v want %v", actual, expected) diff --git a/api/internal_test.go b/api/internal_test.go index 976db669..e45a38fe 100644 --- a/api/internal_test.go +++ b/api/internal_test.go @@ -205,5 +205,9 @@ func TestApiCallToNotListeningServer(t *testing.T) { if err == nil { t.Errorf("request to an inactive URL must fail") } + _, _, err = GetVersion(http.StatusOK) + if err == nil { + t.Errorf("request to an inactive URL must fail") + } SetBaseURL(oldBaseURL) } diff --git a/api/router.go b/api/router.go index b13d3516..f8e5a250 100644 --- a/api/router.go +++ b/api/router.go @@ -5,6 +5,7 @@ import ( "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" + "github.com/drakkan/sftpgo/utils" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/go-chi/render" @@ -30,6 +31,10 @@ func initializeRouter() { 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.Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, sftpd.GetConnectionsStats()) }) diff --git a/api/schema/openapi.yaml b/api/schema/openapi.yaml index 8d1fde98..5cab86fa 100644 --- a/api/schema/openapi.yaml +++ b/api/schema/openapi.yaml @@ -7,6 +7,21 @@ info: servers: - url: /api/v1 paths: + /version: + get: + tags: + - version + summary: Get version details + operationId: get_version + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref : '#/components/schemas/VersionInfo' /sftp_connection: get: tags: @@ -654,3 +669,13 @@ components: type: string nullable: true description: error description if any + VersionInfo: + type: object + properties: + version: + type: string + build_date: + type: string + commit_hash: + type: string + \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index 9f5cdb1f..92414f05 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,9 +20,10 @@ var ( ) func init() { + version := utils.GetAppVersion() rootCmd.Flags().BoolP("version", "v", false, "") - rootCmd.Version = utils.GetAppVersion() - rootCmd.SetVersionTemplate(`{{printf "SFTPGo version "}}{{printf "%s" .Version}} + rootCmd.Version = version.GetVersionAsString() + rootCmd.SetVersionTemplate(`{{printf "SFTPGo version: "}}{{printf "%s" .Version}} `) } diff --git a/config/config.go b/config/config.go index 138def4d..98113e70 100644 --- a/config/config.go +++ b/config/config.go @@ -7,7 +7,6 @@ package config import ( "fmt" - "runtime" "strings" "github.com/drakkan/sftpgo/api" @@ -79,10 +78,7 @@ func init() { replacer := strings.NewReplacer(".", "__") viper.SetEnvKeyReplacer(replacer) viper.SetConfigName(DefaultConfigName) - if runtime.GOOS == "linux" { - viper.AddConfigPath("$HOME/.config/sftpgo") - viper.AddConfigPath("/etc/sftpgo") - } + setViperAdditionalConfigPaths() viper.AddConfigPath(".") viper.AutomaticEnv() } diff --git a/config/config_linux.go b/config/config_linux.go new file mode 100644 index 00000000..967c2122 --- /dev/null +++ b/config/config_linux.go @@ -0,0 +1,11 @@ +// +build linux + +package config + +import "github.com/spf13/viper" + +// linux specific config search path +func setViperAdditionalConfigPaths() { + viper.AddConfigPath("$HOME/.config/sftpgo") + viper.AddConfigPath("/etc/sftpgo") +} diff --git a/config/config_nolinux.go b/config/config_nolinux.go new file mode 100644 index 00000000..fe5d6aeb --- /dev/null +++ b/config/config_nolinux.go @@ -0,0 +1,7 @@ +// +build !linux + +package config + +func setViperAdditionalConfigPaths() { + +} diff --git a/scripts/sftpgo_api_cli.py b/scripts/sftpgo_api_cli.py index 1fcb32cf..e50fc6c0 100755 --- a/scripts/sftpgo_api_cli.py +++ b/scripts/sftpgo_api_cli.py @@ -16,6 +16,7 @@ class SFTPGoApiRequests: self.userPath = urlparse.urljoin(baseUrl, "/api/v1/user") self.quotaScanPath = urlparse.urljoin(baseUrl, "/api/v1/quota_scan") self.activeConnectionsPath = urlparse.urljoin(baseUrl, "/api/v1/sftp_connection") + self.versionPath = urlparse.urljoin(baseUrl, "/api/v1/version") self.debug = debug if authType == "basic": self.auth = requests.auth.HTTPBasicAuth(authUser, authPassword) @@ -98,6 +99,10 @@ class SFTPGoApiRequests: r = requests.post(self.quotaScanPath, json=u, auth=self.auth, verify=self.verify) self.printResponse(r) + def getVersion(self): + r = requests.get(self.versionPath, auth=self.auth, verify=self.verify) + self.printResponse(r) + def addCommonUserArguments(parser): parser.add_argument('username', type=str) @@ -165,6 +170,8 @@ if __name__ == '__main__': parserStartQuotaScans = subparsers.add_parser("start_quota_scan", help="Start a new quota scan") addCommonUserArguments(parserStartQuotaScans) + parserGetVersion = subparsers.add_parser("get_version", help="Get version details") + args = parser.parse_args() api = SFTPGoApiRequests(args.debug, args.base_url, args.auth_type, args.auth_user, args.auth_password, args.verify) @@ -191,4 +198,6 @@ if __name__ == '__main__': api.getQuotaScans() elif args.command == "start_quota_scan": api.startQuotaScan(args.username) + elif args.command == "get_version": + api.getVersion() diff --git a/sftpd/sftpd.go b/sftpd/sftpd.go index 69989d0a..a4ddb7aa 100644 --- a/sftpd/sftpd.go +++ b/sftpd/sftpd.go @@ -193,7 +193,7 @@ func GetConnectionsStats() []ConnectionStatus { } for _, t := range activeTransfers { if t.connectionID == c.ID { - if utils.GetTimeAsMsSinceEpoch(t.lastActivity) > conn.LastActivity { + if t.lastActivity.UnixNano() > c.lastActivity.UnixNano() { conn.LastActivity = utils.GetTimeAsMsSinceEpoch(t.lastActivity) } var operationType string diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 969060dc..27675ad6 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -96,6 +96,7 @@ func TestMain(m *testing.M) { sftpdConf := config.GetSFTPDConfig() httpdConf := config.GetHTTPDConfig() router := api.GetHTTPRouter() + sftpdConf.BindPort = 2022 // we run the test cases with UploadMode atomic. The non atomic code path // simply does not execute some code so if it works in atomic mode will // work in non atomic mode too @@ -105,7 +106,7 @@ func TestMain(m *testing.M) { } else { homeBasePath = "/tmp" sftpdConf.Actions.ExecuteOn = []string{"download", "upload", "rename", "delete"} - sftpdConf.Actions.Command = "/bin/true" + sftpdConf.Actions.Command = "/usr/bin/true" sftpdConf.Actions.HTTPNotificationURL = "http://127.0.0.1:8080/" } @@ -145,6 +146,7 @@ func TestInitialization(t *testing.T) { config.LoadConfig(configDir, "") sftpdConf := config.GetSFTPDConfig() sftpdConf.Umask = "invalid umask" + sftpdConf.BindPort = 2022 err := sftpdConf.Initialize(configDir) if err == nil { t.Errorf("Inizialize must fail, a SFTP server should be already running") diff --git a/utils/utils.go b/utils/utils.go index 3983cc98..a2ea4f9e 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -12,12 +12,6 @@ import ( const logSender = "utils" -var ( - version = "dev" - commit = "" - date = "" -) - // IsStringInSlice searches a string in a slice and returns true if the string is found func IsStringInSlice(obj string, list []string) bool { for _, v := range list { @@ -76,14 +70,7 @@ func SetPathPermissions(path string, uid int, gid int) { } } -// GetAppVersion returns the app version -func GetAppVersion() string { - v := version - if len(commit) > 0 { - v += "-" + commit - } - if len(date) > 0 { - v += "-" + date - } - return v +// GetAppVersion returns VersionInfo struct +func GetAppVersion() VersionInfo { + return versionInfo } diff --git a/utils/version.go b/utils/version.go new file mode 100644 index 00000000..e05a7df6 --- /dev/null +++ b/utils/version.go @@ -0,0 +1,35 @@ +package utils + +var ( + version = "0.9.0-dev" + commit = "" + date = "" + versionInfo VersionInfo +) + +// VersionInfo defines version details +type VersionInfo struct { + Version string `json:"version"` + BuildDate string `json:"build_date"` + CommitHash string `json:"commit_hash"` +} + +// GetVersionAsString returns the string representation of the VersionInfo struct +func (v *VersionInfo) GetVersionAsString() string { + versionString := v.Version + if len(v.CommitHash) > 0 { + versionString += "-" + v.CommitHash + } + if len(v.BuildDate) > 0 { + versionString += "-" + v.BuildDate + } + return versionString +} + +func init() { + versionInfo = VersionInfo{ + Version: version, + CommitHash: commit, + BuildDate: date, + } +}